@streamplace/components 0.7.2 → 0.7.7

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 (91) hide show
  1. package/dist/components/chat/chat-box.js +212 -24
  2. package/dist/components/chat/chat-message.js +5 -5
  3. package/dist/components/chat/chat.js +83 -5
  4. package/dist/components/chat/emoji-suggestions.js +35 -0
  5. package/dist/components/chat/mod-view.js +59 -8
  6. package/dist/components/chat/system-message.js +19 -0
  7. package/dist/components/icons/bluesky-icon.js +9 -0
  8. package/dist/components/keep-awake.js +7 -0
  9. package/dist/components/keep-awake.native.js +16 -0
  10. package/dist/components/mobile-player/fullscreen.js +2 -1
  11. package/dist/components/mobile-player/fullscreen.native.js +3 -3
  12. package/dist/components/mobile-player/player.js +15 -30
  13. package/dist/components/mobile-player/ui/index.js +2 -0
  14. package/dist/components/mobile-player/ui/report-modal.js +90 -0
  15. package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
  17. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
  18. package/dist/components/mobile-player/use-webrtc.js +7 -1
  19. package/dist/components/mobile-player/video-retry.js +29 -0
  20. package/dist/components/mobile-player/video.js +84 -9
  21. package/dist/components/mobile-player/video.native.js +24 -10
  22. package/dist/components/share/sharesheet.js +91 -0
  23. package/dist/components/ui/dialog.js +1 -1
  24. package/dist/components/ui/dropdown.js +6 -6
  25. package/dist/components/ui/index.js +2 -0
  26. package/dist/components/ui/primitives/modal.js +0 -1
  27. package/dist/components/ui/resizeable.js +20 -11
  28. package/dist/components/ui/slider.js +5 -0
  29. package/dist/hooks/index.js +1 -0
  30. package/dist/hooks/usePointerDevice.js +71 -0
  31. package/dist/index.js +10 -3
  32. package/dist/lib/system-messages.js +101 -0
  33. package/dist/livestream-store/chat.js +111 -18
  34. package/dist/livestream-store/livestream-store.js +3 -0
  35. package/dist/livestream-store/problems.js +76 -0
  36. package/dist/livestream-store/websocket-consumer.js +39 -4
  37. package/dist/player-store/player-store.js +33 -4
  38. package/dist/streamplace-store/block.js +51 -12
  39. package/dist/streamplace-store/stream.js +44 -23
  40. package/dist/ui/index.js +79 -0
  41. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  42. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  43. package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/67b1eb60 +0 -0
  44. package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/7c275f90 +0 -0
  45. package/package.json +6 -2
  46. package/src/components/chat/chat-box.tsx +295 -25
  47. package/src/components/chat/chat-message.tsx +6 -7
  48. package/src/components/chat/chat.tsx +192 -41
  49. package/src/components/chat/emoji-suggestions.tsx +94 -0
  50. package/src/components/chat/mod-view.tsx +119 -40
  51. package/src/components/chat/system-message.tsx +38 -0
  52. package/src/components/icons/bluesky-icon.tsx +9 -0
  53. package/src/components/keep-awake.native.tsx +13 -0
  54. package/src/components/keep-awake.tsx +3 -0
  55. package/src/components/mobile-player/fullscreen.native.tsx +12 -3
  56. package/src/components/mobile-player/fullscreen.tsx +10 -3
  57. package/src/components/mobile-player/player.tsx +28 -36
  58. package/src/components/mobile-player/props.tsx +1 -0
  59. package/src/components/mobile-player/ui/index.ts +2 -0
  60. package/src/components/mobile-player/ui/report-modal.tsx +195 -0
  61. package/src/components/mobile-player/ui/streamer-loading-overlay.tsx +154 -0
  62. package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
  63. package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
  64. package/src/components/mobile-player/use-webrtc.tsx +10 -2
  65. package/src/components/mobile-player/video-retry.tsx +28 -0
  66. package/src/components/mobile-player/video.native.tsx +24 -10
  67. package/src/components/mobile-player/video.tsx +100 -21
  68. package/src/components/share/sharesheet.tsx +185 -0
  69. package/src/components/ui/dialog.tsx +1 -1
  70. package/src/components/ui/dropdown.tsx +13 -13
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/primitives/modal.tsx +0 -1
  73. package/src/components/ui/resizeable.tsx +26 -15
  74. package/src/components/ui/slider.tsx +1 -0
  75. package/src/hooks/index.ts +1 -0
  76. package/src/hooks/usePointerDevice.ts +89 -0
  77. package/src/index.tsx +11 -2
  78. package/src/lib/system-messages.ts +135 -0
  79. package/src/livestream-store/chat.tsx +145 -17
  80. package/src/livestream-store/livestream-state.tsx +10 -0
  81. package/src/livestream-store/livestream-store.tsx +3 -0
  82. package/src/livestream-store/problems.tsx +96 -0
  83. package/src/livestream-store/websocket-consumer.tsx +44 -4
  84. package/src/player-store/player-state.tsx +25 -4
  85. package/src/player-store/player-store.tsx +43 -5
  86. package/src/streamplace-store/block.tsx +55 -13
  87. package/src/streamplace-store/stream.tsx +66 -35
  88. package/src/ui/index.ts +86 -0
  89. package/tsconfig.tsbuildinfo +1 -1
  90. package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
  91. package/node-compile-cache/v22.15.0-x64-92db9086-0/56540125 +0 -0
