@streamplace/components 0.7.21 → 0.7.26

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 (49) hide show
  1. package/dist/components/chat/chat-box.js +0 -6
  2. package/dist/components/chat/chat-message.js +9 -4
  3. package/dist/components/chat/chat.js +14 -4
  4. package/dist/components/mobile-player/ui/autoplay-button.js +1 -1
  5. package/dist/components/mobile-player/video-async.native.js +4 -4
  6. package/dist/components/mobile-player/video.js +3 -3
  7. package/dist/components/mobile-player/webrtc-diagnostics.js +67 -13
  8. package/dist/index.js +4 -1
  9. package/dist/lib/system-messages.js +1 -0
  10. package/dist/livestream-store/chat.js +11 -0
  11. package/dist/livestream-store/stream-key.js +1 -0
  12. package/dist/livestream-store/websocket-consumer.js +4 -1
  13. package/dist/player-store/player-store.js +0 -4
  14. package/dist/storage/index.js +5 -0
  15. package/dist/storage/lock.js +40 -0
  16. package/dist/storage/storage.js +14 -0
  17. package/dist/storage/storage.native.js +44 -0
  18. package/dist/storage/storage.shared.js +2 -0
  19. package/dist/streamplace-provider/index.js +1 -0
  20. package/dist/streamplace-store/stream.js +2 -0
  21. package/dist/streamplace-store/streamplace-store.js +75 -2
  22. package/dist/streamplace-store/xrpc.js +10 -1
  23. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  24. package/package.json +5 -4
  25. package/src/components/chat/chat-box.tsx +0 -11
  26. package/src/components/chat/chat-message.tsx +8 -4
  27. package/src/components/chat/chat.tsx +20 -4
  28. package/src/components/mobile-player/ui/autoplay-button.tsx +2 -2
  29. package/src/components/mobile-player/video-async.native.tsx +6 -4
  30. package/src/components/mobile-player/video.tsx +6 -3
  31. package/src/components/mobile-player/webrtc-diagnostics.tsx +73 -15
  32. package/src/index.tsx +4 -0
  33. package/src/lib/system-messages.ts +1 -0
  34. package/src/livestream-store/chat.tsx +15 -0
  35. package/src/livestream-store/stream-key.tsx +1 -0
  36. package/src/livestream-store/websocket-consumer.tsx +4 -1
  37. package/src/player-store/player-state.tsx +0 -12
  38. package/src/player-store/player-store.tsx +0 -8
  39. package/src/storage/index.tsx +3 -0
  40. package/src/storage/lock.tsx +38 -0
  41. package/src/storage/storage.native.tsx +42 -0
  42. package/src/storage/storage.shared.tsx +5 -0
  43. package/src/storage/storage.tsx +15 -0
  44. package/src/streamplace-provider/index.tsx +2 -1
  45. package/src/streamplace-store/stream.tsx +2 -0
  46. package/src/streamplace-store/streamplace-store.tsx +92 -2
  47. package/src/streamplace-store/xrpc.tsx +9 -2
  48. package/tsconfig.json +2 -1
  49. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamplace/components",
3
- "version": "0.7.21",
3
+ "version": "0.7.26",
4
4
  "description": "Streamplace React (Native) Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.tsx",
@@ -20,7 +20,7 @@
20
20
  "tsup": "^8.5.0"
21
21
  },
22
22
  "dependencies": {
23
- "@atproto/api": "^0.15.7",
23
+ "@atproto/api": "^0.16.7",
24
24
  "@atproto/crypto": "^0.4.4",
25
25
  "@emoji-mart/react": "^1.1.1",
26
26
  "@gorhom/bottom-sheet": "^5.1.6",
@@ -29,6 +29,7 @@
29
29
  "@rn-primitives/slider": "^1.2.0",
30
30
  "class-variance-authority": "^0.6.1",
31
31
  "expo-keep-awake": "^14.0.0",
32
+ "expo-sqlite": "~15.2.12",
32
33
  "expo-video": "^2.0.0",
33
34
  "hls.js": "^1.5.17",
34
35
  "lucide-react-native": "^0.514.0",
@@ -40,7 +41,7 @@
40
41
  "react-native-svg": "^15.0.0",
41
42
  "react-native-webrtc": "git+https://github.com/streamplace/react-native-webrtc.git#6b8472a771ac47f89217d327058a8a4124a6ae56",
42
43
  "react-use-websocket": "^4.13.0",
43
- "streamplace": "0.7.21",
44
+ "streamplace": "0.7.25",
44
45
  "viem": "^2.21.44",
45
46
  "zustand": "^5.0.5"
46
47
  },
