@streamplace/components 0.8.11 → 0.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +5 -4
- package/dist/components/chat/chat-message.js.map +1 -1
- package/dist/components/chat/mod-view.d.ts.map +1 -1
- package/dist/components/chat/mod-view.js +4 -3
- package/dist/components/chat/mod-view.js.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js +3 -3
- package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
- package/dist/components/share/sharesheet.d.ts.map +1 -1
- package/dist/components/share/sharesheet.js +5 -14
- package/dist/components/share/sharesheet.js.map +1 -1
- package/dist/components/ui/text.d.ts +2 -2
- package/dist/components/ui/view.d.ts +1 -1
- package/dist/i18n/i18n-loader.d.ts +2 -0
- package/dist/i18n/i18n-loader.d.ts.map +1 -0
- package/dist/i18n/i18n-loader.js +17 -0
- package/dist/i18n/i18n-loader.js.map +1 -0
- package/dist/i18n/i18n-loader.native.d.ts +2 -0
- package/dist/i18n/i18n-loader.native.d.ts.map +1 -0
- package/dist/i18n/i18n-loader.native.js +51 -0
- package/dist/i18n/i18n-loader.native.js.map +1 -0
- package/dist/i18n/i18next-config.d.ts.map +1 -1
- package/dist/i18n/i18next-config.js +5 -49
- package/dist/i18n/i18next-config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/streamplace-provider/poller.d.ts.map +1 -1
- package/dist/streamplace-provider/poller.js +2 -0
- package/dist/streamplace-provider/poller.js.map +1 -1
- package/dist/time-sync/index.d.ts +3 -0
- package/dist/time-sync/index.d.ts.map +1 -0
- package/dist/time-sync/index.js +15 -0
- package/dist/time-sync/index.js.map +1 -0
- package/dist/time-sync/time-sync.d.ts +13 -0
- package/dist/time-sync/time-sync.d.ts.map +1 -0
- package/dist/time-sync/time-sync.js +95 -0
- package/dist/time-sync/time-sync.js.map +1 -0
- package/dist/time-sync/useTimeSync.d.ts +2 -0
- package/dist/time-sync/useTimeSync.d.ts.map +1 -0
- package/dist/time-sync/useTimeSync.js +49 -0
- package/dist/time-sync/useTimeSync.js.map +1 -0
- package/dist/utils/format-handle.d.ts +11 -0
- package/dist/utils/format-handle.d.ts.map +1 -0
- package/dist/utils/format-handle.js +21 -0
- package/dist/utils/format-handle.js.map +1 -0
- package/locales/en-US/common.ftl +5 -0
- package/locales/en-US/settings.ftl +7 -0
- package/locales/es-ES/common.ftl +5 -0
- package/locales/es-ES/settings.ftl +7 -0
- package/locales/fr-FR/common.ftl +5 -0
- package/locales/fr-FR/settings.ftl +7 -0
- package/locales/pt-BR/common.ftl +5 -0
- package/locales/pt-BR/settings.ftl +7 -0
- package/locales/zh-Hant/common.ftl +5 -0
- package/locales/zh-Hant/settings.ftl +7 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +3 -3
- package/src/components/chat/chat-message.tsx +3 -2
- package/src/components/chat/mod-view.tsx +4 -3
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +5 -3
- package/src/components/share/sharesheet.tsx +11 -27
- package/src/i18n/i18n-loader.native.ts +56 -0
- package/src/i18n/i18n-loader.ts +19 -0
- package/src/i18n/i18next-config.ts +6 -57
- package/src/index.tsx +2 -0
- package/src/streamplace-provider/poller.tsx +3 -0
- package/src/time-sync/index.ts +12 -0
- package/src/time-sync/time-sync.ts +112 -0
- package/src/time-sync/useTimeSync.tsx +58 -0
- package/src/utils/format-handle.ts +24 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Native translation loader - imports translations directly for bundling
|
|
2
|
+
// Metro will use this file for React Native builds
|
|
3
|
+
|
|
4
|
+
// Import all translations directly so they're bundled into the app
|
|
5
|
+
import enUSCommon from "../../public/locales/en-US/common.json";
|
|
6
|
+
import enUSSettings from "../../public/locales/en-US/settings.json";
|
|
7
|
+
import esESCommon from "../../public/locales/es-ES/common.json";
|
|
8
|
+
import esESSettings from "../../public/locales/es-ES/settings.json";
|
|
9
|
+
import frFRCommon from "../../public/locales/fr-FR/common.json";
|
|
10
|
+
import frFRSettings from "../../public/locales/fr-FR/settings.json";
|
|
11
|
+
import ptBRCommon from "../../public/locales/pt-BR/common.json";
|
|
12
|
+
import ptBRSettings from "../../public/locales/pt-BR/settings.json";
|
|
13
|
+
import zhHantCommon from "../../public/locales/zh-Hant/common.json";
|
|
14
|
+
import zhHantSettings from "../../public/locales/zh-Hant/settings.json";
|
|
15
|
+
|
|
16
|
+
const translationMap: Record<string, any> = {
|
|
17
|
+
"en-US/common": enUSCommon,
|
|
18
|
+
"en-US/settings": enUSSettings,
|
|
19
|
+
"pt-BR/common": ptBRCommon,
|
|
20
|
+
"pt-BR/settings": ptBRSettings,
|
|
21
|
+
"es-ES/common": esESCommon,
|
|
22
|
+
"es-ES/settings": esESSettings,
|
|
23
|
+
"zh-Hant/common": zhHantCommon,
|
|
24
|
+
"zh-Hant/settings": zhHantSettings,
|
|
25
|
+
"fr-FR/common": frFRCommon,
|
|
26
|
+
"fr-FR/settings": frFRSettings,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function loadTranslationData(
|
|
30
|
+
locale: string,
|
|
31
|
+
namespace: string,
|
|
32
|
+
): Promise<any> {
|
|
33
|
+
// Map base language codes to full locales
|
|
34
|
+
const fullLocale = locale.includes("-")
|
|
35
|
+
? locale
|
|
36
|
+
: {
|
|
37
|
+
en: "en-US",
|
|
38
|
+
pt: "pt-BR",
|
|
39
|
+
es: "es-ES",
|
|
40
|
+
zh: "zh-Hant",
|
|
41
|
+
fr: "fr-FR",
|
|
42
|
+
}[locale] || locale;
|
|
43
|
+
|
|
44
|
+
const localeNamespaceKey = `${fullLocale}/${namespace}`;
|
|
45
|
+
const translations = translationMap[localeNamespaceKey];
|
|
46
|
+
|
|
47
|
+
if (!translations) {
|
|
48
|
+
throw new Error(`No translation mapping for ${localeNamespaceKey}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!translations || Object.keys(translations).length === 0) {
|
|
52
|
+
throw new Error("No translations found");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return translations;
|
|
56
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Web translation loader - loads translations via fetch for code splitting
|
|
2
|
+
// Metro will use this file for web builds
|
|
3
|
+
|
|
4
|
+
export async function loadTranslationData(
|
|
5
|
+
locale: string,
|
|
6
|
+
namespace: string,
|
|
7
|
+
): Promise<any> {
|
|
8
|
+
const response = await fetch(`/locales/${locale}/${namespace}.json`);
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
throw new Error(`HTTP ${response.status}`);
|
|
11
|
+
}
|
|
12
|
+
const translations = await response.json();
|
|
13
|
+
|
|
14
|
+
if (!translations || Object.keys(translations).length === 0) {
|
|
15
|
+
throw new Error("No translations found");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return translations;
|
|
19
|
+
}
|
|
@@ -145,68 +145,17 @@ export const I18NEXT_CONFIG = {
|
|
|
145
145
|
debug: process.env.NODE_ENV === "development",
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
-
//
|
|
148
|
+
// Import platform-specific translation loader
|
|
149
|
+
// Metro will use i18n-loader.native.ts for React Native, i18n-loader.ts for web
|
|
150
|
+
import { loadTranslationData as platformLoadTranslationData } from "./i18n-loader";
|
|
151
|
+
|
|
152
|
+
// Translation loading function with error handling
|
|
149
153
|
async function loadTranslationData(
|
|
150
154
|
locale: string,
|
|
151
155
|
namespace: string,
|
|
152
156
|
): Promise<any> {
|
|
153
157
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
// For web environments, load from public directory
|
|
158
|
-
if (typeof window !== "undefined") {
|
|
159
|
-
const response = await fetch(`/locales/${locale}/${namespace}.json`);
|
|
160
|
-
if (!response.ok) {
|
|
161
|
-
throw new Error(`HTTP ${response.status}`);
|
|
162
|
-
}
|
|
163
|
-
translations = await response.json();
|
|
164
|
-
} else {
|
|
165
|
-
// For React Native, use static requires for bundler compatibility
|
|
166
|
-
// Map base language codes to full locales
|
|
167
|
-
const fullLocale = locale.includes("-")
|
|
168
|
-
? locale
|
|
169
|
-
: {
|
|
170
|
-
en: "en-US",
|
|
171
|
-
pt: "pt-BR",
|
|
172
|
-
es: "es-ES",
|
|
173
|
-
zh: "zh-Hant",
|
|
174
|
-
fr: "fr-FR",
|
|
175
|
-
}[locale] || locale;
|
|
176
|
-
|
|
177
|
-
// Static requires for React Native bundler
|
|
178
|
-
const localeNamespaceKey = `${fullLocale}/${namespace}`;
|
|
179
|
-
const translationMap: Record<string, any> = {
|
|
180
|
-
"en-US/common": require("../../public/locales/en-US/common.json"),
|
|
181
|
-
"pt-BR/common": require("../../public/locales/pt-BR/common.json"),
|
|
182
|
-
"es-ES/common": require("../../public/locales/es-ES/common.json"),
|
|
183
|
-
"zh-Hant/common": require("../../public/locales/zh-Hant/common.json"),
|
|
184
|
-
"fr-FR/common": require("../../public/locales/fr-FR/common.json"),
|
|
185
|
-
"en-US/settings": require("../../public/locales/en-US/settings.json"),
|
|
186
|
-
"pt-BR/settings": require("../../public/locales/pt-BR/settings.json"),
|
|
187
|
-
"es-ES/settings": require("../../public/locales/es-ES/settings.json"),
|
|
188
|
-
"zh-Hant/settings": require("../../public/locales/zh-Hant/settings.json"),
|
|
189
|
-
"fr-FR/settings": require("../../public/locales/fr-FR/settings.json"),
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
translations = translationMap[localeNamespaceKey];
|
|
193
|
-
|
|
194
|
-
if (!translations) {
|
|
195
|
-
throw new Error(
|
|
196
|
-
`No static translation mapping for ${localeNamespaceKey}`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} catch (loadError: any) {
|
|
201
|
-
throw new Error(
|
|
202
|
-
`Failed to load ${namespace} translations for ${locale}: ${loadError.message}`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (!translations || Object.keys(translations).length === 0) {
|
|
207
|
-
throw new Error("No translations found in file");
|
|
208
|
-
}
|
|
209
|
-
|
|
158
|
+
const translations = await platformLoadTranslationData(locale, namespace);
|
|
210
159
|
return translations;
|
|
211
160
|
} catch (error: any) {
|
|
212
161
|
console.error(
|
package/src/index.tsx
CHANGED
|
@@ -37,6 +37,8 @@ export * from "./components/chat/system-message";
|
|
|
37
37
|
export { default as VideoRetry } from "./components/mobile-player/video-retry";
|
|
38
38
|
export * from "./lib/system-messages";
|
|
39
39
|
|
|
40
|
+
export * from "./utils/format-handle";
|
|
41
|
+
|
|
40
42
|
export { DanmuOverlay } from "./components/danmu/danmu-overlay";
|
|
41
43
|
export { DanmuOverlayOBS } from "./components/danmu/danmu-overlay-obs";
|
|
42
44
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useStreamplaceStore,
|
|
8
8
|
} from "../streamplace-store";
|
|
9
9
|
import { usePDSAgent } from "../streamplace-store/xrpc";
|
|
10
|
+
import { useTimeSync } from "../time-sync";
|
|
10
11
|
|
|
11
12
|
export default function Poller({ children }: { children: React.ReactNode }) {
|
|
12
13
|
const url = useStreamplaceStore((state) => state.url);
|
|
@@ -19,6 +20,8 @@ export default function Poller({ children }: { children: React.ReactNode }) {
|
|
|
19
20
|
(state) => state.liveUsersRefresh,
|
|
20
21
|
);
|
|
21
22
|
|
|
23
|
+
useTimeSync();
|
|
24
|
+
|
|
22
25
|
useEffect(() => {
|
|
23
26
|
if (pdsAgent && did) {
|
|
24
27
|
getChatProfile();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
let timeOffset = 0;
|
|
4
|
+
let hasWarned = false;
|
|
5
|
+
let OriginalDate: DateConstructor = Date;
|
|
6
|
+
|
|
7
|
+
const CLOCK_DRIFT_THRESHOLD_MS = 5000; // 5 seconds
|
|
8
|
+
|
|
9
|
+
export function getTimeOffset(): number {
|
|
10
|
+
return timeOffset;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function setTimeOffset(offset: number): void {
|
|
14
|
+
timeOffset = offset;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function checkClockDrift(serverTime: string): {
|
|
18
|
+
hasDrift: boolean;
|
|
19
|
+
driftMs: number;
|
|
20
|
+
driftSeconds: number;
|
|
21
|
+
} {
|
|
22
|
+
const serverDate = new Date(serverTime);
|
|
23
|
+
const clientDate = new Date();
|
|
24
|
+
const drift = Math.abs(serverDate.getTime() - clientDate.getTime());
|
|
25
|
+
|
|
26
|
+
if (drift > CLOCK_DRIFT_THRESHOLD_MS) {
|
|
27
|
+
const driftSeconds = Math.round(drift / 1000);
|
|
28
|
+
if (!hasWarned) {
|
|
29
|
+
hasWarned = true;
|
|
30
|
+
console.warn(
|
|
31
|
+
`clock drift detected: ${driftSeconds}s difference from server time. ` +
|
|
32
|
+
`this may cause issues with time-sensitive operations. ` +
|
|
33
|
+
`please sync your system clock.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return { hasDrift: true, driftMs: drift, driftSeconds };
|
|
37
|
+
} else {
|
|
38
|
+
return {
|
|
39
|
+
hasDrift: false,
|
|
40
|
+
driftMs: drift,
|
|
41
|
+
driftSeconds: Math.round(drift / 1000),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function syncTimeWithServer(
|
|
47
|
+
serverTime: string,
|
|
48
|
+
networkLatencyMs: number,
|
|
49
|
+
): void {
|
|
50
|
+
const serverDate = new OriginalDate(serverTime);
|
|
51
|
+
const clientDate = new OriginalDate();
|
|
52
|
+
const offset = serverDate.getTime() - clientDate.getTime() - networkLatencyMs;
|
|
53
|
+
|
|
54
|
+
setTimeOffset(offset);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getSyncedDate(): Date {
|
|
58
|
+
const now = new Date();
|
|
59
|
+
if (timeOffset !== 0) {
|
|
60
|
+
return new Date(now.getTime() + timeOffset);
|
|
61
|
+
}
|
|
62
|
+
return now;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getSystemDate(): Date {
|
|
66
|
+
return new OriginalDate();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getSystemTime(): number {
|
|
70
|
+
return OriginalDate.now();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function initializeTimeSync(): void {
|
|
74
|
+
if (Platform.OS !== "web") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// store original Date
|
|
79
|
+
OriginalDate = Date;
|
|
80
|
+
const OriginalDatePrototype = OriginalDate.prototype;
|
|
81
|
+
|
|
82
|
+
// create patched Date constructor
|
|
83
|
+
function PatchedDate(this: any, ...args: any[]): any {
|
|
84
|
+
// If called as a function (no `new`), forward to original Date to get the string form
|
|
85
|
+
if (!(this instanceof PatchedDate)) {
|
|
86
|
+
return OriginalDate.apply(undefined, args as any);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If called as a constructor, construct a Date with synced time when no args provided
|
|
90
|
+
if (args.length === 0) {
|
|
91
|
+
const syncedTime = OriginalDate.now() + timeOffset;
|
|
92
|
+
return Reflect.construct(OriginalDate, [syncedTime], PatchedDate);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Otherwise construct with the provided arguments
|
|
96
|
+
return Reflect.construct(OriginalDate, args, PatchedDate);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// copy static methods
|
|
100
|
+
PatchedDate.now = function (): number {
|
|
101
|
+
return OriginalDate.now() + timeOffset;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
PatchedDate.parse = OriginalDate.parse;
|
|
105
|
+
PatchedDate.UTC = OriginalDate.UTC;
|
|
106
|
+
|
|
107
|
+
// copy prototype
|
|
108
|
+
PatchedDate.prototype = OriginalDatePrototype;
|
|
109
|
+
|
|
110
|
+
// replace global Date
|
|
111
|
+
(globalThis as any).Date = PatchedDate;
|
|
112
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { TriangleAlert } from "lucide-react-native";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { StreamplaceAgent } from "streamplace";
|
|
5
|
+
import { useToast } from "../components/ui/toast";
|
|
6
|
+
import { useUrl } from "../streamplace-store/streamplace-store";
|
|
7
|
+
import { checkClockDrift, syncTimeWithServer } from "./time-sync";
|
|
8
|
+
|
|
9
|
+
export function useTimeSync() {
|
|
10
|
+
const url = useUrl();
|
|
11
|
+
const t = useToast();
|
|
12
|
+
const hasShownWarning = useRef(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const checkTime = async () => {
|
|
16
|
+
if (Platform.OS !== "web") {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const agent = new StreamplaceAgent(url);
|
|
21
|
+
const start = new Date().getTime();
|
|
22
|
+
const response = await agent.place.stream.server.getServerTime();
|
|
23
|
+
const roundTripLatency = new Date().getTime() - start;
|
|
24
|
+
const serverTime = response.data.serverTime;
|
|
25
|
+
|
|
26
|
+
// always sync with server time
|
|
27
|
+
syncTimeWithServer(serverTime, roundTripLatency / 2);
|
|
28
|
+
|
|
29
|
+
const driftInfo = checkClockDrift(serverTime);
|
|
30
|
+
|
|
31
|
+
// only show warning if drift is significant
|
|
32
|
+
if (driftInfo.hasDrift && !hasShownWarning.current) {
|
|
33
|
+
hasShownWarning.current = true;
|
|
34
|
+
t.show(
|
|
35
|
+
"Clock drift detected!",
|
|
36
|
+
`Your device clock is ${driftInfo.driftSeconds}s off from server time. Please sync your system clock to avoid issues.`,
|
|
37
|
+
{
|
|
38
|
+
variant: "info",
|
|
39
|
+
iconLeft: TriangleAlert,
|
|
40
|
+
duration: 25,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
console.log(
|
|
44
|
+
`time sync applied: offset ${driftInfo.driftMs}ms. Date() calls will now use server time.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("failed to sync time with server:", error);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
checkTime();
|
|
53
|
+
|
|
54
|
+
const interval = setInterval(checkTime, 1800000); // every 30m
|
|
55
|
+
|
|
56
|
+
return () => clearInterval(interval);
|
|
57
|
+
}, [url, t]);
|
|
58
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AppBskyActorDefs } from "@atproto/api";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* formats a user's handle for display, falling back to DID if handle is invalid
|
|
5
|
+
*/
|
|
6
|
+
export function formatHandle(
|
|
7
|
+
profile: Pick<AppBskyActorDefs.ProfileViewBasic, "handle" | "did">,
|
|
8
|
+
prefix: string = "",
|
|
9
|
+
): string {
|
|
10
|
+
if (profile.handle === "handle.invalid") {
|
|
11
|
+
return profile.did;
|
|
12
|
+
}
|
|
13
|
+
return prefix + profile.handle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* convenience function for formatting a user's handle with @ prefix for display,
|
|
18
|
+
* falling back to DID if handle is invalid
|
|
19
|
+
*/
|
|
20
|
+
export function formatHandleWithAt(
|
|
21
|
+
profile: Pick<AppBskyActorDefs.ProfileViewBasic, "handle" | "did">,
|
|
22
|
+
): string {
|
|
23
|
+
return formatHandle(profile, "@");
|
|
24
|
+
}
|