@streamplace/components 0.9.7 → 0.9.10

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 (195) hide show
  1. package/assets/badges/live.png +0 -0
  2. package/assets/badges/live_2x.png +0 -0
  3. package/assets/badges/mod.png +0 -0
  4. package/assets/badges/mod_2x.png +0 -0
  5. package/assets/badges/vip.png +0 -0
  6. package/assets/badges/vip_2x.png +0 -0
  7. package/dist/components/chat/badge.d.ts +10 -0
  8. package/dist/components/chat/badge.d.ts.map +1 -0
  9. package/dist/components/chat/badge.js +29 -0
  10. package/dist/components/chat/badge.js.map +1 -0
  11. package/dist/components/chat/chat-box.d.ts +5 -1
  12. package/dist/components/chat/chat-box.d.ts.map +1 -1
  13. package/dist/components/chat/chat-box.js +55 -50
  14. package/dist/components/chat/chat-box.js.map +1 -1
  15. package/dist/components/chat/chat-message.d.ts.map +1 -1
  16. package/dist/components/chat/chat-message.js +9 -11
  17. package/dist/components/chat/chat-message.js.map +1 -1
  18. package/dist/components/chat/chat.d.ts.map +1 -1
  19. package/dist/components/chat/chat.js +37 -43
  20. package/dist/components/chat/chat.js.map +1 -1
  21. package/dist/components/chat/emoji-suggestions.d.ts +7 -18
  22. package/dist/components/chat/emoji-suggestions.d.ts.map +1 -1
  23. package/dist/components/chat/emoji-suggestions.js +6 -2
  24. package/dist/components/chat/emoji-suggestions.js.map +1 -1
  25. package/dist/components/chat/system-message.d.ts.map +1 -1
  26. package/dist/components/chat/system-message.js +9 -1
  27. package/dist/components/chat/system-message.js.map +1 -1
  28. package/dist/components/chat/teleport-modal.d.ts +9 -0
  29. package/dist/components/chat/teleport-modal.d.ts.map +1 -0
  30. package/dist/components/chat/teleport-modal.js +148 -0
  31. package/dist/components/chat/teleport-modal.js.map +1 -0
  32. package/dist/components/chat/user-profile-card.d.ts +12 -0
  33. package/dist/components/chat/user-profile-card.d.ts.map +1 -0
  34. package/dist/components/chat/user-profile-card.js +135 -0
  35. package/dist/components/chat/user-profile-card.js.map +1 -0
  36. package/dist/components/dashboard/chat-panel.d.ts +3 -1
  37. package/dist/components/dashboard/chat-panel.d.ts.map +1 -1
  38. package/dist/components/dashboard/chat-panel.js +2 -2
  39. package/dist/components/dashboard/chat-panel.js.map +1 -1
  40. package/dist/components/dashboard/header.d.ts +2 -3
  41. package/dist/components/dashboard/header.d.ts.map +1 -1
  42. package/dist/components/dashboard/header.js +6 -2
  43. package/dist/components/dashboard/header.js.map +1 -1
  44. package/dist/components/dashboard/information-widget.d.ts.map +1 -1
  45. package/dist/components/dashboard/information-widget.js +15 -12
  46. package/dist/components/dashboard/information-widget.js.map +1 -1
  47. package/dist/components/mobile-player/fullscreen.d.ts.map +1 -1
  48. package/dist/components/mobile-player/fullscreen.js +2 -1
  49. package/dist/components/mobile-player/fullscreen.js.map +1 -1
  50. package/dist/components/mobile-player/fullscreen.native.d.ts.map +1 -1
  51. package/dist/components/mobile-player/fullscreen.native.js +3 -2
  52. package/dist/components/mobile-player/fullscreen.native.js.map +1 -1
  53. package/dist/components/mobile-player/player.d.ts.map +1 -1
  54. package/dist/components/mobile-player/player.js +15 -0
  55. package/dist/components/mobile-player/player.js.map +1 -1
  56. package/dist/components/mobile-player/ui/audio-only-overlay.d.ts +2 -0
  57. package/dist/components/mobile-player/ui/audio-only-overlay.d.ts.map +1 -0
  58. package/dist/components/mobile-player/ui/audio-only-overlay.js +29 -0
  59. package/dist/components/mobile-player/ui/audio-only-overlay.js.map +1 -0
  60. package/dist/components/mobile-player/ui/index.d.ts +1 -0
  61. package/dist/components/mobile-player/ui/index.d.ts.map +1 -1
  62. package/dist/components/mobile-player/ui/index.js +1 -0
  63. package/dist/components/mobile-player/ui/index.js.map +1 -1
  64. package/dist/components/mobile-player/ui/input.d.ts +3 -2
  65. package/dist/components/mobile-player/ui/input.d.ts.map +1 -1
  66. package/dist/components/mobile-player/ui/input.js +18 -2
  67. package/dist/components/mobile-player/ui/input.js.map +1 -1
  68. package/dist/components/mobile-player/ui/metrics.d.ts.map +1 -1
  69. package/dist/components/mobile-player/ui/metrics.js +20 -2
  70. package/dist/components/mobile-player/ui/metrics.js.map +1 -1
  71. package/dist/components/mobile-player/ui/streamer-context-menu.d.ts +3 -1
  72. package/dist/components/mobile-player/ui/streamer-context-menu.d.ts.map +1 -1
  73. package/dist/components/mobile-player/ui/streamer-context-menu.js +64 -2
  74. package/dist/components/mobile-player/ui/streamer-context-menu.js.map +1 -1
  75. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
  76. package/dist/components/mobile-player/ui/viewer-context-menu.js +29 -1
  77. package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
  78. package/dist/components/mobile-player/use-webrtc.d.ts +4 -2
  79. package/dist/components/mobile-player/use-webrtc.d.ts.map +1 -1
  80. package/dist/components/mobile-player/use-webrtc.js +89 -15
  81. package/dist/components/mobile-player/use-webrtc.js.map +1 -1
  82. package/dist/components/mobile-player/video-async.native.d.ts.map +1 -1
  83. package/dist/components/mobile-player/video-async.native.js +15 -5
  84. package/dist/components/mobile-player/video-async.native.js.map +1 -1
  85. package/dist/components/mobile-player/video.d.ts.map +1 -1
  86. package/dist/components/mobile-player/video.js +10 -7
  87. package/dist/components/mobile-player/video.js.map +1 -1
  88. package/dist/components/ui/dialog.d.ts.map +1 -1
  89. package/dist/components/ui/dialog.js +8 -0
  90. package/dist/components/ui/dialog.js.map +1 -1
  91. package/dist/components/ui/textarea.d.ts.map +1 -1
  92. package/dist/components/ui/textarea.js +1 -1
  93. package/dist/components/ui/textarea.js.map +1 -1
  94. package/dist/hooks/index.d.ts +1 -0
  95. package/dist/hooks/index.d.ts.map +1 -1
  96. package/dist/hooks/index.js +1 -0
  97. package/dist/hooks/index.js.map +1 -1
  98. package/dist/hooks/useAQState.d.ts +2 -0
  99. package/dist/hooks/useAQState.d.ts.map +1 -0
  100. package/dist/hooks/useAQState.js +37 -0
  101. package/dist/hooks/useAQState.js.map +1 -0
  102. package/dist/hooks/useLivestreamInfo.d.ts +1 -2
  103. package/dist/hooks/useLivestreamInfo.d.ts.map +1 -1
  104. package/dist/hooks/useLivestreamInfo.js +18 -22
  105. package/dist/hooks/useLivestreamInfo.js.map +1 -1
  106. package/dist/hooks/useSegmentTiming.d.ts +1 -1
  107. package/dist/hooks/useSegmentTiming.d.ts.map +1 -1
  108. package/dist/hooks/useSegmentTiming.js +4 -0
  109. package/dist/hooks/useSegmentTiming.js.map +1 -1
  110. package/dist/i18n/i18n-loader.native.d.ts.map +1 -1
  111. package/dist/i18n/i18n-loader.native.js +13 -4
  112. package/dist/i18n/i18n-loader.native.js.map +1 -1
  113. package/dist/lib/slash-commands/teleport.d.ts +5 -1
  114. package/dist/lib/slash-commands/teleport.d.ts.map +1 -1
  115. package/dist/lib/slash-commands/teleport.js +57 -1
  116. package/dist/lib/slash-commands/teleport.js.map +1 -1
  117. package/dist/lib/theme/atoms.d.ts +125 -125
  118. package/dist/livestream-store/chat.d.ts +1 -0
  119. package/dist/livestream-store/chat.d.ts.map +1 -1
  120. package/dist/livestream-store/chat.js +10 -1
  121. package/dist/livestream-store/chat.js.map +1 -1
  122. package/dist/livestream-store/livestream-state.d.ts +2 -0
  123. package/dist/livestream-store/livestream-state.d.ts.map +1 -1
  124. package/dist/livestream-store/livestream-store.d.ts +1 -1
  125. package/dist/livestream-store/livestream-store.d.ts.map +1 -1
  126. package/dist/livestream-store/livestream-store.js +10 -1
  127. package/dist/livestream-store/livestream-store.js.map +1 -1
  128. package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
  129. package/dist/livestream-store/websocket-consumer.js +1 -0
  130. package/dist/livestream-store/websocket-consumer.js.map +1 -1
  131. package/dist/player-store/player-state.d.ts +3 -5
  132. package/dist/player-store/player-state.d.ts.map +1 -1
  133. package/dist/player-store/player-store.d.ts.map +1 -1
  134. package/dist/player-store/player-store.js +28 -5
  135. package/dist/player-store/player-store.js.map +1 -1
  136. package/dist/player-store/single-player-provider.d.ts +0 -2
  137. package/dist/player-store/single-player-provider.d.ts.map +1 -1
  138. package/dist/player-store/single-player-provider.js +0 -2
  139. package/dist/player-store/single-player-provider.js.map +1 -1
  140. package/dist/streamplace-store/branding.d.ts.map +1 -1
  141. package/dist/streamplace-store/branding.js +52 -1
  142. package/dist/streamplace-store/branding.js.map +1 -1
  143. package/dist/streamplace-store/stream.d.ts +4 -2
  144. package/dist/streamplace-store/stream.d.ts.map +1 -1
  145. package/dist/streamplace-store/stream.js +36 -74
  146. package/dist/streamplace-store/stream.js.map +1 -1
  147. package/locales/en-US/common.ftl +13 -1
  148. package/locales/manifest.json +21 -1
  149. package/locales/ro-RO/common.ftl +74 -0
  150. package/locales/ro-RO/settings.ftl +233 -0
  151. package/locales/zh-Hans/common.ftl +57 -0
  152. package/locales/zh-Hans/settings.ftl +222 -0
  153. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  154. package/package.json +2 -2
  155. package/src/components/chat/badge.tsx +45 -0
  156. package/src/components/chat/chat-box.tsx +84 -54
  157. package/src/components/chat/chat-message.tsx +25 -21
  158. package/src/components/chat/chat.tsx +107 -90
  159. package/src/components/chat/emoji-suggestions.tsx +12 -21
  160. package/src/components/chat/system-message.tsx +12 -2
  161. package/src/components/chat/teleport-modal.tsx +310 -0
  162. package/src/components/chat/user-profile-card.tsx +275 -0
  163. package/src/components/dashboard/chat-panel.tsx +8 -0
  164. package/src/components/dashboard/header.tsx +8 -17
  165. package/src/components/dashboard/information-widget.tsx +17 -10
  166. package/src/components/mobile-player/fullscreen.native.tsx +3 -0
  167. package/src/components/mobile-player/fullscreen.tsx +2 -0
  168. package/src/components/mobile-player/player.tsx +22 -1
  169. package/src/components/mobile-player/ui/audio-only-overlay.tsx +48 -0
  170. package/src/components/mobile-player/ui/index.ts +1 -0
  171. package/src/components/mobile-player/ui/input.tsx +42 -12
  172. package/src/components/mobile-player/ui/metrics.tsx +17 -2
  173. package/src/components/mobile-player/ui/streamer-context-menu.tsx +138 -2
  174. package/src/components/mobile-player/ui/viewer-context-menu.tsx +40 -3
  175. package/src/components/mobile-player/use-webrtc.tsx +118 -17
  176. package/src/components/mobile-player/video-async.native.tsx +18 -5
  177. package/src/components/mobile-player/video.tsx +10 -7
  178. package/src/components/ui/dialog.tsx +8 -0
  179. package/src/components/ui/textarea.tsx +2 -0
  180. package/src/hooks/index.ts +1 -0
  181. package/src/hooks/useAQState.ts +37 -0
  182. package/src/hooks/useLivestreamInfo.ts +21 -22
  183. package/src/hooks/useSegmentTiming.tsx +7 -2
  184. package/src/i18n/i18n-loader.native.ts +9 -0
  185. package/src/lib/slash-commands/teleport.ts +68 -0
  186. package/src/livestream-store/chat.tsx +12 -0
  187. package/src/livestream-store/livestream-state.tsx +2 -0
  188. package/src/livestream-store/livestream-store.tsx +9 -1
  189. package/src/livestream-store/websocket-consumer.tsx +1 -0
  190. package/src/player-store/player-state.tsx +4 -7
  191. package/src/player-store/player-store.tsx +33 -7
  192. package/src/player-store/single-player-provider.tsx +0 -4
  193. package/src/streamplace-store/branding.tsx +60 -1
  194. package/src/streamplace-store/stream.tsx +42 -99
  195. package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
