@streamplace/components 0.10.6 → 0.10.8
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.js +1 -1
- package/dist/components/chat/mod-view.d.ts.map +1 -1
- package/dist/components/chat/mod-view.js +31 -6
- package/dist/components/chat/mod-view.js.map +1 -1
- package/dist/components/dashboard/moderator-panel.js +7 -1
- package/dist/components/dashboard/moderator-panel.js.map +1 -1
- package/dist/components/mobile-player/props.d.ts +1 -0
- package/dist/components/mobile-player/props.d.ts.map +1 -1
- package/dist/components/mobile-player/rotation-lock.js +2 -2
- package/dist/components/mobile-player/rotation-lock.js.map +1 -1
- package/dist/components/mobile-player/use-webrtc.d.ts +2 -1
- package/dist/components/mobile-player/use-webrtc.d.ts.map +1 -1
- package/dist/components/mobile-player/use-webrtc.js +30 -15
- package/dist/components/mobile-player/use-webrtc.js.map +1 -1
- package/dist/components/mobile-player/video-async.native.d.ts.map +1 -1
- package/dist/components/mobile-player/video-async.native.js +5 -1
- package/dist/components/mobile-player/video-async.native.js.map +1 -1
- package/dist/components/stream-notification/pin-notification.d.ts +7 -0
- package/dist/components/stream-notification/pin-notification.d.ts.map +1 -0
- package/dist/components/stream-notification/pin-notification.js +63 -0
- package/dist/components/stream-notification/pin-notification.js.map +1 -0
- package/dist/components/ui/dropdown.d.ts.map +1 -1
- package/dist/components/ui/dropdown.js +3 -2
- package/dist/components/ui/dropdown.js.map +1 -1
- package/dist/components/ui/resizeable.js +2 -2
- package/dist/components/ui/resizeable.js.map +1 -1
- package/dist/lib/stream-notifications.d.ts +7 -0
- package/dist/lib/stream-notifications.d.ts.map +1 -1
- package/dist/lib/stream-notifications.js +21 -0
- package/dist/lib/stream-notifications.js.map +1 -1
- package/dist/lib/theme/atoms.d.ts +141 -141
- package/dist/livestream-provider/index.d.ts +1 -0
- package/dist/livestream-provider/index.d.ts.map +1 -1
- package/dist/livestream-provider/index.js +35 -3
- package/dist/livestream-provider/index.js.map +1 -1
- package/dist/livestream-store/chat.d.ts +2 -0
- package/dist/livestream-store/chat.d.ts.map +1 -1
- package/dist/livestream-store/chat.js +80 -1
- package/dist/livestream-store/chat.js.map +1 -1
- package/dist/livestream-store/livestream-state.d.ts +2 -1
- package/dist/livestream-store/livestream-state.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.d.ts +1 -0
- package/dist/livestream-store/livestream-store.d.ts.map +1 -1
- package/dist/livestream-store/livestream-store.js +4 -1
- 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 +14 -0
- package/dist/livestream-store/websocket-consumer.js.map +1 -1
- package/dist/streamplace-store/moderation.d.ts +1 -0
- package/dist/streamplace-store/moderation.d.ts.map +1 -1
- package/dist/streamplace-store/moderation.js +1 -0
- package/dist/streamplace-store/moderation.js.map +1 -1
- package/dist/streamplace-store/moderator-management.d.ts +1 -1
- package/dist/streamplace-store/moderator-management.d.ts.map +1 -1
- package/dist/streamplace-store/xrpc.d.ts +2 -0
- package/dist/streamplace-store/xrpc.d.ts.map +1 -1
- package/dist/streamplace-store/xrpc.js +18 -0
- package/dist/streamplace-store/xrpc.js.map +1 -1
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +3 -3
- package/src/components/chat/chat.tsx +1 -1
- package/src/components/chat/mod-view.tsx +82 -3
- package/src/components/dashboard/moderator-panel.tsx +13 -2
- package/src/components/mobile-player/props.tsx +1 -0
- package/src/components/mobile-player/rotation-lock.tsx +2 -2
- package/src/components/mobile-player/use-webrtc.tsx +47 -12
- package/src/components/mobile-player/video-async.native.tsx +5 -0
- package/src/components/stream-notification/pin-notification.tsx +135 -0
- package/src/components/ui/dropdown.tsx +3 -2
- package/src/components/ui/resizeable.tsx +2 -2
- package/src/lib/stream-notifications.ts +28 -0
- package/src/livestream-provider/index.tsx +38 -2
- package/src/livestream-store/chat.tsx +92 -0
- package/src/livestream-store/livestream-state.tsx +2 -0
- package/src/livestream-store/livestream-store.tsx +4 -0
- package/src/livestream-store/websocket-consumer.tsx +15 -0
- package/src/streamplace-store/moderation.tsx +2 -0
- package/src/streamplace-store/moderator-management.tsx +1 -1
- package/src/streamplace-store/xrpc.tsx +22 -0
|
@@ -17,6 +17,7 @@ import { ChatMessageViewHydrated } from "streamplace";
|
|
|
17
17
|
import {
|
|
18
18
|
useDeleteChatMessage,
|
|
19
19
|
useLivestreamStore,
|
|
20
|
+
usePinChatMessage,
|
|
20
21
|
} from "../../livestream-store";
|
|
21
22
|
import { useStreamplaceStore } from "../../streamplace-store";
|
|
22
23
|
import { formatHandle, formatHandleWithAt } from "../../utils/format-handle";
|
|
@@ -25,6 +26,9 @@ import {
|
|
|
25
26
|
DropdownMenu,
|
|
26
27
|
DropdownMenuGroup,
|
|
27
28
|
DropdownMenuItem,
|
|
29
|
+
DropdownMenuSub,
|
|
30
|
+
DropdownMenuSubContent,
|
|
31
|
+
DropdownMenuSubTrigger,
|
|
28
32
|
DropdownMenuTrigger,
|
|
29
33
|
layout,
|
|
30
34
|
ResponsiveDropdownMenuContent,
|
|
@@ -55,6 +59,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
55
59
|
let [messageRemoved, setMessageRemoved] = useState(false);
|
|
56
60
|
let { createBlock, isLoading: isBlockLoading } = useCreateBlockRecord();
|
|
57
61
|
let { createHideChat, isLoading: isHideLoading } = useCreateHideChatRecord();
|
|
62
|
+
const pinChatMessage = usePinChatMessage();
|
|
58
63
|
|
|
59
64
|
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
60
65
|
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
@@ -91,9 +96,8 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
91
96
|
message &&
|
|
92
97
|
agent?.did &&
|
|
93
98
|
((modPermissions.canHide && message.author.did !== streamerDID) ||
|
|
94
|
-
(modPermissions.
|
|
95
|
-
|
|
96
|
-
message.author.did !== streamerDID))
|
|
99
|
+
(modPermissions.canPin && message.author.did !== streamerDID) ||
|
|
100
|
+
modPermissions.canBan)
|
|
97
101
|
);
|
|
98
102
|
|
|
99
103
|
return (
|
|
@@ -124,6 +128,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
124
128
|
setMessageRemoved={setMessageRemoved}
|
|
125
129
|
createHideChat={createHideChat}
|
|
126
130
|
createBlock={createBlock}
|
|
131
|
+
pinChatMessage={pinChatMessage}
|
|
127
132
|
toast={toast}
|
|
128
133
|
setReportModalOpen={setReportModalOpen}
|
|
129
134
|
setReportSubject={setReportSubject}
|
|
@@ -148,6 +153,11 @@ interface ModViewContentProps {
|
|
|
148
153
|
setMessageRemoved: (removed: boolean) => void;
|
|
149
154
|
createHideChat: (uri: string, streamerDID?: string) => Promise<any>;
|
|
150
155
|
createBlock: (did: string, streamerDID?: string) => Promise<any>;
|
|
156
|
+
pinChatMessage: (
|
|
157
|
+
messageUri: string,
|
|
158
|
+
streamerDID: string,
|
|
159
|
+
expiresAt?: string,
|
|
160
|
+
) => Promise<any>;
|
|
151
161
|
toast: ReturnType<typeof useToast>;
|
|
152
162
|
setReportModalOpen: (open: boolean) => void;
|
|
153
163
|
setReportSubject: (subject: any) => void;
|
|
@@ -166,6 +176,7 @@ function ModViewContent({
|
|
|
166
176
|
setMessageRemoved,
|
|
167
177
|
createHideChat,
|
|
168
178
|
createBlock,
|
|
179
|
+
pinChatMessage,
|
|
169
180
|
toast,
|
|
170
181
|
setReportModalOpen,
|
|
171
182
|
setReportSubject,
|
|
@@ -223,6 +234,74 @@ function ModViewContent({
|
|
|
223
234
|
</Text>
|
|
224
235
|
</DropdownMenuItem>
|
|
225
236
|
)}
|
|
237
|
+
{modPermissions.canPin && (
|
|
238
|
+
<DropdownMenuGroup key="pin-actions">
|
|
239
|
+
<DropdownMenuSub>
|
|
240
|
+
<DropdownMenuSubTrigger
|
|
241
|
+
subMenuTitle="Pin message"
|
|
242
|
+
style={{ padding: 0, margin: 0 }}
|
|
243
|
+
>
|
|
244
|
+
<Text color="primary">Pin this message</Text>
|
|
245
|
+
</DropdownMenuSubTrigger>
|
|
246
|
+
<DropdownMenuSubContent>
|
|
247
|
+
<DropdownMenuGroup title="Pin duration">
|
|
248
|
+
<DropdownMenuItem
|
|
249
|
+
onPress={() => {
|
|
250
|
+
if (!streamerDID) return;
|
|
251
|
+
pinChatMessage(message.uri, streamerDID)
|
|
252
|
+
.then(() => {
|
|
253
|
+
toast.show("Comment pinned", "", { duration: 3 });
|
|
254
|
+
onOpenChange?.(false);
|
|
255
|
+
})
|
|
256
|
+
.catch((e) => {
|
|
257
|
+
toast.show(
|
|
258
|
+
"Error pinning comment",
|
|
259
|
+
e instanceof Error ? e.message : "Failed to pin",
|
|
260
|
+
{ duration: 5 },
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
<Text color="primary">Until stream end</Text>
|
|
266
|
+
</DropdownMenuItem>
|
|
267
|
+
{[5, 10, 15, 30, 60].map((minutes) => (
|
|
268
|
+
<DropdownMenuItem
|
|
269
|
+
key={minutes}
|
|
270
|
+
onPress={() => {
|
|
271
|
+
if (!streamerDID) return;
|
|
272
|
+
const expiresAt = new Date(
|
|
273
|
+
Date.now() + minutes * 60 * 1000,
|
|
274
|
+
);
|
|
275
|
+
pinChatMessage(
|
|
276
|
+
message.uri,
|
|
277
|
+
streamerDID,
|
|
278
|
+
expiresAt.toISOString(),
|
|
279
|
+
)
|
|
280
|
+
.then(() => {
|
|
281
|
+
toast.show("Comment pinned", "", { duration: 3 });
|
|
282
|
+
onOpenChange?.(false);
|
|
283
|
+
})
|
|
284
|
+
.catch((e) => {
|
|
285
|
+
toast.show(
|
|
286
|
+
"Error pinning comment",
|
|
287
|
+
e instanceof Error
|
|
288
|
+
? e.message
|
|
289
|
+
: "Failed to pin",
|
|
290
|
+
{ duration: 5 },
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<Text color="primary">
|
|
296
|
+
{minutes < 60 ? `${minutes} min` : "1 hour"}
|
|
297
|
+
</Text>
|
|
298
|
+
</DropdownMenuItem>
|
|
299
|
+
))}
|
|
300
|
+
</DropdownMenuGroup>
|
|
301
|
+
</DropdownMenuSubContent>
|
|
302
|
+
</DropdownMenuSub>
|
|
303
|
+
</DropdownMenuGroup>
|
|
304
|
+
)}
|
|
226
305
|
{modPermissions.canBan &&
|
|
227
306
|
agent?.did &&
|
|
228
307
|
message.author.did !== agent.did &&
|
|
@@ -372,6 +372,7 @@ function AddModeratorDialog({
|
|
|
372
372
|
ban: false,
|
|
373
373
|
hide: false,
|
|
374
374
|
"livestream.manage": false,
|
|
375
|
+
"message.pin": false,
|
|
375
376
|
});
|
|
376
377
|
const [error, setError] = useState<string | null>(null);
|
|
377
378
|
const toast = useToast();
|
|
@@ -380,7 +381,12 @@ function AddModeratorDialog({
|
|
|
380
381
|
useEffect(() => {
|
|
381
382
|
if (!visible) {
|
|
382
383
|
setModeratorDID("");
|
|
383
|
-
setPermissions({
|
|
384
|
+
setPermissions({
|
|
385
|
+
ban: false,
|
|
386
|
+
hide: false,
|
|
387
|
+
"livestream.manage": false,
|
|
388
|
+
"message.pin": false,
|
|
389
|
+
});
|
|
384
390
|
setError(null);
|
|
385
391
|
}
|
|
386
392
|
}, [visible]);
|
|
@@ -401,7 +407,12 @@ function AddModeratorDialog({
|
|
|
401
407
|
|
|
402
408
|
const selectedPermissions = Object.entries(permissions)
|
|
403
409
|
.filter(([_, enabled]) => enabled)
|
|
404
|
-
.map(([perm]) => perm) as (
|
|
410
|
+
.map(([perm]) => perm) as (
|
|
411
|
+
| "ban"
|
|
412
|
+
| "hide"
|
|
413
|
+
| "livestream.manage"
|
|
414
|
+
| "message.pin"
|
|
415
|
+
)[];
|
|
405
416
|
|
|
406
417
|
if (selectedPermissions.length === 0) {
|
|
407
418
|
setError("Please select at least one permission");
|
|
@@ -64,12 +64,12 @@ export const RotationProvider: React.FC<RotationProviderProps> = ({
|
|
|
64
64
|
try {
|
|
65
65
|
await ScreenOrientation.unlockAsync();
|
|
66
66
|
await ScreenOrientation.lockAsync(
|
|
67
|
-
ScreenOrientation.OrientationLock.
|
|
67
|
+
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
|
|
68
68
|
);
|
|
69
69
|
setIsLocked(true);
|
|
70
70
|
|
|
71
71
|
// set current orientation to landscape right
|
|
72
|
-
setCurrentOrientation(ScreenOrientation.Orientation.
|
|
72
|
+
setCurrentOrientation(ScreenOrientation.Orientation.LANDSCAPE_LEFT);
|
|
73
73
|
|
|
74
74
|
if (__DEV__) {
|
|
75
75
|
console.log("📲 Manual landscape");
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { SessionManager } from "@atproto/api/dist/session-manager";
|
|
1
2
|
import { useEffect, useRef, useState } from "react";
|
|
2
3
|
import * as sdpTransform from "sdp-transform";
|
|
3
4
|
import { StreamplaceAgent } from "streamplace";
|
|
4
5
|
import {
|
|
6
|
+
createAgentForServer,
|
|
5
7
|
PlayerStatus,
|
|
6
8
|
usePlayerStore,
|
|
7
9
|
usePossiblyUnauthedPDSAgent,
|
|
8
10
|
useStreamKey,
|
|
11
|
+
useStreamplaceStore,
|
|
9
12
|
} from "../..";
|
|
10
13
|
import { RTCPeerConnection, RTCSessionDescription } from "./webrtc-primitives";
|
|
11
14
|
|
|
@@ -16,6 +19,7 @@ export default function useWebRTC(
|
|
|
16
19
|
const [stuck, setStuck] = useState<boolean>(false);
|
|
17
20
|
const setStatus = usePlayerStore((x) => x.setStatus);
|
|
18
21
|
let agent = usePossiblyUnauthedPDSAgent();
|
|
22
|
+
const oauthSession = useStreamplaceStore((state) => state.oauthSession);
|
|
19
23
|
|
|
20
24
|
const lastChange = useRef<number>(0);
|
|
21
25
|
|
|
@@ -59,6 +63,7 @@ export default function useWebRTC(
|
|
|
59
63
|
streamer,
|
|
60
64
|
undefined,
|
|
61
65
|
agent,
|
|
66
|
+
oauthSession,
|
|
62
67
|
);
|
|
63
68
|
});
|
|
64
69
|
|
|
@@ -97,7 +102,7 @@ export default function useWebRTC(
|
|
|
97
102
|
clearInterval(handle);
|
|
98
103
|
peerConnection.close();
|
|
99
104
|
};
|
|
100
|
-
}, [streamer, agent]);
|
|
105
|
+
}, [streamer, agent, oauthSession]);
|
|
101
106
|
return [mediaStream, stuck];
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -118,6 +123,7 @@ export async function negotiateConnectionWithClientOffer(
|
|
|
118
123
|
streamer: string,
|
|
119
124
|
bearerToken?: string,
|
|
120
125
|
agent?: StreamplaceAgent,
|
|
126
|
+
oauthSession?: SessionManager | null,
|
|
121
127
|
) {
|
|
122
128
|
/** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer */
|
|
123
129
|
const offer = await peerConnection.createOffer({
|
|
@@ -150,7 +156,13 @@ export async function negotiateConnectionWithClientOffer(
|
|
|
150
156
|
* This specifies how the client should communicate,
|
|
151
157
|
* and what kind of media client and server have negotiated to exchange.
|
|
152
158
|
*/
|
|
153
|
-
let response = await postSDPOffer(
|
|
159
|
+
let response = await postSDPOffer(
|
|
160
|
+
streamer,
|
|
161
|
+
ofr.sdp,
|
|
162
|
+
bearerToken,
|
|
163
|
+
agent,
|
|
164
|
+
oauthSession,
|
|
165
|
+
);
|
|
154
166
|
let text = new TextDecoder().decode(response.data);
|
|
155
167
|
if (response.success) {
|
|
156
168
|
if ((peerConnection.connectionState as string) === "closed") {
|
|
@@ -233,30 +245,53 @@ export async function negotiateIngestConnectionWithClientOffer(
|
|
|
233
245
|
}
|
|
234
246
|
}
|
|
235
247
|
|
|
248
|
+
async function getPlaybackServerAgent(
|
|
249
|
+
agent: StreamplaceAgent,
|
|
250
|
+
oauthSession: SessionManager | null | undefined,
|
|
251
|
+
streamer: string,
|
|
252
|
+
): Promise<StreamplaceAgent> {
|
|
253
|
+
const workerUrl = (window as any).PLAYBACK_WORKER_URL as string | undefined;
|
|
254
|
+
if (!workerUrl) {
|
|
255
|
+
return agent;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const lookupAgent = new StreamplaceAgent(workerUrl);
|
|
260
|
+
const res = await lookupAgent.place.stream.playback.getPlaybackServer({
|
|
261
|
+
stream: streamer,
|
|
262
|
+
});
|
|
263
|
+
if (res.data.servers.length > 0) {
|
|
264
|
+
const serverUrl = res.data.servers[0];
|
|
265
|
+
console.log(`Using playback server: ${serverUrl}`);
|
|
266
|
+
return createAgentForServer(oauthSession, serverUrl);
|
|
267
|
+
}
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.error("getPlaybackServer failed, using default agent:", e);
|
|
270
|
+
}
|
|
271
|
+
return agent;
|
|
272
|
+
}
|
|
273
|
+
|
|
236
274
|
async function postSDPOffer(
|
|
237
275
|
streamer: string,
|
|
238
276
|
data: string,
|
|
239
277
|
bearerToken?: string,
|
|
240
278
|
agent?: StreamplaceAgent,
|
|
279
|
+
oauthSession?: SessionManager | null,
|
|
241
280
|
) {
|
|
242
281
|
if (!agent) {
|
|
243
282
|
throw new Error("No agent found");
|
|
244
283
|
}
|
|
245
|
-
|
|
284
|
+
const playbackAgent = await getPlaybackServerAgent(
|
|
285
|
+
agent,
|
|
286
|
+
oauthSession,
|
|
287
|
+
streamer,
|
|
288
|
+
);
|
|
289
|
+
return await playbackAgent.place.stream.playback.whep(data, {
|
|
246
290
|
qp: {
|
|
247
291
|
rendition: "source",
|
|
248
292
|
streamer: streamer,
|
|
249
293
|
},
|
|
250
294
|
});
|
|
251
|
-
// return await fetch(endpoint, {
|
|
252
|
-
// method: "POST",
|
|
253
|
-
// mode: "cors",
|
|
254
|
-
// headers: {
|
|
255
|
-
// "content-type": "application/sdp",
|
|
256
|
-
// ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
257
|
-
// },
|
|
258
|
-
// body: data,
|
|
259
|
-
// });
|
|
260
295
|
}
|
|
261
296
|
|
|
262
297
|
async function postSDPIngestOffer(
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { EyeOff, Pin, X } from "lucide-react-native";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Linking, Pressable, View } from "react-native";
|
|
4
|
+
import { PinnedRecordViewHydrated, PlaceStreamChatProfile } from "streamplace";
|
|
5
|
+
import {
|
|
6
|
+
Text,
|
|
7
|
+
useCanModerate,
|
|
8
|
+
useLivestreamStore,
|
|
9
|
+
useTheme,
|
|
10
|
+
zero,
|
|
11
|
+
} from "../../";
|
|
12
|
+
import { RichtextSegment, segmentize } from "../../lib/facet";
|
|
13
|
+
import { formatHandleWithAt } from "../../utils/format-handle";
|
|
14
|
+
|
|
15
|
+
const getRgbColor = (color?: PlaceStreamChatProfile.Color) =>
|
|
16
|
+
color ? `rgb(${color.red}, ${color.green}, ${color.blue})` : undefined;
|
|
17
|
+
|
|
18
|
+
function renderSegment(segment: RichtextSegment, index: number) {
|
|
19
|
+
if (segment.features && segment.features.length > 0) {
|
|
20
|
+
const ftr = segment.features[0];
|
|
21
|
+
if (ftr.$type === "app.bsky.richtext.facet#link") {
|
|
22
|
+
return (
|
|
23
|
+
<Text
|
|
24
|
+
key={index}
|
|
25
|
+
style={[{ color: "#007AFF" }]}
|
|
26
|
+
onPress={() => Linking.openURL((ftr as any).uri || "")}
|
|
27
|
+
>
|
|
28
|
+
{segment.text}
|
|
29
|
+
</Text>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (ftr.$type === "app.bsky.richtext.facet#mention") {
|
|
33
|
+
return (
|
|
34
|
+
<Text key={index} style={[{ color: "#007AFF" }]}>
|
|
35
|
+
{segment.text}
|
|
36
|
+
</Text>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return <Text key={index}>{segment.text}</Text>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function PinnedCommentNotification({
|
|
44
|
+
pinnedComment,
|
|
45
|
+
onDismiss,
|
|
46
|
+
onUnpin,
|
|
47
|
+
}: {
|
|
48
|
+
pinnedComment: PinnedRecordViewHydrated;
|
|
49
|
+
onDismiss: () => void;
|
|
50
|
+
onUnpin: () => void;
|
|
51
|
+
}) {
|
|
52
|
+
const z = useTheme();
|
|
53
|
+
const message = pinnedComment.message;
|
|
54
|
+
const pinnedByColor = (pinnedComment.pinnedBy as any)?.color || "#bebebe";
|
|
55
|
+
const record = pinnedComment.record;
|
|
56
|
+
|
|
57
|
+
const currentStreamer = useLivestreamStore((state) => state.profile?.did);
|
|
58
|
+
|
|
59
|
+
console.log("checking if we can mod", currentStreamer);
|
|
60
|
+
|
|
61
|
+
const canActuallyPin = useCanModerate(currentStreamer)?.canPin;
|
|
62
|
+
|
|
63
|
+
const messageRecord = message?.record as any;
|
|
64
|
+
|
|
65
|
+
const [expiresAt] = useState<Date | null>(
|
|
66
|
+
record.expiresAt ? new Date(record.expiresAt) : null,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!expiresAt) return;
|
|
71
|
+
const remaining = expiresAt.getTime() - Date.now();
|
|
72
|
+
if (remaining <= 0) {
|
|
73
|
+
onDismiss();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const timeout = setTimeout(onDismiss, remaining);
|
|
77
|
+
return () => clearTimeout(timeout);
|
|
78
|
+
}, [expiresAt, onDismiss]);
|
|
79
|
+
|
|
80
|
+
const authorName = message ? formatHandleWithAt(message.author) : "unknown";
|
|
81
|
+
const authorColor = getRgbColor((message as any)?.chatProfile?.color);
|
|
82
|
+
|
|
83
|
+
const segments = messageRecord
|
|
84
|
+
? segmentize(messageRecord.text, messageRecord.facets)
|
|
85
|
+
: [];
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View style={[zero.bg.neutral[900], zero.r.lg, { overflow: "hidden" }]}>
|
|
89
|
+
<View
|
|
90
|
+
style={[
|
|
91
|
+
zero.layout.flex.row,
|
|
92
|
+
zero.layout.flex.alignCenter,
|
|
93
|
+
zero.px[3],
|
|
94
|
+
zero.py[2],
|
|
95
|
+
{ gap: 8 },
|
|
96
|
+
]}
|
|
97
|
+
>
|
|
98
|
+
<View style={{ transform: [{ rotate: "-25deg" }] }}>
|
|
99
|
+
<Pin
|
|
100
|
+
size={24}
|
|
101
|
+
color={authorColor || z.theme.colors.primary}
|
|
102
|
+
fill={authorColor || z.theme.colors.primary}
|
|
103
|
+
/>
|
|
104
|
+
</View>
|
|
105
|
+
<View
|
|
106
|
+
style={[zero.layout.flex.column, zero.flex.values[1], { gap: 4 }]}
|
|
107
|
+
>
|
|
108
|
+
<View style={[zero.layout.flex.row, { gap: 4, flexWrap: "wrap" }]}>
|
|
109
|
+
<Text
|
|
110
|
+
style={[
|
|
111
|
+
{
|
|
112
|
+
fontWeight: "600",
|
|
113
|
+
color: authorColor || zero.colors.gray[200],
|
|
114
|
+
},
|
|
115
|
+
]}
|
|
116
|
+
>
|
|
117
|
+
{authorName}
|
|
118
|
+
</Text>
|
|
119
|
+
{segments.map((seg, i) => renderSegment(seg, i))}
|
|
120
|
+
</View>
|
|
121
|
+
</View>
|
|
122
|
+
<View style={[zero.layout.flex.row, { gap: 4 }]}>
|
|
123
|
+
{canActuallyPin && (
|
|
124
|
+
<Pressable onPress={onUnpin} style={{ padding: 4 }}>
|
|
125
|
+
<X size={16} color={zero.colors.gray[400]} />
|
|
126
|
+
</Pressable>
|
|
127
|
+
)}
|
|
128
|
+
<Pressable onPress={onDismiss} style={{ padding: 4 }}>
|
|
129
|
+
<EyeOff size={16} color={zero.colors.gray[400]} />
|
|
130
|
+
</Pressable>
|
|
131
|
+
</View>
|
|
132
|
+
</View>
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -74,7 +74,7 @@ export const DropdownMenuSubTrigger = forwardRef<
|
|
|
74
74
|
inset?: boolean;
|
|
75
75
|
children?: React.ReactNode;
|
|
76
76
|
}
|
|
77
|
-
>(({ inset, children, subMenuTitle, ...props }, ref) => {
|
|
77
|
+
>(({ inset, children, subMenuTitle, style, ...props }, ref) => {
|
|
78
78
|
const { icons } = useTheme();
|
|
79
79
|
const { open } = DropdownMenuPrimitive.useSubContext();
|
|
80
80
|
const Icon =
|
|
@@ -96,6 +96,7 @@ export const DropdownMenuSubTrigger = forwardRef<
|
|
|
96
96
|
layout.flex.alignCenter,
|
|
97
97
|
p[2],
|
|
98
98
|
pr[8],
|
|
99
|
+
style,
|
|
99
100
|
]}
|
|
100
101
|
>
|
|
101
102
|
{children}
|
|
@@ -513,7 +514,7 @@ export const DropdownMenuGroup = forwardRef<
|
|
|
513
514
|
const { theme } = useTheme();
|
|
514
515
|
const { inset, title, children, ...rest } = props;
|
|
515
516
|
return (
|
|
516
|
-
<View style={[
|
|
517
|
+
<View style={[inset && gap[2]]} ref={ref} {...rest}>
|
|
517
518
|
{title && (
|
|
518
519
|
<Text style={[{ color: theme.colors.textMuted }, pb[1], pl[2]]}>
|
|
519
520
|
{title}
|
|
@@ -26,8 +26,8 @@ const AnimatedView = Animated.createAnimatedComponent(View);
|
|
|
26
26
|
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
27
27
|
|
|
28
28
|
const TIMING_CONFIG = {
|
|
29
|
-
duration:
|
|
30
|
-
easing: Easing.
|
|
29
|
+
duration: 400,
|
|
30
|
+
easing: Easing.out(Easing.quad),
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
type ResizableChatSheetProps = {
|
|
@@ -1,8 +1,36 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { PinnedRecordViewHydrated } from "streamplace";
|
|
2
3
|
import { streamNotification } from "../components/stream-notification";
|
|
4
|
+
import { PinnedCommentNotification } from "../components/stream-notification/pin-notification";
|
|
3
5
|
import { TeleportNotification } from "../components/stream-notification/teleport-notification";
|
|
4
6
|
|
|
5
7
|
export const StreamNotifications = {
|
|
8
|
+
pinnedComment: (params: {
|
|
9
|
+
pinnedComment: PinnedRecordViewHydrated;
|
|
10
|
+
onDismiss?: (reason?: "user" | "auto") => void;
|
|
11
|
+
onUnpin?: () => void;
|
|
12
|
+
}) => {
|
|
13
|
+
streamNotification.show({
|
|
14
|
+
id: "pinned-comment",
|
|
15
|
+
render: (isExiting, onDismiss) => {
|
|
16
|
+
return React.createElement(PinnedCommentNotification, {
|
|
17
|
+
pinnedComment: params.pinnedComment,
|
|
18
|
+
onDismiss: () => onDismiss("user"),
|
|
19
|
+
onUnpin: () => {
|
|
20
|
+
params.onUnpin?.();
|
|
21
|
+
onDismiss("user");
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
duration: 0, // manually dismissed or auto-dismissed by TTL
|
|
26
|
+
onDismiss: params.onDismiss,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
pinnedCommentDismiss: () => {
|
|
31
|
+
streamNotification.hide("pinned-comment");
|
|
32
|
+
},
|
|
33
|
+
|
|
6
34
|
teleport: (params: {
|
|
7
35
|
targetHandle: string;
|
|
8
36
|
targetDID: string;
|
|
@@ -4,8 +4,11 @@ import { deleteTeleport } from "../lib/slash-commands/teleport";
|
|
|
4
4
|
import { StreamNotifications } from "../lib/stream-notifications";
|
|
5
5
|
import {
|
|
6
6
|
LivestreamContext,
|
|
7
|
+
getStoreFromContext,
|
|
7
8
|
makeLivestreamStore,
|
|
8
9
|
useLivestreamStore,
|
|
10
|
+
usePinnedComment,
|
|
11
|
+
useUnpinChatMessage,
|
|
9
12
|
} from "../livestream-store";
|
|
10
13
|
import { useDID, usePDSAgent } from "../streamplace-store";
|
|
11
14
|
import { useLivestreamWebsocket } from "./websocket";
|
|
@@ -134,6 +137,40 @@ export function TeleportWatcher({
|
|
|
134
137
|
return <></>;
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
export function PinnedCommentWatcher() {
|
|
141
|
+
const pinnedComment = usePinnedComment();
|
|
142
|
+
const streamerDID = useLivestreamStore((state) => state.profile?.did);
|
|
143
|
+
const unpinChatMessage = useUnpinChatMessage();
|
|
144
|
+
const store = getStoreFromContext();
|
|
145
|
+
const prevPinnedRef = useRef<string | null>(null);
|
|
146
|
+
|
|
147
|
+
// Show/hide notification when pinned comment changes
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const currentUri = pinnedComment?.uri ?? null;
|
|
150
|
+
if (currentUri === prevPinnedRef.current) return;
|
|
151
|
+
prevPinnedRef.current = currentUri;
|
|
152
|
+
|
|
153
|
+
if (pinnedComment) {
|
|
154
|
+
StreamNotifications.pinnedComment({
|
|
155
|
+
pinnedComment,
|
|
156
|
+
onDismiss: () => {
|
|
157
|
+
store.setState({ pinnedComment: null });
|
|
158
|
+
},
|
|
159
|
+
onUnpin: () => {
|
|
160
|
+
if (!streamerDID) return;
|
|
161
|
+
unpinChatMessage(pinnedComment.uri, streamerDID).catch((e) => {
|
|
162
|
+
console.error("Failed to unpin:", e);
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
StreamNotifications.pinnedCommentDismiss();
|
|
168
|
+
}
|
|
169
|
+
}, [pinnedComment, streamerDID, unpinChatMessage]);
|
|
170
|
+
|
|
171
|
+
return <></>;
|
|
172
|
+
}
|
|
173
|
+
|
|
137
174
|
export function LivestreamPoller({
|
|
138
175
|
children,
|
|
139
176
|
src,
|
|
@@ -143,12 +180,11 @@ export function LivestreamPoller({
|
|
|
143
180
|
src: string;
|
|
144
181
|
onTeleport?: (targetHandle: string, targetDID: string) => void;
|
|
145
182
|
}) {
|
|
146
|
-
// Websocket watcher is a sibling instead of a parent to avoid
|
|
147
|
-
// re-rendering when the websocket does stuff
|
|
148
183
|
return (
|
|
149
184
|
<>
|
|
150
185
|
<WebsocketWatcher src={src} />
|
|
151
186
|
<TeleportWatcher onTeleport={onTeleport} />
|
|
187
|
+
<PinnedCommentWatcher />
|
|
152
188
|
{children}
|
|
153
189
|
</>
|
|
154
190
|
);
|