@streamplace/components 0.7.3 → 0.7.7
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/dist/components/chat/chat-box.js +212 -24
- package/dist/components/chat/chat-message.js +5 -5
- package/dist/components/chat/chat.js +83 -5
- package/dist/components/chat/emoji-suggestions.js +35 -0
- package/dist/components/chat/mod-view.js +59 -8
- package/dist/components/chat/system-message.js +19 -0
- package/dist/components/icons/bluesky-icon.js +9 -0
- package/dist/components/keep-awake.js +7 -0
- package/dist/components/keep-awake.native.js +16 -0
- package/dist/components/mobile-player/fullscreen.js +2 -1
- package/dist/components/mobile-player/fullscreen.native.js +3 -3
- package/dist/components/mobile-player/player.js +15 -30
- package/dist/components/mobile-player/ui/index.js +2 -1
- package/dist/components/mobile-player/ui/report-modal.js +90 -0
- package/dist/components/mobile-player/ui/{loading.js → streamer-loading-overlay.js} +1 -1
- package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
- package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
- package/dist/components/mobile-player/use-webrtc.js +7 -1
- package/dist/components/mobile-player/video-retry.js +29 -0
- package/dist/components/mobile-player/video.js +84 -9
- package/dist/components/mobile-player/video.native.js +24 -10
- package/dist/components/share/sharesheet.js +91 -0
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/dropdown.js +6 -6
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/primitives/modal.js +0 -1
- package/dist/components/ui/slider.js +5 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/usePointerDevice.js +71 -0
- package/dist/index.js +10 -3
- package/dist/lib/system-messages.js +101 -0
- package/dist/livestream-store/chat.js +111 -18
- package/dist/livestream-store/livestream-store.js +3 -0
- package/dist/livestream-store/problems.js +76 -0
- package/dist/livestream-store/websocket-consumer.js +39 -4
- package/dist/player-store/player-store.js +30 -4
- package/dist/streamplace-store/block.js +51 -12
- package/dist/ui/index.js +79 -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 +6 -2
- package/src/components/chat/chat-box.tsx +295 -25
- package/src/components/chat/chat-message.tsx +6 -7
- package/src/components/chat/chat.tsx +192 -41
- package/src/components/chat/emoji-suggestions.tsx +94 -0
- package/src/components/chat/mod-view.tsx +119 -40
- package/src/components/chat/system-message.tsx +38 -0
- package/src/components/icons/bluesky-icon.tsx +9 -0
- package/src/components/keep-awake.native.tsx +13 -0
- package/src/components/keep-awake.tsx +3 -0
- package/src/components/mobile-player/fullscreen.native.tsx +12 -3
- package/src/components/mobile-player/fullscreen.tsx +10 -3
- package/src/components/mobile-player/player.tsx +28 -36
- package/src/components/mobile-player/props.tsx +1 -0
- package/src/components/mobile-player/ui/index.ts +2 -1
- package/src/components/mobile-player/ui/report-modal.tsx +195 -0
- package/src/components/mobile-player/ui/{loading.tsx → streamer-loading-overlay.tsx} +1 -1
- package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
- package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
- package/src/components/mobile-player/use-webrtc.tsx +10 -2
- package/src/components/mobile-player/video-retry.tsx +28 -0
- package/src/components/mobile-player/video.native.tsx +24 -10
- package/src/components/mobile-player/video.tsx +100 -21
- package/src/components/share/sharesheet.tsx +185 -0
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/dropdown.tsx +13 -13
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/primitives/modal.tsx +0 -1
- package/src/components/ui/slider.tsx +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/usePointerDevice.ts +89 -0
- package/src/index.tsx +11 -2
- package/src/lib/system-messages.ts +135 -0
- package/src/livestream-store/chat.tsx +145 -17
- package/src/livestream-store/livestream-state.tsx +10 -0
- package/src/livestream-store/livestream-store.tsx +3 -0
- package/src/livestream-store/problems.tsx +96 -0
- package/src/livestream-store/websocket-consumer.tsx +44 -4
- package/src/player-store/player-state.tsx +21 -4
- package/src/player-store/player-store.tsx +38 -5
- package/src/streamplace-store/block.tsx +55 -13
- package/src/ui/index.ts +86 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
usePlayerStore,
|
|
8
8
|
useStreamplaceStore,
|
|
9
9
|
} from "../..";
|
|
10
|
-
import { borderRadius, colors, mt
|
|
10
|
+
import { borderRadius, colors, mt } from "../../lib/theme/atoms";
|
|
11
11
|
import { Text, View } from "../ui/index";
|
|
12
|
+
import { Loader } from "../ui/loader";
|
|
12
13
|
import { srcToUrl } from "./shared";
|
|
13
14
|
import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
|
|
14
15
|
import {
|
|
@@ -146,23 +147,55 @@ const VideoElement = forwardRef<
|
|
|
146
147
|
|
|
147
148
|
const localVideoRef = props.videoRef ?? useRef<HTMLVideoElement | null>(null);
|
|
148
149
|
|
|
150
|
+
// setPipAction comes from Zustand store
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (typeof x.setPipAction === "function") {
|
|
153
|
+
const fn = () => {
|
|
154
|
+
if (localVideoRef.current) {
|
|
155
|
+
try {
|
|
156
|
+
localVideoRef.current.requestPictureInPicture?.();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error("Error requesting Picture-in-Picture:", err);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
console.log("No video ref available for PiP");
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
x.setPipAction(fn);
|
|
165
|
+
}
|
|
166
|
+
// Cleanup on unmount
|
|
167
|
+
return () => {
|
|
168
|
+
if (typeof x.setPipAction === "function") {
|
|
169
|
+
x.setPipAction(undefined);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
149
174
|
const canPlayThrough = (e) => {
|
|
175
|
+
console.log("canPlayThrough called", {
|
|
176
|
+
firstAttempt,
|
|
177
|
+
videoRef: !!localVideoRef.current,
|
|
178
|
+
});
|
|
179
|
+
setStatus(PlayerStatus.PLAYING);
|
|
150
180
|
event("canplaythrough")(e);
|
|
151
181
|
if (firstAttempt && localVideoRef.current) {
|
|
152
182
|
setFirstAttempt(false);
|
|
183
|
+
console.log("Attempting to play video");
|
|
153
184
|
localVideoRef.current.play().catch((err) => {
|
|
185
|
+
console.log("error playing video", err.name);
|
|
154
186
|
if (err.name === "NotAllowedError") {
|
|
155
187
|
if (localVideoRef.current) {
|
|
188
|
+
console.log("Setting muted and retrying");
|
|
156
189
|
setMuted(true);
|
|
157
190
|
localVideoRef.current.muted = true;
|
|
158
191
|
localVideoRef.current
|
|
159
192
|
.play()
|
|
160
193
|
.then(() => {
|
|
161
|
-
console.
|
|
194
|
+
console.log("Muted play succeeded");
|
|
162
195
|
setMuteWasForced(true);
|
|
163
196
|
})
|
|
164
197
|
.catch((err) => {
|
|
165
|
-
console.error("
|
|
198
|
+
console.error("Muted play also failed", err);
|
|
166
199
|
});
|
|
167
200
|
}
|
|
168
201
|
}
|
|
@@ -195,9 +228,17 @@ const VideoElement = forwardRef<
|
|
|
195
228
|
(ref as React.MutableRefObject<HTMLVideoElement | null>).current =
|
|
196
229
|
videoElement;
|
|
197
230
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
231
|
+
(localVideoRef as any).current = videoElement;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const eventLogger = (evType) => (e) => {
|
|
235
|
+
console.log("📺 Video event:", evType);
|
|
236
|
+
const now = new Date();
|
|
237
|
+
if (updateEvents[evType]) {
|
|
238
|
+
x.setStatus(evType);
|
|
239
|
+
}
|
|
240
|
+
console.log("Sending", evType, "status to", url);
|
|
241
|
+
playerEvent(url, now.toISOString(), evType, {});
|
|
201
242
|
};
|
|
202
243
|
|
|
203
244
|
return (
|
|
@@ -212,7 +253,8 @@ const VideoElement = forwardRef<
|
|
|
212
253
|
onMouseMove={setUserInteraction}
|
|
213
254
|
onClick={setUserInteraction}
|
|
214
255
|
onAbort={event("abort")}
|
|
215
|
-
onCanPlay={
|
|
256
|
+
onCanPlay={eventLogger}
|
|
257
|
+
onCanPlayThroughCapture={eventLogger}
|
|
216
258
|
onCanPlayThrough={canPlayThrough}
|
|
217
259
|
onEmptied={event("emptied")}
|
|
218
260
|
onEncrypted={event("encrypted")}
|
|
@@ -236,6 +278,8 @@ const VideoElement = forwardRef<
|
|
|
236
278
|
backgroundColor: "transparent",
|
|
237
279
|
width: "100%",
|
|
238
280
|
height: "100%",
|
|
281
|
+
maxWidth: "100%",
|
|
282
|
+
maxHeight: "100%",
|
|
239
283
|
transform: ingest ? "scaleX(-1)" : undefined,
|
|
240
284
|
}}
|
|
241
285
|
/>
|
|
@@ -286,6 +330,7 @@ export function HLSPlayer(props: VideoProps) {
|
|
|
286
330
|
});
|
|
287
331
|
}
|
|
288
332
|
}, [props.url]);
|
|
333
|
+
|
|
289
334
|
return <VideoElement {...props} ref={localRef} />;
|
|
290
335
|
}
|
|
291
336
|
|
|
@@ -348,6 +393,15 @@ export function WebRTCPlayer(
|
|
|
348
393
|
return <WebRTCPlayerInner url={props.url} videoRef={props.videoRef} />;
|
|
349
394
|
}
|
|
350
395
|
|
|
396
|
+
const connectionStatusMessages: Record<string, string> = {
|
|
397
|
+
initializing: "Starting up...",
|
|
398
|
+
connecting: "Connecting...",
|
|
399
|
+
"connection-failed": "Connecting...",
|
|
400
|
+
connected: "Connected",
|
|
401
|
+
reconnecting: "Reconnecting...",
|
|
402
|
+
checking: "Checking connection...",
|
|
403
|
+
};
|
|
404
|
+
|
|
351
405
|
export function WebRTCPlayerInner({
|
|
352
406
|
videoRef,
|
|
353
407
|
url,
|
|
@@ -384,7 +438,7 @@ export function WebRTCPlayerInner({
|
|
|
384
438
|
if (stuck && status === PlayerStatus.PLAYING) {
|
|
385
439
|
setStatus(PlayerStatus.STALLED);
|
|
386
440
|
}
|
|
387
|
-
if (!stuck &&
|
|
441
|
+
if (!stuck && status === PlayerStatus.STALLED) {
|
|
388
442
|
setStatus(PlayerStatus.PLAYING);
|
|
389
443
|
}
|
|
390
444
|
}, [stuck, status, mediaStream]);
|
|
@@ -431,19 +485,30 @@ export function WebRTCPlayerInner({
|
|
|
431
485
|
}, [mediaStream]);
|
|
432
486
|
|
|
433
487
|
if (!mediaStream) {
|
|
488
|
+
const isError = connectionStatus === "connection-failed";
|
|
434
489
|
return (
|
|
435
490
|
<View
|
|
436
491
|
backgroundColor="#111"
|
|
437
|
-
style={{
|
|
492
|
+
style={{
|
|
493
|
+
minWidth: "100%",
|
|
494
|
+
minHeight: "100%",
|
|
495
|
+
display: "flex",
|
|
496
|
+
alignItems: "center",
|
|
497
|
+
justifyContent: "center",
|
|
498
|
+
}}
|
|
438
499
|
>
|
|
439
500
|
<View
|
|
440
|
-
|
|
441
|
-
|
|
501
|
+
style={{
|
|
502
|
+
borderRadius: borderRadius.md,
|
|
503
|
+
padding: 24,
|
|
504
|
+
alignItems: "center",
|
|
505
|
+
gap: 16,
|
|
506
|
+
}}
|
|
442
507
|
>
|
|
443
|
-
<
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
508
|
+
{!isError && <Loader size="large" />}
|
|
509
|
+
<Text size="lg" weight="semibold">
|
|
510
|
+
{connectionStatusMessages[connectionStatus] || "Connecting..."}
|
|
511
|
+
</Text>
|
|
447
512
|
</View>
|
|
448
513
|
</View>
|
|
449
514
|
);
|
|
@@ -540,15 +605,29 @@ export function WebcamIngestPlayer(props: VideoProps) {
|
|
|
540
605
|
if (error) {
|
|
541
606
|
return (
|
|
542
607
|
<View
|
|
543
|
-
backgroundColor=
|
|
544
|
-
style={
|
|
608
|
+
backgroundColor="#111"
|
|
609
|
+
style={{
|
|
610
|
+
minWidth: "100%",
|
|
611
|
+
minHeight: "100%",
|
|
612
|
+
display: "flex",
|
|
613
|
+
alignItems: "center",
|
|
614
|
+
justifyContent: "center",
|
|
615
|
+
}}
|
|
545
616
|
>
|
|
546
|
-
<View
|
|
547
|
-
|
|
548
|
-
|
|
617
|
+
<View
|
|
618
|
+
backgroundColor={colors.destructive[900]}
|
|
619
|
+
style={{
|
|
620
|
+
borderRadius: borderRadius.md,
|
|
621
|
+
padding: 24,
|
|
622
|
+
alignItems: "center",
|
|
623
|
+
gap: 16,
|
|
624
|
+
maxWidth: 400,
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
<Text size="xl" weight="extrabold" color="default">
|
|
628
|
+
{error.message}
|
|
549
629
|
</Text>
|
|
550
630
|
</View>
|
|
551
|
-
<Text>{error.message}</Text>
|
|
552
631
|
</View>
|
|
553
632
|
);
|
|
554
633
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Code, Copy, Link2, Share2 } from "lucide-react-native";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
import { Clipboard, Linking, Platform, View } from "react-native";
|
|
4
|
+
import { colors } from "../../lib/theme";
|
|
5
|
+
import { useLivestreamStore } from "../../livestream-store";
|
|
6
|
+
import { useUrl } from "../../streamplace-store";
|
|
7
|
+
import { BlueskyIcon } from "../icons/bluesky-icon";
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuGroup,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuSeparator,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
ResponsiveDropdownMenuContent,
|
|
15
|
+
Text,
|
|
16
|
+
} from "../ui";
|
|
17
|
+
|
|
18
|
+
export interface ShareSheetProps {
|
|
19
|
+
onShare?: (action: string, success: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ShareSheet({ onShare }: ShareSheetProps = {}) {
|
|
23
|
+
const profile = useLivestreamStore((x) => x.profile);
|
|
24
|
+
const [isCopying, setIsCopying] = useState(false);
|
|
25
|
+
const url = useUrl();
|
|
26
|
+
|
|
27
|
+
// Get the current stream URL
|
|
28
|
+
const getStreamUrl = useCallback(() => {
|
|
29
|
+
return url + (profile ? `/@${profile.handle}` : "");
|
|
30
|
+
}, [profile]);
|
|
31
|
+
|
|
32
|
+
// Get the embed URL
|
|
33
|
+
const getEmbedUrl = useCallback(() => {
|
|
34
|
+
return url + (profile ? `/embed/${profile.handle}` : "");
|
|
35
|
+
}, [profile]);
|
|
36
|
+
|
|
37
|
+
// Get embed code
|
|
38
|
+
const getEmbedCode = useCallback(() => {
|
|
39
|
+
const embedUrl = getEmbedUrl();
|
|
40
|
+
return `<iframe src="${embedUrl}" width="640" height="360" frameborder="0" allowfullscreen></iframe>`;
|
|
41
|
+
}, [getEmbedUrl]);
|
|
42
|
+
|
|
43
|
+
// Copy to clipboard handler
|
|
44
|
+
const copyToClipboard = useCallback(
|
|
45
|
+
async (text: string, label: string) => {
|
|
46
|
+
setIsCopying(true);
|
|
47
|
+
try {
|
|
48
|
+
if (Platform.OS === "web") {
|
|
49
|
+
await navigator.clipboard.writeText(text);
|
|
50
|
+
} else {
|
|
51
|
+
Clipboard.setString(text);
|
|
52
|
+
}
|
|
53
|
+
onShare?.(`copy_${label.toLowerCase().replace(/\s+/g, "_")}`, true);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
onShare?.(`copy_${label.toLowerCase().replace(/\s+/g, "_")}`, false);
|
|
56
|
+
} finally {
|
|
57
|
+
setIsCopying(false);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[onShare],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Share to Bluesky
|
|
64
|
+
const shareToBluesky = useCallback(() => {
|
|
65
|
+
const streamUrl = getStreamUrl();
|
|
66
|
+
const text = profile
|
|
67
|
+
? `Check out @${profile.handle} live on Streamplace! ${streamUrl}`
|
|
68
|
+
: `Check out this stream on Streamplace! ${streamUrl}`;
|
|
69
|
+
const blueskyUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`;
|
|
70
|
+
Linking.openURL(blueskyUrl);
|
|
71
|
+
onShare?.("share_bluesky", true);
|
|
72
|
+
}, [profile, getStreamUrl, onShare]);
|
|
73
|
+
|
|
74
|
+
// Share to Twitter/X
|
|
75
|
+
const shareToTwitter = useCallback(() => {
|
|
76
|
+
const streamUrl = getStreamUrl();
|
|
77
|
+
const text = profile
|
|
78
|
+
? `Check out @${profile.handle} live on Streamplace!`
|
|
79
|
+
: `Check out this stream on Streamplace!`;
|
|
80
|
+
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(streamUrl)}`;
|
|
81
|
+
Linking.openURL(twitterUrl);
|
|
82
|
+
onShare?.("share_twitter", true);
|
|
83
|
+
}, [profile, getStreamUrl, onShare]);
|
|
84
|
+
|
|
85
|
+
// Native share (mobile)
|
|
86
|
+
const nativeShare = useCallback(async () => {
|
|
87
|
+
const streamUrl = getStreamUrl();
|
|
88
|
+
const text = profile
|
|
89
|
+
? `Check out @${profile.handle} live on Streamplace!`
|
|
90
|
+
: `Check out this stream on Streamplace!`;
|
|
91
|
+
|
|
92
|
+
if (Platform.OS === "web" && navigator.share) {
|
|
93
|
+
try {
|
|
94
|
+
await navigator.share({
|
|
95
|
+
title: "Streamplace",
|
|
96
|
+
text: text,
|
|
97
|
+
url: streamUrl,
|
|
98
|
+
});
|
|
99
|
+
onShare?.("share_native", true);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// User cancelled or error occurred
|
|
102
|
+
onShare?.("share_native", false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, [profile, getStreamUrl, onShare]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<DropdownMenu>
|
|
109
|
+
<DropdownMenuTrigger>
|
|
110
|
+
<Share2 color={colors.gray[200]} />
|
|
111
|
+
</DropdownMenuTrigger>
|
|
112
|
+
<ResponsiveDropdownMenuContent>
|
|
113
|
+
<DropdownMenuGroup title="Share">
|
|
114
|
+
<DropdownMenuItem onPress={shareToBluesky} closeOnPress={true}>
|
|
115
|
+
<View
|
|
116
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
117
|
+
>
|
|
118
|
+
<BlueskyIcon size={20} color={colors.gray[400]} />
|
|
119
|
+
<Text>Share to Bluesky</Text>
|
|
120
|
+
</View>
|
|
121
|
+
</DropdownMenuItem>
|
|
122
|
+
{/* <DropdownMenuItem onPress={shareToTwitter}>
|
|
123
|
+
<View
|
|
124
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
125
|
+
>
|
|
126
|
+
<MessageCircle size={20} color={colors.gray[400]} />
|
|
127
|
+
<Text>Share to X</Text>
|
|
128
|
+
</View>
|
|
129
|
+
</DropdownMenuItem> */}
|
|
130
|
+
{/* navigator isn't on non-web */}
|
|
131
|
+
{Platform.OS !== "web" || (navigator && (navigator as any).share) ? (
|
|
132
|
+
<DropdownMenuItem onPress={nativeShare}>
|
|
133
|
+
<View
|
|
134
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
135
|
+
>
|
|
136
|
+
<Share2 size={20} color={colors.gray[400]} />
|
|
137
|
+
<Text>More Options...</Text>
|
|
138
|
+
</View>
|
|
139
|
+
</DropdownMenuItem>
|
|
140
|
+
) : null}
|
|
141
|
+
</DropdownMenuGroup>
|
|
142
|
+
<DropdownMenuGroup title="Copy">
|
|
143
|
+
<DropdownMenuItem
|
|
144
|
+
onPress={() => copyToClipboard(getStreamUrl(), "Stream link")}
|
|
145
|
+
disabled={isCopying}
|
|
146
|
+
closeOnPress={true}
|
|
147
|
+
>
|
|
148
|
+
<View
|
|
149
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
150
|
+
>
|
|
151
|
+
<Link2 size={20} color={colors.gray[400]} />
|
|
152
|
+
<Text>Copy Link</Text>
|
|
153
|
+
</View>
|
|
154
|
+
</DropdownMenuItem>
|
|
155
|
+
<DropdownMenuSeparator />
|
|
156
|
+
<DropdownMenuItem
|
|
157
|
+
onPress={() => copyToClipboard(getEmbedCode(), "Embed code")}
|
|
158
|
+
disabled={isCopying}
|
|
159
|
+
closeOnPress={true}
|
|
160
|
+
>
|
|
161
|
+
<View
|
|
162
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
163
|
+
>
|
|
164
|
+
<Code size={20} color={colors.gray[400]} />
|
|
165
|
+
<Text>Copy Embed Code</Text>
|
|
166
|
+
</View>
|
|
167
|
+
</DropdownMenuItem>
|
|
168
|
+
<DropdownMenuSeparator />
|
|
169
|
+
<DropdownMenuItem
|
|
170
|
+
closeOnPress={true}
|
|
171
|
+
onPress={() => copyToClipboard(getEmbedUrl(), "Embed URL")}
|
|
172
|
+
disabled={isCopying}
|
|
173
|
+
>
|
|
174
|
+
<View
|
|
175
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
|
176
|
+
>
|
|
177
|
+
<Copy size={20} color={colors.gray[400]} />
|
|
178
|
+
<Text>Copy Embed URL</Text>
|
|
179
|
+
</View>
|
|
180
|
+
</DropdownMenuItem>
|
|
181
|
+
</DropdownMenuGroup>
|
|
182
|
+
</ResponsiveDropdownMenuContent>
|
|
183
|
+
</DropdownMenu>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -254,7 +254,7 @@ DialogFooter.displayName = "DialogFooter";
|
|
|
254
254
|
|
|
255
255
|
// Dialog Close Icon component (Lucide X)
|
|
256
256
|
const DialogCloseIcon = () => {
|
|
257
|
-
return <ThemedX size="md" variant="
|
|
257
|
+
return <ThemedX size="md" variant="default" />;
|
|
258
258
|
};
|
|
259
259
|
|
|
260
260
|
// Create theme-aware styles
|
|
@@ -24,11 +24,9 @@ import {
|
|
|
24
24
|
colors,
|
|
25
25
|
fontSize,
|
|
26
26
|
gap,
|
|
27
|
-
h,
|
|
28
27
|
layout,
|
|
29
28
|
ml,
|
|
30
29
|
mt,
|
|
31
|
-
mx,
|
|
32
30
|
p,
|
|
33
31
|
pb,
|
|
34
32
|
pl,
|
|
@@ -73,6 +71,14 @@ export const DropdownMenuBottomSheet = forwardRef<
|
|
|
73
71
|
index={open ? 3 : -1}
|
|
74
72
|
snapPoints={snapPoints}
|
|
75
73
|
enablePanDownToClose
|
|
74
|
+
enableDynamicSizing
|
|
75
|
+
enableContentPanningGesture={false}
|
|
76
|
+
backdropComponent={({ style }) => (
|
|
77
|
+
<Pressable
|
|
78
|
+
style={[style, StyleSheet.absoluteFill]}
|
|
79
|
+
onPress={() => onOpenChange?.(false)}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
76
82
|
onClose={() => onOpenChange?.(false)}
|
|
77
83
|
style={[overlayStyle]}
|
|
78
84
|
backgroundStyle={[bg.black, a.radius.all.md, a.shadows.md, p[1]]}
|
|
@@ -341,13 +347,7 @@ export const DropdownMenuSeparator = forwardRef<
|
|
|
341
347
|
any,
|
|
342
348
|
DropdownMenuPrimitive.SeparatorProps
|
|
343
349
|
>((props, ref) => {
|
|
344
|
-
return
|
|
345
|
-
<View
|
|
346
|
-
ref={ref}
|
|
347
|
-
style={[mx[2], h[0.5] || { height: 0.5 }, bg.gray[800]]}
|
|
348
|
-
{...props}
|
|
349
|
-
/>
|
|
350
|
-
);
|
|
350
|
+
return <View ref={ref} style={[{ height: 0.5 }, bg.gray[800]]} {...props} />;
|
|
351
351
|
});
|
|
352
352
|
|
|
353
353
|
export function DropdownMenuShortcut(props: any) {
|
|
@@ -370,16 +370,16 @@ export const DropdownMenuGroup = forwardRef<
|
|
|
370
370
|
>((props, ref) => {
|
|
371
371
|
const { inset, title, children, ...rest } = props;
|
|
372
372
|
return (
|
|
373
|
-
<View style={[pt[2], inset
|
|
373
|
+
<View style={[pt[2], inset && gap[2]]} ref={ref} {...rest}>
|
|
374
374
|
{title && (
|
|
375
375
|
<Text style={[textColors.gray[400], pb[1], pl[2]]}>{title}</Text>
|
|
376
376
|
)}
|
|
377
377
|
<View
|
|
378
378
|
style={[
|
|
379
379
|
bg.gray[900],
|
|
380
|
-
Platform.OS === "web" ? px[2] : p[2],
|
|
381
|
-
gap[
|
|
382
|
-
{ borderRadius: borderRadius.lg
|
|
380
|
+
Platform.OS === "web" ? [px[2], py[1]] : p[2],
|
|
381
|
+
gap.all[1],
|
|
382
|
+
{ borderRadius: borderRadius.lg },
|
|
383
383
|
]}
|
|
384
384
|
>
|
|
385
385
|
{children}
|
|
@@ -12,7 +12,9 @@ export * from "./icons";
|
|
|
12
12
|
export * from "./input";
|
|
13
13
|
export * from "./loader";
|
|
14
14
|
export * from "./resizeable";
|
|
15
|
+
export * from "./slider";
|
|
15
16
|
export * from "./text";
|
|
17
|
+
export * from "./textarea";
|
|
16
18
|
export * from "./toast";
|
|
17
19
|
export * from "./view";
|
|
18
20
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Slider from "@rn-primitives/slider";
|
package/src/hooks/index.ts
CHANGED
|
@@ -6,5 +6,6 @@ export * from "./useKeyboardSlide";
|
|
|
6
6
|
export * from "./useLivestreamInfo";
|
|
7
7
|
export * from "./useOuterAndInnerDimensions";
|
|
8
8
|
export * from "./usePlayerDimensions";
|
|
9
|
+
export * from "./usePointerDevice";
|
|
9
10
|
export * from "./useSegmentDimensions";
|
|
10
11
|
export * from "./useSegmentTiming";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
export interface PointerDevice {
|
|
5
|
+
hasHover: boolean;
|
|
6
|
+
hasFinePointer: boolean;
|
|
7
|
+
isMouseDriven: boolean;
|
|
8
|
+
isTouchDriven: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook to detect if the device is primarily mouse-driven vs touch-driven
|
|
13
|
+
* Uses CSS media queries to detect hover and pointer capabilities
|
|
14
|
+
*/
|
|
15
|
+
export function usePointerDevice(): PointerDevice {
|
|
16
|
+
const [pointerDevice, setPointerDevice] = useState<PointerDevice>(() => {
|
|
17
|
+
// Default values for non-web platforms
|
|
18
|
+
if (Platform.OS !== "web") {
|
|
19
|
+
return {
|
|
20
|
+
hasHover: false,
|
|
21
|
+
hasFinePointer: false,
|
|
22
|
+
isMouseDriven: false,
|
|
23
|
+
isTouchDriven: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Initial web detection
|
|
28
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
29
|
+
const hasHover = window.matchMedia("(hover: hover)").matches;
|
|
30
|
+
const hasFinePointer = window.matchMedia("(pointer: fine)").matches;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
hasHover,
|
|
34
|
+
hasFinePointer,
|
|
35
|
+
isMouseDriven: hasHover && hasFinePointer,
|
|
36
|
+
isTouchDriven: !hasHover || !hasFinePointer,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback for SSR or environments without matchMedia
|
|
41
|
+
return {
|
|
42
|
+
hasHover: false,
|
|
43
|
+
hasFinePointer: false,
|
|
44
|
+
isMouseDriven: false,
|
|
45
|
+
isTouchDriven: true,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
// Only run on web platforms
|
|
51
|
+
if (
|
|
52
|
+
Platform.OS !== "web" ||
|
|
53
|
+
typeof window === "undefined" ||
|
|
54
|
+
!window.matchMedia
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hoverQuery = window.matchMedia("(hover: hover)");
|
|
60
|
+
const pointerQuery = window.matchMedia("(pointer: fine)");
|
|
61
|
+
|
|
62
|
+
const updatePointerDevice = () => {
|
|
63
|
+
const hasHover = hoverQuery.matches;
|
|
64
|
+
const hasFinePointer = pointerQuery.matches;
|
|
65
|
+
|
|
66
|
+
setPointerDevice({
|
|
67
|
+
hasHover,
|
|
68
|
+
hasFinePointer,
|
|
69
|
+
isMouseDriven: hasHover && hasFinePointer,
|
|
70
|
+
isTouchDriven: !hasHover || !hasFinePointer,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Set up listeners for media query changes
|
|
75
|
+
hoverQuery.addEventListener("change", updatePointerDevice);
|
|
76
|
+
pointerQuery.addEventListener("change", updatePointerDevice);
|
|
77
|
+
|
|
78
|
+
// Initial update
|
|
79
|
+
updatePointerDevice();
|
|
80
|
+
|
|
81
|
+
// Cleanup
|
|
82
|
+
return () => {
|
|
83
|
+
hoverQuery.removeEventListener("change", updatePointerDevice);
|
|
84
|
+
pointerQuery.removeEventListener("change", updatePointerDevice);
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
return pointerDevice;
|
|
89
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -18,10 +18,19 @@ export * as ui from "./components/ui";
|
|
|
18
18
|
|
|
19
19
|
export * from "./components/ui";
|
|
20
20
|
|
|
21
|
-
export * as
|
|
22
|
-
export * as atoms from "./lib/theme/atoms";
|
|
21
|
+
export * as zero from "./ui";
|
|
23
22
|
|
|
24
23
|
export * from "./hooks";
|
|
25
24
|
|
|
25
|
+
// Theme system exports
|
|
26
|
+
export * from "./lib/theme";
|
|
27
|
+
|
|
26
28
|
export * from "./components/chat/chat";
|
|
27
29
|
export * from "./components/chat/chat-box";
|
|
30
|
+
export * from "./components/chat/system-message";
|
|
31
|
+
export { default as VideoRetry } from "./components/mobile-player/video-retry";
|
|
32
|
+
export * from "./lib/system-messages";
|
|
33
|
+
|
|
34
|
+
export * from "./components/share/sharesheet";
|
|
35
|
+
|
|
36
|
+
export * from "./components/keep-awake";
|