@@ -1,5 +1,5 @@
1
- import { Reply, ShieldEllipsis } from "lucide-react-native";
2
- import { ComponentProps, memo, useRef } from "react";
1
+ import { Ellipsis, Reply, ShieldEllipsis } from "lucide-react-native";
2
+ import { ComponentProps, memo, useEffect, useRef, useState } from "react";
3
3
  import { FlatList, Platform, Pressable } from "react-native";
4
4
  import Swipeable, {
5
5
  SwipeableMethods,
@@ -10,13 +10,14 @@ import Reanimated, {
10
10
  } from "react-native-reanimated";
11
11
  import { ChatMessageViewHydrated } from "streamplace";
12
12
  import {
13
+ SystemMessage,
13
14
  Text,
14
15
  useChat,
15
16
  usePlayerStore,
16
17
  useSetReplyToMessage,
17
18
  View,
18
19
  } from "../../";
19
- import { flex, py, w } from "../../lib/theme/atoms";
20
+ import { bg, flex, px, py, w } from "../../lib/theme/atoms";
20
21
  import { RenderChatMessage } from "./chat-message";
21
22
  import { ModView } from "./mod-view";
22
23
 
@@ -48,57 +49,205 @@ function LeftAction(prog: SharedValue<number>, drag: SharedValue<number>) {
48
49
  );
49
50
  }
50
51
 
52
+ // ios/android, 25, else 100 msgs
51
53
  const SHOWN_MSGS =
52
- Platform.OS === "android" || Platform.OS === "ios" ? 100 : 25;
54
+ Platform.OS === "ios" || Platform.OS === "android" ? 25 : 100;
53
55
 
54
56
  const keyExtractor = (item: ChatMessageViewHydrated, index: number) => {
55
57
  return `${item.uri}`;
56
58
  };
57
59
 
