@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
@@ -1,33 +1,36 @@
1
1
  import Picker from "@emoji-mart/react";
2
2
  import { AtSignIcon, ExternalLink, X } from "lucide-react-native";
3
+ import { env } from "process";
3
4
  import { useEffect, useMemo, useRef, useState } from "react";
4
5
  import { Platform, Pressable, TextInput } from "react-native";
5
6
  import { ChatMessageViewHydrated } from "streamplace";
7
+ import { Button, Loader, Text, useTheme, View } from "../../";
8
+ import { handleSlashCommand } from "../../lib/slash-commands";
9
+ import { registerTeleportCommand } from "../../lib/slash-commands/teleport";
10
+ import { StreamNotifications } from "../../lib/stream-notifications";
11
+ import { SystemMessages } from "../../lib/system-messages";
6
12
  import {
7
- Button,
8
- Loader,
9
- Text,
10
- useChat,
11
- useCreateChatMessage,
12
- useLivestream,
13
- useReplyToMessage,
14
- useSetReplyToMessage,
15
- useTheme,
16
- View,
17
- } from "../../";
18
- import {
19
- bg,
13
+ borders,
20
14
  flex,
21
15
  gap,
22
16
  h,
23
17
  layout,
24
18
  mb,
25
- mr,
26
19
  pl,
27
20
  pr,
28
21
  py,
22
+ r,
29
23
  w,
30
24
  } from "../../lib/theme/atoms";
25
+ import {
26
+ useChat,
27
+ useCreateChatMessage,
28
+ useLivestream,
29
+ useLivestreamStore,
30
+ useReplyToMessage,
31
+ useSetReplyToMessage,
32
+ } from "../../livestream-store";
33
+ import { useDID, usePDSAgent } from "../../streamplace-store";
31
34
  import { Textarea } from "../ui/textarea";
32
35
  import { RenderChatMessage } from "./chat-message";
33
36
  import { EmojiData, EmojiSuggestions } from "./emoji-suggestions";
