@streamplace/components 0.7.13 → 0.7.15

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 (122) hide show
  1. package/package.json +13 -16
  2. package/src/components/mobile-player/fullscreen.native.tsx +15 -20
  3. package/src/components/mobile-player/fullscreen.tsx +10 -2
  4. package/src/components/mobile-player/player.tsx +7 -1
  5. package/src/components/mobile-player/props.tsx +2 -0
  6. package/src/components/mobile-player/video.native.tsx +28 -11
  7. package/src/components/mobile-player/video.tsx +14 -3
  8. package/src/hooks/useLivestreamInfo.ts +6 -2
  9. package/src/lib/browser.ts +27 -0
  10. package/src/livestream-store/stream-key.tsx +1 -28
  11. package/src/streamplace-store/stream.tsx +51 -13
  12. package/dist/assets/emoji-data.json +0 -19371
  13. package/dist/components/chat/chat-box.js +0 -314
  14. package/dist/components/chat/chat-message.js +0 -87
  15. package/dist/components/chat/chat.js +0 -149
  16. package/dist/components/chat/emoji-suggestions.js +0 -35
  17. package/dist/components/chat/mention-suggestions.js +0 -42
  18. package/dist/components/chat/mod-view.js +0 -94
  19. package/dist/components/chat/system-message.js +0 -19
  20. package/dist/components/dashboard/chat-panel.js +0 -38
  21. package/dist/components/dashboard/header.js +0 -80
  22. package/dist/components/dashboard/index.js +0 -14
  23. package/dist/components/dashboard/information-widget.js +0 -234
  24. package/dist/components/dashboard/mod-actions.js +0 -71
  25. package/dist/components/dashboard/problems.js +0 -74
  26. package/dist/components/icons/bluesky-icon.js +0 -9
  27. package/dist/components/keep-awake.js +0 -7
  28. package/dist/components/keep-awake.native.js +0 -16
  29. package/dist/components/mobile-player/fullscreen.js +0 -74
  30. package/dist/components/mobile-player/fullscreen.native.js +0 -155
  31. package/dist/components/mobile-player/player.js +0 -94
  32. package/dist/components/mobile-player/props.js +0 -2
  33. package/dist/components/mobile-player/shared.js +0 -54
  34. package/dist/components/mobile-player/ui/countdown.js +0 -83
  35. package/dist/components/mobile-player/ui/index.js +0 -11
  36. package/dist/components/mobile-player/ui/input.js +0 -42
  37. package/dist/components/mobile-player/ui/metrics.js +0 -44
  38. package/dist/components/mobile-player/ui/report-modal.js +0 -90
  39. package/dist/components/mobile-player/ui/streamer-context-menu.js +0 -7
  40. package/dist/components/mobile-player/ui/streamer-loading-overlay.js +0 -104
  41. package/dist/components/mobile-player/ui/viewer-context-menu.js +0 -51
  42. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +0 -49
  43. package/dist/components/mobile-player/ui/viewers.js +0 -23
  44. package/dist/components/mobile-player/use-webrtc.js +0 -243
  45. package/dist/components/mobile-player/video-retry.js +0 -29
  46. package/dist/components/mobile-player/video.js +0 -460
  47. package/dist/components/mobile-player/video.native.js +0 -276
  48. package/dist/components/mobile-player/webrtc-diagnostics.js +0 -110
  49. package/dist/components/mobile-player/webrtc-primitives.js +0 -27
  50. package/dist/components/mobile-player/webrtc-primitives.native.js +0 -8
  51. package/dist/components/share/sharesheet.js +0 -91
  52. package/dist/components/ui/button.js +0 -223
  53. package/dist/components/ui/dialog.js +0 -206
  54. package/dist/components/ui/dropdown.js +0 -172
  55. package/dist/components/ui/icons.js +0 -25
  56. package/dist/components/ui/index.js +0 -34
  57. package/dist/components/ui/info-box.js +0 -31
  58. package/dist/components/ui/info-row.js +0 -23
  59. package/dist/components/ui/input.js +0 -205
  60. package/dist/components/ui/loader.js +0 -10
  61. package/dist/components/ui/primitives/button.js +0 -125
  62. package/dist/components/ui/primitives/input.js +0 -206
  63. package/dist/components/ui/primitives/modal.js +0 -206
  64. package/dist/components/ui/primitives/text.js +0 -292
  65. package/dist/components/ui/resizeable.js +0 -121
  66. package/dist/components/ui/slider.js +0 -5
  67. package/dist/components/ui/text.js +0 -177
  68. package/dist/components/ui/textarea.js +0 -19
  69. package/dist/components/ui/toast.js +0 -175
  70. package/dist/components/ui/view.js +0 -252
  71. package/dist/hooks/index.js +0 -14
  72. package/dist/hooks/useAvatars.js +0 -35
  73. package/dist/hooks/useCameraToggle.js +0 -12
  74. package/dist/hooks/useKeyboard.js +0 -36
  75. package/dist/hooks/useKeyboardSlide.js +0 -14
  76. package/dist/hooks/useLivestreamInfo.js +0 -65
  77. package/dist/hooks/useOuterAndInnerDimensions.js +0 -30
  78. package/dist/hooks/usePlayerDimensions.js +0 -22
  79. package/dist/hooks/usePointerDevice.js +0 -71
  80. package/dist/hooks/useSegmentDimensions.js +0 -17
  81. package/dist/hooks/useSegmentTiming.js +0 -65
  82. package/dist/index.js +0 -34
  83. package/dist/lib/facet.js +0 -92
  84. package/dist/lib/system-messages.js +0 -101
  85. package/dist/lib/theme/atoms.js +0 -646
  86. package/dist/lib/theme/atoms.types.js +0 -6
  87. package/dist/lib/theme/index.js +0 -35
  88. package/dist/lib/theme/theme.js +0 -256
  89. package/dist/lib/theme/tokens.js +0 -659
  90. package/dist/lib/utils.js +0 -105
  91. package/dist/livestream-provider/index.js +0 -30
  92. package/dist/livestream-provider/websocket.js +0 -45
  93. package/dist/livestream-store/chat.js +0 -286
  94. package/dist/livestream-store/context.js +0 -5
  95. package/dist/livestream-store/index.js +0 -7
  96. package/dist/livestream-store/livestream-state.js +0 -2
  97. package/dist/livestream-store/livestream-store.js +0 -58
  98. package/dist/livestream-store/problems.js +0 -76
  99. package/dist/livestream-store/stream-key.js +0 -119
  100. package/dist/livestream-store/websocket-consumer.js +0 -94
  101. package/dist/player-store/context.js +0 -5
  102. package/dist/player-store/index.js +0 -9
  103. package/dist/player-store/player-provider.js +0 -57
  104. package/dist/player-store/player-state.js +0 -25
  105. package/dist/player-store/player-store.js +0 -199
  106. package/dist/player-store/single-player-provider.js +0 -121
  107. package/dist/streamplace-provider/context.js +0 -5
  108. package/dist/streamplace-provider/index.js +0 -20
  109. package/dist/streamplace-provider/poller.js +0 -49
  110. package/dist/streamplace-provider/xrpc.js +0 -0
  111. package/dist/streamplace-store/block.js +0 -65
  112. package/dist/streamplace-store/index.js +0 -6
  113. package/dist/streamplace-store/stream.js +0 -218
  114. package/dist/streamplace-store/streamplace-store.js +0 -47
  115. package/dist/streamplace-store/user.js +0 -52
  116. package/dist/streamplace-store/xrpc.js +0 -15
  117. package/dist/ui/index.js +0 -79
  118. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  119. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  120. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  121. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  122. package/tsconfig.tsbuildinfo +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.13",
