about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/state/invites.tsx5
-rw-r--r--src/state/persisted/index.ts5
-rw-r--r--src/state/persisted/index.web.ts41
-rw-r--r--src/state/persisted/types.ts5
-rw-r--r--src/state/preferences/alt-text-required.tsx9
-rw-r--r--src/state/preferences/autoplay.tsx4
-rw-r--r--src/state/preferences/disable-haptics.tsx4
-rw-r--r--src/state/preferences/external-embeds-prefs.tsx4
-rw-r--r--src/state/preferences/hidden-posts.tsx4
-rw-r--r--src/state/preferences/in-app-browser.tsx4
-rw-r--r--src/state/preferences/kawaii.tsx4
-rw-r--r--src/state/preferences/languages.tsx4
-rw-r--r--src/state/preferences/large-alt-badge.tsx9
-rw-r--r--src/state/preferences/used-starter-packs.tsx9
-rw-r--r--src/state/session/index.tsx4
-rw-r--r--src/state/shell/color-mode.tsx13
-rw-r--r--src/state/shell/onboarding.tsx9
17 files changed, 95 insertions, 42 deletions
diff --git a/src/state/invites.tsx b/src/state/invites.tsx
index 6a0d1b590..0d40caf25 100644
--- a/src/state/invites.tsx
+++ b/src/state/invites.tsx
@@ -1,4 +1,5 @@
 import React from 'react'
+
 import * as persisted from '#/state/persisted'
 
 type StateContext = persisted.Schema['invites']
@@ -35,8 +36,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('invites'))
+    return persisted.onUpdate('invites', nextInvites => {
+      setState(nextInvites)
     })
   }, [setState])
 
diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts
index 95f814850..6f4beae2c 100644
--- a/src/state/persisted/index.ts
+++ b/src/state/persisted/index.ts
@@ -41,7 +41,10 @@ export async function write<K extends keyof Schema>(
 }
 write satisfies PersistedApi['write']
 
-export function onUpdate(_cb: () => void): () => void {
+export function onUpdate<K extends keyof Schema>(
+  _key: K,
+  _cb: (v: Schema[K]) => void,
+): () => void {
   return () => {}
 }
 onUpdate satisfies PersistedApi['onUpdate']
diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts
index d71b59096..7521776bc 100644
--- a/src/state/persisted/index.web.ts
+++ b/src/state/persisted/index.web.ts
@@ -47,18 +47,36 @@ export async function write<K extends keyof Schema>(
     // Don't fire the update listeners yet to avoid a loop.
     // If there was a change, we'll receive the broadcast event soon enough which will do that.
   }
+  try {
+    if (JSON.stringify({v: _state[key]}) === JSON.stringify({v: value})) {
+      // Fast path for updates that are guaranteed to be noops.
+      // This is good mostly because it avoids useless broadcasts to other tabs.
+      return
+    }
+  } catch (e) {
+    // Ignore and go through the normal path.
+  }
   _state = {
     ..._state,
     [key]: value,
   }
   writeToStorage(_state)
-  broadcast.postMessage({event: UPDATE_EVENT})
+  broadcast.postMessage({event: {type: UPDATE_EVENT, key}})
+  broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading
 }
 write satisfies PersistedApi['write']
 
