@streamplace/components 0.7.2 → 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 -0
- package/dist/components/mobile-player/ui/report-modal.js +90 -0
- package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
- 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/resizeable.js +20 -11
- 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 +33 -4
- package/dist/streamplace-store/block.js +51 -12
- package/dist/streamplace-store/stream.js +44 -23
- 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-92db9086-0 → v22.15.0-x64-efe9a9df-0}/67b1eb60 +0 -0
- package/node-compile-cache/{v22.15.0-x64-92db9086-0 → 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 -0
- package/src/components/mobile-player/ui/report-modal.tsx +195 -0
- package/src/components/mobile-player/ui/streamer-loading-overlay.tsx +154 -0
- 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/resizeable.tsx +26 -15
- 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 +25 -4
- package/src/player-store/player-store.tsx +43 -5
- package/src/streamplace-store/block.tsx +55 -13
- package/src/streamplace-store/stream.tsx +66 -35
- package/src/ui/index.ts +86 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
- package/node-compile-cache/v22.15.0-x64-92db9086-0/56540125 +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { PlayerStatus, usePlayerStore } from "../..";
|
|
3
|
+
|
|
4
|
+
export default function VideoRetry(props: { children: React.ReactNode }) {
|
|
5
|
+
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
6
|
+
const [retries, setRetries] = useState(0);
|
|
7
|
+
const playing = usePlayerStore((x) => x.status === PlayerStatus.PLAYING);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!playing) {
|
|
11
|
+
const jitter = 500 + Math.random() * 1500;
|
|
12
|
+
retryTimeoutRef.current = setTimeout(() => {
|
|
13
|
+
console.log("Retrying video playback...");
|
|
14
|
+
setRetries((prevRetries) => prevRetries + 1);
|
|
15
|
+
}, jitter);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
if (retryTimeoutRef.current) {
|
|
20
|
+
console.log("Clearing retry timeout");
|
|
21
|
+
clearTimeout(retryTimeoutRef.current);
|
|
22
|
+
retryTimeoutRef.current = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}, [!playing]);
|
|
26
|
+
|
|
27
|
+
return <React.Fragment key={retries}>{props.children}</React.Fragment>;
|
|
28
|
+
}
|
|
@@ -89,6 +89,14 @@ export function NativeVideo() {
|
|
|
89
89
|
}, [setStatus]);
|
|
90
90
|
|
|
91
91
|
const player = useVideoPlayer(url, (player) => {
|
|
92
|
+
player.addListener("playingChange", (newIsPlaying) => {
|
|
93
|
+
console.log("playingChange", newIsPlaying);
|
|
94
|
+
if (newIsPlaying) {
|
|
95
|
+
setStatus(PlayerStatus.PLAYING);
|
|
96
|
+
} else {
|
|
97
|
+
setStatus(PlayerStatus.WAITING);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
92
100
|
player.loop = true;
|
|
93
101
|
player.muted = muted;
|
|
94
102
|
player.play();
|
|
@@ -115,12 +123,14 @@ export function NativeVideo() {
|
|
|
115
123
|
).map((evType) => {
|
|
116
124
|
return player.addListener(evType, (...args) => {
|
|
117
125
|
const now = new Date();
|
|
126
|
+
console.log("video native event", evType);
|
|
118
127
|
playerEvent(spurl, now.toISOString(), evType, { args: args });
|
|
119
128
|
});
|
|
120
129
|
});
|
|
121
130
|
|
|
122
131
|
subs.push(
|
|
123
132
|
player.addListener("playingChange", (newIsPlaying) => {
|
|
133
|
+
console.log("playingChange", newIsPlaying);
|
|
124
134
|
if (newIsPlaying) {
|
|
125
135
|
setStatus(PlayerStatus.PLAYING);
|
|
126
136
|
} else {
|
|
@@ -164,6 +174,7 @@ export function NativeWHEP() {
|
|
|
164
174
|
PlayerProtocol.WEBRTC,
|
|
165
175
|
);
|
|
166
176
|
const [stream, stuck] = useWebRTC(url);
|
|
177
|
+
const status = usePlayerStore((x) => x.status);
|
|
167
178
|
|
|
168
179
|
const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth);
|
|
169
180
|
const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight);
|
|
@@ -189,22 +200,25 @@ export function NativeWHEP() {
|
|
|
189
200
|
const volume = usePlayerStore((x) => x.volume);
|
|
190
201
|
|
|
191
202
|
useEffect(() => {
|
|
192
|
-
if (stuck) {
|
|
203
|
+
if (stuck && status === PlayerStatus.PLAYING) {
|
|
204
|
+
console.log("setting status to stalled", status);
|
|
193
205
|
setStatus(PlayerStatus.STALLED);
|
|
194
|
-
}
|
|
206
|
+
}
|
|
207
|
+
if (!stuck && status === PlayerStatus.STALLED) {
|
|
208
|
+
console.log("setting status to playing", status);
|
|
195
209
|
setStatus(PlayerStatus.PLAYING);
|
|
196
210
|
}
|
|
197
|
-
}, [stuck,
|
|
211
|
+
}, [stuck, status]);
|
|
198
212
|
|
|
199
213
|
const mediaStream = stream as unknown as MediaStream;
|
|
200
214
|
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}, [mediaStream, setStatus]);
|
|
215
|
+
// useEffect(() => {
|
|
216
|
+
// if (!mediaStream) {
|
|
217
|
+
// setStatus(PlayerStatus.WAITING);
|
|
218
|
+
// return;
|
|
219
|
+
// }
|
|
220
|
+
// setStatus(PlayerStatus.PLAYING);
|
|
221
|
+
// }, [mediaStream, setStatus]);
|
|
208
222
|
|
|
209
223
|
useEffect(() => {
|
|
210
224
|
if (!mediaStream) {
|
|
@@ -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
|
|
|
@@ -15,7 +15,7 @@ import Animated, {
|
|
|
15
15
|
} from "react-native-reanimated";
|
|
16
16
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
17
17
|
import { useKeyboardSlide } from "../../hooks";
|
|
18
|
-
import { bottom, layout, p, w, zIndex } from "../../lib/theme/atoms";
|
|
18
|
+
import { bottom, h, layout, p, w, zIndex } from "../../lib/theme/atoms";
|
|
19
19
|
import { View } from "./view";
|
|
20
20
|
|
|
21
21
|
const AnimatedView = Animated.createAnimatedComponent(View);
|
|
@@ -154,24 +154,35 @@ export function Resizable({
|
|
|
154
154
|
style,
|
|
155
155
|
]}
|
|
156
156
|
>
|
|
157
|
-
<View style={[layout.flex.row, layout.flex.justifyCenter]}>
|
|
157
|
+
<View style={[layout.flex.row, layout.flex.justifyCenter, h[2]]}>
|
|
158
158
|
<GestureDetector gesture={panGesture}>
|
|
159
159
|
<View
|
|
160
|
-
|
|
161
|
-
style={
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
160
|
+
// Make the touch area much larger, but keep the visible handle small
|
|
161
|
+
style={{
|
|
162
|
+
height: 30, // Large touch area
|
|
163
|
+
width: 120, // Wide enough for thumbs
|
|
164
|
+
alignItems: "center",
|
|
165
|
+
justifyContent: "center",
|
|
166
|
+
//backgroundColor: "rgba(0,255,255,0.1)",
|
|
167
|
+
transform: [{ translateY: -30 }],
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<View
|
|
171
|
+
style={[
|
|
172
|
+
w[32],
|
|
173
|
+
{
|
|
174
|
+
height: 6,
|
|
175
|
+
backgroundColor: "#eeeeee66",
|
|
176
|
+
borderRadius: 999,
|
|
177
|
+
|
|
178
|
+
transform: [{ translateY: 5 }],
|
|
179
|
+
},
|
|
180
|
+
]}
|
|
181
|
+
/>
|
|
182
|
+
</View>
|
|
173
183
|
</GestureDetector>
|
|
174
184
|
</View>
|
|
185
|
+
|
|
175
186
|
{children}
|
|
176
187
|
</AnimatedView>
|
|
177
188
|
</>
|
|
@@ -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";
|