@streamplace/components 0.9.0 → 0.9.4

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 (150) hide show
  1. package/dist/components/chat/chat-box.d.ts.map +1 -1
  2. package/dist/components/chat/chat-box.js +90 -34
  3. package/dist/components/chat/chat-box.js.map +1 -1
  4. package/dist/components/chat/chat-message.d.ts +4 -0
  5. package/dist/components/chat/chat-message.d.ts.map +1 -1
  6. package/dist/components/chat/chat-message.js +3 -2
  7. package/dist/components/chat/chat-message.js.map +1 -1
  8. package/dist/components/chat/chat.d.ts.map +1 -1
  9. package/dist/components/chat/chat.js +56 -3
  10. package/dist/components/chat/chat.js.map +1 -1
  11. package/dist/components/chat/emoji-suggestions.d.ts.map +1 -1
  12. package/dist/components/chat/emoji-suggestions.js +11 -11
  13. package/dist/components/chat/emoji-suggestions.js.map +1 -1
  14. package/dist/components/chat/mention-suggestions.d.ts.map +1 -1
  15. package/dist/components/chat/mention-suggestions.js +20 -19
  16. package/dist/components/chat/mention-suggestions.js.map +1 -1
  17. package/dist/components/chat/mod-view.d.ts.map +1 -1
  18. package/dist/components/chat/mod-view.js +1 -9
  19. package/dist/components/chat/mod-view.js.map +1 -1
  20. package/dist/components/chat/system-message.d.ts +5 -1
  21. package/dist/components/chat/system-message.d.ts.map +1 -1
  22. package/dist/components/chat/system-message.js +4 -4
  23. package/dist/components/chat/system-message.js.map +1 -1
  24. package/dist/components/mobile-player/shared.d.ts +1 -1
  25. package/dist/components/mobile-player/shared.d.ts.map +1 -1
  26. package/dist/components/mobile-player/shared.js +11 -10
  27. package/dist/components/mobile-player/shared.js.map +1 -1
  28. package/dist/components/mobile-player/ui/report-modal.d.ts.map +1 -1
  29. package/dist/components/mobile-player/ui/report-modal.js +1 -1
  30. package/dist/components/mobile-player/ui/report-modal.js.map +1 -1
  31. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts +1 -1
  32. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
  33. package/dist/components/mobile-player/ui/viewer-context-menu.js +60 -43
  34. package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
  35. package/dist/components/mobile-player/ui/viewer-loading-overlay.d.ts.map +1 -1
  36. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +0 -1
  37. package/dist/components/mobile-player/ui/viewer-loading-overlay.js.map +1 -1
  38. package/dist/components/stream-notification/index.d.ts +3 -0
  39. package/dist/components/stream-notification/index.d.ts.map +1 -0
  40. package/dist/components/stream-notification/index.js +9 -0
  41. package/dist/components/stream-notification/index.js.map +1 -0
  42. package/dist/components/stream-notification/stream-notification-manager.d.ts +36 -0
  43. package/dist/components/stream-notification/stream-notification-manager.d.ts.map +1 -0
  44. package/dist/components/stream-notification/stream-notification-manager.js +96 -0
  45. package/dist/components/stream-notification/stream-notification-manager.js.map +1 -0
  46. package/dist/components/stream-notification/stream-notification.d.ts +5 -0
  47. package/dist/components/stream-notification/stream-notification.d.ts.map +1 -0
  48. package/dist/components/stream-notification/stream-notification.js +146 -0
  49. package/dist/components/stream-notification/stream-notification.js.map +1 -0
  50. package/dist/components/stream-notification/teleport-notification.d.ts +8 -0
  51. package/dist/components/stream-notification/teleport-notification.d.ts.map +1 -0
  52. package/dist/components/stream-notification/teleport-notification.js +116 -0
  53. package/dist/components/stream-notification/teleport-notification.js.map +1 -0
  54. package/dist/components/ui/button.d.ts +1 -1
  55. package/dist/components/ui/button.d.ts.map +1 -1
  56. package/dist/components/ui/button.js +7 -0
  57. package/dist/components/ui/button.js.map +1 -1
  58. package/dist/components/ui/dialog.d.ts +2 -2
  59. package/dist/components/ui/dropdown.d.ts +4 -0
  60. package/dist/components/ui/dropdown.d.ts.map +1 -1
  61. package/dist/components/ui/dropdown.js +41 -15
  62. package/dist/components/ui/dropdown.js.map +1 -1
  63. package/dist/components/ui/index.d.ts +1 -0
  64. package/dist/components/ui/index.d.ts.map +1 -1
  65. package/dist/components/ui/index.js +1 -0
  66. package/dist/components/ui/index.js.map +1 -1
  67. package/dist/components/ui/portal.d.ts +2 -0
  68. package/dist/components/ui/portal.d.ts.map +1 -0
  69. package/dist/components/ui/portal.js +5 -0
  70. package/dist/components/ui/portal.js.map +1 -0
  71. package/dist/components/ui/portal.web.d.ts +11 -0
  72. package/dist/components/ui/portal.web.d.ts.map +1 -0
  73. package/dist/components/ui/portal.web.js +22 -0
  74. package/dist/components/ui/portal.web.js.map +1 -0
  75. package/dist/components/ui/resizeable.d.ts +2 -1
  76. package/dist/components/ui/resizeable.d.ts.map +1 -1
  77. package/dist/components/ui/resizeable.js +68 -26
  78. package/dist/components/ui/resizeable.js.map +1 -1
  79. package/dist/components/ui/text.d.ts +1 -1
  80. package/dist/components/ui/view.d.ts +3 -3
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +2 -0
  84. package/dist/index.js.map +1 -1
  85. package/dist/lib/slash-commands/teleport.d.ts +4 -0
  86. package/dist/lib/slash-commands/teleport.d.ts.map +1 -0
  87. package/dist/lib/slash-commands/teleport.js +110 -0
  88. package/dist/lib/slash-commands/teleport.js.map +1 -0
  89. package/dist/lib/slash-commands.d.ts +16 -0
  90. package/dist/lib/slash-commands.d.ts.map +1 -0
  91. package/dist/lib/slash-commands.js +46 -0
  92. package/dist/lib/slash-commands.js.map +1 -0
  93. package/dist/lib/stream-notifications.d.ts +13 -0
  94. package/dist/lib/stream-notifications.d.ts.map +1 -0
  95. package/dist/lib/stream-notifications.js +46 -0
  96. package/dist/lib/stream-notifications.js.map +1 -0
  97. package/dist/lib/system-messages.d.ts +4 -8
  98. package/dist/lib/system-messages.d.ts.map +1 -1
  99. package/dist/lib/system-messages.js +38 -2
  100. package/dist/lib/system-messages.js.map +1 -1
  101. package/dist/lib/theme/atoms.d.ts +193 -193
  102. package/dist/livestream-provider/index.d.ts +7 -2
  103. package/dist/livestream-provider/index.d.ts.map +1 -1
  104. package/dist/livestream-provider/index.js +72 -4
  105. package/dist/livestream-provider/index.js.map +1 -1
  106. package/dist/livestream-store/livestream-state.d.ts +4 -1
  107. package/dist/livestream-store/livestream-state.d.ts.map +1 -1
  108. package/dist/livestream-store/livestream-store.d.ts.map +1 -1
  109. package/dist/livestream-store/livestream-store.js +3 -0
  110. package/dist/livestream-store/livestream-store.js.map +1 -1
  111. package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
  112. package/dist/livestream-store/websocket-consumer.js +30 -43
  113. package/dist/livestream-store/websocket-consumer.js.map +1 -1
  114. package/dist/streamplace-store/index.d.ts +1 -0
  115. package/dist/streamplace-store/index.d.ts.map +1 -1
  116. package/dist/streamplace-store/index.js +1 -0
  117. package/dist/streamplace-store/index.js.map +1 -1
  118. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  119. package/package.json +4 -2
  120. package/src/components/chat/chat-box.tsx +126 -53
  121. package/src/components/chat/chat-message.tsx +1 -1
  122. package/src/components/chat/chat.tsx +79 -5
  123. package/src/components/chat/emoji-suggestions.tsx +27 -25
  124. package/src/components/chat/mention-suggestions.tsx +36 -33
  125. package/src/components/chat/mod-view.tsx +2 -13
  126. package/src/components/chat/system-message.tsx +14 -5
  127. package/src/components/mobile-player/shared.tsx +2 -1
  128. package/src/components/mobile-player/ui/report-modal.tsx +2 -0
  129. package/src/components/mobile-player/ui/viewer-context-menu.tsx +192 -166
  130. package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +0 -1
  131. package/src/components/stream-notification/index.ts +5 -0
  132. package/src/components/stream-notification/stream-notification-manager.ts +140 -0
  133. package/src/components/stream-notification/stream-notification.tsx +227 -0
  134. package/src/components/stream-notification/teleport-notification.tsx +187 -0
  135. package/src/components/ui/button.tsx +7 -0
  136. package/src/components/ui/dropdown.tsx +96 -26
  137. package/src/components/ui/index.ts +1 -0
  138. package/src/components/ui/portal.tsx +1 -0
  139. package/src/components/ui/portal.web.tsx +37 -0
  140. package/src/components/ui/resizeable.tsx +89 -35
  141. package/src/index.tsx +3 -0
  142. package/src/lib/slash-commands/teleport.ts +136 -0
  143. package/src/lib/slash-commands.ts +65 -0
  144. package/src/lib/stream-notifications.ts +51 -0
  145. package/src/lib/system-messages.ts +52 -2
  146. package/src/livestream-provider/index.tsx +106 -3
  147. package/src/livestream-store/livestream-state.tsx +4 -0
  148. package/src/livestream-store/livestream-store.tsx +3 -0
  149. package/src/livestream-store/websocket-consumer.tsx +35 -54
  150. package/src/streamplace-store/index.tsx +1 -0
