diff --git a/js/app/features/base/baseSlice.tsx b/js/app/features/base/baseSlice.tsx index cece36ecc..9fe86a739 100644 --- a/js/app/features/base/baseSlice.tsx +++ b/js/app/features/base/baseSlice.tsx @@ -1,5 +1,5 @@ +import { storage } from "@streamplace/components"; import { createAppSlice } from "../../hooks/createSlice"; -import Storage from "../../storage"; export const STORED_KEY_KEY = "storedKey"; export const DID_KEY = "did"; @@ -26,7 +26,7 @@ export const baseSlice = createAppSlice({ let storedKey: StreamKey | null = null; // Async operation would go here try { - const storedKeyStr = await Storage.getItem(STORED_KEY_KEY); + const storedKeyStr = await storage.getItem(STORED_KEY_KEY); if (storedKeyStr) { storedKey = JSON.parse(storedKeyStr); } diff --git a/js/app/features/base/sidebarSlice.tsx b/js/app/features/base/sidebarSlice.tsx index 16c4a498d..90de866ae 100644 --- a/js/app/features/base/sidebarSlice.tsx +++ b/js/app/features/base/sidebarSlice.tsx @@ -1,7 +1,5 @@ +import { storage } from "@streamplace/components"; import { createAppSlice } from "../../hooks/createSlice"; -import WebStorage from "../../storage/storage"; - -const storage = new WebStorage(); export const SIDEBAR_STORAGE_KEY = "sidebarState"; export interface SidebarState { diff --git a/js/app/features/bluesky/blueskySlice.tsx b/js/app/features/bluesky/blueskySlice.tsx index 34a248dee..7c04d4e72 100644 --- a/js/app/features/bluesky/blueskySlice.tsx +++ b/js/app/features/bluesky/blueskySlice.tsx @@ -9,6 +9,7 @@ import { import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; import { bytesToMultibase, Secp256k1Keypair } from "@atproto/crypto"; import { OAuthSession } from "@streamplace/atproto-oauth-client-react-native"; +import { storage } from "@streamplace/components"; import { DID_KEY, hydrate, STORED_KEY_KEY } from "features/base/baseSlice"; import { openLoginLink } from "features/platform/platformSlice"; import { @@ -16,7 +17,6 @@ import { StreamplaceState, } from "features/streamplace/streamplaceSlice"; import { Platform } from "react-native"; -import Storage from "storage"; import { LivestreamViewHydrated, PlaceStreamChatProfile, @@ -146,10 +146,10 @@ export const blueskySlice = createAppSlice({ typeof err?.message === "string" && err.message.includes("oauth session revoked") ) { - Storage.removeItem("did").catch((e) => { + storage.removeItem("did").catch((e) => { console.error("Error removing did", e); }); - Storage.removeItem(STORED_KEY_KEY).catch((e) => { + storage.removeItem(STORED_KEY_KEY).catch((e) => { console.error("Error removing stored key", e); }); const u = new URL(document.location.href); @@ -170,9 +170,9 @@ export const blueskySlice = createAppSlice({ const client = await createOAuthClient(streamplace.url); const anonPDSAgent = new StreamplaceAgent(streamplace.url); const maybeDIDs = await Promise.all([ - Storage.getItem(DID_KEY), - Storage.getItem("@@atproto/oauth-client-browser(sub)"), - Storage.getItem("@@atproto/oauth-client-react-native:did:(sub)"), + storage.getItem(DID_KEY), + storage.getItem("@@atproto/oauth-client-browser(sub)"), + storage.getItem("@@atproto/oauth-client-react-native:did:(sub)"), ]); const did = maybeDIDs.find((d) => d !== null) || null; let session: OAuthSession | null = null; @@ -197,7 +197,7 @@ export const blueskySlice = createAppSlice({ const { client, session, anonPDSAgent } = action.payload; console.log("loadOAuthClient fulfilled", action.payload); if (session) { - Storage.setItem(DID_KEY, session.did).catch((e) => { + storage.setItem(DID_KEY, session.did).catch((e) => { console.error("Error setting did", e); }); return { @@ -294,8 +294,8 @@ export const blueskySlice = createAppSlice({ logout: create.asyncThunk( async (_, thunkAPI) => { - await Storage.removeItem("did"); - await Storage.removeItem(STORED_KEY_KEY); + await storage.removeItem("did"); + await storage.removeItem(STORED_KEY_KEY); const { bluesky } = thunkAPI.getState() as { bluesky: BlueskyState; }; @@ -425,7 +425,7 @@ export const blueskySlice = createAppSlice({ const client = await createOAuthClient(streamplace.url); try { const ret = await client.callback(params); - await Storage.setItem(DID_KEY, ret.session.did); + await storage.setItem(DID_KEY, ret.session.did); return { session: ret.session as any, client }; } catch (e) { let message = e.message; @@ -653,7 +653,7 @@ export const blueskySlice = createAppSlice({ record, }); if (store) { - await Storage.setItem(STORED_KEY_KEY, JSON.stringify(newKey)); + await storage.setItem(STORED_KEY_KEY, JSON.stringify(newKey)); } return newKey; }, @@ -808,7 +808,7 @@ export const blueskySlice = createAppSlice({ setPDS: create.asyncThunk( async (pds: string, thunkAPI) => { - await Storage.setItem("pdsURL", pds); + await storage.setItem("pdsURL", pds); return pds; }, { diff --git a/js/app/features/streamplace/streamplaceSlice.tsx b/js/app/features/streamplace/streamplaceSlice.tsx index 670de141c..f0f76f004 100644 --- a/js/app/features/streamplace/streamplaceSlice.tsx +++ b/js/app/features/streamplace/streamplaceSlice.tsx @@ -1,8 +1,8 @@ +import { storage } from "@streamplace/components"; import { BlueskyState } from "features/bluesky/blueskyTypes"; import { PlaceStreamLivestream, PlaceStreamSegment } from "streamplace"; import { isWeb } from "tamagui"; import { createAppSlice } from "../../hooks/createSlice"; -import Storage from "../../storage"; let DEFAULT_URL = process.env.EXPO_PUBLIC_STREAMPLACE_URL as string; if (isWeb && process.env.EXPO_PUBLIC_WEB_TRY_LOCAL === "true") { @@ -78,9 +78,9 @@ export const streamplaceSlice = createAppSlice({ initialize: create.asyncThunk( async (_, { getState }) => { let [url, userMutedStr, chatWarningStr] = await Promise.all([ - Storage.getItem(URL_KEY), - Storage.getItem(USER_MUTED_KEY), - Storage.getItem(CHAT_WARNING_KEY), + storage.getItem(URL_KEY), + storage.getItem(USER_MUTED_KEY), + storage.getItem(CHAT_WARNING_KEY), ]); if (!url) { url = DEFAULT_URL; @@ -120,7 +120,7 @@ export const streamplaceSlice = createAppSlice({ setURL: create.reducer((state, action: { payload: string }) => { console.log("setURL", action); - Storage.setItem(URL_KEY, action.payload).catch((err) => { + storage.setItem(URL_KEY, action.payload).catch((err) => { console.error("setURL error", err); }); return { @@ -130,11 +130,11 @@ export const streamplaceSlice = createAppSlice({ }), userMute: create.reducer((state, action: { payload: boolean }) => { - Storage.setItem(USER_MUTED_KEY, JSON.stringify(action.payload)).catch( - (err) => { + storage + .setItem(USER_MUTED_KEY, JSON.stringify(action.payload)) + .catch((err) => { console.error("userMute error", err); - }, - ); + }); return { ...state, userMuted: action.payload, @@ -142,11 +142,11 @@ export const streamplaceSlice = createAppSlice({ }), chatWarn: create.reducer((state, action: { payload: boolean }) => { - Storage.setItem(CHAT_WARNING_KEY, JSON.stringify(action.payload)).catch( - (err) => { + storage + .setItem(CHAT_WARNING_KEY, JSON.stringify(action.payload)) + .catch((err) => { console.error("chatWarn error", err); - }, - ); + }); return { ...state, chatWarned: action.payload, diff --git a/js/app/store/listener.ts b/js/app/store/listener.ts index ee7843ad9..4231ff498 100644 --- a/js/app/store/listener.ts +++ b/js/app/store/listener.ts @@ -1,6 +1,6 @@ import { createListenerMiddleware, isAnyOf } from "@reduxjs/toolkit"; +import { storage } from "@streamplace/components"; import { SIDEBAR_STORAGE_KEY, sidebarSlice } from "features/base/sidebarSlice"; -import storage from "storage"; import { RootState } from "./store"; export const listenerMiddleware = createListenerMiddleware(); diff --git a/js/components/package.json b/js/components/package.json index b048fff37..a827e14f0 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -20,6 +20,7 @@ "tsup": "^8.5.0" }, "dependencies": { + "expo-sqlite": "~15.2.12", "@atproto/api": "^0.16.7", "@atproto/crypto": "^0.4.4", "@emoji-mart/react": "^1.1.1", diff --git a/js/components/src/index.tsx b/js/components/src/index.tsx index f591312f0..14f078853 100644 --- a/js/components/src/index.tsx +++ b/js/components/src/index.tsx @@ -37,3 +37,7 @@ export * from "./components/keep-awake"; // Dashboard components export * as Dashboard from "./components/dashboard"; + +// Storage exports +export { default as storage } from "./storage"; +export type { AQStorage } from "./storage/storage.shared"; diff --git a/js/app/storage/index.tsx b/js/components/src/storage/index.tsx similarity index 100% rename from js/app/storage/index.tsx rename to js/components/src/storage/index.tsx diff --git a/js/app/storage/lock.tsx b/js/components/src/storage/lock.tsx similarity index 100% rename from js/app/storage/lock.tsx rename to js/components/src/storage/lock.tsx diff --git a/js/app/storage/storage.native.tsx b/js/components/src/storage/storage.native.tsx similarity index 100% rename from js/app/storage/storage.native.tsx rename to js/components/src/storage/storage.native.tsx diff --git a/js/app/storage/storage.shared.tsx b/js/components/src/storage/storage.shared.tsx similarity index 100% rename from js/app/storage/storage.shared.tsx rename to js/components/src/storage/storage.shared.tsx diff --git a/js/app/storage/storage.tsx b/js/components/src/storage/storage.tsx similarity index 100% rename from js/app/storage/storage.tsx rename to js/components/src/storage/storage.tsx diff --git a/js/components/src/streamplace-store/streamplace-store.tsx b/js/components/src/streamplace-store/streamplace-store.tsx index 935eb6680..bfc80d362 100644 --- a/js/components/src/streamplace-store/streamplace-store.tsx +++ b/js/components/src/streamplace-store/streamplace-store.tsx @@ -2,6 +2,7 @@ import { SessionManager } from "@atproto/api/dist/session-manager"; import { useContext } from "react"; import { PlaceStreamChatProfile, PlaceStreamLivestream } from "streamplace"; import { createStore, StoreApi, useStore } from "zustand"; +import storage from "../storage"; import { StreamplaceContext } from "../streamplace-provider/context"; // there are three categories of XRPC that we need to handle: @@ -46,47 +47,10 @@ export const makeStreamplaceStore = ({ }: { url: string; }): StoreApi => { - // get local storage if exists - const getStorage = () => { - if (typeof window !== "undefined" && window.localStorage) { - return window.localStorage; - } - return null; - }; - - const storage = getStorage(); const VOLUME_STORAGE_KEY = "globalVolume"; const MUTED_STORAGE_KEY = "globalMuted"; - // Load initial volume state from storage - let initialVolume = 1.0; - let initialMuted = false; - - if (storage) { - try { - const storedVolume = storage.getItem(VOLUME_STORAGE_KEY); - const storedMuted = storage.getItem(MUTED_STORAGE_KEY); - - if (storedVolume) { - const parsedVolume = parseFloat(storedVolume); - if ( - Number.isFinite(parsedVolume) && - parsedVolume >= 0 && - parsedVolume <= 1 - ) { - initialVolume = parsedVolume; - } - } - - if (storedMuted) { - initialMuted = storedMuted === "true"; - } - } catch (e) { - console.warn("Failed to load volume settings from storage:", e); - } - } - - return createStore()((set) => ({ + const store = createStore()((set) => ({ url, liveUsers: null, setLiveUsers: (opts: { @@ -106,9 +70,9 @@ export const makeStreamplaceStore = ({ handle: null, chatProfile: null, - // Volume state - volume: initialVolume, - muted: initialMuted, + // Volume state - start with defaults + volume: 1.0, + muted: false, setVolume: (volume: number) => { // Ensure the value is finite and within bounds @@ -123,17 +87,55 @@ export const makeStreamplaceStore = ({ // Auto-unmute if volume > 0 if (clampedVolume > 0) { set({ muted: false }); - storage?.setItem(MUTED_STORAGE_KEY, "false"); + storage.setItem(MUTED_STORAGE_KEY, "false").catch(console.error); } - storage?.setItem(VOLUME_STORAGE_KEY, clampedVolume.toString()); + storage + .setItem(VOLUME_STORAGE_KEY, clampedVolume.toString()) + .catch(console.error); }, setMuted: (muted: boolean) => { set({ muted }); - storage?.setItem(MUTED_STORAGE_KEY, muted.toString()); + storage.setItem(MUTED_STORAGE_KEY, muted.toString()).catch(console.error); }, })); + + // Load initial volume state from storage asynchronously + (async () => { + try { + const storedVolume = await storage.getItem(VOLUME_STORAGE_KEY); + const storedMuted = await storage.getItem(MUTED_STORAGE_KEY); + + let initialVolume = 1.0; + let initialMuted = false; + + if (storedVolume) { + const parsedVolume = parseFloat(storedVolume); + if ( + Number.isFinite(parsedVolume) && + parsedVolume >= 0 && + parsedVolume <= 1 + ) { + initialVolume = parsedVolume; + } + } + + if (storedMuted) { + initialMuted = storedMuted === "true"; + } + + // Update the store with loaded values + store.setState({ + volume: initialVolume, + muted: initialMuted, + }); + } catch (e) { + console.warn("Failed to load volume settings from storage:", e); + } + })(); + + return store; }; export function getStreamplaceStoreFromContext(): StreamplaceStore { diff --git a/js/components/tsconfig.json b/js/components/tsconfig.json index d913dccdf..64cef3fbf 100644 --- a/js/components/tsconfig.json +++ b/js/components/tsconfig.json @@ -4,7 +4,8 @@ "rootDir": "./src", "outDir": "./dist", "jsx": "react-jsx", - "module": "commonjs" + "module": "commonjs", + "skipLibCheck": true }, "include": ["./src"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3447e9e56..ad11e7250 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -410,6 +410,9 @@ importers: expo-keep-awake: specifier: ^14.0.0 version: 14.1.4(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) + expo-sqlite: + specifier: ~15.2.12 + version: 15.2.12(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) expo-video: specifier: ^2.0.0 version: 2.2.1(expo@53.0.11(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)))(bufferutil@4.0.8)(react-native-webview@13.15.0(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)(utf-8-validate@5.0.10))(react-native@0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0)