@streamplace/components 0.7.18 → 0.7.21

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 (122) hide show
  1. package/dist/assets/emoji-data.json +19371 -0
  2. package/dist/components/chat/chat-box.js +319 -0
  3. package/dist/components/chat/chat-message.js +87 -0
  4. package/dist/components/chat/chat.js +150 -0
  5. package/dist/components/chat/emoji-suggestions.js +35 -0
  6. package/dist/components/chat/mention-suggestions.js +42 -0
  7. package/dist/components/chat/mod-view.js +112 -0
  8. package/dist/components/chat/system-message.js +19 -0
  9. package/dist/components/dashboard/chat-panel.js +38 -0
  10. package/dist/components/dashboard/header.js +80 -0
  11. package/dist/components/dashboard/index.js +14 -0
  12. package/dist/components/dashboard/information-widget.js +234 -0
  13. package/dist/components/dashboard/mod-actions.js +71 -0
  14. package/dist/components/dashboard/problems.js +74 -0
  15. package/dist/components/icons/bluesky-icon.js +9 -0
  16. package/dist/components/keep-awake.js +7 -0
  17. package/dist/components/keep-awake.native.js +16 -0
  18. package/dist/components/mobile-player/fullscreen.js +76 -0
  19. package/dist/components/mobile-player/fullscreen.native.js +141 -0
  20. package/dist/components/mobile-player/player.js +94 -0
  21. package/dist/components/mobile-player/props.js +2 -0
  22. package/dist/components/mobile-player/shared.js +54 -0
  23. package/dist/components/mobile-player/ui/autoplay-button.js +68 -0
  24. package/dist/components/mobile-player/ui/countdown.js +83 -0
  25. package/dist/components/mobile-player/ui/index.js +12 -0
  26. package/dist/components/mobile-player/ui/input.js +42 -0
  27. package/dist/components/mobile-player/ui/metrics.js +44 -0
  28. package/dist/components/mobile-player/ui/report-modal.js +90 -0
  29. package/dist/components/mobile-player/ui/streamer-context-menu.js +7 -0
  30. package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
  31. package/dist/components/mobile-player/ui/viewer-context-menu.js +51 -0
  32. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
  33. package/dist/components/mobile-player/ui/viewers.js +23 -0
  34. package/dist/components/mobile-player/use-webrtc.js +243 -0
  35. package/dist/components/mobile-player/video-async.native.js +276 -0
  36. package/dist/components/mobile-player/video-retry.js +29 -0
  37. package/dist/components/mobile-player/video.js +475 -0
  38. package/dist/components/mobile-player/video.native.js +56 -0
  39. package/dist/components/mobile-player/webrtc-diagnostics.js +110 -0
  40. package/dist/components/mobile-player/webrtc-primitives.js +27 -0
  41. package/dist/components/mobile-player/webrtc-primitives.native.js +8 -0
  42. package/dist/components/share/sharesheet.js +91 -0
  43. package/dist/components/ui/button.js +223 -0
  44. package/dist/components/ui/dialog.js +206 -0
  45. package/dist/components/ui/dropdown.js +172 -0
  46. package/dist/components/ui/icons.js +25 -0
  47. package/dist/components/ui/index.js +34 -0
  48. package/dist/components/ui/info-box.js +31 -0
  49. package/dist/components/ui/info-row.js +23 -0
  50. package/dist/components/ui/input.js +205 -0
  51. package/dist/components/ui/loader.js +10 -0
  52. package/dist/components/ui/primitives/button.js +125 -0
  53. package/dist/components/ui/primitives/input.js +206 -0
  54. package/dist/components/ui/primitives/modal.js +206 -0
  55. package/dist/components/ui/primitives/text.js +292 -0
  56. package/dist/components/ui/resizeable.js +121 -0
  57. package/dist/components/ui/slider.js +5 -0
  58. package/dist/components/ui/text.js +177 -0
  59. package/dist/components/ui/textarea.js +19 -0
  60. package/dist/components/ui/toast.js +175 -0
  61. package/dist/components/ui/view.js +252 -0
  62. package/dist/hooks/index.js +14 -0
  63. package/dist/hooks/useAvatars.js +35 -0
  64. package/dist/hooks/useCameraToggle.js +12 -0
  65. package/dist/hooks/useKeyboard.js +36 -0
  66. package/dist/hooks/useKeyboardSlide.js +14 -0
  67. package/dist/hooks/useLivestreamInfo.js +69 -0
  68. package/dist/hooks/useOuterAndInnerDimensions.js +30 -0
  69. package/dist/hooks/usePlayerDimensions.js +22 -0
  70. package/dist/hooks/usePointerDevice.js +71 -0
  71. package/dist/hooks/useSegmentDimensions.js +17 -0
  72. package/dist/hooks/useSegmentTiming.js +65 -0
  73. package/dist/index.js +34 -0
  74. package/dist/lib/browser.js +35 -0
  75. package/dist/lib/facet.js +92 -0
  76. package/dist/lib/system-messages.js +101 -0
  77. package/dist/lib/theme/atoms.js +646 -0
  78. package/dist/lib/theme/atoms.types.js +6 -0
  79. package/dist/lib/theme/index.js +35 -0
  80. package/dist/lib/theme/theme.js +256 -0
  81. package/dist/lib/theme/tokens.js +659 -0
  82. package/dist/lib/utils.js +105 -0
  83. package/dist/livestream-provider/index.js +30 -0
  84. package/dist/livestream-provider/websocket.js +45 -0
  85. package/dist/livestream-store/chat.js +308 -0
  86. package/dist/livestream-store/context.js +5 -0
  87. package/dist/livestream-store/index.js +7 -0
  88. package/dist/livestream-store/livestream-state.js +2 -0
  89. package/dist/livestream-store/livestream-store.js +58 -0
  90. package/dist/livestream-store/problems.js +76 -0
  91. package/dist/livestream-store/stream-key.js +88 -0
  92. package/dist/livestream-store/websocket-consumer.js +94 -0
  93. package/dist/player-store/context.js +5 -0
  94. package/dist/player-store/index.js +9 -0
  95. package/dist/player-store/player-provider.js +58 -0
  96. package/dist/player-store/player-state.js +25 -0
  97. package/dist/player-store/player-store.js +201 -0
  98. package/dist/player-store/single-player-provider.js +121 -0
  99. package/dist/streamplace-provider/context.js +5 -0
  100. package/dist/streamplace-provider/index.js +20 -0
  101. package/dist/streamplace-provider/poller.js +49 -0
  102. package/dist/streamplace-provider/xrpc.js +0 -0
  103. package/dist/streamplace-store/block.js +65 -0
  104. package/dist/streamplace-store/index.js +6 -0
  105. package/dist/streamplace-store/stream.js +247 -0
  106. package/dist/streamplace-store/streamplace-store.js +47 -0
  107. package/dist/streamplace-store/user.js +52 -0
  108. package/dist/streamplace-store/xrpc.js +15 -0
  109. package/dist/ui/index.js +79 -0
  110. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  111. package/package.json +5 -4
  112. package/src/components/chat/chat-box.tsx +3 -0
  113. package/src/components/chat/mod-view.tsx +39 -5
  114. package/src/components/mobile-player/fullscreen.tsx +2 -0
  115. package/src/components/mobile-player/ui/autoplay-button.tsx +86 -0
  116. package/src/components/mobile-player/ui/index.ts +1 -0
  117. package/src/components/mobile-player/video.tsx +11 -1
  118. package/src/livestream-store/chat.tsx +22 -0
  119. package/src/player-store/player-provider.tsx +2 -1
  120. package/src/player-store/player-state.tsx +6 -0
  121. package/src/player-store/player-store.tsx +4 -0
  122. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModView = void 0;
