@streamplace/components 0.0.1 → 0.7.0

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 (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
package/package.json CHANGED
@@ -1,13 +1,55 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.0.1",
4
- "description": "",
5
- "main": "package.json",
3
+ "version": "0.7.0",
4
+ "description": "Streamplace React (Native) Components",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "src/index.tsx",
8
+ "exports": {
9
+ ".": {
10
+ "@streamplace/dev": "./src/index.tsx",
11
+ "types": "./dist/index.d.mjs",
12
+ "default": "./dist/index.mjs"
13
+ }
14
+ },
6
15
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
16
+ "build": "tsc",
17
+ "postinstall": "pnpm run build",
18
+ "start": "tsc --watch --preserveWatchOutput"
19
+ },
20
+ "keywords": [
21
+ "streamplace"
22
+ ],
23
+ "author": "Streamplace",
24
+ "license": "MIT",
25
+ "packageManager": "pnpm@10.11.0",
26
+ "devDependencies": {
27
+ "tsup": "^8.5.0"
28
+ },
29
+ "dependencies": {
30
+ "@atproto/api": "^0.15.7",
31
+ "@atproto/crypto": "^0.4.4",
32
+ "@gorhom/bottom-sheet": "^5.1.6",
33
+ "@react-navigation/native": "^6.1.18",
34
+ "@rn-primitives/dropdown-menu": "^1.2.0",
35
+ "@rn-primitives/portal": "^1.3.0",
36
+ "class-variance-authority": "^0.6.1",
37
+ "expo-video": "~2.2.1",
38
+ "hls.js": "^1.5.17",
39
+ "lucide-react-native": "^0.514.0",
40
+ "react-native": "0.79.3",
41
+ "react-native-edge-to-edge": "^1.6.2",
42
+ "react-native-gesture-handler": "~2.26.0",
43
+ "react-native-reanimated": "~3.18.0",
44
+ "react-native-safe-area-context": "5.4.1",
45
+ "react-native-webrtc": "git+https://github.com/streamplace/react-native-webrtc.git#6b8472a771ac47f89217d327058a8a4124a6ae56",
46
+ "react-use-websocket": "^4.13.0",
47
+ "streamplace": "0.7.0",
48
+ "viem": "^2.21.44",
49
+ "zustand": "^5.0.5"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "*"
8
53
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
54
+ "gitHead": "c0b9266fbc2cb2a643203e8c0450980c1bd29635"
13
55
  }
