Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at feat/custom-appview 153 lines 3.8 kB view raw
1import {useCallback, useEffect, useState} from 'react' 2import {MMKV} from '@bsky.app/react-native-mmkv' 3 4import {type Account, type Device} from '#/storage/schema' 5 6export * from '#/storage/schema' 7 8/** 9 * Generic storage class. DO NOT use this directly. Instead, use the exported 10 * storage instances below. 11 */ 12export class Storage<Scopes extends unknown[], Schema> { 13 protected sep = ':' 14 protected store: MMKV 15 16 constructor({id}: {id: string}) { 17 this.store = new MMKV({id}) 18 } 19 20 /** 21 * Store a value in storage based on scopes and/or keys 22 * 23 * `set([key], value)` 24 * `set([scope, key], value)` 25 */ 26 set<Key extends keyof Schema>( 27 scopes: [...Scopes, Key], 28 data: Schema[Key], 29 ): void { 30 // stored as `{ data: <value> }` structure to ease stringification 31 this.store.set(scopes.join(this.sep), JSON.stringify({data})) 32 } 33 34 /** 35 * Get a value from storage based on scopes and/or keys 36 * 37 * `get([key])` 38 * `get([scope, key])` 39 */ 40 get<Key extends keyof Schema>( 41 scopes: [...Scopes, Key], 42 ): Schema[Key] | undefined { 43 const res = this.store.getString(scopes.join(this.sep)) 44 if (!res) return undefined 45 // parsed from storage structure `{ data: <value> }` 46 return JSON.parse(res).data 47 } 48 49 /** 50 * Remove a value from storage based on scopes and/or keys 51 * 52 * `remove([key])` 53 * `remove([scope, key])` 54 */ 55 remove<Key extends keyof Schema>(scopes: [...Scopes, Key]) { 56 this.store.delete(scopes.join(this.sep)) 57 } 58 59 /** 60 * Remove many values from the same storage scope by keys 61 * 62 * `removeMany([], [key])` 63 * `removeMany([scope], [key])` 64 */ 65 removeMany<Key extends keyof Schema>(scopes: [...Scopes], keys: Key[]) { 66 keys.forEach(key => this.remove([...scopes, key])) 67 } 68 69 /** 70 * For debugging purposes 71 */ 72 removeAll() { 73 this.store.clearAll() 74 } 75 76 /** 77 * Fires a callback when the storage associated with a given key changes 78 * 79 * @returns Listener - call `remove()` to stop listening 80 */ 81 addOnValueChangedListener<Key extends keyof Schema>( 82 scopes: [...Scopes, Key], 83 callback: () => void, 84 ) { 85 return this.store.addOnValueChangedListener(key => { 86 if (key === scopes.join(this.sep)) { 87 callback() 88 } 89 }) 90 } 91} 92 93type StorageSchema<T extends Storage<any, any>> = 94 T extends Storage<any, infer U> ? U : never 95type StorageScopes<T extends Storage<any, any>> = 96 T extends Storage<infer S, any> ? S : never 97 98/** 99 * Hook to use a storage instance. Acts like a useState hook, but persists the 100 * value in storage. 101 */ 102export function useStorage< 103 Store extends Storage<any, any>, 104 Key extends keyof StorageSchema<Store>, 105>( 106 storage: Store, 107 scopes: [...StorageScopes<Store>, Key], 108): [ 109 StorageSchema<Store>[Key] | undefined, 110 (data: StorageSchema<Store>[Key]) => void, 111] { 112 type Schema = StorageSchema<Store> 113 const [value, setValue] = useState<Schema[Key] | undefined>(() => 114 storage.get(scopes), 115 ) 116 117 useEffect(() => { 118 const sub = storage.addOnValueChangedListener(scopes, () => { 119 setValue(storage.get(scopes)) 120 }) 121 return () => sub.remove() 122 }, [storage, scopes]) 123 124 const setter = useCallback( 125 (data: Schema[Key]) => { 126 setValue(data) 127 storage.set(scopes, data) 128 }, 129 [storage, scopes], 130 ) 131 132 return [value, setter] as const 133} 134 135/** 136 * Device data that's specific to the device and does not vary based on account 137 * 138 * `device.set([key], true)` 139 */ 140export const device = new Storage<[], Device>({id: 'bsky_device'}) 141 142/** 143 * Account data that's specific to the account on this device 144 */ 145export const account = new Storage<[string], Account>({id: 'bsky_account'}) 146 147if (__DEV__ && typeof window !== 'undefined') { 148 // @ts-expect-error - dev global 149 window.bsky_storage = { 150 device, 151 account, 152 } 153}