@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
@@ -26,6 +26,7 @@ import {
26
26
  import { bg, flex, layout, mr, px, py } from "../../lib/theme/atoms";
27
27
  import { RenderChatMessage } from "./chat-message";
28
28
  import { ModView } from "./mod-view";
29
+ import { ProfileCardProvider } from "./user-profile-card";
29
30
 
30
31
  function RightAction(prog: SharedValue<number>, drag: SharedValue<number>) {
31
32
  const styleAnimation = useAnimatedStyle(() => {
@@ -141,77 +142,65 @@ const ActionsBar = memo(
141
142
  },
142
143
  );
143
144
 
144
- const ChatLine = memo(({ item }: { item: ChatMessageViewHydrated }) => {
145
- const setReply = useSetReplyToMessage();
146
- const setModMsg = usePlayerStore((state) => state.setModMessage);
147
- const swipeableRef = useRef<SwipeableMethods | null>(null);
148
- const [isHovered, setIsHovered] = useState(false);
149
- const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
145
+ const ChatLine = memo(
146
+ ({
147
+ item,
148
+ isHovered,
149
+ onHoverIn,
150
+ onHoverOut,
151
+ hoverTimeoutRef,
152
+ }: {
153
+ item: ChatMessageViewHydrated;
154
+ isHovered?: boolean;
155
+ onHoverIn?: () => void;
156
+ onHoverOut?: () => void;
157
+ hoverTimeoutRef?: React.MutableRefObject<NodeJS.Timeout | null>;
158
+ }) => {
159
+ const setReply = useSetReplyToMessage();
160
+ const setModMsg = usePlayerStore((state) => state.setModMessage);
161
+ const swipeableRef = useRef<SwipeableMethods | null>(null);
150
162
 
151
- const handleHoverIn = () => {
152
- if (hoverTimeoutRef.current) {
153
- clearTimeout(hoverTimeoutRef.current);
154
- hoverTimeoutRef.current = null;
163
+ if (item.author.did === "did:sys:system") {
164
+ return (
165
+ <SystemMessage
166
+ variant={getSystemMessageType(item) || SystemMessageType.notification}
167
+ timestamp={new Date(item.record.createdAt)}
168
+ title={item.record.text}
169
+ facets={item.record.facets}
170
+ />
171
+ );
155
172
  }
156
- setIsHovered(true);
157
- };
158
-
159
- const handleHoverOut = () => {
160
- hoverTimeoutRef.current = setTimeout(() => {
161
- setIsHovered(false);
162
- }, 50);
163
- };
164
173
 
165
- useEffect(() => {
166
- return () => {
167
- if (hoverTimeoutRef.current) {
168
- clearTimeout(hoverTimeoutRef.current);
169
- }
170
- };
171
- }, []);
172
-
173
- if (item.author.did === "did:sys:system") {
174
- return (
175
- <SystemMessage
176
- variant={getSystemMessageType(item) || SystemMessageType.notification}
177
- timestamp={new Date(item.record.createdAt)}
178
- title={item.record.text}
179
- facets={item.record.facets}
180
- />
181
- );
182
- }
174
+ if (Platform.OS === "web") {
175
+ return (
176
+ <View
177
+ style={[
178
+ py[1],
179
+ px[2],
180
+ {
181
+ position: "relative",
182
+ borderRadius: 8,
183
+ minWidth: 0,
184
+ maxWidth: "100%",
185
+ },
186
+ isHovered && bg.gray[950],
187
+ ]}
188
+ onPointerEnter={onHoverIn}
189
+ onPointerLeave={onHoverOut}
190
+ >
191
+ <Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
192
+ <RenderChatMessage item={item} />
193
+ </Pressable>
194
+ <ActionsBar
195
+ item={item}
196
+ visible={!!isHovered}
197
+ hoverTimeoutRef={hoverTimeoutRef!}
198
+ />
199
+ </View>
200
+ );
201
+ }
183
202
 
184
- if (Platform.OS === "web") {
185
203
  return (
186
- <View
187
- style={[
188
- py[1],
189
- px[2],
190
- {
191
- position: "relative",
192
- borderRadius: 8,
193
- minWidth: 0,
194
- maxWidth: "100%",
195
- },
196
- isHovered && bg.gray[950],
197
- ]}
198
- onPointerEnter={handleHoverIn}
199
- onPointerLeave={handleHoverOut}
200
- >
201
- <Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
202
- <RenderChatMessage item={item} />
203
- </Pressable>
204
- <ActionsBar
205
- item={item}
206
- visible={isHovered}
207
- hoverTimeoutRef={hoverTimeoutRef}
208
- />
209
- </View>
210
- );
211
- }
212
-
213
- return (
214
- <>
215
204
  <Swipeable
216
205
  containerStyle={[py[1]]}
217
206
  friction={2}
@@ -238,9 +227,9 @@ const ChatLine = memo(({ item }: { item: ChatMessageViewHydrated }) => {
238
227
  >
239
228
  <RenderChatMessage item={item} />
240
229
  </Swipeable>
241
- </>
242
- );
243
- });
230
+ );
231
+ },
232
+ );
244
233
 
245
234
  export function Chat({
246
235
  shownMessages = SHOWN_MSGS,
@@ -254,6 +243,24 @@ export function Chat({
254
243
  const chat = useChat();
255
244
  const [isScrolledUp, setIsScrolledUp] = useState(false);
256
245
  const flatListRef = useRef<FlatList>(null);
246
+ const [hoveredMessageUri, setHoveredMessageUri] = useState<string | null>(
247
+ null,
248
+ );
249
+ const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
250
+
251
+ const handleHoverIn = (uri: string) => {
252
+ if (hoverTimeoutRef.current) {
253
+ clearTimeout(hoverTimeoutRef.current);
254
+ hoverTimeoutRef.current = null;
255
+ }
256
+ setHoveredMessageUri(uri);
257
+ };
258
+
259
+ const handleHoverOut = () => {
260
+ hoverTimeoutRef.current = setTimeout(() => {
261
+ setHoveredMessageUri(null);
262
+ }, 50);
263
+ };
257
264
 
258
265
  // Animation for scroll-to-bottom button
259
266
  const buttonOpacity = useSharedValue(0);
@@ -261,7 +268,7 @@ export function Chat({
261
268
 
262
269
  useEffect(() => {
263
270
  buttonOpacity.value = withTiming(isScrolledUp ? 1 : 0, { duration: 200 });
264
- buttonTranslateY.value = withTiming(isScrolledUp ? 0 : 20, {
271
+ buttonTranslateY.value = withTiming(isScrolledUp ? 0 : 50, {
265
272
  duration: 200,
266
273
  });
267
274
  }, [isScrolledUp]);
@@ -309,25 +316,35 @@ export function Chat({
309
316
  },
310
317
  ].concat(propsStyle || [])}
311
318
  >
312
- <FlatList
313
- ref={flatListRef}
314
- style={[
315
- flex.grow[1],
316
- flex.shrink[1],
317
- { minWidth: 0, maxWidth: "100%" },
318
- ]}
319
- data={chat.slice(0, shownMessages)}
320
- inverted={true}
321
- keyExtractor={keyExtractor}
322
- renderItem={({ item, index }) => <ChatLine item={item} />}
323
- removeClippedSubviews={true}
324
- maxToRenderPerBatch={10}
325
- initialNumToRender={10}
326
- updateCellsBatchingPeriod={50}
327
- onScroll={handleScroll}
328
- scrollEventThrottle={16}
329
- nestedScrollEnabled={true}
330
- />
319
+ <ProfileCardProvider>
320
+ <FlatList
321
+ ref={flatListRef}
322
+ style={[
323
+ flex.grow[1],
324
+ flex.shrink[1],
325
+ { minWidth: 0, maxWidth: "100%" },
326
+ ]}
327
+ data={chat.slice(0, shownMessages)}
328
+ inverted={true}
329
+ keyExtractor={keyExtractor}
330
+ renderItem={({ item }) => (
331
+ <ChatLine
332
+ item={item}
333
+ isHovered={hoveredMessageUri === item.uri}
334
+ onHoverIn={() => handleHoverIn(item.uri)}
335
+ onHoverOut={handleHoverOut}
336
+ hoverTimeoutRef={hoverTimeoutRef}
337
+ />
338
+ )}
339
+ removeClippedSubviews={true}
340
+ maxToRenderPerBatch={10}
341
+ initialNumToRender={10}
342
+ updateCellsBatchingPeriod={50}
343
+ onScroll={handleScroll}
344
+ scrollEventThrottle={16}
345
+ nestedScrollEnabled={true}
346
+ />
347
+ </ProfileCardProvider>
331
348
  <Reanimated.View
332
349
  style={[
333
350
  {
@@ -345,7 +362,7 @@ export function Chat({
345
362
  onPress={scrollToBottom}
346
363
  style={[
347
364
  {
348
- pointerEvents: "auto",
365
+ pointerEvents: isScrolledUp ? "auto" : "none",
349
366
  backgroundColor: theme.colors.primary,
350
367
  opacity: 0.9,
351
368
  borderRadius: 20,
@@ -4,46 +4,37 @@ import { Code, Text, View } from "../..";
4
4
  import { bg, layout, left, right, zIndex } from "../../lib/theme/atoms";
5
5
 
6
6
  export interface EmojiData {
7
- categories: Category[];
8
7
  emojis: { [key: string]: Emoji };
9
8
  aliases: { [key: string]: string };
10
- sheet: Sheet;
11
- }
12
-
13
- export interface Category {
14
- id: string;
15
- emojis: string[];
16
9
  }
17
10
 
18
11
  export interface Emoji {
19
12
  id: string;
20
- name: string;
21
- keywords: string[];
22
- skins: Skin[];
23
- version: number;
24
- emoticons?: string[];
13
+ m: string;
14
+ k: string[];
15
+ s: Skin[];
25
16
  }
26
17
 
27
18
  export interface Skin {
28
- unified: string;
29
- native: string;
30
- }
31
-
32
- export interface Sheet {
33
- cols: number;
34
- rows: number;
19
+ n: string;
35
20
  }
36
21
 
37
22
  interface EmojiSuggestionsProps {
38
23
  emojis: Emoji[];
39
24
  onSelect: (emoji: Emoji) => void;
40
25
  highlightedIndex: number;
26
+ skinTone?: number;
27
+ }
28
+
29
+ export function getSkinNative(emoji: Emoji, skinTone: number = 0): string {
30
+ return (emoji.s[skinTone] ?? emoji.s[0]).n;
41
31
  }
42
32
 
43
33
  export function EmojiSuggestions({
44
34
  emojis,
45
35
  onSelect,
46
36
  highlightedIndex,
37
+ skinTone = 0,
47
38
  }: EmojiSuggestionsProps) {
48
39
  if (!emojis || emojis.length === 0) {
49
40
  return null;
@@ -83,10 +74,10 @@ export function EmojiSuggestions({
83
74
  ]}
84
75
  >
85
76
  <Text style={{ fontSize: 16, marginRight: 8 }}>
86
- {emoji.skins[0]?.native}
77
+ {getSkinNative(emoji, skinTone)}
87
78
  </Text>
88
79
  <Text style={{ color: "white", fontSize: 14 }}>
89
- <Code style={[bg.gray[950]]}>:{emoji.id}:</Code> {emoji.name}
80
+ <Code style={[bg.gray[950]]}>:{emoji.id}:</Code> {emoji.m}
90
81
  </Text>
91
82
  </Pressable>
92
83
  ))}
@@ -1,7 +1,7 @@
1
1
  import { View } from "react-native";
2
2
  import { Main } from "streamplace/src/lexicons/types/place/stream/richtext/facet";
3
3
  import { SystemMessageType } from "../../lib/system-messages";
4
- import { colors, flex, gap, layout, ml, pb, pl, px, w } from "../../ui";
4
+ import { bg, colors, flex, gap, layout, ml, pb, pl, px, r, w } from "../../ui";
5
5
  import { Code, Text } from "../ui/text";
6
6
  import { RichTextMessage } from "./chat-message";
7
7
 
@@ -18,8 +18,18 @@ export function SystemMessage({
18
18
  timestamp,
19
19
  facets,
20
20
  }: SystemMessageProps) {
21
+ const isError = variant === SystemMessageType.command_error;
22
+
21
23
  return (
22
- <View style={[w.percent[100], px[2], pb[2]]}>
24
+ <View
25
+ style={[
26
+ w.percent[100],
27
+ px[2],
28
+ pb[2],
29
+ isError && bg.red[950],
30
+ isError && r.md,
31
+ ]}
32
+ >
23
33
  <Code color="muted" tracking="widest" style={[pl[12], ml[1]]}>
24
34
  SYSTEM MESSAGE
25
35
  </Code>
@@ -0,0 +1,310 @@
1
+ import { Check, X } from "lucide-react-native";
2
+ import React, { useEffect, useMemo, useState } from "react";
3
+ import { Image, Pressable, ScrollView, View } from "react-native";
4
+ import { PlaceStreamLivestream } from "streamplace";
5
+ import { useAvatars, zero } from "../..";
6
+ import { useStreamplaceStore } from "../../streamplace-store";
7
+ import { Button, Input, ResponsiveDialog, Text, useTheme } from "../ui";
8
+
9
+ interface TeleportModalProps {
10
+ open: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ onSubmit: (targetHandle: string, countdownSeconds: number) => void;
13
+ }
14
+
15
+ export const TeleportModal: React.FC<TeleportModalProps> = ({
16
+ open,
17
+ onOpenChange,
18
+ onSubmit,
19
+ }) => {
20
+ const [searchQuery, setSearchQuery] = useState("");
21
+ const [selectedStream, setSelectedStream] =
22
+ useState<PlaceStreamLivestream.LivestreamView | null>(null);
23
+ const [countdownSeconds, setCountdownSeconds] = useState("10");
24
+
25
+ const { theme } = useTheme();
26
+
27
+ const liveUsersCache = useStreamplaceStore((state) => state.liveUsers);
28
+ const liveUsersLoading = useStreamplaceStore(
29
+ (state) => state.liveUsersLoading,
30
+ );
31
+
32
+ const [liveUsers, setLiveUsers] = useState(liveUsersCache);
33
+
34
+ useEffect(() => {
35
+ setLiveUsers(liveUsersCache);
36
+ }, [liveUsersCache]);
37
+
38
+ const profiles = useAvatars(liveUsers?.map((u) => u.author?.did || "") || []);
39
+
40
+ const filteredStreams = useMemo(() => {
41
+ if (!liveUsers) return [];
42
+ if (!searchQuery.trim()) return liveUsers;
43
+
44
+ const query = searchQuery.toLowerCase();
45
+ // filter by handle or stream title
46
+ return liveUsers.filter(
47
+ (stream) =>
48
+ stream.author?.handle?.toLowerCase().includes(query) ||
49
+ stream.record.title?.toString().toLowerCase().includes(query),
50
+ );
51
+ }, [liveUsers, searchQuery]);
52
+
53
+ const handleCancel = () => {
54
+ setSearchQuery("");
55
+ setSelectedStream(null);
56
+ setCountdownSeconds("10");
57
+ onOpenChange(false);
58
+ };
59
+
60
+ const handleSubmit = () => {
61
+ if (!selectedStream?.author?.handle) return;
62
+
63
+ const countdown = parseInt(countdownSeconds, 10);
64
+ if (isNaN(countdown) || countdown < 5 || countdown > 300) {
65
+ return;
66
+ }
67
+
68
+ onSubmit(selectedStream.author.handle, countdown);
69
+ handleCancel();
70
+ };
71
+
72
+ return (
73
+ <ResponsiveDialog
74
+ open={open}
75
+ onOpenChange={onOpenChange}
76
+ showCloseButton={false}
77
+ variant="default"
78
+ size="xl"
79
+ dismissible={false}
80
+ >
81
+ <View style={[zero.py[2]]}>
82
+ <View style={[zero.layout.flex.row, zero.layout.flex.justify.between]}>
83
+ <View style={[zero.mb[4], zero.gap.all[1], zero.layout.flex.column]}>
84
+ <Text size="2xl">Teleport to another live streamer</Text>
85
+ <Text color="muted">
86
+ Select a streamer to teleport your viewers to their stream.
87
+ </Text>
88
+ </View>
89
+ <Pressable onPress={handleCancel} style={[{ padding: 8 }]}>
90
+ <X color={theme.colors.mutedForeground} />
91
+ </Pressable>
92
+ </View>
93
+ <View style={[zero.mb[4]]}>
94
+ <Input
95
+ value={searchQuery}
96
+ onChangeText={setSearchQuery}
97
+ placeholder="Search by handle..."
98
+ autoCapitalize="none"
99
+ autoCorrect={false}
100
+ />
101
+ </View>
102
+
103
+ {liveUsersLoading && !liveUsers ? (
104
+ <View style={[zero.py[8], { alignItems: "center" }]}>
105
+ <Text color="muted">Loading live users...</Text>
106
+ </View>
107
+ ) : filteredStreams.length === 0 ? (
108
+ <View style={[zero.py[8], { alignItems: "center" }]}>
109
+ <Text color="muted">
110
+ {searchQuery
111
+ ? "No matching live users found"
112
+ : "No live users found"}
113
+ </Text>
114
+ </View>
115
+ ) : (
116
+ <ScrollView style={[{ maxHeight: 400 }]}>
117
+ <View
118
+ style={[
119
+ {
120
+ flexDirection: "row",
121
+ flexWrap: "wrap",
122
+ gap: 12,
123
+ },
124
+ ]}
125
+ >
126
+ {filteredStreams.map((stream) => {
127
+ const isSelected = selectedStream?.uri === stream.uri;
128
+ const profile = profiles[stream.author?.did];
129
+
130
+ return (
131
+ <Pressable
132
+ key={stream.uri}
133
+ onPress={() => setSelectedStream(stream)}
134
+ style={[
135
+ {
136
+ width: "49.2%",
137
+ minWidth: 200,
138
+ },
139
+ ]}
140
+ >
141
+ <View
142
+ style={[
143
+ {
144
+ backgroundColor: theme.colors.muted,
145
+ borderRadius: 12,
146
+ overflow: "hidden",
147
+ borderWidth: 2,
148
+ borderColor: isSelected
149
+ ? theme.colors.primary
150
+ : "transparent",
151
+ },
152
+ ]}
153
+ >
154
+ <View
155
+ style={[
156
+ {
157
+ width: "100%",
158
+ aspectRatio: 16 / 9,
159
+ backgroundColor: theme.colors.card,
160
+ position: "relative",
161
+ },
162
+ ]}
163
+ >
164
+ <Image
165
+ source={{
166
+ uri:
167
+ "/api/playback/" +
168
+ stream.author.did +
169
+ "/stream.jpg",
170
+ }}
171
+ style={{
172
+ width: "100%",
173
+ height: "100%",
174
+ }}
175
+ resizeMode="cover"
176
+ />
177
+ {isSelected && (
178
+ <View
179
+ style={[
180
+ {
181
+ position: "absolute",
182
+ top: 8,
183
+ right: 8,
184
+ backgroundColor: theme.colors.primary,
185
+ borderRadius: 999,
186
+ width: 24,
187
+ height: 24,
188
+ alignItems: "center",
189
+ justifyContent: "center",
190
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.6)",
191
+ },
192
+ ]}
193
+ >
194
+ <Check size={16} color="white" />
195
+ </View>
196
+ )}
197
+ {stream.viewerCount && (
198
+ <View
199
+ style={[
200
+ {
201
+ position: "absolute",
202
+ top: 8,
203
+ left: 8,
204
+ backgroundColor: "rgba(0, 0, 0, 0.75)",
205
+ borderRadius: 999,
206
+ paddingHorizontal: 8,
207
+ paddingVertical: 4,
208
+ },
209
+ ]}
210
+ >
211
+ <Text style={[{ fontSize: 12, color: "white" }]}>
212
+ {stream.viewerCount.count} viewer
213
+ {stream.viewerCount.count !== 1 ? "s" : ""}
214
+ </Text>
215
+ </View>
216
+ )}
217
+ </View>
218
+ <View
219
+ style={[
220
+ {
221
+ padding: 12,
222
+ flexDirection: "row",
223
+ gap: 8,
224
+ alignItems: "center",
225
+ },
226
+ ]}
227
+ >
228
+ <View
229
+ style={[
230
+ {
231
+ width: 40,
232
+ height: 40,
233
+ borderRadius: 20,
234
+ overflow: "hidden",
235
+ backgroundColor: theme.colors.card,
236
+ flexShrink: 0,
237
+ },
238
+ ]}
239
+ >
240
+ {profile?.avatar ? (
241
+ <Image
242
+ source={{
243
+ uri: profile.avatar,
244
+ }}
245
+ style={{ width: "100%", height: "100%" }}
246
+ resizeMode="cover"
247
+ />
248
+ ) : (
249
+ <View
250
+ style={{
251
+ width: "100%",
252
+ height: "100%",
253
+ backgroundColor: theme.colors.muted,
254
+ }}
255
+ />
256
+ )}
257
+ </View>
258
+
259
+ {/* Text */}
260
+ <View style={[{ flex: 1, minWidth: 0 }]}>
261
+ <Text numberOfLines={1} ellipsizeMode="tail">
262
+ {stream.author?.handle}
263
+ </Text>
264
+ {stream.record.title ? (
265
+ <Text
266
+ numberOfLines={1}
267
+ ellipsizeMode="tail"
268
+ style={[
269
+ {
270
+ fontSize: 12,
271
+ color: theme.colors.textMuted,
272
+ },
273
+ ]}
274
+ >
275
+ {stream.record.title as any}
276
+ </Text>
277
+ ) : null}
278
+ </View>
279
+ </View>
280
+ </View>
281
+ </Pressable>
282
+ );
283
+ })}
284
+ </View>
285
+ </ScrollView>
286
+ )}
287
+ </View>
288
+ <View
289
+ style={[
290
+ zero.mt[8],
291
+ zero.layout.flex.row,
292
+ zero.layout.flex.justify.end,
293
+ zero.gap.all[2],
294
+ ]}
295
+ >
296
+ <Button width="min" variant="secondary" onPress={handleCancel}>
297
+ <Text>Cancel</Text>
298
+ </Button>
299
+ <Button
300
+ width="min"
301
+ variant="primary"
302
+ onPress={handleSubmit}
303
+ disabled={!selectedStream}
304
+ >
305
+ <Text>Teleport</Text>
306
+ </Button>
307
+ </View>
308
+ </ResponsiveDialog>
309
+ );
310
+ };