@streamplace/components 0.6.37 → 0.7.1

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 (142) hide show
  1. package/dist/components/chat/chat-box.js +109 -0
  2. package/dist/components/chat/chat-message.js +76 -0
  3. package/dist/components/chat/chat.js +56 -0
  4. package/dist/components/chat/mention-suggestions.js +39 -0
  5. package/dist/components/chat/mod-view.js +33 -0
  6. package/dist/components/mobile-player/fullscreen.js +69 -0
  7. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  8. package/dist/components/mobile-player/player.js +103 -0
  9. package/dist/components/mobile-player/props.js +1 -0
  10. package/dist/components/mobile-player/shared.js +51 -0
  11. package/dist/components/mobile-player/ui/countdown.js +79 -0
  12. package/dist/components/mobile-player/ui/index.js +6 -0
  13. package/dist/components/mobile-player/ui/input.js +38 -0
  14. package/dist/components/mobile-player/ui/metrics.js +40 -0
  15. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  17. package/dist/components/mobile-player/ui/viewers.js +19 -0
  18. package/dist/components/mobile-player/use-webrtc.js +232 -0
  19. package/dist/components/mobile-player/video.js +375 -0
  20. package/dist/components/mobile-player/video.native.js +255 -0
  21. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  22. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  23. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  24. package/dist/components/ui/button.js +220 -0
  25. package/dist/components/ui/dialog.js +203 -0
  26. package/dist/components/ui/dropdown.js +148 -0
  27. package/dist/components/ui/icons.js +22 -0
  28. package/dist/components/ui/index.js +22 -0
  29. package/dist/components/ui/input.js +202 -0
  30. package/dist/components/ui/loader.js +7 -0
  31. package/dist/components/ui/primitives/button.js +121 -0
  32. package/dist/components/ui/primitives/input.js +202 -0
  33. package/dist/components/ui/primitives/modal.js +203 -0
  34. package/dist/components/ui/primitives/text.js +286 -0
  35. package/dist/components/ui/resizeable.js +108 -0
  36. package/dist/components/ui/text.js +175 -0
  37. package/dist/components/ui/textarea.js +17 -0
  38. package/dist/components/ui/toast.js +129 -0
  39. package/dist/components/ui/view.js +250 -0
  40. package/dist/hooks/index.js +10 -0
  41. package/dist/hooks/useAvatars.js +32 -0
  42. package/dist/hooks/useCameraToggle.js +9 -0
  43. package/dist/hooks/useKeyboard.js +33 -0
  44. package/dist/hooks/useKeyboardSlide.js +11 -0
  45. package/dist/hooks/useLivestreamInfo.js +62 -0
  46. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  47. package/dist/hooks/usePlayerDimensions.js +19 -0
  48. package/dist/hooks/useSegmentDimensions.js +14 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +10 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +8 -3
  59. package/dist/livestream-store/chat.js +89 -65
  60. package/dist/livestream-store/index.js +1 -0
  61. package/dist/livestream-store/livestream-store.js +3 -0
  62. package/dist/livestream-store/stream-key.js +115 -0
  63. package/dist/player-store/player-provider.js +0 -1
  64. package/dist/player-store/player-store.js +13 -0
  65. package/dist/streamplace-store/block.js +23 -0
  66. package/dist/streamplace-store/index.js +1 -0
  67. package/dist/streamplace-store/stream.js +193 -0
  68. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  69. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  70. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  71. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  72. package/package.json +21 -5
  73. package/src/components/chat/chat-box.tsx +195 -0
  74. package/src/components/chat/chat-message.tsx +192 -0
  75. package/src/components/chat/chat.tsx +128 -0
  76. package/src/components/chat/mention-suggestions.tsx +71 -0
  77. package/src/components/chat/mod-view.tsx +118 -0
  78. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  79. package/src/components/mobile-player/fullscreen.tsx +79 -0
  80. package/src/components/mobile-player/player.tsx +134 -0
  81. package/src/components/mobile-player/props.tsx +11 -0
  82. package/src/components/mobile-player/shared.tsx +56 -0
  83. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  84. package/src/components/mobile-player/ui/index.ts +6 -0
  85. package/src/components/mobile-player/ui/input.tsx +85 -0
  86. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  87. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  88. package/src/components/mobile-player/ui/viewer-context-menu.tsx +71 -0
  89. package/src/components/mobile-player/ui/viewers.tsx +32 -0
  90. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  91. package/src/components/mobile-player/video.native.tsx +405 -0
  92. package/src/components/mobile-player/video.tsx +557 -0
  93. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  94. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  95. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  96. package/src/components/ui/button.tsx +309 -0
  97. package/src/components/ui/dialog.tsx +376 -0
  98. package/src/components/ui/dropdown.tsx +399 -0
  99. package/src/components/ui/icons.tsx +50 -0
  100. package/src/components/ui/index.ts +33 -0
  101. package/src/components/ui/input.tsx +350 -0
  102. package/src/components/ui/loader.tsx +9 -0
  103. package/src/components/ui/primitives/button.tsx +292 -0
  104. package/src/components/ui/primitives/input.tsx +422 -0
  105. package/src/components/ui/primitives/modal.tsx +421 -0
  106. package/src/components/ui/primitives/text.tsx +499 -0
  107. package/src/components/ui/resizeable.tsx +181 -0
  108. package/src/components/ui/text.tsx +330 -0
  109. package/src/components/ui/textarea.tsx +34 -0
  110. package/src/components/ui/toast.tsx +203 -0
  111. package/src/components/ui/view.tsx +344 -0
  112. package/src/hooks/index.ts +10 -0
  113. package/src/hooks/useAvatars.tsx +44 -0
  114. package/src/hooks/useCameraToggle.ts +12 -0
  115. package/src/hooks/useKeyboard.tsx +41 -0
  116. package/src/hooks/useKeyboardSlide.ts +12 -0
  117. package/src/hooks/useLivestreamInfo.ts +67 -0
  118. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  119. package/src/hooks/usePlayerDimensions.ts +23 -0
  120. package/src/hooks/useSegmentDimensions.tsx +18 -0
  121. package/src/hooks/useSegmentTiming.tsx +88 -0
  122. package/src/index.tsx +21 -0
  123. package/src/lib/facet.ts +131 -0
  124. package/src/lib/theme/atoms.ts +760 -0
  125. package/src/lib/theme/atoms.types.ts +258 -0
  126. package/src/lib/theme/index.ts +48 -0
  127. package/src/lib/theme/theme.tsx +436 -0
  128. package/src/lib/theme/tokens.ts +409 -0
  129. package/src/lib/utils.ts +132 -0
  130. package/src/livestream-provider/index.tsx +13 -2
  131. package/src/livestream-store/chat.tsx +115 -78
  132. package/src/livestream-store/index.tsx +1 -0
  133. package/src/livestream-store/livestream-state.tsx +3 -0
  134. package/src/livestream-store/livestream-store.tsx +3 -0
  135. package/src/livestream-store/stream-key.tsx +124 -0
  136. package/src/player-store/player-provider.tsx +0 -1
  137. package/src/player-store/player-state.tsx +28 -0
  138. package/src/player-store/player-store.tsx +22 -0
  139. package/src/streamplace-store/block.tsx +29 -0
  140. package/src/streamplace-store/index.tsx +1 -0
  141. package/src/streamplace-store/stream.tsx +262 -0
  142. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { X } from "lucide-react-native";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { Pressable } from "react-native";