-export function onUpdate(cb: () => void): () => void {
-  _emitter.addListener('update', cb)
-  return () => _emitter.removeListener('update', cb)
+export function onUpdate<K extends keyof Schema>(
+  key: K,
+  cb: (v: Schema[K]) => void,
+): () => void {
+  const listener = () => cb(get(key))
+  _emitter.addListener('update', listener) // Backcompat while upgrading
+  _emitter.addListener('update:' + key, listener)
+  return () => {
+    _emitter.removeListener('update', listener) // Backcompat while upgrading
+    _emitter.removeListener('update:' + key, listener)
+  }
 }
 onUpdate satisfies PersistedApi['onUpdate']
 
@@ -72,12 +90,23 @@ export async function clearStorage() {
 clearStorage satisfies PersistedApi['clearStorage']
 
 async function onBroadcastMessage({data}: MessageEvent) {
-  if (typeof data === 'object' && data.event === UPDATE_EVENT) {
+  if (
+    typeof data === 'object' &&
+    (data.event === UPDATE_EVENT || // Backcompat while upgrading
+      data.event?.type === UPDATE_EVENT)
+  ) {
     // read next state, possibly updated by another tab
     const next = readFromStorage()
+    if (next === _state) {
+      return
+    }
     if (next) {
       _state = next
-      _emitter.emit('update')
+      if (typeof data.event.key === 'string') {
+        _emitter.emit('update:' + data.event.key)
+      } else {
+        _emitter.emit('update') // Backcompat while upgrading
+      }
     } else {
       logger.error(
         `persisted state: handled update update from broadcast channel, but found no data`,
diff --git a/src/state/persisted/types.ts b/src/state/persisted/types.ts
index 95852f796..fd39079bf 100644
--- a/src/state/persisted/types.ts
+++ b/src/state/persisted/types.ts
@@ -4,6 +4,9 @@ export type PersistedApi = {
   init(): Promise<void>
   get<K extends keyof Schema>(key: K): Schema[K]
   write<K extends keyof Schema>(key: K, value: Schema[K]): Promise<void>
-  onUpdate(_cb: () => void): () => void
+  onUpdate<K extends keyof Schema>(
+    key: K,
+    cb: (v: Schema[K]) => void,
+  ): () => void
   clearStorage: () => Promise<void>
 }
diff --git a/src/state/preferences/alt-text-required.tsx b/src/state/preferences/alt-text-required.tsx
index 642e790fb..0ddc173ea 100644
--- a/src/state/preferences/alt-text-required.tsx
+++ b/src/state/preferences/alt-text-required.tsx
@@ -26,9 +26,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('requireAltTextEnabled'))
-    })
+    return persisted.onUpdate(
+      'requireAltTextEnabled',
+      nextRequireAltTextEnabled => {
+        setState(nextRequireAltTextEnabled)
+      },
+    )
   }, [setStateWrapped])
 
   return (
diff --git a/src/state/preferences/autoplay.tsx b/src/state/preferences/autoplay.tsx
index d5aa049f3..141c8161e 100644
--- a/src/state/preferences/autoplay.tsx
+++ b/src/state/preferences/autoplay.tsx
@@ -24,8 +24,8 @@ export function Provider({children}: {children: React.ReactNode}) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(Boolean(persisted.get('disableAutoplay')))
+    return persisted.onUpdate('disableAutoplay', nextDisableAutoplay => {
+      setState(Boolean(nextDisableAutoplay))
     })
   }, [setStateWrapped])
 
diff --git a/src/state/preferences/disable-haptics.tsx b/src/state/preferences/disable-haptics.tsx
index af2c55a18..367d4f7db 100644
--- a/src/state/preferences/disable-haptics.tsx
+++ b/src/state/preferences/disable-haptics.tsx
@@ -24,8 +24,8 @@ export function Provider({children}: {children: React.ReactNode}) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(Boolean(persisted.get('disableHaptics')))
+    return persisted.onUpdate('disableHaptics', nextDisableHaptics => {
+      setState(Boolean(nextDisableHaptics))
     })
   }, [setStateWrapped])
 
diff --git a/src/state/preferences/external-embeds-prefs.tsx b/src/state/preferences/external-embeds-prefs.tsx
index 9ace5d940..04afb89dd 100644
--- a/src/state/preferences/external-embeds-prefs.tsx
+++ b/src/state/preferences/external-embeds-prefs.tsx
@@ -35,8 +35,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('externalEmbeds'))
+    return persisted.onUpdate('externalEmbeds', nextExternalEmbeds => {
+      setState(nextExternalEmbeds)
     })
   }, [setStateWrapped])
 
diff --git a/src/state/preferences/hidden-posts.tsx b/src/state/preferences/hidden-posts.tsx
index 2c6a373e1..510af713d 100644
--- a/src/state/preferences/hidden-posts.tsx
+++ b/src/state/preferences/hidden-posts.tsx
@@ -44,8 +44,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('hiddenPosts'))
+    return persisted.onUpdate('hiddenPosts', nextHiddenPosts => {
+      setState(nextHiddenPosts)
     })
   }, [setStateWrapped])
 
diff --git a/src/state/preferences/in-app-browser.tsx b/src/state/preferences/in-app-browser.tsx
index 73c4bbbe7..76c854105 100644
--- a/src/state/preferences/in-app-browser.tsx
+++ b/src/state/preferences/in-app-browser.tsx
@@ -34,8 +34,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('useInAppBrowser'))
+    return persisted.onUpdate('useInAppBrowser', nextUseInAppBrowser => {
+      setState(nextUseInAppBrowser)
     })
   }, [setStateWrapped])
 
