@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.
Files changed (91) hide show
  1. package/dist/components/chat/chat-box.js +212 -24
  2. package/dist/components/chat/chat-message.js +5 -5
  3. package/dist/components/chat/chat.js +83 -5
  4. package/dist/components/chat/emoji-suggestions.js +35 -0
  5. package/dist/components/chat/mod-view.js +59 -8
  6. package/dist/components/chat/system-message.js +19 -0
  7. package/dist/components/icons/bluesky-icon.js +9 -0
  8. package/dist/components/keep-awake.js +7 -0
  9. package/dist/components/keep-awake.native.js +16 -0
  10. package/dist/components/mobile-player/fullscreen.js +2 -1
  11. package/dist/components/mobile-player/fullscreen.native.js +3 -3
  12. package/dist/components/mobile-player/player.js +15 -30
  13. package/dist/components/mobile-player/ui/index.js +2 -0
  14. package/dist/components/mobile-player/ui/report-modal.js +90 -0
  15. package/dist/components/mobile-player/ui/streamer-loading-overlay.js +104 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -1
  17. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +49 -0
  18. package/dist/components/mobile-player/use-webrtc.js +7 -1
  19. package/dist/components/mobile-player/video-retry.js +29 -0
  20. package/dist/components/mobile-player/video.js +84 -9
  21. package/dist/components/mobile-player/video.native.js +24 -10
  22. package/dist/components/share/sharesheet.js +91 -0
  23. package/dist/components/ui/dialog.js +1 -1
  24. package/dist/components/ui/dropdown.js +6 -6
  25. package/dist/components/ui/index.js +2 -0
  26. package/dist/components/ui/primitives/modal.js +0 -1
  27. package/dist/components/ui/resizeable.js +20 -11
  28. package/dist/components/ui/slider.js +5 -0
  29. package/dist/hooks/index.js +1 -0
  30. package/dist/hooks/usePointerDevice.js +71 -0
  31. package/dist/index.js +10 -3
  32. package/dist/lib/system-messages.js +101 -0
  33. package/dist/livestream-store/chat.js +111 -18
  34. package/dist/livestream-store/livestream-store.js +3 -0
  35. package/dist/livestream-store/problems.js +76 -0
  36. package/dist/livestream-store/websocket-consumer.js +39 -4
  37. package/dist/player-store/player-store.js +33 -4
  38. package/dist/streamplace-store/block.js +51 -12
  39. package/dist/streamplace-store/stream.js +44 -23
  40. package/dist/ui/index.js +79 -0
  41. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  42. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  43. package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/67b1eb60 +0 -0
  44. package/node-compile-cache/{v22.15.0-x64-92db9086-0 → v22.15.0-x64-efe9a9df-0}/7c275f90 +0 -0
  45. package/package.json +6 -2
  46. package/src/components/chat/chat-box.tsx +295 -25
  47. package/src/components/chat/chat-message.tsx +6 -7
  48. package/src/components/chat/chat.tsx +192 -41
  49. package/src/components/chat/emoji-suggestions.tsx +94 -0
  50. package/src/components/chat/mod-view.tsx +119 -40
  51. package/src/components/chat/system-message.tsx +38 -0
  52. package/src/components/icons/bluesky-icon.tsx +9 -0
  53. package/src/components/keep-awake.native.tsx +13 -0
  54. package/src/components/keep-awake.tsx +3 -0
  55. package/src/components/mobile-player/fullscreen.native.tsx +12 -3
  56. package/src/components/mobile-player/fullscreen.tsx +10 -3
  57. package/src/components/mobile-player/player.tsx +28 -36
  58. package/src/components/mobile-player/props.tsx +1 -0
  59. package/src/components/mobile-player/ui/index.ts +2 -0
  60. package/src/components/mobile-player/ui/report-modal.tsx +195 -0
  61. package/src/components/mobile-player/ui/streamer-loading-overlay.tsx +154 -0
  62. package/src/components/mobile-player/ui/viewer-context-menu.tsx +31 -3
  63. package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +66 -0
  64. package/src/components/mobile-player/use-webrtc.tsx +10 -2
  65. package/src/components/mobile-player/video-retry.tsx +28 -0
  66. package/src/components/mobile-player/video.native.tsx +24 -10
  67. package/src/components/mobile-player/video.tsx +100 -21
  68. package/src/components/share/sharesheet.tsx +185 -0
  69. package/src/components/ui/dialog.tsx +1 -1
  70. package/src/components/ui/dropdown.tsx +13 -13
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/primitives/modal.tsx +0 -1
  73. package/src/components/ui/resizeable.tsx +26 -15
  74. package/src/components/ui/slider.tsx +1 -0
  75. package/src/hooks/index.ts +1 -0
  76. package/src/hooks/usePointerDevice.ts +89 -0
  77. package/src/index.tsx +11 -2
  78. package/src/lib/system-messages.ts +135 -0
  79. package/src/livestream-store/chat.tsx +145 -17
  80. package/src/livestream-store/livestream-state.tsx +10 -0
  81. package/src/livestream-store/livestream-store.tsx +3 -0
  82. package/src/livestream-store/problems.tsx +96 -0
  83. package/src/livestream-store/websocket-consumer.tsx +44 -4
  84. package/src/player-store/player-state.tsx +25 -4
  85. package/src/player-store/player-store.tsx +43 -5
  86. package/src/streamplace-store/block.tsx +55 -13
  87. package/src/streamplace-store/stream.tsx +66 -35
  88. package/src/ui/index.ts +86 -0
  89. package/tsconfig.tsbuildinfo +1 -1
  90. package/node-compile-cache/v22.15.0-x64-92db9086-0/37be0eec +0 -0
  91. 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