58
- const ChatLine = memo(({ item }: { item: ChatMessageViewHydrated }) => {
59
- const setReply = useSetReplyToMessage();
60
- const setModMsg = usePlayerStore((state) => state.setModMessage);
61
- const swipeableRef = useRef<SwipeableMethods | null>(null);
62
- return (
63
- <Pressable onLongPress={() => setModMsg(item)}>
64
- <Swipeable
65
- containerStyle={[py[1]]}
66
- friction={2}
67
- enableTrackpadTwoFingerGesture
68
- rightThreshold={40}
69
- renderRightActions={Platform.OS === "android" ? undefined : RightAction}
70
- renderLeftActions={Platform.OS === "android" ? undefined : LeftAction}
71
- overshootFriction={9}
72
- ref={(ref) => {
73
- swipeableRef.current = ref;
74
- }}
75
- onSwipeableOpen={(r) => {
76
- if (r === (Platform.OS === "android" ? "right" : "left")) {
77
- setReply(item);
78
- }
79
- if (r === (Platform.OS === "android" ? "left" : "right")) {
80
- setModMsg(item);
81
- }
82
- // close this swipeable
83
- const swipeable = swipeableRef.current;
84
- if (swipeable) {
85
- swipeable.close();
86
- }
87
- }}
60
+ // Actions bar for larger screens
61
+ const ActionsBar = memo(
62
+ ({
63
+ item,
64
+ visible,
65
+ hoverTimeoutRef,
66
+ }: {
67
+ item: ChatMessageViewHydrated;
68
+ visible: boolean;
69
+ hoverTimeoutRef: React.MutableRefObject<NodeJS.Timeout | null>;
70
+ }) => {
71
+ const setReply = useSetReplyToMessage();
72
+ const setModMsg = usePlayerStore((state) => state.setModMessage);
73
+
74
+ if (!visible) return null;
75
+
76
+ return (
77
+ <View
78
+ style={[
79
+ {
80
+ position: "absolute",
81
+ top: -14,
82
+ right: 8,
83
+ flexDirection: "row",
84
+ backgroundColor: "rgba(180,180,180, 0.5)",
85
+ borderRadius: 6,
86
+ borderWidth: 1,
87
+ padding: 1,
88
+ gap: 4,
89
+ zIndex: 10,
90
+ },
91
+ ]}
88
92
  >
89
- <RenderChatMessage item={item} />
90
- </Swipeable>
91
- </Pressable>
92
- );
93
- });
93
+ <Pressable
94
+ onPress={() => setReply(item)}
95
+ style={[
96
+ {
97
+ padding: 6,
98
+ borderRadius: 4,
99
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
100
+ },
101
+ ]}
102
+ onHoverIn={() => {
103
+ // Keep the actions bar visible when hovering over it
104
+ if (hoverTimeoutRef.current) {
105
+ clearTimeout(hoverTimeoutRef.current);
106
+ hoverTimeoutRef.current = null;
107
+ }
108
+ }}
109
+ >
110
+ <Reply color="white" size={16} />
111
+ </Pressable>
112
+ <Pressable
113
+ onPress={() => setModMsg(item)}
114
+ style={[
115
+ {
116
+ padding: 6,
117
+ borderRadius: 4,
118
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
119
+ },
120
+ ]}
121
+ onHoverIn={() => {
122
+ // Keep the actions bar visible when hovering over it
123
+ if (hoverTimeoutRef.current) {
124
+ clearTimeout(hoverTimeoutRef.current);
125
+ hoverTimeoutRef.current = null;
126
+ }
127
+ }}
128
+ >
129
+ <Ellipsis color="white" size={16} />
130
+ </Pressable>
131
+ </View>
132
+ );
133
+ },
134
+ );
135
+
136
+ const ChatLine = memo(
137
+ ({
138
+ item,
139
+ canModerate,
140
+ }: {
141
+ item: ChatMessageViewHydrated;
142
+ canModerate: boolean;
143
+ }) => {
144
+ const setReply = useSetReplyToMessage();
145
+ const setModMsg = usePlayerStore((state) => state.setModMessage);
146
+ const swipeableRef = useRef<SwipeableMethods | null>(null);
147
+ const [isHovered, setIsHovered] = useState(false);
148
+ const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
149
+
150
+ const handleHoverIn = () => {
151
+ if (hoverTimeoutRef.current) {
152
+ clearTimeout(hoverTimeoutRef.current);
153
+ hoverTimeoutRef.current = null;
154
+ }
155
+ setIsHovered(true);
156
+ };
157
+
158
+ const handleHoverOut = () => {
159
+ hoverTimeoutRef.current = setTimeout(() => {
160
+ setIsHovered(false);
161
+ }, 50);
162
+ };
163
+
164
+ useEffect(() => {
165
+ return () => {
166
+ if (hoverTimeoutRef.current) {
167
+ clearTimeout(hoverTimeoutRef.current);
168
+ }
169
+ };
170
+ }, []);
171
+
172
+ if (item.author.did === "did:sys:system") {
173
+ return (
174
+ <SystemMessage
175
+ timestamp={new Date(item.record.createdAt)}
176
+ title={item.record.text}
177
+ />
178
+ );
179
+ }
180
+
181
+ if (Platform.OS === "web") {
182
+ return (
183
+ <View
184
+ style={[
185
+ py[1],
186
+ px[2],
187
+ { position: "relative", borderRadius: 8 },
188
+ isHovered && bg.gray[950],
189
+ ]}
190
+ onPointerEnter={handleHoverIn}
191
+ onPointerLeave={handleHoverOut}
192
+ >
193
+ <Pressable>
194
+ <RenderChatMessage item={item} />
195
+ </Pressable>
196
+ <ActionsBar
197
+ item={item}
198
+ visible={isHovered}
199
+ hoverTimeoutRef={hoverTimeoutRef}
200
+ />
201
+ </View>
202
+ );
203
+ }
204
+
205
+ return (
206
+ <>
207
+ <Swipeable
208
+ containerStyle={[py[1]]}
209
+ friction={2}
210
+ enableTrackpadTwoFingerGesture
211
+ rightThreshold={40}
212
+ leftThreshold={40}
213
+ renderRightActions={
214
+ Platform.OS === "android" ? undefined : RightAction
215
+ }
216
+ renderLeftActions={Platform.OS === "android" ? undefined : LeftAction}
217
+ overshootFriction={9}
218
+ ref={(ref) => {
219
+ swipeableRef.current = ref;
220
+ }}
221
+ onSwipeableOpen={(r) => {
222
+ if (r === (Platform.OS === "android" ? "right" : "left")) {
223
+ setReply(item);
224
+ }
225
+ if (r === (Platform.OS === "android" ? "left" : "right")) {
226
+ setModMsg(item);
227
+ }
228
+ // close this swipeable
229
+ const swipeable = swipeableRef.current;
230
+ if (swipeable) {
231
+ swipeable.close();
232
+ }
233
+ }}
234
+ >
235
+ <RenderChatMessage item={item} />
236
+ </Swipeable>
237
+ </>
238
+ );
239
+ },
240
+ );
94
241
 
