@streamplace/components 0.7.9 → 0.7.13
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/assets/emoji-data.json +19371 -0
- package/dist/components/chat/chat-box.js +19 -2
- package/dist/components/chat/chat-message.js +12 -4
- package/dist/components/chat/chat.js +15 -4
- package/dist/components/chat/mod-view.js +15 -8
- package/dist/components/dashboard/chat-panel.js +38 -0
- package/dist/components/dashboard/header.js +80 -0
- package/dist/components/dashboard/index.js +14 -0
- package/dist/components/dashboard/information-widget.js +234 -0
- package/dist/components/dashboard/mod-actions.js +71 -0
- package/dist/components/dashboard/problems.js +74 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +15 -6
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/dropdown.js +20 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/info-box.js +31 -0
- package/dist/components/ui/info-row.js +23 -0
- package/dist/components/ui/toast.js +43 -0
- package/dist/index.js +3 -1
- package/dist/lib/theme/atoms.js +66 -45
- package/dist/lib/theme/tokens.js +285 -12
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +2 -2
- package/src/assets/emoji-data.json +19371 -0
- package/src/components/chat/chat-box.tsx +19 -1
- package/src/components/chat/chat-message.tsx +22 -14
- package/src/components/chat/chat.tsx +21 -6
- package/src/components/chat/mod-view.tsx +24 -6
- package/src/components/dashboard/chat-panel.tsx +80 -0
- package/src/components/dashboard/header.tsx +170 -0
- package/src/components/dashboard/index.tsx +5 -0
- package/src/components/dashboard/information-widget.tsx +526 -0
- package/src/components/dashboard/mod-actions.tsx +133 -0
- package/src/components/dashboard/problems.tsx +151 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +67 -38
- package/src/components/ui/button.tsx +2 -2
- package/src/components/ui/dropdown.tsx +38 -3
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/info-box.tsx +60 -0
- package/src/components/ui/info-row.tsx +48 -0
- package/src/components/ui/toast.tsx +110 -0
- package/src/index.tsx +3 -0
- package/src/lib/theme/atoms.ts +97 -43
- package/src/lib/theme/tokens.ts +285 -12
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -246,6 +246,15 @@ export function ChatBox({
|
|
|
246
246
|
reply: replyTo || undefined,
|
|
247
247
|
});
|
|
248
248
|
setSubmitting(false);
|
|
249
|
+
|
|
250
|
+
// if we press "send" button, we want the same action as pressing "Enter"
|
|
251
|
+
// if we're already focused no need to do extra work
|
|
252
|
+
if (textAreaRef.current && !textAreaRef.current.isFocused()) {
|
|
253
|
+
textAreaRef.current.focus();
|
|
254
|
+
requestAnimationFrame(() => {
|
|
255
|
+
textAreaRef.current?.focus();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
249
258
|
};
|
|
250
259
|
useEffect(() => {
|
|
251
260
|
if (replyTo && textAreaRef.current) {
|
|
@@ -327,7 +336,10 @@ export function ChatBox({
|
|
|
327
336
|
numberOfLines={1}
|
|
328
337
|
value={message}
|
|
329
338
|
enterKeyHint="send"
|
|
330
|
-
onSubmitEditing={
|
|
339
|
+
onSubmitEditing={(e) => {
|
|
340
|
+
e.preventDefault();
|
|
341
|
+
submit();
|
|
342
|
+
}}
|
|
331
343
|
multiline={false}
|
|
332
344
|
onChangeText={(text) => {
|
|
333
345
|
setMessage(text);
|
|
@@ -346,6 +358,9 @@ export function ChatBox({
|
|
|
346
358
|
if (filteredEmojis.length > 0) {
|
|
347
359
|
handleEmojiSelect(filteredEmojis[highlightedIndex]);
|
|
348
360
|
}
|
|
361
|
+
} else {
|
|
362
|
+
k.preventDefault();
|
|
363
|
+
submit();
|
|
349
364
|
}
|
|
350
365
|
} else if (k.nativeEvent.key === "ArrowUp") {
|
|
351
366
|
if (showSuggestions || showEmojiSuggestions) {
|
|
@@ -376,6 +391,9 @@ export function ChatBox({
|
|
|
376
391
|
}
|
|
377
392
|
}}
|
|
378
393
|
style={[chatBoxStyle]}
|
|
394
|
+
// "submit" won't blur on enter
|
|
395
|
+
submitBehavior="submit"
|
|
396
|
+
placeholder="Type a message..."
|
|
379
397
|
/>
|
|
380
398
|
<Button
|
|
381
399
|
disabled={submitting}
|
|
@@ -7,16 +7,7 @@ import { memo, useCallback } from "react";
|
|
|
7
7
|
import { Linking, View } from "react-native";
|
|
8
8
|
import { ChatMessageViewHydrated } from "streamplace";
|
|
9
9
|
import { RichtextSegment, segmentize } from "../../lib/facet";
|
|
10
|
-
import {
|
|
11
|
-
borders,
|
|
12
|
-
flex,
|
|
13
|
-
gap,
|
|
14
|
-
ml,
|
|
15
|
-
mr,
|
|
16
|
-
opacity,
|
|
17
|
-
pl,
|
|
18
|
-
w,
|
|
19
|
-
} from "../../lib/theme/atoms";
|
|
10
|
+
import { borders, flex, gap, ml, mr, opacity, pl } from "../../lib/theme/atoms";
|
|
20
11
|
import { atoms, colors, layout } from "../ui";
|
|
21
12
|
|
|
22
13
|
interface Facet {
|
|
@@ -121,7 +112,7 @@ export const RenderChatMessage = memo(
|
|
|
121
112
|
style={[
|
|
122
113
|
gap.all[2],
|
|
123
114
|
layout.flex.row,
|
|
124
|
-
|
|
115
|
+
{ minWidth: 0, maxWidth: "100%" },
|
|
125
116
|
borders.left.width.medium,
|
|
126
117
|
borders.left.color.gray[700],
|
|
127
118
|
ml[4],
|
|
@@ -129,7 +120,14 @@ export const RenderChatMessage = memo(
|
|
|
129
120
|
opacity[80],
|
|
130
121
|
]}
|
|
131
122
|
>
|
|
132
|
-
<Text
|
|
123
|
+
<Text
|
|
124
|
+
numberOfLines={1}
|
|
125
|
+
style={[
|
|
126
|
+
flex.shrink[1],
|
|
127
|
+
mr[4],
|
|
128
|
+
{ minWidth: 0, overflow: "hidden" },
|
|
129
|
+
]}
|
|
130
|
+
>
|
|
133
131
|
<Text
|
|
134
132
|
style={{
|
|
135
133
|
color: getRgbColor((item.replyTo.chatProfile as any).color),
|
|
@@ -149,7 +147,13 @@ export const RenderChatMessage = memo(
|
|
|
149
147
|
</Text>
|
|
150
148
|
</View>
|
|
151
149
|
)}
|
|
152
|
-
<View
|
|
150
|
+
<View
|
|
151
|
+
style={[
|
|
152
|
+
gap.all[2],
|
|
153
|
+
layout.flex.row,
|
|
154
|
+
{ minWidth: 0, maxWidth: "100%" },
|
|
155
|
+
]}
|
|
156
|
+
>
|
|
153
157
|
{showTime && (
|
|
154
158
|
<Text
|
|
155
159
|
style={{
|
|
@@ -160,7 +164,11 @@ export const RenderChatMessage = memo(
|
|
|
160
164
|
{formatTime(item.record.createdAt)}
|
|
161
165
|
</Text>
|
|
162
166
|
)}
|
|
163
|
-
<Text
|
|
167
|
+
<Text
|
|
168
|
+
weight="bold"
|
|
169
|
+
color="default"
|
|
170
|
+
style={[flex.shrink[1], { minWidth: 0, overflow: "hidden" }]}
|
|
171
|
+
>
|
|
164
172
|
<Text
|
|
165
173
|
style={[
|
|
166
174
|
{
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
useSetReplyToMessage,
|
|
18
18
|
View,
|
|
19
19
|
} from "../../";
|
|
20
|
-
import { bg, flex, px, py
|
|
20
|
+
import { bg, flex, px, py } from "../../lib/theme/atoms";
|
|
21
21
|
import { RenderChatMessage } from "./chat-message";
|
|
22
22
|
import { ModView } from "./mod-view";
|
|
23
23
|
|
|
@@ -87,6 +87,8 @@ const ActionsBar = memo(
|
|
|
87
87
|
padding: 1,
|
|
88
88
|
gap: 4,
|
|
89
89
|
zIndex: 10,
|
|
90
|
+
maxWidth: 120,
|
|
91
|
+
flexShrink: 0,
|
|
90
92
|
},
|
|
91
93
|
]}
|
|
92
94
|
>
|
|
@@ -184,13 +186,18 @@ const ChatLine = memo(
|
|
|
184
186
|
style={[
|
|
185
187
|
py[1],
|
|
186
188
|
px[2],
|
|
187
|
-
{
|
|
189
|
+
{
|
|
190
|
+
position: "relative",
|
|
191
|
+
borderRadius: 8,
|
|
192
|
+
minWidth: 0,
|
|
193
|
+
maxWidth: "100%",
|
|
194
|
+
},
|
|
188
195
|
isHovered && bg.gray[950],
|
|
189
196
|
]}
|
|
190
197
|
onPointerEnter={handleHoverIn}
|
|
191
198
|
onPointerLeave={handleHoverOut}
|
|
192
199
|
>
|
|
193
|
-
<Pressable>
|
|
200
|
+
<Pressable style={[{ minWidth: 0, maxWidth: "100%" }]}>
|
|
194
201
|
<RenderChatMessage item={item} />
|
|
195
202
|
</Pressable>
|
|
196
203
|
<ActionsBar
|
|
@@ -253,15 +260,23 @@ export function Chat({
|
|
|
253
260
|
|
|
254
261
|
if (!chat)
|
|
255
262
|
return (
|
|
256
|
-
<View style={[flex.shrink[1]]}>
|
|
263
|
+
<View style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }]}>
|
|
257
264
|
<Text>Loading chaat...</Text>
|
|
258
265
|
</View>
|
|
259
266
|
);
|
|
260
267
|
|
|
261
268
|
return (
|
|
262
|
-
<View
|
|
269
|
+
<View
|
|
270
|
+
style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }].concat(
|
|
271
|
+
propsStyle || [],
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
263
274
|
<FlatList
|
|
264
|
-
style={[
|
|
275
|
+
style={[
|
|
276
|
+
flex.grow[1],
|
|
277
|
+
flex.shrink[1],
|
|
278
|
+
{ minWidth: 0, maxWidth: "100%" },
|
|
279
|
+
]}
|
|
265
280
|
data={chat.slice(0, shownMessages)}
|
|
266
281
|
inverted={true}
|
|
267
282
|
keyExtractor={keyExtractor}
|
|
@@ -45,6 +45,10 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
45
45
|
let { createBlock, isLoading: isBlockLoading } = useCreateBlockRecord();
|
|
46
46
|
let { createHideChat, isLoading: isHideLoading } = useCreateHideChatRecord();
|
|
47
47
|
|
|
48
|
+
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
49
|
+
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
50
|
+
const setModMessage = usePlayerStore((x) => x.setModMessage);
|
|
51
|
+
|
|
48
52
|
// get the channel did
|
|
49
53
|
const channelId = usePlayerStore((state) => state.src);
|
|
50
54
|
// get the logged in user's identity
|
|
@@ -56,13 +60,15 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
56
60
|
</View>;
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
const cleanup = () => {
|
|
64
|
+
setModMessage(null);
|
|
65
|
+
};
|
|
66
|
+
|
|
59
67
|
useEffect(() => {
|
|
60
68
|
if (message) {
|
|
61
|
-
console.log("opening mod view");
|
|
62
69
|
setMessageRemoved(false);
|
|
63
70
|
triggerRef.current?.open();
|
|
64
71
|
} else {
|
|
65
|
-
console.log("closing mod view");
|
|
66
72
|
triggerRef.current?.close();
|
|
67
73
|
}
|
|
68
74
|
}, [message]);
|
|
@@ -70,6 +76,11 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
70
76
|
return (
|
|
71
77
|
<DropdownMenu
|
|
72
78
|
style={[layout.flex.row, layout.flex.alignCenter, gap.all[2], w[80]]}
|
|
79
|
+
onOpenChange={(isOpen) => {
|
|
80
|
+
if (!isOpen) {
|
|
81
|
+
cleanup();
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
73
84
|
>
|
|
74
85
|
<DropdownMenuTrigger ref={triggerRef}>
|
|
75
86
|
{/* Hidden trigger */}
|
|
@@ -155,13 +166,17 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
155
166
|
<DropdownMenuItem
|
|
156
167
|
onPress={() => {
|
|
157
168
|
Linking.openURL(
|
|
158
|
-
`https://${BSKY_FRONTEND_DOMAIN}/profile/${
|
|
169
|
+
`https://${BSKY_FRONTEND_DOMAIN}/profile/${message.author.handle}`,
|
|
159
170
|
);
|
|
160
171
|
}}
|
|
161
172
|
>
|
|
162
173
|
<Text color="primary">View user on {BSKY_FRONTEND_DOMAIN}</Text>
|
|
163
174
|
</DropdownMenuItem>
|
|
164
|
-
<ReportButton
|
|
175
|
+
<ReportButton
|
|
176
|
+
message={message}
|
|
177
|
+
setReportModalOpen={setReportModalOpen}
|
|
178
|
+
setReportSubject={setReportSubject}
|
|
179
|
+
/>
|
|
165
180
|
</DropdownMenuGroup>
|
|
166
181
|
</>
|
|
167
182
|
)}
|
|
@@ -172,11 +187,13 @@ export const ModView = forwardRef<ModViewRef, ModViewProps>(() => {
|
|
|
172
187
|
|
|
173
188
|
export function ReportButton({
|
|
174
189
|
message,
|
|
190
|
+
setReportModalOpen,
|
|
191
|
+
setReportSubject,
|
|
175
192
|
}: {
|
|
176
193
|
message: ChatMessageViewHydrated;
|
|
194
|
+
setReportModalOpen: (open: boolean) => void;
|
|
195
|
+
setReportSubject: (subject: any) => void;
|
|
177
196
|
}) {
|
|
178
|
-
const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen);
|
|
179
|
-
const setReportSubject = usePlayerStore((x) => x.setReportSubject);
|
|
180
197
|
const { onOpenChange } = useRootContext();
|
|
181
198
|
return (
|
|
182
199
|
<DropdownMenuItem
|
|
@@ -188,6 +205,7 @@ export function ReportButton({
|
|
|
188
205
|
$type: "com.atproto.repo.strongRef",
|
|
189
206
|
uri: message.uri,
|
|
190
207
|
cid: message.cid,
|
|
208
|
+
context: message,
|
|
191
209
|
});
|
|
192
210
|
}}
|
|
193
211
|
>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Text, View } from "react-native";
|
|
2
|
+
import emojiData from "../../assets/emoji-data.json";
|
|
3
|
+
import * as zero from "../../ui";
|
|
4
|
+
import { Chat } from "../chat/chat";
|
|
5
|
+
import { ChatBox } from "../chat/chat-box";
|
|
6
|
+
|
|
7
|
+
const { flex, bg, r, borders, p, px, py, text, layout } = zero;
|
|
8
|
+
|
|
9
|
+
interface ChatPanelProps {
|
|
10
|
+
isLive: boolean;
|
|
11
|
+
isConnected: boolean;
|
|
12
|
+
messagesPerMinute?: number;
|
|
13
|
+
canModerate?: boolean;
|
|
14
|
+
shownMessages?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ChatPanel({
|
|
18
|
+
isLive,
|
|
19
|
+
isConnected,
|
|
20
|
+
messagesPerMinute = 0,
|
|
21
|
+
canModerate = false,
|
|
22
|
+
shownMessages = 50,
|
|
23
|
+
}: ChatPanelProps) {
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
style={[
|
|
27
|
+
flex.values[1],
|
|
28
|
+
bg.neutral[900],
|
|
29
|
+
borders.width.thin,
|
|
30
|
+
borders.color.neutral[700],
|
|
31
|
+
layout.flex.column,
|
|
32
|
+
r.lg,
|
|
33
|
+
]}
|
|
34
|
+
>
|
|
35
|
+
<View
|
|
36
|
+
style={[
|
|
37
|
+
layout.flex.row,
|
|
38
|
+
layout.flex.spaceBetween,
|
|
39
|
+
layout.flex.alignCenter,
|
|
40
|
+
borders.bottom.width.thin,
|
|
41
|
+
borders.bottom.color.neutral[700],
|
|
42
|
+
p[4],
|
|
43
|
+
]}
|
|
44
|
+
>
|
|
45
|
+
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
|
|
46
|
+
Chat
|
|
47
|
+
</Text>
|
|
48
|
+
<View style={[layout.flex.row, layout.flex.alignCenter]}>
|
|
49
|
+
<View
|
|
50
|
+
style={[
|
|
51
|
+
{ width: 6, height: 6, borderRadius: 3 },
|
|
52
|
+
isLive && isConnected ? bg.green[500] : bg.gray[500],
|
|
53
|
+
]}
|
|
54
|
+
/>
|
|
55
|
+
<Text style={[text.gray[400], { fontSize: 12, marginLeft: 8 }]}>
|
|
56
|
+
{messagesPerMinute} msg/min
|
|
57
|
+
</Text>
|
|
58
|
+
</View>
|
|
59
|
+
</View>
|
|
60
|
+
<View style={[flex.values[1], px[2], { minHeight: 0 }]}>
|
|
61
|
+
<View style={[flex.values[1], { minHeight: 0 }]}>
|
|
62
|
+
<Chat canModerate={canModerate} shownMessages={shownMessages} />
|
|
63
|
+
</View>
|
|
64
|
+
<View style={[{ flexShrink: 0 }]}>
|
|
65
|
+
<ChatBox
|
|
66
|
+
emojiData={emojiData}
|
|
67
|
+
chatBoxStyle={[
|
|
68
|
+
bg.gray[700],
|
|
69
|
+
borders.width.thin,
|
|
70
|
+
borders.color.gray[600],
|
|
71
|
+
r.md,
|
|
72
|
+
p[3],
|
|
73
|
+
!isConnected && { opacity: 0.6 },
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
</View>
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Car, Radio, Users } from "lucide-react-native";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
import * as zero from "../../ui";
|
|
4
|
+
|
|
5
|
+
const { bg, r, borders, px, py, text, layout, gap } = zero;
|
|
6
|
+
|
|
7
|
+
interface MetricItemProps {
|
|
8
|
+
icon: any;
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
status?: "good" | "warning" | "error";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function MetricItem({ icon: Icon, label, value, status }: MetricItemProps) {
|
|
15
|
+
const statusColors = {
|
|
16
|
+
good: text.green[400],
|
|
17
|
+
warning: text.yellow[400],
|
|
18
|
+
error: text.red[400],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const statusColor = status ? statusColors[status] : text.gray[300];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
|
|
25
|
+
<Icon size={16} color="#9ca3af" />
|
|
26
|
+
<View style={[layout.flex.column]}>
|
|
27
|
+
<Text style={[text.gray[400], { fontSize: 11, fontWeight: "500" }]}>
|
|
28
|
+
{label}
|
|
29
|
+
</Text>
|
|
30
|
+
<Text style={[statusColor, { fontSize: 13, fontWeight: "600" }]}>
|
|
31
|
+
{value}
|
|
32
|
+
</Text>
|
|
33
|
+
</View>
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface StatusIndicatorProps {
|
|
39
|
+
status: "excellent" | "good" | "poor" | "offline";
|
|
40
|
+
isLive: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function StatusIndicator({ status, isLive }: StatusIndicatorProps) {
|
|
44
|
+
const getStatusColor = () => {
|
|
45
|
+
if (!isLive) return bg.gray[500];
|
|
46
|
+
switch (status) {
|
|
47
|
+
case "excellent":
|
|
48
|
+
return bg.green[500];
|
|
49
|
+
case "good":
|
|
50
|
+
return bg.yellow[500];
|
|
51
|
+
case "poor":
|
|
52
|
+
return bg.orange[500];
|
|
53
|
+
case "offline":
|
|
54
|
+
return bg.red[500];
|
|
55
|
+
default:
|
|
56
|
+
return bg.gray[500];
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getStatusText = () => {
|
|
61
|
+
if (!isLive) return "OFFLINE";
|
|
62
|
+
switch (status) {
|
|
63
|
+
case "excellent":
|
|
64
|
+
return "EXCELLENT";
|
|
65
|
+
case "good":
|
|
66
|
+
return "GOOD";
|
|
67
|
+
case "poor":
|
|
68
|
+
return "POOR";
|
|
69
|
+
case "offline":
|
|
70
|
+
return "OFFLINE";
|
|
71
|
+
default:
|
|
72
|
+
return "UNKNOWN";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
|
|
78
|
+
<View
|
|
79
|
+
style={[
|
|
80
|
+
{ width: 8, height: 8, borderRadius: 4 },
|
|
81
|
+
getStatusColor(),
|
|
82
|
+
!isLive && { opacity: 0.6 },
|
|
83
|
+
]}
|
|
84
|
+
/>
|
|
85
|
+
<Text
|
|
86
|
+
style={[
|
|
87
|
+
text.white,
|
|
88
|
+
{ fontSize: 12, fontWeight: "700", letterSpacing: 0.5 },
|
|
89
|
+
!isLive && text.gray[400],
|
|
90
|
+
]}
|
|
91
|
+
>
|
|
92
|
+
{getStatusText()}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface HeaderProps {
|
|
99
|
+
isLive: boolean;
|
|
100
|
+
streamTitle?: string;
|
|
101
|
+
viewers?: number;
|
|
102
|
+
uptime?: string;
|
|
103
|
+
bitrate?: string;
|
|
104
|
+
timeBetweenSegments?: number;
|
|
105
|
+
connectionStatus?: "excellent" | "good" | "poor" | "offline";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function Header({
|
|
109
|
+
isLive,
|
|
110
|
+
streamTitle = "Live Stream",
|
|
111
|
+
viewers = 0,
|
|
112
|
+
uptime = "00:00:00",
|
|
113
|
+
bitrate = "0 mbps",
|
|
114
|
+
timeBetweenSegments = 0,
|
|
115
|
+
connectionStatus = "offline",
|
|
116
|
+
}: HeaderProps) {
|
|
117
|
+
const getConnectionQuality = (): "good" | "warning" | "error" => {
|
|
118
|
+
if (timeBetweenSegments <= 1500) return "good";
|
|
119
|
+
if (timeBetweenSegments <= 3000) return "warning";
|
|
120
|
+
return "error";
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View
|
|
125
|
+
style={[
|
|
126
|
+
px[4],
|
|
127
|
+
py[3],
|
|
128
|
+
r.lg,
|
|
129
|
+
layout.flex.row,
|
|
130
|
+
layout.flex.spaceBetween,
|
|
131
|
+
bg.neutral[900],
|
|
132
|
+
borders.width.thin,
|
|
133
|
+
borders.color.neutral[700],
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
{/* Left side - Stream title and status */}
|
|
137
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[4]]}>
|
|
138
|
+
<View>
|
|
139
|
+
<Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}>
|
|
140
|
+
{streamTitle}
|
|
141
|
+
</Text>
|
|
142
|
+
<StatusIndicator status={connectionStatus} isLive={isLive} />
|
|
143
|
+
</View>
|
|
144
|
+
</View>
|
|
145
|
+
|
|
146
|
+
{/* Right side - Stream metrics */}
|
|
147
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[6]]}>
|
|
148
|
+
{isLive && (
|
|
149
|
+
<>
|
|
150
|
+
<MetricItem
|
|
151
|
+
icon={Users}
|
|
152
|
+
label="Viewers"
|
|
153
|
+
value={viewers.toLocaleString()}
|
|
154
|
+
/>
|
|
155
|
+
<MetricItem icon={Car} label="Bitrate" value={bitrate} />
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{!isLive && (
|
|
160
|
+
<View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
|
|
161
|
+
<Radio size={16} color="#6b7280" />
|
|
162
|
+
<Text style={[text.gray[400], { fontSize: 13 }]}>
|
|
163
|
+
Stream offline
|
|
164
|
+
</Text>
|
|
165
|
+
</View>
|
|
166
|
+
)}
|
|
167
|
+
</View>
|
|
168
|
+
</View>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as ChatPanel } from "./chat-panel";
|
|
2
|
+
export { default as Header } from "./header";
|
|
3
|
+
export { default as InformationWidget } from "./information-widget";
|
|
4
|
+
export { default as ModActions } from "./mod-actions";
|
|
5
|
+
export { default as Problems } from "./problems";
|