@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
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { Check, Shield, Trash2, UserPlus } from "lucide-react-native";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
Input,
|
|
9
|
+
ResponsiveDialog,
|
|
10
|
+
useToast,
|
|
11
|
+
} from "../../components/ui";
|
|
12
|
+
import { useAvatars } from "../../hooks/useAvatars";
|
|
13
|
+
import { atoms } from "../../lib/theme";
|
|
14
|
+
import {
|
|
15
|
+
useAddModerator,
|
|
16
|
+
useListModerators,
|
|
17
|
+
useRemoveModerator,
|
|
18
|
+
} from "../../streamplace-store/moderator-management";
|
|
19
|
+
import { formatHandleWithAt } from "../../utils/format-handle";
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
flex,
|
|
23
|
+
bg,
|
|
24
|
+
r,
|
|
25
|
+
borders,
|
|
26
|
+
p,
|
|
27
|
+
text: textStyle,
|
|
28
|
+
layout,
|
|
29
|
+
gap,
|
|
30
|
+
mb,
|
|
31
|
+
my,
|
|
32
|
+
px,
|
|
33
|
+
py,
|
|
34
|
+
} = atoms;
|
|
35
|
+
|
|
36
|
+
interface ModeratorPanelProps {
|
|
37
|
+
isLive?: boolean;
|
|
38
|
+
embedded?: boolean; // If true, removes outer container styling (for use inside other panels)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function ModeratorPanel({
|
|
42
|
+
isLive,
|
|
43
|
+
embedded = false,
|
|
44
|
+
}: ModeratorPanelProps) {
|
|
45
|
+
const { moderators, isLoading, error, refresh } = useListModerators();
|
|
46
|
+
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
47
|
+
|
|
48
|
+
// Collect all moderator DIDs for batch fetching profiles
|
|
49
|
+
const moderatorDIDs = useMemo(
|
|
50
|
+
() => moderators.map((mod) => mod.value.moderator),
|
|
51
|
+
[moderators],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const containerStyle = embedded
|
|
55
|
+
? [flex.values[1], layout.flex.column]
|
|
56
|
+
: [
|
|
57
|
+
flex.values[1],
|
|
58
|
+
bg.neutral[900],
|
|
59
|
+
r.lg,
|
|
60
|
+
borders.width.thin,
|
|
61
|
+
borders.color.neutral[700],
|
|
62
|
+
layout.flex.column,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View style={containerStyle}>
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<View
|
|
69
|
+
style={[
|
|
70
|
+
layout.flex.row,
|
|
71
|
+
layout.flex.spaceBetween,
|
|
72
|
+
layout.flex.alignCenter,
|
|
73
|
+
borders.bottom.width.thin,
|
|
74
|
+
borders.bottom.color.neutral[700],
|
|
75
|
+
p[4],
|
|
76
|
+
]}
|
|
77
|
+
>
|
|
78
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
|
|
79
|
+
<Shield size={18} color="#ffffff" />
|
|
80
|
+
<Text style={[textStyle.white, { fontSize: 18, fontWeight: "600" }]}>
|
|
81
|
+
Moderators
|
|
82
|
+
</Text>
|
|
83
|
+
</View>
|
|
84
|
+
<Pressable
|
|
85
|
+
onPress={() => setShowAddDialog(true)}
|
|
86
|
+
style={[
|
|
87
|
+
layout.flex.row,
|
|
88
|
+
layout.flex.alignCenter,
|
|
89
|
+
gap.all[2],
|
|
90
|
+
bg.blue[600],
|
|
91
|
+
px[3],
|
|
92
|
+
py[2],
|
|
93
|
+
r.md,
|
|
94
|
+
]}
|
|
95
|
+
>
|
|
96
|
+
<UserPlus size={16} color="#ffffff" />
|
|
97
|
+
<Text style={[textStyle.white, { fontSize: 13, fontWeight: "500" }]}>
|
|
98
|
+
Add
|
|
99
|
+
</Text>
|
|
100
|
+
</Pressable>
|
|
101
|
+
</View>
|
|
102
|
+
|
|
103
|
+
{/* Content */}
|
|
104
|
+
<ScrollView style={[flex.values[1], p[4]]}>
|
|
105
|
+
{isLoading && moderators.length === 0 && (
|
|
106
|
+
<Text
|
|
107
|
+
style={[textStyle.gray[400], { fontSize: 14, textAlign: "center" }]}
|
|
108
|
+
>
|
|
109
|
+
Loading moderators...
|
|
110
|
+
</Text>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{error && (
|
|
114
|
+
<View
|
|
115
|
+
style={[
|
|
116
|
+
bg.red[900],
|
|
117
|
+
p[3],
|
|
118
|
+
r.md,
|
|
119
|
+
borders.width.thin,
|
|
120
|
+
borders.color.red[700],
|
|
121
|
+
]}
|
|
122
|
+
>
|
|
123
|
+
<Text style={[textStyle.red[400], { fontSize: 13 }]}>{error}</Text>
|
|
124
|
+
</View>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{!isLoading && moderators.length === 0 && !error && (
|
|
128
|
+
<View style={[layout.flex.center, p[6]]}>
|
|
129
|
+
<Shield size={48} color="#6b7280" style={[mb[4]]} />
|
|
130
|
+
<Text
|
|
131
|
+
style={[
|
|
132
|
+
textStyle.gray[400],
|
|
133
|
+
{ fontSize: 14, textAlign: "center", marginBottom: 8 },
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
No moderators yet
|
|
137
|
+
</Text>
|
|
138
|
+
<Text
|
|
139
|
+
style={[
|
|
140
|
+
textStyle.gray[500],
|
|
141
|
+
{ fontSize: 12, textAlign: "center" },
|
|
142
|
+
]}
|
|
143
|
+
>
|
|
144
|
+
Add moderators to help manage your chat
|
|
145
|
+
</Text>
|
|
146
|
+
</View>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{moderators.map((mod, index) => (
|
|
150
|
+
<ModeratorCard
|
|
151
|
+
key={mod.rkey}
|
|
152
|
+
moderator={mod}
|
|
153
|
+
onRemove={refresh}
|
|
154
|
+
isLast={index === moderators.length - 1}
|
|
155
|
+
moderatorDIDs={moderatorDIDs}
|
|
156
|
+
/>
|
|
157
|
+
))}
|
|
158
|
+
</ScrollView>
|
|
159
|
+
|
|
160
|
+
{/* Add Moderator Dialog */}
|
|
161
|
+
<AddModeratorDialog
|
|
162
|
+
visible={showAddDialog}
|
|
163
|
+
onClose={() => setShowAddDialog(false)}
|
|
164
|
+
onSuccess={() => {
|
|
165
|
+
setShowAddDialog(false);
|
|
166
|
+
refresh();
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
</View>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface ModeratorCardProps {
|
|
174
|
+
moderator: {
|
|
175
|
+
uri: string;
|
|
176
|
+
cid: string;
|
|
177
|
+
value: {
|
|
178
|
+
moderator: string;
|
|
179
|
+
permissions: string[];
|
|
180
|
+
createdAt: string;
|
|
181
|
+
expirationTime?: string;
|
|
182
|
+
};
|
|
183
|
+
rkey: string;
|
|
184
|
+
};
|
|
185
|
+
onRemove: () => void;
|
|
186
|
+
isLast: boolean;
|
|
187
|
+
moderatorDIDs: string[]; // All moderator DIDs for batch fetching
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ModeratorCard({
|
|
191
|
+
moderator,
|
|
192
|
+
onRemove,
|
|
193
|
+
isLast,
|
|
194
|
+
moderatorDIDs,
|
|
195
|
+
}: ModeratorCardProps) {
|
|
196
|
+
const { removeModerator, isLoading } = useRemoveModerator();
|
|
197
|
+
const [showConfirm, setShowConfirm] = useState(false);
|
|
198
|
+
const toast = useToast();
|
|
199
|
+
|
|
200
|
+
// Use useAvatars hook to batch-fetch profiles for all moderators
|
|
201
|
+
const profiles = useAvatars(moderatorDIDs);
|
|
202
|
+
const profile = profiles[moderator.value.moderator];
|
|
203
|
+
|
|
204
|
+
// Format display name using existing utility
|
|
205
|
+
const displayName = useMemo(() => {
|
|
206
|
+
if (profile) {
|
|
207
|
+
return formatHandleWithAt(profile);
|
|
208
|
+
}
|
|
209
|
+
return moderator.value.moderator; // Fall back to DID
|
|
210
|
+
}, [profile, moderator.value.moderator]);
|
|
211
|
+
|
|
212
|
+
const handleRemove = async () => {
|
|
213
|
+
try {
|
|
214
|
+
await removeModerator(moderator.rkey);
|
|
215
|
+
setShowConfirm(false);
|
|
216
|
+
toast.show(
|
|
217
|
+
"Moderator removed",
|
|
218
|
+
"The moderator has been successfully removed.",
|
|
219
|
+
{ duration: 3 },
|
|
220
|
+
);
|
|
221
|
+
onRemove();
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error("Failed to remove moderator:", err);
|
|
224
|
+
toast.show(
|
|
225
|
+
"Error removing moderator",
|
|
226
|
+
err instanceof Error ? err.message : "Failed to remove moderator",
|
|
227
|
+
{ duration: 5 },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const isExpired =
|
|
233
|
+
moderator.value.expirationTime &&
|
|
234
|
+
new Date(moderator.value.expirationTime) < new Date();
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<View
|
|
238
|
+
style={[
|
|
239
|
+
layout.flex.row,
|
|
240
|
+
layout.flex.spaceBetween,
|
|
241
|
+
layout.flex.alignCenter,
|
|
242
|
+
p[3],
|
|
243
|
+
bg.neutral[800],
|
|
244
|
+
r.md,
|
|
245
|
+
!isLast && mb[2],
|
|
246
|
+
borders.width.thin,
|
|
247
|
+
borders.color.neutral[700],
|
|
248
|
+
]}
|
|
249
|
+
>
|
|
250
|
+
<View style={[flex.values[1]]}>
|
|
251
|
+
<Text
|
|
252
|
+
style={[
|
|
253
|
+
textStyle.white,
|
|
254
|
+
{ fontSize: 14, fontWeight: "500", marginBottom: 4 },
|
|
255
|
+
]}
|
|
256
|
+
numberOfLines={1}
|
|
257
|
+
>
|
|
258
|
+
{displayName}
|
|
259
|
+
</Text>
|
|
260
|
+
<View style={[layout.flex.row, { flexWrap: "wrap" }, gap.all[1]]}>
|
|
261
|
+
{moderator.value.permissions.map((perm) => (
|
|
262
|
+
<View
|
|
263
|
+
key={perm}
|
|
264
|
+
style={[
|
|
265
|
+
bg.blue[900],
|
|
266
|
+
px[2],
|
|
267
|
+
py[1],
|
|
268
|
+
r.sm,
|
|
269
|
+
borders.width.thin,
|
|
270
|
+
borders.color.blue[700],
|
|
271
|
+
]}
|
|
272
|
+
>
|
|
273
|
+
<Text
|
|
274
|
+
style={[
|
|
275
|
+
textStyle.blue[300],
|
|
276
|
+
{ fontSize: 11, fontWeight: "500" },
|
|
277
|
+
]}
|
|
278
|
+
>
|
|
279
|
+
{perm}
|
|
280
|
+
</Text>
|
|
281
|
+
</View>
|
|
282
|
+
))}
|
|
283
|
+
{isExpired && (
|
|
284
|
+
<View
|
|
285
|
+
style={[
|
|
286
|
+
bg.red[900],
|
|
287
|
+
px[2],
|
|
288
|
+
py[1],
|
|
289
|
+
r.sm,
|
|
290
|
+
borders.width.thin,
|
|
291
|
+
borders.color.red[700],
|
|
292
|
+
]}
|
|
293
|
+
>
|
|
294
|
+
<Text
|
|
295
|
+
style={[
|
|
296
|
+
textStyle.red[300],
|
|
297
|
+
{ fontSize: 11, fontWeight: "500" },
|
|
298
|
+
]}
|
|
299
|
+
>
|
|
300
|
+
Expired
|
|
301
|
+
</Text>
|
|
302
|
+
</View>
|
|
303
|
+
)}
|
|
304
|
+
</View>
|
|
305
|
+
{moderator.value.expirationTime && !isExpired && (
|
|
306
|
+
<Text style={[textStyle.gray[400], { fontSize: 11, marginTop: 4 }]}>
|
|
307
|
+
Expires:{" "}
|
|
308
|
+
{new Date(moderator.value.expirationTime).toLocaleDateString()}
|
|
309
|
+
</Text>
|
|
310
|
+
)}
|
|
311
|
+
</View>
|
|
312
|
+
<Pressable
|
|
313
|
+
onPress={() => setShowConfirm(true)}
|
|
314
|
+
disabled={isLoading}
|
|
315
|
+
style={[
|
|
316
|
+
p[2],
|
|
317
|
+
r.md,
|
|
318
|
+
bg.red[900],
|
|
319
|
+
borders.width.thin,
|
|
320
|
+
borders.color.red[700],
|
|
321
|
+
isLoading && { opacity: 0.5 },
|
|
322
|
+
]}
|
|
323
|
+
>
|
|
324
|
+
<Trash2 size={16} color="#f87171" />
|
|
325
|
+
</Pressable>
|
|
326
|
+
|
|
327
|
+
{/* Confirm Delete Dialog */}
|
|
328
|
+
<Dialog
|
|
329
|
+
open={showConfirm}
|
|
330
|
+
onOpenChange={setShowConfirm}
|
|
331
|
+
title="Remove Moderator?"
|
|
332
|
+
description="This will revoke all moderation permissions for this user."
|
|
333
|
+
dismissible={false}
|
|
334
|
+
>
|
|
335
|
+
<DialogFooter>
|
|
336
|
+
<Button
|
|
337
|
+
width="min"
|
|
338
|
+
variant="secondary"
|
|
339
|
+
onPress={() => setShowConfirm(false)}
|
|
340
|
+
disabled={isLoading}
|
|
341
|
+
>
|
|
342
|
+
<Text>Cancel</Text>
|
|
343
|
+
</Button>
|
|
344
|
+
<Button
|
|
345
|
+
width="min"
|
|
346
|
+
variant="destructive"
|
|
347
|
+
onPress={handleRemove}
|
|
348
|
+
disabled={isLoading}
|
|
349
|
+
>
|
|
350
|
+
<Text>{isLoading ? "Removing..." : "Remove"}</Text>
|
|
351
|
+
</Button>
|
|
352
|
+
</DialogFooter>
|
|
353
|
+
</Dialog>
|
|
354
|
+
</View>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface AddModeratorDialogProps {
|
|
359
|
+
visible: boolean;
|
|
360
|
+
onClose: () => void;
|
|
361
|
+
onSuccess: () => void;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function AddModeratorDialog({
|
|
365
|
+
visible,
|
|
366
|
+
onClose,
|
|
367
|
+
onSuccess,
|
|
368
|
+
}: AddModeratorDialogProps) {
|
|
369
|
+
const { addModerator, isLoading } = useAddModerator();
|
|
370
|
+
const [moderatorDID, setModeratorDID] = useState("");
|
|
371
|
+
const [permissions, setPermissions] = useState({
|
|
372
|
+
ban: false,
|
|
373
|
+
hide: false,
|
|
374
|
+
"livestream.manage": false,
|
|
375
|
+
});
|
|
376
|
+
const [error, setError] = useState<string | null>(null);
|
|
377
|
+
const toast = useToast();
|
|
378
|
+
|
|
379
|
+
// Reset form when dialog closes
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
if (!visible) {
|
|
382
|
+
setModeratorDID("");
|
|
383
|
+
setPermissions({ ban: false, hide: false, "livestream.manage": false });
|
|
384
|
+
setError(null);
|
|
385
|
+
}
|
|
386
|
+
}, [visible]);
|
|
387
|
+
|
|
388
|
+
// Clear error when user starts typing
|
|
389
|
+
const handleDIDChange = (text: string) => {
|
|
390
|
+
setModeratorDID(text);
|
|
391
|
+
if (error) setError(null);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const handleAdd = async () => {
|
|
395
|
+
setError(null);
|
|
396
|
+
|
|
397
|
+
if (!moderatorDID.trim()) {
|
|
398
|
+
setError("Please enter a DID or handle");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const selectedPermissions = Object.entries(permissions)
|
|
403
|
+
.filter(([_, enabled]) => enabled)
|
|
404
|
+
.map(([perm]) => perm) as ("ban" | "hide" | "livestream.manage")[];
|
|
405
|
+
|
|
406
|
+
if (selectedPermissions.length === 0) {
|
|
407
|
+
setError("Please select at least one permission");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await addModerator({
|
|
413
|
+
moderatorDID: moderatorDID.trim(),
|
|
414
|
+
permissions: selectedPermissions,
|
|
415
|
+
});
|
|
416
|
+
toast.show(
|
|
417
|
+
"Moderator added",
|
|
418
|
+
"The new moderator has been added successfully.",
|
|
419
|
+
{ duration: 3 },
|
|
420
|
+
);
|
|
421
|
+
onSuccess();
|
|
422
|
+
} catch (err) {
|
|
423
|
+
setError(err instanceof Error ? err.message : "Failed to add moderator");
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<ResponsiveDialog
|
|
429
|
+
open={visible}
|
|
430
|
+
onOpenChange={(open) => {
|
|
431
|
+
if (!open) onClose();
|
|
432
|
+
}}
|
|
433
|
+
title="Add Moderator"
|
|
434
|
+
description="Enter the DID or handle of the user you want to add as a moderator and select their permissions."
|
|
435
|
+
size="md"
|
|
436
|
+
dismissible={false}
|
|
437
|
+
>
|
|
438
|
+
{/* DID Input */}
|
|
439
|
+
<View style={[my[4]]}>
|
|
440
|
+
<Text style={[textStyle.gray[300], { fontSize: 13, marginBottom: 8 }]}>
|
|
441
|
+
Moderator DID or Handle
|
|
442
|
+
</Text>
|
|
443
|
+
<Input
|
|
444
|
+
value={moderatorDID}
|
|
445
|
+
onChangeText={handleDIDChange}
|
|
446
|
+
placeholder="did:plc:... or @handle.bsky.social"
|
|
447
|
+
onSubmitEditing={handleAdd}
|
|
448
|
+
returnKeyType="done"
|
|
449
|
+
/>
|
|
450
|
+
</View>
|
|
451
|
+
|
|
452
|
+
{/* Permissions */}
|
|
453
|
+
<View style={[mb[4]]}>
|
|
454
|
+
<Text style={[textStyle.gray[300], { fontSize: 13, marginBottom: 8 }]}>
|
|
455
|
+
Permissions
|
|
456
|
+
</Text>
|
|
457
|
+
<View style={[gap.all[2]]}>
|
|
458
|
+
<Pressable
|
|
459
|
+
onPress={() => setPermissions((p) => ({ ...p, ban: !p.ban }))}
|
|
460
|
+
style={[
|
|
461
|
+
layout.flex.row,
|
|
462
|
+
layout.flex.alignCenter,
|
|
463
|
+
p[3],
|
|
464
|
+
r.md,
|
|
465
|
+
bg.neutral[800],
|
|
466
|
+
borders.width.thin,
|
|
467
|
+
borders.color.neutral[700],
|
|
468
|
+
]}
|
|
469
|
+
>
|
|
470
|
+
<View
|
|
471
|
+
style={[
|
|
472
|
+
{
|
|
473
|
+
width: 20,
|
|
474
|
+
height: 20,
|
|
475
|
+
borderRadius: 4,
|
|
476
|
+
},
|
|
477
|
+
borders.width.thin,
|
|
478
|
+
borders.color.neutral[600],
|
|
479
|
+
permissions.ban ? bg.blue[600] : bg.neutral[900],
|
|
480
|
+
layout.flex.center,
|
|
481
|
+
{ marginRight: 12 },
|
|
482
|
+
]}
|
|
483
|
+
>
|
|
484
|
+
{permissions.ban && <Check size={12} color="white" />}
|
|
485
|
+
</View>
|
|
486
|
+
<View>
|
|
487
|
+
<Text
|
|
488
|
+
style={[textStyle.white, { fontSize: 14, fontWeight: "500" }]}
|
|
489
|
+
>
|
|
490
|
+
Ban Users
|
|
491
|
+
</Text>
|
|
492
|
+
<Text style={[textStyle.gray[400], { fontSize: 12 }]}>
|
|
493
|
+
Block users from chat
|
|
494
|
+
</Text>
|
|
495
|
+
</View>
|
|
496
|
+
</Pressable>
|
|
497
|
+
|
|
498
|
+
<Pressable
|
|
499
|
+
onPress={() => setPermissions((p) => ({ ...p, hide: !p.hide }))}
|
|
500
|
+
style={[
|
|
501
|
+
layout.flex.row,
|
|
502
|
+
layout.flex.alignCenter,
|
|
503
|
+
p[3],
|
|
504
|
+
r.md,
|
|
505
|
+
bg.neutral[800],
|
|
506
|
+
borders.width.thin,
|
|
507
|
+
borders.color.neutral[700],
|
|
508
|
+
]}
|
|
509
|
+
>
|
|
510
|
+
<View
|
|
511
|
+
style={[
|
|
512
|
+
{
|
|
513
|
+
width: 20,
|
|
514
|
+
height: 20,
|
|
515
|
+
borderRadius: 4,
|
|
516
|
+
},
|
|
517
|
+
borders.width.thin,
|
|
518
|
+
borders.color.neutral[600],
|
|
519
|
+
permissions.hide ? bg.blue[600] : bg.neutral[900],
|
|
520
|
+
layout.flex.center,
|
|
521
|
+
{ marginRight: 12 },
|
|
522
|
+
]}
|
|
523
|
+
>
|
|
524
|
+
{permissions.hide && (
|
|
525
|
+
<Text style={[textStyle.white, { fontSize: 12 }]}>✓</Text>
|
|
526
|
+
)}
|
|
527
|
+
</View>
|
|
528
|
+
<View>
|
|
529
|
+
<Text
|
|
530
|
+
style={[textStyle.white, { fontSize: 14, fontWeight: "500" }]}
|
|
531
|
+
>
|
|
532
|
+
Hide Messages
|
|
533
|
+
</Text>
|
|
534
|
+
<Text style={[textStyle.gray[400], { fontSize: 12 }]}>
|
|
535
|
+
Hide individual chat messages
|
|
536
|
+
</Text>
|
|
537
|
+
</View>
|
|
538
|
+
</Pressable>
|
|
539
|
+
|
|
540
|
+
<Pressable
|
|
541
|
+
onPress={() =>
|
|
542
|
+
setPermissions((p) => ({
|
|
543
|
+
...p,
|
|
544
|
+
"livestream.manage": !p["livestream.manage"],
|
|
545
|
+
}))
|
|
546
|
+
}
|
|
547
|
+
style={[
|
|
548
|
+
layout.flex.row,
|
|
549
|
+
layout.flex.alignCenter,
|
|
550
|
+
p[3],
|
|
551
|
+
r.md,
|
|
552
|
+
bg.neutral[800],
|
|
553
|
+
borders.width.thin,
|
|
554
|
+
borders.color.neutral[700],
|
|
555
|
+
]}
|
|
556
|
+
>
|
|
557
|
+
<View
|
|
558
|
+
style={[
|
|
559
|
+
{
|
|
560
|
+
width: 20,
|
|
561
|
+
height: 20,
|
|
562
|
+
borderRadius: 4,
|
|
563
|
+
},
|
|
564
|
+
borders.width.thin,
|
|
565
|
+
borders.color.neutral[600],
|
|
566
|
+
permissions["livestream.manage"]
|
|
567
|
+
? bg.blue[600]
|
|
568
|
+
: bg.neutral[900],
|
|
569
|
+
layout.flex.center,
|
|
570
|
+
{ marginRight: 12 },
|
|
571
|
+
]}
|
|
572
|
+
>
|
|
573
|
+
{permissions["livestream.manage"] && (
|
|
574
|
+
<Text style={[textStyle.white, { fontSize: 12 }]}>✓</Text>
|
|
575
|
+
)}
|
|
576
|
+
</View>
|
|
577
|
+
<View>
|
|
578
|
+
<Text
|
|
579
|
+
style={[textStyle.white, { fontSize: 14, fontWeight: "500" }]}
|
|
580
|
+
>
|
|
581
|
+
Manage Livestream
|
|
582
|
+
</Text>
|
|
583
|
+
<Text style={[textStyle.gray[400], { fontSize: 12 }]}>
|
|
584
|
+
Update stream title
|
|
585
|
+
</Text>
|
|
586
|
+
</View>
|
|
587
|
+
</Pressable>
|
|
588
|
+
</View>
|
|
589
|
+
</View>
|
|
590
|
+
|
|
591
|
+
{/* Error */}
|
|
592
|
+
{error && (
|
|
593
|
+
<View
|
|
594
|
+
style={[
|
|
595
|
+
bg.red[900],
|
|
596
|
+
p[3],
|
|
597
|
+
r.md,
|
|
598
|
+
borders.width.thin,
|
|
599
|
+
borders.color.red[700],
|
|
600
|
+
mb[4],
|
|
601
|
+
]}
|
|
602
|
+
>
|
|
603
|
+
<Text style={[textStyle.red[400], { fontSize: 13 }]}>{error}</Text>
|
|
604
|
+
</View>
|
|
605
|
+
)}
|
|
606
|
+
|
|
607
|
+
{/* Actions */}
|
|
608
|
+
<DialogFooter>
|
|
609
|
+
<Button
|
|
610
|
+
width="min"
|
|
611
|
+
variant="secondary"
|
|
612
|
+
onPress={onClose}
|
|
613
|
+
disabled={isLoading}
|
|
614
|
+
>
|
|
615
|
+
Cancel
|
|
616
|
+
</Button>
|
|
617
|
+
<Button
|
|
618
|
+
width="min"
|
|
619
|
+
variant="primary"
|
|
620
|
+
onPress={handleAdd}
|
|
621
|
+
disabled={
|
|
622
|
+
isLoading ||
|
|
623
|
+
!moderatorDID.trim() ||
|
|
624
|
+
Object.values(permissions).every((p) => !p)
|
|
625
|
+
}
|
|
626
|
+
>
|
|
627
|
+
{isLoading ? "Adding..." : "Add Moderator"}
|
|
628
|
+
</Button>
|
|
629
|
+
</DialogFooter>
|
|
630
|
+
</ResponsiveDialog>
|
|
631
|
+
);
|
|
632
|
+
}
|
|
@@ -224,7 +224,7 @@ export const MenuLabel = forwardRef<View, MenuLabelProps>(
|
|
|
224
224
|
ref={ref as any}
|
|
225
225
|
style={mergeStyles(
|
|
226
226
|
px[4],
|
|
227
|
-
|
|
227
|
+
pt[2],
|
|
228
228
|
{ color: theme.colors.textMuted },
|
|
229
229
|
a.fontSize.base,
|
|
230
230
|
style,
|
|
@@ -274,7 +274,6 @@ export const MenuInfo = forwardRef<View, MenuInfoProps>(
|
|
|
274
274
|
style={mergeStyles(
|
|
275
275
|
{ color: theme.colors.textMuted, marginTop: -8 },
|
|
276
276
|
pt[1],
|
|
277
|
-
pl[4],
|
|
278
277
|
pb[2],
|
|
279
278
|
fontSize.sm,
|
|
280
279
|
style,
|
|
@@ -13,7 +13,14 @@ if (!rnqc && !expoCrypto) {
|
|
|
13
13
|
throw new Error(
|
|
14
14
|
"Livestreaming requires one of react-native-quick-crypto or expo-crypto",
|
|
15
15
|
);
|
|
16
|
-
}
|
|
16
|
+
}
|
|
17
|
+
if (rnqc) {
|
|
18
|
+
console.log("Using react-native-quick-crypto for crypto polyfill");
|
|
19
|
+
// we import this in the main app, but in case this file is used standalone:
|
|
20
|
+
globalThis.crypto = rnqc as any as Crypto;
|
|
21
|
+
}
|
|
22
|
+
if (expoCrypto) {
|
|
23
|
+
console.log("Using expo-crypto for crypto polyfill");
|
|
17
24
|
// @atproto/crypto dependencies expect crypto.getRandomValues to be a function
|
|
18
25
|
if (typeof globalThis.crypto === "undefined") {
|
|
19
26
|
globalThis.crypto = {} as any;
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
useFavicon,
|
|
5
|
+
useSiteDescription,
|
|
6
|
+
useSiteTitle,
|
|
7
|
+
} from "../streamplace-store";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook to set the document title, description, and favicon on web based on branding.
|
|
11
|
+
* No-op on native platforms.
|
|
12
|
+
*/
|
|
13
|
+
export function useDocumentTitle() {
|
|
14
|
+
const siteTitle = useSiteTitle();
|
|
15
|
+
const siteDescription = useSiteDescription();
|
|
16
|
+
const favicon = useFavicon();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (Platform.OS === "web" && typeof document !== "undefined") {
|
|
20
|
+
// set title
|
|
21
|
+
document.title = siteTitle;
|
|
22
|
+
|
|
23
|
+
// set or update meta description
|
|
24
|
+
let metaDescription = document.querySelector('meta[name="description"]');
|
|
25
|
+
if (!metaDescription) {
|
|
26
|
+
metaDescription = document.createElement("meta");
|
|
27
|
+
metaDescription.setAttribute("name", "description");
|
|
28
|
+
document.head.appendChild(metaDescription);
|
|
29
|
+
}
|
|
30
|
+
metaDescription.setAttribute("content", siteDescription);
|
|
31
|
+
|
|
32
|
+
// set or update favicon
|
|
33
|
+
if (favicon) {
|
|
34
|
+
let link: HTMLLinkElement | null =
|
|
35
|
+
document.querySelector('link[rel="icon"]');
|
|
36
|
+
if (!link) {
|
|
37
|
+
link = document.createElement("link");
|
|
38
|
+
link.rel = "icon";
|
|
39
|
+
document.head.appendChild(link);
|
|
40
|
+
}
|
|
41
|
+
link.href = favicon;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, [siteTitle, siteDescription, favicon]);
|
|
45
|
+
}
|