@@ -0,0 +1,275 @@
1
+ import { ProfileViewBasic } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
2
+ import { TriggerRef } from "@rn-primitives/dropdown-menu";
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+ import { Image, Platform, Pressable, View } from "react-native";
13
+ import { ChatMessageViewHydrated } from "streamplace";
14
+ import { useAvatars } from "../../hooks/useAvatars";
15
+ import { useLivestreamStore } from "../../livestream-store";
16
+ import { useUrl } from "../../streamplace-store";
17
+ import { useTheme } from "../../ui";
18
+ import { formatHandleWithAt } from "../../utils/format-handle";
19
+ import {
20
+ DropdownMenu,
21
+ DropdownMenuContent,
22
+ DropdownMenuTrigger,
23
+ } from "../ui/dropdown";
24
+ import { Text } from "../ui/text";
25
+ import { Badge } from "./badge";
26
+
27
+ interface BadgeMeta {
28
+ label: string;
29
+ description: string;
30
+ issuedBy?: string;
31
+ }
32
+
33
+ const BADGE_META: Record<string, BadgeMeta> = {
34
+ "place.stream.badge.defs#mod": {
35
+ label: "Moderator",
36
+ description: "This user is a moderator.",
37
+ issuedBy: "{issuer} for {streamer}",
38
+ },
39
+ "place.stream.badge.defs#streamer": {
40
+ label: "Streamer",
41
+ description: "This user is the streamer.",
42
+ },
43
+ "place.stream.badge.defs#vip": {
44
+ label: "VIP",
45
+ description: "This user is a very important person.",
46
+ },
47
+ };
48
+
49
+ interface OpenCardContextValue {
50
+ openUri: string | null;
51
+ setOpenUri: (uri: string | null) => void;
52
+ }
53
+
54
+ const OpenCardContext = createContext<OpenCardContextValue>({
55
+ openUri: null,
56
+ setOpenUri: () => {},
57
+ });
58
+
59
+ export const ProfileCardProvider = ({
60
+ children,
61
+ }: {
62
+ children: React.ReactNode;
63
+ }) => {
64
+ const [openUri, setOpenUri] = useState<string | null>(null);
65
+ return (
66
+ <OpenCardContext.Provider value={{ openUri, setOpenUri }}>
67
+ {children}
68
+ </OpenCardContext.Provider>
69
+ );
70
+ };
71
+
72
+ const BadgeRow = ({
73
+ badge,
74
+ serviceDid,
75
+ }: {
76
+ badge: NonNullable<ChatMessageViewHydrated["badges"]>[number];
77
+ serviceDid: string;
78
+ }) => {
79
+ const streamer = useLivestreamStore((x) => x.livestream?.author);
80
+ const isServiceIssued = badge.issuer === serviceDid;
81
+ const issuerDids = useMemo(
82
+ () => (isServiceIssued ? [] : [badge.issuer]),
83
+ [isServiceIssued, badge.issuer],
84
+ );
85
+ const issuerProfiles = useAvatars(issuerDids);
86
+ const meta = BADGE_META[badge.badgeType];
87
+
88
+ if (!meta) return null;
89
+
90
+ let issuerLabel = isServiceIssued
91
+ ? "Streamplace"
92
+ : issuerProfiles[badge.issuer]?.handle
93
+ ? `@${issuerProfiles[badge.issuer].handle}`
94
+ : badge.issuer;
95
+ if (meta.issuedBy) {
96
+ issuerLabel = meta.issuedBy
97
+ .replace("{issuer}", issuerLabel)
98
+ .replace(
99
+ "{streamer}",
100
+ streamer?.handle ? formatHandleWithAt(streamer) : "the streamer",
101
+ );
102
+ }
103
+
104
+ return (
105
+ <View
106
+ style={{
107
+ flexDirection: "row",
108
+ alignItems: "center",
109
+ gap: 12,
110
+ paddingLeft: 6,
111
+ }}
112
+ >
113
+ <Badge badgeType={badge.badgeType} size={32} />
114
+ <View style={{ flex: 1, gap: 2 }}>
115
+ <Text size="xs">{meta.label}</Text>
116
+ <Text size="xs" color="muted">
117
+ Issued by {issuerLabel}
118
+ </Text>
119
+ <Text size="xs" color="muted">
120
+ {meta.description}
121
+ </Text>
122
+ </View>
123
+ </View>
124
+ );
125
+ };
126
+
127
+ export const UserProfileCard = ({
128
+ uri,
129
+ author,
130
+ badges,
131
+ children,
132
+ }: {
133
+ uri: string;
134
+ author: ProfileViewBasic;
135
+ badges: ChatMessageViewHydrated["badges"];
136
+ children: React.ReactNode;
137
+ }) => {
138
+ const { theme } = useTheme();
139
+ const nodeUrl = useUrl();
140
+ const serviceDid = nodeUrl
141
+ ? `did:web:${nodeUrl.replace(/^https?:\/\//, "")}`
142
+ : null;
143
+
144
+ const { openUri, setOpenUri } = useContext(OpenCardContext);
145
+ const isOpen = openUri === uri;
146
+ const thisRef = useRef<TriggerRef>(null);
147
+ const [hovered, setHovered] = useState(false);
148
+
149
+ const profiles = useAvatars(author.did ? [author.did] : []);
150
+ const profile = profiles[author.did];
151
+
152
+ useEffect(() => {
153
+ isOpen ? thisRef.current?.open() : thisRef.current?.close();
154
+ }, [isOpen]);
155
+
156
+ const onOpenChange = useCallback(
157
+ (open: boolean) => {
158
+ setOpenUri(open ? uri : null);
159
+ },
160
+ [uri, setOpenUri],
161
+ );
162
+
163
+ const serviceBadges = useMemo(
164
+ () => badges?.filter((b) => serviceDid && b.issuer === serviceDid) ?? [],
165
+ [badges, serviceDid],
166
+ );
167
+
168
+ return (
169
+ <DropdownMenu onOpenChange={onOpenChange}>
170
+ <DropdownMenuTrigger ref={thisRef} asChild>
171
+ <Pressable
172
+ onPress={() => {}}
173
+ {...(Platform.OS === "web"
174
+ ? {
175
+ onHoverIn: () => setHovered(true),
176
+ onHoverOut: () => setHovered(false),
177
+ }
178
+ : {})}
179
+ style={{
180
+ paddingHorizontal: 3,
181
+ flexDirection: "row",
182
+ gap: 4,
183
+ marginLeft: -3,
184
+ paddingLeft: 3,
185
+ marginRight: -2,
186
+ ...(Platform.OS === "web" && hovered
187
+ ? { backgroundColor: "rgba(255,255,255,0.15)", borderRadius: 6 }
188
+ : {}),
189
+ }}
190
+ >
191
+ {children}
192
+ </Pressable>
193
+ </DropdownMenuTrigger>
194
+ <DropdownMenuContent style={{ minWidth: 280, maxWidth: 320 }}>
195
+ <View>
196
+ {profile?.banner ? (
197
+ <Image
198
+ source={{ uri: profile.banner }}
199
+ style={{
200
+ width: "100%",
201
+ height: 80,
202
+ borderRadius: theme.borderRadius.md,
203
+ }}
204
+ />
205
+ ) : (
206
+ <View
207
+ style={{
208
+ width: "100%",
209
+ height: 80,
210
+ borderRadius: theme.borderRadius.md,
211
+ backgroundColor: theme.colors.muted,
212
+ }}
213
+ />
214
+ )}
215
+ <View
216
+ style={{
217
+ flexDirection: "row",
218
+ alignItems: "flex-end",
219
+ marginTop: -24,
220
+ paddingHorizontal: 12,
221
+ }}
222
+ >
223
+ {profile?.avatar ? (
224
+ <Image
225
+ source={{ uri: profile.avatar }}
226
+ style={{
227
+ width: 48,
228
+ height: 48,
229
+ borderRadius: 24,
230
+ borderWidth: 2,
231
+ borderColor: theme.colors.card,
232
+ }}
233
+ />
234
+ ) : (
235
+ <View
236
+ style={{
237
+ width: 48,
238
+ height: 48,
239
+ borderRadius: 24,
240
+ backgroundColor: theme.colors.mutedForeground,
241
+ borderWidth: 2,
242
+ borderColor: theme.colors.card,
243
+ }}
244
+ />
245
+ )}
246
+ </View>
247
+ <View style={{ paddingHorizontal: 12 }}>
248
+ <Text>@{author.handle}</Text>
249
+ {profile?.description ? (
250
+ <Text size="sm" color="muted" numberOfLines={4}>
251
+ {profile.description}
252
+ </Text>
253
+ ) : null}
254
+ </View>
255
+ {serviceBadges.length > 0 && serviceDid ? (
256
+ <View
257
+ style={{
258
+ marginTop: 12,
259
+ paddingHorizontal: 12,
260
+ paddingBottom: 8,
261
+ borderTopWidth: 1,
262
+ borderTopColor: theme.colors.border,
263
+ paddingTop: 8,
264
+ }}
265
+ >
266
+ {serviceBadges.map((badge, i) => (
267
+ <BadgeRow key={i} badge={badge} serviceDid={serviceDid} />
268
+ ))}
269
+ </View>
270
+ ) : null}
271
+ </View>
272
+ </DropdownMenuContent>
273
+ </DropdownMenu>
274
+ );
275
+ };
@@ -1,3 +1,4 @@
1
+ import React from "react";
1
2
  import { Text, View } from "react-native";
