@streamplace/components 0.6.37 → 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.
- package/dist/components/chat/chat-box.js +109 -0
- package/dist/components/chat/chat-message.js +76 -0
- package/dist/components/chat/chat.js +56 -0
- package/dist/components/chat/mention-suggestions.js +39 -0
- package/dist/components/chat/mod-view.js +33 -0
- package/dist/components/mobile-player/fullscreen.js +69 -0
- package/dist/components/mobile-player/fullscreen.native.js +151 -0
- package/dist/components/mobile-player/player.js +103 -0
- package/dist/components/mobile-player/props.js +1 -0
- package/dist/components/mobile-player/shared.js +51 -0
- package/dist/components/mobile-player/ui/countdown.js +79 -0
- package/dist/components/mobile-player/ui/index.js +5 -0
- package/dist/components/mobile-player/ui/input.js +38 -0
- package/dist/components/mobile-player/ui/metrics.js +40 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
- package/dist/components/mobile-player/use-webrtc.js +232 -0
- package/dist/components/mobile-player/video.js +375 -0
- package/dist/components/mobile-player/video.native.js +238 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
- package/dist/components/mobile-player/webrtc-primitives.js +25 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
- package/dist/components/ui/button.js +220 -0
- package/dist/components/ui/dialog.js +203 -0
- package/dist/components/ui/dropdown.js +148 -0
- package/dist/components/ui/icons.js +22 -0
- package/dist/components/ui/index.js +22 -0
- package/dist/components/ui/input.js +202 -0
- package/dist/components/ui/loader.js +7 -0
- package/dist/components/ui/primitives/button.js +121 -0
- package/dist/components/ui/primitives/input.js +202 -0
- package/dist/components/ui/primitives/modal.js +203 -0
- package/dist/components/ui/primitives/text.js +286 -0
- package/dist/components/ui/resizeable.js +101 -0
- package/dist/components/ui/text.js +175 -0
- package/dist/components/ui/textarea.js +17 -0
- package/dist/components/ui/toast.js +129 -0
- package/dist/components/ui/view.js +250 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/useAvatars.js +32 -0
- package/dist/hooks/useCameraToggle.js +9 -0
- package/dist/hooks/useKeyboard.js +33 -0
- package/dist/hooks/useKeyboardSlide.js +11 -0
- package/dist/hooks/useLivestreamInfo.js +62 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
- package/dist/hooks/usePlayerDimensions.js +19 -0
- package/dist/hooks/useSegmentTiming.js +62 -0
- package/dist/index.js +10 -0
- package/dist/lib/facet.js +88 -0
- package/dist/lib/theme/atoms.js +620 -0
- package/dist/lib/theme/atoms.types.js +5 -0
- package/dist/lib/theme/index.js +9 -0
- package/dist/lib/theme/theme.js +248 -0
- package/dist/lib/theme/tokens.js +383 -0
- package/dist/lib/utils.js +94 -0
- package/dist/livestream-provider/index.js +8 -3
- package/dist/livestream-store/chat.js +89 -65
- package/dist/livestream-store/index.js +1 -0
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/stream-key.js +115 -0
- package/dist/player-store/player-provider.js +0 -1
- package/dist/player-store/player-store.js +13 -0
- package/dist/streamplace-store/block.js +23 -0
- package/dist/streamplace-store/index.js +1 -0
- package/dist/streamplace-store/stream.js +193 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +20 -4
- package/src/components/chat/chat-box.tsx +195 -0
- package/src/components/chat/chat-message.tsx +192 -0
- package/src/components/chat/chat.tsx +128 -0
- package/src/components/chat/mention-suggestions.tsx +71 -0
- package/src/components/chat/mod-view.tsx +118 -0
- package/src/components/mobile-player/fullscreen.native.tsx +193 -0
- package/src/components/mobile-player/fullscreen.tsx +79 -0
- package/src/components/mobile-player/player.tsx +134 -0
- package/src/components/mobile-player/props.tsx +11 -0
- package/src/components/mobile-player/shared.tsx +56 -0
- package/src/components/mobile-player/ui/countdown.tsx +119 -0
- package/src/components/mobile-player/ui/index.ts +5 -0
- package/src/components/mobile-player/ui/input.tsx +85 -0
- package/src/components/mobile-player/ui/metrics.tsx +69 -0
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
- package/src/components/mobile-player/use-webrtc.tsx +282 -0
- package/src/components/mobile-player/video.native.tsx +360 -0
- package/src/components/mobile-player/video.tsx +557 -0
- package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
- package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
- package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
- package/src/components/ui/button.tsx +309 -0
- package/src/components/ui/dialog.tsx +376 -0
- package/src/components/ui/dropdown.tsx +399 -0
- package/src/components/ui/icons.tsx +50 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +350 -0
- package/src/components/ui/loader.tsx +9 -0
- package/src/components/ui/primitives/button.tsx +292 -0
- package/src/components/ui/primitives/input.tsx +422 -0
- package/src/components/ui/primitives/modal.tsx +421 -0
- package/src/components/ui/primitives/text.tsx +499 -0
- package/src/components/ui/resizeable.tsx +169 -0
- package/src/components/ui/text.tsx +330 -0
- package/src/components/ui/textarea.tsx +34 -0
- package/src/components/ui/toast.tsx +203 -0
- package/src/components/ui/view.tsx +344 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAvatars.tsx +44 -0
- package/src/hooks/useCameraToggle.ts +12 -0
- package/src/hooks/useKeyboard.tsx +41 -0
- package/src/hooks/useKeyboardSlide.ts +12 -0
- package/src/hooks/useLivestreamInfo.ts +67 -0
- package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
- package/src/hooks/usePlayerDimensions.ts +23 -0
- package/src/hooks/useSegmentTiming.tsx +88 -0
- package/src/index.tsx +21 -0
- package/src/lib/facet.ts +131 -0
- package/src/lib/theme/atoms.ts +760 -0
- package/src/lib/theme/atoms.types.ts +258 -0
- package/src/lib/theme/index.ts +48 -0
- package/src/lib/theme/theme.tsx +436 -0
- package/src/lib/theme/tokens.ts +409 -0
- package/src/lib/utils.ts +132 -0
- package/src/livestream-provider/index.tsx +13 -2
- package/src/livestream-store/chat.tsx +115 -78
- package/src/livestream-store/index.tsx +1 -0
- package/src/livestream-store/livestream-state.tsx +3 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/stream-key.tsx +124 -0
- package/src/player-store/player-provider.tsx +0 -1
- package/src/player-store/player-state.tsx +28 -0
- package/src/player-store/player-store.tsx +22 -0
- package/src/streamplace-store/block.tsx +29 -0
- package/src/streamplace-store/index.tsx +1 -0
- package/src/streamplace-store/stream.tsx +262 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamplace/components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Streamplace React (Native) Components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "src/index.tsx",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
+
"@streamplace/dev": "./src/index.tsx",
|
|
10
11
|
"types": "./dist/index.d.mjs",
|
|
11
12
|
"default": "./dist/index.mjs"
|
|
12
13
|
}
|
|
@@ -27,13 +28,28 @@
|
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"@atproto/api": "^0.15.7",
|
|
30
|
-
"
|
|
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",
|
|
31
46
|
"react-use-websocket": "^4.13.0",
|
|
32
|
-
"streamplace": "0.
|
|
47
|
+
"streamplace": "0.7.0",
|
|
48
|
+
"viem": "^2.21.44",
|
|
33
49
|
"zustand": "^5.0.5"
|
|
34
50
|
},
|
|
35
51
|
"peerDependencies": {
|
|
36
52
|
"react": "*"
|
|
37
53
|
},
|
|
38
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "c0b9266fbc2cb2a643203e8c0450980c1bd29635"
|
|
39
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
|
+
}
|