mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at responsive-updates 262 lines 7.9 kB view raw
1/** 2 * The root store is the base of all modeled state. 3 */ 4 5import {makeAutoObservable} from 'mobx' 6import {BskyAgent} from '@atproto/api' 7import {createContext, useContext} from 'react' 8import {DeviceEventEmitter, EmitterSubscription} from 'react-native' 9import {z} from 'zod' 10import {isObj, hasProp} from 'lib/type-guards' 11import {LogModel} from './log' 12import {SessionModel} from './session' 13import {ShellUiModel} from './ui/shell' 14import {HandleResolutionsCache} from './cache/handle-resolutions' 15import {ProfilesCache} from './cache/profiles-view' 16import {PostsCache} from './cache/posts' 17import {LinkMetasCache} from './cache/link-metas' 18import {MeModel} from './me' 19import {InvitedUsers} from './invited-users' 20import {PreferencesModel} from './ui/preferences' 21import {resetToTab} from '../../Navigation' 22import {ImageSizesCache} from './cache/image-sizes' 23import {MutedThreads} from './muted-threads' 24import {reset as resetNavigation} from '../../Navigation' 25 26// TEMPORARY (APP-700) 27// remove after backend testing finishes 28// -prf 29import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header' 30import {OnboardingModel} from './discovery/onboarding' 31 32export const appInfo = z.object({ 33 build: z.string(), 34 name: z.string(), 35 namespace: z.string(), 36 version: z.string(), 37}) 38export type AppInfo = z.infer<typeof appInfo> 39 40export class RootStoreModel { 41 agent: BskyAgent 42 appInfo?: AppInfo 43 log = new LogModel() 44 session = new SessionModel(this) 45 shell = new ShellUiModel(this) 46 preferences = new PreferencesModel(this) 47 me = new MeModel(this) 48 onboarding = new OnboardingModel(this) 49 invitedUsers = new InvitedUsers(this) 50 handleResolutions = new HandleResolutionsCache() 51 profiles = new ProfilesCache(this) 52 posts = new PostsCache(this) 53 linkMetas = new LinkMetasCache(this) 54 imageSizes = new ImageSizesCache() 55 mutedThreads = new MutedThreads() 56 57 constructor(agent: BskyAgent) { 58 this.agent = agent 59 makeAutoObservable(this, { 60 agent: false, 61 serialize: false, 62 hydrate: false, 63 }) 64 } 65 66 setAppInfo(info: AppInfo) { 67 this.appInfo = info 68 } 69 70 serialize(): unknown { 71 return { 72 appInfo: this.appInfo, 73 session: this.session.serialize(), 74 me: this.me.serialize(), 75 onboarding: this.onboarding.serialize(), 76 shell: this.shell.serialize(), 77 preferences: this.preferences.serialize(), 78 invitedUsers: this.invitedUsers.serialize(), 79 mutedThreads: this.mutedThreads.serialize(), 80 } 81 } 82 83 hydrate(v: unknown) { 84 if (isObj(v)) { 85 if (hasProp(v, 'appInfo')) { 86 const appInfoParsed = appInfo.safeParse(v.appInfo) 87 if (appInfoParsed.success) { 88 this.setAppInfo(appInfoParsed.data) 89 } 90 } 91 if (hasProp(v, 'me')) { 92 this.me.hydrate(v.me) 93 } 94 if (hasProp(v, 'onboarding')) { 95 this.onboarding.hydrate(v.onboarding) 96 } 97 if (hasProp(v, 'session')) { 98 this.session.hydrate(v.session) 99 } 100 if (hasProp(v, 'shell')) { 101 this.shell.hydrate(v.shell) 102 } 103 if (hasProp(v, 'preferences')) { 104 this.preferences.hydrate(v.preferences) 105 } 106 if (hasProp(v, 'invitedUsers')) { 107 this.invitedUsers.hydrate(v.invitedUsers) 108 } 109 if (hasProp(v, 'mutedThreads')) { 110 this.mutedThreads.hydrate(v.mutedThreads) 111 } 112 } 113 } 114 115 /** 116 * Called during init to resume any stored session. 117 */ 118 async attemptSessionResumption() { 119 this.log.debug('RootStoreModel:attemptSessionResumption') 120 try { 121 await this.session.attemptSessionResumption() 122 this.log.debug('Session initialized', { 123 hasSession: this.session.hasSession, 124 }) 125 this.updateSessionState() 126 } catch (e: any) { 127 this.log.warn('Failed to initialize session', e) 128 } 129 } 130 131 /** 132 * Called by the session model. Refreshes session-oriented state. 133 */ 134 async handleSessionChange( 135 agent: BskyAgent, 136 {hadSession}: {hadSession: boolean}, 137 ) { 138 this.log.debug('RootStoreModel:handleSessionChange') 139 this.agent = agent 140 applyDebugHeader(this.agent) 141 this.me.clear() 142 /* dont await */ this.preferences.sync() 143 await this.me.load() 144 if (!hadSession) { 145 await resetNavigation() 146 } 147 this.emitSessionReady() 148 } 149 150 /** 151 * Called by the session model. Handles session drops by informing the user. 152 */ 153 async handleSessionDrop() { 154 this.log.debug('RootStoreModel:handleSessionDrop') 155 resetToTab('HomeTab') 156 this.me.clear() 157 this.emitSessionDropped() 158 } 159 160 /** 161 * Clears all session-oriented state. 162 */ 163 clearAllSessionState() { 164 this.log.debug('RootStoreModel:clearAllSessionState') 165 this.session.clear() 166 resetToTab('HomeTab') 167 this.me.clear() 168 } 169 170 /** 171 * Periodic poll for new session state. 172 */ 173 async updateSessionState() { 174 if (!this.session.hasSession) { 175 return 176 } 177 try { 178 await this.me.updateIfNeeded() 179 await this.preferences.sync() 180 } catch (e: any) { 181 this.log.error('Failed to fetch latest state', e) 182 } 183 } 184 185 // global event bus 186 // = 187 // - some events need to be passed around between views and models 188 // in order to keep state in sync; these methods are for that 189 190 // a post was deleted by the local user 191 onPostDeleted(handler: (uri: string) => void): EmitterSubscription { 192 return DeviceEventEmitter.addListener('post-deleted', handler) 193 } 194 emitPostDeleted(uri: string) { 195 DeviceEventEmitter.emit('post-deleted', uri) 196 } 197 198 // a list was deleted by the local user 199 onListDeleted(handler: (uri: string) => void): EmitterSubscription { 200 return DeviceEventEmitter.addListener('list-deleted', handler) 201 } 202 emitListDeleted(uri: string) { 203 DeviceEventEmitter.emit('list-deleted', uri) 204 } 205 206 // the session has started and been fully hydrated 207 onSessionLoaded(handler: () => void): EmitterSubscription { 208 return DeviceEventEmitter.addListener('session-loaded', handler) 209 } 210 emitSessionLoaded() { 211 DeviceEventEmitter.emit('session-loaded') 212 } 213 214 // the session has completed all setup; good for post-initialization behaviors like triggering modals 215 onSessionReady(handler: () => void): EmitterSubscription { 216 return DeviceEventEmitter.addListener('session-ready', handler) 217 } 218 emitSessionReady() { 219 DeviceEventEmitter.emit('session-ready') 220 } 221 222 // the session was dropped due to bad/expired refresh tokens 223 onSessionDropped(handler: () => void): EmitterSubscription { 224 return DeviceEventEmitter.addListener('session-dropped', handler) 225 } 226 emitSessionDropped() { 227 DeviceEventEmitter.emit('session-dropped') 228 } 229 230 // the current screen has changed 231 // TODO is this still needed? 232 onNavigation(handler: () => void): EmitterSubscription { 233 return DeviceEventEmitter.addListener('navigation', handler) 234 } 235 emitNavigation() { 236 DeviceEventEmitter.emit('navigation') 237 } 238 239 // a "soft reset" typically means scrolling to top and loading latest 240 // but it can depend on the screen 241 onScreenSoftReset(handler: () => void): EmitterSubscription { 242 return DeviceEventEmitter.addListener('screen-soft-reset', handler) 243 } 244 emitScreenSoftReset() { 245 DeviceEventEmitter.emit('screen-soft-reset') 246 } 247 248 // the unread notifications count has changed 249 onUnreadNotifications(handler: (count: number) => void): EmitterSubscription { 250 return DeviceEventEmitter.addListener('unread-notifications', handler) 251 } 252 emitUnreadNotifications(count: number) { 253 DeviceEventEmitter.emit('unread-notifications', count) 254 } 255} 256 257const throwawayInst = new RootStoreModel( 258 new BskyAgent({service: 'http://localhost'}), 259) // this will be replaced by the loader, we just need to supply a value at init 260const RootStoreContext = createContext<RootStoreModel>(throwawayInst) 261export const RootStoreProvider = RootStoreContext.Provider 262export const useStores = () => useContext(RootStoreContext)