diff --git a/src/state/preferences/kawaii.tsx b/src/state/preferences/kawaii.tsx
index 4aa95ef8b..421689164 100644
--- a/src/state/preferences/kawaii.tsx
+++ b/src/state/preferences/kawaii.tsx
@@ -21,8 +21,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('kawaii'))
+    return persisted.onUpdate('kawaii', nextKawaii => {
+      setState(nextKawaii)
     })
   }, [setStateWrapped])
 
diff --git a/src/state/preferences/languages.tsx b/src/state/preferences/languages.tsx
index b7494c1f9..5093cd725 100644
--- a/src/state/preferences/languages.tsx
+++ b/src/state/preferences/languages.tsx
@@ -43,8 +43,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('languagePrefs'))
+    return persisted.onUpdate('languagePrefs', nextLanguagePrefs => {
+      setState(nextLanguagePrefs)
     })
   }, [setStateWrapped])
 
diff --git a/src/state/preferences/large-alt-badge.tsx b/src/state/preferences/large-alt-badge.tsx
index b3d597c5c..9d2c9fa54 100644
--- a/src/state/preferences/large-alt-badge.tsx
+++ b/src/state/preferences/large-alt-badge.tsx
@@ -26,9 +26,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('largeAltBadgeEnabled'))
-    })
+    return persisted.onUpdate(
+      'largeAltBadgeEnabled',
+      nextLargeAltBadgeEnabled => {
+        setState(nextLargeAltBadgeEnabled)
+      },
+    )
   }, [setStateWrapped])
 
   return (
diff --git a/src/state/preferences/used-starter-packs.tsx b/src/state/preferences/used-starter-packs.tsx
index 8d5d9e828..e4de479d5 100644
--- a/src/state/preferences/used-starter-packs.tsx
+++ b/src/state/preferences/used-starter-packs.tsx
@@ -19,9 +19,12 @@ export function Provider({children}: {children: React.ReactNode}) {
   }
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setState(persisted.get('hasCheckedForStarterPack'))
-    })
+    return persisted.onUpdate(
+      'hasCheckedForStarterPack',
+      nextHasCheckedForStarterPack => {
+        setState(nextHasCheckedForStarterPack)
+      },
+    )
   }, [])
 
   return (
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 3aac19025..09fcf8664 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -185,8 +185,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   }, [state])
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      const synced = persisted.get('session')
+    return persisted.onUpdate('session', nextSession => {
+      const synced = nextSession
       addSessionDebugLog({type: 'persisted:receive', data: synced})
       dispatch({
         type: 'synced-accounts',
diff --git a/src/state/shell/color-mode.tsx b/src/state/shell/color-mode.tsx
index f3339d240..47b936c0b 100644
--- a/src/state/shell/color-mode.tsx
+++ b/src/state/shell/color-mode.tsx
@@ -1,4 +1,5 @@
 import React from 'react'
+
 import * as persisted from '#/state/persisted'
 
 type StateContext = {
@@ -43,10 +44,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      setColorMode(persisted.get('colorMode'))
-      setDarkTheme(persisted.get('darkTheme'))
+    const unsub1 = persisted.onUpdate('darkTheme', nextDarkTheme => {
+      setDarkTheme(nextDarkTheme)
+    })
+    const unsub2 = persisted.onUpdate('colorMode', nextColorMode => {
+      setColorMode(nextColorMode)
     })
+    return () => {
+      unsub1()
+      unsub2()
+    }
   }, [])
 
   return (
diff --git a/src/state/shell/onboarding.tsx b/src/state/shell/onboarding.tsx
index 6a18b461f..d3a8fec46 100644
--- a/src/state/shell/onboarding.tsx
+++ b/src/state/shell/onboarding.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
-import * as persisted from '#/state/persisted'
+
 import {track} from '#/lib/analytics/analytics'
+import * as persisted from '#/state/persisted'
 
 export const OnboardingScreenSteps = {
   Welcome: 'Welcome',
@@ -81,13 +82,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   React.useEffect(() => {
-    return persisted.onUpdate(() => {
-      const next = persisted.get('onboarding').step
+    return persisted.onUpdate('onboarding', nextOnboarding => {
+      const next = nextOnboarding.step
       // TODO we've introduced a footgun
       if (state.step !== next) {
         dispatch({
           type: 'set',
-          step: persisted.get('onboarding').step as OnboardingStep,
+          step: nextOnboarding.step as OnboardingStep,
         })
       }
     })