@streamplace/components 0.8.18 → 0.9.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.d.ts +1 -1
- package/dist/components/chat/chat-box.d.ts.map +1 -1
- package/dist/components/chat/chat-box.js +3 -0
- package/dist/components/chat/chat-box.js.map +1 -1
- package/dist/components/chat/mod-view.d.ts +4 -2
- package/dist/components/chat/mod-view.d.ts.map +1 -1
- package/dist/components/chat/mod-view.js +142 -42
- package/dist/components/chat/mod-view.js.map +1 -1
- package/dist/components/dashboard/chat-panel.d.ts +2 -1
- package/dist/components/dashboard/chat-panel.d.ts.map +1 -1
- package/dist/components/dashboard/chat-panel.js +2 -3
- package/dist/components/dashboard/chat-panel.js.map +1 -1
- package/dist/components/dashboard/index.d.ts +1 -0
- package/dist/components/dashboard/index.d.ts.map +1 -1
- package/dist/components/dashboard/index.js +3 -1
- package/dist/components/dashboard/index.js.map +1 -1
- package/dist/components/dashboard/moderator-panel.d.ts +7 -0
- package/dist/components/dashboard/moderator-panel.d.ts.map +1 -0
- package/dist/components/dashboard/moderator-panel.js +256 -0
- package/dist/components/dashboard/moderator-panel.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +1 -1
- package/dist/components/ui/menu.d.ts.map +1 -1
- package/dist/components/ui/menu.js +2 -2
- package/dist/components/ui/menu.js.map +1 -1
- package/dist/crypto-polyfill.native.js +7 -1
- package/dist/crypto-polyfill.native.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useDocumentTitle.d.ts +6 -0
- package/dist/hooks/useDocumentTitle.d.ts.map +1 -0
- package/dist/hooks/useDocumentTitle.js +40 -0
- package/dist/hooks/useDocumentTitle.js.map +1 -0
- package/dist/lib/theme/atoms.d.ts +138 -138
- package/dist/lib/theme/branded-theme-provider.d.ts +13 -0
- package/dist/lib/theme/branded-theme-provider.d.ts.map +1 -0
- package/dist/lib/theme/branded-theme-provider.js +34 -0
- package/dist/lib/theme/branded-theme-provider.js.map +1 -0
- package/dist/lib/theme/index.d.ts +1 -0
- package/dist/lib/theme/index.d.ts.map +1 -1
- package/dist/lib/theme/index.js +4 -1
- package/dist/lib/theme/index.js.map +1 -1
- package/dist/livestream-store/chat.d.ts +1 -1
- package/dist/livestream-store/chat.d.ts.map +1 -1
- package/dist/livestream-store/chat.js +1 -3
- package/dist/livestream-store/chat.js.map +1 -1
- package/dist/livestream-store/livestream-state.d.ts +3 -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 +2 -0
- package/dist/livestream-store/livestream-store.js.map +1 -1
- package/dist/livestream-store/stream-key.d.ts +1 -0
- package/dist/livestream-store/stream-key.d.ts.map +1 -1
- package/dist/livestream-store/stream-key.js +2 -0
- package/dist/livestream-store/stream-key.js.map +1 -1
- package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
- package/dist/livestream-store/websocket-consumer.js +48 -0
- package/dist/livestream-store/websocket-consumer.js.map +1 -1
- package/dist/streamplace-provider/index.d.ts +3 -0
- package/dist/streamplace-provider/index.d.ts.map +1 -1
- package/dist/streamplace-provider/index.js +12 -1
- package/dist/streamplace-provider/index.js.map +1 -1
- package/dist/streamplace-store/block.d.ts +36 -2
- package/dist/streamplace-store/block.d.ts.map +1 -1
- package/dist/streamplace-store/block.js +121 -18
- package/dist/streamplace-store/block.js.map +1 -1
- package/dist/streamplace-store/branding.d.ts +27 -0
- package/dist/streamplace-store/branding.d.ts.map +1 -0
- package/dist/streamplace-store/branding.js +195 -0
- package/dist/streamplace-store/branding.js.map +1 -0
- package/dist/streamplace-store/index.d.ts +4 -0
- package/dist/streamplace-store/index.d.ts.map +1 -1
- package/dist/streamplace-store/index.js +4 -0
- package/dist/streamplace-store/index.js.map +1 -1
- package/dist/streamplace-store/moderation.d.ts +16 -0
- package/dist/streamplace-store/moderation.d.ts.map +1 -0
- package/dist/streamplace-store/moderation.js +141 -0
- package/dist/streamplace-store/moderation.js.map +1 -0
- package/dist/streamplace-store/moderator-management.d.ts +44 -0
- package/dist/streamplace-store/moderator-management.d.ts.map +1 -0
- package/dist/streamplace-store/moderator-management.js +136 -0
- package/dist/streamplace-store/moderator-management.js.map +1 -0
- package/dist/streamplace-store/streamplace-store.d.ts +6 -0
- package/dist/streamplace-store/streamplace-store.d.ts.map +1 -1
- package/dist/streamplace-store/streamplace-store.js +6 -0
- package/dist/streamplace-store/streamplace-store.js.map +1 -1
- package/dist/streamplace-store/xrpc.d.ts +1 -0
- package/dist/streamplace-store/xrpc.d.ts.map +1 -1
- package/dist/streamplace-store/xrpc.js +16 -0
- package/dist/streamplace-store/xrpc.js.map +1 -1
- package/locales/en-US/settings.ftl +91 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +3 -3
- package/src/components/chat/chat-box.tsx +3 -1
- package/src/components/chat/mod-view.tsx +431 -121
- package/src/components/dashboard/chat-panel.tsx +2 -1
- package/src/components/dashboard/index.tsx +1 -0
- package/src/components/dashboard/moderator-panel.tsx +632 -0
- package/src/components/ui/menu.tsx +1 -2
- package/src/crypto-polyfill.native.tsx +8 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useDocumentTitle.tsx +45 -0
- package/src/lib/theme/branded-theme-provider.tsx +58 -0
- package/src/lib/theme/index.ts +3 -0
- package/src/livestream-store/chat.tsx +0 -2
- package/src/livestream-store/livestream-state.tsx +5 -0
- package/src/livestream-store/livestream-store.tsx +2 -0
- package/src/livestream-store/stream-key.tsx +3 -0
- package/src/livestream-store/websocket-consumer.tsx +60 -0
- package/src/streamplace-provider/index.tsx +23 -4
- package/src/streamplace-store/block.tsx +139 -19
- package/src/streamplace-store/branding.tsx +216 -0
- package/src/streamplace-store/index.tsx +4 -0
- package/src/streamplace-store/moderation.tsx +185 -0
- package/src/streamplace-store/moderator-management.tsx +175 -0
- package/src/streamplace-store/streamplace-store.tsx +15 -0
- package/src/streamplace-store/xrpc.tsx +18 -1
- package/dist/assets/emoji-data.json +0 -19371
- package/src/assets/emoji-data.json +0 -19371
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
import { TriggerRef, useRootContext } from "@rn-primitives/dropdown-menu";
|
|
2
2
|
import { forwardRef, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { gap, mr, w } from "../../lib/theme/atoms";
|
|
4
|
-
import {
|
|
4
|
+
import { usePlayerStore } from "../../player-store";
|
|
5
5
|
import {
|
|
6
6
|
useCreateBlockRecord,
|
|
7
7
|
useCreateHideChatRecord,
|
|
8
|
+
useUpdateLivestreamRecord,
|
|
8
9
|
} from "../../streamplace-store/block";
|
|
10
|
+
import {
|
|
11
|
+
ModerationPermissions,
|
|
12
|
+
useCanModerate,
|
|
13
|
+
} from "../../streamplace-store/moderation";
|
|
9
14
|
import { usePDSAgent } from "../../streamplace-store/xrpc";
|
|
10
15
|
|
|
11
16
|
import { Linking } from "react-native";
|
|
12
17
|
import { ChatMessageViewHydrated } from "streamplace";
|
|
13
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
useDeleteChatMessage,
|
|
20
|
+
useLivestream,
|
|
21
|
+
useLivestreamStore,
|
|
22
|
+
} from "../../livestream-store";
|
|
14
23
|
import { useStreamplaceStore } from "../../streamplace-store";
|
|
15
24
|
import { formatHandle, formatHandleWithAt } from "../../utils/format-handle";
|
|
16
25
|
import {
|
|
17
26
|
atoms,
|
|
27
|
+
Button,
|
|
28
|
+
DialogFooter,
|
|
18
29
|
DropdownMenu,
|
|
19
30
|
DropdownMenuGroup,
|
|
20
31
|
DropdownMenuItem,
|
|
21
32
|
DropdownMenuTrigger,
|
|
22
33
|
layout,
|
|
34
|
+
ResponsiveDialog,
|
|
23
35
|
ResponsiveDropdownMenuContent,
|
|
24
36
|
Text,
|
|
37
|
+
Textarea,
|
|
25
38
|
useToast,
|
|
26
39
|
View,
|
|
27
40
|
} from "../ui";
|
|
@@ -42,33 +55,37 @@ export type ModViewRef = {
|
|
|
42
55
|
export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
43
56
|
const triggerRef = useRef<TriggerRef>(null);
|
|
44
57
|
const message = usePlayerStore((state) => state.modMessage);
|
|
58
|
+
const toast = useToast();
|
|
45
59
|
|
|
46
60
|
let agent = usePDSAgent();
|
|
47
61
|
let [messageRemoved, setMessageRemoved] = useState(false);
|
|
48
62
|
let { createBlock, isLoading: isBlockLoading } = useCreateBlockRecord();
|
|
49
63
|
let { createHideChat, isLoading: isHideLoading } = useCreateHideChatRecord();
|
|
64
|
+
let { updateLivestream, isLoading: isUpdateTitleLoading } =
|
|
65
|
+
useUpdateLivestreamRecord();
|
|
66
|
+
const livestream = useLivestream();
|
|
67
|
+
const [showUpdateTitleDialog, setShowUpdateTitleDialog] = useState(false);
|
|
50
68
|
|
|
51
69
|
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
52
70
|
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
53
71
|
const setModMessage = usePlayerStore((x) => x.setModMessage);
|
|
54
72
|
const deleteChatMessage = useDeleteChatMessage();
|
|
55
|
-
|
|
73
|
+
|
|
74
|
+
// Get the streamer's DID from the livestream profile
|
|
75
|
+
const streamerDID = useLivestreamStore((x) => x.profile?.did);
|
|
76
|
+
// Check moderation permissions for the current user on this streamer's channel
|
|
77
|
+
const modPermissions = useCanModerate(streamerDID);
|
|
56
78
|
|
|
57
79
|
// get the channel did
|
|
58
80
|
const channelId = usePlayerStore((state) => state.src);
|
|
59
81
|
// get the logged in user's identity
|
|
60
82
|
const handle = useStreamplaceStore((state) => state.handle);
|
|
61
83
|
|
|
62
|
-
if (!agent?.did) {
|
|
63
|
-
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
|
|
64
|
-
<Text>Log in to submit mod actions</Text>
|
|
65
|
-
</View>;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
84
|
const cleanup = () => {
|
|
69
85
|
setModMessage(null);
|
|
70
86
|
};
|
|
71
87
|
|
|
88
|
+
// Effect must be called unconditionally (before any early returns)
|
|
72
89
|
useEffect(() => {
|
|
73
90
|
if (message) {
|
|
74
91
|
setMessageRemoved(false);
|
|
@@ -78,125 +95,258 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
78
95
|
}
|
|
79
96
|
}, [message]);
|
|
80
97
|
|
|
98
|
+
// Early return AFTER all hooks have been called
|
|
99
|
+
if (!agent?.did) {
|
|
100
|
+
return <></>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Can show moderation actions if user can hide, ban, or manage livestream
|
|
104
|
+
const canModerate =
|
|
105
|
+
modPermissions.canHide ||
|
|
106
|
+
modPermissions.canBan ||
|
|
107
|
+
modPermissions.canManageLivestream;
|
|
108
|
+
|
|
109
|
+
// Check if any moderation actions are actually available for this message
|
|
110
|
+
// This must match the individual action checks inside the DropdownMenuGroup
|
|
111
|
+
const hasAvailableActions = !!(
|
|
112
|
+
message &&
|
|
113
|
+
agent?.did &&
|
|
114
|
+
((modPermissions.canHide && message.author.did !== streamerDID) ||
|
|
115
|
+
(modPermissions.canBan &&
|
|
116
|
+
message.author.did !== agent.did &&
|
|
117
|
+
message.author.did !== streamerDID))
|
|
118
|
+
);
|
|
119
|
+
|
|
81
120
|
return (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
121
|
+
<>
|
|
122
|
+
<DropdownMenu
|
|
123
|
+
style={[layout.flex.row, layout.flex.alignCenter, gap.all[2], w[80]]}
|
|
124
|
+
onOpenChange={(isOpen) => {
|
|
125
|
+
if (!isOpen) {
|
|
126
|
+
cleanup();
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<DropdownMenuTrigger ref={triggerRef}>
|
|
131
|
+
{/* Hidden trigger */}
|
|
132
|
+
<View />
|
|
133
|
+
</DropdownMenuTrigger>
|
|
134
|
+
<ResponsiveDropdownMenuContent>
|
|
135
|
+
{message && (
|
|
136
|
+
<ModViewContent
|
|
137
|
+
message={message}
|
|
138
|
+
modPermissions={modPermissions}
|
|
139
|
+
agent={agent}
|
|
140
|
+
streamerDID={streamerDID}
|
|
141
|
+
hasAvailableActions={hasAvailableActions}
|
|
142
|
+
isHideLoading={isHideLoading}
|
|
143
|
+
isBlockLoading={isBlockLoading}
|
|
144
|
+
messageRemoved={messageRemoved}
|
|
145
|
+
setMessageRemoved={setMessageRemoved}
|
|
146
|
+
createHideChat={createHideChat}
|
|
147
|
+
createBlock={createBlock}
|
|
148
|
+
toast={toast}
|
|
149
|
+
setShowUpdateTitleDialog={setShowUpdateTitleDialog}
|
|
150
|
+
isUpdateTitleLoading={isUpdateTitleLoading}
|
|
151
|
+
livestream={livestream}
|
|
152
|
+
setReportModalOpen={setReportModalOpen}
|
|
153
|
+
setReportSubject={setReportSubject}
|
|
154
|
+
deleteChatMessage={deleteChatMessage}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
</ResponsiveDropdownMenuContent>
|
|
158
|
+
</DropdownMenu>
|
|
159
|
+
|
|
160
|
+
{/* Update Stream Title Dialog - rendered outside dropdown */}
|
|
161
|
+
{showUpdateTitleDialog && (
|
|
162
|
+
<UpdateStreamTitleDialog
|
|
163
|
+
livestream={livestream}
|
|
164
|
+
streamerDID={streamerDID}
|
|
165
|
+
updateLivestream={updateLivestream}
|
|
166
|
+
isLoading={isUpdateTitleLoading}
|
|
167
|
+
onClose={() => setShowUpdateTitleDialog(false)}
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
interface ModViewContentProps {
|
|
175
|
+
message: ChatMessageViewHydrated;
|
|
176
|
+
modPermissions: ModerationPermissions;
|
|
177
|
+
agent: ReturnType<typeof usePDSAgent>;
|
|
178
|
+
streamerDID?: string;
|
|
179
|
+
hasAvailableActions: boolean;
|
|
180
|
+
isHideLoading: boolean;
|
|
181
|
+
isBlockLoading: boolean;
|
|
182
|
+
messageRemoved: boolean;
|
|
183
|
+
setMessageRemoved: (removed: boolean) => void;
|
|
184
|
+
createHideChat: (uri: string, streamerDID?: string) => Promise<any>;
|
|
185
|
+
createBlock: (did: string, streamerDID?: string) => Promise<any>;
|
|
186
|
+
toast: ReturnType<typeof useToast>;
|
|
187
|
+
setShowUpdateTitleDialog: (show: boolean) => void;
|
|
188
|
+
isUpdateTitleLoading: boolean;
|
|
189
|
+
livestream: any;
|
|
190
|
+
setReportModalOpen: (open: boolean) => void;
|
|
191
|
+
setReportSubject: (subject: any) => void;
|
|
192
|
+
deleteChatMessage: (uri: string) => Promise<any>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ModViewContent({
|
|
196
|
+
message,
|
|
197
|
+
modPermissions,
|
|
198
|
+
agent,
|
|
199
|
+
streamerDID,
|
|
200
|
+
hasAvailableActions,
|
|
201
|
+
isHideLoading,
|
|
202
|
+
isBlockLoading,
|
|
203
|
+
messageRemoved,
|
|
204
|
+
setMessageRemoved,
|
|
205
|
+
createHideChat,
|
|
206
|
+
createBlock,
|
|
207
|
+
toast,
|
|
208
|
+
setShowUpdateTitleDialog,
|
|
209
|
+
isUpdateTitleLoading,
|
|
210
|
+
livestream,
|
|
211
|
+
setReportModalOpen,
|
|
212
|
+
setReportSubject,
|
|
213
|
+
deleteChatMessage,
|
|
214
|
+
}: ModViewContentProps) {
|
|
215
|
+
const { onOpenChange } = useRootContext();
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<>
|
|
219
|
+
<DropdownMenuGroup key="message-display">
|
|
220
|
+
<DropdownMenuItem>
|
|
221
|
+
<View
|
|
222
|
+
style={[layout.flex.column, mr[5], { gap: 6, maxWidth: "100%" }]}
|
|
223
|
+
>
|
|
224
|
+
<Text
|
|
225
|
+
style={{
|
|
226
|
+
fontVariant: ["tabular-nums"],
|
|
227
|
+
color: atoms.colors.gray[300],
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{new Date(message.record.createdAt).toLocaleTimeString([], {
|
|
231
|
+
hour: "2-digit",
|
|
232
|
+
minute: "2-digit",
|
|
233
|
+
hour12: false,
|
|
234
|
+
})}{" "}
|
|
235
|
+
{formatHandleWithAt(message.author)}: {message.record.text}
|
|
236
|
+
</Text>
|
|
237
|
+
</View>
|
|
238
|
+
</DropdownMenuItem>
|
|
239
|
+
</DropdownMenuGroup>
|
|
169
240
|
|
|
170
|
-
|
|
241
|
+
{hasAvailableActions && (
|
|
242
|
+
<DropdownMenuGroup
|
|
243
|
+
key="moderation-actions"
|
|
244
|
+
title={`Moderation actions`}
|
|
245
|
+
>
|
|
246
|
+
{modPermissions.canHide && message.author.did !== streamerDID && (
|
|
247
|
+
<DropdownMenuItem
|
|
248
|
+
disabled={isHideLoading || messageRemoved}
|
|
249
|
+
onPress={() => {
|
|
250
|
+
if (isHideLoading || messageRemoved) return;
|
|
251
|
+
createHideChat(message.uri, streamerDID ?? undefined)
|
|
252
|
+
.then((r) => setMessageRemoved(true))
|
|
253
|
+
.catch((e) => console.error(e));
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<Text
|
|
257
|
+
color={isHideLoading || messageRemoved ? "muted" : "warning"}
|
|
258
|
+
>
|
|
259
|
+
{isHideLoading
|
|
260
|
+
? "Hiding..."
|
|
261
|
+
: messageRemoved
|
|
262
|
+
? "Message hidden"
|
|
263
|
+
: "Hide this message"}
|
|
264
|
+
</Text>
|
|
265
|
+
</DropdownMenuItem>
|
|
266
|
+
)}
|
|
267
|
+
{modPermissions.canBan &&
|
|
268
|
+
agent?.did &&
|
|
269
|
+
message.author.did !== agent.did &&
|
|
270
|
+
message.author.did !== streamerDID && (
|
|
171
271
|
<DropdownMenuItem
|
|
272
|
+
disabled={isBlockLoading}
|
|
172
273
|
onPress={() => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
274
|
+
if (isBlockLoading) return;
|
|
275
|
+
createBlock(message.author.did, streamerDID ?? undefined)
|
|
276
|
+
.then((r) => {
|
|
277
|
+
toast.show(
|
|
278
|
+
"User blocked",
|
|
279
|
+
`${formatHandleWithAt(message.author)} has been blocked from this channel.`,
|
|
280
|
+
{ duration: 3 },
|
|
281
|
+
);
|
|
282
|
+
onOpenChange?.(false);
|
|
283
|
+
})
|
|
284
|
+
.catch((e) => {
|
|
285
|
+
console.error(e);
|
|
286
|
+
toast.show(
|
|
287
|
+
"Error blocking user",
|
|
288
|
+
e instanceof Error ? e.message : "Failed to block user",
|
|
289
|
+
{ duration: 5 },
|
|
290
|
+
);
|
|
291
|
+
});
|
|
176
292
|
}}
|
|
177
293
|
>
|
|
178
|
-
<Text color="
|
|
294
|
+
<Text color="destructive">
|
|
295
|
+
{isBlockLoading
|
|
296
|
+
? "Blocking..."
|
|
297
|
+
: `Block user ${formatHandleWithAt(message.author)} from this channel`}
|
|
298
|
+
</Text>
|
|
179
299
|
</DropdownMenuItem>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
300
|
+
)}
|
|
301
|
+
</DropdownMenuGroup>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
{modPermissions.canManageLivestream && (
|
|
305
|
+
<DropdownMenuGroup key="stream-actions" title={`Stream actions`}>
|
|
306
|
+
<DropdownMenuItem
|
|
307
|
+
onPress={() => {
|
|
308
|
+
setShowUpdateTitleDialog(true);
|
|
309
|
+
}}
|
|
310
|
+
disabled={isUpdateTitleLoading || !livestream}
|
|
311
|
+
>
|
|
312
|
+
<Text
|
|
313
|
+
color={isUpdateTitleLoading || !livestream ? "muted" : "primary"}
|
|
314
|
+
>
|
|
315
|
+
{isUpdateTitleLoading ? "Updating..." : "Update stream title"}
|
|
316
|
+
</Text>
|
|
317
|
+
</DropdownMenuItem>
|
|
318
|
+
</DropdownMenuGroup>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
<DropdownMenuGroup key="user-actions" title={`User actions`}>
|
|
322
|
+
<DropdownMenuItem
|
|
323
|
+
onPress={() => {
|
|
324
|
+
Linking.openURL(
|
|
325
|
+
`https://${BSKY_FRONTEND_DOMAIN}/profile/${formatHandle(message.author)}`,
|
|
326
|
+
);
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
<Text color="primary">View user on {BSKY_FRONTEND_DOMAIN}</Text>
|
|
330
|
+
</DropdownMenuItem>
|
|
331
|
+
{message.author.did === agent?.did && (
|
|
332
|
+
<DeleteButton
|
|
333
|
+
message={message}
|
|
334
|
+
deleteChatMessage={deleteChatMessage}
|
|
335
|
+
onOpenChange={onOpenChange}
|
|
336
|
+
/>
|
|
337
|
+
)}
|
|
338
|
+
{message.author.did !== agent?.did && (
|
|
339
|
+
<ReportButton
|
|
340
|
+
message={message}
|
|
341
|
+
setReportModalOpen={setReportModalOpen}
|
|
342
|
+
setReportSubject={setReportSubject}
|
|
343
|
+
onOpenChange={onOpenChange}
|
|
344
|
+
/>
|
|
195
345
|
)}
|
|
196
|
-
</
|
|
197
|
-
|
|
346
|
+
</DropdownMenuGroup>
|
|
347
|
+
</>
|
|
198
348
|
);
|
|
199
|
-
}
|
|
349
|
+
}
|
|
200
350
|
|
|
201
351
|
enum DeleteState {
|
|
202
352
|
None,
|
|
@@ -207,12 +357,13 @@ enum DeleteState {
|
|
|
207
357
|
export function DeleteButton({
|
|
208
358
|
message,
|
|
209
359
|
deleteChatMessage,
|
|
360
|
+
onOpenChange,
|
|
210
361
|
}: {
|
|
211
362
|
message: ChatMessageViewHydrated;
|
|
212
363
|
deleteChatMessage: (uri: string) => Promise<any>;
|
|
364
|
+
onOpenChange?: (open: boolean) => void;
|
|
213
365
|
}) {
|
|
214
366
|
const [confirming, setConfirming] = useState<DeleteState>(DeleteState.None);
|
|
215
|
-
const { onOpenChange } = useRootContext();
|
|
216
367
|
const toast = useToast();
|
|
217
368
|
return (
|
|
218
369
|
<DropdownMenuItem
|
|
@@ -253,12 +404,13 @@ export function ReportButton({
|
|
|
253
404
|
message,
|
|
254
405
|
setReportModalOpen,
|
|
255
406
|
setReportSubject,
|
|
407
|
+
onOpenChange,
|
|
256
408
|
}: {
|
|
257
409
|
message: ChatMessageViewHydrated;
|
|
258
410
|
setReportModalOpen: (open: boolean) => void;
|
|
259
411
|
setReportSubject: (subject: any) => void;
|
|
412
|
+
onOpenChange?: (open: boolean) => void;
|
|
260
413
|
}) {
|
|
261
|
-
const { onOpenChange } = useRootContext();
|
|
262
414
|
return (
|
|
263
415
|
<DropdownMenuItem
|
|
264
416
|
onPress={() => {
|
|
@@ -277,3 +429,161 @@ export function ReportButton({
|
|
|
277
429
|
</DropdownMenuItem>
|
|
278
430
|
);
|
|
279
431
|
}
|
|
432
|
+
|
|
433
|
+
interface UpdateStreamTitleDialogProps {
|
|
434
|
+
livestream: any;
|
|
435
|
+
streamerDID?: string;
|
|
436
|
+
updateLivestream: (
|
|
437
|
+
livestreamUri: string,
|
|
438
|
+
title: string,
|
|
439
|
+
streamerDID?: string,
|
|
440
|
+
) => Promise<any>;
|
|
441
|
+
isLoading: boolean;
|
|
442
|
+
onClose: () => void;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function UpdateStreamTitleDialog({
|
|
446
|
+
livestream,
|
|
447
|
+
streamerDID,
|
|
448
|
+
updateLivestream,
|
|
449
|
+
isLoading,
|
|
450
|
+
onClose,
|
|
451
|
+
}: UpdateStreamTitleDialogProps) {
|
|
452
|
+
const [title, setTitle] = useState(livestream?.record?.title || "");
|
|
453
|
+
const [error, setError] = useState<string | null>(null);
|
|
454
|
+
const toast = useToast();
|
|
455
|
+
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
if (livestream?.record?.title) {
|
|
458
|
+
setTitle(livestream.record.title);
|
|
459
|
+
}
|
|
460
|
+
}, [livestream?.record?.title]);
|
|
461
|
+
|
|
462
|
+
const handleUpdate = async () => {
|
|
463
|
+
setError(null);
|
|
464
|
+
|
|
465
|
+
if (!title.trim()) {
|
|
466
|
+
setError("Please enter a stream title");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!livestream?.uri) {
|
|
471
|
+
setError("No livestream found");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
await updateLivestream(livestream.uri, title.trim(), streamerDID);
|
|
477
|
+
toast.show(
|
|
478
|
+
"Stream title updated",
|
|
479
|
+
"The stream title has been successfully updated.",
|
|
480
|
+
{ duration: 3 },
|
|
481
|
+
);
|
|
482
|
+
onClose();
|
|
483
|
+
} catch (err) {
|
|
484
|
+
setError(
|
|
485
|
+
err instanceof Error ? err.message : "Failed to update stream title",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<ResponsiveDialog
|
|
492
|
+
open={true}
|
|
493
|
+
onOpenChange={(open) => {
|
|
494
|
+
if (!open) {
|
|
495
|
+
onClose();
|
|
496
|
+
setError(null);
|
|
497
|
+
setTitle(livestream?.record?.title || "");
|
|
498
|
+
}
|
|
499
|
+
}}
|
|
500
|
+
title="Update Stream Title"
|
|
501
|
+
description="Update the title of the livestream."
|
|
502
|
+
size="md"
|
|
503
|
+
dismissible={false}
|
|
504
|
+
>
|
|
505
|
+
<View style={[{ padding: 16, paddingBottom: 0 }]}>
|
|
506
|
+
<View style={[{ marginBottom: 16 }]}>
|
|
507
|
+
<Text
|
|
508
|
+
style={[
|
|
509
|
+
{ color: atoms.colors.gray[300], fontSize: 13, marginBottom: 8 },
|
|
510
|
+
]}
|
|
511
|
+
>
|
|
512
|
+
Stream Title
|
|
513
|
+
</Text>
|
|
514
|
+
<Textarea
|
|
515
|
+
value={title}
|
|
516
|
+
onChangeText={(text) => {
|
|
517
|
+
setTitle(text);
|
|
518
|
+
setError(null);
|
|
519
|
+
}}
|
|
520
|
+
placeholder="Enter stream title..."
|
|
521
|
+
maxLength={140}
|
|
522
|
+
multiline
|
|
523
|
+
style={[
|
|
524
|
+
{
|
|
525
|
+
padding: 12,
|
|
526
|
+
borderRadius: 8,
|
|
527
|
+
backgroundColor: atoms.colors.neutral[800],
|
|
528
|
+
color: atoms.colors.white,
|
|
529
|
+
borderWidth: 1,
|
|
530
|
+
borderColor: atoms.colors.neutral[600],
|
|
531
|
+
minHeight: 100,
|
|
532
|
+
fontSize: 16,
|
|
533
|
+
},
|
|
534
|
+
]}
|
|
535
|
+
/>
|
|
536
|
+
<Text
|
|
537
|
+
style={[
|
|
538
|
+
{ color: atoms.colors.gray[400], fontSize: 12, marginTop: 4 },
|
|
539
|
+
]}
|
|
540
|
+
>
|
|
541
|
+
{title.length}/140 characters
|
|
542
|
+
</Text>
|
|
543
|
+
</View>
|
|
544
|
+
|
|
545
|
+
{error && (
|
|
546
|
+
<View
|
|
547
|
+
style={[
|
|
548
|
+
{
|
|
549
|
+
backgroundColor: atoms.colors.red[900],
|
|
550
|
+
padding: 12,
|
|
551
|
+
borderRadius: 8,
|
|
552
|
+
borderWidth: 1,
|
|
553
|
+
borderColor: atoms.colors.red[700],
|
|
554
|
+
marginBottom: 16,
|
|
555
|
+
},
|
|
556
|
+
]}
|
|
557
|
+
>
|
|
558
|
+
<Text style={[{ color: atoms.colors.red[400], fontSize: 13 }]}>
|
|
559
|
+
{error}
|
|
560
|
+
</Text>
|
|
561
|
+
</View>
|
|
562
|
+
)}
|
|
563
|
+
</View>
|
|
564
|
+
|
|
565
|
+
<DialogFooter>
|
|
566
|
+
<Button
|
|
567
|
+
width="min"
|
|
568
|
+
variant="secondary"
|
|
569
|
+
onPress={() => {
|
|
570
|
+
onClose();
|
|
571
|
+
setError(null);
|
|
572
|
+
setTitle(livestream?.record?.title || "");
|
|
573
|
+
}}
|
|
574
|
+
disabled={isLoading}
|
|
575
|
+
>
|
|
576
|
+
<Text>Cancel</Text>
|
|
577
|
+
</Button>
|
|
578
|
+
<Button
|
|
579
|
+
variant="primary"
|
|
580
|
+
width="min"
|
|
581
|
+
onPress={handleUpdate}
|
|
582
|
+
disabled={isLoading || !title.trim()}
|
|
583
|
+
>
|
|
584
|
+
<Text>{isLoading ? "Updating..." : "Update Title"}</Text>
|
|
585
|
+
</Button>
|
|
586
|
+
</DialogFooter>
|
|
587
|
+
</ResponsiveDialog>
|
|
588
|
+
);
|
|
589
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Text, View } from "react-native";
|
|
2
|
-
import emojiData from "../../assets/emoji-data.json";
|
|
3
2
|
import * as zero from "../../ui";
|
|
4
3
|
import { Chat } from "../chat/chat";
|
|
5
4
|
import { ChatBox } from "../chat/chat-box";
|
|
@@ -11,6 +10,7 @@ interface ChatPanelProps {
|
|
|
11
10
|
isConnected: boolean;
|
|
12
11
|
messagesPerMinute?: number;
|
|
13
12
|
shownMessages?: number;
|
|
13
|
+
emojiData?: any;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export default function ChatPanel({
|
|
@@ -18,6 +18,7 @@ export default function ChatPanel({
|
|
|
18
18
|
isConnected,
|
|
19
19
|
messagesPerMinute = 0,
|
|
20
20
|
shownMessages = 50,
|
|
21
|
+
emojiData = null,
|
|
21
22
|
}: ChatPanelProps) {
|
|
22
23
|
return (
|
|
23
24
|
<View
|
|
@@ -2,4 +2,5 @@ export { default as ChatPanel } from "./chat-panel";
|
|
|
2
2
|
export { default as Header } from "./header";
|
|
3
3
|
export { default as InformationWidget } from "./information-widget";
|
|
4
4
|
export { default as ModActions } from "./mod-actions";
|
|
5
|
+
export { default as ModeratorPanel } from "./moderator-panel";
|
|
5
6
|
export { default as Problems, ProblemsWrapperRef } from "./problems";
|