@@ -65,7 +68,7 @@ export function ChatBox({
65
68
 
66
69
  let linfo = useLivestream();
67
70
 
68
- const { theme } = useTheme();
71
+ const { theme, zero: zt } = useTheme();
69
72
 
70
73
  const chat = useChat();
71
74
  const createChatMessage = useCreateChatMessage();
@@ -73,6 +76,18 @@ export function ChatBox({
73
76
  const setReplyToMessage = useSetReplyToMessage();
74
77
  const textAreaRef = useRef<TextInput>(null);
75
78
 
79
+ const pdsAgent = usePDSAgent();
80
+ const userDID = useDID();
81
+ const setActiveTeleportUri = useLivestreamStore(
82
+ (state) => state.setActiveTeleportUri,
83
+ );
84
+
85
+ useEffect(() => {
86
+ if (pdsAgent && userDID) {
87
+ registerTeleportCommand(pdsAgent, userDID, setActiveTeleportUri);
88
+ }
89
+ }, [pdsAgent, userDID, setActiveTeleportUri]);
90
+
76
91
  const authors = useMemo(() => {
77
92
  if (!chat) return null;
78
93
  return chat.reduce((acc, msg) => {
@@ -84,6 +99,12 @@ export function ChatBox({
84
99
  }, new Map<string, ChatMessageViewHydrated["chatProfile"]>());
85
100
  }, [chat]);
86
101
 
102
+ useEffect(() => {
103
+ if (pdsAgent && linfo?.author?.did && pdsAgent.did === linfo.author.did) {
104
+ registerTeleportCommand(pdsAgent, pdsAgent.did, setActiveTeleportUri);
105
+ }
106
+ }, [pdsAgent, linfo?.author?.did, setActiveTeleportUri]);
107
+
87
108
  const handleMentionSelect = (handle: string) => {
88
109
  const beforeAt = message.slice(0, message.lastIndexOf("@"));
89
110
  setMessage(`${beforeAt}@${handle} `);
@@ -117,7 +138,7 @@ export function ChatBox({
117
138
  const colonIndex = text.lastIndexOf(":");
118
139
  if (colonIndex !== -1) {
119
140
  const searchText = text.slice(colonIndex + 1).toLowerCase();
120
- if (searchText.length > 0) {
141
+ if (searchText.length >= 3) {
121
142
  if (!emojiData) return;
122
143
  const aliasMatches = Object.entries(emojiData.aliases)
123
144
  .map(([alias, emojiId]) => {
@@ -232,20 +253,44 @@ export function ChatBox({
232
253
  }
233
254
  };
234
255
 
235
- const submit = () => {
256
+ const submit = async () => {
236
257
  if (!message.trim()) return;
258
+
259
+ const messageText = message;
237
260
  setMessage("");
238
261
  setReplyToMessage(null);
239
262
 
263
+ if (messageText.startsWith("/")) {
264
+ const result = await handleSlashCommand(messageText);
265
+ if (result.handled) {
266
+ if (result.error) {
267
+ console.error("Slash command error:", result.error);
268
+ SystemMessages.commandError(result.error);
269
+ }
270
+ return;
271
+ }
272
+ }
240
273
  setSubmitting(true);
241
- createChatMessage({
242
- text: message,
243
- reply: replyTo || undefined,
244
- });
245
- setSubmitting(false);
246
274
 
247
- // if we press "send" button, we want the same action as pressing "Enter"
248
- // if we're already focused no need to do extra work
275
+ try {
276
+ const result = await handleSlashCommand(messageText);
277
+
278
+ if (result.handled) {
279
+ if (result.error) {
280
+ console.error("Slash command error:", result.error);
281
+ }
282
+ } else {
283
+ createChatMessage({
284
+ text: messageText,
285
+ reply: replyTo || undefined,
286
+ });
287
+ }
288
+ } catch (err) {
289
+ console.error("Error submitting message:", err);
290
+ } finally {
291
+ setSubmitting(false);
292
+ }
293
+
249
294
  if (textAreaRef.current && !textAreaRef.current.isFocused()) {
250
295
  textAreaRef.current.focus();
251
296
  requestAnimationFrame(() => {
@@ -268,38 +313,50 @@ export function ChatBox({
268
313
  layout.flex.alignCenter,
269
314
  layout.flex.spaceBetween,
270
315
  pl[2],
271
- pr[6],
272
- mr[6],
316
+ pr[1],
273
317
  mb[2],
274
318
  py[1],
275
- bg.gray[800],
276
- { borderRadius: 16 },
319
+ r["2xl"],
320
+ zt.bg.card,
277
321
  ]}
278
322
  >
279
- <RenderChatMessage
280
- item={replyTo}
281
- showReply={false}
282
- userCache={authors || new Map()}
283
- />
284
- <Pressable onPress={() => setReplyToMessage(null)}>
285
- <View
286
- style={[
287
- layout.flex.row,
288
- layout.flex.alignCenter,
289
- layout.flex.justifyCenter,
290
- h[12],
291
- w[12],
292
- bg.gray[600],
293
- { borderRadius: 999 },
294
- ]}
295
- >
296
- <X size={24} />
297
- </View>
323
+ <View style={{ flex: 1, minWidth: 0, marginRight: 8 }}>
324
+ <RenderChatMessage
325
+ item={replyTo}
326
+ showReply={false}
327
+ userCache={authors || new Map()}
328
+ />
329
+ </View>
330
+ <Pressable
331
+ onPress={() => setReplyToMessage(null)}
332
+ style={[
333
+ layout.flex.row,
334
+ layout.flex.alignCenter,
335
+ layout.flex.justifyCenter,
336
+ h[8],
337
+ w[8],
338
+ zt.bg.muted,
339
+ zt.border.border,
340
+ borders.width.thin,
341
+ { borderRadius: 999 },
342
+ ]}
343
+ >
344
+ <X size={24} style={[zt.text.primaryForeground]} />
298
345
  </Pressable>
299
346
  </View>
300
347
  )}
301
348
  {showEmojiSelector && (
302
- <>
349
+ <View
350
+ style={{
351
+ position: "absolute",
352
+ top: 0,
353
+ left: 0,
354
+ right: 0,
355
+ bottom: 0,
356
+ zIndex: 200,
357
+ }}
358
+ pointerEvents="box-none"
359
+ >
303
360
  {/* Overlay to catch outside clicks */}
304
361
  <Pressable
305
362
  style={{
@@ -308,7 +365,6 @@ export function ChatBox({
308
365
  left: 0,
309
366
  right: 0,
310
367
  bottom: 0,
311
- zIndex: 200,
312
368
  }}
313
369
  onPress={() => setShowEmojiSelector(false)}
314
370
  />
@@ -319,13 +375,14 @@ export function ChatBox({
319
375
  left: 0,
320
376
  zIndex: 2001,
321
377
  }}
378
+ pointerEvents="auto"
322
379
  >
323
380
  <Picker
324
381
  data={emojiData}
325
382
  onEmojiSelect={(e) => setMessage(message + e.native)}
326
383
  />
327
384
  </View>
328
- </>
385
+ </View>
329
386
  )}
330
387
  <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
331
388
  <Textarea
@@ -440,19 +497,35 @@ export function ChatBox({
440
497
  { justifyContent: "flex-end" },
441
498
  ]}
442
499
  >
500
+ {env.NODE_ENV === "development" && (
501
+ <Button
502
+ variant="secondary"
503
+ style={{ borderRadius: 16 }}
504
+ width="min"
505
+ onPress={() => {
506
+ StreamNotifications.teleport({
507
+ targetHandle: "test.bsky.social",
508
+ targetDID: "did:plc:test",
509
+ countdown: 30,
510
+ canCancel: true,
511
+ onDismiss: (reason) =>
512
+ console.log("teleport dismissed:", reason),
513
+ });
514
+ }}
515
+ >
516
+ Test Notification
517
+ </Button>
518
+ )}
443
519
  <Button
444
520
  variant="secondary"
445
521
  style={{ borderRadius: 16, maxWidth: 44, aspectRatio: 1 }}
446
522
  aria-label="Insert Mention"
447
523
  onPress={() => {
448
- // if the last character is not @, add it
449
524
  !message.endsWith("@") && setMessage(message + "@");
450
- // get all the text after the last @
451
525
  const atIndex = message.lastIndexOf("@");
452
526
  const searchText = message.slice(atIndex + 1).toLowerCase();
453
527
  updateSuggestions(searchText);
454
528
  setShowSuggestions(true);
455
- // focus the textarea
456
529
  textAreaRef.current?.focus();
457
530
  }}
458
531
  >
@@ -76,7 +76,7 @@ const segmentedObject = (
76
76
  }
77
77
  };
78
78
 
79
- const RichTextMessage = ({
79
+ export const RichTextMessage = ({
80
80
  text,
81
81
  facets,
82
82
  }: {
@@ -1,4 +1,4 @@
1
- import { Ellipsis, Reply } from "lucide-react-native";
1
+ import { ChevronDown, Ellipsis, Reply } from "lucide-react-native";
2
2
  import { ComponentProps, memo, useEffect, useRef, useState } from "react";
3
3
  import { Keyboard, Platform, Pressable } from "react-native";
4
4
  import { FlatList } from "react-native-gesture-handler";
@@ -8,17 +8,22 @@ import Swipeable, {
8
8
  import Reanimated, {
9
9
  SharedValue,
10
10
  useAnimatedStyle,
11
+ useSharedValue,
12
+ withTiming,
11
13
  } from "react-native-reanimated";
12
14
  import { ChatMessageViewHydrated } from "streamplace";
13
15
  import {
16
+ getSystemMessageType,
14
17
  SystemMessage,
18
+ SystemMessageType,
15
19
  Text,
16
20
  useChat,
17
21
  usePlayerStore,
18
22
  useSetReplyToMessage,
23
+ useTheme,
19
24
  View,
20
25
  } from "../../";
21
- import { bg, flex, px, py } from "../../lib/theme/atoms";
26
+ import { bg, flex, layout, mr, px, py } from "../../lib/theme/atoms";
22
27
  import { RenderChatMessage } from "./chat-message";
23
28
  import { ModView } from "./mod-view";
24
29
 
@@ -168,8 +173,10 @@ const ChatLine = memo(({ item }: { item: ChatMessageViewHydrated }) => {
168
173
  if (item.author.did === "did:sys:system") {
169
174
  return (
170
175
  <SystemMessage
176
+ variant={getSystemMessageType(item) || SystemMessageType.notification}
171
177
  timestamp={new Date(item.record.createdAt)}
172
178
  title={item.record.text}
179
+ facets={item.record.facets}
173
180
  />
174
181
  );
175
182
  }
@@ -243,8 +250,30 @@ export function Chat({
243
250
  shownMessages?: number;
244
251
  style?: ComponentProps<typeof View>["style"];
245
252
  }) {
253
+ const { theme } = useTheme();
246
254
  const chat = useChat();
247
255
  const [isScrolledUp, setIsScrolledUp] = useState(false);
256
+ const flatListRef = useRef<FlatList>(null);
257
+
258
+ // Animation for scroll-to-bottom button
259
+ const buttonOpacity = useSharedValue(0);
260
+ const buttonTranslateY = useSharedValue(20);
261
+
262
+ useEffect(() => {
263
+ buttonOpacity.value = withTiming(isScrolledUp ? 1 : 0, { duration: 200 });
264
+ buttonTranslateY.value = withTiming(isScrolledUp ? 0 : 20, {
265
+ duration: 200,
266
+ });
267
+ }, [isScrolledUp]);
268
+
269
+ const buttonAnimatedStyle = useAnimatedStyle(() => ({
270
+ opacity: buttonOpacity.value,
271
+ transform: [{ translateY: buttonTranslateY.value }],
272
+ }));
273
+
274
+ const scrollToBottom = () => {
275
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
276
+ };
248
277
 
249
278
  const handleScroll = (event: any) => {
250
279
  const { contentOffset } = event.nativeEvent;
@@ -270,11 +299,18 @@ export function Chat({
270
299
 
271
300
  return (
272
301
  <View
273
- style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }].concat(
274
- propsStyle || [],
275
- )}
302
+ style={[
303
+ flex.shrink[1],
304
+ {
305
+ minWidth: 0,
306
+ maxWidth: "100%",
307
+ position: "relative",
308
+ overflow: "visible",
309
+ },
310
+ ].concat(propsStyle || [])}
276
311
  >
277
312
  <FlatList
313
+ ref={flatListRef}
278
314
  style={[
279
315
  flex.grow[1],
280
316
  flex.shrink[1],
@@ -292,6 +328,44 @@ export function Chat({
292
328
  scrollEventThrottle={16}
293
329
  nestedScrollEnabled={true}
294
330
  />
331
+ <Reanimated.View
332
+ style={[
333
+ {
334
+ position: "absolute",
335
+ bottom: 16,
336
+ left: 0,
337
+ right: 0,
338
+ alignItems: "center",
339
+ pointerEvents: isScrolledUp ? "box-none" : "none",
340
+ },
341
+ buttonAnimatedStyle,
342
+ ]}
343
+ >
344
+ <Pressable
345
+ onPress={scrollToBottom}
346
+ style={[
347
+ {
348
+ pointerEvents: "auto",
349
+ backgroundColor: theme.colors.primary,
350
+ opacity: 0.9,
351
+ borderRadius: 20,
352
+ shadowColor: "#000",
353
+ shadowOffset: { width: 0, height: 2 },
354
+ shadowOpacity: 0.25,
355
+ shadowRadius: 4,
356
+ elevation: 5,
357
+ },
358
+ layout.flex.row,
359
+ layout.flex.center,
360
+ px[2],
361
+ py[1],
362
+ { gap: 6 },
363
+ ]}
364
+ >
365
+ <ChevronDown size={24} style={{ marginTop: 2 }} color="white" />
366
+ <Text style={[mr[1]]}>Scroll to bottom</Text>
367
+ </Pressable>
368
+ </Reanimated.View>
295
369
  <ModView />
296
370
  </View>
297
371
  );
@@ -1,4 +1,5 @@
1
1
  import { Pressable } from "react-native";
2
+ import { ScrollView } from "react-native-gesture-handler";
2
3
  import { Code, Text, View } from "../..";
3
4
  import { bg, layout, left, right, zIndex } from "../../lib/theme/atoms";
4
5
 
@@ -61,34 +62,35 @@ export function EmojiSuggestions({
61
62
  borderRadius: 8,
62
63
  boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
63
64
  maxHeight: 200,
64
- overflow: "auto",
65
65
  },
66
66
  ]}
67
67
  >
68
- {emojis.map((emoji, index) => (
69
- <Pressable
70
- key={emoji.id}
71
- onPress={() => onSelect(emoji)}
72
- style={[
73
- {
74
- padding: 8,
75
- flexDirection: "row",
76
- alignItems: "center",
77
- backgroundColor:
78
- index === highlightedIndex
79
- ? "rgba(255, 255, 255, 0.1)"
80
- : "transparent",
81
- },
82
- ]}
83
- >
84
- <Text style={{ fontSize: 16, marginRight: 8 }}>
85
- {emoji.skins[0]?.native}
86
- </Text>
87
- <Text style={{ color: "white", fontSize: 14 }}>
88
- <Code style={[bg.gray[950]]}>:{emoji.id}:</Code> {emoji.name}
89
- </Text>
90
- </Pressable>
91
- ))}
68
+ <ScrollView>
69
+ {emojis.map((emoji, index) => (
70
+ <Pressable
71
+ key={emoji.id}
72
+ onPress={() => onSelect(emoji)}
73
+ style={[
74
+ {
75
+ padding: 8,
76
+ flexDirection: "row",
77
+ alignItems: "center",
78
+ backgroundColor:
79
+ index === highlightedIndex
80
+ ? "rgba(255, 255, 255, 0.1)"
81
+ : "transparent",
82
+ },
83
+ ]}
84
+ >
85
+ <Text style={{ fontSize: 16, marginRight: 8 }}>
86
+ {emoji.skins[0]?.native}
87
+ </Text>
88
+ <Text style={{ color: "white", fontSize: 14 }}>
89
+ <Code style={[bg.gray[950]]}>:{emoji.id}:</Code> {emoji.name}
90
+ </Text>
91
+ </Pressable>
92
+ ))}
93
+ </ScrollView>
92
94
  </View>
93
95
  );
94
96
  }
@@ -1,7 +1,7 @@
1
- import { Pressable } from "react-native";
1
+ import { Pressable, ScrollView } from "react-native";
2
2
  import { ChatMessageViewHydrated } from "streamplace";
3
3
  import { Text, View } from "../..";
4
- import { bg, layout, left, right, zIndex } from "../../lib/theme/atoms";
4
+ import { bg, layout, left, right } from "../../lib/theme/atoms";
5
5
 
6
6
  interface MentionSuggestionsProps {
7
7
  authors: Map<string, ChatMessageViewHydrated["chatProfile"]>;
@@ -27,45 +27,48 @@ export function MentionSuggestions({
27
27
 
28
28
  left[0],
29
29
  right[0],
30
- zIndex[50],
31
30
  {
32
31
  bottom: "100%",
33
32
  borderRadius: 8,
34
33
  boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
34
+ maxHeight: 200,
35
+ zIndex: 999999,
35
36
  },
36
37
  ]}
37
38
  >
38
- {authorHandles.map((handle, index) => {
39
- let profile = authors.get(handle);
40
- return (
41
- <Pressable
42
- key={handle}
43
- onPress={() => onSelect(handle)}
44
- style={[
45
- {
46
- padding: 8,
47
- flexDirection: "row",
48
- alignItems: "center",
49
- backgroundColor:
50
- index === highlightedIndex
51
- ? "rgba(0, 0, 0, 0.1)"
52
- : "rgba(0, 0, 0, 0.5)",
53
- },
54
- ]}
55
- >
56
- <Text
57
- style={{
58
- color: profile?.color
59
- ? `rgb(${profile.color.red}, ${profile.color.green}, ${profile.color.blue})`
60
- : "black",
61
- fontWeight: "bold",
62
- }}
39
+ <ScrollView>
40
+ {authorHandles.map((handle, index) => {
41
+ let profile = authors.get(handle);
42
+ return (
43
+ <Pressable
44
+ key={handle}
45
+ onPress={() => onSelect(handle)}
46
+ style={[
47
+ {
48
+ padding: 8,
49
+ flexDirection: "row",
50
+ alignItems: "center",
51
+ backgroundColor:
52
+ index === highlightedIndex
53
+ ? "rgba(0, 0, 0, 0.1)"
54
+ : "rgba(0, 0, 0, 0.5)",
55
+ },
56
+ ]}
63
57
  >
64
- @{handle}
65
- </Text>
66
- </Pressable>
67
- );
68
- })}
58
+ <Text
59
+ style={{
60
+ color: profile?.color
61
+ ? `rgb(${profile.color.red}, ${profile.color.green}, ${profile.color.blue})`
62
+ : "black",
63
+ fontWeight: "bold",
64
+ }}
65
+ >
66
+ @{handle}
67
+ </Text>
68
+ </Pressable>
69
+ );
70
+ })}
71
+ </ScrollView>
69
72
  </View>
70
73
  );
71
74
  }
@@ -95,17 +95,6 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
95
95
  }
96
96
  }, [message]);
97
97
 
98
- // Early return AFTER all hooks have been called
99
- if (!agent?.did) {
100
- return <></>;
101
- }
102
-
103
- // Can show moderation actions if user can hide, ban, or manage livestream
104
- const canModerate =
105
- modPermissions.canHide ||
106
- modPermissions.canBan ||
107
- modPermissions.canManageLivestream;
108
-
109
98
  // Check if any moderation actions are actually available for this message
110
99
  // This must match the individual action checks inside the DropdownMenuGroup
111
100
  const hasAvailableActions = !!(
@@ -328,14 +317,14 @@ function ModViewContent({
328
317
  >
329
318
  <Text color="primary">View user on {BSKY_FRONTEND_DOMAIN}</Text>
330
319
  </DropdownMenuItem>
331
- {message.author.did === agent?.did && (
320
+ {agent?.did && message.author.did === agent.did && (
332
321
  <DeleteButton
333
322
  message={message}
334
323
  deleteChatMessage={deleteChatMessage}
335
324
  onOpenChange={onOpenChange}
336
325
  />
337
326
  )}
338
- {message.author.did !== agent?.did && (
327
+ {(!agent?.did || message.author.did !== agent.did) && (
339
328
  <ReportButton
340
329
  message={message}
341
330
  setReportModalOpen={setReportModalOpen}
@@ -1,14 +1,23 @@
1
1
  import { View } from "react-native";
2
- import { flex, gap, layout, ml, pb, pl, px, w } from "../../ui";
3
- import { atoms } from "../ui";
2
+ import { Main } from "streamplace/src/lexicons/types/place/stream/richtext/facet";
3
+ import { SystemMessageType } from "../../lib/system-messages";
4
+ import { colors, flex, gap, layout, ml, pb, pl, px, w } from "../../ui";
4
5
  import { Code, Text } from "../ui/text";
6
+ import { RichTextMessage } from "./chat-message";
5
7
 
6
8
  interface SystemMessageProps {
9
+ variant: SystemMessageType;
7
10
  title: string;
8
11
  timestamp: Date;
12
+ facets?: Main[];
9
13
  }
10
14
 
11
- export function SystemMessage({ title, timestamp }: SystemMessageProps) {
15
+ export function SystemMessage({
16
+ variant,
17
+ title,
18
+ timestamp,
19
+ facets,
20
+ }: SystemMessageProps) {
12
21
  return (
13
22
  <View style={[w.percent[100], px[2], pb[2]]}>
14
23
  <Code color="muted" tracking="widest" style={[pl[12], ml[1]]}>
@@ -18,7 +27,7 @@ export function SystemMessage({ title, timestamp }: SystemMessageProps) {
18
27
  <Text
19
28
  style={{
20
29
  fontVariant: ["tabular-nums"],
21
- color: atoms.colors.gray[300],
30
+ color: colors.gray[400],
22
31
  }}
23
32
  >
24
33
  {timestamp.toLocaleTimeString([], {
@@ -28,7 +37,7 @@ export function SystemMessage({ title, timestamp }: SystemMessageProps) {
28
37
  })}
29
38
  </Text>
30
39
  <Text weight="bold" color="default" style={[flex.shrink[1]]}>
31
- {title}
40
+ <RichTextMessage facets={facets} text={title} />
32
41
  </Text>
33
42
  </View>
34
43
  </View>
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
- import { PlayerProtocol, useStreamplaceStore } from "../..";
2
+ import { PlayerProtocol } from "../../player-store/player-state";
3
+ import { useStreamplaceStore } from "../../streamplace-store";
3
4
 
4
5
  const protocolSuffixes = {
5
6
  m3u8: PlayerProtocol.HLS,