@streamplace/components 0.7.32 → 0.7.35
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 +4 -4
- package/dist/components/chat/mod-view.js +2 -1
- package/dist/components/dashboard/chat-panel.js +2 -2
- package/dist/components/dashboard/header.js +16 -2
- package/dist/components/dashboard/problems.js +29 -28
- package/dist/components/mobile-player/ui/viewer-context-menu.js +44 -1
- package/dist/components/ui/button.js +9 -9
- package/dist/components/ui/dropdown.js +15 -10
- package/dist/components/ui/icons.js +6 -0
- package/dist/components/ui/primitives/button.js +0 -7
- package/dist/lib/theme/tokens.js +9 -0
- package/dist/player-store/player-store.js +11 -1
- package/dist/streamplace-store/graph.js +195 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/package.json +2 -2
- package/src/components/chat/chat.tsx +90 -104
- package/src/components/chat/mod-view.tsx +3 -2
- package/src/components/dashboard/chat-panel.tsx +1 -3
- package/src/components/dashboard/header.tsx +37 -3
- package/src/components/dashboard/index.tsx +1 -1
- package/src/components/dashboard/problems.tsx +57 -46
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +100 -1
- package/src/components/ui/button.tsx +10 -13
- package/src/components/ui/dropdown.tsx +27 -13
- package/src/components/ui/icons.tsx +14 -0
- package/src/components/ui/primitives/button.tsx +0 -7
- package/src/lib/theme/tokens.ts +9 -0
- package/src/player-store/player-store.tsx +10 -0
- package/src/streamplace-store/graph.tsx +232 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useCreateFollowRecord = useCreateFollowRecord;
|
|
4
|
+
exports.useDeleteFollowRecord = useDeleteFollowRecord;
|
|
5
|
+
exports.useGraphManager = useGraphManager;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const streamplace_store_1 = require("./streamplace-store");
|
|
8
|
+
const xrpc_1 = require("./xrpc");
|
|
9
|
+
function useCreateFollowRecord() {
|
|
10
|
+
let agent = (0, xrpc_1.usePDSAgent)();
|
|
11
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
12
|
+
const createFollow = async (subjectDID) => {
|
|
13
|
+
if (!agent) {
|
|
14
|
+
throw new Error("No PDS agent found");
|
|
15
|
+
}
|
|
16
|
+
if (!agent.did) {
|
|
17
|
+
throw new Error("No user DID found, assuming not logged in");
|
|
18
|
+
}
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
try {
|
|
21
|
+
const record = {
|
|
22
|
+
$type: "app.bsky.graph.follow",
|
|
23
|
+
subject: subjectDID,
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
const result = await agent.com.atproto.repo.createRecord({
|
|
27
|
+
repo: agent.did,
|
|
28
|
+
collection: "app.bsky.graph.follow",
|
|
29
|
+
record,
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return { createFollow, isLoading };
|
|
38
|
+
}
|
|
39
|
+
function useDeleteFollowRecord() {
|
|
40
|
+
let agent = (0, xrpc_1.usePDSAgent)();
|
|
41
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
42
|
+
const deleteFollow = async (followRecordUri) => {
|
|
43
|
+
if (!agent) {
|
|
44
|
+
throw new Error("No PDS agent found");
|
|
45
|
+
}
|
|
46
|
+
if (!agent.did) {
|
|
47
|
+
throw new Error("No user DID found, assuming not logged in");
|
|
48
|
+
}
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
try {
|
|
51
|
+
const result = await agent.com.atproto.repo.deleteRecord({
|
|
52
|
+
repo: agent.did,
|
|
53
|
+
collection: "app.bsky.graph.follow",
|
|
54
|
+
rkey: followRecordUri.split("/").pop(),
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return { deleteFollow, isLoading };
|
|
63
|
+
}
|
|
64
|
+
function useGraphManager(subjectDID) {
|
|
65
|
+
const agent = (0, xrpc_1.usePDSAgent)();
|
|
66
|
+
const [isFollowing, setIsFollowing] = (0, react_1.useState)(null);
|
|
67
|
+
const [followUri, setFollowUri] = (0, react_1.useState)(null);
|
|
68
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
69
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
70
|
+
const userDID = agent?.did;
|
|
71
|
+
const streamplaceUrl = (0, streamplace_store_1.useStreamplaceStore)((state) => state.url);
|
|
72
|
+
const fetchFollowStatus = async () => {
|
|
73
|
+
if (!userDID || !subjectDID || !streamplaceUrl) {
|
|
74
|
+
setIsFollowing(null);
|
|
75
|
+
setFollowUri(null);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
setIsLoading(true);
|
|
79
|
+
setError(null);
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(`${streamplaceUrl}/xrpc/place.stream.graph.getFollowingUser?subjectDID=${encodeURIComponent(subjectDID)}&userDID=${encodeURIComponent(userDID)}`, {
|
|
82
|
+
credentials: "include",
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const errorText = await res.text();
|
|
86
|
+
throw new Error(`Failed to fetch follow status: ${errorText}`);
|
|
87
|
+
}
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
if (data.follow) {
|
|
90
|
+
setIsFollowing(true);
|
|
91
|
+
setFollowUri(data.follow.uri);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
setIsFollowing(false);
|
|
95
|
+
setFollowUri(null);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
setError(`Could not determine follow state: ${err instanceof Error ? err.message : `Unknown error: ${err}`}`);
|
|
100
|
+
setIsFollowing(null);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
setIsLoading(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
(0, react_1.useEffect)(() => {
|
|
107
|
+
if (!userDID || !subjectDID) {
|
|
108
|
+
setIsFollowing(null);
|
|
109
|
+
setFollowUri(null);
|
|
110
|
+
setError(null);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
fetchFollowStatus();
|
|
114
|
+
}, [userDID, subjectDID, streamplaceUrl]);
|
|
115
|
+
const follow = async () => {
|
|
116
|
+
if (!agent || !subjectDID) {
|
|
117
|
+
throw new Error("Cannot follow: not logged in or no subject DID");
|
|
118
|
+
}
|
|
119
|
+
if (!agent.did) {
|
|
120
|
+
throw new Error("No user DID found, assuming not logged in");
|
|
121
|
+
}
|
|
122
|
+
setIsLoading(true);
|
|
123
|
+
setError(null);
|
|
124
|
+
const previousState = isFollowing;
|
|
125
|
+
setIsFollowing(true); // Optimistic
|
|
126
|
+
try {
|
|
127
|
+
const record = {
|
|
128
|
+
$type: "app.bsky.graph.follow",
|
|
129
|
+
subject: subjectDID,
|
|
130
|
+
createdAt: new Date().toISOString(),
|
|
131
|
+
};
|
|
132
|
+
const result = await agent.com.atproto.repo.createRecord({
|
|
133
|
+
repo: agent.did,
|
|
134
|
+
collection: "app.bsky.graph.follow",
|
|
135
|
+
record,
|
|
136
|
+
});
|
|
137
|
+
setFollowUri(result.data.uri);
|
|
138
|
+
setIsFollowing(true);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
setIsFollowing(previousState);
|
|
142
|
+
const errorMsg = `Failed to follow: ${err instanceof Error ? err.message : "Unknown error"}`;
|
|
143
|
+
setError(errorMsg);
|
|
144
|
+
throw new Error(errorMsg);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
setIsLoading(false);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const unfollow = async () => {
|
|
151
|
+
if (!agent || !subjectDID) {
|
|
152
|
+
throw new Error("Cannot unfollow: not logged in or no subject DID");
|
|
153
|
+
}
|
|
154
|
+
if (!agent.did) {
|
|
155
|
+
throw new Error("No user DID found, assuming not logged in");
|
|
156
|
+
}
|
|
157
|
+
if (!followUri) {
|
|
158
|
+
throw new Error("Cannot unfollow: no follow URI found");
|
|
159
|
+
}
|
|
160
|
+
setIsLoading(true);
|
|
161
|
+
setError(null);
|
|
162
|
+
const previousState = isFollowing;
|
|
163
|
+
const previousUri = followUri;
|
|
164
|
+
setIsFollowing(false); // Optimistic
|
|
165
|
+
setFollowUri(null);
|
|
166
|
+
try {
|
|
167
|
+
await agent.com.atproto.repo.deleteRecord({
|
|
168
|
+
repo: agent.did,
|
|
169
|
+
collection: "app.bsky.graph.follow",
|
|
170
|
+
rkey: followUri.split("/").pop(),
|
|
171
|
+
});
|
|
172
|
+
setIsFollowing(false);
|
|
173
|
+
setFollowUri(null);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
setIsFollowing(previousState);
|
|
177
|
+
setFollowUri(previousUri);
|
|
178
|
+
const errorMsg = `Failed to unfollow: ${err instanceof Error ? err.message : "Unknown error"}`;
|
|
179
|
+
setError(errorMsg);
|
|
180
|
+
throw new Error(errorMsg);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
setIsLoading(false);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
isFollowing,
|
|
188
|
+
followUri,
|
|
189
|
+
isLoading,
|
|
190
|
+
error,
|
|
191
|
+
follow,
|
|
192
|
+
unfollow,
|
|
193
|
+
refresh: fetchFollowStatus,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamplace/components",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.35",
|
|
4
4
|
"description": "Streamplace React (Native) Components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "src/index.tsx",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"start": "tsc --watch --preserveWatchOutput",
|
|
55
55
|
"prepare": "tsc"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "eaefc85434e81e296c7248f6e6965a2f31d50ff8"
|
|
58
58
|
}
|
|
@@ -136,124 +136,112 @@ const ActionsBar = memo(
|
|
|
136
136
|
},
|
|
137
137
|
);
|
|
138
138
|
|
|
139
|
-
const ChatLine = memo(
|
|
140
|
-
(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
canModerate: boolean;
|
|
146
|
-
}) => {
|
|
147
|
-
const setReply = useSetReplyToMessage();
|
|
148
|
-
const setModMsg = usePlayerStore((state) => state.setModMessage);
|
|
149
|
-
const swipeableRef = useRef<SwipeableMethods | null>(null);
|
|
150
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
151
|
-
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
139
|
+
const ChatLine = memo(({ item }: { item: ChatMessageViewHydrated }) => {
|
|
140
|
+
const setReply = useSetReplyToMessage();
|
|
141
|
+
const setModMsg = usePlayerStore((state) => state.setModMessage);
|
|
142
|
+
const swipeableRef = useRef<SwipeableMethods | null>(null);
|
|
143
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
144
|
+
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
152
145
|
|
|
153
|
-
|
|
146
|
+
const handleHoverIn = () => {
|
|
147
|
+
if (hoverTimeoutRef.current) {
|
|
148
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
149
|
+
hoverTimeoutRef.current = null;
|
|
150
|
+
}
|
|
151
|
+
setIsHovered(true);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleHoverOut = () => {
|
|
155
|
+
hoverTimeoutRef.current = setTimeout(() => {
|
|
156
|
+
setIsHovered(false);
|
|
157
|
+
}, 50);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
return () => {
|
|
154
162
|
if (hoverTimeoutRef.current) {
|
|
155
163
|
clearTimeout(hoverTimeoutRef.current);
|
|
156
|
-
hoverTimeoutRef.current = null;
|
|
157
164
|
}
|
|
158
|
-
setIsHovered(true);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const handleHoverOut = () => {
|
|
162
|
-
hoverTimeoutRef.current = setTimeout(() => {
|
|
163
|
-
setIsHovered(false);
|
|
164
|
-
}, 50);
|
|
165
165
|
};
|
|
166
|
+
}, []);
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (item.author.did === "did:sys:system") {
|
|
176
|
-
return (
|
|
177
|
-
<SystemMessage
|
|
178
|
-
timestamp={new Date(item.record.createdAt)}
|
|
179
|
-
title={item.record.text}
|
|
180
|
-
/>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (Platform.OS === "web") {
|
|
185
|
-
return (
|
|
186
|
-
<View
|
|
187
|
-
style={[
|
|
188
|
-
py[1],
|
|
189
|
-
px[2],
|
|
190
|
-
{
|
|
191
|
-
position: "relative",
|
|
192
|
-
borderRadius: 8,
|
|
193
|
-
minWidth: 0,
|
|
194
|
-
maxWidth: "100%",
|
|
195
|
-
},
|
|
196
|
-
isHovered && bg.gray[950],
|
|
197
|
-
]}
|
|
198
|
-
onPointerEnter={handleHoverIn}
|
|
199
|
-
onPointerLeave={handleHoverOut}
|
|
200
|
-
>
|
|
201
|
-
<Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
|
|
202
|
-
<RenderChatMessage item={item} />
|
|
203
|
-
</Pressable>
|
|
204
|
-
<ActionsBar
|
|
205
|
-
item={item}
|
|
206
|
-
visible={isHovered}
|
|
207
|
-
hoverTimeoutRef={hoverTimeoutRef}
|
|
208
|
-
/>
|
|
209
|
-
</View>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
168
|
+
if (item.author.did === "did:sys:system") {
|
|
169
|
+
return (
|
|
170
|
+
<SystemMessage
|
|
171
|
+
timestamp={new Date(item.record.createdAt)}
|
|
172
|
+
title={item.record.text}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
212
176
|
|
|
177
|
+
if (Platform.OS === "web") {
|
|
213
178
|
return (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (r === (Platform.OS === "android" ? "left" : "right")) {
|
|
232
|
-
setModMsg(item);
|
|
233
|
-
}
|
|
234
|
-
// close this swipeable
|
|
235
|
-
const swipeable = swipeableRef.current;
|
|
236
|
-
if (swipeable) {
|
|
237
|
-
swipeable.close();
|
|
238
|
-
}
|
|
239
|
-
}}
|
|
240
|
-
>
|
|
179
|
+
<View
|
|
180
|
+
style={[
|
|
181
|
+
py[1],
|
|
182
|
+
px[2],
|
|
183
|
+
{
|
|
184
|
+
position: "relative",
|
|
185
|
+
borderRadius: 8,
|
|
186
|
+
minWidth: 0,
|
|
187
|
+
maxWidth: "100%",
|
|
188
|
+
},
|
|
189
|
+
isHovered && bg.gray[950],
|
|
190
|
+
]}
|
|
191
|
+
onPointerEnter={handleHoverIn}
|
|
192
|
+
onPointerLeave={handleHoverOut}
|
|
193
|
+
>
|
|
194
|
+
<Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
|
|
241
195
|
<RenderChatMessage item={item} />
|
|
242
|
-
</
|
|
243
|
-
|
|
196
|
+
</Pressable>
|
|
197
|
+
<ActionsBar
|
|
198
|
+
item={item}
|
|
199
|
+
visible={isHovered}
|
|
200
|
+
hoverTimeoutRef={hoverTimeoutRef}
|
|
201
|
+
/>
|
|
202
|
+
</View>
|
|
244
203
|
);
|
|
245
|
-
}
|
|
246
|
-
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<Swipeable
|
|
209
|
+
containerStyle={[py[1]]}
|
|
210
|
+
friction={2}
|
|
211
|
+
enableTrackpadTwoFingerGesture
|
|
212
|
+
rightThreshold={40}
|
|
213
|
+
leftThreshold={40}
|
|
214
|
+
renderRightActions={Platform.OS === "android" ? undefined : RightAction}
|
|
215
|
+
renderLeftActions={Platform.OS === "android" ? undefined : LeftAction}
|
|
216
|
+
overshootFriction={9}
|
|
217
|
+
ref={swipeableRef}
|
|
218
|
+
onSwipeableOpen={(r) => {
|
|
219
|
+
if (r === (Platform.OS === "android" ? "right" : "left")) {
|
|
220
|
+
setReply(item);
|
|
221
|
+
}
|
|
222
|
+
if (r === (Platform.OS === "android" ? "left" : "right")) {
|
|
223
|
+
setModMsg(item);
|
|
224
|
+
}
|
|
225
|
+
// close this swipeable
|
|
226
|
+
const swipeable = swipeableRef.current;
|
|
227
|
+
if (swipeable) {
|
|
228
|
+
swipeable.close();
|
|
229
|
+
}
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
<RenderChatMessage item={item} />
|
|
233
|
+
</Swipeable>
|
|
234
|
+
</>
|
|
235
|
+
);
|
|
236
|
+
});
|
|
247
237
|
|
|
248
238
|
export function Chat({
|
|
249
239
|
shownMessages = SHOWN_MSGS,
|
|
250
240
|
style: propsStyle,
|
|
251
|
-
canModerate = false,
|
|
252
241
|
...props
|
|
253
242
|
}: ComponentProps<typeof View> & {
|
|
254
243
|
shownMessages?: number;
|
|
255
244
|
style?: ComponentProps<typeof View>["style"];
|
|
256
|
-
canModerate?: boolean;
|
|
257
245
|
}) {
|
|
258
246
|
const chat = useChat();
|
|
259
247
|
const [isScrolledUp, setIsScrolledUp] = useState(false);
|
|
@@ -276,7 +264,7 @@ export function Chat({
|
|
|
276
264
|
if (!chat)
|
|
277
265
|
return (
|
|
278
266
|
<View style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }]}>
|
|
279
|
-
<Text>Loading
|
|
267
|
+
<Text>Loading chat...</Text>
|
|
280
268
|
</View>
|
|
281
269
|
);
|
|
282
270
|
|
|
@@ -295,9 +283,7 @@ export function Chat({
|
|
|
295
283
|
data={chat.slice(0, shownMessages)}
|
|
296
284
|
inverted={true}
|
|
297
285
|
keyExtractor={keyExtractor}
|
|
298
|
-
renderItem={({ item, index }) =>
|
|
299
|
-
<ChatLine item={item} canModerate={canModerate} />
|
|
300
|
-
)}
|
|
286
|
+
renderItem={({ item, index }) => <ChatLine item={item} />}
|
|
301
287
|
removeClippedSubviews={true}
|
|
302
288
|
maxToRenderPerBatch={10}
|
|
303
289
|
initialNumToRender={10}
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { usePlayerStore } from "../../player-store";
|
|
4
|
+
import { useIsMyStream, usePlayerStore } from "../../player-store";
|
|
5
5
|
import {
|
|
6
6
|
useCreateBlockRecord,
|
|
7
7
|
useCreateHideChatRecord,
|
|
@@ -51,6 +51,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
51
51
|
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
52
52
|
const setModMessage = usePlayerStore((x) => x.setModMessage);
|
|
53
53
|
const deleteChatMessage = useDeleteChatMessage();
|
|
54
|
+
const isMyStream = useIsMyStream();
|
|
54
55
|
|
|
55
56
|
// get the channel did
|
|
56
57
|
const channelId = usePlayerStore((state) => state.src);
|
|
@@ -119,7 +120,7 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
119
120
|
</DropdownMenuGroup>
|
|
120
121
|
|
|
121
122
|
{/* TODO: Checking for non-owner moderators */}
|
|
122
|
-
{
|
|
123
|
+
{isMyStream() && (
|
|
123
124
|
<DropdownMenuGroup title={`Moderation actions`}>
|
|
124
125
|
<DropdownMenuItem
|
|
125
126
|
disabled={isHideLoading || messageRemoved}
|
|
@@ -10,7 +10,6 @@ interface ChatPanelProps {
|
|
|
10
10
|
isLive: boolean;
|
|
11
11
|
isConnected: boolean;
|
|
12
12
|
messagesPerMinute?: number;
|
|
13
|
-
canModerate?: boolean;
|
|
14
13
|
shownMessages?: number;
|
|
15
14
|
}
|
|
16
15
|
|
|
@@ -18,7 +17,6 @@ export default function ChatPanel({
|
|
|
18
17
|
isLive,
|
|
19
18
|
isConnected,
|
|
20
19
|
messagesPerMinute = 0,
|
|
21
|
-
canModerate = false,
|
|
22
20
|
shownMessages = 50,
|
|
23
21
|
}: ChatPanelProps) {
|
|
24
22
|
return (
|
|
@@ -59,7 +57,7 @@ export default function ChatPanel({
|
|
|
59
57
|
</View>
|
|
60
58
|
<View style={[flex.values[1], px[2], { minHeight: 0 }]}>
|
|
61
59
|
<View style={[flex.values[1], { minHeight: 0 }]}>
|
|
62
|
-
<Chat
|
|
60
|
+
<Chat shownMessages={shownMessages} />
|
|
63
61
|
</View>
|
|
64
62
|
<View style={[{ flexShrink: 0 }]}>
|
|
65
63
|
<ChatBox
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Car, Radio, Users } from "lucide-react-native";
|
|
2
|
-
import { Text, View } from "react-native";
|
|
1
|
+
import { AlertCircle, Car, Radio, Users } from "lucide-react-native";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
3
|
import * as zero from "../../ui";
|
|
4
4
|
|
|
5
5
|
const { bg, r, borders, px, py, text, layout, gap } = zero;
|
|
@@ -103,6 +103,8 @@ interface HeaderProps {
|
|
|
103
103
|
bitrate?: string;
|
|
104
104
|
timeBetweenSegments?: number;
|
|
105
105
|
connectionStatus?: "excellent" | "good" | "poor" | "offline";
|
|
106
|
+
problemsCount?: number;
|
|
107
|
+
onProblemsPress?: () => void;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
export default function Header({
|
|
@@ -113,6 +115,8 @@ export default function Header({
|
|
|
113
115
|
bitrate = "0 mbps",
|
|
114
116
|
timeBetweenSegments = 0,
|
|
115
117
|
connectionStatus = "offline",
|
|
118
|
+
problemsCount = 0,
|
|
119
|
+
onProblemsPress,
|
|
116
120
|
}: HeaderProps) {
|
|
117
121
|
const getConnectionQuality = (): "good" | "warning" | "error" => {
|
|
118
122
|
if (timeBetweenSegments <= 1500) return "good";
|
|
@@ -139,7 +143,37 @@ export default function Header({
|
|
|
139
143
|
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
|
|
140
144
|
{streamTitle}
|
|
141
145
|
</Text>
|
|
142
|
-
<
|
|
146
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[3]]}>
|
|
147
|
+
<StatusIndicator status={connectionStatus} isLive={isLive} />
|
|
148
|
+
{problemsCount > 0 && (
|
|
149
|
+
<Pressable onPress={onProblemsPress}>
|
|
150
|
+
<View
|
|
151
|
+
style={[
|
|
152
|
+
layout.flex.row,
|
|
153
|
+
layout.flex.alignCenter,
|
|
154
|
+
gap.all[1],
|
|
155
|
+
px[2],
|
|
156
|
+
py[1],
|
|
157
|
+
r.md,
|
|
158
|
+
bg.orange[900],
|
|
159
|
+
borders.width.thin,
|
|
160
|
+
borders.color.orange[700],
|
|
161
|
+
{ marginVertical: -8 },
|
|
162
|
+
]}
|
|
163
|
+
>
|
|
164
|
+
<AlertCircle size={14} color="#fb923c" />
|
|
165
|
+
<Text
|
|
166
|
+
style={[
|
|
167
|
+
text.orange[400],
|
|
168
|
+
{ fontSize: 11, fontWeight: "600" },
|
|
169
|
+
]}
|
|
170
|
+
>
|
|
171
|
+
{problemsCount} {problemsCount === 1 ? "Issue" : "Issues"}
|
|
172
|
+
</Text>
|
|
173
|
+
</View>
|
|
174
|
+
</Pressable>
|
|
175
|
+
)}
|
|
176
|
+
</View>
|
|
143
177
|
</View>
|
|
144
178
|
</View>
|
|
145
179
|
|
|
@@ -2,4 +2,4 @@ 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 Problems } from "./problems";
|
|
5
|
+
export { default as Problems, ProblemsWrapperRef } from "./problems";
|