2
3
  import * as zero from "../../ui";
3
4
  import { Chat } from "../chat/chat";
@@ -11,6 +12,11 @@ interface ChatPanelProps {
11
12
  messagesPerMinute?: number;
12
13
  shownMessages?: number;
13
14
  emojiData?: any;
15
+ emojiPicker?: (
16
+ isOpen: boolean,
17
+ onClose: () => void,
18
+ onSelect: (emoji: any) => void,
19
+ ) => React.ReactNode;
14
20
  }
15
21
 
16
22
  export default function ChatPanel({
@@ -19,6 +25,7 @@ export default function ChatPanel({
19
25
  messagesPerMinute = 0,
20
26
  shownMessages = 50,
21
27
  emojiData = null,
28
+ emojiPicker,
22
29
  }: ChatPanelProps) {
23
30
  return (
24
31
  <View
@@ -63,6 +70,7 @@ export default function ChatPanel({
63
70
  <View style={[{ flexShrink: 0 }]}>
64
71
  <ChatBox
65
72
  emojiData={emojiData}
73
+ emojiPicker={emojiPicker}
66
74
  chatBoxStyle={[
67
75
  bg.gray[700],
68
76
  borders.width.thin,
@@ -1,4 +1,4 @@
1
- import { AlertCircle, Car, Radio, Users } from "lucide-react-native";
1
+ import { AlertCircle, Radio } from "lucide-react-native";
2
2
  import { Pressable, Text, View } from "react-native";
3
3
  import * as zero from "../../ui";
4
4
 
@@ -8,7 +8,7 @@ interface MetricItemProps {
8
8
  icon: any;
9
9
  label: string;
10
10
  value: string;
11
- status?: "good" | "warning" | "error";
11
+ status?: "good" | "warning" | "error" | "pre-live";
12
12
  }
13
13
 
14
14
  function MetricItem({ icon: Icon, label, value, status }: MetricItemProps) {
@@ -36,7 +36,7 @@ function MetricItem({ icon: Icon, label, value, status }: MetricItemProps) {
36
36
  }
37
37
 
38
38
  interface StatusIndicatorProps {
39
- status: "excellent" | "good" | "poor" | "offline";
39
+ status: "excellent" | "good" | "poor" | "offline" | "pre-live";
40
40
  isLive: boolean;
41
41
  }
42
42
 
@@ -44,6 +44,8 @@ function StatusIndicator({ status, isLive }: StatusIndicatorProps) {
44
44
  const getStatusColor = () => {
45
45
  if (!isLive) return bg.gray[500];
46
46
  switch (status) {
47
+ case "pre-live":
48
+ return bg.blue[500];
47
49
  case "excellent":
48
50
  return bg.green[500];
49
51
  case "good":
@@ -60,6 +62,8 @@ function StatusIndicator({ status, isLive }: StatusIndicatorProps) {
60
62
  const getStatusText = () => {
61
63
  if (!isLive) return "OFFLINE";
62
64
  switch (status) {
65
+ case "pre-live":
66
+ return "NOT LIVE";
63
67
  case "excellent":
64
68
  return "EXCELLENT";
65
69
  case "good":
@@ -98,11 +102,10 @@ function StatusIndicator({ status, isLive }: StatusIndicatorProps) {
98
102
  interface HeaderProps {
99
103
  isLive: boolean;
100
104
  streamTitle?: string;
101
- viewers?: number;
102
105
  uptime?: string;
103
106
  bitrate?: string;
104
107
  timeBetweenSegments?: number;
105
- connectionStatus?: "excellent" | "good" | "poor" | "offline";
108
+ connectionStatus?: "excellent" | "good" | "poor" | "offline" | "pre-live";
106
109
  problemsCount?: number;
107
110
  onProblemsPress?: () => void;
108
111
  }
@@ -110,7 +113,6 @@ interface HeaderProps {
110
113
  export default function Header({
111
114
  isLive,
112
115
  streamTitle = "Live Stream",
113
- viewers = 0,
114
116
  uptime = "00:00:00",
115
117
  bitrate = "0 mbps",
116
118
  timeBetweenSegments = 0,
@@ -179,17 +181,6 @@ export default function Header({
179
181
 
180
182
  {/* Right side - Stream metrics */}
181
183
  <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[6]]}>
182
- {isLive && (
183
- <>
184
- <MetricItem
185
- icon={Users}
186
- label="Viewers"
187
- value={viewers.toLocaleString()}
188
- />
189
- <MetricItem icon={Car} label="Bitrate" value={bitrate} />
190
- </>
191
- )}
192
-
193
184
  {!isLive && (
194
185
  <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
195
186
  <Radio size={16} color="#6b7280" />
@@ -12,11 +12,8 @@ import {
12
12
  import React, { useCallback, useEffect, useMemo, useState } from "react";
13
13
  import { LayoutChangeEvent, Text, TouchableOpacity, View } from "react-native";
14
14
  import Svg, { Path, Line as SvgLine, Text as SvgText } from "react-native-svg";
15
- import {
16
- useLivestreamStore,
17
- useSegment,
18
- useViewers,
19
- } from "../../livestream-store";
15
+ import { useAQState } from "../../hooks";
16
+ import { useLivestream, useSegment, useViewers } from "../../livestream-store";
20
17
  import * as zero from "../../ui";
21
18
  import { InfoBox, InfoRow } from "../ui";
22
19
 
@@ -38,7 +35,7 @@ export default function InformationWidget({
38
35
  const [bitrateHistory, setBitrateHistory] = useState<number[]>(
39
36
  Array.from({ length: BITRATE_HISTORY_LENGTH }, () => 0),
40
37
  );
41
- const [showViewers, setShowViewers] = useState(false);
38
+ const [showViewers, setShowViewers] = useAQState("showViewers", true);
42
39
  const [componentWidth, setComponentWidth] = useState<number>(220);
43
40
  const [componentHeight, setComponentHeight] = useState<number>(400);
44
41
  const [streamStartTime, setStreamStartTime] = useState<Date | null>(null);
@@ -49,7 +46,7 @@ export default function InformationWidget({
49
46
  const isCompactHeight = layoutMeasured && componentHeight < 350;
50
47
 
51
48
  const seg = useSegment();
52
- const livestream = useLivestreamStore((x) => x.livestream);
49
+ const livestream = useLivestream();
53
50
  const viewers = useViewers();
54
51
 
55
52
  const getBitrate = useCallback((): number => {
@@ -172,8 +169,9 @@ export default function InformationWidget({
172
169
  width: 8,
173
170
  height: 8,
174
171
  borderRadius: 4,
175
- backgroundColor:
176
- getConnectionStatus() === "good"
172
+ backgroundColor: !livestream
173
+ ? "#3b82f6"
174
+ : getConnectionStatus() === "good"
177
175
  ? "#22c55e"
178
176
  : getConnectionStatus() === "warning"
179
177
  ? "#f59e0b"
@@ -181,6 +179,11 @@ export default function InformationWidget({
181
179
  },
182
180
  ]}
183
181
  />
182
+ {!livestream && (
183
+ <Text style={[text.blue[400], { fontSize: 13, fontWeight: "600" }]}>
184
+ (not live)
185
+ </Text>
186
+ )}
184
187
  </View>
185
188
  <TouchableOpacity
186
189
  onPress={() => setShowViewers(!showViewers)}
@@ -314,6 +317,7 @@ export default function InformationWidget({
314
317
  data={bitrateHistory}
315
318
  width={componentWidth - 40}
316
319
  height={120}
320
+ color={livestream ? "#22c55e" : "#3b82f6"}
317
321
  />
318
322
  </View>
319
323
  )}
@@ -394,6 +398,7 @@ export default function InformationWidget({
394
398
  data={bitrateHistory}
395
399
  width={componentWidth - 40}
396
400
  height={isCompactHeight ? 80 : 120}
401
+ color={livestream ? "#22c55e" : "#3b82f6"}
397
402
  />
398
403
  </View>
399
404
  )}
@@ -431,10 +436,12 @@ function BitrateChart({
431
436
  data,
432
437
  width,
433
438
  height,
439
+ color = "#22c55e",
434
440
  }: {
435
441
  data: number[];
436
442
  width: number;
437
443
  height: number;
444
+ color?: string;
438
445
  }) {
439
446
  const maxDataValue = Math.max(...data, 1);
440
447
  const minDataValue = Math.min(...data);
@@ -514,7 +521,7 @@ function BitrateChart({
514
521
  </SvgText>
515
522
  <Path
516
523
  d={pathData}
517
- stroke="#22c55e"
524
+ stroke={color}
518
525
  strokeWidth="2"
519
526
  fill="none"
520
527
  strokeLinecap="round"
@@ -15,6 +15,7 @@ import {
15
15
  usePlayerStore,
16
16
  VideoRetry,
17
17
  } from "../..";
18
+ import { AudioOnlyOverlay } from "./ui/audio-only-overlay";
18
19
  import Video from "./video.native";
19
20
 
20
21
  // Standard 16:9 video aspect ratio
@@ -166,6 +167,7 @@ export function Fullscreen(props: {
166
167
  objectFit={props.objectFit}
167
168
  pictureInPictureEnabled={props.pictureInPictureEnabled}
168
169
  />
170
+ <AudioOnlyOverlay />
169
171
  <DanmuOverlay
170
172
  enabled={danmuEnabled}
171
173
  opacity={danmuOpacity}
@@ -188,6 +190,7 @@ export function Fullscreen(props: {
188
190
  pictureInPictureEnabled={props.pictureInPictureEnabled}
189
191
  />
190
192
  </VideoRetry>
193
+ <AudioOnlyOverlay />
191
194
  <DanmuOverlay
192
195
  enabled={danmuEnabled}
193
196
  opacity={danmuOpacity}
@@ -11,6 +11,7 @@ import {
11
11
  usePlayerStore,
12
12
  } from "../..";
13
13
  import { View } from "../../components/ui";
14
+ import { AudioOnlyOverlay } from "./ui/audio-only-overlay";
14
15
  import Video from "./video";
15
16
  import VideoRetry from "./video-retry";
16
17
 
@@ -105,6 +106,7 @@ export function Fullscreen(props: {
105
106
  pictureInPictureEnabled={props.pictureInPictureEnabled}
106
107
  />
107
108
  </VideoRetry>
109
+ <AudioOnlyOverlay />
108
110
  <DanmuOverlay
109
111
  enabled={danmuEnabled}
110
112
  opacity={danmuOpacity}
@@ -5,7 +5,11 @@ import {
5
5
  PlayerStatusTracker,
6
6
  usePlayerStore,
7
7
  } from "../../player-store";
8
- import { useStreamplaceStore } from "../../streamplace-store";
8
+ import {
9
+ useMuted,
10
+ useSetMuted,
11
+ useStreamplaceStore,
12
+ } from "../../streamplace-store";
9
13
  import { Text, View } from "../ui";
10
14
  import { Fullscreen } from "./fullscreen";
11
15
  import { PlayerProps } from "./props";
@@ -29,6 +33,23 @@ export function Player(
29
33
  const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
30
34
  const reportSubject = usePlayerStore((x) => x.reportSubject);
31
35
 
36
+ const setMuted = useSetMuted();
37
+ const muted = useMuted();
38
+
39
+ // if we set muted, set it and restore after
40
+ useEffect(() => {
41
+ let wasMuted: null | boolean = null;
42
+ setTimeout(() => {
43
+ if (props.muted != undefined) {
44
+ wasMuted = muted;
45
+ setMuted(props.muted);
46
+ }
47
+ }, 200);
48
+ return () => {
49
+ wasMuted !== null && setMuted(wasMuted);
50
+ };
51
+ }, [props.muted]);
52
+
32
53
  useEffect(() => {
33
54
  setReportingURL(props.reportingURL ?? null);
34
55
  }, [props.reportingURL]);
@@ -0,0 +1,48 @@
1
+ import { Volume2 } from "lucide-react-native";
2
+ import { zero } from "../../..";
3
+ import { usePlayerStore } from "../../../player-store";
4
+ import { Text, View } from "../../ui";
5
+
6
+ export function AudioOnlyOverlay() {
7
+ const selectedRendition = usePlayerStore((x) => x.selectedRendition);
8
+ const setSelectedRendition = usePlayerStore((x) => x.setSelectedRendition);
9
+
10
+ if (selectedRendition !== "audio") {
11
+ return null;
12
+ }
13
+
14
+ return (
15
+ <View
16
+ style={[
17
+ zero.layout.position.absolute,
18
+ zero.position.top[0],
19
+ zero.position.left[0],
20
+ zero.position.right[0],
21
+ zero.position.bottom[0],
22
+ zero.layout.flex.center,
23
+ ]}
24
+ >
25
+ <View
26
+ style={[
27
+ zero.layout.flex.column,
28
+ zero.layout.flex.alignCenter,
29
+ zero.gap.all[3],
30
+ zero.px[6],
31
+ ]}
32
+ >
33
+ <Volume2 color="#fff" size={48} />
34
+ <Text size="lg" weight="semibold" center>
35
+ Audio Only mode
36
+ </Text>
37
+ <Text
38
+ size="sm"
39
+ color="muted"
40
+ center
41
+ onPress={() => setSelectedRendition("source")}
42
+ >
43
+ Go to Settings &gt; Quality to switch back to video.
44
+ </Text>
45
+ </View>
46
+ </View>
47
+ );
48
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./audio-only-overlay";
1
2
  export * from "./autoplay-button";
2
3
  export * from "./countdown";
3
4
  export * from "./input";
@@ -7,15 +7,17 @@ const { gap, h, layout, mt, p, position, px, py, sizes, w } = atoms;
7
7
  type InputPanelProps = {
8
8
  title: string | undefined;
9
9
  setTitle: (title: string) => void;
10
- ingestStarting: boolean;
11
10
  toggleGoLive: () => void;
11
+ isLive: boolean;
12
+ toggleStopStream?: () => void;
12
13
  };
13
14
 
14
15
  export function InputPanel({
15
16
  title,
16
17
  setTitle,
17
- ingestStarting,
18
18
  toggleGoLive,
19
+ isLive,
20
+ toggleStopStream,
19
21
  }: InputPanelProps) {
20
22
  const { slideKeyboard } = useKeyboardSlide();
21
23
  return (
@@ -37,16 +39,44 @@ export function InputPanel({
37
39
  { padding: 10 },
38
40
  ]}
39
41
  >
40
- <View backgroundColor="rgba(64,64,64,0.8)" borderRadius={12}>
41
- <Input
42
- value={title}
43
- onChange={setTitle}
44
- placeholder="Enter stream title"
45
- onEndEditing={Keyboard.dismiss}
46
- />
47
- </View>
48
- {ingestStarting ? (
49
- <Text>Starting your stream...</Text>
42
+ {!isLive && (
43
+ <View backgroundColor="rgba(64,64,64,0.8)" borderRadius={12}>
44
+ <Input
45
+ value={title}
46
+ onChange={setTitle}
47
+ placeholder="Enter stream title"
48
+ onEndEditing={Keyboard.dismiss}
49
+ />
50
+ </View>
51
+ )}
52
+ {isLive ? (
53
+ <View style={[layout.flex.center]}>
54
+ <Pressable
55
+ onPress={toggleStopStream}
56
+ style={[
57
+ px[4],
58
+ py[2],
59
+ layout.flex.row,
60
+ layout.flex.center,
61
+ gap.all[1],
62
+ {
63
+ backgroundColor: "rgba(64,64,64, 0.8)",
64
+ borderRadius: 12,
65
+ },
66
+ ]}
67
+ >
68
+ <View
69
+ style={[
70
+ p[2],
71
+ {
72
+ backgroundColor: "rgba(256,0,0, 0.8)",
73
+ borderRadius: 12,
74
+ },
75
+ ]}
76
+ />
77
+ <Text center>Stop Stream</Text>
78
+ </Pressable>
79
+ </View>
50
80
  ) : (
51
81
  <View style={[layout.flex.center]}>
52
82
  <Pressable