4
+ exports.DeleteButton = DeleteButton;
5
+ exports.ReportButton = ReportButton;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const dropdown_menu_1 = require("@rn-primitives/dropdown-menu");
8
+ const react_1 = require("react");
9
+ const atoms_1 = require("../../lib/theme/atoms");
10
+ const player_store_1 = require("../../player-store");
11
+ const block_1 = require("../../streamplace-store/block");
12
+ const xrpc_1 = require("../../streamplace-store/xrpc");
13
+ const react_native_1 = require("react-native");
14
+ const livestream_store_1 = require("../../livestream-store");
15
+ const streamplace_store_1 = require("../../streamplace-store");
16
+ const ui_1 = require("../ui");
17
+ const BSKY_FRONTEND_DOMAIN = "bsky.app";
18
+ exports.ModView = (0, react_1.forwardRef)(() => {
19
+ const triggerRef = (0, react_1.useRef)(null);
20
+ const message = (0, player_store_1.usePlayerStore)((state) => state.modMessage);
21
+ let agent = (0, xrpc_1.usePDSAgent)();
22
+ let [messageRemoved, setMessageRemoved] = (0, react_1.useState)(false);
23
+ let { createBlock, isLoading: isBlockLoading } = (0, block_1.useCreateBlockRecord)();
24
+ let { createHideChat, isLoading: isHideLoading } = (0, block_1.useCreateHideChatRecord)();
25
+ const setReportModalOpen = (0, player_store_1.usePlayerStore)((x) => x.setReportModalOpen);
26
+ const setReportSubject = (0, player_store_1.usePlayerStore)((x) => x.setReportSubject);
27
+ const setModMessage = (0, player_store_1.usePlayerStore)((x) => x.setModMessage);
28
+ // get the channel did
29
+ const channelId = (0, player_store_1.usePlayerStore)((state) => state.src);
30
+ // get the logged in user's identity
31
+ const handle = (0, streamplace_store_1.useStreamplaceStore)((state) => state.handle);
32
+ if (!agent?.did) {
33
+ (0, jsx_runtime_1.jsx)(ui_1.View, { style: [ui_1.layout.flex.row, ui_1.layout.flex.alignCenter, atoms_1.gap.all[2]], children: (0, jsx_runtime_1.jsx)(ui_1.Text, { children: "Log in to submit mod actions" }) });
34
+ }
35
+ const cleanup = () => {
36
+ setModMessage(null);
37
+ };
38
+ (0, react_1.useEffect)(() => {
39
+ if (message) {
40
+ setMessageRemoved(false);
41
+ triggerRef.current?.open();
42
+ }
43
+ else {
44
+ triggerRef.current?.close();
45
+ }
46
+ }, [message]);
47
+ return ((0, jsx_runtime_1.jsxs)(ui_1.DropdownMenu, { style: [ui_1.layout.flex.row, ui_1.layout.flex.alignCenter, atoms_1.gap.all[2], atoms_1.w[80]], onOpenChange: (isOpen) => {
48
+ if (!isOpen) {
49
+ cleanup();
50
+ }
51
+ }, children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuTrigger, { ref: triggerRef, children: (0, jsx_runtime_1.jsx)(ui_1.View, {}) }), (0, jsx_runtime_1.jsx)(ui_1.ResponsiveDropdownMenuContent, { children: message && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuGroup, { children: (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { children: (0, jsx_runtime_1.jsx)(ui_1.View, { style: [
52
+ ui_1.layout.flex.column,
53
+ atoms_1.mr[5],
54
+ { gap: 6, maxWidth: "100%" },
55
+ ], children: (0, jsx_runtime_1.jsxs)(ui_1.Text, { style: {
56
+ fontVariant: ["tabular-nums"],
57
+ color: ui_1.atoms.colors.gray[300],
58
+ }, children: [new Date(message.record.createdAt).toLocaleTimeString([], {
59
+ hour: "2-digit",
60
+ minute: "2-digit",
61
+ hour12: false,
62
+ }), " ", "@", message.author.handle, ": ", message.record.text] }) }) }) }), channelId === handle && ((0, jsx_runtime_1.jsxs)(ui_1.DropdownMenuGroup, { title: `Moderation actions`, children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { disabled: isHideLoading || messageRemoved, onPress: () => {
63
+ if (isHideLoading || messageRemoved)
64
+ return;
65
+ createHideChat(message.uri)
66
+ .then((r) => setMessageRemoved(true))
67
+ .catch((e) => console.error(e));
68
+ }, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { color: isHideLoading || messageRemoved ? "muted" : "destructive", children: isHideLoading
69
+ ? "Removing..."
70
+ : messageRemoved
71
+ ? "Message removed"
72
+ : "Remove this message" }) }), (0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { disabled: message.author.did === agent?.did || isBlockLoading, onPress: () => {
73
+ createBlock(message.author.did)
74
+ .then((r) => console.log(r))
75
+ .catch((e) => console.error(e));
76
+ }, children: message.author.did === agent?.did ? ((0, jsx_runtime_1.jsx)(ui_1.Text, { color: "muted", children: "Block yourself (you can't block yourself)" })) : ((0, jsx_runtime_1.jsx)(ui_1.Text, { color: "destructive", children: isBlockLoading
77
+ ? "Blocking..."
78
+ : `Block user @${message.author.handle} from this channel` })) })] })), (0, jsx_runtime_1.jsxs)(ui_1.DropdownMenuGroup, { title: `User actions`, children: [(0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
79
+ react_native_1.Linking.openURL(`https://${BSKY_FRONTEND_DOMAIN}/profile/${message.author.handle}`);
80
+ }, children: (0, jsx_runtime_1.jsxs)(ui_1.Text, { color: "primary", children: ["View user on ", BSKY_FRONTEND_DOMAIN] }) }), message.author.did === agent?.did && ((0, jsx_runtime_1.jsx)(DeleteButton, { message: message })), message.author.did !== agent?.did && ((0, jsx_runtime_1.jsx)(ReportButton, { message: message, setReportModalOpen: setReportModalOpen, setReportSubject: setReportSubject }))] })] })) })] }));
81
+ });
82
+ function DeleteButton({ message, }) {
83
+ const deleteChatMessage = (0, livestream_store_1.useDeleteChatMessage)();
84
+ const [confirming, setConfirming] = (0, react_1.useState)(false);
85
+ const { onOpenChange } = (0, dropdown_menu_1.useRootContext)();
86
+ return ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
87
+ if (!message)
88
+ return;
89
+ if (!confirming) {
90
+ setConfirming(true);
91
+ return;
92
+ }
93
+ deleteChatMessage(message.uri).then(() => {
94
+ onOpenChange?.(false);
95
+ });
96
+ }, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { color: "destructive", children: confirming ? "Are you sure?" : "Delete message" }) }));
97
+ }
98
+ function ReportButton({ message, setReportModalOpen, setReportSubject, }) {
99
+ const { onOpenChange } = (0, dropdown_menu_1.useRootContext)();
100
+ return ((0, jsx_runtime_1.jsx)(ui_1.DropdownMenuItem, { onPress: () => {
101
+ if (!message)
102
+ return;
103
+ onOpenChange?.(false);
104
+ setReportModalOpen(true);
105
+ setReportSubject({
106
+ $type: "com.atproto.repo.strongRef",
107
+ uri: message.uri,
108
+ cid: message.cid,
109
+ context: message,
110
+ });
111
+ }, children: (0, jsx_runtime_1.jsx)(ui_1.Text, { color: "warning", children: "Report chat..." }) }));
112
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SystemMessage = SystemMessage;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_native_1 = require("react-native");
6
+ const ui_1 = require("../../ui");
7
+ const ui_2 = require("../ui");
8
+ const text_1 = require("../ui/text");
9
+ function SystemMessage({ title, timestamp }) {
10
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [ui_1.w.percent[100], ui_1.px[2], ui_1.pb[2]], children: [(0, jsx_runtime_1.jsx)(text_1.Code, { color: "muted", tracking: "widest", style: [ui_1.pl[12], ui_1.ml[1]], children: "SYSTEM MESSAGE" }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [ui_1.gap.all[2], ui_1.layout.flex.row], children: [(0, jsx_runtime_1.jsx)(text_1.Text, { style: {
11
+ fontVariant: ["tabular-nums"],
12
+ color: ui_2.atoms.colors.gray[300],
13
+ }, children: timestamp.toLocaleTimeString([], {
14
+ hour: "2-digit",
15
+ minute: "2-digit",
16
+ hour12: false,
17
+ }) }), (0, jsx_runtime_1.jsx)(text_1.Text, { weight: "bold", color: "default", style: [ui_1.flex.shrink[1]], children: title })] })] }));
18
+ }
19
+ exports.default = SystemMessage;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = ChatPanel;
4
+ const tslib_1 = require("tslib");
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_native_1 = require("react-native");
7
+ const emoji_data_json_1 = tslib_1.__importDefault(require("../../assets/emoji-data.json"));
8
+ const zero = tslib_1.__importStar(require("../../ui"));
9
+ const chat_1 = require("../chat/chat");
10
+ const chat_box_1 = require("../chat/chat-box");
11
+ const { flex, bg, r, borders, p, px, py, text, layout } = zero;
12
+ function ChatPanel({ isLive, isConnected, messagesPerMinute = 0, canModerate = false, shownMessages = 50, }) {
13
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
14
+ flex.values[1],
15
+ bg.neutral[900],
16
+ borders.width.thin,
17
+ borders.color.neutral[700],
18
+ layout.flex.column,
19
+ r.lg,
20
+ ], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
21
+ layout.flex.row,
22
+ layout.flex.spaceBetween,
23
+ layout.flex.alignCenter,
24
+ borders.bottom.width.thin,
25
+ borders.bottom.color.neutral[700],
26
+ p[4],
27
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [text.white, { fontSize: 18, fontWeight: "600" }], children: "Chat" }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
28
+ { width: 6, height: 6, borderRadius: 3 },
29
+ isLive && isConnected ? bg.green[500] : bg.gray[500],
30
+ ] }), (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [text.gray[400], { fontSize: 12, marginLeft: 8 }], children: [messagesPerMinute, " msg/min"] })] })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [flex.values[1], px[2], { minHeight: 0 }], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [flex.values[1], { minHeight: 0 }], children: (0, jsx_runtime_1.jsx)(chat_1.Chat, { canModerate: canModerate, shownMessages: shownMessages }) }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [{ flexShrink: 0 }], children: (0, jsx_runtime_1.jsx)(chat_box_1.ChatBox, { emojiData: emoji_data_json_1.default, chatBoxStyle: [
31
+ bg.gray[700],
32
+ borders.width.thin,
33
+ borders.color.gray[600],
34
+ r.md,
35
+ p[3],
36
+ !isConnected && { opacity: 0.6 },
37
+ ] }) })] })] }));
38
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = Header;
4
+ const tslib_1 = require("tslib");
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const lucide_react_native_1 = require("lucide-react-native");
7
+ const react_native_1 = require("react-native");
8
+ const zero = tslib_1.__importStar(require("../../ui"));
9
+ const { bg, r, borders, px, py, text, layout, gap } = zero;
10
+ function MetricItem({ icon: Icon, label, value, status }) {
11
+ const statusColors = {
12
+ good: text.green[400],
13
+ warning: text.yellow[400],
14
+ error: text.red[400],
15
+ };
16
+ const statusColor = status ? statusColors[status] : text.gray[300];
17
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: [(0, jsx_runtime_1.jsx)(Icon, { size: 16, color: "#9ca3af" }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.column], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [text.gray[400], { fontSize: 11, fontWeight: "500" }], children: label }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [statusColor, { fontSize: 13, fontWeight: "600" }], children: value })] })] }));
18
+ }
19
+ function StatusIndicator({ status, isLive }) {
20
+ const getStatusColor = () => {
21
+ if (!isLive)
22
+ return bg.gray[500];
23
+ switch (status) {
24
+ case "excellent":
25
+ return bg.green[500];
26
+ case "good":
27
+ return bg.yellow[500];
28
+ case "poor":
29
+ return bg.orange[500];
30
+ case "offline":
31
+ return bg.red[500];
32
+ default:
33
+ return bg.gray[500];
34
+ }
35
+ };
36
+ const getStatusText = () => {
37
+ if (!isLive)
38
+ return "OFFLINE";
39
+ switch (status) {
40
+ case "excellent":
41
+ return "EXCELLENT";
42
+ case "good":
43
+ return "GOOD";
44
+ case "poor":
45
+ return "POOR";
46
+ case "offline":
47
+ return "OFFLINE";
48
+ default:
49
+ return "UNKNOWN";
50
+ }
51
+ };
52
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
53
+ { width: 8, height: 8, borderRadius: 4 },
54
+ getStatusColor(),
55
+ !isLive && { opacity: 0.6 },
56
+ ] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
57
+ text.white,
58
+ { fontSize: 12, fontWeight: "700", letterSpacing: 0.5 },
59
+ !isLive && text.gray[400],
60
+ ], children: getStatusText() })] }));
61
+ }
62
+ function Header({ isLive, streamTitle = "Live Stream", viewers = 0, uptime = "00:00:00", bitrate = "0 mbps", timeBetweenSegments = 0, connectionStatus = "offline", }) {
63
+ const getConnectionQuality = () => {
64
+ if (timeBetweenSegments <= 1500)
65
+ return "good";
66
+ if (timeBetweenSegments <= 3000)
67
+ return "warning";
68
+ return "error";
69
+ };
70
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
71
+ px[4],
72
+ py[3],
73
+ r.lg,
74
+ layout.flex.row,
75
+ layout.flex.spaceBetween,
76
+ bg.neutral[900],
77
+ borders.width.thin,
78
+ borders.color.neutral[700],
79
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[4]], children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [text.white, { fontSize: 18, fontWeight: "600" }], children: streamTitle }), (0, jsx_runtime_1.jsx)(StatusIndicator, { status: connectionStatus, isLive: isLive })] }) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[6]], children: [isLive && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(MetricItem, { icon: lucide_react_native_1.Users, label: "Viewers", value: viewers.toLocaleString() }), (0, jsx_runtime_1.jsx)(MetricItem, { icon: lucide_react_native_1.Car, label: "Bitrate", value: bitrate })] })), !isLive && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Radio, { size: 16, color: "#6b7280" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [text.gray[400], { fontSize: 13 }], children: "Stream offline" })] }))] })] }));
80
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Problems = exports.ModActions = exports.InformationWidget = exports.Header = exports.ChatPanel = void 0;
4
+ const tslib_1 = require("tslib");
5
+ var chat_panel_1 = require("./chat-panel");
6
+ Object.defineProperty(exports, "ChatPanel", { enumerable: true, get: function () { return tslib_1.__importDefault(chat_panel_1).default; } });
7
+ var header_1 = require("./header");
8
+ Object.defineProperty(exports, "Header", { enumerable: true, get: function () { return tslib_1.__importDefault(header_1).default; } });
9
+ var information_widget_1 = require("./information-widget");
10
+ Object.defineProperty(exports, "InformationWidget", { enumerable: true, get: function () { return tslib_1.__importDefault(information_widget_1).default; } });
11
+ var mod_actions_1 = require("./mod-actions");
12
+ Object.defineProperty(exports, "ModActions", { enumerable: true, get: function () { return tslib_1.__importDefault(mod_actions_1).default; } });
13
+ var problems_1 = require("./problems");
14
+ Object.defineProperty(exports, "Problems", { enumerable: true, get: function () { return tslib_1.__importDefault(problems_1).default; } });
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = InformationWidget;
4
+ const tslib_1 = require("tslib");
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const lucide_react_native_1 = require("lucide-react-native");
7
+ const react_1 = tslib_1.__importStar(require("react"));
8
+ const react_native_1 = require("react-native");
9
+ const react_native_svg_1 = tslib_1.__importStar(require("react-native-svg"));
10
+ const livestream_store_1 = require("../../livestream-store");
11
+ const zero = tslib_1.__importStar(require("../../ui"));
12
+ const ui_1 = require("../ui");
13
+ const BITRATE_HISTORY_LENGTH = 30;
14
+ const { bg, r, borders, px, py, text, layout, gap, flex, p } = zero;
15
+ function InformationWidget({ embedMode = false, wideMode, // Optional override
16
+ showChart = true, }) {
17
+ const [bitrateHistory, setBitrateHistory] = (0, react_1.useState)(Array.from({ length: BITRATE_HISTORY_LENGTH }, () => 0));
18
+ const [showViewers, setShowViewers] = (0, react_1.useState)(false);
19
+ const [componentWidth, setComponentWidth] = (0, react_1.useState)(220);
20
+ const [componentHeight, setComponentHeight] = (0, react_1.useState)(400);
21
+ const [streamStartTime, setStreamStartTime] = (0, react_1.useState)(null);
22
+ const [layoutMeasured, setLayoutMeasured] = (0, react_1.useState)(false);
23
+ const isWideMode = wideMode !== undefined ? wideMode : layoutMeasured && componentWidth > 400;
24
+ const isCompactHeight = layoutMeasured && componentHeight < 350;
25
+ const seg = (0, livestream_store_1.useSegment)();
26
+ const livestream = (0, livestream_store_1.useLivestreamStore)((x) => x.livestream);
27
+ const viewers = (0, livestream_store_1.useViewers)();
28
+ const getBitrate = (0, react_1.useCallback)(() => {
29
+ if (!seg?.size || !seg?.duration)
30
+ return 0;
31
+ const kbps = (seg.size * 8) / ((seg.duration || 1000000000) / 1000000000) / 1000;
32
+ return kbps;
33
+ }, [seg?.size, seg?.duration]);
34
+ const getMediaInfo = (0, react_1.useMemo)(() => {
35
+ const videoTrack = seg?.video?.[0];
36
+ const audioTrack = seg?.audio?.[0];
37
+ return {
38
+ resolution: videoTrack?.width && videoTrack?.height
39
+ ? `${videoTrack.width}x${videoTrack.height}`
40
+ : "Unknown",
41
+ fps: videoTrack?.framerate
42
+ ? `${(videoTrack.framerate.num / videoTrack.framerate.den).toFixed(2)} FPS`
43
+ : "Unknown",
44
+ videoCodec: videoTrack?.codec
45
+ ? videoTrack.codec.toUpperCase()
46
+ : "Unknown",
47
+ };
48
+ }, [seg?.video, seg?.audio]);
49
+ const currentBitrate = getBitrate();
50
+ (0, react_1.useEffect)(() => {
51
+ setBitrateHistory((prev) => [...prev.slice(1), currentBitrate]);
52
+ }, [currentBitrate]);
53
+ (0, react_1.useEffect)(() => {
54
+ if (seg?.startTime && !streamStartTime) {
55
+ setStreamStartTime(new Date(seg.startTime));
56
+ }
57
+ }, [seg?.startTime, streamStartTime]);
58
+ const getBitrateStatus = () => {
59
+ if (currentBitrate > 2000)
60
+ return "good";
61
+ if (currentBitrate > 1000)
62
+ return "warning";
63
+ if (currentBitrate > 0)
64
+ return "error";
65
+ return "neutral";
66
+ };
67
+ const getConnectionStatus = () => {
68
+ if (!seg)
69
+ return "error";
70
+ if (currentBitrate > 1500)
71
+ return "good";
72
+ if (currentBitrate > 500)
73
+ return "warning";
74
+ return "error";
75
+ };
76
+ const avgBitrate = bitrateHistory.length > 0
77
+ ? bitrateHistory.reduce((a, b) => a + b, 0) / bitrateHistory.length
78
+ : 0;
79
+ const peakBitrate = Math.max(...bitrateHistory, 0);
80
+ const uptimeMinutes = streamStartTime
81
+ ? Math.floor((Date.now() - streamStartTime.getTime()) / 60000)
82
+ : 0;
83
+ const estimatedLatency = seg?.duration
84
+ ? Math.floor((seg.duration / 1000000000) * 2)
85
+ : 0;
86
+ const displayBitrate = `${(currentBitrate / 1000).toFixed(2)} Mbps`;
87
+ const displayResolution = getMediaInfo.resolution;
88
+ const displayFps = getMediaInfo.fps;
89
+ const streamTitle = livestream?.record?.title || "Untitled Stream";
90
+ const viewerCount = viewers ?? 0;
91
+ const handleLayout = (0, react_1.useCallback)((event) => {
92
+ const { width, height } = event.nativeEvent.layout;
93
+ console.log("InformationWidget onLayout - size:", `${width}x${height}`);
94
+ if (width > 0 && height > 0) {
95
+ setComponentWidth(width);
96
+ setComponentHeight(height);
97
+ setLayoutMeasured(true);
98
+ }
99
+ }, []);
100
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { onLayout: handleLayout, style: [
101
+ embedMode
102
+ ? { backgroundColor: "rgba(23, 23, 23, 0.9)" }
103
+ : bg.neutral[900],
104
+ embedMode ? undefined : borders.width.thin,
105
+ embedMode ? undefined : borders.color.neutral[700],
106
+ r.lg,
107
+ px[4],
108
+ py[4],
109
+ gap.all[6],
110
+ flex.values[1],
111
+ {
112
+ minWidth: isWideMode ? 500 : 220,
113
+ width: "100%",
114
+ },
115
+ ], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
116
+ layout.flex.row,
117
+ layout.flex.spaceBetween,
118
+ layout.flex.alignCenter,
119
+ ], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[1]], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [text.white, { fontSize: 18, fontWeight: "700" }], numberOfLines: 1, children: "Stream Health" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
120
+ {
121
+ width: 8,
122
+ height: 8,
123
+ borderRadius: 4,
124
+ backgroundColor: getConnectionStatus() === "good"
125
+ ? "#22c55e"
126
+ : getConnectionStatus() === "warning"
127
+ ? "#f59e0b"
128
+ : "#ef4444",
129
+ },
130
+ ] })] }), (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: () => setShowViewers(!showViewers), style: [
131
+ layout.flex.column,
132
+ layout.flex.alignCenter,
133
+ gap.all[1],
134
+ { minWidth: 120 },
135
+ ], children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: [showViewers ? ((0, jsx_runtime_1.jsx)(lucide_react_native_1.Eye, { size: 14, color: "#9ca3af" })) : ((0, jsx_runtime_1.jsx)(lucide_react_native_1.EyeClosed, { size: 14, color: "#9ca3af" })), (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [
136
+ text.white,
137
+ { fontSize: 16, fontWeight: "600", textAlign: "center" },
138
+ ], children: [showViewers ? `${viewerCount}` : "...", " viewer", showViewers && viewerCount !== 1 ? "s" : ""] })] }) })] }), isWideMode ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [gap.all[3]], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, gap.all[2]], children: [(0, jsx_runtime_1.jsx)(ui_1.InfoBox, { icon: lucide_react_native_1.Car, label: "Bitrate", value: displayBitrate, status: getBitrateStatus() }), (0, jsx_runtime_1.jsx)(ui_1.InfoBox, { icon: lucide_react_native_1.Monitor, label: "Resolution", value: displayResolution }), (0, jsx_runtime_1.jsx)(ui_1.InfoBox, { icon: lucide_react_native_1.Video, label: "FPS", value: displayFps })] }), showChart && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [gap.all[2]], children: [!isCompactHeight && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
139
+ layout.flex.row,
140
+ layout.flex.spaceBetween,
141
+ layout.flex.alignCenter,
142
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
143
+ text.gray[200],
144
+ { fontSize: 14, fontWeight: "600" },
145
+ ], children: "Live Performance" }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, gap.all[4]], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.alignCenter], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
146
+ text.gray[400],
147
+ { fontSize: 11, fontWeight: "500" },
148
+ ], children: "AVG" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
149
+ text.white,
150
+ { fontSize: 13, fontWeight: "600" },
151
+ ], children: avgBitrate > 0
152
+ ? `${(avgBitrate / 1000).toFixed(1)}M`
153
+ : "0M" })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.alignCenter], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
154
+ text.gray[400],
155
+ { fontSize: 11, fontWeight: "500" },
156
+ ], children: "PEAK" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
157
+ text.white,
158
+ { fontSize: 13, fontWeight: "600" },
159
+ ], children: peakBitrate > 0
160
+ ? `${(peakBitrate / 1000).toFixed(1)}M`
161
+ : "0M" })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.alignCenter], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
162
+ text.gray[400],
163
+ { fontSize: 11, fontWeight: "500" },
164
+ ], children: "CAPTURED" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
165
+ text.white,
166
+ { fontSize: 13, fontWeight: "600" },
167
+ ], children: uptimeMinutes > 60
168
+ ? `${Math.floor(uptimeMinutes / 60)}h ${uptimeMinutes % 60}m`
169
+ : `${uptimeMinutes}m` })] })] })] })), (0, jsx_runtime_1.jsx)(BitrateChart, { data: bitrateHistory, width: componentWidth - 40, height: 120 })] }))] })) : ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [gap.all[3]], children: [!isCompactHeight && ((0, jsx_runtime_1.jsxs)(react_native_1.TouchableOpacity, { onPress: () => setShowViewers(!showViewers), style: [
170
+ layout.flex.row,
171
+ layout.flex.spaceBetween,
172
+ layout.flex.alignCenter,
173
+ py[2],
174
+ ], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[3]], children: [(0, jsx_runtime_1.jsx)(lucide_react_native_1.Eye, { size: 16, color: "#9ca3af" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [text.gray[300], { fontSize: 13, fontWeight: "500" }], children: "Viewers" })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
175
+ showViewers ? text.green[400] : text.white,
176
+ { fontSize: 13, fontWeight: "600" },
177
+ ], children: showViewers ? `${viewerCount} watching` : "•••" }), showViewers ? ((0, jsx_runtime_1.jsx)(lucide_react_native_1.ChevronUp, { size: 14, color: "#9ca3af" })) : ((0, jsx_runtime_1.jsx)(lucide_react_native_1.ChevronDown, { size: 14, color: "#9ca3af" }))] })] })), showChart && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [gap.all[2]], children: [!isCompactHeight && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
178
+ layout.flex.row,
179
+ layout.flex.spaceBetween,
180
+ layout.flex.alignCenter,
181
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
182
+ text.gray[200],
183
+ { fontSize: 14, fontWeight: "600" },
184
+ ], children: "Performance" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
185
+ text.gray[400],
186
+ { fontSize: 11, fontWeight: "500" },
187
+ ], children: avgBitrate > 0
188
+ ? `AVG ${(avgBitrate / 1000).toFixed(1)}M`
189
+ : "No data" })] })), (0, jsx_runtime_1.jsx)(BitrateChart, { data: bitrateHistory, width: componentWidth - 40, height: isCompactHeight ? 80 : 120 })] })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [gap.all[1]], children: [(0, jsx_runtime_1.jsx)(ui_1.InfoRow, { icon: lucide_react_native_1.Signal, label: "Connection", value: getConnectionStatus() === "good"
190
+ ? "Excellent"
191
+ : getConnectionStatus() === "warning"
192
+ ? "Good"
193
+ : "Poor", status: getConnectionStatus() }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [gap.all[1], layout.flex.row], children: [(0, jsx_runtime_1.jsx)(ui_1.InfoBox, { icon: lucide_react_native_1.Zap, label: "Bitrate", value: displayBitrate, status: getBitrateStatus() }), (0, jsx_runtime_1.jsx)(ui_1.InfoBox, { icon: lucide_react_native_1.Video, label: "FPS", value: displayFps })] })] })] }))] }));
194
+ }
195
+ function BitrateChart({ data, width, height, }) {
196
+ const maxDataValue = Math.max(...data, 1);
197
+ const minDataValue = Math.min(...data);
198
+ const getSmartRange = (max) => {
199
+ if (max <= 1000)
200
+ return { min: 0, max: 1000, step: 500 };
201
+ if (max <= 2000)
202
+ return { min: 1000, max: 2000, step: 1000 };
203
+ if (max <= 7000)
204
+ return { min: 4000, max: 7000, step: 1500 };
205
+ if (max <= 10000)
206
+ return { min: 4000, max: 10000, step: 5000 };
207
+ const roundedMax = Math.ceil(max / 5000) * 5000;
208
+ return { min: 0, max: roundedMax, step: roundedMax / 2 };
209
+ };
210
+ const { min: minValue, max: maxValue, step } = getSmartRange(maxDataValue);
211
+ const range = maxValue - minValue;
212
+ const chartWidth = width - 40;
213
+ const chartStartX = 40;
214
+ const verticalPadding = 10;
215
+ const chartHeight = height - verticalPadding * 2;
216
+ const points = data
217
+ .map((value, index) => {
218
+ const x = chartStartX + (index / (data.length - 1)) * chartWidth;
219
+ // Clamp value to chart range and plot against the smart scale
220
+ const clampedValue = Math.max(minValue, Math.min(maxValue, value));
221
+ const y = verticalPadding +
222
+ chartHeight -
223
+ ((clampedValue - minValue) / range) * chartHeight;
224
+ return `${x},${y}`;
225
+ })
226
+ .join(" ");
227
+ const pathData = `M ${points.replace(/ /g, " L ")}`;
228
+ const ticks = [
229
+ { value: minValue, y: height - verticalPadding },
230
+ { value: minValue + step, y: height / 2 },
231
+ { value: maxValue, y: verticalPadding },
232
+ ];
233
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { height, width, marginVertical: 8 }, children: (0, jsx_runtime_1.jsxs)(react_native_svg_1.default, { width: width, height: height, children: [ticks.map((tick, index) => ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_svg_1.Line, { x1: chartStartX, y1: tick.y, x2: width, y2: tick.y, stroke: "rgba(255,255,255,0.1)", strokeWidth: "1" }), (0, jsx_runtime_1.jsx)(react_native_svg_1.Text, { x: "35", y: tick.y + 4, fontSize: 10, fontFamily: "sans-serif", fill: "#9ca3af", textAnchor: "end", children: (tick.value / 1000).toLocaleString() })] }, index))), (0, jsx_runtime_1.jsx)(react_native_svg_1.Text, { x: 12, y: height / 2, transform: `rotate(-90, 12, ${height / 2})`, fontSize: 10, fontFamily: "sans-serif", fill: "#9ca3af", textAnchor: "middle", children: "mbits/s" }), (0, jsx_runtime_1.jsx)(react_native_svg_1.Path, { d: pathData, stroke: "#22c55e", strokeWidth: "2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })] }) }));
234
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = ModActions;
4
+ const tslib_1 = require("tslib");
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const lucide_react_native_1 = require("lucide-react-native");
7
+ const react_native_1 = require("react-native");
8
+ const zero = tslib_1.__importStar(require("../../ui"));
9
+ const { flex, bg, r, borders, p, text, layout, gap, mb } = zero;
10
+ const defaultActions = [
11
+ {
12
+ icon: lucide_react_native_1.Shield,
13
+ label: "Ban User",
14
+ color: "red",
15
+ action: () => console.log("Ban user action"),
16
+ },
17
+ {
18
+ icon: lucide_react_native_1.MessageCircle,
19
+ label: "Timeout",
20
+ color: "yellow",
21
+ action: () => console.log("Timeout user action"),
22
+ },
23
+ {
24
+ icon: lucide_react_native_1.Eye,
25
+ label: "Monitor",
26
+ color: "blue",
27
+ action: () => console.log("Monitor stream action"),
28
+ },
29
+ {
30
+ icon: lucide_react_native_1.AlertTriangle,
31
+ label: "Report",
32
+ color: "orange",
33
+ action: () => console.log("Report content action"),
34
+ },
35
+ ];
36
+ function ModActions({ isLive, isConnected, messageCount = 0, actions = defaultActions, }) {
37
+ const canModerate = isLive && isConnected;
38
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
39
+ flex.values[1],
40
+ bg.gray[800],
41
+ r[3],
42
+ borders.width.thin,
43
+ borders.color.gray[700],
44
+ p[4],
45
+ ], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
46
+ layout.flex.row,
47
+ layout.flex.spaceBetween,
48
+ layout.flex.alignCenter,
49
+ mb[4],
50
+ ], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [text.white, { fontSize: 18, fontWeight: "600" }], children: "Moderation" }), (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: [text.gray[400], { fontSize: 12 }], children: [messageCount, " messages"] })] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [layout.flex.row, gap.all[3]], children: actions.map((action, index) => ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { style: [
51
+ flex.grow[1],
52
+ bg.gray[700],
53
+ r[2],
54
+ p[3],
55
+ layout.flex.row,
56
+ layout.flex.alignCenter,
57
+ gap.all[2],
58
+ borders.width.thin,
59
+ borders.color.gray[600],
60
+ ], disabled: !canModerate, onPress: action.action, children: [(0, jsx_runtime_1.jsx)(action.icon, { size: 20, color: canModerate ? "#ffffff" : "#6b7280" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
61
+ canModerate ? text.white : text.gray[400],
62
+ { fontSize: 14, fontWeight: "500" },
63
+ ], children: action.label })] }, index))) }), !canModerate && ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
64
+ text.gray[500],
65
+ { fontSize: 12, textAlign: "center", marginTop: 16 },
66
+ ], children: !isLive
67
+ ? "Moderation tools available when live"
68
+ : !isConnected
69
+ ? "Waiting for stream connection..."
70
+ : "Moderation tools unavailable" }))] }));
71
+ }