@streamplace/components 0.0.1 → 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 (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,261 @@
1
+ import { RichText } from "@atproto/api";
2
+ import { useCallback } from "react";
3
+ import {
4
+ ChatMessageViewHydrated,
5
+ PlaceStreamChatMessage,
6
+ PlaceStreamDefs,
7
+ } from "streamplace";
8
+ import { useChatProfile, useDID, useHandle } from "../streamplace-store";
9
+ import { usePDSAgent } from "../streamplace-store/xrpc";
10
+ import { LivestreamState } from "./livestream-state";
11
+ import { getStoreFromContext, useLivestreamStore } from "./livestream-store";
12
+
13
+ export const useReplyToMessage = () =>
14
+ useLivestreamStore((state) => state.replyToMessage);
15
+
16
+ export const useSetReplyToMessage = () => {
17
+ const store = getStoreFromContext();
18
+ return useCallback(
19
+ (message: ChatMessageViewHydrated | null) => {
20
+ store.setState({ replyToMessage: message });
21
+ },
22
+ [store],
23
+ );
24
+ };
25
+
26
+ export type NewChatMessage = {
27
+ text: string;
28
+ reply?: {
29
+ cid: string;
30
+ uri: string;
31
+ };
32
+ };
33
+
34
+ export const useCreateChatMessage = () => {
35
+ const pdsAgent = usePDSAgent();
36
+ const store = getStoreFromContext();
37
+ const userDID = useDID();
38
+ const userHandle = useHandle();
39
+ const chatProfile = useChatProfile();
40
+
41
+ return async (msg: NewChatMessage) => {
42
+ if (!pdsAgent || !userDID) {
43
+ throw new Error("No PDS agent or user DID found");
44
+ }
45
+
46
+ let state = store.getState();
47
+
48
+ const streamerProfile = state.profile;
49
+
50
+ if (!streamerProfile) {
51
+ throw new Error("Profile not found");
52
+ }
53
+
54
+ const rt = new RichText({ text: msg.text });
55
+ await rt.detectFacets(pdsAgent);
56
+
57
+ const record: PlaceStreamChatMessage.Record = {
58
+ text: msg.text,
59
+ createdAt: new Date().toISOString(),
60
+ streamer: streamerProfile.did,
61
+ facets: rt.facets as PlaceStreamChatMessage.Record["facets"],
62
+ ...(msg.reply
63
+ ? {
64
+ reply: {
65
+ root: {
66
+ cid: msg.reply.cid,
67
+ uri: msg.reply.uri,
68
+ },
69
+ parent: {
70
+ cid: msg.reply.cid,
71
+ uri: msg.reply.uri,
72
+ },
73
+ },
74
+ }
75
+ : {}),
76
+ };
77
+
78
+ const localChat: ChatMessageViewHydrated = {
79
+ uri: `local-${Date.now()}`,
80
+ cid: "",
81
+ author: {
82
+ did: userDID,
83
+ handle: userHandle || userDID,
84
+ },
85
+ record: record,
86
+ indexedAt: new Date().toISOString(),
87
+ chatProfile: chatProfile || undefined,
88
+ };
89
+
90
+ state = reduceChat(state, [localChat], []);
91
+ store.setState(state);
92
+
93
+ await pdsAgent.com.atproto.repo.createRecord({
94
+ repo: userDID,
95
+ collection: "place.stream.chat.message",
96
+ record,
97
+ });
98
+ };
99
+ };
100
+
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
+ };
114
+
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 = (
138
+ state: LivestreamState,
139
+ newMessages: ChatMessageViewHydrated[],
140
+ blocks: PlaceStreamDefs.BlockView[],
141
+ ): LivestreamState => {
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>();
150
+
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) {
166
+ const date = new Date(message.record.createdAt);
167
+ const key = `${date.getTime()}-${message.uri}`;
168
+
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
182
+ if (!message.uri.startsWith("local-")) {
183
+ const existingLocalKey = Object.keys(newChatIndex).find((k) => {
184
+ const msg = newChatIndex[k];
185
+ return (
186
+ msg.uri.startsWith("local-") &&
187
+ msg.record.text === message.record.text &&
188
+ msg.author.did === message.author.did &&
189
+ Math.abs(new Date(msg.record.createdAt).getTime() - date.getTime()) <
190
+ 10000 // Within 10 seconds
191
+ );
192
+ });
193
+
194
+ if (existingLocalKey) {
195
+ delete newChatIndex[existingLocalKey];
196
+ removedKeys.add(existingLocalKey);
197
+ hasChanges = true;
198
+ }
199
+ }
200
+
201
+ // add reply info
202
+ let processedMessage = message;
203
+ if (message.record.reply) {
204
+ const reply = message.record.reply as {
205
+ parent?: { uri: string; cid: string };
206
+ root?: { uri: string; cid: string };
207
+ };
208
+
209
+ const parentUri = reply?.parent?.uri || reply?.root?.uri;
210
+ if (parentUri) {
211
+ const parentMsgKey = Object.keys(newChatIndex).find(
212
+ (k) => newChatIndex[k].uri === parentUri,
213
+ );
214
+
215
+ if (parentMsgKey) {
216
+ const parentMsg = newChatIndex[parentMsgKey];
217
+ processedMessage = {
218
+ ...message,
219
+ replyTo: {
220
+ cid: parentMsg.cid,
221
+ uri: parentMsg.uri,
222
+ author: parentMsg.author,
223
+ record: parentMsg.record,
224
+ chatProfile: parentMsg.chatProfile,
225
+ indexedAt: parentMsg.indexedAt,
226
+ },
227
+ };
228
+ }
229
+ }
230
+ }
231
+
232
+ messagesToAdd.push({ key, message: processedMessage });
233
+ hasChanges = true;
234
+ }
235
+
236
+ // Add new messages to index
237
+ for (const { key, message } of messagesToAdd) {
238
+ newChatIndex[key] = message;
239
+ }
240
+
241
+ // only rebuild if we have changes
242
+ if (!hasChanges) {
243
+ return state;
244
+ }
245
+
246
+ // Build the new sorted chat list efficiently
247
+ const newChatList = buildSortedChatList(
248
+ newChatIndex,
249
+ state.chat,
250
+ messagesToAdd,
251
+ removedKeys,
252
+ );
253
+
254
+ return {
255
+ ...state,
256
+ chatIndex: newChatIndex,
257
+ chat: newChatList,
258
+ };
259
+ };
260
+
261
+ export const reduceChat = reduceChatIncremental;
@@ -0,0 +1,10 @@
1
+ import { createContext } from "react";
2
+ import { LivestreamStore } from "../livestream-store/livestream-store";
3
+
4
+ type LivestreamContextType = {
5
+ store: LivestreamStore;
6
+ };
7
+
8
+ export const LivestreamContext = createContext<LivestreamContextType | null>(
9
+ null,
10
+ );
@@ -0,0 +1,4 @@
1
+ export * from "./chat";
2
+ export * from "./context";
3
+ export * from "./livestream-store";
4
+ export * from "./stream-key";
@@ -0,0 +1,21 @@
1
+ import { AppBskyActorDefs } from "@atproto/api";
2
+ import {
3
+ ChatMessageViewHydrated,
4
+ LivestreamViewHydrated,
5
+ PlaceStreamDefs,
6
+ PlaceStreamSegment,
7
+ } from "streamplace";
8
+
9
+ export interface LivestreamState {
10
+ profile: AppBskyActorDefs.ProfileViewBasic | null;
11
+ chatIndex: { [key: string]: ChatMessageViewHydrated };
12
+ chat: ChatMessageViewHydrated[];
13
+ authors: { [key: string]: ChatMessageViewHydrated["chatProfile"] };
14
+ livestream: LivestreamViewHydrated | null;
15
+ viewers: number | null;
16
+ segment: PlaceStreamSegment.Record | null;
17
+ renditions: PlaceStreamDefs.Rendition[];
18
+ replyToMessage: ChatMessageViewHydrated | null;
19
+ streamKey: string | null;
20
+ setStreamKey: (key: string | null) => void;
21
+ }
@@ -0,0 +1,59 @@
1
+ import { useContext } from "react";
2
+ import { createStore, StoreApi, useStore } from "zustand";
3
+ import { LivestreamContext } from "./context";
4
+ import { LivestreamState } from "./livestream-state";
5
+ import { handleWebSocketMessages } from "./websocket-consumer";
6
+
7
+ export type LivestreamStore = StoreApi<LivestreamState>;
8
+
9
+ export const makeLivestreamStore = (): StoreApi<LivestreamState> => {
10
+ return createStore<LivestreamState>()((set) => ({
11
+ profile: null,
12
+ chatIndex: {},
13
+ chat: [],
14
+ livestream: null,
15
+ viewers: null,
16
+ segment: null,
17
+ renditions: [],
18
+ replyToMessage: null,
19
+ streamKey: null,
20
+ setStreamKey: (sk) => set({ streamKey: sk }),
21
+ authors: {},
22
+ }));
23
+ };
24
+
25
+ export function getStoreFromContext(): LivestreamStore {
26
+ const context = useContext(LivestreamContext);
27
+ if (!context) {
28
+ throw new Error(
29
+ "useLivestreamStore must be used within a LivestreamProvider",
30
+ );
31
+ }
32
+ return context.store;
33
+ }
34
+
35
+ export function useLivestreamStore<U>(
36
+ selector: (state: LivestreamState) => U,
37
+ ): U {
38
+ const store = getStoreFromContext();
39
+ return useStore(store, selector);
40
+ }
41
+
42
+ export const useHandleWebsocketMessages = () => {
43
+ const store = getStoreFromContext();
44
+ return (messages: any[]) => {
45
+ store.setState((state) => handleWebSocketMessages(state, messages));
46
+ };
47
+ };
48
+
49
+ export const useChat = () => useLivestreamStore((x) => x.chat);
50
+
51
+ export const useProfile = () => useLivestreamStore((x) => x.profile);
52
+
53
+ export const useViewers = () => useLivestreamStore((x) => x.viewers);
54
+
55
+ export const useLivestream = () => useLivestreamStore((x) => x.livestream);
56
+
57
+ export const useSegment = () => useLivestreamStore((x) => x.segment);
58
+
59
+ export const useRenditions = () => useLivestreamStore((x) => x.renditions);
@@ -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
+ };
@@ -0,0 +1,62 @@
1
+ import { AppBskyActorDefs } from "@atproto/api";
2
+ import {
3
+ ChatMessageViewHydrated,
4
+ LivestreamViewHydrated,
5
+ PlaceStreamChatDefs,
6
+ PlaceStreamChatMessage,
7
+ PlaceStreamDefs,
8
+ PlaceStreamLivestream,
9
+ PlaceStreamSegment,
10
+ } from "streamplace";
11
+ import { reduceChat } from "./chat";
12
+ import { LivestreamState } from "./livestream-state";
13
+
14
+ export const handleWebSocketMessages = (
15
+ state: LivestreamState,
16
+ messages: any[],
17
+ ): LivestreamState => {
18
+ for (const message of messages) {
19
+ if (PlaceStreamLivestream.isLivestreamView(message)) {
20
+ state = {
21
+ ...state,
22
+ livestream: message as LivestreamViewHydrated,
23
+ };
24
+ } else if (PlaceStreamLivestream.isViewerCount(message)) {
25
+ state = {
26
+ ...state,
27
+ viewers: message.count,
28
+ };
29
+ } else if (PlaceStreamChatDefs.isMessageView(message)) {
30
+ // Explicitly map MessageView to MessageViewHydrated
31
+ const hydrated: ChatMessageViewHydrated = {
32
+ uri: message.uri,
33
+ cid: message.cid,
34
+ author: message.author,
35
+ record: message.record as PlaceStreamChatMessage.Record,
36
+ indexedAt: message.indexedAt,
37
+ chatProfile: (message as any).chatProfile,
38
+ replyTo: (message as any).replyTo,
39
+ };
40
+ state = reduceChat(state, [hydrated], []);
41
+ } else if (PlaceStreamSegment.isRecord(message)) {
42
+ state = {
43
+ ...state,
44
+ segment: message as PlaceStreamSegment.Record,
45
+ };
46
+ } else if (PlaceStreamDefs.isBlockView(message)) {
47
+ const block = message as PlaceStreamDefs.BlockView;
48
+ state = reduceChat(state, [], [block]);
49
+ } else if (PlaceStreamDefs.isRenditions(message)) {
50
+ state = {
51
+ ...state,
52
+ renditions: message.renditions,
53
+ };
54
+ } else if (AppBskyActorDefs.isProfileViewBasic(message)) {
55
+ state = {
56
+ ...state,
57
+ profile: message,
58
+ };
59
+ }
60
+ }
61
+ return reduceChat(state, [], []);
62
+ };
@@ -0,0 +1,11 @@
1
+ import { createContext } from "react";
2
+ import { StoreApi } from "zustand";
3
+ import { PlayerState } from "./player-state";
4
+
5
+ type PlayerContextType = {
6
+ players: Record<string, StoreApi<PlayerState>>;
7
+ createPlayer: (id?: string) => string;
8
+ removePlayer: (id: string) => void;
9
+ };
10
+
11
+ export const PlayerContext = createContext<PlayerContextType | null>(null);
@@ -0,0 +1,6 @@
1
+ // barrel file :)
2
+ export * from "./context";
3
+ export * from "./player-provider";
4
+ export * from "./player-state";
5
+ export * from "./player-store";
6
+ export * from "./single-player-provider";
@@ -0,0 +1,89 @@
1
+ import React, { useCallback, useMemo, useState } from "react";
2
+ import { StoreApi } from "zustand";
3
+ import { PlayerContext } from "./context";
4
+ import { PlayerState } from "./player-state";
5
+ import { makePlayerStore } from "./player-store";
6
+
7
+ interface PlayerProviderProps {
8
+ children: React.ReactNode;
9
+ initialPlayers?: string[];
10
+ defaultId?: string;
11
+ }
12
+
13
+ export const PlayerProvider: React.FC<PlayerProviderProps> = ({
14
+ children,
15
+ initialPlayers = [],
16
+ defaultId = Math.random().toString(36).slice(8),
17
+ }) => {
18
+ const [players, setPlayers] = useState<Record<string, StoreApi<PlayerState>>>(
19
+ () => {
20
+ // Initialize with any initial player IDs provided
21
+ const initialPlayerStores: Record<string, StoreApi<PlayerState>> = {};
22
+ for (const playerId of initialPlayers) {
23
+ initialPlayerStores[playerId] = makePlayerStore(playerId);
24
+ }
25
+
26
+ // Always create at least one player by default
27
+ if (initialPlayers.length === 0) {
28
+ initialPlayerStores[defaultId] = makePlayerStore(defaultId);
29
+ }
30
+
31
+ return initialPlayerStores;
32
+ },
33
+ );
34
+
35
+ const createPlayer = useCallback((id?: string) => {
36
+ const playerId = id || Math.random().toString(36).slice(8);
37
+ const playerStore = makePlayerStore(playerId);
38
+
39
+ setPlayers((prev) => ({
40
+ ...prev,
41
+ [playerId]: playerStore,
42
+ }));
43
+
44
+ return playerId;
45
+ }, []);
46
+
47
+ const removePlayer = useCallback((id: string) => {
48
+ setPlayers((prev) => {
49
+ // Don't remove the last player
50
+ if (Object.keys(prev).length <= 1) {
51
+ console.warn("Cannot remove the last player");
52
+ return prev;
53
+ }
54
+
55
+ const newPlayers = { ...prev };
56
+ delete newPlayers[id];
57
+ return newPlayers;
58
+ });
59
+ }, []);
60
+
61
+ const contextValue = useMemo(
62
+ () => ({
63
+ players,
64
+ createPlayer,
65
+ removePlayer,
66
+ }),
67
+ [players, createPlayer, removePlayer],
68
+ );
69
+
70
+ return (
71
+ <PlayerContext.Provider value={contextValue}>
72
+ {children}
73
+ </PlayerContext.Provider>
74
+ );
75
+ };
76
+
77
+ // HOC to wrap components that need player context
78
+ export function withPlayerProvider<P extends object>(
79
+ Component: React.ComponentType<P>,
80
+ ): React.FC<P & { initialPlayers?: string[] }> {
81
+ return function WithPlayerProvider(props: P & { initialPlayers?: string[] }) {
82
+ const { initialPlayers, ...componentProps } = props;
83
+ return (
84
+ <PlayerProvider initialPlayers={initialPlayers}>
85
+ <Component {...(componentProps as P)} />
86
+ </PlayerProvider>
87
+ );
88
+ };
89
+ }