@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.
- package/LICENSE +18 -0
- package/README.md +35 -0
- package/dist/components/chat/chat-box.js +109 -0
- package/dist/components/chat/chat-message.js +76 -0
- package/dist/components/chat/chat.js +56 -0
- package/dist/components/chat/mention-suggestions.js +39 -0
- package/dist/components/chat/mod-view.js +33 -0
- package/dist/components/mobile-player/fullscreen.js +69 -0
- package/dist/components/mobile-player/fullscreen.native.js +151 -0
- package/dist/components/mobile-player/player.js +103 -0
- package/dist/components/mobile-player/props.js +1 -0
- package/dist/components/mobile-player/shared.js +51 -0
- package/dist/components/mobile-player/ui/countdown.js +79 -0
- package/dist/components/mobile-player/ui/index.js +5 -0
- package/dist/components/mobile-player/ui/input.js +38 -0
- package/dist/components/mobile-player/ui/metrics.js +40 -0
- package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
- package/dist/components/mobile-player/use-webrtc.js +232 -0
- package/dist/components/mobile-player/video.js +375 -0
- package/dist/components/mobile-player/video.native.js +238 -0
- package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
- package/dist/components/mobile-player/webrtc-primitives.js +25 -0
- package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
- package/dist/components/ui/button.js +220 -0
- package/dist/components/ui/dialog.js +203 -0
- package/dist/components/ui/dropdown.js +148 -0
- package/dist/components/ui/icons.js +22 -0
- package/dist/components/ui/index.js +22 -0
- package/dist/components/ui/input.js +202 -0
- package/dist/components/ui/loader.js +7 -0
- package/dist/components/ui/primitives/button.js +121 -0
- package/dist/components/ui/primitives/input.js +202 -0
- package/dist/components/ui/primitives/modal.js +203 -0
- package/dist/components/ui/primitives/text.js +286 -0
- package/dist/components/ui/resizeable.js +101 -0
- package/dist/components/ui/text.js +175 -0
- package/dist/components/ui/textarea.js +17 -0
- package/dist/components/ui/toast.js +129 -0
- package/dist/components/ui/view.js +250 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/useAvatars.js +32 -0
- package/dist/hooks/useCameraToggle.js +9 -0
- package/dist/hooks/useKeyboard.js +33 -0
- package/dist/hooks/useKeyboardSlide.js +11 -0
- package/dist/hooks/useLivestreamInfo.js +62 -0
- package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
- package/dist/hooks/usePlayerDimensions.js +19 -0
- package/dist/hooks/useSegmentTiming.js +62 -0
- package/dist/index.js +16 -0
- package/dist/lib/facet.js +88 -0
- package/dist/lib/theme/atoms.js +620 -0
- package/dist/lib/theme/atoms.types.js +5 -0
- package/dist/lib/theme/index.js +9 -0
- package/dist/lib/theme/theme.js +248 -0
- package/dist/lib/theme/tokens.js +383 -0
- package/dist/lib/utils.js +94 -0
- package/dist/livestream-provider/index.js +25 -0
- package/dist/livestream-provider/websocket.js +41 -0
- package/dist/livestream-store/chat.js +186 -0
- package/dist/livestream-store/context.js +2 -0
- package/dist/livestream-store/index.js +4 -0
- package/dist/livestream-store/livestream-state.js +1 -0
- package/dist/livestream-store/livestream-store.js +42 -0
- package/dist/livestream-store/stream-key.js +115 -0
- package/dist/livestream-store/websocket-consumer.js +55 -0
- package/dist/player-store/context.js +2 -0
- package/dist/player-store/index.js +6 -0
- package/dist/player-store/player-provider.js +52 -0
- package/dist/player-store/player-state.js +22 -0
- package/dist/player-store/player-store.js +159 -0
- package/dist/player-store/single-player-provider.js +109 -0
- package/dist/streamplace-provider/context.js +2 -0
- package/dist/streamplace-provider/index.js +16 -0
- package/dist/streamplace-provider/poller.js +46 -0
- package/dist/streamplace-provider/xrpc.js +0 -0
- package/dist/streamplace-store/block.js +23 -0
- package/dist/streamplace-store/index.js +3 -0
- package/dist/streamplace-store/stream.js +193 -0
- package/dist/streamplace-store/streamplace-store.js +37 -0
- package/dist/streamplace-store/user.js +47 -0
- package/dist/streamplace-store/xrpc.js +12 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
- package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
- package/package.json +50 -8
- package/src/components/chat/chat-box.tsx +195 -0
- package/src/components/chat/chat-message.tsx +192 -0
- package/src/components/chat/chat.tsx +128 -0
- package/src/components/chat/mention-suggestions.tsx +71 -0
- package/src/components/chat/mod-view.tsx +118 -0
- package/src/components/mobile-player/fullscreen.native.tsx +193 -0
- package/src/components/mobile-player/fullscreen.tsx +79 -0
- package/src/components/mobile-player/player.tsx +134 -0
- package/src/components/mobile-player/props.tsx +11 -0
- package/src/components/mobile-player/shared.tsx +56 -0
- package/src/components/mobile-player/ui/countdown.tsx +119 -0
- package/src/components/mobile-player/ui/index.ts +5 -0
- package/src/components/mobile-player/ui/input.tsx +85 -0
- package/src/components/mobile-player/ui/metrics.tsx +69 -0
- package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
- package/src/components/mobile-player/use-webrtc.tsx +282 -0
- package/src/components/mobile-player/video.native.tsx +360 -0
- package/src/components/mobile-player/video.tsx +557 -0
- package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
- package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
- package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
- package/src/components/ui/button.tsx +309 -0
- package/src/components/ui/dialog.tsx +376 -0
- package/src/components/ui/dropdown.tsx +399 -0
- package/src/components/ui/icons.tsx +50 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +350 -0
- package/src/components/ui/loader.tsx +9 -0
- package/src/components/ui/primitives/button.tsx +292 -0
- package/src/components/ui/primitives/input.tsx +422 -0
- package/src/components/ui/primitives/modal.tsx +421 -0
- package/src/components/ui/primitives/text.tsx +499 -0
- package/src/components/ui/resizeable.tsx +169 -0
- package/src/components/ui/text.tsx +330 -0
- package/src/components/ui/textarea.tsx +34 -0
- package/src/components/ui/toast.tsx +203 -0
- package/src/components/ui/view.tsx +344 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAvatars.tsx +44 -0
- package/src/hooks/useCameraToggle.ts +12 -0
- package/src/hooks/useKeyboard.tsx +41 -0
- package/src/hooks/useKeyboardSlide.ts +12 -0
- package/src/hooks/useLivestreamInfo.ts +67 -0
- package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
- package/src/hooks/usePlayerDimensions.ts +23 -0
- package/src/hooks/useSegmentTiming.tsx +88 -0
- package/src/index.tsx +27 -0
- package/src/lib/facet.ts +131 -0
- package/src/lib/theme/atoms.ts +760 -0
- package/src/lib/theme/atoms.types.ts +258 -0
- package/src/lib/theme/index.ts +48 -0
- package/src/lib/theme/theme.tsx +436 -0
- package/src/lib/theme/tokens.ts +409 -0
- package/src/lib/utils.ts +132 -0
- package/src/livestream-provider/index.tsx +48 -0
- package/src/livestream-provider/websocket.tsx +47 -0
- package/src/livestream-store/chat.tsx +261 -0
- package/src/livestream-store/context.tsx +10 -0
- package/src/livestream-store/index.tsx +4 -0
- package/src/livestream-store/livestream-state.tsx +21 -0
- package/src/livestream-store/livestream-store.tsx +59 -0
- package/src/livestream-store/stream-key.tsx +124 -0
- package/src/livestream-store/websocket-consumer.tsx +62 -0
- package/src/player-store/context.tsx +11 -0
- package/src/player-store/index.tsx +6 -0
- package/src/player-store/player-provider.tsx +89 -0
- package/src/player-store/player-state.tsx +187 -0
- package/src/player-store/player-store.tsx +239 -0
- package/src/player-store/single-player-provider.tsx +181 -0
- package/src/streamplace-provider/context.tsx +10 -0
- package/src/streamplace-provider/index.tsx +32 -0
- package/src/streamplace-provider/poller.tsx +55 -0
- package/src/streamplace-provider/xrpc.tsx +0 -0
- package/src/streamplace-store/block.tsx +29 -0
- package/src/streamplace-store/index.tsx +3 -0
- package/src/streamplace-store/stream.tsx +262 -0
- package/src/streamplace-store/streamplace-store.tsx +89 -0
- package/src/streamplace-store/user.tsx +57 -0
- package/src/streamplace-store/xrpc.tsx +15 -0
- package/tsconfig.json +9 -0
- 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
|
+
});
|