@streamplace/components 0.0.1 → 0.7.0

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 (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -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 +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2025 Streamplace.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @streamplace/components
2
+
3
+ Heavily WIP but looks something like this:
4
+
5
+ ```tsx
6
+ import {
7
+ StreamplaceProvider,
8
+ LivestreamProvider,
9
+ } from "@streamplace/components";
10
+
11
+ export function Provider() {
12
+ <StreamplaceProvider url="https://stream.place" oauthSession={userSession}>
13
+ {/* Everything inside of here can access that Streamplace node */}
14
+
15
+ <LivestreamProvider src="example.bsky.social" /* or did:plc:xxxx */>
16
+ {/* Everything in here has an active subscription to the livestream
17
+ context via Websocket; things like chat data and stream title */}
18
+ <App />
19
+ </LivestreamProvider>
20
+ </StreamplaceProvider>;
21
+ }
22
+
23
+ export function App() {
24
+ const chat = useChat();
25
+ return (
26
+ <View>
27
+ {chat.map((msg) => (
28
+ <Text>
29
+ @{msg.author.handle}: {msg.record.text}
30
+ </Text>
31
+ ))}
32
+ </View>
33
+ );
34
+ }
35
+ ```
@@ -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
+ });