@streamplace/components 0.0.1 → 0.7.0

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 (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Design tokens for React Native components
3
+ * Inspired by shadcn/ui but adapted for React Native styling
4
+ */
5
+
6
+ export const colors = {
7
+ // Primary colors
8
+ primary: {
9
+ 50: "#eff6ff",
10
+ 100: "#dbeafe",
11
+ 200: "#bfdbfe",
12
+ 300: "#93c5fd",
13
+ 400: "#60a5fa",
14
+ 500: "#3b82f6",
15
+ 600: "#2563eb",
16
+ 700: "#1d4ed8",
17
+ 800: "#1e40af",
18
+ 900: "#1e3a8a",
19
+ 950: "#172554",
20
+ },
21
+
22
+ // Grayscale
23
+ gray: {
24
+ 50: "#fafafa",
25
+ 100: "#f5f5f5",
26
+ 200: "#e5e5e5",
27
+ 300: "#d4d4d4",
28
+ 400: "#a3a3a3",
29
+ 500: "#737373",
30
+ 600: "#525252",
31
+ 700: "#404040",
32
+ 800: "#262626",
33
+ 900: "#171717",
34
+ 950: "#0d0d0d",
35
+ },
36
+
37
+ // Semantic colors
38
+ destructive: {
39
+ 50: "#fef2f2",
40
+ 100: "#fee2e2",
41
+ 200: "#fecaca",
42
+ 300: "#fca5a5",
43
+ 400: "#f87171",
44
+ 500: "#ef4444",
45
+ 600: "#dc2626",
46
+ 700: "#b91c1c",
47
+ 800: "#991b1b",
48
+ 900: "#7f1d1d",
49
+ 950: "#450a0a",
50
+ },
51
+
52
+ success: {
53
+ 50: "#f0fdf4",
54
+ 100: "#dcfce7",
55
+ 200: "#bbf7d0",
56
+ 300: "#86efac",
57
+ 400: "#4ade80",
58
+ 500: "#22c55e",
59
+ 600: "#16a34a",
60
+ 700: "#15803d",
61
+ 800: "#166534",
62
+ 900: "#14532d",
63
+ 950: "#052e16",
64
+ },
65
+
66
+ warning: {
67
+ 50: "#fffbeb",
68
+ 100: "#fef3c7",
69
+ 200: "#fde68a",
70
+ 300: "#fcd34d",
71
+ 400: "#fbbf24",
72
+ 500: "#f59e0b",
73
+ 600: "#d97706",
74
+ 700: "#b45309",
75
+ 800: "#92400e",
76
+ 900: "#78350f",
77
+ 950: "#451a03",
78
+ },
79
+
80
+ // iOS system colors (adaptive)
81
+ ios: {
82
+ systemBlue: "#007AFF",
83
+ systemGreen: "#34C759",
84
+ systemRed: "#FF3B30",
85
+ systemOrange: "#FF9500",
86
+ systemYellow: "#FFCC00",
87
+ systemPurple: "#AF52DE",
88
+ systemPink: "#FF2D92",
89
+ systemTeal: "#5AC8FA",
90
+ systemIndigo: "#5856D6",
91
+ systemGray: "#8E8E93",
92
+ systemGray2: "#AEAEB2",
93
+ systemGray3: "#C7C7CC",
94
+ systemGray4: "#D1D1D6",
95
+ systemGray5: "#E5E5EA",
96
+ systemGray6: "#F2F2F7",
97
+ },
98
+
99
+ // Android Material colors
100
+ android: {
101
+ primary: "#6200EE",
102
+ primaryVariant: "#3700B3",
103
+ secondary: "#03DAC6",
104
+ secondaryVariant: "#018786",
105
+ background: "#FFFFFF",
106
+ surface: "#FFFFFF",
107
+ error: "#B00020",
108
+ onPrimary: "#FFFFFF",
109
+ onSecondary: "#000000",
110
+ onBackground: "#000000",
111
+ onSurface: "#000000",
112
+ onError: "#FFFFFF",
113
+ },
114
+
115
+ // Transparent colors
116
+ transparent: "transparent",
117
+ black: "#000000",
118
+ white: "#FFFFFF",
119
+ } as const;
120
+
121
+ export const spacing = {
122
+ 0: 0,
123
+ 1: 4,
124
+ 2: 8,
125
+ 3: 12,
126
+ 4: 16,
127
+ 5: 20,
128
+ 6: 24,
129
+ 7: 28,
130
+ 8: 32,
131
+ 9: 36,
132
+ 10: 40,
133
+ 11: 44,
134
+ 12: 48,
135
+ 14: 56,
136
+ 16: 64,
137
+ 20: 80,
138
+ 24: 96,
139
+ 28: 112,
140
+ 32: 128,
141
+ 36: 144,
142
+ 40: 160,
143
+ 44: 176,
144
+ 48: 192,
145
+ 52: 208,
146
+ 56: 224,
147
+ 60: 240,
148
+ 64: 256,
149
+ 72: 288,
150
+ 80: 320,
151
+ 96: 384,
152
+ auto: "auto",
153
+ } as const;
154
+
155
+ export const borderRadius = {
156
+ none: 0,
157
+ sm: 4,
158
+ md: 8,
159
+ lg: 12,
160
+ xl: 16,
161
+ "2xl": 20,
162
+ "3xl": 24,
163
+ full: 999,
164
+ } as const;
165
+
166
+ export const typography = {
167
+ // iOS system font sizes
168
+ ios: {
169
+ largeTitle: {
170
+ fontSize: 34,
171
+ lineHeight: 41,
172
+ fontWeight: "700" as const,
173
+ },
174
+ title1: {
175
+ fontSize: 28,
176
+ lineHeight: 34,
177
+ fontWeight: "700" as const,
178
+ },
179
+ title2: {
180
+ fontSize: 22,
181
+ lineHeight: 28,
182
+ fontWeight: "700" as const,
183
+ },
184
+ title3: {
185
+ fontSize: 20,
186
+ lineHeight: 25,
187
+ fontWeight: "600" as const,
188
+ },
189
+ headline: {
190
+ fontSize: 17,
191
+ lineHeight: 22,
192
+ fontWeight: "600" as const,
193
+ },
194
+ body: {
195
+ fontSize: 17,
196
+ lineHeight: 22,
197
+ fontWeight: "400" as const,
198
+ },
199
+ callout: {
200
+ fontSize: 16,
201
+ lineHeight: 21,
202
+ fontWeight: "400" as const,
203
+ },
204
+ subhead: {
205
+ fontSize: 15,
206
+ lineHeight: 20,
207
+ fontWeight: "400" as const,
208
+ },
209
+ footnote: {
210
+ fontSize: 13,
211
+ lineHeight: 18,
212
+ fontWeight: "400" as const,
213
+ },
214
+ caption1: {
215
+ fontSize: 12,
216
+ lineHeight: 16,
217
+ fontWeight: "400" as const,
218
+ },
219
+ caption2: {
220
+ fontSize: 11,
221
+ lineHeight: 13,
222
+ fontWeight: "400" as const,
223
+ },
224
+ },
225
+
226
+ // Android Material typography
227
+ android: {
228
+ headline1: {
229
+ fontSize: 96,
230
+ lineHeight: 112,
231
+ fontWeight: "300" as const,
232
+ },
233
+ headline2: {
234
+ fontSize: 60,
235
+ lineHeight: 72,
236
+ fontWeight: "300" as const,
237
+ },
238
+ headline3: {
239
+ fontSize: 48,
240
+ lineHeight: 56,
241
+ fontWeight: "400" as const,
242
+ },
243
+ headline4: {
244
+ fontSize: 34,
245
+ lineHeight: 42,
246
+ fontWeight: "400" as const,
247
+ },
248
+ headline5: {
249
+ fontSize: 24,
250
+ lineHeight: 32,
251
+ fontWeight: "400" as const,
252
+ },
253
+ headline6: {
254
+ fontSize: 20,
255
+ lineHeight: 28,
256
+ fontWeight: "500" as const,
257
+ },
258
+ subtitle1: {
259
+ fontSize: 16,
260
+ lineHeight: 24,
261
+ fontWeight: "400" as const,
262
+ },
263
+ subtitle2: {
264
+ fontSize: 14,
265
+ lineHeight: 22,
266
+ fontWeight: "500" as const,
267
+ },
268
+ body1: {
269
+ fontSize: 16,
270
+ lineHeight: 24,
271
+ fontWeight: "400" as const,
272
+ },
273
+ body2: {
274
+ fontSize: 14,
275
+ lineHeight: 20,
276
+ fontWeight: "400" as const,
277
+ },
278
+ button: {
279
+ fontSize: 14,
280
+ lineHeight: 16,
281
+ fontWeight: "500" as const,
282
+ },
283
+ caption: {
284
+ fontSize: 12,
285
+ lineHeight: 16,
286
+ fontWeight: "400" as const,
287
+ },
288
+ overline: {
289
+ fontSize: 10,
290
+ lineHeight: 16,
291
+ fontWeight: "400" as const,
292
+ },
293
+ },
294
+
295
+ // Universal typography scale
296
+ universal: {
297
+ xs: {
298
+ fontSize: 12,
299
+ lineHeight: 16,
300
+ fontWeight: "400" as const,
301
+ },
302
+ sm: {
303
+ fontSize: 14,
304
+ lineHeight: 20,
305
+ fontWeight: "400" as const,
306
+ },
307
+ base: {
308
+ fontSize: 16,
309
+ lineHeight: 24,
310
+ fontWeight: "400" as const,
311
+ },
312
+ lg: {
313
+ fontSize: 18,
314
+ lineHeight: 28,
315
+ fontWeight: "400" as const,
316
+ },
317
+ xl: {
318
+ fontSize: 20,
319
+ lineHeight: 28,
320
+ fontWeight: "500" as const,
321
+ },
322
+ "2xl": {
323
+ fontSize: 24,
324
+ lineHeight: 32,
325
+ fontWeight: "600" as const,
326
+ },
327
+ "3xl": {
328
+ fontSize: 30,
329
+ lineHeight: 36,
330
+ fontWeight: "700" as const,
331
+ },
332
+ "4xl": {
333
+ fontSize: 36,
334
+ lineHeight: 40,
335
+ fontWeight: "700" as const,
336
+ },
337
+ },
338
+ } as const;
339
+
340
+ export const shadows = {
341
+ none: {
342
+ shadowColor: "transparent",
343
+ shadowOffset: { width: 0, height: 0 },
344
+ shadowOpacity: 0,
345
+ shadowRadius: 0,
346
+ elevation: 0,
347
+ },
348
+ sm: {
349
+ shadowColor: colors.black,
350
+ shadowOffset: { width: 0, height: 1 },
351
+ shadowOpacity: 0.05,
352
+ shadowRadius: 2,
353
+ elevation: 2,
354
+ },
355
+ md: {
356
+ shadowColor: colors.black,
357
+ shadowOffset: { width: 0, height: 2 },
358
+ shadowOpacity: 0.1,
359
+ shadowRadius: 4,
360
+ elevation: 4,
361
+ },
362
+ lg: {
363
+ shadowColor: colors.black,
364
+ shadowOffset: { width: 0, height: 4 },
365
+ shadowOpacity: 0.15,
366
+ shadowRadius: 8,
367
+ elevation: 8,
368
+ },
369
+ xl: {
370
+ shadowColor: colors.black,
371
+ shadowOffset: { width: 0, height: 8 },
372
+ shadowOpacity: 0.2,
373
+ shadowRadius: 16,
374
+ elevation: 16,
375
+ },
376
+ } as const;
377
+
378
+ // Touch targets (iOS Human Interface Guidelines)
379
+ export const touchTargets = {
380
+ minimum: 44, // Minimum touch target size
381
+ comfortable: 48, // Comfortable touch target size
382
+ large: 56, // Large touch target size
383
+ } as const;
384
+
385
+ // Animation durations
386
+ export const animations = {
387
+ fast: 150,
388
+ normal: 200,
389
+ slow: 300,
390
+ slower: 500,
391
+ } as const;
392
+
393
+ // Breakpoints for responsive design
394
+ export const breakpoints = {
395
+ sm: 640,
396
+ md: 768,
397
+ lg: 1024,
398
+ xl: 1280,
399
+ "2xl": 1536,
400
+ } as const;
401
+
402
+ export type Colors = typeof colors;
403
+ export type Spacing = typeof spacing;
404
+ export type BorderRadius = typeof borderRadius;
405
+ export type Typography = typeof typography;
406
+ export type Shadows = typeof shadows;
407
+ export type TouchTargets = typeof touchTargets;
408
+ export type Animations = typeof animations;
409
+ export type Breakpoints = typeof breakpoints;
@@ -0,0 +1,132 @@
1
+ import { ImageStyle, StyleSheet, TextStyle, ViewStyle } from "react-native";
2
+
3
+ // React Native style utilities
4
+ type Style = ViewStyle | TextStyle | ImageStyle;
5
+
6
+ /**
7
+ * Merges React Native styles similar to how cn() merges CSS classes
8
+ * Handles arrays, objects, and falsy values
9
+ */
10
+ export function mergeStyles(
11
+ ...styles: (Style | Style[] | undefined | null | false)[]
12
+ ): Style {
13
+ const validStyles = styles.filter(Boolean).flat() as Style[];
14
+ return StyleSheet.flatten(validStyles) || {};
15
+ }
16
+
17
+ /**
18
+ * Creates a style merger function that includes base styles
19
+ * Useful for component variants
20
+ */
21
+ export function createStyleMerger(baseStyle: Style) {
22
+ return (...styles: (Style | Style[] | undefined | null | false)[]) => {
23
+ return mergeStyles(baseStyle, ...styles);
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Conditionally applies styles based on boolean conditions
29
+ */
30
+ export function conditionalStyle(
31
+ condition: boolean,
32
+ trueStyle: Style,
33
+ falseStyle?: Style,
34
+ ): Style | undefined {
35
+ return condition ? trueStyle : falseStyle;
36
+ }
37
+
38
+ /**
39
+ * Creates responsive values based on screen dimensions
40
+ */
41
+ export function responsiveValue<T>(
42
+ values: {
43
+ sm?: T;
44
+ md?: T;
45
+ lg?: T;
46
+ xl?: T;
47
+ default: T;
48
+ },
49
+ screenWidth: number,
50
+ ): T {
51
+ if (screenWidth >= 1280 && values.xl !== undefined) return values.xl;
52
+ if (screenWidth >= 1024 && values.lg !== undefined) return values.lg;
53
+ if (screenWidth >= 768 && values.md !== undefined) return values.md;
54
+ if (screenWidth >= 640 && values.sm !== undefined) return values.sm;
55
+ return values.default;
56
+ }
57
+
58
+ /**
59
+ * Creates platform-specific styles
60
+ */
61
+ export function platformStyle(styles: {
62
+ ios?: Style;
63
+ android?: Style;
64
+ web?: Style;
65
+ default?: Style;
66
+ }): Style {
67
+ const Platform = require("react-native").Platform;
68
+
69
+ if (Platform.OS === "ios" && styles.ios) return styles.ios;
70
+ if (Platform.OS === "android" && styles.android) return styles.android;
71
+ if (Platform.OS === "web" && styles.web) return styles.web;
72
+ return styles.default || {};
73
+ }
74
+
75
+ /**
76
+ * Converts hex color to rgba
77
+ */
78
+ export function hexToRgba(hex: string, alpha: number = 1): string {
79
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
80
+ if (!result) return hex;
81
+
82
+ const r = parseInt(result[1], 16);
83
+ const g = parseInt(result[2], 16);
84
+ const b = parseInt(result[3], 16);
85
+
86
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
87
+ }
88
+
89
+ /**
90
+ * Creates a debounced function for performance
91
+ */
92
+ export function debounce<T extends (...args: any[]) => any>(
93
+ func: T,
94
+ delay: number,
95
+ ): (...args: Parameters<T>) => void {
96
+ let timeoutId: NodeJS.Timeout;
97
+
98
+ return (...args: Parameters<T>) => {
99
+ clearTimeout(timeoutId);
100
+ timeoutId = setTimeout(() => func(...args), delay);
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Creates a throttled function for performance
106
+ */
107
+ export function throttle<T extends (...args: any[]) => any>(
108
+ func: T,
109
+ delay: number,
110
+ ): (...args: Parameters<T>) => void {
111
+ let lastCall = 0;
112
+
113
+ return (...args: Parameters<T>) => {
114
+ const now = Date.now();
115
+ if (now - lastCall >= delay) {
116
+ lastCall = now;
117
+ func(...args);
118
+ }
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Type-safe component prop forwarding
124
+ */
125
+ export function forwardProps<T extends Record<string, any>>(
126
+ props: T,
127
+ omit: (keyof T)[],
128
+ ): Omit<T, keyof T extends string ? keyof T : never> {
129
+ const result = { ...props };
130
+ omit.forEach((key) => delete result[key]);
131
+ return result;
132
+ }
@@ -0,0 +1,48 @@
1
+ import React, { useContext, useRef } from "react";
2
+ import { LivestreamContext, makeLivestreamStore } from "../livestream-store";
3
+ import { useLivestreamWebsocket } from "./websocket";
4
+
5
+ export function LivestreamProvider({
6
+ children,
7
+ src,
8
+ }: {
9
+ children: React.ReactNode;
10
+ src: string;
11
+ }) {
12
+ const context = useContext(LivestreamContext);
13
+ const store = useRef(makeLivestreamStore()).current;
14
+ if (context) {
15
+ // this is ok, there's use cases for having one in another
16
+ // like having a player component that's independently usable
17
+ // but can also be embedded within an entire livestream page
18
+ return <>{children}</>;
19
+ }
20
+ (window as any).livestreamStore = store;
21
+ return (
22
+ <LivestreamContext.Provider value={{ store: store }}>
23
+ <LivestreamPoller src={src}>{children}</LivestreamPoller>
24
+ </LivestreamContext.Provider>
25
+ );
26
+ }
27
+
28
+ export function WebsocketWatcher({ src }: { src: string }) {
29
+ useLivestreamWebsocket(src);
30
+ return <></>;
31
+ }
32
+
33
+ export function LivestreamPoller({
34
+ children,
35
+ src,
36
+ }: {
37
+ children: React.ReactNode;
38
+ src: string;
39
+ }) {
40
+ // Websocket watcher is a sibling instead of a parent to avoid
41
+ // re-rendering when the websocket does stuff
42
+ return (
43
+ <>
44
+ <WebsocketWatcher src={src} />
45
+ {children}
46
+ </>
47
+ );
48
+ }
@@ -0,0 +1,47 @@
1
+ import { useRef } from "react";
2
+ import useWebSocket from "react-use-websocket";
3
+ import { useHandleWebsocketMessages } from "../livestream-store";
4
+ import { useUrl } from "../streamplace-store";
5
+
6
+ export function useLivestreamWebsocket(src: string) {
7
+ const url = useUrl();
8
+ const handleWebSocketMessages = useHandleWebsocketMessages();
9
+
10
+ let wsUrl = url.replace(/^http\:/, "ws:");
11
+ wsUrl = wsUrl.replace(/^https\:/, "wss:");
12
+
13
+ const ref = useRef<any[]>([]);
14
+ const handle = useRef<NodeJS.Timeout | null>(null);
15
+
16
+ const { readyState } = useWebSocket(`${wsUrl}/api/websocket/${src}`, {
17
+ reconnectInterval: 1000,
18
+ shouldReconnect: () => true,
19
+
20
+ onOpen: () => {
21
+ ref.current = [];
22
+ },
23
+
24
+ onError: (e) => {
25
+ console.log("onError", e);
26
+ },
27
+
28
+ // spamming the redux store with messages causes a zillion re-renders,
29
+ // so we batch them up a bit
30
+ onMessage: (msg) => {
31
+ try {
32
+ const data = JSON.parse(msg.data);
33
+ ref.current.push(data);
34
+ if (handle.current) {
35
+ return;
36
+ }
37
+ handle.current = setTimeout(() => {
38
+ handleWebSocketMessages(ref.current);
39
+ ref.current = [];
40
+ handle.current = null;
41
+ }, 250);
42
+ } catch (e) {
43
+ console.log("onMessage parse error", e);
44
+ }
45
+ },
46
+ });
47
+ }