@@ -0,0 +1,195 @@
1
+ import { X } from "lucide-react-native";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Pressable, TextInput } from "react-native";
4
+ import { ChatMessageViewHydrated } from "streamplace";
5
+ import {
6
+ Button,
7
+ Loader,
8
+ Text,
9
+ useChat,
10
+ useCreateChatMessage,
11
+ useReplyToMessage,
12
+ useSetReplyToMessage,
13
+ View,
14
+ } from "../../";
15
+ import { bg, flex, gap, h, layout, mb, pl, pr, w } from "../../lib/theme/atoms";
16
+ import { usePDSAgent } from "../../streamplace-store/xrpc";
17
+ import { Textarea } from "../ui/textarea";
18
+ import { RenderChatMessage } from "./chat-message";
19
+ import { MentionSuggestions } from "./mention-suggestions";
20
+
21
+ export function ChatBox({
22
+ isPopout,
23
+ chatBoxStyle,
24
+ }: {
25
+ isPopout?: boolean;
26
+ chatBoxStyle?: any;
27
+ }) {
28
+ const [submitting, setSubmitting] = useState(false);
29
+ const [message, setMessage] = useState("");
30
+ const [showSuggestions, setShowSuggestions] = useState(false);
31
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
32
+ const [filteredAuthors, setFilteredAuthors] = useState<Map<string, any>>(
33
+ new Map(),
34
+ );
35
+
36
+ const chat = useChat();
37
+ const createChatMessage = useCreateChatMessage();
38
+ const replyTo = useReplyToMessage();
39
+ const setReplyToMessage = useSetReplyToMessage();
40
+ const textAreaRef = useRef<TextInput>(null);
41
+
42
+ // are we logged in?
43
+
44
+ let agent = usePDSAgent();
45
+
46
+ if (!agent?.did) {
47
+ <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
48
+ <Text>Log in to chat.</Text>
49
+ </View>;
50
+ }
51
+
52
+ const authors = useMemo(() => {
53
+ if (!chat) return null;
54
+ return chat.reduce((acc, msg) => {
55
+ acc.set(msg.author.handle, msg.chatProfile);
56
+ return acc;
57
+ }, new Map<string, ChatMessageViewHydrated["chatProfile"]>());
58
+ }, [chat]);
59
+
60
+ const handleMentionSelect = (handle: string) => {
61
+ const beforeAt = message.slice(0, message.lastIndexOf("@"));
62
+ setMessage(`${beforeAt}@${handle} `);
63
+ setShowSuggestions(false);
64
+ };
65
+
66
+ const updateSuggestions = (text: string) => {
67
+ const atIndex = text.lastIndexOf("@");
68
+ if (atIndex === -1 || !authors) {
69
+ setShowSuggestions(false);
70
+ return;
71
+ }
72
+
73
+ const searchText = text.slice(atIndex + 1).toLowerCase();
74
+
75
+ const filteredAuthorsMap = new Map(
76
+ Array.from(authors.entries()).filter(([handle]) =>
77
+ handle.toLowerCase().includes(searchText),
78
+ ),
79
+ );
80
+
81
+ setFilteredAuthors(filteredAuthorsMap);
82
+
83
+ setHighlightedIndex(0);
84
+ setShowSuggestions(filteredAuthorsMap.size > 0);
85
+ };
86
+
87
+ const submit = () => {
88
+ if (!message.trim()) return;
89
+ setMessage("");
90
+ setReplyToMessage(null);
91
+
92
+ setSubmitting(true);
93
+ createChatMessage({
94
+ text: message,
95
+ reply: replyTo || undefined,
96
+ });
97
+ setSubmitting(false);
98
+ };
99
+ useEffect(() => {
100
+ if (replyTo && textAreaRef.current) {
101
+ textAreaRef.current.focus();
102
+ }
103
+ }, [replyTo]);
104
+
105
+ return (
106
+ <View style={[layout.flex.column, flex.shrink[1], gap.all[2]]}>
107
+ {replyTo && (
108
+ <View
109
+ style={[
110
+ layout.flex.row,
111
+ layout.flex.alignCenter,
112
+ layout.flex.spaceBetween,
113
+ h[12],
114
+ pl[2],
115
+ pr[10],
116
+ mb[2],
117
+ bg.gray[800],
118
+ { borderRadius: 16 },
119
+ ]}
120
+ >
121
+ <RenderChatMessage
122
+ item={replyTo}
123
+ showReply={false}
124
+ userCache={authors || new Map()}
125
+ />
126
+ <Pressable onPress={() => setReplyToMessage(null)}>
127
+ <View
128
+ style={[
129
+ layout.flex.row,
130
+ layout.flex.alignCenter,
131
+ layout.flex.justifyCenter,
132
+ h[12],
133
+ w[12],
134
+ bg.gray[600],
135
+ { borderRadius: 999 },
136
+ ]}
137
+ >
138
+ <X size={24} />
139
+ </View>
140
+ </Pressable>
141
+ </View>
142
+ )}
143
+ <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
144
+ <Textarea
145
+ ref={textAreaRef}
146
+ numberOfLines={1}
147
+ value={message}
148
+ enterKeyHint="send"
149
+ onSubmitEditing={submit}
150
+ multiline={false}
151
+ onChangeText={(text) => {
152
+ setMessage(text);
153
+ updateSuggestions(text);
154
+ }}
155
+ onKeyPress={(k) => {
156
+ if (k.nativeEvent.key === "Enter") {
157
+ if (showSuggestions) {
158
+ k.preventDefault();
159
+ const handles = Array.from(filteredAuthors.keys());
160
+ if (handles.length > 0) {
161
+ handleMentionSelect(handles[highlightedIndex]);
162
+ }
163
+ } else submit();
164
+ } else if (k.nativeEvent.key === "ArrowUp") {
165
+ setHighlightedIndex((prev) => Math.max(prev - 1, 0));
166
+ } else if (k.nativeEvent.key === "ArrowDown") {
167
+ setHighlightedIndex((prev) =>
168
+ Math.min(
169
+ prev + 1,
170
+ Array.from(filteredAuthors.keys()).length - 1,
171
+ ),
172
+ );
173
+ }
174
+ }}
175
+ style={[chatBoxStyle]}
176
+ />
177
+ <Button
178
+ disabled={submitting}
179
+ variant="secondary"
180
+ style={{ borderRadius: 16, height: 36, minWidth: 80 }}
181
+ onPress={submit}
182
+ >
183
+ {submitting ? <Loader /> : "Send"}
184
+ </Button>
185
+ </View>
186
+ {showSuggestions && (
187
+ <MentionSuggestions
188
+ authors={filteredAuthors || []}
189
+ highlightedIndex={highlightedIndex}
190
+ onSelect={handleMentionSelect}
191
+ />
192
+ )}
193
+ </View>
194
+ );
195
+ }
@@ -0,0 +1,192 @@
1
+ import { $Typed } from "@atproto/api";
2
+ import {
3
+ Link,
4
+ Mention,
5
+ } from "@atproto/api/dist/client/types/app/bsky/richtext/facet";
6
+ import { memo, useCallback } from "react";
7
+ import { Linking, View } from "react-native";
8
+ import { ChatMessageViewHydrated } from "streamplace";
9
+ import { RichtextSegment, segmentize } from "../../lib/facet";
10
+ import {
11
+ borders,
12
+ flex,
13
+ gap,
14
+ ml,
15
+ mr,
16
+ opacity,
17
+ pl,
18
+ w,
19
+ } from "../../lib/theme/atoms";
20
+ import { atoms, layout } from "../ui";
21
+
22
+ interface Facet {
23
+ index: {
24
+ byteStart: number;
25
+ byteEnd: number;
26
+ };
27
+ features: Array<{
28
+ $type: string;
29
+ uri?: string;
30
+ did?: string;
31
+ }>;
32
+ }
33
+
34
+ import { useLivestreamStore } from "../../livestream-store";
35
+ import { Text } from "../ui/text";
36
+
37
+ const getRgbColor = (color?: { red: number; green: number; blue: number }) =>
38
+ color ? `rgb(${color.red}, ${color.green}, ${color.blue})` : "$accentColor";
39
+
40
+ const segmentedObject = (
41
+ obj: RichtextSegment,
42
+ index: number,
43
+ userCache?: { [key: string]: ChatMessageViewHydrated["chatProfile"] },
44
+ ) => {
45
+ if (obj.features && obj.features.length > 0) {
46
+ let ftr = obj.features[0];
47
+ // afaik there shouldn't be a case where facets overlap, at least currently
48
+ if (ftr.$type === "app.bsky.richtext.facet#link") {
49
+ let linkftr = ftr as $Typed<Link>;
50
+ return (
51
+ <Text
52
+ key={`mention-${index}`}
53
+ style={[{ color: atoms.colors.ios.systemBlue, cursor: "pointer" }]}
54
+ onPress={() => Linking.openURL(linkftr.uri || "")}
55
+ >
56
+ {obj.text}
57
+ </Text>
58
+ );
59
+ } else if (ftr.$type === "app.bsky.richtext.facet#mention") {
60
+ let mtnftr = ftr as $Typed<Mention>;
61
+ const profile = userCache?.[mtnftr.did];
62
+ console.log(profile, mtnftr.did, userCache);
63
+ return (
64
+ <Text
65
+ key={`mention-${index}`}
66
+ style={[
67
+ {
68
+ cursor: "pointer",
69
+ color: getRgbColor(profile?.color),
70
+ },
71
+ ]}
72
+ onPress={() =>
73
+ Linking.openURL(`https://bsky.app/profile/${mtnftr.did || ""}`)
74
+ }
75
+ >
76
+ {obj.text}
77
+ </Text>
78
+ );
79
+ }
80
+ } else {
81
+ return <Text key={`text-${index}`}>{obj.text}</Text>;
82
+ }
83
+ };
84
+
85
+ const RichTextMessage = ({
86
+ text,
87
+ facets,
88
+ }: {
89
+ text: string;
90
+ facets: ChatMessageViewHydrated["record"]["facets"];
91
+ }) => {
92
+ if (!facets?.length) return <Text>{text}</Text>;
93
+
94
+ const userCache = useLivestreamStore((state) => state.authors);
95
+
96
+ let segs = segmentize(text, facets as Facet[]);
97
+
98
+ return segs.map((seg, i) => segmentedObject(seg, i, userCache));
99
+ };
100
+ export const RenderChatMessage = memo(
101
+ function RenderChatMessage({
102
+ item,
103
+ showReply = true,
104
+ showTime = true,
105
+ }: {
106
+ item: ChatMessageViewHydrated;
107
+ userCache?: Map<string, ChatMessageViewHydrated["chatProfile"]>;
108
+ showReply?: boolean;
109
+ showTime?: boolean;
110
+ }) {
111
+ const formatTime = useCallback((dateString: string) => {
112
+ return new Date(dateString).toLocaleString(undefined, {
113
+ hour: "2-digit",
114
+ minute: "2-digit",
115
+ hour12: false,
116
+ });
117
+ }, []);
118
+
119
+ return (
120
+ <>
121
+ {item.replyTo && showReply && (
122
+ <View
123
+ style={[
124
+ gap.all[2],
125
+ layout.flex.row,
126
+ w.percent[100],
127
+ borders.left.width.medium,
128
+ borders.left.color.gray[700],
129
+ ml[4],
130
+ pl[4],
131
+ opacity[80],
132
+ ]}
133
+ >
134
+ <Text numberOfLines={1} style={[flex.shrink[1], mr[4]]}>
135
+ <Text
136
+ style={{
137
+ color: getRgbColor((item.replyTo.chatProfile as any).color),
138
+ fontWeight: "thin",
139
+ }}
140
+ >
141
+ @{(item.replyTo.author as any).handle}
142
+ </Text>{" "}
143
+ <Text
144
+ style={{
145
+ color: atoms.colors.gray[300],
146
+ fontStyle: "italic",
147
+ }}
148
+ >
149
+ {(item.replyTo.record as any).text}
150
+ </Text>
151
+ </Text>
152
+ </View>
153
+ )}
154
+ <View style={[gap.all[2], layout.flex.row, w.percent[100]]}>
155
+ {showTime && (
156
+ <Text
157
+ style={{
158
+ fontVariant: ["tabular-nums"],
159
+ color: atoms.colors.gray[300],
160
+ }}
161
+ >
162
+ {formatTime(item.record.createdAt)}
163
+ </Text>
164
+ )}
165
+ <Text weight="bold" color="default" style={[flex.shrink[1]]}>
166
+ <Text
167
+ style={[
168
+ {
169
+ cursor: "pointer",
170
+ color: getRgbColor(item.chatProfile?.color),
171
+ },
172
+ ]}
173
+ >
174
+ @{item.author.handle}
175
+ </Text>
176
+ :{" "}
177
+ <RichTextMessage
178
+ text={item.record.text}
179
+ facets={item.record.facets || []}
180
+ />
181
+ </Text>
182
+ </View>
183
+ </>
184
+ );
185
+ },
186
+ (prevProps, nextProps) => {
187
+ return (
188
+ prevProps.item.author.handle === nextProps.item.author.handle &&
189
+ prevProps.item.record.text === nextProps.item.record.text
190
+ );
191
+ },
192
+ );
@@ -0,0 +1,128 @@
1
+ import { Reply, ShieldEllipsis } from "lucide-react-native";
2
+ import { ComponentProps, memo, useRef } from "react";
3
+ import { FlatList, Platform, Pressable } from "react-native";
4
+ import Swipeable, {
5
+ SwipeableMethods,
6
+ } from "react-native-gesture-handler/ReanimatedSwipeable";
7
+ import Reanimated, {
8
+ SharedValue,
9
+ useAnimatedStyle,
10
+ } from "react-native-reanimated";
11
+ import { ChatMessageViewHydrated } from "streamplace";
12
+ import {
13
+ Text,
14
+ useChat,
15
+ usePlayerStore,
16
+ useSetReplyToMessage,
17
+ View,
18
+ } from "../../";
19
+ import { flex, py, w } from "../../lib/theme/atoms";
20
+ import { RenderChatMessage } from "./chat-message";
21
+ import { ModView } from "./mod-view";
22
+
23
+ function RightAction(prog: SharedValue<number>, drag: SharedValue<number>) {
24
+ const styleAnimation = useAnimatedStyle(() => {
25
+ return {
26
+ transform: [{ translateX: drag.value + 25 }],
27
+ };
28
+ });
29
+
30
+ return (
31
+ <Reanimated.View style={[styleAnimation]}>
32
+ <Reply color="white" />
33
+ </Reanimated.View>
34
+ );
35
+ }
36
+
37
+ function LeftAction(prog: SharedValue<number>, drag: SharedValue<number>) {
38
+ const styleAnimation = useAnimatedStyle(() => {
39
+ return {
40
+ transform: [{ translateX: drag.value - 25 }],
41
+ };
42
+ });
43
+
44
+ return (
45
+ <Reanimated.View style={[styleAnimation]}>
46
+ <ShieldEllipsis color="white" />
47
+ </Reanimated.View>
48
+ );
49
+ }
50
+
51
+ const SHOWN_MSGS =
52
+ Platform.OS === "android" || Platform.OS === "ios" ? 100 : 25;
53
+
54
+ const keyExtractor = (item: ChatMessageViewHydrated, index: number) => {
55
+ return `${item.uri}`;
56
+ };
57
+
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
+ }}
88
+ >
89
+ <RenderChatMessage item={item} />
90
+ </Swipeable>
91
+ </Pressable>
92
+ );
93
+ });
94
+
95
+ export function Chat({
96
+ shownMessages = SHOWN_MSGS,
97
+ style: propsStyle,
98
+ ...props
99
+ }: ComponentProps<typeof View> & {
100
+ shownMessages?: number;
101
+ style?: ComponentProps<typeof View>["style"];
102
+ }) {
103
+ const chat = useChat();
104
+
105
+ if (!chat)
106
+ return (
107
+ <View style={[flex.shrink[1]]}>
108
+ <Text>Loading chaat...</Text>
109
+ </View>
110
+ );
111
+
112
+ return (
113
+ <View style={[flex.shrink[1]].concat(propsStyle || [])}>
114
+ <FlatList
115
+ style={[flex.grow[1], flex.shrink[1], w.percent[100]]}
116
+ data={chat}
117
+ inverted={true}
118
+ keyExtractor={keyExtractor}
119
+ renderItem={({ item, index }) => <ChatLine item={item} />}
120
+ removeClippedSubviews={true}
121
+ maxToRenderPerBatch={10}
122
+ initialNumToRender={10}
123
+ updateCellsBatchingPeriod={50}
124
+ />
125
+ <ModView />
126
+ </View>
127
+ );
128
+ }
@@ -0,0 +1,71 @@
1
+ import { Pressable } from "react-native";
2
+ import { ChatMessageViewHydrated } from "streamplace";
3
+ import { Text, View } from "../..";
4
+ import { bg, layout, left, right, zIndex } from "../../lib/theme/atoms";
5
+
6
+ interface MentionSuggestionsProps {
7
+ authors: Map<string, ChatMessageViewHydrated["chatProfile"]>;
8
+ onSelect: (authorHandle: string) => void;
9
+ highlightedIndex: number;
10
+ }
11
+
12
+ export function MentionSuggestions({
13
+ authors,
14
+ onSelect,
15
+ highlightedIndex,
16
+ }: MentionSuggestionsProps) {
17
+ if (!authors || authors.size === 0) {
18
+ return null; // No authors to display
19
+ }
20
+
21
+ const authorHandles = Array.from(authors.keys());
22
+ return (
23
+ <View
24
+ style={[
25
+ bg.gray[800],
26
+ layout.position.absolute,
27
+
28
+ left[0],
29
+ right[0],
30
+ zIndex[50],
31
+ {
32
+ bottom: "100%",
33
+ borderRadius: 8,
34
+ boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
35
+ },
36
+ ]}
37
+ >
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
+ }}
63
+ >
64
+ @{handle}
65
+ </Text>
66
+ </Pressable>
67
+ );
68
+ })}
69
+ </View>
70
+ );
71
+ }