-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7b2a7db
commit 2a11929
Showing
9 changed files
with
380 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default class BroadcastChannel { | ||
constructor(public name: string) {} | ||
postMessage(_data: any) {} | ||
close() {} | ||
onmessage: (event: MessageEvent) => void = () => {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default BroadcastChannel |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import React from 'react' | ||
|
||
import {logger} from '#/logger' | ||
import {schema, Schema} from '#/state/persisted/schema' | ||
import {migrate} from '#/state/persisted/legacy' | ||
import * as store from '#/state/persisted/store' | ||
import BroadcastChannel from '#/state/persisted/broadcast' | ||
|
||
export type {Schema} from '#/state/persisted/schema' | ||
export {schema} from '#/state/persisted/schema' | ||
|
||
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL') | ||
const UPDATE_EVENT = 'BSKY_UPDATE' | ||
|
||
/** | ||
* Initializes and returns persisted data state, so that it can be passed to | ||
* the Provider. | ||
*/ | ||
export async function init() { | ||
logger.debug('persisted state: initializing') | ||
|
||
try { | ||
await migrate() // migrate old store | ||
const stored = await store.read() // check for new store | ||
if (!stored) await store.write(schema) // opt: init new store | ||
return stored || schema // return new store | ||
} catch (e) { | ||
logger.error('persisted state: failed to load root state from storage', { | ||
error: e, | ||
}) | ||
// AsyncStorage failured, but we can still continue in memory | ||
return schema | ||
} | ||
} | ||
|
||
const context = React.createContext< | ||
Schema & { | ||
setColorMode: (colorMode: Schema['colorMode']) => void | ||
} | ||
>({ | ||
...schema, | ||
setColorMode: () => {}, | ||
}) | ||
|
||
export function Provider({ | ||
data, | ||
children, | ||
}: React.PropsWithChildren<{data: Schema}>) { | ||
const [state, setState] = React.useState<Schema>(data) | ||
|
||
React.useEffect(() => { | ||
broadcast.onmessage = async ({data}) => { | ||
// validate event | ||
if (typeof data === 'object' && data.event === UPDATE_EVENT) { | ||
// read next state, possibly updated by another tab | ||
const next = await store.read() | ||
|
||
if (next) { | ||
logger.debug( | ||
`persisted state: handling update from broadcast channel`, | ||
) | ||
setState(next) | ||
} else { | ||
logger.error( | ||
`persisted state: received update but no data found in storage`, | ||
) | ||
} | ||
} | ||
} | ||
|
||
return () => { | ||
broadcast.close() | ||
} | ||
}, [setState]) | ||
|
||
/** | ||
* Commit a complete state object to storage, and broadcast to other tabs | ||
* that an update has occurred. | ||
*/ | ||
const _write = React.useCallback(async (next: Schema) => { | ||
await store.write(next) | ||
// must happen on next tick, otherwise the tab will read stale storage data | ||
setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0) | ||
logger.debug(`persisted state: wrote root state to storage`) | ||
}, []) | ||
|
||
/* | ||
* Other state methods go below | ||
*/ | ||
|
||
const setColorMode = React.useCallback( | ||
(colorMode: Schema['colorMode']) => { | ||
setState(s => { | ||
const next = {...s, colorMode} | ||
_write(next) | ||
return next | ||
}) | ||
}, | ||
[_write, setState], | ||
) | ||
|
||
const ctx = { | ||
...state, | ||
setColorMode, | ||
} | ||
|
||
return <context.Provider value={ctx}>{children}</context.Provider> | ||
} | ||
|
||
export function usePersisted() { | ||
return React.useContext(context) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import AsyncStorage from '@react-native-async-storage/async-storage' | ||
|
||
import {logger} from '#/logger' | ||
import {schema, Schema} from '#/state/persisted/schema' | ||
import {write, read} from '#/state/persisted/store' | ||
|
||
/** | ||
* The shape of the serialized data from our legacy Mobx store. | ||
*/ | ||
type LegacySchema = { | ||
shell: { | ||
colorMode: 'system' | 'light' | 'dark' | ||
} | ||
session: { | ||
data: { | ||
service: string | ||
did: `did:plc:${string}` | ||
} | ||
accounts: { | ||
service: string | ||
did: `did:plc:${string}` | ||
refreshJwt: string | ||
accessJwt: string | ||
handle: string | ||
email: string | ||
displayName: string | ||
aviUrl: string | ||
emailConfirmed: boolean | ||
}[] | ||
} | ||
me: { | ||
did: `did:plc:${string}` | ||
handle: string | ||
displayName: string | ||
description: string | ||
avatar: string | ||
} | ||
onboarding: { | ||
step: string | ||
} | ||
preferences: { | ||
primaryLanguage: string | ||
contentLanguages: string[] | ||
postLanguage: string | ||
postLanguageHistory: string[] | ||
contentLabels: { | ||
nsfw: string | ||
nudity: string | ||
suggestive: string | ||
gore: string | ||
hate: string | ||
spam: string | ||
impersonation: string | ||
} | ||
savedFeeds: string[] | ||
pinnedFeeds: string[] | ||
requireAltTextEnabled: boolean | ||
} | ||
invitedUsers: { | ||
seenDids: string[] | ||
copiedInvites: string[] | ||
} | ||
mutedThreads: {uris: string[]} | ||
reminders: {lastEmailConfirm: string} | ||
} | ||
|
||
const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root' | ||
|
||
export function transform(legacy: LegacySchema): Schema { | ||
return { | ||
colorMode: legacy.shell.colorMode || schema.colorMode, | ||
accounts: legacy.session.accounts || schema.accounts, | ||
currentAccount: | ||
legacy.session.accounts.find(a => a.did === legacy.session.data.did) || | ||
schema.currentAccount, | ||
lastEmailConfirmReminder: | ||
legacy.reminders.lastEmailConfirm || schema.lastEmailConfirmReminder, | ||
primaryLanguage: | ||
legacy.preferences.primaryLanguage || schema.primaryLanguage, | ||
contentLanguages: | ||
legacy.preferences.contentLanguages || schema.contentLanguages, | ||
postLanguage: legacy.preferences.postLanguage || schema.postLanguage, | ||
postLanguageHistory: | ||
legacy.preferences.postLanguageHistory || schema.postLanguageHistory, | ||
requireAltTextEnabled: | ||
legacy.preferences.requireAltTextEnabled || schema.requireAltTextEnabled, | ||
mutedThreads: legacy.mutedThreads.uris || schema.mutedThreads, | ||
invitedUsers: { | ||
seenDids: legacy.invitedUsers.seenDids || schema.invitedUsers.seenDids, | ||
copiedInvites: | ||
legacy.invitedUsers.copiedInvites || schema.invitedUsers.copiedInvites, | ||
}, | ||
onboarding: { | ||
step: legacy.onboarding.step || schema.onboarding.step, | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* Migrates legacy persisted state to new store if new store doesn't exist in | ||
* local storage AND old storage exists. | ||
*/ | ||
export async function migrate() { | ||
logger.debug('persisted state: migrate') | ||
|
||
try { | ||
const rawLegacyData = await AsyncStorage.getItem( | ||
DEPRECATED_ROOT_STATE_STORAGE_KEY, | ||
) | ||
const alreadyMigrated = Boolean(await read()) | ||
|
||
if (!alreadyMigrated && rawLegacyData) { | ||
logger.debug('persisted state: migrating legacy store') | ||
const legacyData = JSON.parse(rawLegacyData) | ||
const newData = transform(legacyData) | ||
await write(newData) | ||
logger.debug('persisted state: migrated legacy store') | ||
} | ||
} catch (e) { | ||
logger.error('persisted state: error migrating legacy store', {error: e}) | ||
} | ||
} |
Oops, something went wrong.