@@ -52,5 +53,5 @@
52
53
  "start": "tsc --watch --preserveWatchOutput",
53
54
  "prepare": "tsc"
54
55
  },
55
- "gitHead": "e930332a9465e0ffd9a78a01ea39b134cd78e49e"
56
+ "gitHead": "4dcda25a5e66a2c1a9f412bee2e09c4dd528a939"
56
57
  }
@@ -27,7 +27,6 @@ import {
27
27
  py,
28
28
  w,
29
29
  } from "../../lib/theme/atoms";
30
- import { usePDSAgent } from "../../streamplace-store/xrpc";
31
30
  import { Textarea } from "../ui/textarea";
32
31
  import { RenderChatMessage } from "./chat-message";
33
32
  import { EmojiData, EmojiSuggestions } from "./emoji-suggestions";
@@ -70,16 +69,6 @@ export function ChatBox({
70
69
  const setReplyToMessage = useSetReplyToMessage();
71
70
  const textAreaRef = useRef<TextInput>(null);
72
71
 
73
- // are we logged in?
74
-
75
- let agent = usePDSAgent();
76
-
77
- if (!agent?.did) {
78
- <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}>
79
- <Text>Log in to chat.</Text>
80
- </View>;
81
- }
82
-
83
72
  const authors = useMemo(() => {
84
73
  if (!chat) return null;
85
74
  return chat.reduce((acc, msg) => {
@@ -66,6 +66,9 @@ const segmentedObject = (
66
66
  {obj.text}
67
67
  </Text>
68
68
  );
69
+ } else {
70
+ // render as normal text if we don't recognize the facet type
71
+ return <Text key={`unknown-facet-${index}`}>{obj.text}</Text>;
69
72
  }
70
73
  } else {
71
74
  return <Text key={`text-${index}`}>{obj.text}</Text>;
@@ -105,9 +108,10 @@ export const RenderChatMessage = memo(
105
108
  hour12: false,
106
109
  });
107
110
  }, []);
111
+ const replyTo = (item.replyTo as ChatMessageViewHydrated) || null;
108
112
  return (
109
113
  <>
110
- {item.replyTo && showReply && (
114
+ {replyTo && showReply && (
111
115
  <View
112
116
  style={[
113
117
  gap.all[2],
@@ -130,11 +134,11 @@ export const RenderChatMessage = memo(
130
134
  >
131
135
  <Text
132
136
  style={{
133
- color: getRgbColor((item.replyTo.chatProfile as any).color),
137
+ color: getRgbColor(replyTo.chatProfile?.color),
134
138
  fontWeight: "thin",
135
139
  }}
136
140
  >
137
- @{(item.replyTo.author as any).handle}
141
+ @{(replyTo.author as any).handle}
138
142
  </Text>{" "}
139
143
  <Text
140
144
  style={{
@@ -142,7 +146,7 @@ export const RenderChatMessage = memo(
142
146
  fontStyle: "italic",
143
147
  }}
144
148
  >
145
- {(item.replyTo.record as any).text}
149
+ {replyTo.record.text}
146
150
  </Text>
147
151
  </Text>
148
152
  </View>
@@ -1,6 +1,6 @@
1
1
  import { Ellipsis, Reply } from "lucide-react-native";
2
2
  import { ComponentProps, memo, useEffect, useRef, useState } from "react";
3
- import { Platform, Pressable } from "react-native";
3
+ import { Keyboard, Platform, Pressable } from "react-native";
4
4
  import { FlatList } from "react-native-gesture-handler";
5
5
  import Swipeable, {
6
6
  SwipeableMethods,
@@ -223,9 +223,7 @@ const ChatLine = memo(
223
223
  }
224
224
  renderLeftActions={Platform.OS === "android" ? undefined : LeftAction}
225
225
  overshootFriction={9}
226
- ref={(ref) => {
227
- swipeableRef.current = ref;
228
- }}
226
+ ref={swipeableRef}
229
227
  onSwipeableOpen={(r) => {
230
228
  if (r === (Platform.OS === "android" ? "right" : "left")) {
231
229
  setReply(item);
@@ -258,6 +256,22 @@ export function Chat({
258
256
  canModerate?: boolean;
259
257
  }) {
260
258
  const chat = useChat();
259
+ const [isScrolledUp, setIsScrolledUp] = useState(false);
260
+
261
+ const handleScroll = (event: any) => {
262
+ const { contentOffset } = event.nativeEvent;
263
+
264
+ const scrolledUp = contentOffset.y > 20; // threshold
265
+
266
+ if (scrolledUp !== isScrolledUp) {
267
+ setIsScrolledUp(scrolledUp);
268
+
269
+ // Dismiss keyboard when scrolled up
270
+ if (scrolledUp && Platform.OS !== "web") {
271
+ Keyboard.dismiss();
272
+ }
273
+ }
274
+ };
261
275
 
262
276
  if (!chat)
263
277
  return (
@@ -288,6 +302,8 @@ export function Chat({
288
302
  maxToRenderPerBatch={10}
289
303
  initialNumToRender={10}
290
304
  updateCellsBatchingPeriod={50}
305
+ onScroll={handleScroll}
306
+ scrollEventThrottle={16}
291
307
  />
292
308
  <ModView />
293
309
  </View>
@@ -1,12 +1,12 @@
1
1
  import { Play } from "lucide-react-native";
2
2
  import { Pressable } from "react-native";
3
- import { View, layout, usePlayerStore } from "../../..";
3
+ import { View, layout, usePlayerStore, useSetMuted } from "../../..";
4
4
  import { h, p, w } from "../../../ui";
5
5
 
6
6
  export function AutoplayButton() {
7
7
  const autoplayFailed = usePlayerStore((x) => x.autoplayFailed);
8
8
  const setAutoplayFailed = usePlayerStore((x) => x.setAutoplayFailed);
9
- const setMuted = usePlayerStore((x) => x.setMuted);
9
+ const setMuted = useSetMuted();
10
10
  const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced);
11
11
  const setUserInteraction = usePlayerStore((x) => x.setUserInteraction);
12
12
  const videoRef = usePlayerStore((x) => x.videoRef);
@@ -14,7 +14,9 @@ import {
14
14
  PlayerProtocol,
15
15
  PlayerStatus,
16
16
  Text,
17
+ useEffectiveVolume,
17
18
  usePlayerStore as useIngestPlayerStore,
19
+ useMuted,
18
20
  usePlayerStore,
19
21
  useStreamplaceStore,
20
22
  View,
@@ -71,8 +73,8 @@ export function NativeVideo(props?: {
71
73
  const src = usePlayerStore((x) => x.src);
72
74
  const { url } = srcToUrl({ src: src, selectedRendition }, protocol);
73
75
  const setStatus = usePlayerStore((x) => x.setStatus);
74
- const muted = usePlayerStore((x) => x.muted);
75
- const volume = usePlayerStore((x) => x.volume);
76
+ const muted = useMuted();
77
+ const volume = useEffectiveVolume();
76
78
  const setFullscreen = usePlayerStore((x) => x.setFullscreen);
77
79
  const fullscreen = usePlayerStore((x) => x.fullscreen);
78
80
  const playerEvent = usePlayerStore((x) => x.playerEvent);
@@ -211,8 +213,8 @@ export function NativeWHEP(props?: {
211
213
  }, []);
212
214
 
213
215
  const setStatus = usePlayerStore((x) => x.setStatus);
214
- const muted = usePlayerStore((x) => x.muted);
215
- const volume = usePlayerStore((x) => x.volume);
216
+ const muted = useMuted();
217
+ const volume = useEffectiveVolume();
216
218
 
217
219
  useEffect(() => {
218
220
  if (stuck && status === PlayerStatus.PLAYING) {
@@ -4,7 +4,10 @@ import {
4
4
  IngestMediaSource,
5
5
  PlayerProtocol,
6
6
  PlayerStatus,
7
+ useEffectiveVolume,
8
+ useMuted,
7
9
  usePlayerStore,
10
+ useSetMuted,
8
11
  useStreamplaceStore,
9
12
  } from "../..";
10
13
  import { borderRadius, colors, mt } from "../../lib/theme/atoms";
@@ -135,11 +138,11 @@ const VideoElement = forwardRef<
135
138
  const x = usePlayerStore((x) => x);
136
139
  const url = useStreamplaceStore((x) => x.url);
137
140
  const playerEvent = usePlayerStore((x) => x.playerEvent);
138
- const setMuted = usePlayerStore((x) => x.setMuted);
139
141
  const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced);
140
- const muted = usePlayerStore((x) => x.muted);
141
142
  const ingest = usePlayerStore((x) => x.ingestConnectionState !== null);
142
- const volume = usePlayerStore((x) => x.volume);
143
+ const volume = useEffectiveVolume();
144
+ const muted = useMuted();
145
+ const setMuted = useSetMuted();
143
146
  const setStatus = usePlayerStore((x) => x.setStatus);
144
147
  const setUserInteraction = usePlayerStore((x) => x.setUserInteraction);
145
148
  const setVideoRef = usePlayerStore((x) => x.setVideoRef);
@@ -7,6 +7,7 @@ export interface WebRTCDiagnostics {
7
7
  rtcSessionDescription: boolean;
8
8
  getUserMedia: boolean;
9
9
  getDisplayMedia: boolean;
10
+ isHwH264Supported: boolean;
10
11
  errors: string[];
11
12
  warnings: string[];
12
13
  }
@@ -19,6 +20,7 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
19
20
  rtcSessionDescription: false,
20
21
  getUserMedia: false,
21
22
  getDisplayMedia: false,
23
+ isHwH264Supported: false,
22
24
  errors: [],
23
25
  warnings: [],
24
26
  });
@@ -27,6 +29,23 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
27
29
  const errors: string[] = [];
28
30
  const warnings: string[] = [];
29
31
 
32
+ const checkH264Support = async (): Promise<boolean> => {
33
+ try {
34
+ const pc = new RTCPeerConnection();
35
+ const offer = await pc.createOffer();
36
+ pc.close();
37
+
38
+ if (offer.sdp) {
39
+ const h264Match = offer.sdp.search(/rtpmap:([0-9]+) H264/g);
40
+ return h264Match !== -1;
41
+ }
42
+ return false;
43
+ } catch (error) {
44
+ console.warn("Failed to check H.264 support:", error);
45
+ return false;
46
+ }
47
+ };
48
+
30
49
  // Check if we're in a browser environment
31
50
  if (typeof window === "undefined") {
32
51
  errors.push("Running in non-browser environment");
@@ -37,6 +56,7 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
37
56
  rtcSessionDescription: false,
38
57
  getUserMedia: false,
39
58
  getDisplayMedia: false,
59
+ isHwH264Supported: false,
40
60
  errors,
41
61
  warnings,
42
62
  });
@@ -105,22 +125,45 @@ export function useWebRTCDiagnostics(): WebRTCDiagnostics {
105
125
 
106
126
  const browserSupport = rtcPeerConnection && rtcSessionDescription;
107
127
 
108
- setDiagnostics({
109
- done: true,
110
- browserSupport,
111
- rtcPeerConnection,
112
- rtcSessionDescription,
113
- getUserMedia,
114
- getDisplayMedia,
115
- errors,
116
- warnings,
117
- });
128
+ // Check H.264 support asynchronously
129
+ if (rtcPeerConnection) {
130
+ checkH264Support().then((isHwH264Supported) => {
131
+ if (!isHwH264Supported) {
132
+ warnings.push(
133
+ "H.264 hardware acceleration is not supported\n In Firefox, try enabling 'media.webrtc.hw.h264.enabled' in about:config",
134
+ );
135
+ }
136
+ setDiagnostics({
137
+ done: true,
138
+ browserSupport,
139
+ rtcPeerConnection,
140
+ rtcSessionDescription,
141
+ getUserMedia,
142
+ getDisplayMedia,
143
+ isHwH264Supported,
144
+ errors,
145
+ warnings,
146
+ });
147
+ });
148
+ } else {
149
+ setDiagnostics({
150
+ done: true,
151
+ browserSupport,
152
+ rtcPeerConnection,
153
+ rtcSessionDescription,
154
+ getUserMedia,
155
+ getDisplayMedia,
156
+ isHwH264Supported: false,
157
+ errors,
158
+ warnings,
159
+ });
160
+ }
118
161
  }, []);
119
162
 
120
163
  return diagnostics;
121
164
  }
122
165
 
123
- export function logWebRTCDiagnostics() {
166
+ export async function logWebRTCDiagnostics() {
124
167
  console.group("WebRTC Diagnostics");
125
168
 
126
169
  // Log browser support
@@ -133,17 +176,32 @@ export function logWebRTCDiagnostics() {
133
176
  console.log("User Agent:", navigator.userAgent);
134
177
  console.log("Protocol:", location.protocol);
135
178
  console.log("Host:", location.hostname);
136
-
137
- // Test basic WebRTC functionality
179
+ console.groupEnd();
138
180
  if (window.RTCPeerConnection) {
139
181
  try {
140
182
  const pc = new RTCPeerConnection();
141
- console.log("RTCPeerConnection creation: Success");
183
+ // Check H.264 support
184
+ try {
185
+ const offer = await pc.createOffer({ offerToReceiveVideo: true });
186
+ const isHwH264Supported = offer.sdp
187
+ ? offer.sdp.search(/rtpmap:([0-9]+) H264/g) !== -1
188
+ : false;
189
+ console.group("WebRTC Peer Connection Test");
190
+ console.log("RTCPeerConnection creation: ✓ Success");
191
+ console.log(
192
+ "H.264 support:",
193
+ isHwH264Supported ? "✓ Supported" : "✗ Not supported",
194
+ );
195
+ } catch (error) {
196
+ console.group("WebRTC Peer Connection Test");
197
+ console.error("H.264 check failed:", error);
198
+ }
199
+
142
200
  pc.close();
143
201
  } catch (error) {
202
+ console.group("WebRTC Peer Connection Test");
144
203
  console.error("RTCPeerConnection creation: ✗ Failed", error);
145
204
  }
146
205
  }
147
-
148
206
  console.groupEnd();
149
207
  }
package/src/index.tsx CHANGED
@@ -37,3 +37,7 @@ export * from "./components/keep-awake";
37
37
 
38
38
  // Dashboard components
39
39
  export * as Dashboard from "./components/dashboard";
40
+
41
+ // Storage exports
42
+ export { default as storage } from "./storage";
43
+ export type { AQStorage } from "./storage/storage.shared";
@@ -46,6 +46,7 @@ export const createSystemMessage = (
46
46
  indexedAt: now.toISOString(),
47
47
  chatProfile: {
48
48
  color: { red: 128, green: 128, blue: 128 }, // Gray color for system messages
49
+ $type: "place.stream.chat.profile",
49
50
  },
50
51
  };
51
52
  };
@@ -75,7 +75,21 @@ export const useCreateChatMessage = () => {
75
75
  const rt = new RichText({ text: msg.text });
76
76
  await rt.detectFacets(pdsAgent);
77
77
 
78
+ // filter out any facets that aren't in the allowed list
79
+ rt.facets = rt.facets?.filter((facet) => {
80
+ return (
81
+ // if all features are in the allowed list
82
+ facet.features.every((feature) =>
83
+ [
84
+ "app.bsky.richtext.facet#link",
85
+ "app.bsky.richtext.facet#mention",
86
+ ].includes(feature.$type),
87
+ )
88
+ );
89
+ });
90
+
78
91
  const record: PlaceStreamChatMessage.Record = {
92
+ $type: "place.stream.chat.message",
79
93
  text: msg.text,
80
94
  createdAt: new Date().toISOString(),
81
95
  streamer: streamerProfile.did,
@@ -295,6 +309,7 @@ export const reduceChatIncremental = (
295
309
  processedMessage = {
296
310
  ...message,
297
311
  replyTo: {
312
+ $type: "place.stream.chat.defs#messageView",
298
313
  cid: parentMsg.cid,
299
314
  uri: parentMsg.uri,
300
315
  author: parentMsg.author,
@@ -75,6 +75,7 @@ export const useStreamKey = (): {
75
75
  }
76
76
 
77
77
  const record: PlaceStreamKey.Record = {
78
+ $type: "place.stream.key",
78
79
  signingKey: keypair.did(),
79
80
  createdAt: new Date().toISOString(),
80
81
  createdBy: "Streamplace on " + platform,
@@ -20,7 +20,7 @@ export const handleWebSocketMessages = (
20
20
  state: LivestreamState,
21
21
  messages: any[],
22
22
  ): LivestreamState => {
23
- for (const message of messages) {
23
+ for (let message of messages) {
24
24
  if (PlaceStreamLivestream.isLivestreamView(message)) {
25
25
  const newLivestream = message as LivestreamViewHydrated;
26
26
  const oldLivestream = state.livestream;
@@ -41,11 +41,13 @@ export const handleWebSocketMessages = (
41
41
  livestream: newLivestream,
42
42
  };
43
43
  } else if (PlaceStreamLivestream.isViewerCount(message)) {
44
+ message = message as PlaceStreamLivestream.ViewerCount;
44
45
  state = {
45
46
  ...state,
46
47
  viewers: message.count,
47
48
  };
48
49
  } else if (PlaceStreamChatDefs.isMessageView(message)) {
50
+ message = message as PlaceStreamChatDefs.MessageView;
49
51
  // Explicitly map MessageView to MessageViewHydrated
50
52
  const hydrated: ChatMessageViewHydrated = {
51
53
  uri: message.uri,
@@ -74,6 +76,7 @@ export const handleWebSocketMessages = (
74
76
  const block = message as PlaceStreamDefs.BlockView;
75
77
  state = reduceChat(state, [], [block], []);
76
78
  } else if (PlaceStreamDefs.isRenditions(message)) {
79
+ message = message as PlaceStreamDefs.Renditions;
77
80
  state = {
78
81
  ...state,
79
82
  renditions: message.renditions,
@@ -69,18 +69,6 @@ export interface PlayerState {
69
69
  /** Function to set the ingestStarted timestamp */
70
70
  setIngestStarted: (timestamp: number | null) => void;
71
71
 
72
- /** Player muted state */
73
- muted: boolean;
74
-
75
- /** Function to set the muted state */
76
- setMuted: (isMuted: boolean) => void;
77
-
78
- /** Player volume level (0.0 to 1.0) */
79
- volume: number;
80
-
81
- /** Function to set the volume level */
82
- setVolume: (volume: number) => void;
83
-
84
72
  /** Player fullscreen state */
85
73
  fullscreen: boolean;
86
74
 
@@ -52,14 +52,6 @@ export const makePlayerStore = (id?: string): StoreApi<PlayerState> => {
52
52
  setIngestStarted: (timestamp: number | null) =>
53
53
  set(() => ({ ingestStarted: timestamp })),
54
54
 
55
- muted: false,
56
- setMuted: (isMuted: boolean) =>
57
- set(() => ({ muted: isMuted, muteWasForced: false })),
58
-
59
- volume: 1.0,
60
- setVolume: (volume: number) =>
61
- set(() => ({ volume, muteWasForced: false })),
62
-
63
55
  fullscreen: false,
64
56
  setFullscreen: (isFullscreen: boolean) =>
65
57
  set(() => ({ fullscreen: isFullscreen })),
@@ -0,0 +1,3 @@
1
+ import Storage from "./storage";
2
+
3
+ export default new Storage();
@@ -0,0 +1,38 @@
1
+ type Cont = () => void;
2
+
3
+ export class Lock {
4
+ private readonly queue: Cont[] = [];
5
+ private acquired = false;
6
+
7
+ public async acquire(): Promise<void> {
8
+ if (!this.acquired) {
9
+ this.acquired = true;
10
+ } else {
11
+ return new Promise<void>((resolve, _) => {
12
+ this.queue.push(resolve);
13
+ });
14
+ }
15
+ }
16
+
17
+ public async release(): Promise<void> {
18
+ if (this.queue.length === 0 && this.acquired) {
19
+ this.acquired = false;
20
+ return;
21
+ }
22
+
23
+ const continuation = this.queue.shift();
24
+ return new Promise((res: Cont) => {
25
+ continuation!();
26
+ res();
27
+ });
28
+ }
29
+
30
+ public async critical<T>(task: () => Promise<T>) {
31
+ await this.acquire();
32
+ try {
33
+ return await task();
34
+ } finally {
35
+ await this.release();
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,42 @@
1
+ import Storage from "expo-sqlite/kv-store";
2
+ import { Lock } from "./lock";
3
+ import { AQStorage } from "./storage.shared";
4
+
5
+ // Needed because concurrent calls seem to return with a locked database
6
+ const lock = new Lock();
7
+
8
+ export default class NativeStorage implements AQStorage {
9
+ async getItem(key: string): Promise<string | null> {
10
+ return lock.critical(async () => {
11
+ try {
12
+ const value = await Storage.getItem(key);
13
+ return value ?? null;
14
+ } catch (e) {
15
+ console.error(`error in NativeStorage.getItem: ${e}`);
16
+ throw e;
17
+ }
18
+ });
19
+ }
20
+
21
+ async setItem(key: string, value: string): Promise<void> {
22
+ return lock.critical(async () => {
23
+ try {
24
+ await Storage.setItem(key, value);
25
+ } catch (e) {
26
+ console.error(`error in NativeStorage.setItem: ${e}`);
27
+ throw e;
28
+ }
29
+ });
30
+ }
31
+
32
+ async removeItem(key: string): Promise<void> {
33
+ return lock.critical(async () => {
34
+ try {
35
+ await Storage.removeItem(key);
36
+ } catch (e) {
37
+ console.error(`error in NativeStorage.removeItem: ${e}`);
38
+ throw e;
39
+ }
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,5 @@
1
+ export interface AQStorage {
2
+ getItem: (key: string) => Promise<string | null>;
3
+ setItem: (key: string, value: string) => Promise<void>;
4
+ removeItem: (key: string) => Promise<void>;
5
+ }
@@ -0,0 +1,15 @@
1
+ import { AQStorage } from "./storage.shared";
2
+
3
+ export default class WebStorage implements AQStorage {
4
+ async getItem(key: string): Promise<string | null> {
5
+ return localStorage.getItem(key);
6
+ }
7
+
8
+ async setItem(key: string, value: string): Promise<void> {
9
+ localStorage.setItem(key, value);
10
+ }
11
+
12
+ async removeItem(key: string): Promise<void> {
13
+ localStorage.removeItem(key);
14
+ }
15
+ }
@@ -11,8 +11,9 @@ export function StreamplaceProvider({
11
11
  }: {
12
12
  children: React.ReactNode;
13
13
  url: string;
14
- oauthSession?: SessionManager;
14
+ oauthSession?: SessionManager | null;
15
15
  }) {
16
+ console.log("session in provider is", oauthSession);
16
17
  // todo: handle url changes?
17
18
  const store = useRef(makeStreamplaceStore({ url })).current;
18
19
 
@@ -248,6 +248,7 @@ export function useCreateStreamRecord() {
248
248
  }
249
249
 
250
250
  const record: PlaceStreamLivestream.Record = {
251
+ $type: "place.stream.livestream",
251
252
  title: title,
252
253
  url: finalUrl,
253
254
  createdAt: new Date().toISOString(),
@@ -313,6 +314,7 @@ export function useUpdateStreamRecord(customUrl: string | null = null) {
313
314
  }
314
315
 
315
316
  const record: PlaceStreamLivestream.Record = {
317
+ $type: "place.stream.livestream",
316
318
  title: title,
317
319
  url: finalUrl,
318
320
  createdAt: new Date().toISOString(),