@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,344 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import { forwardRef } from "react";
3
+ import {
4
+ View as RNView,
5
+ ViewProps as RNViewProps,
6
+ ViewStyle,
7
+ } from "react-native";
8
+ import { borderRadius as radius, spacing } from "../../lib/theme/atoms";
9
+ import { useTheme } from "../../lib/theme/theme";
10
+
11
+ // View variants using class-variance-authority pattern
12
+ const viewVariants = cva("", {
13
+ variants: {
14
+ variant: {
15
+ default: "default",
16
+ card: "card",
17
+ overlay: "overlay",
18
+ surface: "surface",
19
+ container: "container",
20
+ },
21
+ padding: {
22
+ none: "none",
23
+ xs: "xs",
24
+ sm: "sm",
25
+ md: "md",
26
+ lg: "lg",
27
+ xl: "xl",
28
+ },
29
+ margin: {
30
+ none: "none",
31
+ xs: "xs",
32
+ sm: "sm",
33
+ md: "md",
34
+ lg: "lg",
35
+ xl: "xl",
36
+ },
37
+ direction: {
38
+ row: "row",
39
+ column: "column",
40
+ "row-reverse": "row-reverse",
41
+ "column-reverse": "column-reverse",
42
+ },
43
+ align: {
44
+ start: "start",
45
+ center: "center",
46
+ end: "end",
47
+ stretch: "stretch",
48
+ baseline: "baseline",
49
+ },
50
+ justify: {
51
+ start: "start",
52
+ center: "center",
53
+ end: "end",
54
+ between: "between",
55
+ around: "around",
56
+ evenly: "evenly",
57
+ },
58
+ flex: {
59
+ none: "none",
60
+ auto: "auto",
61
+ initial: "initial",
62
+ },
63
+ },
64
+ defaultVariants: {
65
+ variant: "default",
66
+ padding: "none",
67
+ margin: "none",
68
+ direction: "column",
69
+ align: "stretch",
70
+ justify: "start",
71
+ flex: "none",
72
+ },
73
+ });
74
+
75
+ export interface ViewProps
76
+ extends Omit<RNViewProps, "style">,
77
+ Omit<VariantProps<typeof viewVariants>, "flex"> {
78
+ // Style props
79
+ style?: ViewStyle | ViewStyle[];
80
+
81
+ // Convenience props
82
+ fullWidth?: boolean;
83
+ fullHeight?: boolean;
84
+ centered?: boolean;
85
+
86
+ // Background
87
+ backgroundColor?: string;
88
+
89
+ // Border
90
+ borderColor?: string;
91
+ borderWidth?: number;
92
+ borderRadius?: number;
93
+
94
+ // Shadow (web only)
95
+ shadow?: boolean;
96
+
97
+ // Custom flex values
98
+ flex?: number | "none" | "auto" | "initial";
99
+ }
100
+
101
+ export const View = forwardRef<RNView, ViewProps>(
102
+ (
103
+ {
104
+ variant = "default",
105
+ padding = "none",
106
+ margin = "none",
107
+ direction = "column",
108
+ align = "stretch",
109
+ justify = "start",
110
+ flex = "none",
111
+ fullWidth = false,
112
+ fullHeight = false,
113
+ centered = false,
114
+ backgroundColor,
115
+ borderColor,
116
+ borderWidth,
117
+ borderRadius,
118
+ shadow = false,
119
+ style,
120
+ ...props
121
+ },
122
+ ref,
123
+ ) => {
124
+ const { theme } = useTheme();
125
+
126
+ // Map variant to styles
127
+ const variantStyles: ViewStyle = (() => {
128
+ switch (variant) {
129
+ case "card":
130
+ return {
131
+ backgroundColor: theme.colors.card,
132
+ borderRadius: radius.lg,
133
+ shadowColor: "#000",
134
+ shadowOffset: { width: 0, height: 2 },
135
+ shadowOpacity: 0.1,
136
+ shadowRadius: 4,
137
+ elevation: 3,
138
+ };
139
+ case "overlay":
140
+ return {
141
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
142
+ };
143
+ case "surface":
144
+ return {
145
+ backgroundColor: theme.colors.background,
146
+ };
147
+ case "container":
148
+ return {
149
+ backgroundColor: theme.colors.background,
150
+ padding: spacing[4],
151
+ };
152
+ default:
153
+ return {};
154
+ }
155
+ })();
156
+
157
+ // Map padding to numeric values
158
+ const paddingValue = (() => {
159
+ switch (padding) {
160
+ case "xs":
161
+ return spacing[1];
162
+ case "sm":
163
+ return spacing[2];
164
+ case "md":
165
+ return spacing[3];
166
+ case "lg":
167
+ return spacing[4];
168
+ case "xl":
169
+ return spacing[5];
170
+ default:
171
+ return undefined;
172
+ }
173
+ })();
174
+
175
+ // Map margin to numeric values
176
+ const marginValue = (() => {
177
+ switch (margin) {
178
+ case "xs":
179
+ return spacing[1];
180
+ case "sm":
181
+ return spacing[2];
182
+ case "md":
183
+ return spacing[3];
184
+ case "lg":
185
+ return spacing[4];
186
+ case "xl":
187
+ return spacing[5];
188
+ default:
189
+ return undefined;
190
+ }
191
+ })();
192
+
193
+ // Map flex direction
194
+ const flexDirection = (() => {
195
+ switch (direction) {
196
+ case "row":
197
+ return "row";
198
+ case "column":
199
+ return "column";
200
+ case "row-reverse":
201
+ return "row-reverse";
202
+ case "column-reverse":
203
+ return "column-reverse";
204
+ default:
205
+ return "column";
206
+ }
207
+ })() as ViewStyle["flexDirection"];
208
+
209
+ // Map align items
210
+ const alignItems = (() => {
211
+ switch (align) {
212
+ case "start":
213
+ return "flex-start";
214
+ case "center":
215
+ return "center";
216
+ case "end":
217
+ return "flex-end";
218
+ case "stretch":
219
+ return "stretch";
220
+ case "baseline":
221
+ return "baseline";
222
+ default:
223
+ return "stretch";
224
+ }
225
+ })() as ViewStyle["alignItems"];
226
+
227
+ // Map justify content
228
+ const justifyContent = (() => {
229
+ switch (justify) {
230
+ case "start":
231
+ return "flex-start";
232
+ case "center":
233
+ return "center";
234
+ case "end":
235
+ return "flex-end";
236
+ case "between":
237
+ return "space-between";
238
+ case "around":
239
+ return "space-around";
240
+ case "evenly":
241
+ return "space-evenly";
242
+ default:
243
+ return "flex-start";
244
+ }
245
+ })() as ViewStyle["justifyContent"];
246
+
247
+ // Map flex value
248
+ const flexValue = (() => {
249
+ if (typeof flex === "number") {
250
+ return flex;
251
+ }
252
+ switch (flex) {
253
+ case "auto":
254
+ return undefined; // auto is default
255
+ case "initial":
256
+ return 0;
257
+ case "none":
258
+ default:
259
+ return undefined;
260
+ }
261
+ })();
262
+
263
+ const computedStyle: ViewStyle = {
264
+ ...variantStyles,
265
+ ...(paddingValue !== undefined && { padding: paddingValue }),
266
+ ...(marginValue !== undefined && { margin: marginValue }),
267
+ flexDirection,
268
+ alignItems,
269
+ justifyContent,
270
+ ...(flexValue !== undefined && { flex: flexValue }),
271
+ ...(fullWidth && { width: "100%" }),
272
+ ...(fullHeight && { height: "100%" }),
273
+ ...(centered && {
274
+ alignItems: "center",
275
+ justifyContent: "center",
276
+ }),
277
+ ...(backgroundColor && { backgroundColor }),
278
+ ...(borderColor && { borderColor }),
279
+ ...(borderWidth !== undefined && { borderWidth }),
280
+ ...(borderRadius !== undefined && { borderRadius }),
281
+ ...(shadow && {
282
+ shadowColor: "#000",
283
+ shadowOffset: { width: 0, height: 2 },
284
+ shadowOpacity: 0.1,
285
+ shadowRadius: 4,
286
+ elevation: 3,
287
+ }),
288
+ };
289
+
290
+ const finalStyle = Array.isArray(style)
291
+ ? [computedStyle, ...style]
292
+ : [computedStyle, style];
293
+
294
+ return <RNView ref={ref} style={finalStyle} {...props} />;
295
+ },
296
+ );
297
+
298
+ View.displayName = "View";
299
+
300
+ // Convenience components
301
+ export const Card = forwardRef<RNView, Omit<ViewProps, "variant">>(
302
+ (props, ref) => <View ref={ref} variant="card" {...props} />,
303
+ );
304
+
305
+ Card.displayName = "Card";
306
+
307
+ export const Container = forwardRef<RNView, Omit<ViewProps, "variant">>(
308
+ (props, ref) => <View ref={ref} variant="container" {...props} />,
309
+ );
310
+
311
+ Container.displayName = "Container";
312
+
313
+ export const Surface = forwardRef<RNView, Omit<ViewProps, "variant">>(
314
+ (props, ref) => <View ref={ref} variant="surface" {...props} />,
315
+ );
316
+
317
+ Surface.displayName = "Surface";
318
+
319
+ export const Overlay = forwardRef<RNView, Omit<ViewProps, "variant">>(
320
+ (props, ref) => <View ref={ref} variant="overlay" {...props} />,
321
+ );
322
+
323
+ Overlay.displayName = "Overlay";
324
+
325
+ export const Row = forwardRef<RNView, Omit<ViewProps, "direction">>(
326
+ (props, ref) => <View ref={ref} direction="row" {...props} />,
327
+ );
328
+
329
+ Row.displayName = "Row";
330
+
331
+ export const Column = forwardRef<RNView, Omit<ViewProps, "direction">>(
332
+ (props, ref) => <View ref={ref} direction="column" {...props} />,
333
+ );
334
+
335
+ Column.displayName = "Column";
336
+
337
+ export const Center = forwardRef<RNView, ViewProps>((props, ref) => (
338
+ <View ref={ref} centered {...props} />
339
+ ));
340
+
341
+ Center.displayName = "Center";
342
+
343
+ // Export view variants for external use
344
+ export { viewVariants };
@@ -0,0 +1,9 @@
1
+ // barrel file :)
2
+ export * from "./useAvatars";
3
+ export * from "./useCameraToggle";
4
+ export * from "./useKeyboard";
5
+ export * from "./useKeyboardSlide";
6
+ export * from "./useLivestreamInfo";
7
+ export * from "./useOuterAndInnerDimensions";
8
+ export * from "./usePlayerDimensions";
9
+ export * from "./useSegmentTiming";
@@ -0,0 +1,44 @@
1
+ import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { usePDSAgent } from "../streamplace-store/xrpc";
4
+
5
+ export function useAvatars(
6
+ dids: string[],
7
+ ): Record<string, ProfileViewDetailed> {
8
+ let agent = usePDSAgent();
9
+ const [profiles, setProfiles] = useState<Record<string, ProfileViewDetailed>>(
10
+ {},
11
+ );
12
+ const inFlight = useRef<Set<string>>(new Set());
13
+
14
+ const missingDids = useMemo(
15
+ () =>
16
+ dids.filter((did) => !(did in profiles) && !inFlight.current.has(did)),
17
+ [dids, profiles],
18
+ );
19
+
20
+ useEffect(() => {
21
+ if (missingDids.length === 0 || !agent) return;
22
+ const toFetch = missingDids.slice(0, 25);
23
+ toFetch.forEach((did) => inFlight.current.add(did));
24
+
25
+ const fetchProfiles = async () => {
26
+ try {
27
+ const result = await agent.getProfiles({ actors: toFetch });
28
+ const newProfiles: Record<string, ProfileViewDetailed> = {};
29
+ result.data.profiles.forEach((p) => {
30
+ newProfiles[p.did] = p;
31
+ });
32
+ setProfiles((prev) => ({ ...prev, ...newProfiles }));
33
+ } catch (e) {
34
+ console.error("Failed to fetch profiles", e);
35
+ } finally {
36
+ toFetch.forEach((did) => inFlight.current.delete(did));
37
+ }
38
+ };
39
+
40
+ fetchProfiles();
41
+ }, [missingDids, agent]);
42
+
43
+ return profiles;
44
+ }
@@ -0,0 +1,12 @@
1
+ import { usePlayerStore } from "../player-store";
2
+
3
+ export function useCameraToggle() {
4
+ const ingestCamera = usePlayerStore((x) => x.ingestCamera);
5
+ const setIngestCamera = usePlayerStore((x) => x.setIngestCamera);
6
+
7
+ const doSetIngestCamera = () => {
8
+ setIngestCamera(ingestCamera === "user" ? "environment" : "user");
9
+ };
10
+
11
+ return { ingestCamera, doSetIngestCamera };
12
+ }
@@ -0,0 +1,41 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Keyboard } from "react-native";
3
+
4
+ export function useKeyboard() {
5
+ const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
6
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
7
+ useEffect(() => {
8
+ const willShowSubscription = Keyboard.addListener(
9
+ "keyboardWillShow",
10
+ (e) => {
11
+ // setIsKeyboardVisible(true);
12
+ setKeyboardHeight(e.endCoordinates.height);
13
+ console.log("keyboardWillShow", e.endCoordinates.height);
14
+ },
15
+ );
16
+ const willHideSubscription = Keyboard.addListener(
17
+ "keyboardWillHide",
18
+ (e) => {
19
+ // setIsKeyboardVisible(false);
20
+ setKeyboardHeight(0);
21
+ console.log("keyboardWillHide", e.endCoordinates.height);
22
+ },
23
+ );
24
+ const showSubscription = Keyboard.addListener("keyboardDidShow", (e) => {
25
+ setIsKeyboardVisible(true);
26
+ setKeyboardHeight(e.endCoordinates.height);
27
+ console.log("keyboardDidShow", e.endCoordinates.height);
28
+ });
29
+ const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
30
+ setIsKeyboardVisible(false);
31
+ setKeyboardHeight(0);
32
+ });
33
+ return () => {
34
+ showSubscription.remove();
35
+ hideSubscription.remove();
36
+ willShowSubscription.remove();
37
+ };
38
+ }, []);
39
+
40
+ return { isKeyboardVisible, keyboardHeight };
41
+ }
@@ -0,0 +1,12 @@
1
+ import { useKeyboard } from "../hooks/useKeyboard";
2
+ import { useOuterAndInnerDimensions } from "../hooks/useOuterAndInnerDimensions";
3
+
4
+ export function useKeyboardSlide() {
5
+ const { keyboardHeight } = useKeyboard();
6
+ const { outerHeight, innerHeight } = useOuterAndInnerDimensions();
7
+ let slideKeyboard = 0;
8
+ if (keyboardHeight > 0) {
9
+ slideKeyboard = -keyboardHeight + (outerHeight - innerHeight);
10
+ }
11
+ return { keyboardHeight, slideKeyboard };
12
+ }
@@ -0,0 +1,67 @@
1
+ import { useState } from "react";
2
+ import { useLivestreamStore } from "../livestream-store";
3
+ import { usePlayerStore } from "../player-store";
4
+ import { useCreateStreamRecord } from "../streamplace-store";
5
+
6
+ export function useLivestreamInfo() {
7
+ const ingest = usePlayerStore((x) => x.ingestConnectionState);
8
+ const profile = useLivestreamStore((x) => x.profile);
9
+ const ingestStarting = usePlayerStore((x) => x.ingestStarting);
10
+ const setIngestStarting = usePlayerStore((x) => x.setIngestStarting);
11
+ const setIngestLive = usePlayerStore((x) => x.setIngestLive);
12
+
13
+ const createStreamRecord = useCreateStreamRecord();
14
+
15
+ const [title, setTitle] = useState<string>("");
16
+ const [showCountdown, setShowCountdown] = useState(false);
17
+ const [recordSubmitted, setRecordSubmitted] = useState(false);
18
+
19
+ const handleSubmit = async () => {
20
+ try {
21
+ if (title !== "") {
22
+ setRecordSubmitted(true);
23
+ await createStreamRecord(title);
24
+ }
25
+ } catch (error) {
26
+ console.error("Error creating livestream:", error);
27
+ throw new Error("Failed to create livestream record");
28
+ } finally {
29
+ setRecordSubmitted(false);
30
+ }
31
+ };
32
+
33
+ const toggleGoLive = (
34
+ keyboardHeight?: number,
35
+ closeKeyboard?: () => void,
36
+ ) => {
37
+ if (!ingestStarting) {
38
+ // Optionally close keyboard if provided
39
+ if (closeKeyboard) closeKeyboard();
40
+ setShowCountdown(true);
41
+ setIngestStarting(true);
42
+ setIngestLive(true);
43
+ // wait ~3 seconds before announcing
44
+ setTimeout(() => {
45
+ handleSubmit();
46
+ }, 3000);
47
+ } else {
48
+ setIngestStarting(false);
49
+ setIngestLive(false);
50
+ }
51
+ };
52
+
53
+ return {
54
+ ingest,
55
+ profile,
56
+ title,
57
+ setTitle,
58
+ showCountdown,
59
+ setShowCountdown,
60
+ recordSubmitted,
61
+ setRecordSubmitted,
62
+ ingestStarting,
63
+ setIngestStarting,
64
+ handleSubmit,
65
+ toggleGoLive,
66
+ };
67
+ }
@@ -0,0 +1,32 @@
1
+ import { useCallback, useState } from "react";
2
+ import { LayoutChangeEvent } from "react-native";
3
+
4
+ export function useOuterAndInnerDimensions() {
5
+ const [outerDimensions, setOuterDimensions] = useState({
6
+ width: 0,
7
+ height: 0,
8
+ });
9
+ const [innerDimensions, setInnerDimensions] = useState({
10
+ width: 0,
11
+ height: 0,
12
+ });
13
+
14
+ const onOuterLayout = useCallback((event: LayoutChangeEvent) => {
15
+ const { width, height } = event.nativeEvent.layout;
16
+ setOuterDimensions({ width, height });
17
+ }, []);
18
+
19
+ const onInnerLayout = useCallback((event: LayoutChangeEvent) => {
20
+ const { width, height } = event.nativeEvent.layout;
21
+ setInnerDimensions({ width, height });
22
+ }, []);
23
+
24
+ return {
25
+ outerWidth: outerDimensions.width,
26
+ outerHeight: outerDimensions.height,
27
+ innerWidth: innerDimensions.width,
28
+ innerHeight: innerDimensions.height,
29
+ onOuterLayout,
30
+ onInnerLayout,
31
+ };
32
+ }
@@ -0,0 +1,23 @@
1
+ import { Dimensions } from "react-native";
2
+ import { usePlayerStore } from "../player-store";
3
+
4
+ /**
5
+ * usePlayerDimensions
6
+ * Returns player and device dimensions, and whether the player aspect ratio is greater than the device's.
7
+ */
8
+ export function usePlayerDimensions() {
9
+ const { width, height } = Dimensions.get("window");
10
+ const pHeight = Number(usePlayerStore((x) => x.playerHeight)) || 0;
11
+ const pWidth = Number(usePlayerStore((x) => x.playerWidth)) || 0;
12
+
13
+ const isPlayerRatioGreater =
14
+ pHeight > 0 && pWidth > 0 ? pWidth / pHeight > width / height : false;
15
+
16
+ return {
17
+ width,
18
+ height,
19
+ pWidth,
20
+ pHeight,
21
+ isPlayerRatioGreater,
22
+ };
23
+ }
@@ -0,0 +1,88 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useLivestreamStore } from "../livestream-store";
3
+
4
+ export type ConnectionQuality = "good" | "degraded" | "poor";
5
+
6
+ function getLiveConnectionQuality(
7
+ timeBetweenSegments: number | null,
8
+ range: number | null,
9
+ numOfSegments: number = 1,
10
+ ): ConnectionQuality {
11
+ if (timeBetweenSegments === null || range === null) return "poor";
12
+
13
+ if (timeBetweenSegments <= 1500 && range <= (1500 * 60) / numOfSegments) {
14
+ return "good";
15
+ }
16
+ if (timeBetweenSegments <= 3000 && range <= (3000 * 60) / numOfSegments) {
17
+ return "degraded";
18
+ }
19
+ return "poor";
20
+ }
21
+
22
+ export function useSegmentTiming() {
23
+ const latestSegment = useLivestreamStore((x) => x.segment);
24
+ const [segmentDeltas, setSegmentDeltas] = useState<number[]>([]);
25
+ const prevSegmentRef = useRef<any>();
26
+ const prevTimestampRef = useRef<number | null>(null);
27
+
28
+ // Dummy state to force update every second
29
+ const [, setNow] = useState(Date.now());
30
+
31
+ useEffect(() => {
32
+ const interval = setInterval(() => {
33
+ setNow(Date.now());
34
+ }, 1000);
35
+ return () => clearInterval(interval);
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ if (latestSegment && prevSegmentRef.current !== latestSegment) {
40
+ const now = Date.now();
41
+ if (prevTimestampRef.current !== null) {
42
+ const delta = now - prevTimestampRef.current;
43
+ // Only store the last 25 deltas
44
+ setSegmentDeltas((prev) => [...prev, delta].slice(-25));
45
+ }
46
+ prevTimestampRef.current = now;
47
+ prevSegmentRef.current = latestSegment;
48
+ }
49
+ }, [latestSegment]);
50
+
51
+ // The most recent time between segments
52
+ const timeBetweenSegments =
53
+ segmentDeltas.length > 0
54
+ ? segmentDeltas[segmentDeltas.length - 1]
55
+ : prevTimestampRef.current
56
+ ? Date.now() - prevTimestampRef.current
57
+ : null;
58
+
59
+ // Calculate mean and range of deltas
60
+ const mean =
61
+ segmentDeltas.length > 0
62
+ ? Math.round(
63
+ segmentDeltas.reduce((acc, curr) => acc + curr, 0) /
64
+ segmentDeltas.length,
65
+ )
66
+ : null;
67
+
68
+ const range =
69
+ segmentDeltas.length > 0
70
+ ? Math.max(...segmentDeltas) - Math.min(...segmentDeltas)
71
+ : null;
72
+
73
+ let to_ret = {
74
+ segmentDeltas,
75
+ timeBetweenSegments,
76
+ mean,
77
+ range,
78
+ connectionQuality: "poor",
79
+ };
80
+
81
+ to_ret.connectionQuality = getLiveConnectionQuality(
82
+ timeBetweenSegments,
83
+ range,
84
+ segmentDeltas.length,
85
+ );
86
+
87
+ return to_ret;
88
+ }