- } else {
206
+ }
207
+ if (!stuck && status === PlayerStatus.STALLED) {
208
+ console.log("setting status to playing", status);
195
209
  setStatus(PlayerStatus.PLAYING);
196
210
  }
197
- }, [stuck, setStatus]);
211
+ }, [stuck, status]);
198
212
 
199
213
  const mediaStream = stream as unknown as MediaStream;
200
214
 
201
- useEffect(() => {
202
- if (!mediaStream) {
203
- setStatus(PlayerStatus.WAITING);
204
- return;
205
- }
206
- setStatus(PlayerStatus.PLAYING);
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, p } from "../../lib/theme/atoms";
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.warn("Browser forced video to start muted");
194
+ console.log("Muted play succeeded");
162
195
  setMuteWasForced(true);
163
196
  })
164
197
  .catch((err) => {
165
- console.error("error playing video", err);
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
- // if (localVideoRef && typeof localVideoRef !== "function") {
199
- // localVideoRef.current = videoElement;
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={event("canplay")}
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 && mediaStream) {
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={{ minWidth: "100%", minHeight: "100%" }}
492
+ style={{
493
+ minWidth: "100%",
494
+ minHeight: "100%",
495
+ display: "flex",
496
+ alignItems: "center",
497
+ justifyContent: "center",
498
+ }}
438
499
  >
439
500
  <View
440
- backgroundColor={colors.primary[800]}
441
- style={{ borderRadius: borderRadius.md }}
501
+ style={{
502
+ borderRadius: borderRadius.md,
503
+ padding: 24,
504
+ alignItems: "center",
505
+ gap: 16,
506
+ }}
442
507
  >
443
- <View>
444
- <Text>Connecting...</Text>
445
- </View>
446
- <Text>Establishing WebRTC connection ({connectionStatus})</Text>
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={colors.destructive[900]}
544
- style={[p[4], { borderRadius: borderRadius.md }]}
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
- <Text size="xl" weight="extrabold">
548
- Error encountered!
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="muted" />;
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 ? gap[2] : gap[1]]} ref={ref} {...rest}>
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[2],
382
- { borderRadius: borderRadius.lg, gap: 10 },
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
 
@@ -334,7 +334,6 @@ const primitiveStyles = StyleSheet.create({
334
334
  padding: 16,
335
335
  },
336
336
  content: {
337
- backgroundColor: "white",
338
337
  borderRadius: 8,
339
338
  overflow: "hidden",
340
339
  },
@@ -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
- hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
161
- style={[
162
- w[32],
163
- {
164
- height: 6,
165
- transform: [{ translateY: -10 }],
166
- backgroundColor: "#eeeeee66",
167
- alignItems: "center",
168
- justifyContent: "center",
169
- borderRadius: 999,
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";
@@ -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";