@@ -173,6 +173,7 @@ export const ReportModal: React.FC<ReportModalProps> = ({
173
173
  </View>
174
174
  <DialogFooter>
175
175
  <Button
176
+ width="min"
176
177
  variant="secondary"
177
178
  onPress={handleCancel}
178
179
  disabled={isSubmitting}
@@ -180,6 +181,7 @@ export const ReportModal: React.FC<ReportModalProps> = ({
180
181
  <Text>Cancel</Text>
181
182
  </Button>
182
183
  <Button
184
+ width="min"
183
185
  variant="primary"
184
186
  onPress={handleSubmit}
185
187
  disabled={!selectedReason || isSubmitting}
@@ -1,6 +1,12 @@
1
1
  import { useRootContext } from "@rn-primitives/dropdown-menu";
2
- import { Menu } from "lucide-react-native";
2
+ import { Cog } from "lucide-react-native";
3
+ import { useState } from "react";
3
4
  import { Image, Linking, Platform, Pressable, View } from "react-native";
5
+ import Animated, {
6
+ Easing,
7
+ useAnimatedStyle,
8
+ withTiming,
9
+ } from "react-native-reanimated";
4
10
  import {
5
11
  ContentRights,
6
12
  ContentWarnings,
@@ -10,7 +16,6 @@ import {
10
16
  useLivestreamInfo,
11
17
  zero,
12
18
  } from "../../..";
13
- import { colors } from "../../../lib/theme";
14
19
  import { useLivestreamStore } from "../../../livestream-store";
15
20
  import { PlayerProtocol, usePlayerStore } from "../../../player-store/";
16
21
  import { useGraphManager } from "../../../streamplace-store/graph";
@@ -21,7 +26,6 @@ import {
21
26
  DropdownMenuGroup,
22
27
  DropdownMenuInfo,
23
28
  DropdownMenuItem,
24
- DropdownMenuPortal,
25
29
  DropdownMenuRadioGroup,
26
30
  DropdownMenuRadioItem,
27
31
  DropdownMenuSeparator,
@@ -31,13 +35,15 @@ import {
31
35
  DropdownMenuTrigger,
32
36
  ResponsiveDropdownMenuContent,
33
37
  Text,
38
+ useTheme,
34
39
  } from "../../ui";
35
40
 
36
41
  export function ContextMenu({
37
42
  dropdownPortalContainer,
38
43
  }: {
39
- dropdownPortalContainer?: any;
44
+ dropdownPortalContainer?: string;
40
45
  }) {
46
+ const th = useTheme();
41
47
  const quality = usePlayerStore((x) => x.selectedRendition);
42
48
  const setQuality = usePlayerStore((x) => x.setSelectedRendition);
43
49
  const qualities = useLivestreamStore((x) => x.renditions);
@@ -58,6 +64,8 @@ export function ContextMenu({
58
64
  const ls = useLivestreamStore((x) => x.livestream);
59
65
  const segment = useLivestreamStore((x) => x.segment);
60
66
 
67
+ const [isOpen, setIsOpen] = useState(false);
68
+
61
69
  // Get content rights from the latest segment
62
70
  const contentRights = segment?.contentRights;
63
71
  const contentWarnings = segment?.contentWarnings?.warnings || [];
@@ -73,188 +81,206 @@ export function ContextMenu({
73
81
  const isMobile = Platform.OS === "ios" || Platform.OS === "android";
74
82
 
75
83
  // dummy portal for mobile
76
- const Portal = isMobile ? View : DropdownMenuPortal;
84
+ //const Portal: typeof DropdownMenuPortal = DropdownMenu;
77
85
 
78
86
  const DropdownMenuContent = ResponsiveDropdownMenuContent;
79
87
 
88
+ const iconRotate = useAnimatedStyle(() => {
89
+ return {
90
+ transform: [
91
+ {
92
+ rotateZ: withTiming(isOpen ? "240deg" : "0deg", {
93
+ duration: 650,
94
+ easing: Easing.out(Easing.ease),
95
+ }),
96
+ },
97
+ ],
98
+ };
99
+ });
100
+
101
+ // rerender when dropdown portal container changes so we swap portals 'seamlessly'
80
102
  return (
81
- <DropdownMenu>
103
+ <DropdownMenu onOpenChange={setIsOpen} key={dropdownPortalContainer}>
82
104
  <DropdownMenuTrigger>
83
- <Menu color={colors.gray[200]} />
105
+ <Animated.View style={[iconRotate]}>
106
+ <Cog color={th.theme.colors.foreground} />
107
+ </Animated.View>
84
108
  </DropdownMenuTrigger>
85
- <Portal container={dropdownPortalContainer}>
86
- <DropdownMenuContent side="top" align="end">
87
- {Platform.OS !== "web" && (
88
- <DropdownMenuGroup title="Streamer">
89
- <View
90
- style={[
91
- zero.layout.flex.row,
92
- zero.layout.flex.center,
93
- zero.gap.all[3],
94
- { flex: 1, minWidth: 0 },
95
- ]}
96
- >
97
- {profile?.did && avatars[profile?.did]?.avatar && (
98
- <Image
99
- key="avatar"
100
- source={{
101
- uri: avatars[profile?.did]?.avatar,
109
+ <DropdownMenuContent
110
+ side="top"
111
+ align="end"
112
+ portalHost={dropdownPortalContainer}
113
+ >
114
+ {Platform.OS !== "web" && (
115
+ <DropdownMenuGroup title="Streamer">
116
+ <View
117
+ style={[
118
+ zero.layout.flex.row,
119
+ zero.layout.flex.center,
120
+ zero.gap.all[3],
121
+ { flex: 1, minWidth: 0 },
122
+ ]}
123
+ >
124
+ {profile?.did && avatars[profile?.did]?.avatar && (
125
+ <Image
126
+ key="avatar"
127
+ source={{
128
+ uri: avatars[profile?.did]?.avatar,
129
+ }}
130
+ style={{ width: 42, height: 42, borderRadius: 999 }}
131
+ resizeMode="cover"
132
+ />
133
+ )}
134
+ <View style={{ flex: 1, minWidth: 0 }}>
135
+ <View
136
+ style={[
137
+ zero.layout.flex.row,
138
+ zero.layout.flex.alignCenter,
139
+ zero.gap.all[2],
140
+ ]}
141
+ >
142
+ <Pressable
143
+ onPress={() => {
144
+ if (profile?.handle) {
145
+ const url = `https://bsky.app/profile/${formatHandle(profile)}`;
146
+ Linking.openURL(url);
147
+ }
102
148
  }}
103
- style={{ width: 42, height: 42, borderRadius: 999 }}
104
- resizeMode="cover"
105
- />
106
- )}
107
- <View style={{ flex: 1, minWidth: 0 }}>
108
- <View
109
- style={[
110
- zero.layout.flex.row,
111
- zero.layout.flex.alignCenter,
112
- zero.gap.all[2],
113
- ]}
114
149
  >
115
- <Pressable
116
- onPress={() => {
117
- if (profile?.handle) {
118
- const url = `https://bsky.app/profile/${formatHandle(profile)}`;
119
- Linking.openURL(url);
120
- }
121
- }}
122
- >
123
- <Text>{profile && formatHandleWithAt(profile)}</Text>
124
- </Pressable>
125
- {/*{did && profile && (
150
+ <Text>{profile && formatHandleWithAt(profile)}</Text>
151
+ </Pressable>
152
+ {/*{did && profile && (
126
153
  <FollowButton streamerDID={profile?.did} currentUserDID={did} />
127
154
  )}*/}
128
- </View>
129
- <Text
130
- color="muted"
131
- size="sm"
132
- numberOfLines={2}
133
- ellipsizeMode="tail"
134
- >
135
- {ls?.record.title || "Stream Title"}
136
- </Text>
137
155
  </View>
138
- </View>
139
- <DropdownMenuSeparator />
140
- <DropdownMenuItem
141
- disabled={graphManager.isLoading || !profile?.did}
142
- onPress={async () => {
143
- try {
144
- if (graphManager.isFollowing) {
145
- await graphManager.unfollow();
146
- } else {
147
- await graphManager.follow();
148
- }
149
- } catch (err) {
150
- console.error("Follow/unfollow error:", err);
151
- }
152
- }}
153
- >
154
156
  <Text
155
- color={graphManager.isFollowing ? "destructive" : "default"}
157
+ color="muted"
158
+ size="sm"
159
+ numberOfLines={2}
160
+ ellipsizeMode="tail"
156
161
  >
157
- {graphManager.isLoading
158
- ? "Loading..."
159
- : graphManager.isFollowing
160
- ? "Unfollow"
161
- : "Follow"}
162
+ {ls?.record.title || "Stream Title"}
162
163
  </Text>
163
- </DropdownMenuItem>
164
- <DropdownMenuSeparator />
165
- <DropdownMenuItem
166
- onPress={() => {
167
- if (profile?.handle) {
168
- const url = `https://bsky.app/profile/${formatHandle(profile)}`;
169
- Linking.openURL(url);
164
+ </View>
165
+ </View>
166
+ <DropdownMenuSeparator />
167
+ <DropdownMenuItem
168
+ disabled={graphManager.isLoading || !profile?.did}
169
+ onPress={async () => {
170
+ try {
171
+ if (graphManager.isFollowing) {
172
+ await graphManager.unfollow();
173
+ } else {
174
+ await graphManager.follow();
170
175
  }
171
- }}
176
+ } catch (err) {
177
+ console.error("Follow/unfollow error:", err);
178
+ }
179
+ }}
180
+ >
181
+ <Text
182
+ color={graphManager.isFollowing ? "destructive" : "default"}
172
183
  >
173
- <Text>View Profile on Bluesky</Text>
174
- </DropdownMenuItem>
175
- </DropdownMenuGroup>
176
- )}
177
-
178
- <DropdownMenuGroup>
179
- <DropdownMenuSub>
180
- <DropdownMenuSubTrigger subMenuTitle="Quality">
181
- <View
182
- style={[
183
- zero.flex.values[1],
184
- isMobile ? zero.layout.flex.row : zero.layout.flex.column,
185
- zero.layout.flex.spaceBetween,
186
- zero.pr[4],
187
- ]}
188
- >
189
- <Text>Quality</Text>
190
- <Text muted size={isMobile ? "base" : "sm"}>
191
- {quality === "source" ? "Source" : quality},{" "}
192
- {lowLatency ? "Low Latency" : ""}
193
- </Text>
194
- </View>
195
- </DropdownMenuSubTrigger>
196
- <DropdownMenuSubContent>
197
- <DropdownMenuGroup title="Resolution">
198
- <DropdownMenuRadioGroup
199
- value={quality}
200
- onValueChange={setQuality}
201
- >
202
- <DropdownMenuRadioItem value="source">
203
- <Text>Source (Original Quality)</Text>
204
- </DropdownMenuRadioItem>
205
- {qualities.map((r) => (
206
- <DropdownMenuRadioItem key={r.name} value={r.name}>
207
- <Text>{r.name}</Text>
208
- </DropdownMenuRadioItem>
209
- ))}
210
- </DropdownMenuRadioGroup>
211
- </DropdownMenuGroup>
212
- <DropdownMenuGroup>
213
- <DropdownMenuCheckboxItem
214
- checked={lowLatency}
215
- onCheckedChange={() => setLowLatency(!lowLatency)}
216
- >
217
- <Text>Low Latency</Text>
218
- </DropdownMenuCheckboxItem>
219
- </DropdownMenuGroup>
220
- <DropdownMenuInfo description="Reduces the delay between video and chat for a more real-time experience." />
221
- </DropdownMenuSubContent>
222
- </DropdownMenuSub>
223
- </DropdownMenuGroup>
224
- <DropdownMenuGroup title="Advanced">
225
- <DropdownMenuCheckboxItem
226
- checked={debugInfo}
227
- onCheckedChange={() => setShowDebugInfo(!debugInfo)}
184
+ {graphManager.isLoading
185
+ ? "Loading..."
186
+ : graphManager.isFollowing
187
+ ? "Unfollow"
188
+ : "Follow"}
189
+ </Text>
190
+ </DropdownMenuItem>
191
+ <DropdownMenuSeparator />
192
+ <DropdownMenuItem
193
+ onPress={() => {
194
+ if (profile?.handle) {
195
+ const url = `https://bsky.app/profile/${formatHandle(profile)}`;
196
+ Linking.openURL(url);
197
+ }
198
+ }}
228
199
  >
229
- <Text>Show Debug Info</Text>
230
- </DropdownMenuCheckboxItem>
200
+ <Text>View Profile on Bluesky</Text>
201
+ </DropdownMenuItem>
231
202
  </DropdownMenuGroup>
232
- <DropdownMenuGroup title="Report">
233
- <ReportButton
234
- livestream={livestream}
235
- setReportModalOpen={setReportModalOpen}
236
- setReportSubject={setReportSubject}
237
- />
238
- </DropdownMenuGroup>
239
- <View style={[pt[3], px[2], gap.all[2]]}>
240
- {contentWarnings && contentWarnings.length > 0 && (
241
- <View style={[gap.all[1]]}>
242
- <Text size="base" color="muted">
243
- Stream may contain
203
+ )}
204
+
205
+ <DropdownMenuGroup>
206
+ <DropdownMenuSub>
207
+ <DropdownMenuSubTrigger subMenuTitle="Quality">
208
+ <View
209
+ style={[
210
+ zero.flex.values[1],
211
+ isMobile ? zero.layout.flex.row : zero.layout.flex.column,
212
+ zero.layout.flex.spaceBetween,
213
+ zero.pr[4],
214
+ ]}
215
+ >
216
+ <Text>Quality</Text>
217
+ <Text muted size={isMobile ? "base" : "sm"}>
218
+ {quality === "source" ? "Source" : quality},{" "}
219
+ {lowLatency ? "Low Latency" : ""}
244
220
  </Text>
245
- <ContentWarnings warnings={contentWarnings} compact={true} />
246
221
  </View>
247
- )}
248
- {contentRights && Object.keys(contentRights).length > 0 && (
249
- <ContentRights
250
- contentRights={contentRights}
251
- size="xs"
252
- color="muted"
253
- />
254
- )}
255
- </View>
256
- </DropdownMenuContent>
257
- </Portal>
222
+ </DropdownMenuSubTrigger>
223
+ <DropdownMenuSubContent portalHost={dropdownPortalContainer}>
224
+ <DropdownMenuGroup title="Resolution">
225
+ <DropdownMenuRadioGroup
226
+ value={quality}
227
+ onValueChange={setQuality}
228
+ >
229
+ <DropdownMenuRadioItem value="source">
230
+ <Text>Source (Original Quality)</Text>
231
+ </DropdownMenuRadioItem>
232
+ {qualities.map((r) => (
233
+ <DropdownMenuRadioItem key={r.name} value={r.name}>
234
+ <Text>{r.name}</Text>
235
+ </DropdownMenuRadioItem>
236
+ ))}
237
+ </DropdownMenuRadioGroup>
238
+ </DropdownMenuGroup>
239
+ <DropdownMenuGroup>
240
+ <DropdownMenuCheckboxItem
241
+ checked={lowLatency}
242
+ onCheckedChange={() => setLowLatency(!lowLatency)}
243
+ >
244
+ <Text>Low Latency</Text>
245
+ </DropdownMenuCheckboxItem>
246
+ </DropdownMenuGroup>
247
+ <DropdownMenuInfo description="Reduces the delay between video and chat for a more real-time experience." />
248
+ </DropdownMenuSubContent>
249
+ </DropdownMenuSub>
250
+ </DropdownMenuGroup>
251
+ <DropdownMenuGroup title="Advanced">
252
+ <DropdownMenuCheckboxItem
253
+ checked={debugInfo}
254
+ onCheckedChange={() => setShowDebugInfo(!debugInfo)}
255
+ >
256
+ <Text>Show Debug Info</Text>
257
+ </DropdownMenuCheckboxItem>
258
+ </DropdownMenuGroup>
259
+ <DropdownMenuGroup title="Report">
260
+ <ReportButton
261
+ livestream={livestream}
262
+ setReportModalOpen={setReportModalOpen}
263
+ setReportSubject={setReportSubject}
264
+ />
265
+ </DropdownMenuGroup>
266
+ <View style={[pt[3], px[2], gap.all[2]]}>
267
+ {contentWarnings && contentWarnings.length > 0 && (
268
+ <View style={[gap.all[1]]}>
269
+ <Text size="base" color="muted">
270
+ Stream may contain
271
+ </Text>
272
+ <ContentWarnings warnings={contentWarnings} compact={true} />
273
+ </View>
274
+ )}
275
+ {contentRights && Object.keys(contentRights).length > 0 && (
276
+ <ContentRights
277
+ contentRights={contentRights}
278
+ size="xs"
279
+ color="muted"
280
+ />
281
+ )}
282
+ </View>
283
+ </DropdownMenuContent>
258
284
  </DropdownMenu>
259
285
  );
260
286
  }
@@ -52,7 +52,6 @@ export function ViewerLoadingOverlay() {
52
52
  position: "absolute",
53
53
  width: "100%",
54
54
  height: "100%",
55
- zIndex: 998,
56
55
  alignItems: "center",
57
56
  justifyContent: "center",
58
57
  backgroundColor: "rgba(0,0,0,0.3)",
@@ -0,0 +1,5 @@
1
+ export { StreamNotificationProvider } from "./stream-notification";
2
+ export {
3
+ streamNotification,
4
+ streamNotificationManager,
5
+ } from "./stream-notification-manager";
@@ -0,0 +1,140 @@
1
+ export type NotificationConfig = {
2
+ id?: string;
3
+ message?: string;
4
+ render?: (
5
+ isExiting: boolean,
6
+ onDismiss: (reason?: "user" | "auto") => void,
7
+ startTime?: number,
8
+ ) => React.ReactNode;
9
+ duration?: number; // seconds, 0 = manual dismiss only
10
+ actionLabel?: string;
11
+ onAction?: () => void;
12
+ onDismiss?: (reason?: "user" | "auto") => void;
13
+ variant?: "default" | "info" | "warning";
14
+ };
15
+
16
+ export type StreamNotification = NotificationConfig & {
17
+ id: string;
18
+ visible: boolean;
19
+ shouldDismiss?: boolean;
20
+ dismissReason?: "user" | "auto";
21
+ startTime?: number;
22
+ };
23
+
24
+ type Listener = (notifications: StreamNotification[]) => void;
25
+
26
+ class StreamNotificationManager {
27
+ private notifications: StreamNotification[] = [];
28
+ private listeners: Set<Listener> = new Set();
29
+ private dismissTimers: Map<string, NodeJS.Timeout> = new Map();
30
+
31
+ show(config: NotificationConfig) {
32
+ const notification: StreamNotification = {
33
+ id: config.id || `notification-${Date.now()}`,
34
+ message: config.message,
35
+ render: config.render,
36
+ duration: config.duration ?? 5,
37
+ actionLabel: config.actionLabel,
38
+ onAction: config.onAction,
39
+ onDismiss: config.onDismiss,
40
+ variant: config.variant ?? "default",
41
+ visible: true,
42
+ startTime: Date.now(),
43
+ };
44
+
45
+ // if notification with same ID exists, dismiss it first
46
+ const existingIndex = this.notifications.findIndex(
47
+ (n) => n.id === notification.id,
48
+ );
49
+ if (existingIndex !== -1) {
50
+ const existingTimer = this.dismissTimers.get(notification.id);
51
+ if (existingTimer) {
52
+ clearTimeout(existingTimer);
53
+ this.dismissTimers.delete(notification.id);
54
+ }
55
+ this.notifications = this.notifications.filter(
56
+ (n) => n.id !== notification.id,
57
+ );
58
+ }
59
+
60
+ this.notifications = [...this.notifications, notification];
61
+ this.notifyListeners();
62
+
63
+ // auto-dismiss if duration > 0
64
+ if (notification.duration && notification.duration > 0) {
65
+ const timer = setTimeout(() => {
66
+ this.requestDismiss(notification.id, "auto");
67
+ }, notification.duration * 1000);
68
+ this.dismissTimers.set(notification.id, timer);
69
+ }
70
+ }
71
+
72
+ requestDismiss(id: string, reason: "user" | "auto" = "user") {
73
+ const notification = this.notifications.find((n) => n.id === id);
74
+ if (!notification) {
75
+ console.log("Notification not found!");
76
+ return;
77
+ }
78
+
79
+ // mark the notification for dismissal
80
+ notification.shouldDismiss = true;
81
+ notification.dismissReason = reason;
82
+ this.notifyListeners();
83
+ // after 500ms, just hide it for real
84
+ setTimeout(() => {
85
+ this.hide(id, reason);
86
+ }, 500);
87
+ }
88
+
89
+ hide(id: string, reason: "user" | "auto" = "user") {
90
+ console.log("Hide called with id:", id, "reason:", reason);
91
+ console.log(
92
+ "Current notifications:",
93
+ this.notifications.map((n) => n.id),
94
+ );
95
+ const notification = this.notifications.find((n) => n.id === id);
96
+ if (!notification) {
97
+ console.log("Notification not found!");
98
+ return;
99
+ }
100
+
101
+ const timer = this.dismissTimers.get(id);
102
+ if (timer) {
103
+ clearTimeout(timer);
104
+ this.dismissTimers.delete(id);
105
+ }
106
+
107
+ this.notifications = this.notifications.filter((n) => n.id !== id);
108
+ console.log(
109
+ "Remaining notifications:",
110
+ this.notifications.map((n) => n.id),
111
+ );
112
+ this.notifyListeners();
113
+
114
+ notification.onDismiss?.(reason);
115
+ }
116
+
117
+ getAll(): StreamNotification[] {
118
+ return this.notifications;
119
+ }
120
+
121
+ subscribe(listener: Listener) {
122
+ this.listeners.add(listener);
123
+ return () => {
124
+ this.listeners.delete(listener);
125
+ };
126
+ }
127
+
128
+ private notifyListeners() {
129
+ this.listeners.forEach((listener) => {
130
+ listener(this.notifications);
131
+ });
132
+ }
133
+ }
134
+
135
+ export const streamNotificationManager = new StreamNotificationManager();
136
+
137
+ export const streamNotification = {
138
+ show: (config: NotificationConfig) => streamNotificationManager.show(config),
139
+ hide: (id: string) => streamNotificationManager.hide(id),
140
+ };