@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
@@ -1,3 +1,4 @@
1
+ import { ComAtprotoModerationCreateReport } from "@atproto/api";
1
2
  import { ChatMessageViewHydrated } from "streamplace";
2
3
 
3
4
  export enum PlayerProtocol {
@@ -99,10 +100,6 @@ export interface PlayerState {
99
100
  setPlayTime: (playTime: number) => void;
100
101
 
101
102
  /** Flag indicating if player is in offline state */
102
- offline: boolean;
103
-
104
- /** Function to set the offline state */
105
- setOffline: (offline: boolean) => void;
106
103
  /** Reference to the video element for direct manipulation (used for PiP) */
107
104
  videoRef:
108
105
  | React.MutableRefObject<HTMLVideoElement | null>
@@ -119,6 +116,10 @@ export interface PlayerState {
119
116
  | undefined,
120
117
  ) => void;
121
118
 
119
+ pipAction: (() => void) | undefined;
120
+ /** Function to set the Picture-in-Picture action */
121
+ setPipAction: (action: (() => void) | undefined) => void;
122
+
122
123
  /** Player element width (CSS value or number) */
123
124
  playerWidth?: string | number;
124
125
  /** Function to set the player width */
@@ -176,6 +177,26 @@ export interface PlayerState {
176
177
 
177
178
  /** Function to set the mod message */
178
179
  setModMessage: (message: ChatMessageViewHydrated | null) => void;
180
+
181
+ /** URL to send player events to (if not default) */
182
+ reportingURL: string | null;
183
+
184
+ /** Function to set the reporting URL */
185
+ setReportingURL: (reportingURL: string | null) => void;
186
+
187
+ /** Is the report modal open? */
188
+ reportModalOpen: boolean;
189
+
190
+ /** Function to set the report modal open state */
191
+ setReportModalOpen: (reportModalOpen: boolean) => void;
192
+
193
+ /** Subject to report */
194
+ reportSubject: ComAtprotoModerationCreateReport.InputSchema["subject"] | null;
195
+
196
+ /** Function to set the report subject */
197
+ setReportSubject: (
198
+ subject: ComAtprotoModerationCreateReport.InputSchema["subject"] | null,
199
+ ) => void;
179
200
  }
180
201
 
181
202
  export type PlayerEvent = {
@@ -1,6 +1,8 @@
1
- import { useContext } from "react";
1
+ import { ComAtprotoModerationCreateReport } from "@atproto/api";
2
+ import { useContext, useEffect, useState } from "react";
2
3
  import { ChatMessageViewHydrated } from "streamplace";
3
4
  import { createStore, StoreApi, useStore } from "zustand";
5
+ import { useLivestreamStore } from "../livestream-store";
4
6
  import { PlayerContext } from "./context";
5
7
  import {
6
8
  IngestMediaSource,
@@ -68,9 +70,6 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
68
70
  playTime: 0,
69
71
  setPlayTime: (playTime: number) => set(() => ({ playTime })),
70
72
 
71
- offline: false,
72
- setOffline: (offline: boolean) => set(() => ({ offline })),
73
-
74
73
  videoRef: undefined,
75
74
  setVideoRef: (
76
75
  videoRef:
@@ -83,6 +82,11 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
83
82
  pipMode: false,
84
83
  setPipMode: (pipMode: boolean) => set(() => ({ pipMode })),
85
84
 
85
+ // Picture-in-Picture action function (set by player component)
86
+ pipAction: undefined,
87
+ setPipAction: (action: (() => void) | undefined) =>
88
+ set(() => ({ pipAction: action })),
89
+
86
90
  // Player element width/height setters for global sync
87
91
  playerWidth: undefined,
88
92
  setPlayerWidth: (playerWidth: number) => set(() => ({ playerWidth })),
@@ -109,6 +113,10 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
109
113
  ingestLive: false,
110
114
  setIngestLive: (ingestLive: boolean) => set(() => ({ ingestLive })),
111
115
 
116
+ reportingURL: null,
117
+ setReportingURL: (reportingURL: string | null) =>
118
+ set(() => ({ reportingURL })),
119
+
112
120
  playerEvent: async (
113
121
  url: string,
114
122
  time: string,
@@ -126,7 +134,8 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
126
134
  };
127
135
  try {
128
136
  // fetch url from sp provider
129
- fetch(`${url}/api/player-event`, {
137
+ const reportingURL = x.reportingURL ?? `${url}/api/player-event`;
138
+ fetch(reportingURL, {
130
139
  method: "POST",
131
140
  body: JSON.stringify(data),
132
141
  });
@@ -163,6 +172,15 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
163
172
  modMessage: null,
164
173
  setModMessage: (modMessage: ChatMessageViewHydrated | null) =>
165
174
  set(() => ({ modMessage })),
175
+
176
+ reportModalOpen: false,
177
+ setReportModalOpen: (reportModalOpen: boolean) =>
178
+ set(() => ({ reportModalOpen })),
179
+
180
+ reportSubject: null,
181
+ setReportSubject: (
182
+ subject: ComAtprotoModerationCreateReport.InputSchema["subject"] | null,
183
+ ) => set(() => ({ reportSubject: subject })),
166
184
  }));
167
185
  };
168
186
 
@@ -237,3 +255,23 @@ export const intoPlayerProtocol = (protocol: string): PlayerProtocol => {
237
255
  return PlayerProtocol.WEBRTC;
238
256
  }
239
257
  };
258
+
259
+ // returns true if the livestream has been offline for more than 10 seconds and we're not playing
260
+ export const useOffline = () => {
261
+ const status = usePlayerStore((x) => x.status);
262
+ const segment = useLivestreamStore((x) => x.segment);
263
+ const [now, setNow] = useState(Date.now());
264
+ useEffect(() => {
265
+ const interval = setInterval(() => {
266
+ setNow(Date.now());
267
+ }, 500);
268
+ return () => clearInterval(interval);
269
+ }, []);
270
+ if (status === PlayerStatus.PLAYING) {
271
+ return false;
272
+ }
273
+ if (!segment?.startTime) {
274
+ return false;
275
+ }
276
+ return now - Date.parse(segment.startTime) > 10000;
277
+ };
@@ -1,10 +1,12 @@
1
1
  import { AppBskyGraphBlock } from "@atproto/api";
2
+ import { useState } from "react";
2
3
  import { usePDSAgent } from "./xrpc";
3
4
 
4
5
  export function useCreateBlockRecord() {
5
6
  let agent = usePDSAgent();
7
+ const [isLoading, setIsLoading] = useState(false);
6
8
 
7
- return async (subjectDID: string) => {
9
+ const createBlock = async (subjectDID: string) => {
8
10
  if (!agent) {
9
11
  throw new Error("No PDS agent found");
10
12
  }
@@ -13,17 +15,57 @@ export function useCreateBlockRecord() {
13
15
  throw new Error("No user DID found, assuming not logged in");
14
16
  }
15
17
 
16
- const record: AppBskyGraphBlock.Record = {
17
- $type: "app.bsky.graph.block",
18
- subject: subjectDID,
19
- createdAt: new Date().toISOString(),
20
- };
21
- return await agent.com.atproto.repo.createRecord({
22
- repo: agent.did,
23
- collection: "app.bsky.graph.block",
24
- record,
25
- });
26
-
27
- return record;
18
+ setIsLoading(true);
19
+ try {
20
+ const record: AppBskyGraphBlock.Record = {
21
+ $type: "app.bsky.graph.block",
22
+ subject: subjectDID,
23
+ createdAt: new Date().toISOString(),
24
+ };
25
+ const result = await agent.com.atproto.repo.createRecord({
26
+ repo: agent.did,
27
+ collection: "app.bsky.graph.block",
28
+ record,
29
+ });
30
+ return result;
31
+ } finally {
32
+ setIsLoading(false);
33
+ }
34
+ };
35
+
36
+ return { createBlock, isLoading };
37
+ }
38
+
39
+ export function useCreateHideChatRecord() {
40
+ let agent = usePDSAgent();
41
+ const [isLoading, setIsLoading] = useState(false);
42
+
43
+ const createHideChat = async (chatMessageUri: string) => {
44
+ if (!agent) {
45
+ throw new Error("No PDS agent found");
46
+ }
47
+
48
+ if (!agent.did) {
49
+ throw new Error("No user DID found, assuming not logged in");
50
+ }
51
+
52
+ setIsLoading(true);
53
+ try {
54
+ const record = {
55
+ $type: "place.stream.chat.gate",
56
+ hiddenMessage: chatMessageUri,
57
+ };
58
+
59
+ const result = await agent.com.atproto.repo.createRecord({
60
+ repo: agent.did,
61
+ collection: "place.stream.chat.gate",
62
+ record,
63
+ });
64
+ return result;
65
+ } finally {
66
+ setIsLoading(false);
67
+ }
28
68
  };
69
+
70
+ return { createHideChat, isLoading };
29
71
  }
@@ -6,41 +6,62 @@ import { LivestreamViewHydrated } from "streamplace/src/useful-types";
6
6
  import { useUrl } from "./streamplace-store";
7
7
  import { usePDSAgent } from "./xrpc";
8
8
 
9
- const uploadThumbnail = async (
10
- pdsAgent: StreamplaceAgent,
11
- customThumbnail?: Blob,
12
- ) => {
13
- if (customThumbnail) {
14
- let tries = 0;
15
- try {
16
- let thumbnail = await pdsAgent.uploadBlob(customThumbnail);
17
-
18
- while (
19
- thumbnail.data.blob.size === 0 &&
20
- customThumbnail.size !== 0 &&
21
- tries < 3
22
- ) {
23
- console.warn(
24
- "Reuploading blob as blob sizes don't match! Blob size recieved is",
25
- thumbnail.data.blob.size,
26
- "and sent blob size is",
27
- customThumbnail.size,
28
- );
29
- thumbnail = await pdsAgent.uploadBlob(customThumbnail);
30
- }
9
+ import { useEffect, useRef } from "react";
31
10
 
32
- if (tries === 3) {
33
- throw new Error("Could not successfully upload blob (tried thrice)");
34
- }
11
+ const useUploadThumbnail = () => {
12
+ const abortRef = useRef<AbortController | null>(null);
13
+
14
+ useEffect(() => {
15
+ return () => {
16
+ // On unmount, abort any ongoing upload
17
+ abortRef.current?.abort();
18
+ };
19
+ }, []);
20
+
21
+ const uploadThumbnail = async (
22
+ pdsAgent: StreamplaceAgent,
23
+ customThumbnail?: Blob,
24
+ ) => {
25
+ if (!customThumbnail) return undefined;
26
+
27
+ abortRef.current = new AbortController();
28
+ const { signal } = abortRef.current;
29
+
30
+ const maxTries = 3;
31
+ let lastError: unknown = null;
35
32
 
36
- if (thumbnail.success) {
37
- console.log("Successfully uploaded thumbnail");
38
- return thumbnail.data.blob;
33
+ for (let tries = 0; tries < maxTries; tries++) {
34
+ try {
35
+ const thumbnail = await pdsAgent.uploadBlob(customThumbnail, {
36
+ signal,
37
+ });
38
+ if (
39
+ thumbnail.success &&
40
+ thumbnail.data.blob.size === customThumbnail.size
41
+ ) {
42
+ console.log("Successfully uploaded thumbnail");
43
+ return thumbnail.data.blob;
44
+ } else {
45
+ console.warn(
46
+ `Blob size mismatch (attempt ${tries + 1}): received ${thumbnail.data.blob.size}, expected ${customThumbnail.size}`,
47
+ );
48
+ }
49
+ } catch (e) {
50
+ if (signal.aborted) {
51
+ console.warn("Upload aborted");
52
+ return undefined;
53
+ }
54
+ lastError = e;
55
+ console.warn(`Error uploading thumbnail (attempt ${tries + 1}): ${e}`);
39
56
  }
40
- } catch (e) {
41
- throw new Error("Error uploading thumbnail: " + e);
42
57
  }
43
- }
58
+
59
+ throw new Error(
60
+ `Could not successfully upload blob after ${maxTries} attempts. Last error: ${lastError}`,
61
+ );
62
+ };
63
+
64
+ return uploadThumbnail;
44
65
  };
45
66
 
46
67
  async function createNewPost(
@@ -57,13 +78,14 @@ async function createNewPost(
57
78
  }
58
79
  }
59
80
 
60
- function buildGoLivePost(
81
+ async function buildGoLivePost(
61
82
  text: string,
62
83
  url: URL,
63
84
  profile: ProfileViewDetailed,
64
85
  params: URLSearchParams,
65
86
  thumbnail: BlobRef | undefined,
66
- ): AppBskyFeedPost.Record {
87
+ agent: StreamplaceAgent,
88
+ ): Promise<AppBskyFeedPost.Record> {
67
89
  const now = new Date();
68
90
  const linkUrl = `${url.protocol}//${url.host}/${profile.handle}?${params.toString()}`;
69
91
  const prefix = `🔴 LIVE `;
@@ -72,7 +94,7 @@ function buildGoLivePost(
72
94
  const content = prefix + textUrl + suffix;
73
95
 
74
96
  const rt = new RichText({ text: content });
75
- rt.detectFacetsWithoutResolution();
97
+ await rt.detectFacets(agent);
76
98
  const record: AppBskyFeedPost.Record = {
77
99
  $type: "app.bsky.feed.post",
78
100
  text: content,
@@ -99,6 +121,7 @@ function buildGoLivePost(
99
121
  export function useCreateStreamRecord() {
100
122
  let agent = usePDSAgent();
101
123
  let url = useUrl();
124
+ const uploadThumbnail = useUploadThumbnail();
102
125
 
103
126
  return async (
104
127
  title: string,
@@ -175,7 +198,14 @@ export function useCreateStreamRecord() {
175
198
  time: new Date().toISOString(),
176
199
  });
177
200
 
178
- let post = buildGoLivePost(title, u, profile.data, params, thumbnail);
201
+ let post = await buildGoLivePost(
202
+ title,
203
+ u,
204
+ profile.data,
205
+ params,
206
+ thumbnail,
207
+ agent,
208
+ );
179
209
 
180
210
  newPost = await createNewPost(agent, post);
181
211
 
@@ -206,6 +236,7 @@ export function useCreateStreamRecord() {
206
236
  export function useUpdateStreamRecord() {
207
237
  let agent = usePDSAgent();
208
238
  let url = useUrl();
239
+ const uploadThumbnail = useUploadThumbnail();
209
240
 
210
241
  return async (
211
242
  title: string,
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @streamplace/components/ui - Streamplace ZeroCSS
3
+ *
4
+ * Clean export path for ZeroCSS styling utilities, design tokens, and atomic styles.
5
+ * ZeroCSS provides a zero-config, atomic styling system optimized for React Native.
6
+ */
7
+
8
+ // Export the most commonly used ZeroCSS utilities
9
+ export {
10
+ // Core atoms object
11
+ atoms,
12
+ // Common shorthand utilities
13
+ bg,
14
+ // Border utilities
15
+ borders,
16
+ bottom,
17
+ // Flex utilities
18
+ flex,
19
+ // Gap utilities (React Native 0.71+)
20
+ gap,
21
+ h,
22
+ // Layout utilities
23
+ layout,
24
+ left,
25
+ m,
26
+ mb,
27
+ ml,
28
+ mr,
29
+ mt,
30
+ mx,
31
+ my,
32
+ p,
33
+ pb,
34
+ pl,
35
+ // Position utilities
36
+ position,
37
+ pr,
38
+ pt,
39
+ px,
40
+ py,
41
+ r,
42
+ right,
43
+ text,
44
+ top,
45
+ w,
46
+ } from "../lib/theme/atoms";
47
+
48
+ // Export ZeroCSS design tokens
49
+ export {
50
+ borderRadius,
51
+ breakpoints,
52
+ colors,
53
+ shadows,
54
+ spacing,
55
+ typography,
56
+ } from "../lib/theme/tokens";
57
+
58
+ // Export ZeroCSS utility functions
59
+ export {
60
+ debounce,
61
+ mergeStyles,
62
+ platformStyle,
63
+ responsiveValue,
64
+ } from "../lib/utils";
65
+
66
+ // Export ZeroCSS theme system
67
+ export {
68
+ ThemeProvider,
69
+ createThemeColors,
70
+ createThemeIcons,
71
+ createThemeStyles,
72
+ createThemedStyles,
73
+ darkTheme,
74
+ lightTheme,
75
+ usePlatformTypography,
76
+ useTheme,
77
+ type Theme,
78
+ type ThemeIcons,
79
+ type ThemeStyles,
80
+ } from "../lib/theme/theme";
81
+
82
+ // Namespace exports for power users
83
+ export * as theme from "../lib/theme";
84
+ export * as atomsNS from "../lib/theme/atoms";
85
+ export * as tokens from "../lib/theme/tokens";
86
+ export * as utils from "../lib/utils";