5
+ import { Button, Loader, Text, useChat, useCreateChatMessage, useReplyToMessage, useSetReplyToMessage, View, } from "../../";
6
+ import { bg, flex, gap, h, layout, mb, pl, pr, w } from "../../lib/theme/atoms";
7
+ import { usePDSAgent } from "../../streamplace-store/xrpc";
8
+ import { Textarea } from "../ui/textarea";
9
+ import { RenderChatMessage } from "./chat-message";
10
+ import { MentionSuggestions } from "./mention-suggestions";
11
+ export function ChatBox({ isPopout, chatBoxStyle, }) {
12
+ const [submitting, setSubmitting] = useState(false);
13
+ const [message, setMessage] = useState("");
14
+ const [showSuggestions, setShowSuggestions] = useState(false);
15
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
16
+ const [filteredAuthors, setFilteredAuthors] = useState(new Map());
17
+ const chat = useChat();
18
+ const createChatMessage = useCreateChatMessage();
19
+ const replyTo = useReplyToMessage();
20
+ const setReplyToMessage = useSetReplyToMessage();
21
+ const textAreaRef = useRef(null);
22
+ // are we logged in?
23
+ let agent = usePDSAgent();
24
+ if (!agent?.did) {
25
+ _jsx(View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: _jsx(Text, { children: "Log in to chat." }) });
26
+ }
27
+ const authors = useMemo(() => {
28
+ if (!chat)
29
+ return null;
30
+ return chat.reduce((acc, msg) => {
31
+ acc.set(msg.author.handle, msg.chatProfile);
32
+ return acc;
33
+ }, new Map());
34
+ }, [chat]);
35
+ const handleMentionSelect = (handle) => {
36
+ const beforeAt = message.slice(0, message.lastIndexOf("@"));
37
+ setMessage(`${beforeAt}@${handle} `);
38
+ setShowSuggestions(false);
39
+ };
40
+ const updateSuggestions = (text) => {
41
+ const atIndex = text.lastIndexOf("@");
42
+ if (atIndex === -1 || !authors) {
43
+ setShowSuggestions(false);
44
+ return;
45
+ }
46
+ const searchText = text.slice(atIndex + 1).toLowerCase();
47
+ const filteredAuthorsMap = new Map(Array.from(authors.entries()).filter(([handle]) => handle.toLowerCase().includes(searchText)));
48
+ setFilteredAuthors(filteredAuthorsMap);
49
+ setHighlightedIndex(0);
50
+ setShowSuggestions(filteredAuthorsMap.size > 0);
51
+ };
52
+ const submit = () => {
53
+ if (!message.trim())
54
+ return;
55
+ setMessage("");
56
+ setReplyToMessage(null);
57
+ setSubmitting(true);
58
+ createChatMessage({
59
+ text: message,
60
+ reply: replyTo || undefined,
61
+ });
62
+ setSubmitting(false);
63
+ };
64
+ useEffect(() => {
65
+ if (replyTo && textAreaRef.current) {
66
+ textAreaRef.current.focus();
67
+ }
68
+ }, [replyTo]);
69
+ return (_jsxs(View, { style: [layout.flex.column, flex.shrink[1], gap.all[2]], children: [replyTo && (_jsxs(View, { style: [
70
+ layout.flex.row,
71
+ layout.flex.alignCenter,
72
+ layout.flex.spaceBetween,
73
+ h[12],
74
+ pl[2],
75
+ pr[10],
76
+ mb[2],
77
+ bg.gray[800],
78
+ { borderRadius: 16 },
79
+ ], children: [_jsx(RenderChatMessage, { item: replyTo, showReply: false, userCache: authors || new Map() }), _jsx(Pressable, { onPress: () => setReplyToMessage(null), children: _jsx(View, { style: [
80
+ layout.flex.row,
81
+ layout.flex.alignCenter,
82
+ layout.flex.justifyCenter,
83
+ h[12],
84
+ w[12],
85
+ bg.gray[600],
86
+ { borderRadius: 999 },
87
+ ], children: _jsx(X, { size: 24 }) }) })] })), _jsxs(View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: [_jsx(Textarea, { ref: textAreaRef, numberOfLines: 1, value: message, enterKeyHint: "send", onSubmitEditing: submit, multiline: false, onChangeText: (text) => {
88
+ setMessage(text);
89
+ updateSuggestions(text);
90
+ }, onKeyPress: (k) => {
91
+ if (k.nativeEvent.key === "Enter") {
92
+ if (showSuggestions) {
93
+ k.preventDefault();
94
+ const handles = Array.from(filteredAuthors.keys());
95
+ if (handles.length > 0) {
96
+ handleMentionSelect(handles[highlightedIndex]);
97
+ }
98
+ }
99
+ else
100
+ submit();
101
+ }
102
+ else if (k.nativeEvent.key === "ArrowUp") {
103
+ setHighlightedIndex((prev) => Math.max(prev - 1, 0));
104
+ }
105
+ else if (k.nativeEvent.key === "ArrowDown") {
106
+ setHighlightedIndex((prev) => Math.min(prev + 1, Array.from(filteredAuthors.keys()).length - 1));
107
+ }
108
+ }, style: [chatBoxStyle] }), _jsx(Button, { disabled: submitting, variant: "secondary", style: { borderRadius: 16, height: 36, minWidth: 80 }, onPress: submit, children: submitting ? _jsx(Loader, {}) : "Send" })] }), showSuggestions && (_jsx(MentionSuggestions, { authors: filteredAuthors || [], highlightedIndex: highlightedIndex, onSelect: handleMentionSelect }))] }));
109
+ }
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { memo, useCallback } from "react";
3
+ import { Linking, View } from "react-native";
4
+ import { segmentize } from "../../lib/facet";
5
+ import { borders, flex, gap, ml, mr, opacity, pl, w, } from "../../lib/theme/atoms";
6
+ import { atoms, layout } from "../ui";
7
+ import { useLivestreamStore } from "../../livestream-store";
8
+ import { Text } from "../ui/text";
9
+ const getRgbColor = (color) => color ? `rgb(${color.red}, ${color.green}, ${color.blue})` : "$accentColor";
10
+ const segmentedObject = (obj, index, userCache) => {
11
+ if (obj.features && obj.features.length > 0) {
12
+ let ftr = obj.features[0];
13
+ // afaik there shouldn't be a case where facets overlap, at least currently
14
+ if (ftr.$type === "app.bsky.richtext.facet#link") {
15
+ let linkftr = ftr;
16
+ return (_jsx(Text, { style: [{ color: atoms.colors.ios.systemBlue, cursor: "pointer" }], onPress: () => Linking.openURL(linkftr.uri || ""), children: obj.text }, `mention-${index}`));
17
+ }
18
+ else if (ftr.$type === "app.bsky.richtext.facet#mention") {
19
+ let mtnftr = ftr;
20
+ const profile = userCache?.[mtnftr.did];
21
+ console.log(profile, mtnftr.did, userCache);
22
+ return (_jsx(Text, { style: [
23
+ {
24
+ cursor: "pointer",
25
+ color: getRgbColor(profile?.color),
26
+ },
27
+ ], onPress: () => Linking.openURL(`https://bsky.app/profile/${mtnftr.did || ""}`), children: obj.text }, `mention-${index}`));
28
+ }
29
+ }
30
+ else {
31
+ return _jsx(Text, { children: obj.text }, `text-${index}`);
32
+ }
33
+ };
34
+ const RichTextMessage = ({ text, facets, }) => {
35
+ if (!facets?.length)
36
+ return _jsx(Text, { children: text });
37
+ const userCache = useLivestreamStore((state) => state.authors);
38
+ let segs = segmentize(text, facets);
39
+ return segs.map((seg, i) => segmentedObject(seg, i, userCache));
40
+ };
41
+ export const RenderChatMessage = memo(function RenderChatMessage({ item, showReply = true, showTime = true, }) {
42
+ const formatTime = useCallback((dateString) => {
43
+ return new Date(dateString).toLocaleString(undefined, {
44
+ hour: "2-digit",
45
+ minute: "2-digit",
46
+ hour12: false,
47
+ });
48
+ }, []);
49
+ return (_jsxs(_Fragment, { children: [item.replyTo && showReply && (_jsx(View, { style: [
50
+ gap.all[2],
51
+ layout.flex.row,
52
+ w.percent[100],
53
+ borders.left.width.medium,
54
+ borders.left.color.gray[700],
55
+ ml[4],
56
+ pl[4],
57
+ opacity[80],
58
+ ], children: _jsxs(Text, { numberOfLines: 1, style: [flex.shrink[1], mr[4]], children: [_jsxs(Text, { style: {
59
+ color: getRgbColor(item.replyTo.chatProfile.color),
60
+ fontWeight: "thin",
61
+ }, children: ["@", item.replyTo.author.handle] }), " ", _jsx(Text, { style: {
62
+ color: atoms.colors.gray[300],
63
+ fontStyle: "italic",
64
+ }, children: item.replyTo.record.text })] }) })), _jsxs(View, { style: [gap.all[2], layout.flex.row, w.percent[100]], children: [showTime && (_jsx(Text, { style: {
65
+ fontVariant: ["tabular-nums"],
66
+ color: atoms.colors.gray[300],
67
+ }, children: formatTime(item.record.createdAt) })), _jsxs(Text, { weight: "bold", color: "default", style: [flex.shrink[1]], children: [_jsxs(Text, { style: [
68
+ {
69
+ cursor: "pointer",
70
+ color: getRgbColor(item.chatProfile?.color),
71
+ },
72
+ ], children: ["@", item.author.handle] }), ":", " ", _jsx(RichTextMessage, { text: item.record.text, facets: item.record.facets || [] })] })] })] }));
73
+ }, (prevProps, nextProps) => {
74
+ return (prevProps.item.author.handle === nextProps.item.author.handle &&
75
+ prevProps.item.record.text === nextProps.item.record.text);
76
+ });
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Reply, ShieldEllipsis } from "lucide-react-native";
3
+ import { memo, useRef } from "react";
4
+ import { FlatList, Platform, Pressable } from "react-native";
5
+ import Swipeable from "react-native-gesture-handler/ReanimatedSwipeable";
6
+ import Reanimated, { useAnimatedStyle, } from "react-native-reanimated";
7
+ import { Text, useChat, usePlayerStore, useSetReplyToMessage, View, } from "../../";
8
+ import { flex, py, w } from "../../lib/theme/atoms";
9
+ import { RenderChatMessage } from "./chat-message";
10
+ import { ModView } from "./mod-view";
11
+ function RightAction(prog, drag) {
12
+ const styleAnimation = useAnimatedStyle(() => {
13
+ return {
14
+ transform: [{ translateX: drag.value + 25 }],
15
+ };
16
+ });
17
+ return (_jsx(Reanimated.View, { style: [styleAnimation], children: _jsx(Reply, { color: "white" }) }));
18
+ }
19
+ function LeftAction(prog, drag) {
20
+ const styleAnimation = useAnimatedStyle(() => {
21
+ return {
22
+ transform: [{ translateX: drag.value - 25 }],
23
+ };
24
+ });
25
+ return (_jsx(Reanimated.View, { style: [styleAnimation], children: _jsx(ShieldEllipsis, { color: "white" }) }));
26
+ }
27
+ const SHOWN_MSGS = Platform.OS === "android" || Platform.OS === "ios" ? 100 : 25;
28
+ const keyExtractor = (item, index) => {
29
+ return `${item.uri}`;
30
+ };
31
+ const ChatLine = memo(({ item }) => {
32
+ const setReply = useSetReplyToMessage();
33
+ const setModMsg = usePlayerStore((state) => state.setModMessage);
34
+ const swipeableRef = useRef(null);
35
+ return (_jsx(Pressable, { onLongPress: () => setModMsg(item), children: _jsx(Swipeable, { containerStyle: [py[1]], friction: 2, enableTrackpadTwoFingerGesture: true, rightThreshold: 40, renderRightActions: Platform.OS === "android" ? undefined : RightAction, renderLeftActions: Platform.OS === "android" ? undefined : LeftAction, overshootFriction: 9, ref: (ref) => {
36
+ swipeableRef.current = ref;
37
+ }, onSwipeableOpen: (r) => {
38
+ if (r === (Platform.OS === "android" ? "right" : "left")) {
39
+ setReply(item);
40
+ }
41
+ if (r === (Platform.OS === "android" ? "left" : "right")) {
42
+ setModMsg(item);
43
+ }
44
+ // close this swipeable
45
+ const swipeable = swipeableRef.current;
46
+ if (swipeable) {
47
+ swipeable.close();
48
+ }
49
+ }, children: _jsx(RenderChatMessage, { item: item }) }) }));
50
+ });
51
+ export function Chat({ shownMessages = SHOWN_MSGS, style: propsStyle, ...props }) {
52
+ const chat = useChat();
53
+ if (!chat)
54
+ return (_jsx(View, { style: [flex.shrink[1]], children: _jsx(Text, { children: "Loading chaat..." }) }));
55
+ return (_jsxs(View, { style: [flex.shrink[1]].concat(propsStyle || []), children: [_jsx(FlatList, { style: [flex.grow[1], flex.shrink[1], w.percent[100]], data: chat, inverted: true, keyExtractor: keyExtractor, renderItem: ({ item, index }) => _jsx(ChatLine, { item: item }), removeClippedSubviews: true, maxToRenderPerBatch: 10, initialNumToRender: 10, updateCellsBatchingPeriod: 50 }), _jsx(ModView, {})] }));
56
+ }
@@ -0,0 +1,39 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Pressable } from "react-native";
3
+ import { Text, View } from "../..";
4
+ import { bg, layout, left, right, zIndex } from "../../lib/theme/atoms";
5
+ export function MentionSuggestions({ authors, onSelect, highlightedIndex, }) {
6
+ if (!authors || authors.size === 0) {
7
+ return null; // No authors to display
8
+ }
9
+ const authorHandles = Array.from(authors.keys());
10
+ return (_jsx(View, { style: [
11
+ bg.gray[800],
12
+ layout.position.absolute,
13
+ left[0],
14
+ right[0],
15
+ zIndex[50],
16
+ {
17
+ bottom: "100%",
18
+ borderRadius: 8,
19
+ boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
20
+ },
21
+ ], children: authorHandles.map((handle, index) => {
22
+ let profile = authors.get(handle);
23
+ return (_jsx(Pressable, { onPress: () => onSelect(handle), style: [
24
+ {
25
+ padding: 8,
26
+ flexDirection: "row",
27
+ alignItems: "center",
28
+ backgroundColor: index === highlightedIndex
29
+ ? "rgba(0, 0, 0, 0.1)"
30
+ : "rgba(0, 0, 0, 0.5)",
31
+ },
32
+ ], children: _jsxs(Text, { style: {
33
+ color: profile?.color
34
+ ? `rgb(${profile.color.red}, ${profile.color.green}, ${profile.color.blue})`
35
+ : "black",
36
+ fontWeight: "bold",
37
+ }, children: ["@", handle] }) }, handle));
38
+ }) }));
39
+ }
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useRef } from "react";
3
+ import { gap, mr } from "../../lib/theme/atoms";
4
+ import { usePlayerStore } from "../../player-store";
5
+ import { useCreateBlockRecord } from "../../streamplace-store/block";
6
+ import { usePDSAgent } from "../../streamplace-store/xrpc";
7
+ import { DropdownMenu, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger, layout, ResponsiveDropdownMenuContent, Text, View, } from "../ui";
8
+ import { RenderChatMessage } from "./chat-message";
9
+ export const ModView = forwardRef(() => {
10
+ const triggerRef = useRef(null);
11
+ const message = usePlayerStore((state) => state.modMessage);
12
+ let agent = usePDSAgent();
13
+ let createBlockRecord = useCreateBlockRecord();
14
+ if (!agent?.did) {
15
+ _jsx(View, { style: [layout.flex.row, layout.flex.alignCenter, gap.all[2]], children: _jsx(Text, { children: "Log in to submit mod actions" }) });
16
+ }
17
+ useEffect(() => {
18
+ if (message) {
19
+ console.log("opening mod view");
20
+ triggerRef.current?.open();
21
+ }
22
+ else {
23
+ console.log("closing mod view");
24
+ triggerRef.current?.close();
25
+ }
26
+ }, [message]);
27
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { ref: triggerRef, children: _jsx(View, {}) }), _jsx(ResponsiveDropdownMenuContent, { children: message && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuGroup, { children: _jsx(DropdownMenuItem, { children: _jsx(View, { style: [layout.flex.column, mr[5], { gap: 6 }], children: _jsx(RenderChatMessage, { item: message }) }) }) }), _jsx(DropdownMenuGroup, { title: `Moderation actions`, children: _jsx(DropdownMenuItem, { disabled: message.author.did === agent?.did, onPress: () => {
28
+ console.log("Creating block record");
29
+ createBlockRecord(message.author.did)
30
+ .then((r) => console.log(r))
31
+ .catch((e) => console.error(e));
32
+ }, children: _jsx(Text, { color: "destructive", children: message.author.did === agent?.did ? (_jsx(_Fragment, { children: "Block yourself (you can't block yourself)" })) : (_jsxs(_Fragment, { children: ["Block user @", message.author.handle, " from this channel"] })) }) }) })] })) })] }));
33
+ });
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { getFirstPlayerID, usePlayerStore } from "../..";
4
+ import { View } from "../../components/ui";
5
+ import Video from "./video";
6
+ export function Fullscreen(props) {
7
+ const playerId = getFirstPlayerID();
8
+ const protocol = usePlayerStore((x) => x.protocol, playerId);
9
+ const fullscreen = usePlayerStore((x) => x.fullscreen, playerId);
10
+ const setFullscreen = usePlayerStore((x) => x.setFullscreen, playerId);
11
+ const setSrc = usePlayerStore((x) => x.setSrc);
12
+ const divRef = useRef(null);
13
+ const videoRef = useRef(null);
14
+ useEffect(() => {
15
+ setSrc(props.src);
16
+ }, [props.src]);
17
+ useEffect(() => {
18
+ if (!divRef.current) {
19
+ return;
20
+ }
21
+ (async () => {
22
+ if (fullscreen && !document.fullscreenElement) {
23
+ try {
24
+ const div = divRef.current;
25
+ if (typeof div.requestFullscreen === "function") {
26
+ await div.requestFullscreen();
27
+ }
28
+ else if (videoRef.current) {
29
+ if (typeof videoRef.current.webkitEnterFullscreen ===
30
+ "function") {
31
+ await videoRef.current.webkitEnterFullscreen();
32
+ }
33
+ else if (typeof videoRef.current.requestFullscreen === "function") {
34
+ await videoRef.current.requestFullscreen();
35
+ }
36
+ }
37
+ setFullscreen(true);
38
+ }
39
+ catch (e) {
40
+ console.error("fullscreen failed", e.message);
41
+ }
42
+ }
43
+ if (!fullscreen) {
44
+ if (document.fullscreenElement) {
45
+ try {
46
+ await document.exitFullscreen();
47
+ }
48
+ catch (e) {
49
+ console.error("fullscreen exit failed", e.message);
50
+ }
51
+ }
52
+ setFullscreen(false);
53
+ }
54
+ })();
55
+ }, [fullscreen, protocol]);
56
+ useEffect(() => {
57
+ const listener = () => {
58
+ console.log("fullscreenchange", document.fullscreenElement);
59
+ setFullscreen(!!document.fullscreenElement);
60
+ };
61
+ document.body.addEventListener("fullscreenchange", listener);
62
+ document.body.addEventListener("webkitfullscreenchange", listener);
63
+ return () => {
64
+ document.body.removeEventListener("fullscreenchange", listener);
65
+ document.body.removeEventListener("webkitfullscreenchange", listener);
66
+ };
67
+ }, []);
68
+ return (_jsx(View, { ref: divRef, children: _jsx(Video, {}) }));
69
+ }
@@ -0,0 +1,151 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useNavigation } from "@react-navigation/native";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { BackHandler, Dimensions, StyleSheet, View } from "react-native";
5
+ import { SystemBars } from "react-native-edge-to-edge";
6
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
7
+ import { PlayerProtocol, useLivestreamStore, usePlayerStore } from "../..";
8
+ import Video from "./video.native";
9
+ // Standard 16:9 video aspect ratio
10
+ const VIDEO_ASPECT_RATIO = 16 / 9;
11
+ export function Fullscreen(props) {
12
+ const ref = useRef(null);
13
+ const insets = useSafeAreaInsets();
14
+ const navigation = useNavigation();
15
+ const [dimensions, setDimensions] = useState(Dimensions.get("window"));
16
+ // Get state from player store
17
+ const protocol = usePlayerStore((x) => x.protocol);
18
+ const fullscreen = usePlayerStore((x) => x.fullscreen);
19
+ const setFullscreen = usePlayerStore((x) => x.setFullscreen);
20
+ const handle = useLivestreamStore((x) => x.profile?.handle);
21
+ const setSrc = usePlayerStore((x) => x.setSrc);
22
+ useEffect(() => {
23
+ setSrc(props.src);
24
+ }, [props.src]);
25
+ // Re-calculate dimensions on orientation change
26
+ useEffect(() => {
27
+ const updateDimensions = () => {
28
+ setDimensions(Dimensions.get("window"));
29
+ };
30
+ const subscription = Dimensions.addEventListener("change", updateDimensions);
31
+ return () => {
32
+ subscription.remove();
33
+ };
34
+ }, []);
35
+ // Hide status bar when in fullscreen mode
36
+ useEffect(() => {
37
+ if (fullscreen) {
38
+ SystemBars.setHidden(true);
39
+ console.log("setting sidebar hidden");
40
+ // Hide the navigation header
41
+ navigation.setOptions({
42
+ headerShown: false,
43
+ });
44
+ // Handle hardware back button
45
+ const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
46
+ setFullscreen(false);
47
+ return true;
48
+ });
49
+ return () => {
50
+ backHandler.remove();
51
+ };
52
+ }
53
+ else {
54
+ SystemBars.setHidden(false);
55
+ // Restore the navigation header
56
+ navigation.setOptions({
57
+ headerShown: true,
58
+ });
59
+ }
60
+ return () => {
61
+ SystemBars.setHidden(false);
62
+ // Ensure header is restored if component unmounts
63
+ navigation.setOptions({
64
+ headerShown: true,
65
+ });
66
+ };
67
+ }, [fullscreen, navigation, setFullscreen]);
68
+ // Handle fullscreen state changes for native video players
69
+ useEffect(() => {
70
+ // For WebRTC, we handle fullscreen manually via the custom implementation
71
+ if (protocol === PlayerProtocol.WEBRTC) {
72
+ return;
73
+ }
74
+ // For HLS and other protocols, sync with native fullscreen
75
+ if (ref.current) {
76
+ if (fullscreen) {
77
+ ref.current.enterFullscreen();
78
+ }
79
+ else {
80
+ ref.current.exitFullscreen();
81
+ }
82
+ }
83
+ }, [fullscreen, protocol]);
84
+ if (fullscreen && protocol === PlayerProtocol.WEBRTC) {
85
+ // Determine if we're in landscape mode
86
+ const isLandscape = dimensions.width > dimensions.height;
87
+ // Calculate video container dimensions based on screen size and orientation
88
+ let videoWidth;
89
+ let videoHeight;
90
+ if (isLandscape) {
91
+ // In landscape, account for safe areas and use available height
92
+ const availableHeight = dimensions.height - (insets.top + insets.bottom);
93
+ const availableWidth = dimensions.width - (insets.left + insets.right);
94
+ videoHeight = availableHeight;
95
+ videoWidth = videoHeight * VIDEO_ASPECT_RATIO;
96
+ // If calculated width exceeds available width, constrain and maintain aspect ratio
97
+ if (videoWidth > availableWidth) {
98
+ videoWidth = availableWidth;
99
+ videoHeight = videoWidth / VIDEO_ASPECT_RATIO;
100
+ }
101
+ }
102
+ else {
103
+ // In portrait, account for safe areas
104
+ const availableWidth = dimensions.width - (insets.left + insets.right);
105
+ videoWidth = availableWidth;
106
+ videoHeight = videoWidth / VIDEO_ASPECT_RATIO;
107
+ }
108
+ // Calculate position to center the video, accounting for safe areas
109
+ const leftPosition = (dimensions.width - videoWidth) / 2;
110
+ const topPosition = (dimensions.height - videoHeight) / 2;
111
+ // When in custom fullscreen mode
112
+ return (_jsx(View, { style: [
113
+ styles.fullscreenContainer,
114
+ {
115
+ width: isLandscape ? dimensions.width + 40 : dimensions.width,
116
+ height: dimensions.height,
117
+ },
118
+ ], children: _jsx(View, { style: [
119
+ styles.videoContainer,
120
+ {
121
+ width: isLandscape ? videoWidth + 40 : videoWidth,
122
+ height: videoHeight,
123
+ left: leftPosition,
124
+ top: topPosition,
125
+ },
126
+ ], children: _jsx(Video, {}) }) }));
127
+ }
128
+ // Normal non-fullscreen mode
129
+ return (_jsx(_Fragment, { children: _jsx(Video, {}) }));
130
+ }
131
+ const styles = StyleSheet.create({
132
+ fullscreenContainer: {
133
+ position: "absolute",
134
+ top: 0,
135
+ left: 0,
136
+ right: 0,
137
+ bottom: 0,
138
+ backgroundColor: "#000",
139
+ zIndex: 9999,
140
+ elevation: 9999,
141
+ margin: 0,
142
+ padding: 0,
143
+ justifyContent: "center",
144
+ alignItems: "center",
145
+ },
146
+ videoContainer: {
147
+ position: "absolute",
148
+ backgroundColor: "#111",
149
+ overflow: "hidden",
150
+ },
151
+ });
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { flex, layout, w, zIndex } from "../../lib/theme/atoms";
4
+ import { useSegment } from "../../livestream-store";
5
+ import { PlayerStatus, usePlayerStore, } from "../../player-store";
6
+ import { useStreamplaceStore } from "../../streamplace-store";
7
+ import { Text, View } from "../ui";
8
+ import { Fullscreen } from "./fullscreen";
9
+ const OFFLINE_THRESHOLD = 10000;
10
+ export * as PlayerUI from "./ui";
11
+ export function Player(props) {
12
+ const playing = usePlayerStore((x) => x.status === PlayerStatus.PLAYING);
13
+ const setOffline = usePlayerStore((x) => x.setOffline);
14
+ const setIngest = usePlayerStore((x) => x.setIngestConnectionState);
15
+ const clearControlsTimeout = usePlayerStore((x) => x.clearControlsTimeout);
16
+ // Will call back every few seconds to send health updates
17
+ usePlayerStatus();
18
+ useEffect(() => {
19
+ setIngest(props.ingest ? "new" : null);
20
+ }, []);
21
+ if (typeof props.src !== "string") {
22
+ return (_jsx(View, { children: _jsx(Text, { children: "No source provided \uD83E\uDD37" }) }));
23
+ }
24
+ useEffect(() => {
25
+ return () => {
26
+ clearControlsTimeout();
27
+ };
28
+ }, []);
29
+ const segment = useSegment();
30
+ const [lastCheck, setLastCheck] = useState(0);
31
+ useEffect(() => {
32
+ if (playing) {
33
+ setOffline(false);
34
+ return;
35
+ }
36
+ if (!segment) {
37
+ setOffline(false);
38
+ return;
39
+ }
40
+ const startTime = Date.parse(segment.startTime);
41
+ if (!startTime) {
42
+ console.error("startTime is not a number", segment.startTime);
43
+ return;
44
+ }
45
+ const timeSinceStart = Date.now() - startTime;
46
+ if (timeSinceStart > OFFLINE_THRESHOLD) {
47
+ setOffline(true);
48
+ return;
49
+ }
50
+ const handle = setTimeout(() => {
51
+ setLastCheck(Date.now());
52
+ }, 1000);
53
+ return () => clearTimeout(handle);
54
+ }, [segment, playing, lastCheck]);
55
+ return (_jsx(_Fragment, { children: _jsx(View, { style: [zIndex[0], flex.values[1], w.percent[100], layout.flex.center], children: _jsx(Fullscreen, { src: props.src }) }) }));
56
+ }
57
+ const POLL_INTERVAL = 5000;
58
+ export function usePlayerStatus() {
59
+ const playerStatus = usePlayerStore((x) => x.status);
60
+ const url = useStreamplaceStore((x) => x.url);
61
+ const playerEvent = usePlayerStore((x) => x.playerEvent);
62
+ const [whatDoing, setWhatDoing] = useState(PlayerStatus.START);
63
+ const [whatDid, setWhatDid] = useState({});
64
+ const [doingSince, setDoingSince] = useState(Date.now());
65
+ const [lastUpdated, setLastUpdated] = useState(0);
66
+ const updateWhatDid = (now) => {
67
+ const prev = whatDid[whatDoing] ?? 0;
68
+ const duration = now.getTime() - doingSince;
69
+ const ret = {
70
+ ...whatDid,
71
+ [whatDoing]: prev + duration,
72
+ };
73
+ return ret;
74
+ };
75
+ // callback to update the status
76
+ useEffect(() => {
77
+ const now = new Date();
78
+ if (playerStatus !== whatDoing) {
79
+ setWhatDid(updateWhatDid(now));
80
+ setWhatDoing(playerStatus);
81
+ setDoingSince(now.getTime());
82
+ }
83
+ }, [playerStatus]);
84
+ useEffect(() => {
85
+ if (lastUpdated === 0) {
86
+ return;
87
+ }
88
+ const now = new Date();
89
+ const fullWhatDid = updateWhatDid(now);
90
+ setWhatDid({});
91
+ setDoingSince(now.getTime());
92
+ playerEvent(url, now.toISOString(), "aq-played", {
93
+ whatHappened: fullWhatDid,
94
+ });
95
+ }, [lastUpdated]);
96
+ useEffect(() => {
97
+ const interval = setInterval((_) => {
98
+ setLastUpdated(Date.now());
99
+ }, POLL_INTERVAL);
100
+ return () => clearInterval(interval);
101
+ }, []);
102
+ return [whatDoing];
103
+ }
@@ -0,0 +1 @@
1
+ export {};