@streamplace/components 0.8.16 → 0.8.18
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/mobile-player/video-async.native.d.ts.map +1 -1
- package/dist/components/mobile-player/video-async.native.js +1 -1
- package/dist/components/mobile-player/video-async.native.js.map +1 -1
- package/dist/components/ui/menu.d.ts +14 -0
- package/dist/components/ui/menu.d.ts.map +1 -1
- package/dist/components/ui/menu.js +81 -2
- package/dist/components/ui/menu.js.map +1 -1
- package/dist/components/ui/text.d.ts +1 -1
- package/dist/lib/theme/atoms.d.ts +7 -7
- package/dist/lib/theme/theme.d.ts +1 -1
- package/dist/lib/theme/tokens.d.ts +1 -1
- package/dist/lib/theme/tokens.js +1 -1
- package/dist/livestream-provider/index.d.ts +2 -1
- package/dist/livestream-provider/index.d.ts.map +1 -1
- package/dist/livestream-provider/index.js +4 -2
- package/dist/livestream-provider/index.js.map +1 -1
- package/dist/livestream-provider/websocket.d.ts.map +1 -1
- package/dist/livestream-provider/websocket.js +15 -1
- package/dist/livestream-provider/websocket.js.map +1 -1
- package/dist/livestream-store/livestream-state.d.ts +2 -0
- package/dist/livestream-store/livestream-state.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.d.ts +1 -0
- package/dist/livestream-store/livestream-store.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.js +5 -1
- package/dist/livestream-store/livestream-store.js.map +1 -1
- package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
- package/dist/livestream-store/websocket-consumer.js +97 -75
- package/dist/livestream-store/websocket-consumer.js.map +1 -1
- package/i18next.config.js +21 -0
- package/locales/en-US/common.ftl +16 -0
- package/locales/en-US/settings.ftl +13 -0
- package/locales/es-ES/common.ftl +16 -0
- package/locales/es-ES/settings.ftl +1 -1
- package/locales/fr-FR/common.ftl +16 -0
- package/locales/pt-BR/common.ftl +16 -0
- package/locales/pt-BR/settings.ftl +1 -1
- package/locales/zh-Hant/common.ftl +16 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +5 -4
- package/scripts/{extract-i18n.js → migrate-i18n.js} +157 -80
- package/src/components/mobile-player/video-async.native.tsx +2 -1
- package/src/components/ui/menu.tsx +180 -3
- package/src/lib/theme/tokens.ts +1 -1
- package/src/livestream-provider/index.tsx +5 -1
- package/src/livestream-provider/websocket.tsx +15 -1
- package/src/livestream-store/livestream-state.tsx +2 -0
- package/src/livestream-store/livestream-store.tsx +5 -0
- package/src/livestream-store/websocket-consumer.tsx +95 -73
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* i18n
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* i18n migration script
|
|
5
|
+
* Migrates extracted JSON keys to .ftl files for translation
|
|
6
|
+
*
|
|
7
|
+
* This script expects that i18next-cli has already extracted keys to JSON files.
|
|
8
|
+
* It reads those JSON files, compares them to existing .ftl files, and adds any
|
|
9
|
+
* new keys to the .ftl files.
|
|
10
|
+
*
|
|
11
|
+
* For keys with i18next context/plural suffixes (e.g., key_male, key_female, key_one, key_other),
|
|
12
|
+
* it will convert them into Fluent select expressions.
|
|
8
13
|
*
|
|
9
14
|
* Usage:
|
|
10
|
-
* node
|
|
11
|
-
* node
|
|
12
|
-
* node
|
|
15
|
+
* node migrate-i18n.js # Report new keys
|
|
16
|
+
* node migrate-i18n.js --add-to=common # Add new keys to common.ftl
|
|
17
|
+
* node migrate-i18n.js --add-to=settings # Add new keys to settings.ftl
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
|
-
const { execSync } = require("child_process");
|
|
16
20
|
const fs = require("fs");
|
|
17
21
|
const path = require("path");
|
|
18
22
|
|
|
@@ -24,7 +28,6 @@ const addToNamespace = args
|
|
|
24
28
|
|
|
25
29
|
// Paths
|
|
26
30
|
const COMPONENTS_ROOT = path.join(__dirname, "..");
|
|
27
|
-
const APP_ROOT = path.join(__dirname, "..", "..", "app");
|
|
28
31
|
const MANIFEST_PATH = path.join(COMPONENTS_ROOT, "locales/manifest.json");
|
|
29
32
|
const LOCALES_FTL_DIR = path.join(COMPONENTS_ROOT, "locales");
|
|
30
33
|
const LOCALES_JSON_DIR = path.join(COMPONENTS_ROOT, "public/locales");
|
|
@@ -32,72 +35,141 @@ const LOCALES_JSON_DIR = path.join(COMPONENTS_ROOT, "public/locales");
|
|
|
32
35
|
// Load manifest
|
|
33
36
|
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
|
|
34
37
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
indentation: 2,
|
|
42
|
-
keepRemoved: true,
|
|
43
|
-
keySeparator: false,
|
|
44
|
-
namespaceSeparator: false,
|
|
45
|
-
|
|
46
|
-
lexers: {
|
|
47
|
-
js: ["JavascriptLexer"],
|
|
48
|
-
ts: ["JavascriptLexer"],
|
|
49
|
-
jsx: ["JsxLexer"],
|
|
50
|
-
tsx: ["JsxLexer"],
|
|
51
|
-
html: false,
|
|
52
|
-
htm: false,
|
|
53
|
-
handlebars: false,
|
|
54
|
-
hbs: false,
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
locales: manifest.supportedLocales,
|
|
58
|
-
output: path.join(LOCALES_JSON_DIR, "$LOCALE/$NAMESPACE.json"),
|
|
59
|
-
input: [
|
|
60
|
-
path.join(COMPONENTS_ROOT, "src/**/*.{js,jsx,ts,tsx}"),
|
|
61
|
-
path.join(APP_ROOT, "src/**/*.{js,jsx,ts,tsx}"),
|
|
62
|
-
path.join(APP_ROOT, "components/**/*.{js,jsx,ts,tsx}"),
|
|
63
|
-
"!**/node_modules/**",
|
|
64
|
-
"!**/dist/**",
|
|
65
|
-
"!**/*.test.{js,jsx,ts,tsx}",
|
|
66
|
-
"!**/*.spec.{js,jsx,ts,tsx}",
|
|
67
|
-
],
|
|
68
|
-
|
|
69
|
-
verbose: true,
|
|
70
|
-
sort: true,
|
|
71
|
-
failOnWarnings: false,
|
|
72
|
-
failOnUpdate: false,
|
|
73
|
-
};
|
|
38
|
+
// Plural forms that i18next uses
|
|
39
|
+
const PLURAL_FORMS = ["zero", "one", "two", "few", "many", "other"];
|
|
40
|
+
|
|
41
|
+
// Separators used by i18next-cli (configured in i18next.config.js)
|
|
42
|
+
const CONTEXT_SEPARATOR = "|";
|
|
43
|
+
const PLURAL_SEPARATOR = "/";
|
|
74
44
|
|
|
75
45
|
/**
|
|
76
|
-
*
|
|
46
|
+
* Group keys by base name, detecting context and plural variants
|
|
47
|
+
* Returns { baseKey: { base: true, variants: { context: Set, plurals: Set } } }
|
|
77
48
|
*/
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
function groupKeysByBase(keys) {
|
|
50
|
+
const groups = {};
|
|
51
|
+
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
if (!key.includes(CONTEXT_SEPARATOR) && !key.includes(PLURAL_SEPARATOR)) {
|
|
54
|
+
// Simple key with no variants
|
|
55
|
+
if (!groups[key]) {
|
|
56
|
+
groups[key] = {
|
|
57
|
+
base: true,
|
|
58
|
+
variants: { contexts: new Set(), plurals: new Set() },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
groups[key].base = true;
|
|
62
|
+
} else {
|
|
63
|
+
// Key with variants
|
|
64
|
+
// Format: base|context/plural or base/plural or base|context
|
|
65
|
+
let baseKey = key;
|
|
66
|
+
const detectedContexts = new Set();
|
|
67
|
+
const detectedPlurals = new Set();
|
|
68
|
+
|
|
69
|
+
// Split by context separator first
|
|
70
|
+
if (key.includes(CONTEXT_SEPARATOR)) {
|
|
71
|
+
const contextParts = key.split(CONTEXT_SEPARATOR);
|
|
72
|
+
baseKey = contextParts[0];
|
|
73
|
+
|
|
74
|
+
// The remaining part might have plurals
|
|
75
|
+
const contextAndPlural = contextParts[1];
|
|
76
|
+
|
|
77
|
+
if (contextAndPlural.includes(PLURAL_SEPARATOR)) {
|
|
78
|
+
const pluralParts = contextAndPlural.split(PLURAL_SEPARATOR);
|
|
79
|
+
detectedContexts.add(pluralParts[0]);
|
|
80
|
+
pluralParts.slice(1).forEach((p) => {
|
|
81
|
+
if (PLURAL_FORMS.includes(p)) {
|
|
82
|
+
detectedPlurals.add(p);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
detectedContexts.add(contextAndPlural);
|
|
87
|
+
}
|
|
88
|
+
} else if (key.includes(PLURAL_SEPARATOR)) {
|
|
89
|
+
// No context, just plural
|
|
90
|
+
const pluralParts = key.split(PLURAL_SEPARATOR);
|
|
91
|
+
baseKey = pluralParts[0];
|
|
92
|
+
pluralParts.slice(1).forEach((p) => {
|
|
93
|
+
if (PLURAL_FORMS.includes(p)) {
|
|
94
|
+
detectedPlurals.add(p);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
if (!groups[baseKey]) {
|
|
100
|
+
groups[baseKey] = {
|
|
101
|
+
base: false,
|
|
102
|
+
variants: { contexts: new Set(), plurals: new Set() },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
90
105
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.error("❌ Error extracting i18n keys:", error.message);
|
|
95
|
-
return false;
|
|
96
|
-
} finally {
|
|
97
|
-
if (fs.existsSync(configPath)) {
|
|
98
|
-
fs.unlinkSync(configPath);
|
|
106
|
+
detectedContexts.forEach((c) => groups[baseKey].variants.contexts.add(c));
|
|
107
|
+
detectedPlurals.forEach((p) => groups[baseKey].variants.plurals.add(p));
|
|
99
108
|
}
|
|
100
109
|
}
|
|
110
|
+
|
|
111
|
+
return groups;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convert a group of keys into Fluent format
|
|
116
|
+
*/
|
|
117
|
+
function convertToFluentFormat(baseKey, group) {
|
|
118
|
+
const hasContexts = group.variants.contexts.size > 0;
|
|
119
|
+
const hasPlurals = group.variants.plurals.size > 0;
|
|
120
|
+
|
|
121
|
+
if (!hasContexts && !hasPlurals) {
|
|
122
|
+
// Simple key
|
|
123
|
+
return `${baseKey} = ${baseKey}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build Fluent select expression
|
|
127
|
+
let selector = "";
|
|
128
|
+
let variants = [];
|
|
129
|
+
|
|
130
|
+
if (hasContexts && hasPlurals) {
|
|
131
|
+
// Both context and plural - outer selector is context, inner is plural
|
|
132
|
+
selector = "$context";
|
|
133
|
+
const contextsList = Array.from(group.variants.contexts).sort();
|
|
134
|
+
const pluralsList = Array.from(group.variants.plurals).sort();
|
|
135
|
+
|
|
136
|
+
contextsList.forEach((context, idx) => {
|
|
137
|
+
const isDefault = idx === contextsList.length - 1;
|
|
138
|
+
const prefix = isDefault ? "*" : " ";
|
|
139
|
+
|
|
140
|
+
// Build inner plural select
|
|
141
|
+
const pluralVariants = pluralsList
|
|
142
|
+
.map((p) => {
|
|
143
|
+
const pluralPrefix = p === "other" ? "*" : "";
|
|
144
|
+
return `${pluralPrefix}[${p}] ${baseKey}`;
|
|
145
|
+
})
|
|
146
|
+
.join(" ");
|
|
147
|
+
|
|
148
|
+
variants.push(
|
|
149
|
+
`\n ${prefix}[${context}] { $count -> ${pluralVariants} }`,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
} else if (hasContexts) {
|
|
153
|
+
// Only context
|
|
154
|
+
selector = "$context";
|
|
155
|
+
const contextsList = Array.from(group.variants.contexts).sort();
|
|
156
|
+
contextsList.forEach((context, idx) => {
|
|
157
|
+
const isDefault = idx === contextsList.length - 1;
|
|
158
|
+
const prefix = isDefault ? "*" : " ";
|
|
159
|
+
variants.push(`\n ${prefix}[${context}] ${baseKey}`);
|
|
160
|
+
});
|
|
161
|
+
} else if (hasPlurals) {
|
|
162
|
+
// Only plural
|
|
163
|
+
selector = "$count";
|
|
164
|
+
const pluralsList = Array.from(group.variants.plurals).sort();
|
|
165
|
+
pluralsList.forEach((plural) => {
|
|
166
|
+
const isDefault = plural === "other";
|
|
167
|
+
const prefix = isDefault ? "*" : " ";
|
|
168
|
+
variants.push(`\n ${prefix}[${plural}] ${baseKey}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `# TODO: Convert to proper Fluent select expression\n${baseKey} = { ${selector} ->${variants.join("")}\n}`;
|
|
101
173
|
}
|
|
102
174
|
|
|
103
175
|
/**
|
|
@@ -151,7 +223,7 @@ function getNamespaces(localeJsonDir) {
|
|
|
151
223
|
}
|
|
152
224
|
|
|
153
225
|
/**
|
|
154
|
-
* Add new keys to a .ftl file
|
|
226
|
+
* Add new keys to a .ftl file, converting context/plural keys to Fluent format
|
|
155
227
|
*/
|
|
156
228
|
function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
157
229
|
const targetFile = path.join(localeDir, `${namespace}.ftl`);
|
|
@@ -169,6 +241,15 @@ function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
|
169
241
|
fs.writeFileSync(targetFile, header);
|
|
170
242
|
}
|
|
171
243
|
|
|
244
|
+
// Group keys by base to detect context/plural variants
|
|
245
|
+
const keyGroups = groupKeysByBase(newKeys);
|
|
246
|
+
|
|
247
|
+
// Build content
|
|
248
|
+
const fluentEntries = [];
|
|
249
|
+
for (const [baseKey, group] of Object.entries(keyGroups)) {
|
|
250
|
+
fluentEntries.push(convertToFluentFormat(baseKey, group));
|
|
251
|
+
}
|
|
252
|
+
|
|
172
253
|
// Append new keys
|
|
173
254
|
let content = fs.readFileSync(targetFile, "utf8");
|
|
174
255
|
|
|
@@ -177,7 +258,7 @@ function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
|
177
258
|
}
|
|
178
259
|
|
|
179
260
|
content += "\n# Newly extracted keys\n";
|
|
180
|
-
content +=
|
|
261
|
+
content += fluentEntries.join("\n\n") + "\n";
|
|
181
262
|
|
|
182
263
|
fs.writeFileSync(targetFile, content);
|
|
183
264
|
|
|
@@ -188,7 +269,7 @@ function addKeysToFtlFile(localeDir, namespace, newKeys, locale) {
|
|
|
188
269
|
* Migrate extracted JSON keys to .ftl files
|
|
189
270
|
*/
|
|
190
271
|
function migrateKeysToFtl() {
|
|
191
|
-
console.log("
|
|
272
|
+
console.log("🔄 Analyzing extracted keys...");
|
|
192
273
|
|
|
193
274
|
const newKeysByLocaleAndNamespace = {}; // locale -> namespace -> [keys]
|
|
194
275
|
|
|
@@ -297,7 +378,9 @@ function migrateKeysToFtl() {
|
|
|
297
378
|
|
|
298
379
|
console.log("\n💡 Next steps:");
|
|
299
380
|
console.log(" 1. Review the new keys in your .ftl files");
|
|
300
|
-
console.log(
|
|
381
|
+
console.log(
|
|
382
|
+
" 2. Convert TODO placeholders to proper Fluent translations",
|
|
383
|
+
);
|
|
301
384
|
console.log(" 3. Run `pnpm i18n:compile` to update compiled JSON files");
|
|
302
385
|
} else {
|
|
303
386
|
// Just report
|
|
@@ -318,19 +401,13 @@ function migrateKeysToFtl() {
|
|
|
318
401
|
);
|
|
319
402
|
console.log("\nTo add these keys to a specific namespace file, run:");
|
|
320
403
|
Array.from(namespaceSet).forEach((ns) => {
|
|
321
|
-
console.log(` node
|
|
404
|
+
console.log(` node migrate-i18n.js --add-to=${ns}`);
|
|
322
405
|
});
|
|
323
406
|
}
|
|
324
407
|
}
|
|
325
408
|
|
|
326
409
|
function main() {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (success) {
|
|
330
|
-
migrateKeysToFtl();
|
|
331
|
-
} else {
|
|
332
|
-
process.exit(1);
|
|
333
|
-
}
|
|
410
|
+
migrateKeysToFtl();
|
|
334
411
|
}
|
|
335
412
|
|
|
336
413
|
main();
|
|
@@ -414,7 +414,8 @@ export function NativeIngestPlayer(props?: {
|
|
|
414
414
|
variant="secondary"
|
|
415
415
|
>
|
|
416
416
|
<View style={[layout.flex.row, gap.all[1]]}>
|
|
417
|
-
<Text>Open Settings</Text>
|
|
417
|
+
<Text>Open Settings</Text>
|
|
418
|
+
<ArrowRight color="white" size="18" />
|
|
418
419
|
</View>
|
|
419
420
|
</Button>
|
|
420
421
|
)}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
cloneElement,
|
|
4
|
+
forwardRef,
|
|
5
|
+
isValidElement,
|
|
6
|
+
ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { Animated, Platform, View, ViewStyle } from "react-native";
|
|
9
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
10
|
+
import {
|
|
11
|
+
runOnJS,
|
|
12
|
+
useAnimatedStyle,
|
|
13
|
+
useSharedValue,
|
|
14
|
+
withSpring,
|
|
15
|
+
} from "react-native-reanimated";
|
|
3
16
|
import {
|
|
4
17
|
a,
|
|
5
18
|
borderRadius,
|
|
@@ -64,11 +77,114 @@ export interface MenuItemProps {
|
|
|
64
77
|
disabled?: boolean;
|
|
65
78
|
style?: ViewStyle;
|
|
66
79
|
onPress?: () => void;
|
|
80
|
+
draggable?: boolean;
|
|
81
|
+
dragHandle?: ReactNode;
|
|
82
|
+
_dragIndex?: number;
|
|
83
|
+
_dragTotalItems?: number;
|
|
84
|
+
_onDragMove?: (fromIndex: number, toIndex: number) => void;
|
|
85
|
+
_onDragEnd?: (fromIndex: number, toIndex: number) => void;
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
export const MenuItem = forwardRef<View, MenuItemProps>(
|
|
70
|
-
(
|
|
89
|
+
(
|
|
90
|
+
{
|
|
91
|
+
children,
|
|
92
|
+
disabled,
|
|
93
|
+
style,
|
|
94
|
+
draggable,
|
|
95
|
+
dragHandle,
|
|
96
|
+
_dragIndex,
|
|
97
|
+
_dragTotalItems,
|
|
98
|
+
_onDragMove,
|
|
99
|
+
_onDragEnd,
|
|
100
|
+
},
|
|
101
|
+
ref,
|
|
102
|
+
) => {
|
|
71
103
|
const { theme } = useTheme();
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
draggable &&
|
|
107
|
+
_dragIndex !== undefined &&
|
|
108
|
+
_dragTotalItems !== undefined &&
|
|
109
|
+
_onDragMove &&
|
|
110
|
+
_onDragEnd
|
|
111
|
+
) {
|
|
112
|
+
const translateY = useSharedValue(0);
|
|
113
|
+
const isDragging = useSharedValue(false);
|
|
114
|
+
const ITEM_HEIGHT = 60;
|
|
115
|
+
|
|
116
|
+
const panGesture = Gesture.Pan()
|
|
117
|
+
.onStart(() => {
|
|
118
|
+
isDragging.value = true;
|
|
119
|
+
})
|
|
120
|
+
.onUpdate((event) => {
|
|
121
|
+
translateY.value = event.translationY;
|
|
122
|
+
|
|
123
|
+
const newIndex = Math.round(
|
|
124
|
+
_dragIndex + translateY.value / ITEM_HEIGHT,
|
|
125
|
+
);
|
|
126
|
+
const clampedIndex = Math.max(
|
|
127
|
+
0,
|
|
128
|
+
Math.min(_dragTotalItems - 1, newIndex),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (clampedIndex !== _dragIndex) {
|
|
132
|
+
runOnJS(_onDragMove)(_dragIndex, clampedIndex);
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.onEnd(() => {
|
|
136
|
+
const newIndex = Math.round(
|
|
137
|
+
_dragIndex + translateY.value / ITEM_HEIGHT,
|
|
138
|
+
);
|
|
139
|
+
const clampedIndex = Math.max(
|
|
140
|
+
0,
|
|
141
|
+
Math.min(_dragTotalItems - 1, newIndex),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
runOnJS(_onDragEnd)(_dragIndex, clampedIndex);
|
|
145
|
+
|
|
146
|
+
translateY.value = withSpring(0);
|
|
147
|
+
isDragging.value = false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
151
|
+
transform: [{ translateY: translateY.value }],
|
|
152
|
+
zIndex: isDragging.value ? 100 : 1,
|
|
153
|
+
opacity: isDragging.value ? 0.8 : 1,
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Animated.View style={animatedStyle}>
|
|
158
|
+
<View
|
|
159
|
+
ref={ref}
|
|
160
|
+
style={[
|
|
161
|
+
a.layout.flex.row,
|
|
162
|
+
a.layout.flex.alignCenter,
|
|
163
|
+
a.radius.all.sm,
|
|
164
|
+
py[1],
|
|
165
|
+
pl[3],
|
|
166
|
+
pr[2],
|
|
167
|
+
disabled && { opacity: 0.5 },
|
|
168
|
+
style,
|
|
169
|
+
]}
|
|
170
|
+
>
|
|
171
|
+
{dragHandle && (
|
|
172
|
+
<GestureDetector gesture={panGesture}>
|
|
173
|
+
<View style={{ marginRight: 8 }}>{dragHandle}</View>
|
|
174
|
+
</GestureDetector>
|
|
175
|
+
)}
|
|
176
|
+
{typeof children === "string" ? (
|
|
177
|
+
<Text style={{ color: theme.colors.popoverForeground }}>
|
|
178
|
+
{children}
|
|
179
|
+
</Text>
|
|
180
|
+
) : (
|
|
181
|
+
children
|
|
182
|
+
)}
|
|
183
|
+
</View>
|
|
184
|
+
</Animated.View>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
72
188
|
return (
|
|
73
189
|
<View
|
|
74
190
|
ref={ref}
|
|
@@ -169,3 +285,64 @@ export const MenuInfo = forwardRef<View, MenuInfoProps>(
|
|
|
169
285
|
);
|
|
170
286
|
},
|
|
171
287
|
);
|
|
288
|
+
|
|
289
|
+
export interface MenuDraggableGroupProps {
|
|
290
|
+
children: ReactNode;
|
|
291
|
+
onMove: (fromIndex: number, toIndex: number) => void;
|
|
292
|
+
onDragEnd: (fromIndex: number, toIndex: number) => void;
|
|
293
|
+
dragHandle?: ReactNode;
|
|
294
|
+
style?: ViewStyle;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const MenuDraggableGroup = forwardRef<View, MenuDraggableGroupProps>(
|
|
298
|
+
({ children, onMove, onDragEnd, dragHandle, style }, ref) => {
|
|
299
|
+
const { theme } = useTheme();
|
|
300
|
+
|
|
301
|
+
const childrenArray = Children.toArray(children);
|
|
302
|
+
const draggableItems = childrenArray.filter(
|
|
303
|
+
(child) =>
|
|
304
|
+
isValidElement(child) &&
|
|
305
|
+
(child.type === MenuItem || child.type === MenuSeparator),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
let itemIndex = 0;
|
|
309
|
+
const enhancedChildren = Children.map(children, (child) => {
|
|
310
|
+
if (isValidElement(child)) {
|
|
311
|
+
if (child.type === MenuItem) {
|
|
312
|
+
const currentIndex = itemIndex;
|
|
313
|
+
itemIndex++;
|
|
314
|
+
|
|
315
|
+
return cloneElement(child, {
|
|
316
|
+
draggable: true,
|
|
317
|
+
dragHandle: dragHandle || child.props.dragHandle,
|
|
318
|
+
_dragIndex: currentIndex,
|
|
319
|
+
_dragTotalItems: draggableItems.filter(
|
|
320
|
+
(c) => isValidElement(c) && c.type === MenuItem,
|
|
321
|
+
).length,
|
|
322
|
+
_onDragMove: onMove,
|
|
323
|
+
_onDragEnd: onDragEnd,
|
|
324
|
+
} as any);
|
|
325
|
+
}
|
|
326
|
+
if (child.type === MenuSeparator) {
|
|
327
|
+
return child;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return child;
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<View
|
|
335
|
+
ref={ref}
|
|
336
|
+
style={[
|
|
337
|
+
{ backgroundColor: theme.colors.muted + "c0" },
|
|
338
|
+
Platform.OS === "web" ? [px[1], py[1]] : p[1],
|
|
339
|
+
gap.all[1],
|
|
340
|
+
{ borderRadius: borderRadius.lg },
|
|
341
|
+
style,
|
|
342
|
+
]}
|
|
343
|
+
>
|
|
344
|
+
{enhancedChildren}
|
|
345
|
+
</View>
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
);
|
package/src/lib/theme/tokens.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { useLivestreamWebsocket } from "./websocket";
|
|
|
5
5
|
export function LivestreamProvider({
|
|
6
6
|
children,
|
|
7
7
|
src,
|
|
8
|
+
ignoreOuterContext = false,
|
|
8
9
|
}: {
|
|
9
10
|
children: React.ReactNode;
|
|
10
11
|
src: string;
|
|
12
|
+
ignoreOuterContext?: boolean;
|
|
11
13
|
}) {
|
|
12
14
|
const context = useContext(LivestreamContext);
|
|
13
15
|
const store = useRef(makeLivestreamStore()).current;
|
|
@@ -15,7 +17,9 @@ export function LivestreamProvider({
|
|
|
15
17
|
// this is ok, there's use cases for having one in another
|
|
16
18
|
// like having a player component that's independently usable
|
|
17
19
|
// but can also be embedded within an entire livestream page
|
|
18
|
-
|
|
20
|
+
if (!ignoreOuterContext) {
|
|
21
|
+
return <>{children}</>;
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
(window as any).livestreamStore = store;
|
|
21
25
|
return (
|
|
@@ -12,17 +12,30 @@ export function useLivestreamWebsocket(src: string) {
|
|
|
12
12
|
|
|
13
13
|
const ref = useRef<any[]>([]);
|
|
14
14
|
const handle = useRef<NodeJS.Timeout | null>(null);
|
|
15
|
+
const hasReceivedMessage = useRef(false);
|
|
16
|
+
const hasErrored = useRef(false);
|
|
15
17
|
|
|
16
18
|
const { readyState } = useWebSocket(`${wsUrl}/api/websocket/${src}`, {
|
|
17
19
|
reconnectInterval: 1000,
|
|
18
|
-
shouldReconnect: () =>
|
|
20
|
+
shouldReconnect: () => !hasErrored.current,
|
|
19
21
|
|
|
20
22
|
onOpen: () => {
|
|
21
23
|
ref.current = [];
|
|
24
|
+
hasReceivedMessage.current = false;
|
|
22
25
|
},
|
|
23
26
|
|
|
24
27
|
onError: (e) => {
|
|
25
28
|
console.log("onError", e);
|
|
29
|
+
if (!hasReceivedMessage.current) {
|
|
30
|
+
hasErrored.current = true;
|
|
31
|
+
handleWebSocketMessages([
|
|
32
|
+
{
|
|
33
|
+
$type: "place.stream.error",
|
|
34
|
+
code: "user_not_found",
|
|
35
|
+
message: "this stream doesn't exist or is unavailable",
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
26
39
|
},
|
|
27
40
|
|
|
28
41
|
// spamming the redux store with messages causes a zillion re-renders,
|
|
@@ -30,6 +43,7 @@ export function useLivestreamWebsocket(src: string) {
|
|
|
30
43
|
onMessage: (msg) => {
|
|
31
44
|
try {
|
|
32
45
|
const data = JSON.parse(msg.data);
|
|
46
|
+
hasReceivedMessage.current = true;
|
|
33
47
|
ref.current.push(data);
|
|
34
48
|
if (handle.current) {
|
|
35
49
|
return;
|
|
@@ -21,6 +21,8 @@ export interface LivestreamState {
|
|
|
21
21
|
replyToMessage: ChatMessageViewHydrated | null;
|
|
22
22
|
streamKey: string | null;
|
|
23
23
|
setStreamKey: (key: string | null) => void;
|
|
24
|
+
websocketConnected: boolean;
|
|
25
|
+
hasReceivedSegment: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface LivestreamProblem {
|
|
@@ -22,6 +22,8 @@ export const makeLivestreamStore = (): StoreApi<LivestreamState> => {
|
|
|
22
22
|
authors: {},
|
|
23
23
|
recentSegments: [],
|
|
24
24
|
problems: [],
|
|
25
|
+
websocketConnected: false,
|
|
26
|
+
hasReceivedSegment: false,
|
|
25
27
|
}));
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -59,4 +61,7 @@ export const useLivestream = () => useLivestreamStore((x) => x.livestream);
|
|
|
59
61
|
|
|
60
62
|
export const useSegment = () => useLivestreamStore((x) => x.segment);
|
|
61
63
|
|
|
64
|
+
export const useRecentSegments = () =>
|
|
65
|
+
useLivestreamStore((x) => x.recentSegments);
|
|
66
|
+
|
|
62
67
|
export const useRenditions = () => useLivestreamStore((x) => x.renditions);
|