3
+ "version": "0.7.15",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -11,17 +11,11 @@
11
11
  "default": "./dist/index.js"
12
12
  }
13
13
  },
14
- "scripts": {
15
- "build": "tsc",
16
- "prepare": "pnpm run build",
17
- "start": "tsc --watch --preserveWatchOutput"
18
- },
19
14
  "keywords": [
20
15
  "streamplace"
21
16
  ],
22
17
  "author": "Streamplace",
23
18
  "license": "MIT",
24
- "packageManager": "pnpm@10.11.0",
25
19
  "devDependencies": {
26
20
  "tsup": "^8.5.0"
27
21
  },
@@ -30,21 +24,20 @@
30
24
  "@atproto/crypto": "^0.4.4",
31
25
  "@emoji-mart/react": "^1.1.1",
32
26
  "@gorhom/bottom-sheet": "^5.1.6",
33
- "@react-navigation/native": "^6.1.18",
34
27
  "@rn-primitives/dropdown-menu": "^1.2.0",
35
28
  "@rn-primitives/portal": "^1.3.0",
36
29
  "@rn-primitives/slider": "^1.2.0",
37
30
  "class-variance-authority": "^0.6.1",
38
- "expo-keep-awake": "~14.1.4",
39
- "expo-video": "~2.2.1",
31
+ "expo-keep-awake": "^14.0.0",
32
+ "expo-video": "^2.0.0",
40
33
  "hls.js": "^1.5.17",
