@streamplace/components 0.8.11 → 0.8.14
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 +5 -4
- 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/dialog.d.ts +1 -1
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/input.js +7 -1
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/primitives/input.d.ts.map +1 -1
- package/dist/components/ui/primitives/input.js +3 -1
- package/dist/components/ui/primitives/input.js.map +1 -1
- package/dist/components/ui/text.d.ts +4 -4
- 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/livestream-store/chat.d.ts.map +1 -1
- package/dist/livestream-store/chat.js +7 -1
- package/dist/livestream-store/chat.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 +5 -3
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +5 -3
- package/src/components/share/sharesheet.tsx +11 -27
- package/src/components/ui/input.tsx +7 -1
- package/src/components/ui/primitives/input.tsx +3 -1
- 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/livestream-store/chat.tsx +7 -1
- 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
|
@@ -82,6 +82,7 @@ finish = Finalizar
|
|
|
82
82
|
|
|
83
83
|
## Categorias de Navegação
|
|
84
84
|
about = Sobre
|
|
85
|
+
account = Conta
|
|
85
86
|
advanced = Avançado
|
|
86
87
|
danmu = Danmu
|
|
87
88
|
developer = Desenvolvedor
|
|
@@ -97,6 +98,12 @@ refresh = Atualizar
|
|
|
97
98
|
save-button = Salvar
|
|
98
99
|
sign-in = Entrar
|
|
99
100
|
update = Atualizar
|
|
101
|
+
log-out = Sair
|
|
102
|
+
|
|
103
|
+
## Configurações da Conta
|
|
104
|
+
account-greeting = Olá, @{ $handle }.
|
|
105
|
+
edit-profile-bluesky = Editar perfil (no Bluesky)
|
|
106
|
+
change-name-color = Mudar cor do nome
|
|
100
107
|
|
|
101
108
|
## Gerenciamento de Chaves
|
|
102
109
|
key-management = Gerenciamento de Chaves
|
|
@@ -32,6 +32,11 @@ info = 資訊
|
|
|
32
32
|
search-placeholder = 搜尋...
|
|
33
33
|
message-input = 輸入您的訊息...
|
|
34
34
|
|
|
35
|
+
## Authentication & Access
|
|
36
|
+
please-log-in-to-access-this-page = 請登入以存取此頁面
|
|
37
|
+
go-to-settings = 前往設定
|
|
38
|
+
go-back = 返回
|
|
39
|
+
|
|
35
40
|
## Demo and Testing
|
|
36
41
|
welcome-user = 歡迎,{ $username }!
|
|
37
42
|
notification-count = { $count ->
|
|
@@ -83,6 +83,7 @@ finish = 完成
|
|
|
83
83
|
|
|
84
84
|
## 導航類別
|
|
85
85
|
about = 關於
|
|
86
|
+
account = 帳戶
|
|
86
87
|
advanced = 進階
|
|
87
88
|
danmu = 彈幕
|
|
88
89
|
developer = 開發者
|
|
@@ -98,6 +99,12 @@ refresh = 重新整理
|
|
|
98
99
|
save-button = 儲存
|
|
99
100
|
sign-in = 登入
|
|
100
101
|
update = 更新
|
|
102
|
+
log-out = 登出
|
|
103
|
+
|
|
104
|
+
## 帳戶設定
|
|
105
|
+
account-greeting = 嗨,@{ $handle }。
|
|
106
|
+
edit-profile-bluesky = 編輯個人資料(在 Bluesky)
|
|
107
|
+
change-name-color = 變更名稱顏色
|
|
101
108
|
|
|
102
109
|
## 金鑰管理
|
|
103
110
|
key-management = 金鑰管理
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamplace/components",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.14",
|
|
4
4
|
"description": "Streamplace React (Native) Components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "src/index.tsx",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsc",
|
|
74
74
|
"start": "tsc --watch --preserveWatchOutput",
|
|
75
|
-
"prepare": "
|
|
75
|
+
"prepare": "node scripts/compile-translations.js && tsc",
|
|
76
76
|
"i18n:compile": "node scripts/compile-translations.js",
|
|
77
77
|
"i18n:watch": "nodemon scripts/compile-translations.js --watch locales/**/*.ftl",
|
|
78
78
|
"i18n:extract": "node scripts/extract-i18n.js"
|
|
79
79
|
},
|
|
80
|
-
"gitHead": "
|
|
80
|
+
"gitHead": "e5835f817342ca5b587bc9bb3e541082bbeac4ea"
|
|
81
81
|
}
|
|
@@ -8,6 +8,7 @@ import { Linking, View } from "react-native";
|
|
|
8
8
|
import { ChatMessageViewHydrated } from "streamplace";
|
|
9
9
|
import { RichtextSegment, segmentize } from "../../lib/facet";
|
|
10
10
|
import { borders, flex, gap, ml, mr, opacity, pl } from "../../lib/theme/atoms";
|
|
11
|
+
import { formatHandleWithAt } from "../../utils/format-handle";
|
|
11
12
|
import { atoms, colors, layout } from "../ui";
|
|
12
13
|
|
|
13
14
|
interface Facet {
|
|
@@ -138,7 +139,7 @@ export const RenderChatMessage = memo(
|
|
|
138
139
|
fontWeight: "thin",
|
|
139
140
|
}}
|
|
140
141
|
>
|
|
141
|
-
|
|
142
|
+
{formatHandleWithAt(replyTo.author)}
|
|
142
143
|
</Text>{" "}
|
|
143
144
|
<Text
|
|
144
145
|
style={{
|
|
@@ -181,7 +182,7 @@ export const RenderChatMessage = memo(
|
|
|
181
182
|
},
|
|
182
183
|
]}
|
|
183
184
|
>
|
|
184
|
-
|
|
185
|
+
{formatHandleWithAt(item.author)}
|
|
185
186
|
</Text>
|
|
186
187
|
:{" "}
|
|
187
188
|
<RichTextMessage
|
|
@@ -12,6 +12,7 @@ import { Linking } from "react-native";
|
|
|
12
12
|
import { ChatMessageViewHydrated } from "streamplace";
|
|
13
13
|
import { useDeleteChatMessage } from "../../livestream-store";
|
|
14
14
|
import { useStreamplaceStore } from "../../streamplace-store";
|
|
15
|
+
import { formatHandle, formatHandleWithAt } from "../../utils/format-handle";
|
|
15
16
|
import {
|
|
16
17
|
atoms,
|
|
17
18
|
DropdownMenu,
|
|
@@ -113,7 +114,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
113
114
|
minute: "2-digit",
|
|
114
115
|
hour12: false,
|
|
115
116
|
})}{" "}
|
|
116
|
-
|
|
117
|
+
{formatHandleWithAt(message.author)}: {message.record.text}
|
|
117
118
|
</Text>
|
|
118
119
|
</View>
|
|
119
120
|
</DropdownMenuItem>
|
|
@@ -159,7 +160,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
159
160
|
<Text color="destructive">
|
|
160
161
|
{isBlockLoading
|
|
161
162
|
? "Blocking..."
|
|
162
|
-
: `Block user
|
|
163
|
+
: `Block user ${formatHandleWithAt(message.author)} from this channel`}
|
|
163
164
|
</Text>
|
|
164
165
|
)}
|
|
165
166
|
</DropdownMenuItem>
|
|
@@ -170,7 +171,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
170
171
|
<DropdownMenuItem
|
|
171
172
|
onPress={() => {
|
|
172
173
|
Linking.openURL(
|
|
173
|
-
`https://${BSKY_FRONTEND_DOMAIN}/profile/${message.author
|
|
174
|
+
`https://${BSKY_FRONTEND_DOMAIN}/profile/${formatHandle(message.author)}`,
|
|
174
175
|
);
|
|
175
176
|
}}
|
|
176
177
|
>
|
|
@@ -215,6 +216,7 @@ export function DeleteButton({
|
|
|
215
216
|
const toast = useToast();
|
|
216
217
|
return (
|
|
217
218
|
<DropdownMenuItem
|
|
219
|
+
closeOnPress={false}
|
|
218
220
|
onPress={() => {
|
|
219
221
|
if (!message) return;
|
|
220
222
|
if (!confirming) {
|
|
@@ -4,6 +4,8 @@ import { Image, Linking, Platform, Pressable, View } from "react-native";
|
|
|
4
4
|
import {
|
|
5
5
|
ContentRights,
|
|
6
6
|
ContentWarnings,
|
|
7
|
+
formatHandle,
|
|
8
|
+
formatHandleWithAt,
|
|
7
9
|
useAvatars,
|
|
8
10
|
useLivestreamInfo,
|
|
9
11
|
zero,
|
|
@@ -113,12 +115,12 @@ export function ContextMenu({
|
|
|
113
115
|
<Pressable
|
|
114
116
|
onPress={() => {
|
|
115
117
|
if (profile?.handle) {
|
|
116
|
-
const url = `https://bsky.app/profile/${profile
|
|
118
|
+
const url = `https://bsky.app/profile/${formatHandle(profile)}`;
|
|
117
119
|
Linking.openURL(url);
|
|
118
120
|
}
|
|
119
121
|
}}
|
|
120
122
|
>
|
|
121
|
-
<Text
|
|
123
|
+
<Text>{profile && formatHandleWithAt(profile)}</Text>
|
|
122
124
|
</Pressable>
|
|
123
125
|
{/*{did && profile && (
|
|
124
126
|
<FollowButton streamerDID={profile?.did} currentUserDID={did} />
|
|
@@ -163,7 +165,7 @@ export function ContextMenu({
|
|
|
163
165
|
<DropdownMenuItem
|
|
164
166
|
onPress={() => {
|
|
165
167
|
if (profile?.handle) {
|
|
166
|
-
const url = `https://bsky.app/profile/${profile
|
|
168
|
+
const url = `https://bsky.app/profile/${formatHandle(profile)}`;
|
|
167
169
|
Linking.openURL(url);
|
|
168
170
|
}
|
|
169
171
|
}}
|
|
@@ -4,6 +4,7 @@ import { Clipboard, Linking, Platform, View } from "react-native";
|
|
|
4
4
|
import { colors } from "../../lib/theme";
|
|
5
5
|
import { useLivestreamStore } from "../../livestream-store";
|
|
6
6
|
import { useUrl } from "../../streamplace-store";
|
|
7
|
+
import { formatHandle } from "../../utils/format-handle";
|
|
7
8
|
import { BlueskyIcon } from "../icons/bluesky-icon";
|
|
8
9
|
import {
|
|
9
10
|
DropdownMenu,
|
|
@@ -26,12 +27,12 @@ export function ShareSheet({ onShare }: ShareSheetProps = {}) {
|
|
|
26
27
|
|
|
27
28
|
// Get the current stream URL
|
|
28
29
|
const getStreamUrl = useCallback(() => {
|
|
29
|
-
return url + (profile ?
|
|
30
|
+
return url + (profile ? `/${formatHandle(profile)}` : "");
|
|
30
31
|
}, [profile]);
|
|
31
32
|
|
|
32
33
|
// Get the embed URL
|
|
33
34
|
const getEmbedUrl = useCallback(() => {
|
|
34
|
-
return url + (profile ? `/embed/${profile
|
|
35
|
+
return url + (profile ? `/embed/${formatHandle(profile)}` : "");
|
|
35
36
|
}, [profile]);
|
|
36
37
|
|
|
37
38
|
// Get embed code
|
|
@@ -63,31 +64,22 @@ export function ShareSheet({ onShare }: ShareSheetProps = {}) {
|
|
|
63
64
|
// Share to Bluesky
|
|
64
65
|
const shareToBluesky = useCallback(() => {
|
|
65
66
|
const streamUrl = getStreamUrl();
|
|
66
|
-
const text =
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const text =
|
|
68
|
+
profile && profile.handle
|
|
69
|
+
? `Check out @${profile.handle} live on Streamplace! ${streamUrl}`
|
|
70
|
+
: `Check out this stream on Streamplace! ${streamUrl}`;
|
|
69
71
|
const blueskyUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`;
|
|
70
72
|
Linking.openURL(blueskyUrl);
|
|
71
73
|
onShare?.("share_bluesky", true);
|
|
72
74
|
}, [profile, getStreamUrl, onShare]);
|
|
73
75
|
|
|
74
|
-
// Share to Twitter/X
|
|
75
|
-
const shareToTwitter = useCallback(() => {
|
|
76
|
-
const streamUrl = getStreamUrl();
|
|
77
|
-
const text = profile
|
|
78
|
-
? `Check out @${profile.handle} live on Streamplace!`
|
|
79
|
-
: `Check out this stream on Streamplace!`;
|
|
80
|
-
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(streamUrl)}`;
|
|
81
|
-
Linking.openURL(twitterUrl);
|
|
82
|
-
onShare?.("share_twitter", true);
|
|
83
|
-
}, [profile, getStreamUrl, onShare]);
|
|
84
|
-
|
|
85
76
|
// Native share (mobile)
|
|
86
77
|
const nativeShare = useCallback(async () => {
|
|
87
78
|
const streamUrl = getStreamUrl();
|
|
88
|
-
const text =
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
const text =
|
|
80
|
+
profile && profile.handle
|
|
81
|
+
? `Check out @${profile.handle} live on Streamplace!`
|
|
82
|
+
: `Check out this stream on Streamplace!`;
|
|
91
83
|
|
|
92
84
|
if (Platform.OS === "web" && navigator.share) {
|
|
93
85
|
try {
|
|
@@ -119,14 +111,6 @@ export function ShareSheet({ onShare }: ShareSheetProps = {}) {
|
|
|
119
111
|
<Text>Share to Bluesky</Text>
|
|
120
112
|
</View>
|
|
121
113
|
</DropdownMenuItem>
|
|
122
|
-
{/* <DropdownMenuItem onPress={shareToTwitter}>
|
|
123
|
-
<View
|
|
124
|
-
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
125
|
-
>
|
|
126
|
-
<MessageCircle size={20} color={colors.gray[400]} />
|
|
127
|
-
<Text>Share to X</Text>
|
|
128
|
-
</View>
|
|
129
|
-
</DropdownMenuItem> */}
|
|
130
114
|
{/* navigator isn't on non-web */}
|
|
131
115
|
{Platform.OS !== "web" || (navigator && (navigator as any).share) ? (
|
|
132
116
|
<DropdownMenuItem onPress={nativeShare}>
|
|
@@ -238,7 +238,13 @@ export const Input = forwardRef<any, InputProps>(
|
|
|
238
238
|
error={!!error}
|
|
239
239
|
onFocus={handleFocus}
|
|
240
240
|
onBlur={handleBlur}
|
|
241
|
-
style={[
|
|
241
|
+
style={[
|
|
242
|
+
containerStyles,
|
|
243
|
+
textStyles,
|
|
244
|
+
containerStyle,
|
|
245
|
+
inputStyle,
|
|
246
|
+
{ outline: "none" },
|
|
247
|
+
]}
|
|
242
248
|
placeholderTextColor={
|
|
243
249
|
disabled ? theme.colors.textDisabled : theme.colors.textMuted
|
|
244
250
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
View,
|
|
17
17
|
ViewProps,
|
|
18
18
|
} from "react-native";
|
|
19
|
+
import { tokens } from "../../../ui";
|
|
19
20
|
|
|
20
21
|
// Base input primitive interface
|
|
21
22
|
export interface InputPrimitiveProps extends Omit<TextInputProps, "onChange"> {
|
|
@@ -329,7 +330,8 @@ const primitiveStyles = StyleSheet.create({
|
|
|
329
330
|
borderWidth: 1,
|
|
330
331
|
borderColor: "#d1d5db",
|
|
331
332
|
borderRadius: 8,
|
|
332
|
-
|
|
333
|
+
boxShadow: "none",
|
|
334
|
+
fontFamily: tokens.fontFamilies.regular,
|
|
333
335
|
...Platform.select({
|
|
334
336
|
ios: {
|
|
335
337
|
paddingVertical: 12,
|
|
@@ -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
|
|
|
@@ -215,7 +215,13 @@ export const reduceChatIncremental = (
|
|
|
215
215
|
for (const msg of newMessages) {
|
|
216
216
|
if (msg.deleted) {
|
|
217
217
|
hasChanges = true;
|
|
218
|
-
|
|
218
|
+
// find and remove the message from the index
|
|
219
|
+
for (const [key, message] of Object.entries(newChatIndex)) {
|
|
220
|
+
if (message.uri === msg.uri) {
|
|
221
|
+
delete newChatIndex[key];
|
|
222
|
+
removedKeys.add(key);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
newMessages = newMessages.filter((msg) => msg.deleted !== true);
|
|
@@ -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
|
+
}
|