@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.
Files changed (48) hide show
  1. package/dist/assets/emoji-data.json +19371 -0
  2. package/dist/components/chat/chat-box.js +19 -2
  3. package/dist/components/chat/chat-message.js +12 -4
  4. package/dist/components/chat/chat.js +15 -4
  5. package/dist/components/chat/mod-view.js +15 -8
  6. package/dist/components/dashboard/chat-panel.js +38 -0
  7. package/dist/components/dashboard/header.js +80 -0
  8. package/dist/components/dashboard/index.js +14 -0
  9. package/dist/components/dashboard/information-widget.js +234 -0
  10. package/dist/components/dashboard/mod-actions.js +71 -0
  11. package/dist/components/dashboard/problems.js +74 -0
  12. package/dist/components/mobile-player/ui/viewer-context-menu.js +15 -6
  13. package/dist/components/ui/button.js +2 -2
  14. package/dist/components/ui/dropdown.js +20 -1
  15. package/dist/components/ui/index.js +2 -0
  16. package/dist/components/ui/info-box.js +31 -0
  17. package/dist/components/ui/info-row.js +23 -0
  18. package/dist/components/ui/toast.js +43 -0
  19. package/dist/index.js +3 -1
  20. package/dist/lib/theme/atoms.js +66 -45
  21. package/dist/lib/theme/tokens.js +285 -12
  22. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  23. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  24. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  25. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  26. package/package.json +2 -2
  27. package/src/assets/emoji-data.json +19371 -0
  28. package/src/components/chat/chat-box.tsx +19 -1
  29. package/src/components/chat/chat-message.tsx +22 -14
  30. package/src/components/chat/chat.tsx +21 -6
  31. package/src/components/chat/mod-view.tsx +24 -6
  32. package/src/components/dashboard/chat-panel.tsx +80 -0
  33. package/src/components/dashboard/header.tsx +170 -0
  34. package/src/components/dashboard/index.tsx +5 -0
  35. package/src/components/dashboard/information-widget.tsx +526 -0
  36. package/src/components/dashboard/mod-actions.tsx +133 -0
  37. package/src/components/dashboard/problems.tsx +151 -0
  38. package/src/components/mobile-player/ui/viewer-context-menu.tsx +67 -38
  39. package/src/components/ui/button.tsx +2 -2
  40. package/src/components/ui/dropdown.tsx +38 -3
  41. package/src/components/ui/index.ts +2 -0
  42. package/src/components/ui/info-box.tsx +60 -0
  43. package/src/components/ui/info-row.tsx +48 -0
  44. package/src/components/ui/toast.tsx +110 -0
  45. package/src/index.tsx +3 -0
  46. package/src/lib/theme/atoms.ts +97 -43
  47. package/src/lib/theme/tokens.ts +285 -12
  48. 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={submit}
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
- w.percent[100],
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 numberOfLines={1} style={[flex.shrink[1], mr[4]]}>
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 style={[gap.all[2], layout.flex.row, w.percent[100]]}>
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 weight="bold" color="default" style={[flex.shrink[1]]}>
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, w } from "../../lib/theme/atoms";
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
- { position: "relative", borderRadius: 8 },
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 style={[flex.shrink[1]].concat(propsStyle || [])}>
269
+ <View
270
+ style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }].concat(
271
+ propsStyle || [],
272
+ )}
273
+ >
263
274
  <FlatList
264
- style={[flex.grow[1], flex.shrink[1], w.percent[100]]}
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/${channelId}`,
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 message={message} />
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";