41
34
  "lucide-react-native": "^0.514.0",
42
- "react-native": "0.79.3",
35
+ "react-native": "^0.79.0",
43
36
  "react-native-edge-to-edge": "^1.6.2",
44
- "react-native-gesture-handler": "~2.26.0",
45
- "react-native-reanimated": "~3.18.0",
46
- "react-native-safe-area-context": "5.4.1",
47
- "react-native-svg": "15.12.0",
37
+ "react-native-gesture-handler": "^2.20.0",
38
+ "react-native-reanimated": "^3.0.0",
39
+ "react-native-safe-area-context": "^5.0.0",
40
+ "react-native-svg": "^15.0.0",
48
41
  "react-native-webrtc": "git+https://github.com/streamplace/react-native-webrtc.git#6b8472a771ac47f89217d327058a8a4124a6ae56",
49
42
  "react-use-websocket": "^4.13.0",
50
43
  "streamplace": "0.7.2",
@@ -54,5 +47,9 @@
54
47
  "peerDependencies": {
55
48
  "react": "*"
56
49
  },
57
- "gitHead": "2cf5c37b35ec76d6f06872939690e95de81bd257"
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "start": "tsc --watch --preserveWatchOutput"
53
+ },
54
+ "gitHead": "28aa086a00a3bf52bb21c5e50d139a879bcb6bc7"
58
55
  }
@@ -1,4 +1,3 @@
1
- import { useNavigation } from "@react-navigation/native";
2
1
  import { VideoView } from "expo-video";
3
2
  import { useEffect, useRef, useState } from "react";
4
3
  import { BackHandler, Dimensions, StyleSheet, View } from "react-native";
@@ -15,10 +14,14 @@ import Video from "./video.native";
15
14
  // Standard 16:9 video aspect ratio
16
15
  const VIDEO_ASPECT_RATIO = 16 / 9;
17
16
 
18
- export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
17
+ export function Fullscreen(props: {
18
+ src: string;
19
+ children?: React.ReactNode;
20
+ objectFit?: "contain" | "cover";
21
+ pictureInPictureEnabled?: boolean;
22
+ }) {
19
23
  const ref = useRef<VideoView>(null);
20
24
  const insets = useSafeAreaInsets();
21
- const navigation = useNavigation();
22
25
  const [dimensions, setDimensions] = useState(Dimensions.get("window"));
23
26
 
24
27
  // Get state from player store
@@ -55,11 +58,6 @@ export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
55
58
  SystemBars.setHidden(true);
56
59
  console.log("setting sidebar hidden");
57
60
 
58
- // Hide the navigation header
59
- navigation.setOptions({
60
- headerShown: false,
61
- });
62
-
63
61
  // Handle hardware back button
64
62
  const backHandler = BackHandler.addEventListener(
65
63
  "hardwareBackPress",
@@ -74,21 +72,12 @@ export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
74
72
  };
75
73
  } else {
76
74
  SystemBars.setHidden(false);
77
-
78
- // Restore the navigation header
79
- navigation.setOptions({
80
- headerShown: true,
81
- });
82
75
  }
