@streamplace/components 0.6.37 → 0.7.0

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 (138) hide show
  1. package/dist/components/chat/chat-box.js +109 -0
  2. package/dist/components/chat/chat-message.js +76 -0
  3. package/dist/components/chat/chat.js +56 -0
  4. package/dist/components/chat/mention-suggestions.js +39 -0
  5. package/dist/components/chat/mod-view.js +33 -0
  6. package/dist/components/mobile-player/fullscreen.js +69 -0
  7. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  8. package/dist/components/mobile-player/player.js +103 -0
  9. package/dist/components/mobile-player/props.js +1 -0
  10. package/dist/components/mobile-player/shared.js +51 -0
  11. package/dist/components/mobile-player/ui/countdown.js +79 -0
  12. package/dist/components/mobile-player/ui/index.js +5 -0
  13. package/dist/components/mobile-player/ui/input.js +38 -0
  14. package/dist/components/mobile-player/ui/metrics.js +40 -0
  15. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  17. package/dist/components/mobile-player/use-webrtc.js +232 -0
  18. package/dist/components/mobile-player/video.js +375 -0
  19. package/dist/components/mobile-player/video.native.js +238 -0
  20. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  21. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  22. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  23. package/dist/components/ui/button.js +220 -0
  24. package/dist/components/ui/dialog.js +203 -0
  25. package/dist/components/ui/dropdown.js +148 -0
  26. package/dist/components/ui/icons.js +22 -0
  27. package/dist/components/ui/index.js +22 -0
  28. package/dist/components/ui/input.js +202 -0
  29. package/dist/components/ui/loader.js +7 -0
  30. package/dist/components/ui/primitives/button.js +121 -0
  31. package/dist/components/ui/primitives/input.js +202 -0
  32. package/dist/components/ui/primitives/modal.js +203 -0
  33. package/dist/components/ui/primitives/text.js +286 -0
  34. package/dist/components/ui/resizeable.js +101 -0
  35. package/dist/components/ui/text.js +175 -0
  36. package/dist/components/ui/textarea.js +17 -0
  37. package/dist/components/ui/toast.js +129 -0
  38. package/dist/components/ui/view.js +250 -0
  39. package/dist/hooks/index.js +9 -0
  40. package/dist/hooks/useAvatars.js +32 -0
  41. package/dist/hooks/useCameraToggle.js +9 -0
  42. package/dist/hooks/useKeyboard.js +33 -0
  43. package/dist/hooks/useKeyboardSlide.js +11 -0
  44. package/dist/hooks/useLivestreamInfo.js +62 -0
  45. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  46. package/dist/hooks/usePlayerDimensions.js +19 -0
  47. package/dist/hooks/useSegmentTiming.js +62 -0
  48. package/dist/index.js +10 -0
  49. package/dist/lib/facet.js +88 -0
  50. package/dist/lib/theme/atoms.js +620 -0
  51. package/dist/lib/theme/atoms.types.js +5 -0
  52. package/dist/lib/theme/index.js +9 -0
  53. package/dist/lib/theme/theme.js +248 -0
  54. package/dist/lib/theme/tokens.js +383 -0
  55. package/dist/lib/utils.js +94 -0
  56. package/dist/livestream-provider/index.js +8 -3
  57. package/dist/livestream-store/chat.js +89 -65
  58. package/dist/livestream-store/index.js +1 -0
  59. package/dist/livestream-store/livestream-store.js +3 -0
  60. package/dist/livestream-store/stream-key.js +115 -0
  61. package/dist/player-store/player-provider.js +0 -1
  62. package/dist/player-store/player-store.js +13 -0
  63. package/dist/streamplace-store/block.js +23 -0
  64. package/dist/streamplace-store/index.js +1 -0
  65. package/dist/streamplace-store/stream.js +193 -0
  66. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  67. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  68. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  69. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  70. package/package.json +20 -4
  71. package/src/components/chat/chat-box.tsx +195 -0
  72. package/src/components/chat/chat-message.tsx +192 -0
  73. package/src/components/chat/chat.tsx +128 -0
  74. package/src/components/chat/mention-suggestions.tsx +71 -0
  75. package/src/components/chat/mod-view.tsx +118 -0
  76. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  77. package/src/components/mobile-player/fullscreen.tsx +79 -0
  78. package/src/components/mobile-player/player.tsx +134 -0
  79. package/src/components/mobile-player/props.tsx +11 -0
  80. package/src/components/mobile-player/shared.tsx +56 -0
  81. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  82. package/src/components/mobile-player/ui/index.ts +5 -0
  83. package/src/components/mobile-player/ui/input.tsx +85 -0
  84. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  85. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  86. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  87. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  88. package/src/components/mobile-player/video.native.tsx +360 -0
  89. package/src/components/mobile-player/video.tsx +557 -0
  90. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  91. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  92. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  93. package/src/components/ui/button.tsx +309 -0
  94. package/src/components/ui/dialog.tsx +376 -0
  95. package/src/components/ui/dropdown.tsx +399 -0
  96. package/src/components/ui/icons.tsx +50 -0
  97. package/src/components/ui/index.ts +33 -0
  98. package/src/components/ui/input.tsx +350 -0
  99. package/src/components/ui/loader.tsx +9 -0
  100. package/src/components/ui/primitives/button.tsx +292 -0
  101. package/src/components/ui/primitives/input.tsx +422 -0
  102. package/src/components/ui/primitives/modal.tsx +421 -0
  103. package/src/components/ui/primitives/text.tsx +499 -0
  104. package/src/components/ui/resizeable.tsx +169 -0
  105. package/src/components/ui/text.tsx +330 -0
  106. package/src/components/ui/textarea.tsx +34 -0
  107. package/src/components/ui/toast.tsx +203 -0
  108. package/src/components/ui/view.tsx +344 -0
  109. package/src/hooks/index.ts +9 -0
  110. package/src/hooks/useAvatars.tsx +44 -0
  111. package/src/hooks/useCameraToggle.ts +12 -0
  112. package/src/hooks/useKeyboard.tsx +41 -0
  113. package/src/hooks/useKeyboardSlide.ts +12 -0
  114. package/src/hooks/useLivestreamInfo.ts +67 -0
  115. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  116. package/src/hooks/usePlayerDimensions.ts +23 -0
  117. package/src/hooks/useSegmentTiming.tsx +88 -0
  118. package/src/index.tsx +21 -0
  119. package/src/lib/facet.ts +131 -0
  120. package/src/lib/theme/atoms.ts +760 -0
  121. package/src/lib/theme/atoms.types.ts +258 -0
  122. package/src/lib/theme/index.ts +48 -0
  123. package/src/lib/theme/theme.tsx +436 -0
  124. package/src/lib/theme/tokens.ts +409 -0
  125. package/src/lib/utils.ts +132 -0
  126. package/src/livestream-provider/index.tsx +13 -2
  127. package/src/livestream-store/chat.tsx +115 -78
  128. package/src/livestream-store/index.tsx +1 -0
  129. package/src/livestream-store/livestream-state.tsx +3 -0
  130. package/src/livestream-store/livestream-store.tsx +3 -0
  131. package/src/livestream-store/stream-key.tsx +124 -0
  132. package/src/player-store/player-provider.tsx +0 -1
  133. package/src/player-store/player-state.tsx +28 -0
  134. package/src/player-store/player-store.tsx +22 -0
  135. package/src/streamplace-store/block.tsx +29 -0
  136. package/src/streamplace-store/index.tsx +1 -0
  137. package/src/streamplace-store/stream.tsx +262 -0
  138. package/tsconfig.tsbuildinfo +1 -1