95
242
  export function Chat({
96
243
  shownMessages = SHOWN_MSGS,
97
244
  style: propsStyle,
245
+ canModerate = false,
98
246
  ...props
99
247
  }: ComponentProps<typeof View> & {
100
248
  shownMessages?: number;
101
249
  style?: ComponentProps<typeof View>["style"];
250
+ canModerate?: boolean;
102
251
  }) {
103
252
  const chat = useChat();
104
253
 
@@ -113,10 +262,12 @@ export function Chat({
113
262
  <View style={[flex.shrink[1]].concat(propsStyle || [])}>
114
263
  <FlatList
115
264
  style={[flex.grow[1], flex.shrink[1], w.percent[100]]}
116
- data={chat}
265
+ data={chat.slice(0, shownMessages)}
117
266
  inverted={true}
118
267
  keyExtractor={keyExtractor}
119
- renderItem={({ item, index }) => <ChatLine item={item} />}
268
+ renderItem={({ item, index }) => (
269
+ <ChatLine item={item} canModerate={canModerate} />
270
+ )}
120
271
  removeClippedSubviews={true}
121
272
  maxToRenderPerBatch={10}
122
273
  initialNumToRender={10}
@@ -0,0 +1,94 @@
1
+ import { Pressable } from "react-native";
2
+ import { Code, Text, View } from "../..";
3
+ import { bg, layout, left, right, zIndex } from "../../lib/theme/atoms";
4
+
5
+ export interface EmojiData {
6
+ categories: Category[];
7
+ emojis: { [key: string]: Emoji };
8
+ aliases: { [key: string]: string };
9
+ sheet: Sheet;
10
+ }
11
+
12
+ export interface Category {
13
+ id: string;
14
+ emojis: string[];
15
+ }
16
+
17
+ export interface Emoji {
18
+ id: string;
19
+ name: string;
20
+ keywords: string[];
21
+ skins: Skin[];
22
+ version: number;
23
+ emoticons?: string[];
24
+ }
25
+
26
+ export interface Skin {
27
+ unified: string;
28
+ native: string;
29
+ }
30
+
31
+ export interface Sheet {
32
+ cols: number;
33
+ rows: number;
34
+ }
35
+
36
+ interface EmojiSuggestionsProps {
37
+ emojis: Emoji[];
38
+ onSelect: (emoji: Emoji) => void;
39
+ highlightedIndex: number;
40
+ }
41
+
42
+ export function EmojiSuggestions({
43
+ emojis,
44
+ onSelect,
45
+ highlightedIndex,
46
+ }: EmojiSuggestionsProps) {
47
+ if (!emojis || emojis.length === 0) {
48
+ return null;
49
+ }
50
+
51
+ return (
52
+ <View
53
+ style={[
54
+ bg.gray[800],
55
+ layout.position.absolute,
56
+ left[0],
57
+ right[0],
58
+ zIndex[50],
59
+ {
60
+ bottom: "100%",
61
+ borderRadius: 8,
62
+ boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
63
+ maxHeight: 200,
64
+ overflow: "auto",
65
+ },
66
+ ]}
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
+ ))}
92
+ </View>
93
+ );
94
+ }
@@ -1,11 +1,18 @@
1
- import { TriggerRef } from "@rn-primitives/dropdown-menu";
2
- import { forwardRef, useEffect, useRef } from "react";
3
- import { gap, mr } from "../../lib/theme/atoms";
1
+ import { TriggerRef, useRootContext } from "@rn-primitives/dropdown-menu";
2
+ import { forwardRef, useEffect, useRef, useState } from "react";
3
+ import { gap, mr, w } from "../../lib/theme/atoms";
4
4
  import { usePlayerStore } from "../../player-store";