83
76
 
84
77
  return () => {
85
78
  SystemBars.setHidden(false);
86
- // Ensure header is restored if component unmounts
87
- navigation.setOptions({
88
- headerShown: true,
89
- });
90
79
  };
91
- }, [fullscreen, navigation, setFullscreen]);
80
+ }, [fullscreen, setFullscreen]);
92
81
 
93
82
  // Handle fullscreen state changes for native video players
94
83
  useEffect(() => {
@@ -161,7 +150,10 @@ export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
161
150
  },
162
151
  ]}
163
152
  >
164
- <Video />
153
+ <Video
154
+ objectFit={props.objectFit}
155
+ pictureInPictureEnabled={props.pictureInPictureEnabled}
156
+ />
165
157
  {props.children}
166
158
  </View>
167
159
  </View>
@@ -172,7 +164,10 @@ export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
172
164
  return (
173
165
  <>
174
166
  <VideoRetry>
175
- <Video />
167
+ <Video
168
+ objectFit={props.objectFit}
169
+ pictureInPictureEnabled={props.pictureInPictureEnabled}
170
+ />
176
171
  </VideoRetry>
177
172
  {props.children}
178
173
  </>
@@ -5,7 +5,12 @@ import { View } from "../../components/ui";
5
5
  import Video from "./video";
6
6
  import VideoRetry from "./video-retry";
7
7
 
8
- export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
8
+ export function Fullscreen(props: {
9
+ src: string;
10
+ children?: React.ReactNode;
11
+ objectFit?: "contain" | "cover";
12
+ pictureInPictureEnabled?: boolean;
13
+ }) {
9
14
  const playerId = getFirstPlayerID();
10
15
  const protocol = usePlayerStore((x) => x.protocol, playerId);
11
16
  const fullscreen = usePlayerStore((x) => x.fullscreen, playerId);
@@ -78,7 +83,10 @@ export function Fullscreen(props: { src: string; children?: React.ReactNode }) {
78
83
  style={{ width: "100%", height: "100%", overflow: "hidden" }}
79
84
  >
80
85
  <VideoRetry>
81
- <Video />
86
+ <Video
87
+ objectFit={props.objectFit}
88
+ pictureInPictureEnabled={props.pictureInPictureEnabled}
89
+ />
82
90
  </VideoRetry>
83
91
  {props.children}
84
92
  </View>
@@ -69,7 +69,13 @@ export function Player(
69
69
  onOpenChange={setReportModalOpen}
70
70
  subject={reportSubject!}
71
71
  />
72
- <Fullscreen src={props.src}>{props.children}</Fullscreen>
72
+ <Fullscreen
73
+ src={props.src}
74
+ objectFit={props.objectFit}
75
+ pictureInPictureEnabled={props.pictureInPictureEnabled}
76
+ >
77
+ {props.children}
78
+ </Fullscreen>
73
79
  </View>
74
80
  </>
75
81
  );
@@ -9,4 +9,6 @@ export type PlayerProps = {
9
9
  ingest?: boolean;
10
10
  embedded?: boolean;
11
11
  reportingURL?: string;
12
+ objectFit?: "contain" | "cover";
13
+ pictureInPictureEnabled?: boolean;
12
14
  };
@@ -34,24 +34,36 @@ import useWebRTC, { useWebRTCIngest } from "./use-webrtc";
34
34
  import { mediaDevices, WebRTCMediaStream } from "./webrtc-primitives.native";
35
35
 
36
36
  // Add NativeIngestPlayer to the switch below!
37
- export default function VideoNative() {
37
+ export default function VideoNative(props?: {
38
+ objectFit?: "contain" | "cover";
39
+ pictureInPictureEnabled?: boolean;
40
+ }) {
38
41
  const protocol = usePlayerStore((x) => x.protocol);
39
42
  const ingest = usePlayerStore((x) => x.ingestConnectionState) != null;
40
43
 
41
44
  return (
42
45
  <View>
43
46
  {ingest ? (
44
- <NativeIngestPlayer />
47
+ <NativeIngestPlayer objectFit={props?.objectFit} />
45
48
  ) : protocol === PlayerProtocol.WEBRTC ? (
46
- <NativeWHEP />
49
+ <NativeWHEP
50
+ objectFit={props?.objectFit}
51
+ pictureInPictureEnabled={props?.pictureInPictureEnabled}
52
+ />
47
53
  ) : (
48
- <NativeVideo />
54
+ <NativeVideo
55
+ objectFit={props?.objectFit}
56
+ pictureInPictureEnabled={props?.pictureInPictureEnabled}
57
+ />
49
58
  )}
50
59
  </View>
51
60
  );
52
61
  }
53
62
 
54
- export function NativeVideo() {
63
+ export function NativeVideo(props?: {
64
+ objectFit?: "contain" | "cover" | "fill" | "none" | "scale-down";
65
+ pictureInPictureEnabled?: boolean;
66
+ }) {
55
67
  const videoRef = useRef<VideoView | null>(null);
56
68
  const protocol = usePlayerStore((x) => x.protocol);
57
69
 
@@ -159,14 +171,17 @@ export function NativeVideo() {
159
171
  onFullscreenExit={() => {
160
172
  setFullscreen(false);
161
173
  }}
162
- allowsPictureInPicture
174
+ allowsPictureInPicture={props?.pictureInPictureEnabled !== false}
163
175
  onLayout={handleLayout}
164
176
  />
165
177
  </>
166
178
  );
167
179
  }
168
180
 
169
- export function NativeWHEP() {
181
+ export function NativeWHEP(props?: {
182
+ objectFit?: "contain" | "cover";
183
+ pictureInPictureEnabled?: boolean;
184
+ }) {
170
185
  const selectedRendition = usePlayerStore((x) => x.selectedRendition);
171
186
  const src = usePlayerStore((x) => x.src);
172
187
  const { url } = srcToUrl(
@@ -246,10 +261,10 @@ export function NativeWHEP() {
246
261
  <>
247
262
  <RTCView
248
263
  mirror={false}
249
- objectFit={"contain"}
264
+ objectFit={props?.objectFit || "contain"}
250
265
  streamURL={mediaStream.toURL()}
251
266
  onLayout={handleLayout}
252
- pictureInPictureEnabled={true}
267
+ pictureInPictureEnabled={props?.pictureInPictureEnabled !== false}
253
268
  autoStartPictureInPicture={true}
254
269
  pictureInPicturePreferredSize={{
255
270
  width: 160,
@@ -265,7 +280,9 @@ export function NativeWHEP() {
265
280
  );
266
281
  }
267
282
 
268
- export function NativeIngestPlayer() {
283
+ export function NativeIngestPlayer(props?: {
284
+ objectFit?: "contain" | "cover";
285
+ }) {
269
286
  const ingestStarting = useIngestPlayerStore((x) => x.ingestStarting);
270
287
  const ingestMediaSource = useIngestPlayerStore((x) => x.ingestMediaSource);
271
288
  const ingestAutoStart = useIngestPlayerStore((x) => x.ingestAutoStart);
@@ -406,7 +423,7 @@ export function NativeIngestPlayer() {
406
423
  return (
407
424
  <RTCViewIngest
408
425
  mirror={ingestCamera !== "environment"}
409
- objectFit={"contain"}
426
+ objectFit={props?.objectFit || "contain"}
410
427
  streamURL={localMediaStream.toURL()}
411
428
  zOrder={0}
412
429
  style={{
@@ -34,6 +34,8 @@ function assignVideoRef(
34
34
  type VideoProps = {
35
35
  url: string;
36
36
  videoRef?: React.RefObject<HTMLVideoElement>;
37
+ objectFit?: "contain" | "cover";
38
+ pictureInPictureEnabled?: boolean;
37
39
  };
38
40
 
39
41
  function useVideoDimensions(videoRef: React.RefObject<HTMLVideoElement>) {
@@ -67,7 +69,10 @@ function useVideoDimensions(videoRef: React.RefObject<HTMLVideoElement>) {
67
69
  return dimensions;
68
70
  }
69
71
 
70
- export default function WebVideo() {
72
+ export default function WebVideo(props?: {
73
+ objectFit?: "contain" | "cover";
74
+ pictureInPictureEnabled?: boolean;
75
+ }) {
71
76
  const inProto = usePlayerStore((x) => x.protocol);
72
77
  const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null);
73
78
  const selectedRendition = usePlayerStore((x) => x.selectedRendition);
@@ -86,7 +91,12 @@ export default function WebVideo() {
86
91
  }
87
92
  }, [dimensions, setPlayerWidth, setPlayerHeight]);
88
93
 
89
- const playerProps = { url, videoRef };
94
+ const playerProps = {
95
+ url,
96
+ videoRef,
97
+ objectFit: props?.objectFit,
98
+ pictureInPictureEnabled: props?.pictureInPictureEnabled,
99
+ };
90
100
 
91
101
  return (
92
102
  <>
@@ -274,7 +284,7 @@ const VideoElement = forwardRef<
274
284
  onVolumeChange={event("volumechange")}
275
285
  onWaiting={event("waiting")}
276
286
  style={{
277
- objectFit: "contain",
287
+ objectFit: props.objectFit || "contain",
278
288
  backgroundColor: "transparent",
279
289
  width: "100%",
280
290
  height: "100%",
@@ -282,6 +292,7 @@ const VideoElement = forwardRef<
282
292
  maxHeight: "100%",
283
293
  transform: ingest ? "scaleX(-1)" : undefined,
284
294
  }}
295
+ disablePictureInPicture={props.pictureInPictureEnabled === false}
285
296
  />
286
297
  );
287
298
  });
@@ -3,7 +3,7 @@ import { useLivestreamStore } from "../livestream-store";
3
3
  import { usePlayerStore } from "../player-store";
4
4
  import { useCreateStreamRecord } from "../streamplace-store";
5
5
 
6
- export function useLivestreamInfo() {
6
+ export function useLivestreamInfo(url?: string) {
7
7
  const ingest = usePlayerStore((x) => x.ingestConnectionState);
8
8
  const profile = useLivestreamStore((x) => x.profile);
9
9
  const ingestStarting = usePlayerStore((x) => x.ingestStarting);
@@ -20,7 +20,11 @@ export function useLivestreamInfo() {
20
20
  try {
21
21
  if (title !== "") {
22
22
  setRecordSubmitted(true);
23
- await createStreamRecord(title);
23
+ // Create the livestream record with title and custom url if available
24
+ await createStreamRecord({
25
+ title,
26
+ customUrl: url || null,
27
+ });
24
28
  }
25
29
  } catch (error) {
26
30
  console.error("Error creating livestream:", error);
@@ -0,0 +1,27 @@
1
+ export function getBrowserName(userAgent: string) {
2
+ // The order matters here, and this may report false positives for unlisted browsers.
3
+
4
+ if (userAgent.includes("Firefox")) {
5
+ // "Mozilla/5.0 (X11; Linux i686; rv:104.0) Gecko/20100101 Firefox/104.0"
6
+ return "Mozilla Firefox";
7
+ } else if (userAgent.includes("SamsungBrowser")) {
8
+ // "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G955F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.4 Chrome/67.0.3396.87 Mobile Safari/537.36"
9
+ return "Samsung Internet";
10
+ } else if (userAgent.includes("Opera") || userAgent.includes("OPR")) {
11
+ // "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 OPR/90.0.4480.54"
12
+ return "Opera";
13
+ } else if (userAgent.includes("Edge")) {
14
+ // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"
15
+ return "Microsoft Edge (Legacy)";
16
+ } else if (userAgent.includes("Edg")) {
17
+ // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Edg/104.0.1293.70"
18
+ return "Microsoft Edge (Chromium)";
19
+ } else if (userAgent.includes("Chrome")) {
20
+ // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
21
+ return "Google Chrome or Chromium";
22
+ } else if (userAgent.includes("Safari")) {
23
+ // "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1"
24
+ return "Apple Safari";
25
+ }
26
+ return "unknown";
27
+ }
@@ -3,37 +3,10 @@ import { useEffect, useState } from "react";
3
3
  import { Platform } from "react-native";
4
4
  import { PlaceStreamKey } from "streamplace";
5
5
  import { privateKeyToAccount } from "viem/accounts";
6
+ import { getBrowserName } from "../lib/browser";
6
7
  import { usePDSAgent } from "../streamplace-store/xrpc";
7
8
  import { useLivestreamStore } from "./livestream-store";
8
9
 
9
- function getBrowserName(userAgent: string) {
10
- // The order matters here, and this may report false positives for unlisted browsers.
11
-
12
- if (userAgent.includes("Firefox")) {
13
- // "Mozilla/5.0 (X11; Linux i686; rv:104.0) Gecko/20100101 Firefox/104.0"
14
- return "Mozilla Firefox";
15
- } else if (userAgent.includes("SamsungBrowser")) {
16
- // "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G955F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.4 Chrome/67.0.3396.87 Mobile Safari/537.36"
17
- return "Samsung Internet";
18
- } else if (userAgent.includes("Opera") || userAgent.includes("OPR")) {
19
- // "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 OPR/90.0.4480.54"
20
- return "Opera";
21
- } else if (userAgent.includes("Edge")) {
22
- // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"
23
- return "Microsoft Edge (Legacy)";
24
- } else if (userAgent.includes("Edg")) {
25
- // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Edg/104.0.1293.70"
26
- return "Microsoft Edge (Chromium)";
27
- } else if (userAgent.includes("Chrome")) {
28
- // "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
29
- return "Google Chrome or Chromium";
30
- } else if (userAgent.includes("Safari")) {
31
- // "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1"
32
- return "Apple Safari";
33
- }
34
- return "unknown";
35
- }
36
-
37
10
  export const useStreamKey = (): {
38
11
  streamKey: {
39
12
  privateKey: string;
@@ -6,7 +6,11 @@ import { LivestreamViewHydrated } from "streamplace/src/useful-types";
6
6
  import { useUrl } from "./streamplace-store";
7
7
  import { usePDSAgent } from "./xrpc";
8
8
 
9
+ import PackageJson from "../../package.json";
10
+
9
11
  import { useEffect, useRef } from "react";
12
+ import { Platform } from "react-native";
13
+ import { getBrowserName } from "../lib/browser";
10
14
 
11
15
  const useUploadThumbnail = () => {
12
16
  const abortRef = useRef<AbortController | null>(null);
@@ -123,11 +127,23 @@ export function useCreateStreamRecord() {
123
127
  let url = useUrl();
124
128
  const uploadThumbnail = useUploadThumbnail();
125
129
 
126
- return async (
127
- title: string,
128
- customThumbnail?: Blob,
129
- submitPost: boolean = true,
130
- ) => {
130
+ return async ({
131
+ title,
132
+ customThumbnail,
133
+ submitPost,
134
+ customUrl,
135
+ }: {
136
+ title: string;
137
+ customThumbnail?: Blob;
138
+ submitPost?: boolean;
139
+ customUrl?: string | null;
140
+ }) => {
141
+ if (!submitPost) {
142
+ submitPost = true;
143
+ }
144
+ if (!customUrl) {
145
+ customUrl = null;
146
+ }
131
147
  if (!agent) {
132
148
  throw new Error("No PDS agent found");
133
149
  }
@@ -136,9 +152,11 @@ export function useCreateStreamRecord() {
136
152
  throw new Error("No user DID found, assuming not logged in");
137
153
  }
138
154
 
139
- let thumbnail: BlobRef | undefined = undefined;
155
+ // Use customUrl if provided, otherwise fall back to the store URL
156
+ const finalUrl = customUrl || url;
157
+ const u = new URL(finalUrl);
140
158
 
141
- const u = new URL(url);
159
+ let thumbnail: BlobRef | undefined = undefined;
142
160
 
143
161
  if (customThumbnail) {
144
162
  try {
@@ -185,10 +203,10 @@ export function useCreateStreamRecord() {
185
203
 
186
204
  let newPost: undefined | { uri: string; cid: string } = undefined;
187
205
 
188
- if (submitPost) {
189
- const did = agent.did;
190
- const profile = await agent.getProfile({ actor: did });
206
+ const did = agent.did;
207
+ const profile = await agent.getProfile({ actor: did });
191
208
 
209
+ if (submitPost) {
192
210
  if (!profile) {
193
211
  throw new Error("No profile found for the user DID");
194
212
  }
@@ -216,10 +234,27 @@ export function useCreateStreamRecord() {
216
234
  }
217
235
  }
218
236
 
237
+ // get version? only works on andoid/ios
238
+ //
239
+ let platform: string = Platform.OS;
240
+ let platVersion: string = Platform.Version.toString();
241
+ if (
242
+ platform === "web" &&
243
+ typeof window !== "undefined" &&
244
+ window.navigator
245
+ ) {
246
+ platVersion = getBrowserName(window.navigator.userAgent);
247
+ }
248
+
219
249
  const record: PlaceStreamLivestream.Record = {
220
250
  title: title,
221
- url: url,
251
+ url: finalUrl,
222
252
  createdAt: new Date().toISOString(),
253
+ // would match up with e.g. https://stream.place/iame.li
254
+ canonicalUrl: `${finalUrl}/${profile.data.handle}`,
255
+ // user agent style string
256
+ // e.g. `@streamplace/components/0.1.0 (ios, 32.0)`
257
+ agent: `@streamplace/components/${PackageJson.version} (${platform}, ${platVersion})`,
223
258
  post: newPost,
224
259
  thumb: thumbnail,
225
260
  };
@@ -233,7 +268,7 @@ export function useCreateStreamRecord() {
233
268
  };
234
269
  }
235
270
 
236
- export function useUpdateStreamRecord() {
271
+ export function useUpdateStreamRecord(customUrl: string | null = null) {
237
272
  let agent = usePDSAgent();
238
273
  let url = useUrl();
239
274
  const uploadThumbnail = useUploadThumbnail();
@@ -255,6 +290,9 @@ export function useUpdateStreamRecord() {
255
290
  throw new Error("No latest record");
256
291
  }
257
292
 
293
+ // Use customUrl if provided, otherwise fall back to the store URL
294
+ const finalUrl = customUrl || url;
295
+
258
296
  let rkey = livestream.uri.split("/").pop();
259
297
  let oldRecordValue: PlaceStreamLivestream.Record = livestream.record;
260
298
 
@@ -275,7 +313,7 @@ export function useUpdateStreamRecord() {
275
313
 
276
314
  const record: PlaceStreamLivestream.Record = {
277
315
  title: title,
278
- url: url,
316
+ url: finalUrl,
279
317
  createdAt: new Date().toISOString(),
280
318
  post: oldRecordValue.post,
281
319
  thumb: thumbnail,