@@ -1,8 +1,5 @@
1
1
  import { RichText } from "@atproto/api";
2
- import {
3
- isLink,
4
- isMention,
5
- } from "@atproto/api/dist/client/types/app/bsky/richtext/facet";
2
+ import { useCallback } from "react";
6
3
  import {
7
4
  ChatMessageViewHydrated,
8
5
  PlaceStreamChatMessage,
@@ -18,9 +15,12 @@ export const useReplyToMessage = () =>
18
15
 
19
16
  export const useSetReplyToMessage = () => {
20
17
  const store = getStoreFromContext();
21
- return (message: ChatMessageViewHydrated | null) => {
22
- store.setState({ replyToMessage: message });
23
- };
18
+ return useCallback(
19
+ (message: ChatMessageViewHydrated | null) => {
20
+ store.setState({ replyToMessage: message });
21
+ },
22
+ [store],
23
+ );
24
24
  };
25
25
 
26
26
  export type NewChatMessage = {
@@ -52,12 +52,13 @@ export const useCreateChatMessage = () => {
52
52
  }
53
53
 
54
54
  const rt = new RichText({ text: msg.text });
55
- rt.detectFacetsWithoutResolution();
55
+ await rt.detectFacets(pdsAgent);
56
56
 
57
57
  const record: PlaceStreamChatMessage.Record = {
58
58
  text: msg.text,
59
59
  createdAt: new Date().toISOString(),
60
60
  streamer: streamerProfile.did,
61
+ facets: rt.facets as PlaceStreamChatMessage.Record["facets"],
61
62
  ...(msg.reply
62
63
  ? {
63
64
  reply: {
@@ -72,34 +73,6 @@ export const useCreateChatMessage = () => {
72
73
  },
73
74
  }
74
75
  : {}),
75
- ...(rt.facets && rt.facets.length > 0
76
- ? {
77
- facets: rt.facets.map((facet) => ({
78
- index: facet.index,
79
- features: facet.features
80
- .filter(
81
- (feature) =>
82
- feature.$type === "app.bsky.richtext.facet#link" ||
83
- feature.$type === "app.bsky.richtext.facet#mention",
84
- )
85
- .map((feature) => {
86
- if (isLink(feature)) {
87
- return {
88
- $type: "app.bsky.richtext.facet#link",
89
- uri: feature.uri,
90
- };
91
- } else if (isMention(feature)) {
92
- return {
93
- $type: "app.bsky.richtext.facet#mention",
94
- did: feature.did,
95
- };
96
- } else {
97
- throw new Error("invalid code path");
98
- }
99
- }),
100
- })),
101
- }
102
- : {}),
103
76
  };
104
77
 
105
78
  const localChat: ChatMessageViewHydrated = {
@@ -125,40 +98,108 @@ export const useCreateChatMessage = () => {
125
98
  };
126
99
  };
127
100
 
128
- const CHAT_LIMIT = 20;
101
+ const buildSortedChatList = (
102
+ chatIndex: { [key: string]: ChatMessageViewHydrated },
103
+ existingChatList: ChatMessageViewHydrated[],
104
+ newMessages: { key: string; message: ChatMessageViewHydrated }[],
105
+ removedKeys: Set<string>,
106
+ ): ChatMessageViewHydrated[] => {
107
+ const sortedKeys = Object.keys(chatIndex).sort((a, b) => {
108
+ const aTime = parseInt(a.split("-")[0], 10);
109
+ const bTime = parseInt(b.split("-")[0], 10);
110
+ return bTime - aTime;
111
+ });
112
+ return sortedKeys.map((key) => chatIndex[key]);
113
+ };
129
114
 
130
- export const reduceChat = (
115
+ const profileIsDifferent = (
116
+ newProfile: ChatMessageViewHydrated["chatProfile"],
117
+ oldProfile: ChatMessageViewHydrated["chatProfile"],
118
+ ) => {
119
+ if (!oldProfile) {
120
+ return true;
121
+ }
122
+ if (!newProfile) {
123
+ return false;
124
+ }
125
+ if (!oldProfile.color) {
126
+ return true;
127
+ }
128
+ if (!newProfile.color) {
129
+ // idk. shouldn't happen.
130
+ return false;
131
+ }
132
+ const { red: newRed, green: newGreen, blue: newBlue } = newProfile.color;
133
+ const { red: oldRed, green: oldGreen, blue: oldBlue } = oldProfile.color;
134
+ return newRed !== oldRed || newGreen !== oldGreen || newBlue !== oldBlue;
135
+ };
136
+
137
+ export const reduceChatIncremental = (
131
138
  state: LivestreamState,
132
- messages: ChatMessageViewHydrated[],
139
+ newMessages: ChatMessageViewHydrated[],
133
140
  blocks: PlaceStreamDefs.BlockView[],
134
141
  ): LivestreamState => {
135
- state = { ...state } as LivestreamState;
136
- let newChat: { [key: string]: ChatMessageViewHydrated } = {
137
- ...state.chatIndex,
138
- };
142
+ if (newMessages.length === 0 && blocks.length === 0) {
143
+ return state;
144
+ }
145
+
146
+ const newChatIndex = { ...state.chatIndex };
147
+ const newAuthors = { ...state.authors };
148
+ let hasChanges = false;
149
+ const removedKeys = new Set<string>();
139
150
 
140
- // Add new messages
141
- for (let message of messages) {
151
+ // handle blocks
152
+ if (blocks.length > 0) {
153
+ const blockedDIDs = new Set(blocks.map((block) => block.record.subject));
154
+ for (const [key, message] of Object.entries(newChatIndex)) {
155
+ if (blockedDIDs.has(message.author.did)) {
156
+ delete newChatIndex[key];
157
+ removedKeys.add(key);
158
+ hasChanges = true;
159
+ }
160
+ }
161
+ }
162
+
163
+ const messagesToAdd: { key: string; message: ChatMessageViewHydrated }[] = [];
164
+
165
+ for (const message of newMessages) {
142
166
  const date = new Date(message.record.createdAt);
143
167
  const key = `${date.getTime()}-${message.uri}`;
144
168
 
145
- // Remove existing local message matching the server one
169
+ // only change the ref if the profile is different to avoid re-renders elsewhere
170
+ if (
171
+ profileIsDifferent(message.chatProfile, newAuthors[message.author.handle])
172
+ ) {
173
+ newAuthors[message.author.handle] = message.chatProfile;
174
+ }
175
+
176
+ // skip messages we already have
177
+ if (newChatIndex[key] && newChatIndex[key].uri === message.uri) {
178
+ continue;
179
+ }
180
+
181
+ // if we have a local message, replace it with the new one
146
182
  if (!message.uri.startsWith("local-")) {
147
- const existingLocalMessageKey = Object.keys(newChat).find((k) => {
148
- const msg = newChat[k];
183
+ const existingLocalKey = Object.keys(newChatIndex).find((k) => {
184
+ const msg = newChatIndex[k];
149
185
  return (
150
186
  msg.uri.startsWith("local-") &&
151
187
  msg.record.text === message.record.text &&
152
- msg.author.did === message.author.did
188
+ msg.author.did === message.author.did &&
189
+ Math.abs(new Date(msg.record.createdAt).getTime() - date.getTime()) <
190
+ 10000 // Within 10 seconds
153
191
  );
154
192
  });
155
193
 
156
- if (existingLocalMessageKey) {
157
- delete newChat[existingLocalMessageKey];
194
+ if (existingLocalKey) {
195
+ delete newChatIndex[existingLocalKey];
196
+ removedKeys.add(existingLocalKey);
197
+ hasChanges = true;
158
198
  }
159
199
  }
160
200
 
161
- // Handle reply information for local-first messages
201
+ // add reply info
202
+ let processedMessage = message;
162
203
  if (message.record.reply) {
163
204
  const reply = message.record.reply as {
164
205
  parent?: { uri: string; cid: string };
@@ -166,17 +207,14 @@ export const reduceChat = (
166
207
  };
167
208
 
168
209
  const parentUri = reply?.parent?.uri || reply?.root?.uri;
169
-
170
210
  if (parentUri) {
171
- // First try to find the parent message in our chat
172
- const parentMsgKey = Object.keys(newChat).find(
173
- (k) => newChat[k].uri === parentUri,
211
+ const parentMsgKey = Object.keys(newChatIndex).find(
212
+ (k) => newChatIndex[k].uri === parentUri,
174
213
  );
175
214
 
176
215
  if (parentMsgKey) {
177
- // Found the parent message, add its info to our message
178
- const parentMsg = newChat[parentMsgKey];
179
- message = {
216
+ const parentMsg = newChatIndex[parentMsgKey];
217
+ processedMessage = {
180
218
  ...message,
181
219
  replyTo: {
182
220
  cid: parentMsg.cid,
@@ -191,34 +229,33 @@ export const reduceChat = (
191
229
  }
192
230
  }
193
231
 
194
- newChat[key] = message;
232
+ messagesToAdd.push({ key, message: processedMessage });
233
+ hasChanges = true;
195
234
  }
196
235
 
197
- for (const block of blocks) {
198
- for (const [k, v] of Object.entries(newChat)) {
199
- if (v.author.did === block.record.subject) {
200
- delete newChat[k];
201
- }
202
- }
236
+ // Add new messages to index
237
+ for (const { key, message } of messagesToAdd) {
238
+ newChatIndex[key] = message;
203
239
  }
204
240
 
205
- let newChatList = Object.values(newChat).sort((a, b) =>
206
- new Date(a.record.createdAt) > new Date(b.record.createdAt) ? 1 : -1,
207
- );
208
-
209
- newChatList = newChatList.slice(-CHAT_LIMIT);
241
+ // only rebuild if we have changes
242
+ if (!hasChanges) {
243
+ return state;
244
+ }
210
245
 
211
- newChat = newChatList.reduce(
212
- (acc, msg) => {
213
- acc[msg.uri] = msg;
214
- return acc;
215
- },
216
- {} as { [key: string]: ChatMessageViewHydrated },
246
+ // Build the new sorted chat list efficiently
247
+ const newChatList = buildSortedChatList(
248
+ newChatIndex,
249
+ state.chat,
250
+ messagesToAdd,
251
+ removedKeys,
217
252
  );
218
253
 
219
254
  return {
220
255
  ...state,
221
- chatIndex: newChat,
256
+ chatIndex: newChatIndex,
222
257
  chat: newChatList,
223
258
  };
224
259
  };
260
+
261
+ export const reduceChat = reduceChatIncremental;
@@ -1,3 +1,4 @@
1
1
  export * from "./chat";
2
2
  export * from "./context";
3
3
  export * from "./livestream-store";
4
+ export * from "./stream-key";
@@ -10,9 +10,12 @@ export interface LivestreamState {
10
10
  profile: AppBskyActorDefs.ProfileViewBasic | null;
11
11
  chatIndex: { [key: string]: ChatMessageViewHydrated };
12
12
  chat: ChatMessageViewHydrated[];
13
+ authors: { [key: string]: ChatMessageViewHydrated["chatProfile"] };
13
14
  livestream: LivestreamViewHydrated | null;
14
15
  viewers: number | null;
15
16
  segment: PlaceStreamSegment.Record | null;
16
17
  renditions: PlaceStreamDefs.Rendition[];
17
18
  replyToMessage: ChatMessageViewHydrated | null;
19
+ streamKey: string | null;
20
+ setStreamKey: (key: string | null) => void;
18
21
  }
@@ -16,6 +16,9 @@ export const makeLivestreamStore = (): StoreApi<LivestreamState> => {
16
16
  segment: null,
17
17
  renditions: [],
18
18
  replyToMessage: null,
19
+ streamKey: null,
20
+ setStreamKey: (sk) => set({ streamKey: sk }),
21
+ authors: {},
19
22
  }));
20
23
  };
21
24
 
@@ -0,0 +1,124 @@
1
+ import { bytesToMultibase, Secp256k1Keypair } from "@atproto/crypto";
2
+ import { useEffect, useState } from "react";
3
+ import { Platform } from "react-native";
4
+ import { PlaceStreamKey } from "streamplace";
5
+ import { privateKeyToAccount } from "viem/accounts";
6
+ import { usePDSAgent } from "../streamplace-store/xrpc";
7
+ import { useLivestreamStore } from "./livestream-store";
8
+
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
+ export const useStreamKey = (): {
38
+ streamKey: {
39
+ privateKey: string;
40
+ did: string;
41
+ address: string;
42
+ } | null;
43
+ error: string | null;
44
+ } => {
45
+ const pdsAgent = usePDSAgent();
46
+ const streamKey = useLivestreamStore((state) => state.streamKey);
47
+ const setStreamKey = useLivestreamStore((state) => state.setStreamKey);
48
+ const [key, setKey] = useState<any>(streamKey ? JSON.parse(streamKey) : null);
49
+ const [error, setError] = useState<string | null>(null);
50
+
51
+ useEffect(() => {
52
+ if (key) return; // already have key
53
+
54
+ const generateKey = async () => {
55
+ if (!pdsAgent) {
56
+ setError("PDS Agent is not available");
57
+ return;
58
+ }
59
+ let did = pdsAgent.did;
60
+ if (!did) {
61
+ setError("PDS Agent did is not available (not logged in?)");
62
+ return;
63
+ }
64
+
65
+ const keypair = await Secp256k1Keypair.create({ exportable: true });
66
+ const exportedKey = await keypair.export();
67
+ const didBytes = new TextEncoder().encode(did);
68
+ const combinedKey = new Uint8Array([...exportedKey, ...didBytes]);
69
+ const multibaseKey = bytesToMultibase(combinedKey, "base58btc");
70
+ const hexKey = Array.from(exportedKey)
71
+ .map((b) => b.toString(16).padStart(2, "0"))
72
+ .join("");
73
+ const account = privateKeyToAccount(`0x${hexKey}`);
74
+ const newKey = {
75
+ privateKey: multibaseKey,
76
+ did: keypair.did(),
77
+ address: account.address.toLowerCase(),
78
+ };
79
+
80
+ let platform: string = Platform.OS;
81
+ if (
82
+ Platform.OS === "web" &&
83
+ typeof window !== "undefined" &&
84
+ window.navigator
85
+ ) {
86
+ if (window.navigator.userAgent.includes("streamplace-desktop")) {
87
+ platform = "Desktop";
88
+ } else {
89
+ platform = getBrowserName(window.navigator.userAgent);
90
+ if (platform !== "unknown") {
91
+ platform = platform;
92
+ }
93
+ }
94
+ } else if (platform === "android") {
95
+ platform = "Android";
96
+ } else if (platform === "ios") {
97
+ platform = "iOS";
98
+ } else if (platform === "macos") {
99
+ platform = "macOS";
100
+ } else if (platform === "windows") {
101
+ platform = "Windows";
102
+ }
103
+
104
+ const record: PlaceStreamKey.Record = {
105
+ signingKey: keypair.did(),
106
+ createdAt: new Date().toISOString(),
107
+ createdBy: "Streamplace on " + platform,
108
+ };
109
+ await pdsAgent.com.atproto.repo.createRecord({
110
+ repo: did,
111
+ collection: "place.stream.key",
112
+ record,
113
+ });
114
+
115
+ setStreamKey(JSON.stringify(newKey));
116
+ setKey(newKey);
117
+ };
118
+
119
+ generateKey();
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, [key, setStreamKey]);
122
+
123
+ return { streamKey: key, error };
124
+ };
@@ -33,7 +33,6 @@ export const PlayerProvider: React.FC<PlayerProviderProps> = ({
33
33
  );
34
34
 
35
35
  const createPlayer = useCallback((id?: string) => {
36
- console.log("Creating new player");
37
36
  const playerId = id || Math.random().toString(36).slice(8);
38
37
  const playerStore = makePlayerStore(playerId);
39
38
 
@@ -1,3 +1,5 @@
1
+ import { ChatMessageViewHydrated } from "streamplace";
2
+
1
3
  export enum PlayerProtocol {
2
4
  WEBRTC = "webrtc",
3
5
  HLS = "hls",
@@ -41,6 +43,10 @@ export interface PlayerState {
41
43
  /** Function to set the ingestStarting flag */
42
44
  setIngestStarting: (ingestStarting: boolean) => void;
43
45
 
46
+ /** Flag indicating if ingest is live */
47
+ ingestLive: boolean;
48
+ setIngestLive: (ingestLive: boolean) => void;
49
+
44
50
  /** Current connection state of ingest RTP/RTC peer connection */
45
51
  ingestConnectionState: RTCPeerConnectionState | null;
46
52
 
@@ -50,6 +56,9 @@ export interface PlayerState {
50
56
  ingestMediaSource?: IngestMediaSource;
51
57
  setIngestMediaSource?: (source: IngestMediaSource) => void;
52
58
 
59
+ ingestCamera: "user" | "environment";
60
+ setIngestCamera: (camera: "user" | "environment") => void;
61
+
53
62
  ingestAutoStart?: boolean;
54
63
  setIngestAutoStart?: (autoStart: boolean) => void;
55
64
 
@@ -110,6 +119,16 @@ export interface PlayerState {
110
119
  | undefined,
111
120
  ) => void;
112
121
 
122
+ /** Player element width (CSS value or number) */
123
+ playerWidth?: string | number;
124
+ /** Function to set the player width */
125
+ setPlayerWidth: (width: number) => void;
126
+
127
+ /** Player element height (CSS value or number) */
128
+ playerHeight?: string | number;
129
+ /** Function to set the player height */
130
+ setPlayerHeight: (height: number) => void;
131
+
113
132
  /** Flag indicating if player is in Picture-in-Picture mode */
114
133
  pipMode: boolean;
115
134
 
@@ -148,6 +167,15 @@ export interface PlayerState {
148
167
  clearControlsTimeout: () => void;
149
168
 
150
169
  setUserInteraction: () => void;
170
+
171
+ showDebugInfo: boolean;
172
+ setShowDebugInfo: (showDebugInfo: boolean) => void;
173
+
174
+ /** Message to be moderated */
175
+ modMessage: ChatMessageViewHydrated | null;
176
+
177
+ /** Function to set the mod message */
178
+ setModMessage: (message: ChatMessageViewHydrated | null) => void;
151
179
  }
152
180
 
153
181
  export type PlayerEvent = {
@@ -1,4 +1,5 @@
1
1
  import { useContext } from "react";
2
+ import { ChatMessageViewHydrated } from "streamplace";
2
3
  import { createStore, StoreApi, useStore } from "zustand";
3
4
  import { PlayerContext } from "./context";
4
5
  import {
@@ -32,6 +33,10 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
32
33
  setIngestMediaSource: (ingestMediaSource: IngestMediaSource | undefined) =>
33
34
  set(() => ({ ingestMediaSource })),
34
35
 
36
+ ingestCamera: "user",
37
+ setIngestCamera: (ingestCamera: "user" | "environment") =>
38
+ set(() => ({ ingestCamera })),
39
+
35
40
  ingestConnectionState: null,
36
41
  setIngestConnectionState: (
37
42
  ingestConnectionState: RTCPeerConnectionState | null,
@@ -78,6 +83,12 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
78
83
  pipMode: false,
79
84
  setPipMode: (pipMode: boolean) => set(() => ({ pipMode })),
80
85
 
86
+ // Player element width/height setters for global sync
87
+ playerWidth: undefined,
88
+ setPlayerWidth: (playerWidth: number) => set(() => ({ playerWidth })),
89
+ playerHeight: undefined,
90
+ setPlayerHeight: (playerHeight: number) => set(() => ({ playerHeight })),
91
+
81
92
  // * Whether mute was forced by the browser or not for autoplay
82
93
  // * Will get set to 'false' if the user has interacted with the volume
83
94
  muteWasForced: false,
@@ -95,6 +106,9 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
95
106
  telemetry: true,
96
107
  setTelemetry: (telemetry: boolean) => set(() => ({ telemetry })),
97
108
 
109
+ ingestLive: false,
110
+ setIngestLive: (ingestLive: boolean) => set(() => ({ ingestLive })),
111
+
98
112
  playerEvent: async (
99
113
  url: string,
100
114
  time: string,
@@ -141,6 +155,14 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
141
155
  let controlsTimeout = setTimeout(() => p.setShowControls(false), 1000);
142
156
  return { showControls: true, controlsTimeout };
143
157
  }),
158
+
159
+ showDebugInfo: false,
160
+ setShowDebugInfo: (showDebugInfo: boolean) =>
161
+ set(() => ({ showDebugInfo })),
162
+
163
+ modMessage: null,
164
+ setModMessage: (modMessage: ChatMessageViewHydrated | null) =>
165
+ set(() => ({ modMessage })),
144
166
  }));
145
167
  };
146
168
 
@@ -0,0 +1,29 @@
1
+ import { AppBskyGraphBlock } from "@atproto/api";
2
+ import { usePDSAgent } from "./xrpc";
3
+
4
+ export function useCreateBlockRecord() {
5
+ let agent = usePDSAgent();
6
+
7
+ return async (subjectDID: string) => {
8
+ if (!agent) {
9
+ throw new Error("No PDS agent found");
10
+ }
11
+
12
+ if (!agent.did) {
13
+ throw new Error("No user DID found, assuming not logged in");
14
+ }
15
+
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;
28
+ };
29
+ }
@@ -1,2 +1,3 @@
1
+ export * from "./stream";
1
2
  export * from "./streamplace-store";
2
3
  export * from "./user";