@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.
Files changed (73) hide show
  1. package/dist/components/chat/chat-message.d.ts.map +1 -1
  2. package/dist/components/chat/chat-message.js +5 -4
  3. package/dist/components/chat/chat-message.js.map +1 -1
  4. package/dist/components/chat/mod-view.d.ts.map +1 -1
  5. package/dist/components/chat/mod-view.js +4 -3
  6. package/dist/components/chat/mod-view.js.map +1 -1
  7. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
  8. package/dist/components/mobile-player/ui/viewer-context-menu.js +3 -3
  9. package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
  10. package/dist/components/share/sharesheet.d.ts.map +1 -1
  11. package/dist/components/share/sharesheet.js +5 -14
  12. package/dist/components/share/sharesheet.js.map +1 -1
  13. package/dist/components/ui/text.d.ts +2 -2
  14. package/dist/components/ui/view.d.ts +1 -1
  15. package/dist/i18n/i18n-loader.d.ts +2 -0
  16. package/dist/i18n/i18n-loader.d.ts.map +1 -0
  17. package/dist/i18n/i18n-loader.js +17 -0
  18. package/dist/i18n/i18n-loader.js.map +1 -0
  19. package/dist/i18n/i18n-loader.native.d.ts +2 -0
  20. package/dist/i18n/i18n-loader.native.d.ts.map +1 -0
  21. package/dist/i18n/i18n-loader.native.js +51 -0
  22. package/dist/i18n/i18n-loader.native.js.map +1 -0
  23. package/dist/i18n/i18next-config.d.ts.map +1 -1
  24. package/dist/i18n/i18next-config.js +5 -49
  25. package/dist/i18n/i18next-config.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/streamplace-provider/poller.d.ts.map +1 -1
  31. package/dist/streamplace-provider/poller.js +2 -0
  32. package/dist/streamplace-provider/poller.js.map +1 -1
  33. package/dist/time-sync/index.d.ts +3 -0
  34. package/dist/time-sync/index.d.ts.map +1 -0
  35. package/dist/time-sync/index.js +15 -0
  36. package/dist/time-sync/index.js.map +1 -0
  37. package/dist/time-sync/time-sync.d.ts +13 -0
  38. package/dist/time-sync/time-sync.d.ts.map +1 -0
  39. package/dist/time-sync/time-sync.js +95 -0
  40. package/dist/time-sync/time-sync.js.map +1 -0
  41. package/dist/time-sync/useTimeSync.d.ts +2 -0
  42. package/dist/time-sync/useTimeSync.d.ts.map +1 -0
  43. package/dist/time-sync/useTimeSync.js +49 -0
  44. package/dist/time-sync/useTimeSync.js.map +1 -0
  45. package/dist/utils/format-handle.d.ts +11 -0
  46. package/dist/utils/format-handle.d.ts.map +1 -0
  47. package/dist/utils/format-handle.js +21 -0
  48. package/dist/utils/format-handle.js.map +1 -0
  49. package/locales/en-US/common.ftl +5 -0
  50. package/locales/en-US/settings.ftl +7 -0
  51. package/locales/es-ES/common.ftl +5 -0
  52. package/locales/es-ES/settings.ftl +7 -0
  53. package/locales/fr-FR/common.ftl +5 -0
  54. package/locales/fr-FR/settings.ftl +7 -0
  55. package/locales/pt-BR/common.ftl +5 -0
  56. package/locales/pt-BR/settings.ftl +7 -0
  57. package/locales/zh-Hant/common.ftl +5 -0
  58. package/locales/zh-Hant/settings.ftl +7 -0
  59. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  60. package/package.json +3 -3
  61. package/src/components/chat/chat-message.tsx +3 -2
  62. package/src/components/chat/mod-view.tsx +4 -3
  63. package/src/components/mobile-player/ui/viewer-context-menu.tsx +5 -3
  64. package/src/components/share/sharesheet.tsx +11 -27
  65. package/src/i18n/i18n-loader.native.ts +56 -0
  66. package/src/i18n/i18n-loader.ts +19 -0
  67. package/src/i18n/i18next-config.ts +6 -57
  68. package/src/index.tsx +2 -0
  69. package/src/streamplace-provider/poller.tsx +3 -0
  70. package/src/time-sync/index.ts +12 -0
  71. package/src/time-sync/time-sync.ts +112 -0
  72. package/src/time-sync/useTimeSync.tsx +58 -0
  73. 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
- // Translation loading function that loads compiled JSON files per namespace
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
- let translations: any = {};
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,12 @@
1
+ export {
2
+ checkClockDrift,
3
+ getSyncedDate,
4
+ getSystemDate,
5
+ getSystemTime,
6
+ getTimeOffset,
7
+ initializeTimeSync,
8
+ setTimeOffset,
9
+ syncTimeWithServer,
10
+ } from "./time-sync";
11
+
12
+ export { useTimeSync } from "./useTimeSync";
@@ -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
+ }