@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,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.reduceChat = exports.reduceChatIncremental = exports.useCreateChatMessage = exports.useSetReplyToMessage = exports.useReplyToMessage = void 0;
3
+ exports.reduceChat = exports.useReportChatMessage = exports.useSubmitReport = exports.reduceChatIncremental = exports.useCreateChatMessage = exports.useAddPendingHide = exports.usePendingHides = exports.useSetReplyToMessage = exports.useReplyToMessage = void 0;
4
4
  const api_1 = require("@atproto/api");
5
5
  const react_1 = require("react");
6
6
  const streamplace_store_1 = require("../streamplace-store");
@@ -15,6 +15,23 @@ const useSetReplyToMessage = () => {
15
15
  }, [store]);
16
16
  };
17
17
  exports.useSetReplyToMessage = useSetReplyToMessage;
18
+ const usePendingHides = () => (0, livestream_store_1.useLivestreamStore)((state) => state.pendingHides);
19
+ exports.usePendingHides = usePendingHides;
20
+ const useAddPendingHide = () => {
21
+ const store = (0, livestream_store_1.getStoreFromContext)();
22
+ return (0, react_1.useCallback)((messageUri) => {
23
+ const state = store.getState();
24
+ if (!state.pendingHides.includes(messageUri)) {
25
+ const newPendingHides = [...state.pendingHides, messageUri];
26
+ const newState = (0, exports.reduceChat)(state, [], [], [messageUri]);
27
+ store.setState({
28
+ ...newState,
29
+ pendingHides: newPendingHides,
30
+ });
31
+ }
32
+ }, [store]);
33
+ };
34
+ exports.useAddPendingHide = useAddPendingHide;
18
35
  const useCreateChatMessage = () => {
19
36
  const pdsAgent = (0, xrpc_1.usePDSAgent)();
20
37
  const store = (0, livestream_store_1.getStoreFromContext)();
@@ -63,7 +80,7 @@ const useCreateChatMessage = () => {
63
80
  indexedAt: new Date().toISOString(),
64
81
  chatProfile: chatProfile || undefined,
65
82
  };
66
- state = (0, exports.reduceChat)(state, [localChat], []);
83
+ state = (0, exports.reduceChat)(state, [localChat], [], []);
67
84
  store.setState(state);
68
85
  await pdsAgent.com.atproto.repo.createRecord({
69
86
  repo: userDID,
@@ -79,7 +96,9 @@ const buildSortedChatList = (chatIndex, existingChatList, newMessages, removedKe
79
96
  const bTime = parseInt(b.split("-")[0], 10);
80
97
  return bTime - aTime;
81
98
  });
82
- return sortedKeys.map((key) => chatIndex[key]);
99
+ return sortedKeys
100
+ .map((key) => chatIndex[key])
101
+ .filter((msg) => !removedKeys.has(msg.uri));
83
102
  };
84
103
  const profileIsDifferent = (newProfile, oldProfile) => {
85
104
  if (!oldProfile) {
@@ -99,14 +118,25 @@ const profileIsDifferent = (newProfile, oldProfile) => {
99
118
  const { red: oldRed, green: oldGreen, blue: oldBlue } = oldProfile.color;
100
119
  return newRed !== oldRed || newGreen !== oldGreen || newBlue !== oldBlue;
101
120
  };
102
- const reduceChatIncremental = (state, newMessages, blocks) => {
103
- if (newMessages.length === 0 && blocks.length === 0) {
121
+ const reduceChatIncremental = (state, newMessages, blocks, hideUris = []) => {
122
+ if (newMessages.length === 0 &&
123
+ blocks.length === 0 &&
124
+ hideUris.length === 0) {
104
125
  return state;
105
126
  }
106
127
  const newChatIndex = { ...state.chatIndex };
107
128
  const newAuthors = { ...state.authors };
108
129
  let hasChanges = false;
109
130
  const removedKeys = new Set();
131
+ console.log("newMessages", newMessages);
132
+ for (const msg of newMessages) {
133
+ if (msg.deleted) {
134
+ hasChanges = true;
135
+ console.log("deleted", msg.uri);
136
+ removedKeys.add(msg.uri);
137
+ }
138
+ }
139
+ newMessages = newMessages.filter((msg) => msg.deleted !== true);
110
140
  // handle blocks
111
141
  if (blocks.length > 0) {
112
142
  const blockedDIDs = new Set(blocks.map((block) => block.record.subject));
@@ -118,13 +148,26 @@ const reduceChatIncremental = (state, newMessages, blocks) => {
118
148
  }
119
149
  }
120
150
  }
151
+ if (hideUris.length > 0) {
152
+ for (const [key, message] of Object.entries(newChatIndex)) {
153
+ if (hideUris.includes(message.uri)) {
154
+ delete newChatIndex[key];
155
+ removedKeys.add(key);
156
+ hasChanges = true;
157
+ }
158
+ }
159
+ }
121
160
  const messagesToAdd = [];
122
161
  for (const message of newMessages) {
162
+ // don't worry about messages that will be hidden
163
+ if (state.pendingHides.includes(message.uri)) {
164
+ continue;
165
+ }
123
166
  const date = new Date(message.record.createdAt);
124
167
  const key = `${date.getTime()}-${message.uri}`;
125
168
  // only change the ref if the profile is different to avoid re-renders elsewhere
126
- if (profileIsDifferent(message.chatProfile, newAuthors[message.author.handle])) {
127
- newAuthors[message.author.handle] = message.chatProfile;
169
+ if (profileIsDifferent(message.chatProfile, newAuthors[message.author.did])) {
170
+ newAuthors[message.author.did] = message.chatProfile;
128
171
  }
129
172
  // skip messages we already have
130
173
  if (newChatIndex[key] && newChatIndex[key].uri === message.uri) {
@@ -156,17 +199,20 @@ const reduceChatIncremental = (state, newMessages, blocks) => {
156
199
  const parentMsgKey = Object.keys(newChatIndex).find((k) => newChatIndex[k].uri === parentUri);
157
200
  if (parentMsgKey) {
158
201
  const parentMsg = newChatIndex[parentMsgKey];
159
- processedMessage = {
160
- ...message,
161
- replyTo: {
162
- cid: parentMsg.cid,
163
- uri: parentMsg.uri,
164
- author: parentMsg.author,
165
- record: parentMsg.record,
166
- chatProfile: parentMsg.chatProfile,
167
- indexedAt: parentMsg.indexedAt,
168
- },
169
- };
202
+ // Don't allow replies to system messages
203
+ if (parentMsg.author.did !== "did:sys:system") {
204
+ processedMessage = {
205
+ ...message,
206
+ replyTo: {
207
+ cid: parentMsg.cid,
208
+ uri: parentMsg.uri,
209
+ author: parentMsg.author,
210
+ record: parentMsg.record,
211
+ chatProfile: parentMsg.chatProfile,
212
+ indexedAt: parentMsg.indexedAt,
213
+ },
214
+ };
215
+ }
170
216
  }
171
217
  }
172
218
  }
@@ -183,11 +229,58 @@ const reduceChatIncremental = (state, newMessages, blocks) => {
183
229
  }
184
230
  // Build the new sorted chat list efficiently
185
231
  const newChatList = buildSortedChatList(newChatIndex, state.chat, messagesToAdd, removedKeys);
232
+ // Clean up pendingHides - remove URIs that we've now processed
233
+ let newPendingHides = state.pendingHides;
234
+ if (hideUris.length > 0) {
235
+ newPendingHides = state.pendingHides.filter((uri) => !hideUris.includes(uri));
236
+ }
186
237
  return {
187
238
  ...state,
239
+ authors: newAuthors,
188
240
  chatIndex: newChatIndex,
189
241
  chat: newChatList,
242
+ pendingHides: newPendingHides,
190
243
  };
191
244
  };
192
245
  exports.reduceChatIncremental = reduceChatIncremental;
246
+ const useSubmitReport = () => {
247
+ const pdsAgent = (0, xrpc_1.usePDSAgent)();
248
+ const userDID = (0, streamplace_store_1.useDID)();
249
+ return (0, react_1.useCallback)(async (subject, reasonType, reason,
250
+ // no clue about this
251
+ moderationSvcDid = "did:web:stream.place") => {
252
+ if (!pdsAgent || !userDID) {
253
+ throw new Error("No PDS agent or user DID found");
254
+ }
255
+ try {
256
+ const response = await pdsAgent.com.atproto.moderation.createReport({
257
+ reasonType,
258
+ reason,
259
+ subject: subject,
260
+ }, {
261
+ headers: {
262
+ // "atproto-proxy": `${userDID}#atproto_labeler`,
263
+ },
264
+ });
265
+ return response;
266
+ }
267
+ catch (error) {
268
+ console.error("Failed to submit report:", error);
269
+ throw error;
270
+ }
271
+ }, [pdsAgent, userDID]);
272
+ };
273
+ exports.useSubmitReport = useSubmitReport;
274
+ const useReportChatMessage = () => {
275
+ const submitReport = (0, exports.useSubmitReport)();
276
+ return (0, react_1.useCallback)(async (message, reasonType, reason) => {
277
+ const reportSubject = {
278
+ $type: "com.atproto.repo.strongRef",
279
+ uri: message.uri,
280
+ cid: message.cid,
281
+ };
282
+ return await submitReport(reportSubject, reasonType, reason);
283
+ }, [submitReport]);
284
+ };
285
+ exports.useReportChatMessage = useReportChatMessage;
193
286
  exports.reduceChat = exports.reduceChatIncremental;
@@ -14,12 +14,15 @@ const makeLivestreamStore = () => {
14
14
  chat: [],
15
15
  livestream: null,
16
16
  viewers: null,
17
+ pendingHides: [],
17
18
  segment: null,
18
19
  renditions: [],
19
20
  replyToMessage: null,
20
21
  streamKey: null,
21
22
  setStreamKey: (sk) => set({ streamKey: sk }),
22
23
  authors: {},
24
+ recentSegments: [],
25
+ problems: [],
23
26
  }));
24
27
  };
25
28
  exports.makeLivestreamStore = makeLivestreamStore;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findProblems = void 0;
4
+ const VARIANCE_THRESHOLD = 0.5;
5
+ const DURATION_THRESHOLD = 5000000000; // 5s in ns
6
+ const detectVariableSegmentLength = (segments) => {
7
+ if (segments.length < 3) {
8
+ // Need at least 3 segments to detect variability
9
+ return { variable: false, duration: false };
10
+ }
11
+ const durations = segments
12
+ .map((segment) => segment.duration)
13
+ .filter((duration) => duration !== undefined && duration > 0);
14
+ if (durations.length < 3) {
15
+ return { variable: false, duration: false };
16
+ }
17
+ // Calculate mean
18
+ const mean = durations.reduce((sum, duration) => sum + duration, 0) /
19
+ durations.length;
20
+ // Calculate standard deviation
21
+ const variance = durations.reduce((sum, duration) => {
22
+ const diff = duration - mean;
23
+ return sum + diff * diff;
24
+ }, 0) / durations.length;
25
+ const stdDev = Math.sqrt(variance);
26
+ // Calculate coefficient of variation (CV)
27
+ const cv = stdDev / mean;
28
+ // CV > 0.5 indicates high variability
29
+ // This threshold can be adjusted based on testing
30
+ return {
31
+ variable: cv > VARIANCE_THRESHOLD,
32
+ duration: mean > DURATION_THRESHOLD,
33
+ };
34
+ };
35
+ const findProblems = (segments) => {
36
+ const problems = [];
37
+ let hasBFrames = false;
38
+ for (const segment of segments) {
39
+ const video = segment.video?.[0];
40
+ if (!video) {
41
+ // i mean yes this is a problem but it can't happen yet
42
+ continue;
43
+ }
44
+ if (video.bframes === true) {
45
+ hasBFrames = true;
46
+ break;
47
+ }
48
+ }
49
+ if (hasBFrames) {
50
+ problems.push({
51
+ code: "bframes",
52
+ message: "Your stream contains B-Frames, which are not supported in Streamplace. Your stream will stutter.",
53
+ severity: "error",
54
+ link: "https://stream.place/docs/guides/start-streaming/obs/#obs-configuration",
55
+ });
56
+ }
57
+ const { variable, duration } = detectVariableSegmentLength(segments);
58
+ if (variable) {
59
+ problems.push({
60
+ code: "variable_segment_length",
61
+ message: "Your stream contains variable segment lengths, which may cause playback issues.",
62
+ severity: "warning",
63
+ link: "https://stream.place/docs/guides/start-streaming/obs/#obs-configuration",
64
+ });
65
+ }
66
+ if (duration) {
67
+ problems.push({
68
+ code: "long_segments",
69
+ message: "Your stream contains long segments (>5s). This will work fine, but increases the delay of the livestream.",
70
+ severity: "warning",
71
+ link: "https://stream.place/docs/guides/start-streaming/obs/#obs-configuration",
72
+ });
73
+ }
74
+ return problems;
75
+ };
76
+ exports.findProblems = findProblems;
@@ -3,13 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleWebSocketMessages = void 0;
4
4
  const api_1 = require("@atproto/api");
5
5
  const streamplace_1 = require("streamplace");
6
+ const system_messages_1 = require("../lib/system-messages");
6
7
  const chat_1 = require("./chat");
8
+ const problems_1 = require("./problems");
9
+ const MAX_RECENT_SEGMENTS = 10;
7
10
  const handleWebSocketMessages = (state, messages) => {
8
11
  for (const message of messages) {
9
12
  if (streamplace_1.PlaceStreamLivestream.isLivestreamView(message)) {
13
+ const newLivestream = message;
14
+ const oldLivestream = state.livestream;
15
+ // check if this is actually new
16
+ if (!oldLivestream || oldLivestream.uri !== newLivestream.uri) {
17
+ const streamTitle = newLivestream.record.title || "something cool!";
18
+ const systemMessage = system_messages_1.SystemMessages.streamStart(streamTitle);
19
+ // set proper times
20
+ systemMessage.indexedAt = newLivestream.indexedAt;
21
+ systemMessage.record.createdAt = newLivestream.record.createdAt;
22
+ state = (0, chat_1.reduceChat)(state, [systemMessage], []);
23
+ }
10
24
  state = {
11
25
  ...state,
12
- livestream: message,
26
+ livestream: newLivestream,
13
27
  };
14
28
  }
15
29
  else if (streamplace_1.PlaceStreamLivestream.isViewerCount(message)) {
@@ -28,18 +42,26 @@ const handleWebSocketMessages = (state, messages) => {
28
42
  indexedAt: message.indexedAt,
29
43
  chatProfile: message.chatProfile,
30
44
  replyTo: message.replyTo,
45
+ deleted: message.deleted,
31
46
  };
32
- state = (0, chat_1.reduceChat)(state, [hydrated], []);
47
+ state = (0, chat_1.reduceChat)(state, [hydrated], [], []);
33
48
  }
34
49
  else if (streamplace_1.PlaceStreamSegment.isRecord(message)) {
50
+ const newRecentSegments = [...state.recentSegments];
51
+ newRecentSegments.unshift(message);
52
+ if (newRecentSegments.length > MAX_RECENT_SEGMENTS) {
53
+ newRecentSegments.pop();
54
+ }
35
55
  state = {
36
56
  ...state,
37
57
  segment: message,
58
+ recentSegments: newRecentSegments,
59
+ problems: (0, problems_1.findProblems)(newRecentSegments),
38
60
  };
39
61
  }
40
62
  else if (streamplace_1.PlaceStreamDefs.isBlockView(message)) {
41
63
  const block = message;
42
- state = (0, chat_1.reduceChat)(state, [], [block]);
64
+ state = (0, chat_1.reduceChat)(state, [], [block], []);
43
65
  }
44
66
  else if (streamplace_1.PlaceStreamDefs.isRenditions(message)) {
45
67
  state = {
@@ -53,7 +75,20 @@ const handleWebSocketMessages = (state, messages) => {
53
75
  profile: message,
54
76
  };
55
77
  }
78
+ else if (streamplace_1.PlaceStreamChatGate.isRecord(message)) {
79
+ const hideRecord = message;
80
+ const hiddenMessageUri = hideRecord.hiddenMessage;
81
+ const newPendingHides = [...state.pendingHides];
82
+ if (!newPendingHides.includes(hiddenMessageUri)) {
83
+ newPendingHides.push(hiddenMessageUri);
84
+ }
85
+ state = {
86
+ ...state,
87
+ pendingHides: newPendingHides,
88
+ };
89
+ state = (0, chat_1.reduceChat)(state, [], [], [hiddenMessageUri]);
90
+ }
56
91
  }
57
- return (0, chat_1.reduceChat)(state, [], []);
92
+ return (0, chat_1.reduceChat)(state, [], [], []);
58
93
  };
59
94
  exports.handleWebSocketMessages = handleWebSocketMessages;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.intoPlayerProtocol = exports.usePlayerProtocol = exports.makePlayerStore = void 0;
3
+ exports.useOffline = exports.intoPlayerProtocol = exports.usePlayerProtocol = exports.makePlayerStore = void 0;
4
4
  exports.usePlayerContext = usePlayerContext;
5
5
  exports.getPlayerStoreById = getPlayerStoreById;
6
6
  exports.getFirstPlayerID = getFirstPlayerID;
@@ -8,6 +8,7 @@ exports.getPlayerStoreFromContext = getPlayerStoreFromContext;
8
8
  exports.usePlayerStore = usePlayerStore;
9
9
  const react_1 = require("react");
10
10
  const zustand_1 = require("zustand");
11
+ const livestream_store_1 = require("../livestream-store");
11
12
  const context_1 = require("./context");
12
13
  const player_state_1 = require("./player-state");
13
14
  const makePlayerStore = (id) => {
@@ -41,12 +42,13 @@ const makePlayerStore = (id) => {
41
42
  setStatus: (status) => set(() => ({ status })),
42
43
  playTime: 0,
43
44
  setPlayTime: (playTime) => set(() => ({ playTime })),
44
- offline: false,
45
- setOffline: (offline) => set(() => ({ offline })),
46
45
  videoRef: undefined,
47
46
  setVideoRef: (videoRef) => set(() => ({ videoRef })),
48
47
  pipMode: false,
49
48
  setPipMode: (pipMode) => set(() => ({ pipMode })),
49
+ // Picture-in-Picture action function (set by player component)
50
+ pipAction: undefined,
51
+ setPipAction: (action) => set(() => ({ pipAction: action })),
50
52
  // Player element width/height setters for global sync
51
53
  playerWidth: undefined,
52
54
  setPlayerWidth: (playerWidth) => set(() => ({ playerWidth })),
@@ -65,6 +67,8 @@ const makePlayerStore = (id) => {
65
67
  setTelemetry: (telemetry) => set(() => ({ telemetry })),
66
68
  ingestLive: false,
67
69
  setIngestLive: (ingestLive) => set(() => ({ ingestLive })),
70
+ reportingURL: null,
71
+ setReportingURL: (reportingURL) => set(() => ({ reportingURL })),
68
72
  playerEvent: async (url, time, eventType, meta) => set((x) => {
69
73
  const data = {
70
74
  time: time,
@@ -76,7 +80,8 @@ const makePlayerStore = (id) => {
76
80
  };
77
81
  try {
78
82
  // fetch url from sp provider
79
- fetch(`${url}/api/player-event`, {
83
+ const reportingURL = x.reportingURL ?? `${url}/api/player-event`;
84
+ fetch(reportingURL, {
80
85
  method: "POST",
81
86
  body: JSON.stringify(data),
82
87
  });
@@ -106,6 +111,10 @@ const makePlayerStore = (id) => {
106
111
  setShowDebugInfo: (showDebugInfo) => set(() => ({ showDebugInfo })),
107
112
  modMessage: null,
108
113
  setModMessage: (modMessage) => set(() => ({ modMessage })),
114
+ reportModalOpen: false,
115
+ setReportModalOpen: (reportModalOpen) => set(() => ({ reportModalOpen })),
116
+ reportSubject: null,
117
+ setReportSubject: (subject) => set(() => ({ reportSubject: subject })),
109
118
  }));
110
119
  };
111
120
  exports.makePlayerStore = makePlayerStore;
@@ -168,3 +177,23 @@ const intoPlayerProtocol = (protocol) => {
168
177
  }
169
178
  };
170
179
  exports.intoPlayerProtocol = intoPlayerProtocol;
180
+ // returns true if the livestream has been offline for more than 10 seconds and we're not playing
181
+ const useOffline = () => {
182
+ const status = usePlayerStore((x) => x.status);
183
+ const segment = (0, livestream_store_1.useLivestreamStore)((x) => x.segment);
184
+ const [now, setNow] = (0, react_1.useState)(Date.now());
185
+ (0, react_1.useEffect)(() => {
186
+ const interval = setInterval(() => {
187
+ setNow(Date.now());
188
+ }, 500);
189
+ return () => clearInterval(interval);
190
+ }, []);
191
+ if (status === player_state_1.PlayerStatus.PLAYING) {
192
+ return false;
193
+ }
194
+ if (!segment?.startTime) {
195
+ return false;
196
+ }
197
+ return now - Date.parse(segment.startTime) > 10000;
198
+ };
199
+ exports.useOffline = useOffline;
@@ -1,26 +1,65 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useCreateBlockRecord = useCreateBlockRecord;
4
+ exports.useCreateHideChatRecord = useCreateHideChatRecord;
5
+ const react_1 = require("react");
4
6
  const xrpc_1 = require("./xrpc");
5
7
  function useCreateBlockRecord() {
6
8
  let agent = (0, xrpc_1.usePDSAgent)();
7
- return async (subjectDID) => {
9
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
10
+ const createBlock = async (subjectDID) => {
8
11
  if (!agent) {
9
12
  throw new Error("No PDS agent found");
10
13
  }
11
14
  if (!agent.did) {
12
15
  throw new Error("No user DID found, assuming not logged in");
13
16
  }
14
- const record = {
15
- $type: "app.bsky.graph.block",
16
- subject: subjectDID,
17
- createdAt: new Date().toISOString(),
18
- };
19
- return await agent.com.atproto.repo.createRecord({
20
- repo: agent.did,
21
- collection: "app.bsky.graph.block",
22
- record,
23
- });
24
- return record;
17
+ setIsLoading(true);
18
+ try {
19
+ const record = {
20
+ $type: "app.bsky.graph.block",
21
+ subject: subjectDID,
22
+ createdAt: new Date().toISOString(),
23
+ };
24
+ const result = await agent.com.atproto.repo.createRecord({
25
+ repo: agent.did,
26
+ collection: "app.bsky.graph.block",
27
+ record,
28
+ });
29
+ return result;
30
+ }
31
+ finally {
32
+ setIsLoading(false);
33
+ }
34
+ };
35
+ return { createBlock, isLoading };
36
+ }
37
+ function useCreateHideChatRecord() {
38
+ let agent = (0, xrpc_1.usePDSAgent)();
39
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
40
+ const createHideChat = async (chatMessageUri) => {
41
+ if (!agent) {
42
+ throw new Error("No PDS agent found");
43
+ }
44
+ if (!agent.did) {
45
+ throw new Error("No user DID found, assuming not logged in");
46
+ }
47
+ setIsLoading(true);
48
+ try {
49
+ const record = {
50
+ $type: "place.stream.chat.gate",
51
+ hiddenMessage: chatMessageUri,
52
+ };
53
+ const result = await agent.com.atproto.repo.createRecord({
54
+ repo: agent.did,
55
+ collection: "place.stream.chat.gate",
56
+ record,
57
+ });
58
+ return result;
59
+ }
60
+ finally {
61
+ setIsLoading(false);
62
+ }
25
63
  };
64
+ return { createHideChat, isLoading };
26
65
  }
@@ -5,29 +5,48 @@ exports.useUpdateStreamRecord = useUpdateStreamRecord;
5
5
  const api_1 = require("@atproto/api");
6
6
  const streamplace_store_1 = require("./streamplace-store");
7
7
  const xrpc_1 = require("./xrpc");
8
- const uploadThumbnail = async (pdsAgent, customThumbnail) => {
9
- if (customThumbnail) {
10
- let tries = 0;
11
- try {
12
- let thumbnail = await pdsAgent.uploadBlob(customThumbnail);
13
- while (thumbnail.data.blob.size === 0 &&
14
- customThumbnail.size !== 0 &&
15
- tries < 3) {
16
- console.warn("Reuploading blob as blob sizes don't match! Blob size recieved is", thumbnail.data.blob.size, "and sent blob size is", customThumbnail.size);
17
- thumbnail = await pdsAgent.uploadBlob(customThumbnail);
18
- }
19
- if (tries === 3) {
20
- throw new Error("Could not successfully upload blob (tried thrice)");
8
+ const react_1 = require("react");
9
+ const useUploadThumbnail = () => {
10
+ const abortRef = (0, react_1.useRef)(null);
11
+ (0, react_1.useEffect)(() => {
12
+ return () => {
13
+ // On unmount, abort any ongoing upload
14
+ abortRef.current?.abort();
15
+ };
16
+ }, []);
17
+ const uploadThumbnail = async (pdsAgent, customThumbnail) => {
18
+ if (!customThumbnail)
19
+ return undefined;
20
+ abortRef.current = new AbortController();
21
+ const { signal } = abortRef.current;
22
+ const maxTries = 3;
23
+ let lastError = null;
24
+ for (let tries = 0; tries < maxTries; tries++) {
25
+ try {
26
+ const thumbnail = await pdsAgent.uploadBlob(customThumbnail, {
27
+ signal,
28
+ });
29
+ if (thumbnail.success &&
30
+ thumbnail.data.blob.size === customThumbnail.size) {
31
+ console.log("Successfully uploaded thumbnail");
32
+ return thumbnail.data.blob;
33
+ }
34
+ else {
35
+ console.warn(`Blob size mismatch (attempt ${tries + 1}): received ${thumbnail.data.blob.size}, expected ${customThumbnail.size}`);
36
+ }
21
37
  }
22
- if (thumbnail.success) {
23
- console.log("Successfully uploaded thumbnail");
24
- return thumbnail.data.blob;
38
+ catch (e) {
39
+ if (signal.aborted) {
40
+ console.warn("Upload aborted");
41
+ return undefined;
42
+ }
43
+ lastError = e;
44
+ console.warn(`Error uploading thumbnail (attempt ${tries + 1}): ${e}`);
25
45
  }
26
46
  }
27
- catch (e) {
28
- throw new Error("Error uploading thumbnail: " + e);
29
- }
30
- }
47
+ throw new Error(`Could not successfully upload blob after ${maxTries} attempts. Last error: ${lastError}`);
48
+ };
49
+ return uploadThumbnail;
31
50
  };
32
51
  async function createNewPost(agent, record) {
33
52
  try {
@@ -39,7 +58,7 @@ async function createNewPost(agent, record) {
39
58
  throw error;
40
59
  }
41
60
  }
42
- function buildGoLivePost(text, url, profile, params, thumbnail) {
61
+ async function buildGoLivePost(text, url, profile, params, thumbnail, agent) {
43
62
  const now = new Date();
44
63
  const linkUrl = `${url.protocol}//${url.host}/${profile.handle}?${params.toString()}`;
45
64
  const prefix = `🔴 LIVE `;
@@ -47,7 +66,7 @@ function buildGoLivePost(text, url, profile, params, thumbnail) {
47
66
  const suffix = ` ${text}`;
48
67
  const content = prefix + textUrl + suffix;
49
68
  const rt = new api_1.RichText({ text: content });
50
- rt.detectFacetsWithoutResolution();
69
+ await rt.detectFacets(agent);
51
70
  const record = {
52
71
  $type: "app.bsky.feed.post",
53
72
  text: content,
@@ -72,6 +91,7 @@ function buildGoLivePost(text, url, profile, params, thumbnail) {
72
91
  function useCreateStreamRecord() {
73
92
  let agent = (0, xrpc_1.usePDSAgent)();
74
93
  let url = (0, streamplace_store_1.useUrl)();
94
+ const uploadThumbnail = useUploadThumbnail();
75
95
  return async (title, customThumbnail, submitPost = true) => {
76
96
  if (!agent) {
77
97
  throw new Error("No PDS agent found");
@@ -130,7 +150,7 @@ function useCreateStreamRecord() {
130
150
  did: did,
131
151
  time: new Date().toISOString(),
132
152
  });
133
- let post = buildGoLivePost(title, u, profile.data, params, thumbnail);
153
+ let post = await buildGoLivePost(title, u, profile.data, params, thumbnail, agent);
134
154
  newPost = await createNewPost(agent, post);
135
155
  if (!newPost.uri || !newPost.cid) {
136
156
  throw new Error("Cannot read properties of undefined (reading 'uri' or 'cid')");
@@ -154,6 +174,7 @@ function useCreateStreamRecord() {
154
174
  function useUpdateStreamRecord() {
155
175
  let agent = (0, xrpc_1.usePDSAgent)();
156
176
  let url = (0, streamplace_store_1.useUrl)();
177
+ const uploadThumbnail = useUploadThumbnail();
157
178
  return async (title, livestream, customThumbnail) => {
158
179
  if (!agent) {
159
180
  throw new Error("No PDS agent found");