5
- import { useCreateBlockRecord } from "../../streamplace-store/block";
5
+ import {
6
+ useCreateBlockRecord,
7
+ useCreateHideChatRecord,
8
+ } from "../../streamplace-store/block";
6
9
  import { usePDSAgent } from "../../streamplace-store/xrpc";
7
10
 
11
+ import { Linking } from "react-native";
12
+ import { ChatMessageViewHydrated } from "streamplace";
13
+ import { useStreamplaceStore } from "../../streamplace-store";
8
14
  import {
15
+ atoms,
9
16
  DropdownMenu,
10
17
  DropdownMenuGroup,
11
18
  DropdownMenuItem,
@@ -15,7 +22,8 @@ import {
15
22
  Text,
16
23
  View,
17
24
  } from "../ui";
18
- import { RenderChatMessage } from "./chat-message";
25
+
26
+ const BSKY_FRONTEND_DOMAIN = "bsky.app";
19
27
 
20
28
  type ModViewProps = {
21
29
  onClose?: () => void;
@@ -33,7 +41,14 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
33
41
  const message = usePlayerStore((state) => state.modMessage);
34
42
 
35
43
  let agent = usePDSAgent();
36
- let createBlockRecord = useCreateBlockRecord();
44
+ let [messageRemoved, setMessageRemoved] = useState(false);
45
+ let { createBlock, isLoading: isBlockLoading } = useCreateBlockRecord();
46
+ let { createHideChat, isLoading: isHideLoading } = useCreateHideChatRecord();
47
+
48
+ // get the channel did
49
+ const channelId = usePlayerStore((state) => state.src);
50
+ // get the logged in user's identity
51
+ const handle = useStreamplaceStore((state) => state.handle);
37
52
 
38
53
  if (!agent?.did) {
39
54
  <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
@@ -44,6 +59,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
44
59
  useEffect(() => {
45
60
  if (message) {
46
61
  console.log("opening mod view");
62
+ setMessageRemoved(false);
47
63
  triggerRef.current?.open();
48
64
  } else {
49
65
  console.log("closing mod view");
@@ -52,7 +68,9 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
52
68
  }, [message]);
53
69
 
54
70
  return (
55
- <DropdownMenu>
71
+ <DropdownMenu
72
+ style={[layout.flex.row, layout.flex.alignCenter, gap.all[2], w[80]]}
73
+ >
56
74
  <DropdownMenuTrigger ref={triggerRef}>
57
75
  {/* Hidden trigger */}
58
76
  <View />
@@ -62,53 +80,88 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
62
80
  <>
63
81
  <DropdownMenuGroup>
64
82
  <DropdownMenuItem>
65
- <View style={[layout.flex.column, mr[5], { gap: 6 }]}>
66
- <RenderChatMessage item={message} />
83
+ <View
84
+ style={[
85
+ layout.flex.column,
86
+ mr[5],
87
+ { gap: 6, maxWidth: "100%" },
88
+ ]}
89
+ >
90
+ <Text
91
+ style={{
92
+ fontVariant: ["tabular-nums"],
93
+ color: atoms.colors.gray[300],
94
+ }}
95
+ >
96
+ {new Date(message.record.createdAt).toLocaleTimeString([], {
97
+ hour: "2-digit",
98
+ minute: "2-digit",
99
+ hour12: false,
100
+ })}{" "}
101
+ @{message.author.handle}: {message.record.text}
102
+ </Text>
67
103
  </View>
68
104
  </DropdownMenuItem>
69
105
  </DropdownMenuGroup>
70
106
 
71
- <DropdownMenuGroup title={`Moderation actions`}>
72
- {/* <DropdownMenuItem
73
- onPress={
74
- onDeleteMessage
75
- ? () => onDeleteMessage(modMessage)
76
- : undefined
77
- }
107
+ {/* TODO: Checking for non-owner moderators */}
108
+ {channelId === handle && (
109
+ <DropdownMenuGroup title={`Moderation actions`}>
110
+ <DropdownMenuItem
111
+ disabled={isHideLoading || messageRemoved}
112
+ onPress={() => {
113
+ if (isHideLoading || messageRemoved) return;
114
+ createHideChat(message.uri)
115
+ .then((r) => setMessageRemoved(true))
116
+ .catch((e) => console.error(e));
117
+ }}
78
118
  >
79
- <Text customColor={colors.ios.systemTeal}>
80
- Delete message
119
+ <Text
120
+ color={
121
+ isHideLoading || messageRemoved ? "muted" : "destructive"
122
+ }
123
+ >
124
+ {isHideLoading
125
+ ? "Removing..."
126
+ : messageRemoved
127
+ ? "Message removed"
128
+ : "Remove this message"}
81
129
  </Text>
82
130
  </DropdownMenuItem>
83
- <DropdownMenuSeparator />
84
131
  <DropdownMenuItem
85
- onPress={
86
- onBanUser
87
- ? () => onBanUser(modMessage.author.handle)
88
- : undefined
89
- }
132
+ disabled={message.author.did === agent?.did || isBlockLoading}
133
+ onPress={() => {
134
+ createBlock(message.author.did)
135
+ .then((r) => console.log(r))
136
+ .catch((e) => console.error(e));
137
+ }}
90
138
  >
91
- <Text color="destructive">
92
- Ban user @{modMessage.author.handle}
93
- </Text>
94
- </DropdownMenuItem> */}
139
+ {message.author.did === agent?.did ? (
140
+ <Text color="muted">
141
+ Block yourself (you can't block yourself)
142
+ </Text>
143
+ ) : (
144
+ <Text color="destructive">
145
+ {isBlockLoading
146
+ ? "Blocking..."
147
+ : `Block user @${message.author.handle} from this channel`}
148
+ </Text>
149
+ )}
150
+ </DropdownMenuItem>
151
+ </DropdownMenuGroup>
152
+ )}
153
+
154
+ <DropdownMenuGroup title={`User actions`}>
95
155
  <DropdownMenuItem
96
- disabled={message.author.did === agent?.did}
97
156
  onPress={() => {
98
- console.log("Creating block record");
99
- createBlockRecord(message.author.did)
100
- .then((r) => console.log(r))
101
- .catch((e) => console.error(e));
157
+ Linking.openURL(
158
+ `https://${BSKY_FRONTEND_DOMAIN}/profile/${channelId}`,
159
+ );
102
160
  }}
103
161
  >
104
- <Text color="destructive">
105
- {message.author.did === agent?.did ? (
106
- <>Block yourself (you can't block yourself)</>
107
- ) : (
108
- <>Block user @{message.author.handle} from this channel</>
109
- )}
110
- </Text>
162
+ <Text color="primary">View user on {BSKY_FRONTEND_DOMAIN}</Text>
111
163
  </DropdownMenuItem>
164
+ <ReportButton message={message} />
112
165
  </DropdownMenuGroup>
113
166
  </>
114
167
  )}
@@ -116,3 +169,29 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
116
169
  </DropdownMenu>
117
170
  );
118
171
  });
172
+
173
+ export function ReportButton({
174
+ message,
175
+ }: {
176
+ message: ChatMessageViewHydrated;
177
+ }) {
178
+ const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
179
+ const setReportSubject = usePlayerStore((x) => x.setReportSubject);
180
+ const { onOpenChange } = useRootContext();
181
+ return (
182
+ <DropdownMenuItem
183
+ onPress={() => {
184
+ if (!message) return;
185
+ onOpenChange?.(false);
186
+ setReportModalOpen(true);
187
+ setReportSubject({
188
+ $type: "com.atproto.repo.strongRef",
189
+ uri: message.uri,
190
+ cid: message.cid,
191
+ });
192
+ }}
193
+ >
194
+ <Text color="warning">Report chat...</Text>
195
+ </DropdownMenuItem>
196
+ );
197
+ }
@@ -0,0 +1,38 @@
1
+ import { View } from "react-native";
2
+ import { flex, gap, layout, ml, pb, pl, px, w } from "../../ui";
3
+ import { atoms } from "../ui";
4
+ import { Code, Text } from "../ui/text";
5
+
6
+ interface SystemMessageProps {
7
+ title: string;
8
+ timestamp: Date;
9
+ }
10
+
11
+ export function SystemMessage({ title, timestamp }: SystemMessageProps) {
12
+ return (
13
+ <View style={[w.percent[100], px[2], pb[2]]}>
14
+ <Code color="muted" tracking="widest" style={[pl[12], ml[1]]}>
15
+ SYSTEM MESSAGE
16
+ </Code>
17
+ <View style={[gap.all[2], layout.flex.row]}>
18
+ <Text
19
+ style={{
20
+ fontVariant: ["tabular-nums"],
21
+ color: atoms.colors.gray[300],
22
+ }}
23
+ >
24
+ {timestamp.toLocaleTimeString([], {
25
+ hour: "2-digit",
26
+ minute: "2-digit",
27
+ hour12: false,
28
+ })}
29
+ </Text>
30
+ <Text weight="bold" color="default" style={[flex.shrink[1]]}>
31
+ {title}
32
+ </Text>
33
+ </View>
34
+ </View>
35
+ );
36
+ }
37
+
38
+ export default SystemMessage;
@@ -0,0 +1,9 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+
3
+ export function BlueskyIcon({ size = 20, color = "#000" }) {
4
+ return (
5
+ <Svg width={size} height={size} viewBox="0 0 600 530" fill={color}>
6
+ <Path d="M136 44c66 50 138 151 164 205 26-54 98-155 164-205 48-36 126-64 126 25 0 18-10 149-16 170-21 74-96 93-163 81 117 20 147 86 82 153-122 125-176-32-189-72-3-8-4-11-4-8 0-3-1 0-4 8-13 40-67 197-189 72-65-67-35-133 82-153-67 12-142-7-163-81-6-21-16-152-16-170 0-89 78-61 126-25z" />
7
+ </Svg>
8
+ );
9
+ }
@@ -0,0 +1,13 @@
1
+ import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
2
+ import { useEffect } from "react";
3
+
4
+ export function KeepAwake() {
5
+ // useKeepAwake();
6
+ useEffect(() => {
7
+ activateKeepAwakeAsync();
8
+ return () => {
9
+ deactivateKeepAwake();
10
+ };
11
+ }, []);
12
+ return <></>;
13
+ }
@@ -0,0 +1,3 @@
1
+ export function KeepAwake() {
2
+ return <></>;
3
+ }