@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.
- package/dist/components/chat/chat-box.d.ts.map +1 -1
- package/dist/components/chat/chat-box.js +90 -34
- package/dist/components/chat/chat-box.js.map +1 -1
- package/dist/components/chat/chat-message.d.ts +4 -0
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +3 -2
- package/dist/components/chat/chat-message.js.map +1 -1
- package/dist/components/chat/chat.d.ts.map +1 -1
- package/dist/components/chat/chat.js +56 -3
- package/dist/components/chat/chat.js.map +1 -1
- package/dist/components/chat/emoji-suggestions.d.ts.map +1 -1
- package/dist/components/chat/emoji-suggestions.js +11 -11
- package/dist/components/chat/emoji-suggestions.js.map +1 -1
- package/dist/components/chat/mention-suggestions.d.ts.map +1 -1
- package/dist/components/chat/mention-suggestions.js +20 -19
- package/dist/components/chat/mention-suggestions.js.map +1 -1
- package/dist/components/chat/mod-view.d.ts.map +1 -1
- package/dist/components/chat/mod-view.js +1 -9
- package/dist/components/chat/mod-view.js.map +1 -1
- package/dist/components/chat/system-message.d.ts +5 -1
- package/dist/components/chat/system-message.d.ts.map +1 -1
- package/dist/components/chat/system-message.js +4 -4
- package/dist/components/chat/system-message.js.map +1 -1
- package/dist/components/mobile-player/shared.d.ts +1 -1
- package/dist/components/mobile-player/shared.d.ts.map +1 -1
- package/dist/components/mobile-player/shared.js +11 -10
- package/dist/components/mobile-player/shared.js.map +1 -1
- package/dist/components/mobile-player/ui/report-modal.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/report-modal.js +1 -1
- package/dist/components/mobile-player/ui/report-modal.js.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.d.ts +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js +60 -43
- package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
- package/dist/components/mobile-player/ui/viewer-loading-overlay.d.ts.map +1 -1
- package/dist/components/mobile-player/ui/viewer-loading-overlay.js +0 -1
- package/dist/components/mobile-player/ui/viewer-loading-overlay.js.map +1 -1
- package/dist/components/stream-notification/index.d.ts +3 -0
- package/dist/components/stream-notification/index.d.ts.map +1 -0
- package/dist/components/stream-notification/index.js +9 -0
- package/dist/components/stream-notification/index.js.map +1 -0
- package/dist/components/stream-notification/stream-notification-manager.d.ts +36 -0
- package/dist/components/stream-notification/stream-notification-manager.d.ts.map +1 -0
- package/dist/components/stream-notification/stream-notification-manager.js +96 -0
- package/dist/components/stream-notification/stream-notification-manager.js.map +1 -0
- package/dist/components/stream-notification/stream-notification.d.ts +5 -0
- package/dist/components/stream-notification/stream-notification.d.ts.map +1 -0
- package/dist/components/stream-notification/stream-notification.js +146 -0
- package/dist/components/stream-notification/stream-notification.js.map +1 -0
- package/dist/components/stream-notification/teleport-notification.d.ts +8 -0
- package/dist/components/stream-notification/teleport-notification.d.ts.map +1 -0
- package/dist/components/stream-notification/teleport-notification.js +116 -0
- package/dist/components/stream-notification/teleport-notification.js.map +1 -0
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +7 -0
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/dialog.d.ts +2 -2
- package/dist/components/ui/dropdown.d.ts +4 -0
- package/dist/components/ui/dropdown.d.ts.map +1 -1
- package/dist/components/ui/dropdown.js +41 -15
- package/dist/components/ui/dropdown.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/components/ui/portal.d.ts +2 -0
- package/dist/components/ui/portal.d.ts.map +1 -0
- package/dist/components/ui/portal.js +5 -0
- package/dist/components/ui/portal.js.map +1 -0
- package/dist/components/ui/portal.web.d.ts +11 -0
- package/dist/components/ui/portal.web.d.ts.map +1 -0
- package/dist/components/ui/portal.web.js +22 -0
- package/dist/components/ui/portal.web.js.map +1 -0
- package/dist/components/ui/resizeable.d.ts +2 -1
- package/dist/components/ui/resizeable.d.ts.map +1 -1
- package/dist/components/ui/resizeable.js +68 -26
- package/dist/components/ui/resizeable.js.map +1 -1
- package/dist/components/ui/text.d.ts +1 -1
- package/dist/components/ui/view.d.ts +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/slash-commands/teleport.d.ts +4 -0
- package/dist/lib/slash-commands/teleport.d.ts.map +1 -0
- package/dist/lib/slash-commands/teleport.js +110 -0
- package/dist/lib/slash-commands/teleport.js.map +1 -0
- package/dist/lib/slash-commands.d.ts +16 -0
- package/dist/lib/slash-commands.d.ts.map +1 -0
- package/dist/lib/slash-commands.js +46 -0
- package/dist/lib/slash-commands.js.map +1 -0
- package/dist/lib/stream-notifications.d.ts +13 -0
- package/dist/lib/stream-notifications.d.ts.map +1 -0
- package/dist/lib/stream-notifications.js +46 -0
- package/dist/lib/stream-notifications.js.map +1 -0
- package/dist/lib/system-messages.d.ts +4 -8
- package/dist/lib/system-messages.d.ts.map +1 -1
- package/dist/lib/system-messages.js +38 -2
- package/dist/lib/system-messages.js.map +1 -1
- package/dist/lib/theme/atoms.d.ts +193 -193
- package/dist/livestream-provider/index.d.ts +7 -2
- package/dist/livestream-provider/index.d.ts.map +1 -1
- package/dist/livestream-provider/index.js +72 -4
- package/dist/livestream-provider/index.js.map +1 -1
- package/dist/livestream-store/livestream-state.d.ts +4 -1
- package/dist/livestream-store/livestream-state.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/livestream-store.js.map +1 -1
- package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
- package/dist/livestream-store/websocket-consumer.js +30 -43
- package/dist/livestream-store/websocket-consumer.js.map +1 -1
- package/dist/streamplace-store/index.d.ts +1 -0
- package/dist/streamplace-store/index.d.ts.map +1 -1
- package/dist/streamplace-store/index.js +1 -0
- package/dist/streamplace-store/index.js.map +1 -1
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +4 -2
- package/src/components/chat/chat-box.tsx +126 -53
- package/src/components/chat/chat-message.tsx +1 -1
- package/src/components/chat/chat.tsx +79 -5
- package/src/components/chat/emoji-suggestions.tsx +27 -25
- package/src/components/chat/mention-suggestions.tsx +36 -33
- package/src/components/chat/mod-view.tsx +2 -13
- package/src/components/chat/system-message.tsx +14 -5
- package/src/components/mobile-player/shared.tsx +2 -1
- package/src/components/mobile-player/ui/report-modal.tsx +2 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +192 -166
- package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +0 -1
- package/src/components/stream-notification/index.ts +5 -0
- package/src/components/stream-notification/stream-notification-manager.ts +140 -0
- package/src/components/stream-notification/stream-notification.tsx +227 -0
- package/src/components/stream-notification/teleport-notification.tsx +187 -0
- package/src/components/ui/button.tsx +7 -0
- package/src/components/ui/dropdown.tsx +96 -26
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/portal.tsx +1 -0
- package/src/components/ui/portal.web.tsx +37 -0
- package/src/components/ui/resizeable.tsx +89 -35
- package/src/index.tsx +3 -0
- package/src/lib/slash-commands/teleport.ts +136 -0
- package/src/lib/slash-commands.ts +65 -0
- package/src/lib/stream-notifications.ts +51 -0
- package/src/lib/system-messages.ts +52 -2
- package/src/livestream-provider/index.tsx +106 -3
- package/src/livestream-store/livestream-state.tsx +4 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/websocket-consumer.tsx +35 -54
- package/src/streamplace-store/index.tsx +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChevronUp } from "lucide-react-native";
|
|
2
|
-
import { ComponentProps, useEffect } from "react";
|
|
2
|
+
import { ComponentProps, useEffect, useState } from "react";
|
|
3
3
|
import { Dimensions } from "react-native";
|
|
4
4
|
import {
|
|
5
5
|
Gesture,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import Animated, {
|
|
10
10
|
Extrapolation,
|
|
11
11
|
interpolate,
|
|
12
|
+
runOnJS,
|
|
12
13
|
useAnimatedStyle,
|
|
13
14
|
useSharedValue,
|
|
14
15
|
withSpring,
|
|
@@ -27,6 +28,7 @@ type ResizableChatSheetProps = {
|
|
|
27
28
|
isPlayerRatioGreater: boolean;
|
|
28
29
|
style?: ComponentProps<typeof AnimatedView>["style"];
|
|
29
30
|
children?: React.ReactNode;
|
|
31
|
+
renderAbove?: (isCollapsed: boolean) => React.ReactNode;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const SPRING_CONFIG = { damping: 20, stiffness: 100 };
|
|
@@ -36,6 +38,7 @@ export function Resizable({
|
|
|
36
38
|
isPlayerRatioGreater,
|
|
37
39
|
style = {},
|
|
38
40
|
children,
|
|
41
|
+
renderAbove,
|
|
39
42
|
}: ResizableChatSheetProps) {
|
|
40
43
|
const { slideKeyboard } = useKeyboardSlide();
|
|
41
44
|
const { bottom: safeBottom } = useSafeAreaInsets();
|
|
@@ -45,13 +48,16 @@ export function Resizable({
|
|
|
45
48
|
|
|
46
49
|
const sheetHeight = useSharedValue(MIN_HEIGHT);
|
|
47
50
|
const startHeight = useSharedValue(MIN_HEIGHT);
|
|
51
|
+
const [isCollapsed, setIsCollapsed] = useState(true);
|
|
52
|
+
const wasCollapsed = useSharedValue(true);
|
|
48
53
|
|
|
49
54
|
useEffect(() => {
|
|
50
55
|
setTimeout(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
56
|
+
const targetHeight = startingPercentage
|
|
57
|
+
? startingPercentage * SCREEN_HEIGHT
|
|
58
|
+
: MIN_HEIGHT;
|
|
59
|
+
sheetHeight.value = withSpring(targetHeight, SPRING_CONFIG);
|
|
60
|
+
setIsCollapsed(targetHeight < COLLAPSE_HEIGHT);
|
|
55
61
|
}, 1000);
|
|
56
62
|
}, []);
|
|
57
63
|
|
|
@@ -65,8 +71,14 @@ export function Resizable({
|
|
|
65
71
|
if (newHeight < MIN_HEIGHT) newHeight = MIN_HEIGHT;
|
|
66
72
|
sheetHeight.value = newHeight;
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
const nowCollapsed = newHeight < COLLAPSE_HEIGHT;
|
|
75
|
+
if (nowCollapsed && !wasCollapsed.value) {
|
|
69
76
|
sheetHeight.value = withSpring(MIN_HEIGHT, SPRING_CONFIG);
|
|
77
|
+
wasCollapsed.value = true;
|
|
78
|
+
runOnJS(setIsCollapsed)(true);
|
|
79
|
+
} else if (!nowCollapsed && wasCollapsed.value) {
|
|
80
|
+
wasCollapsed.value = false;
|
|
81
|
+
runOnJS(setIsCollapsed)(false);
|
|
70
82
|
}
|
|
71
83
|
});
|
|
72
84
|
|
|
@@ -97,6 +109,19 @@ export function Resizable({
|
|
|
97
109
|
],
|
|
98
110
|
}));
|
|
99
111
|
|
|
112
|
+
const aboveElementStyle = useAnimatedStyle(() => ({
|
|
113
|
+
// show inside area when not collapsed, and show outside area when collapsed
|
|
114
|
+
height: sheetHeight.value < COLLAPSE_HEIGHT ? 0 : sheetHeight.value,
|
|
115
|
+
transform: [
|
|
116
|
+
{
|
|
117
|
+
translateY:
|
|
118
|
+
sheetHeight.value < COLLAPSE_HEIGHT
|
|
119
|
+
? withSpring(-120)
|
|
120
|
+
: withSpring(20),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
}));
|
|
124
|
+
|
|
100
125
|
return (
|
|
101
126
|
<>
|
|
102
127
|
<Animated.View
|
|
@@ -111,10 +136,11 @@ export function Resizable({
|
|
|
111
136
|
>
|
|
112
137
|
<Pressable
|
|
113
138
|
onPress={() => {
|
|
114
|
-
sheetHeight.value
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
const isCurrentlyCollapsed = sheetHeight.value === MIN_HEIGHT;
|
|
140
|
+
sheetHeight.value = isCurrentlyCollapsed
|
|
141
|
+
? withSpring(MAX_HEIGHT, SPRING_CONFIG)
|
|
142
|
+
: withSpring(MIN_HEIGHT, SPRING_CONFIG);
|
|
143
|
+
setIsCollapsed(!isCurrentlyCollapsed);
|
|
118
144
|
}}
|
|
119
145
|
>
|
|
120
146
|
<View
|
|
@@ -155,36 +181,64 @@ export function Resizable({
|
|
|
155
181
|
]}
|
|
156
182
|
>
|
|
157
183
|
<View style={[layout.flex.row, layout.flex.justifyCenter, h[2]]}>
|
|
158
|
-
<
|
|
159
|
-
<
|
|
160
|
-
// Make the touch area much larger, but keep the visible handle small
|
|
161
|
-
style={{
|
|
162
|
-
height: 30, // Large touch area
|
|
163
|
-
width: 120, // Wide enough for thumbs
|
|
164
|
-
alignItems: "center",
|
|
165
|
-
justifyContent: "center",
|
|
166
|
-
//backgroundColor: "rgba(0,255,255,0.1)",
|
|
167
|
-
transform: [{ translateY: -30 }],
|
|
168
|
-
}}
|
|
169
|
-
>
|
|
184
|
+
<View style={{ alignItems: "center", width: "100%" }}>
|
|
185
|
+
<GestureDetector gesture={panGesture}>
|
|
170
186
|
<View
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
// Make the touch area much larger, but keep the visible handle small
|
|
188
|
+
style={{
|
|
189
|
+
height: 30, // Large touch area
|
|
190
|
+
width: 120, // Wide enough for thumbs
|
|
191
|
+
alignItems: "center",
|
|
192
|
+
justifyContent: "center",
|
|
193
|
+
//backgroundColor: "rgba(0,255,255,0.1)",
|
|
194
|
+
transform: [{ translateY: -30 }],
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<View
|
|
198
|
+
style={[
|
|
199
|
+
w[32],
|
|
200
|
+
{
|
|
201
|
+
height: 6,
|
|
202
|
+
backgroundColor: "#eeeeee66",
|
|
203
|
+
borderRadius: 999,
|
|
204
|
+
|
|
205
|
+
transform: [{ translateY: 5 }],
|
|
206
|
+
},
|
|
207
|
+
]}
|
|
208
|
+
/>
|
|
209
|
+
</View>
|
|
210
|
+
</GestureDetector>
|
|
211
|
+
</View>
|
|
184
212
|
</View>
|
|
185
213
|
|
|
186
214
|
{children}
|
|
187
215
|
</AnimatedView>
|
|
216
|
+
<Animated.View
|
|
217
|
+
style={[
|
|
218
|
+
aboveElementStyle,
|
|
219
|
+
{
|
|
220
|
+
width: "100%",
|
|
221
|
+
pointerEvents: "none",
|
|
222
|
+
position: "absolute",
|
|
223
|
+
bottom: 0,
|
|
224
|
+
},
|
|
225
|
+
]}
|
|
226
|
+
>
|
|
227
|
+
<View
|
|
228
|
+
style={{
|
|
229
|
+
pointerEvents: "auto",
|
|
230
|
+
width: "100%",
|
|
231
|
+
// hate doing it this way, but can't figure out
|
|
232
|
+
// how to make it size to content otherwise
|
|
233
|
+
minHeight: 50,
|
|
234
|
+
height: "100%",
|
|
235
|
+
maxHeight: 75,
|
|
236
|
+
flex: 0,
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
{renderAbove?.(isCollapsed)}
|
|
240
|
+
</View>
|
|
241
|
+
</Animated.View>
|
|
188
242
|
</>
|
|
189
243
|
);
|
|
190
244
|
}
|
package/src/index.tsx
CHANGED
|
@@ -37,6 +37,9 @@ export * from "./components/chat/system-message";
|
|
|
37
37
|
export { default as VideoRetry } from "./components/mobile-player/video-retry";
|
|
38
38
|
export * from "./lib/system-messages";
|
|
39
39
|
|
|
40
|
+
export * from "./components/stream-notification";
|
|
41
|
+
export * from "./lib/stream-notifications";
|
|
42
|
+
|
|
40
43
|
export * from "./utils/format-handle";
|
|
41
44
|
|
|
42
45
|
export { DanmuOverlay } from "./components/danmu/danmu-overlay";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { PlaceStreamLiveTeleport, StreamplaceAgent } from "streamplace";
|
|
2
|
+
import {
|
|
3
|
+
registerSlashCommand,
|
|
4
|
+
SlashCommandHandler,
|
|
5
|
+
SlashCommandResult,
|
|
6
|
+
} from "../slash-commands";
|
|
7
|
+
|
|
8
|
+
export async function deleteTeleport(
|
|
9
|
+
pdsAgent: StreamplaceAgent,
|
|
10
|
+
userDID: string,
|
|
11
|
+
uri: string,
|
|
12
|
+
) {
|
|
13
|
+
const rkey = uri.split("/").pop();
|
|
14
|
+
if (!rkey) {
|
|
15
|
+
throw new Error("No rkey found in teleport URI");
|
|
16
|
+
}
|
|
17
|
+
return await pdsAgent.com.atproto.repo.deleteRecord({
|
|
18
|
+
repo: userDID,
|
|
19
|
+
collection: "place.stream.live.teleport",
|
|
20
|
+
rkey: rkey,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function registerTeleportCommand(
|
|
25
|
+
pdsAgent: StreamplaceAgent,
|
|
26
|
+
userDID: string,
|
|
27
|
+
setActiveTeleportUri?: (uri: string | null) => void,
|
|
28
|
+
) {
|
|
29
|
+
const teleportHandler: SlashCommandHandler = async (
|
|
30
|
+
args,
|
|
31
|
+
rawInput,
|
|
32
|
+
): Promise<SlashCommandResult> => {
|
|
33
|
+
if (args.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
handled: true,
|
|
36
|
+
error: "Usage: /teleport @handle.bsky.social [duration_seconds]",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let targetHandle = args[0];
|
|
41
|
+
|
|
42
|
+
if (targetHandle.startsWith("@")) {
|
|
43
|
+
targetHandle = targetHandle.slice(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!targetHandle.includes(".")) {
|
|
47
|
+
return {
|
|
48
|
+
handled: true,
|
|
49
|
+
error: "Invalid handle format. Expected: handle.bsky.social",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let countdownSeconds = 10;
|
|
54
|
+
if (args.length > 1) {
|
|
55
|
+
const parsedDuration = parseInt(args[1], 10);
|
|
56
|
+
if (isNaN(parsedDuration)) {
|
|
57
|
+
return {
|
|
58
|
+
handled: true,
|
|
59
|
+
error: "Countdown must be a number (seconds)",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (parsedDuration < 5 || parsedDuration > 300) {
|
|
63
|
+
return {
|
|
64
|
+
handled: true,
|
|
65
|
+
error: "Countdown must be between 5 seconds and 5 minutes",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
countdownSeconds = parsedDuration;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let targetDID: string;
|
|
72
|
+
try {
|
|
73
|
+
const resolution = await pdsAgent.resolveHandle({
|
|
74
|
+
handle: targetHandle,
|
|
75
|
+
});
|
|
76
|
+
targetDID = resolution.data.did;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return {
|
|
79
|
+
handled: true,
|
|
80
|
+
error: `Could not resolve handle: ${targetHandle}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (targetDID === userDID) {
|
|
85
|
+
return {
|
|
86
|
+
handled: true,
|
|
87
|
+
error: "You cannot teleport to yourself",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const startsAt = new Date(
|
|
92
|
+
Date.now() + countdownSeconds * 1000,
|
|
93
|
+
).toISOString();
|
|
94
|
+
|
|
95
|
+
const record: PlaceStreamLiveTeleport.Record = {
|
|
96
|
+
$type: "place.stream.live.teleport",
|
|
97
|
+
streamer: targetDID,
|
|
98
|
+
startsAt,
|
|
99
|
+
countdownSeconds,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const result = await pdsAgent.com.atproto.repo.createRecord({
|
|
104
|
+
repo: userDID,
|
|
105
|
+
collection: "place.stream.live.teleport",
|
|
106
|
+
record,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// store the URI in the livestream store
|
|
110
|
+
if (setActiveTeleportUri) {
|
|
111
|
+
setActiveTeleportUri(result.data.uri);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { handled: true };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return {
|
|
117
|
+
handled: true,
|
|
118
|
+
error: err instanceof Error ? err.message : "Failed to create teleport",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
registerSlashCommand({
|
|
124
|
+
name: "teleport",
|
|
125
|
+
description: "Start a teleport to another streamer",
|
|
126
|
+
usage: "/teleport @handle.bsky.social [duration_seconds]",
|
|
127
|
+
handler: teleportHandler,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
registerSlashCommand({
|
|
131
|
+
name: "tp",
|
|
132
|
+
description: "Start a teleport to another streamer (alias for /teleport)",
|
|
133
|
+
usage: "/tp @handle.bsky.social [duration_seconds]",
|
|
134
|
+
handler: teleportHandler,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface SlashCommandResult {
|
|
2
|
+
handled: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type SlashCommandHandler = (
|
|
7
|
+
args: string[],
|
|
8
|
+
rawInput: string,
|
|
9
|
+
) => Promise<SlashCommandResult>;
|
|
10
|
+
|
|
11
|
+
export interface SlashCommand {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
usage: string;
|
|
15
|
+
handler: SlashCommandHandler;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const commands = new Map<string, SlashCommand>();
|
|
19
|
+
|
|
20
|
+
export function registerSlashCommand(command: SlashCommand) {
|
|
21
|
+
commands.set(command.name, command);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function unregisterSlashCommand(name: string) {
|
|
25
|
+
commands.delete(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleSlashCommand(
|
|
29
|
+
input: string,
|
|
30
|
+
): Promise<SlashCommandResult> {
|
|
31
|
+
const trimmed = input.trim();
|
|
32
|
+
if (!trimmed.startsWith("/")) {
|
|
33
|
+
return { handled: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
37
|
+
const commandName = parts[0]?.toLowerCase();
|
|
38
|
+
const args = parts.slice(1);
|
|
39
|
+
|
|
40
|
+
if (!commandName) {
|
|
41
|
+
return { handled: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const command = commands.get(commandName);
|
|
45
|
+
if (!command) {
|
|
46
|
+
return {
|
|
47
|
+
// for now - return false
|
|
48
|
+
handled: false,
|
|
49
|
+
error: `Unknown command: /${commandName}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return await command.handler(args, trimmed);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return {
|
|
57
|
+
handled: true,
|
|
58
|
+
error: err instanceof Error ? err.message : "Command failed",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getRegisteredCommands(): SlashCommand[] {
|
|
64
|
+
return Array.from(commands.values());
|
|
65
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { streamNotification } from "../components/stream-notification";
|
|
3
|
+
import { TeleportNotification } from "../components/stream-notification/teleport-notification";
|
|
4
|
+
|
|
5
|
+
export const StreamNotifications = {
|
|
6
|
+
teleport: (params: {
|
|
7
|
+
targetHandle: string;
|
|
8
|
+
targetDID: string;
|
|
9
|
+
countdown: number;
|
|
10
|
+
canCancel: boolean;
|
|
11
|
+
onDismiss?: (reason?: "user" | "auto") => void;
|
|
12
|
+
}) => {
|
|
13
|
+
streamNotification.show({
|
|
14
|
+
id: "teleport",
|
|
15
|
+
render: (isExiting, onDismiss, startTime) => {
|
|
16
|
+
return React.createElement(TeleportNotification, {
|
|
17
|
+
targetHandle: params.targetHandle,
|
|
18
|
+
countdown: params.countdown,
|
|
19
|
+
canCancel: params.canCancel,
|
|
20
|
+
startTime: startTime,
|
|
21
|
+
onDismiss: onDismiss,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
duration: 0, // manually dismissed by countdown or user cancel
|
|
25
|
+
variant: "warning",
|
|
26
|
+
onDismiss: params.onDismiss,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
teleportCancelled: () => {
|
|
31
|
+
streamNotification.hide("teleport");
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
teleportNow: (targetHandle: string) => {
|
|
35
|
+
streamNotification.show({
|
|
36
|
+
id: "teleport-now",
|
|
37
|
+
message: `Teleporting to @${targetHandle}...`,
|
|
38
|
+
duration: 2,
|
|
39
|
+
variant: "info",
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
activate: (message: string) => {
|
|
44
|
+
streamNotification.show({
|
|
45
|
+
id: "stream-activate",
|
|
46
|
+
message: message,
|
|
47
|
+
duration: 3,
|
|
48
|
+
variant: "info",
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -4,6 +4,7 @@ export enum SystemMessageType {
|
|
|
4
4
|
stream_start = "stream_start",
|
|
5
5
|
stream_end = "stream_end",
|
|
6
6
|
notification = "notification",
|
|
7
|
+
command_error = "command_error",
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export interface SystemMessageMetadata {
|
|
@@ -22,6 +23,8 @@ export interface SystemMessageMetadata {
|
|
|
22
23
|
* @param metadata Optional metadata for the message
|
|
23
24
|
* @returns A properly formatted ChatMessageViewHydrated object
|
|
24
25
|
*/
|
|
26
|
+
let systemMessageCounter = 0;
|
|
27
|
+
|
|
25
28
|
export const createSystemMessage = (
|
|
26
29
|
type: SystemMessageType,
|
|
27
30
|
text: string,
|
|
@@ -29,10 +32,11 @@ export const createSystemMessage = (
|
|
|
29
32
|
date: Date = new Date(),
|
|
30
33
|
): ChatMessageViewHydrated => {
|
|
31
34
|
const now = date;
|
|
35
|
+
const uniqueId = `${now.getTime()}-${systemMessageCounter++}`;
|
|
32
36
|
|
|
33
37
|
return {
|
|
34
|
-
uri: `at://did:sys:system/place.stream.chat.message/${
|
|
35
|
-
cid: `system-${
|
|
38
|
+
uri: `at://did:sys:system/place.stream.chat.message/${uniqueId}`,
|
|
39
|
+
cid: `system-${uniqueId}`,
|
|
36
40
|
author: {
|
|
37
41
|
did: "did:sys:system",
|
|
38
42
|
handle: type, // Use handle to specify the type of system message
|
|
@@ -73,8 +77,54 @@ export const SystemMessages = {
|
|
|
73
77
|
{ duration },
|
|
74
78
|
),
|
|
75
79
|
|
|
80
|
+
teleportArrival: (
|
|
81
|
+
streamerName: string,
|
|
82
|
+
streamerDid: string,
|
|
83
|
+
count: number,
|
|
84
|
+
chatProfile?: any,
|
|
85
|
+
): ChatMessageViewHydrated => {
|
|
86
|
+
const text =
|
|
87
|
+
count > 0
|
|
88
|
+
? `${count} viewer${count !== 1 ? "s" : ""} teleported from ${streamerName}'s stream! Say hi!`
|
|
89
|
+
: `Someone teleported from ${streamerName}'s stream! Say hi!`;
|
|
90
|
+
const message = createSystemMessage(SystemMessageType.notification, text, {
|
|
91
|
+
streamerName,
|
|
92
|
+
count,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// create a mention facet for the streamer name so it gets colored using existing mention rendering
|
|
96
|
+
if (chatProfile && streamerDid) {
|
|
97
|
+
const nameStart = text.indexOf(streamerName);
|
|
98
|
+
|
|
99
|
+
// encode byte positions
|
|
100
|
+
const encoder = new TextEncoder();
|
|
101
|
+
const byteStart = encoder.encode(text.substring(0, nameStart)).length;
|
|
102
|
+
const byteEnd = byteStart + encoder.encode(streamerName).length;
|
|
103
|
+
|
|
104
|
+
message.record.facets = [
|
|
105
|
+
{
|
|
106
|
+
index: {
|
|
107
|
+
byteStart,
|
|
108
|
+
byteEnd,
|
|
109
|
+
},
|
|
110
|
+
features: [
|
|
111
|
+
{
|
|
112
|
+
$type: "app.bsky.richtext.facet#mention",
|
|
113
|
+
did: streamerDid,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return message;
|
|
121
|
+
},
|
|
122
|
+
|
|
76
123
|
notification: (message: string): ChatMessageViewHydrated =>
|
|
77
124
|
createSystemMessage(SystemMessageType.notification, message),
|
|
125
|
+
|
|
126
|
+
commandError: (message: string): ChatMessageViewHydrated =>
|
|
127
|
+
createSystemMessage(SystemMessageType.command_error, message),
|
|
78
128
|
};
|
|
79
129
|
|
|
80
130
|
/**
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import React, { useContext, useRef } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import React, { useContext, useEffect, useRef } from "react";
|
|
2
|
+
import { useAvatars } from "../hooks";
|
|
3
|
+
import { deleteTeleport } from "../lib/slash-commands/teleport";
|
|
4
|
+
import { StreamNotifications } from "../lib/stream-notifications";
|
|
5
|
+
import {
|
|
6
|
+
LivestreamContext,
|
|
7
|
+
makeLivestreamStore,
|
|
8
|
+
useLivestreamStore,
|
|
9
|
+
} from "../livestream-store";
|
|
10
|
+
import { useDID, usePDSAgent } from "../streamplace-store";
|
|
3
11
|
import { useLivestreamWebsocket } from "./websocket";
|
|
4
12
|
|
|
5
13
|
export function LivestreamProvider({
|
|
6
14
|
children,
|
|
7
15
|
src,
|
|
16
|
+
onTeleport,
|
|
8
17
|
ignoreOuterContext = false,
|
|
9
18
|
}: {
|
|
10
19
|
children: React.ReactNode;
|
|
11
20
|
src: string;
|
|
21
|
+
onTeleport?: (targetHandle: string, targetDID: string) => void;
|
|
12
22
|
ignoreOuterContext?: boolean;
|
|
13
23
|
}) {
|
|
14
24
|
const context = useContext(LivestreamContext);
|
|
@@ -24,7 +34,9 @@ export function LivestreamProvider({
|
|
|
24
34
|
(window as any).livestreamStore = store;
|
|
25
35
|
return (
|
|
26
36
|
<LivestreamContext.Provider value={{ store: store }}>
|
|
27
|
-
<LivestreamPoller src={src}
|
|
37
|
+
<LivestreamPoller src={src} onTeleport={onTeleport}>
|
|
38
|
+
{children}
|
|
39
|
+
</LivestreamPoller>
|
|
28
40
|
</LivestreamContext.Provider>
|
|
29
41
|
);
|
|
30
42
|
}
|
|
@@ -34,18 +46,109 @@ export function WebsocketWatcher({ src }: { src: string }) {
|
|
|
34
46
|
return <></>;
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
export function TeleportWatcher({
|
|
50
|
+
onTeleport,
|
|
51
|
+
}: {
|
|
52
|
+
onTeleport?: (targetHandle: string, targetDID: string) => void;
|
|
53
|
+
}) {
|
|
54
|
+
const activeTeleport = useLivestreamStore((state) => state.activeTeleport);
|
|
55
|
+
const activeTeleportUri = useLivestreamStore(
|
|
56
|
+
(state) => state.activeTeleportUri,
|
|
57
|
+
);
|
|
58
|
+
const profile = useAvatars(activeTeleport ? [activeTeleport.streamer] : []);
|
|
59
|
+
const livestreamProfile = useLivestreamStore((state) => state.profile);
|
|
60
|
+
const pdsAgent = usePDSAgent();
|
|
61
|
+
const userDID = useDID();
|
|
62
|
+
const prevActiveTeleportRef = useRef(activeTeleport);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!activeTeleport || !profile[activeTeleport.streamer]) return;
|
|
66
|
+
|
|
67
|
+
const startsAt = new Date(activeTeleport.startsAt);
|
|
68
|
+
const now = new Date();
|
|
69
|
+
const countdown = Math.max(
|
|
70
|
+
0,
|
|
71
|
+
Math.floor((startsAt.getTime() - now.getTime()) / 1000),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// resolve the DID to a handle for display
|
|
75
|
+
const targetHandle =
|
|
76
|
+
profile[activeTeleport.streamer]?.handle || activeTeleport.streamer;
|
|
77
|
+
|
|
78
|
+
// check if the current user is the streamer of the current livestream
|
|
79
|
+
const canCancel = livestreamProfile?.did === userDID;
|
|
80
|
+
|
|
81
|
+
StreamNotifications.teleport({
|
|
82
|
+
targetHandle: targetHandle,
|
|
83
|
+
targetDID: activeTeleport.streamer,
|
|
84
|
+
countdown: countdown,
|
|
85
|
+
canCancel: canCancel,
|
|
86
|
+
onDismiss: async (reason) => {
|
|
87
|
+
console.log(
|
|
88
|
+
"🔍 StreamNotifications.onDismiss called with reason:",
|
|
89
|
+
reason,
|
|
90
|
+
);
|
|
91
|
+
if (reason === "user" && activeTeleportUri && pdsAgent && userDID) {
|
|
92
|
+
try {
|
|
93
|
+
await deleteTeleport(pdsAgent, userDID, activeTeleportUri);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error("Failed to delete teleport:", err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (reason === "auto" && onTeleport) {
|
|
99
|
+
console.log(
|
|
100
|
+
"🔍 Calling onTeleport with:",
|
|
101
|
+
targetHandle,
|
|
102
|
+
activeTeleport.streamer,
|
|
103
|
+
);
|
|
104
|
+
onTeleport(targetHandle, activeTeleport.streamer);
|
|
105
|
+
} else if (reason === "auto" && !onTeleport) {
|
|
106
|
+
console.log("🔍 onTeleport is not defined!");
|
|
107
|
+
} else if (reason === "auto") {
|
|
108
|
+
console.log(
|
|
109
|
+
"🔍 Reason is auto but teleport function not called for unknown reason",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}, [
|
|
115
|
+
activeTeleport,
|
|
116
|
+
activeTeleportUri,
|
|
117
|
+
profile,
|
|
118
|
+
onTeleport,
|
|
119
|
+
pdsAgent,
|
|
120
|
+
userDID,
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (
|
|
125
|
+
prevActiveTeleportRef.current &&
|
|
126
|
+
!activeTeleport &&
|
|
127
|
+
!activeTeleportUri
|
|
128
|
+
) {
|
|
129
|
+
StreamNotifications.teleportCancelled();
|
|
130
|
+
}
|
|
131
|
+
prevActiveTeleportRef.current = activeTeleport;
|
|
132
|
+
}, [activeTeleport, activeTeleportUri]);
|
|
133
|
+
|
|
134
|
+
return <></>;
|
|
135
|
+
}
|
|
136
|
+
|
|
37
137
|
export function LivestreamPoller({
|
|
38
138
|
children,
|
|
39
139
|
src,
|
|
140
|
+
onTeleport,
|
|
40
141
|
}: {
|
|
41
142
|
children: React.ReactNode;
|
|
42
143
|
src: string;
|
|
144
|
+
onTeleport?: (targetHandle: string, targetDID: string) => void;
|
|
43
145
|
}) {
|
|
44
146
|
// Websocket watcher is a sibling instead of a parent to avoid
|
|
45
147
|
// re-rendering when the websocket does stuff
|
|
46
148
|
return (
|
|
47
149
|
<>
|
|
48
150
|
<WebsocketWatcher src={src} />
|
|
151
|
+
<TeleportWatcher onTeleport={onTeleport} />
|
|
49
152
|
{children}
|
|
50
153
|
</>
|
|
51
154
|
);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ChatMessageViewHydrated,
|
|
4
4
|
LivestreamViewHydrated,
|
|
5
5
|
PlaceStreamDefs,
|
|
6
|
+
PlaceStreamLiveTeleport,
|
|
6
7
|
PlaceStreamModerationPermission,
|
|
7
8
|
PlaceStreamSegment,
|
|
8
9
|
} from "streamplace";
|
|
@@ -22,6 +23,9 @@ export interface LivestreamState {
|
|
|
22
23
|
replyToMessage: ChatMessageViewHydrated | null;
|
|
23
24
|
streamKey: string | null;
|
|
24
25
|
setStreamKey: (key: string | null) => void;
|
|
26
|
+
activeTeleport: PlaceStreamLiveTeleport.Record | null;
|
|
27
|
+
activeTeleportUri: string | null;
|
|
28
|
+
setActiveTeleportUri: (uri: string | null) => void;
|
|
25
29
|
websocketConnected: boolean;
|
|
26
30
|
hasReceivedSegment: boolean;
|
|
27
31
|
moderationPermissions: PlaceStreamModerationPermission.Record[];
|