about summary refs log tree commit diff
path: root/src/state/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models')
-rw-r--r--src/state/models/cache/handle-resolutions.ts5
-rw-r--r--src/state/models/cache/image-sizes.ts38
-rw-r--r--src/state/models/cache/link-metas.ts44
-rw-r--r--src/state/models/cache/posts.ts70
-rw-r--r--src/state/models/cache/profiles-view.ts50
-rw-r--r--src/state/models/me.ts115
-rw-r--r--src/state/models/root-store.ts207
-rw-r--r--src/state/models/session.ts43
-rw-r--r--src/state/models/ui/shell.ts32
9 files changed, 0 insertions, 604 deletions
diff --git a/src/state/models/cache/handle-resolutions.ts b/src/state/models/cache/handle-resolutions.ts
deleted file mode 100644
index 2e2b69661..000000000
--- a/src/state/models/cache/handle-resolutions.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {LRUMap} from 'lru_map'
-
-export class HandleResolutionsCache {
-  cache: LRUMap<string, string> = new LRUMap(500)
-}
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
deleted file mode 100644
index c30a68f4d..000000000
--- a/src/state/models/cache/image-sizes.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import {Image} from 'react-native'
-import type {Dimensions} from 'lib/media/types'
-
-export class ImageSizesCache {
-  sizes: Map<string, Dimensions> = new Map()
-  activeRequests: Map<string, Promise<Dimensions>> = new Map()
-
-  constructor() {}
-
-  get(uri: string): Dimensions | undefined {
-    return this.sizes.get(uri)
-  }
-
-  async fetch(uri: string): Promise<Dimensions> {
-    const Dimensions = this.sizes.get(uri)
-    if (Dimensions) {
-      return Dimensions
-    }
-
-    const prom =
-      this.activeRequests.get(uri) ||
-      new Promise<Dimensions>(resolve => {
-        Image.getSize(
-          uri,
-          (width: number, height: number) => resolve({width, height}),
-          (err: any) => {
-            console.error('Failed to fetch image dimensions for', uri, err)
-            resolve({width: 0, height: 0})
-          },
-        )
-      })
-    this.activeRequests.set(uri, prom)
-    const res = await prom
-    this.activeRequests.delete(uri)
-    this.sizes.set(uri, res)
-    return res
-  }
-}
diff --git a/src/state/models/cache/link-metas.ts b/src/state/models/cache/link-metas.ts
deleted file mode 100644
index 607968c80..000000000
--- a/src/state/models/cache/link-metas.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {LRUMap} from 'lru_map'
-import {RootStoreModel} from '../root-store'
-import {LinkMeta, getLinkMeta} from 'lib/link-meta/link-meta'
-
-type CacheValue = Promise<LinkMeta> | LinkMeta
-export class LinkMetasCache {
-  cache: LRUMap<string, CacheValue> = new LRUMap(100)
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-        cache: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  // public api
-  // =
-
-  async getLinkMeta(url: string) {
-    const cached = this.cache.get(url)
-    if (cached) {
-      try {
-        return await cached
-      } catch (e) {
-        // ignore, we'll try again
-      }
-    }
-    try {
-      const promise = getLinkMeta(this.rootStore, url)
-      this.cache.set(url, promise)
-      const res = await promise
-      this.cache.set(url, res)
-      return res
-    } catch (e) {
-      this.cache.delete(url)
-      throw e
-    }
-  }
-}
diff --git a/src/state/models/cache/posts.ts b/src/state/models/cache/posts.ts
deleted file mode 100644
index d3632f436..000000000
--- a/src/state/models/cache/posts.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import {LRUMap} from 'lru_map'
-import {RootStoreModel} from '../root-store'
-import {
-  AppBskyFeedDefs,
-  AppBskyEmbedRecord,
-  AppBskyEmbedRecordWithMedia,
-  AppBskyFeedPost,
-} from '@atproto/api'
-
-type PostView = AppBskyFeedDefs.PostView
-
-export class PostsCache {
-  cache: LRUMap<string, PostView> = new LRUMap(500)
-
-  constructor(public rootStore: RootStoreModel) {}
-
-  set(uri: string, postView: PostView) {
-    this.cache.set(uri, postView)
-    if (postView.author.handle) {
-      this.rootStore.handleResolutions.cache.set(
-        postView.author.handle,
-        postView.author.did,
-      )
-    }
-  }
-
-  fromFeedItem(feedItem: AppBskyFeedDefs.FeedViewPost) {
-    this.set(feedItem.post.uri, feedItem.post)
-    if (
-      feedItem.reply?.parent &&
-      AppBskyFeedDefs.isPostView(feedItem.reply?.parent)
-    ) {
-      this.set(feedItem.reply.parent.uri, feedItem.reply.parent)
-    }
-    const embed = feedItem.post.embed
-    if (
-      AppBskyEmbedRecord.isView(embed) &&
-      AppBskyEmbedRecord.isViewRecord(embed.record) &&
-      AppBskyFeedPost.isRecord(embed.record.value) &&
-      AppBskyFeedPost.validateRecord(embed.record.value).success
-    ) {
-      this.set(embed.record.uri, embedViewToPostView(embed.record))
-    }
-    if (
-      AppBskyEmbedRecordWithMedia.isView(embed) &&
-      AppBskyEmbedRecord.isViewRecord(embed.record?.record) &&
-      AppBskyFeedPost.isRecord(embed.record.record.value) &&
-      AppBskyFeedPost.validateRecord(embed.record.record.value).success
-    ) {
-      this.set(
-        embed.record.record.uri,
-        embedViewToPostView(embed.record.record),
-      )
-    }
-  }
-}
-
-function embedViewToPostView(
-  embedView: AppBskyEmbedRecord.ViewRecord,
-): PostView {
-  return {
-    $type: 'app.bsky.feed.post#view',
-    uri: embedView.uri,
-    cid: embedView.cid,
-    author: embedView.author,
-    record: embedView.value,
-    indexedAt: embedView.indexedAt,
-    labels: embedView.labels,
-  }
-}
diff --git a/src/state/models/cache/profiles-view.ts b/src/state/models/cache/profiles-view.ts
deleted file mode 100644
index e5a9be587..000000000
--- a/src/state/models/cache/profiles-view.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {LRUMap} from 'lru_map'
-import {RootStoreModel} from '../root-store'
-import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
-
-type CacheValue = Promise<GetProfile.Response> | GetProfile.Response
-export class ProfilesCache {
-  cache: LRUMap<string, CacheValue> = new LRUMap(100)
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-        cache: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  // public api
-  // =
-
-  async getProfile(did: string) {
-    const cached = this.cache.get(did)
-    if (cached) {
-      try {
-        return await cached
-      } catch (e) {
-        // ignore, we'll try again
-      }
-    }
-    try {
-      const promise = this.rootStore.agent.getProfile({
-        actor: did,
-      })
-      this.cache.set(did, promise)
-      const res = await promise
-      this.cache.set(did, res)
-      return res
-    } catch (e) {
-      this.cache.delete(did)
-      throw e
-    }
-  }
-
-  overwrite(did: string, res: GetProfile.Response) {
-    this.cache.set(did, res)
-  }
-}
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
deleted file mode 100644
index 1e802fb78..000000000
--- a/src/state/models/me.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {RootStoreModel} from './root-store'
-import {isObj, hasProp} from 'lib/type-guards'
-import {logger} from '#/logger'
-
-const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min
-
-export class MeModel {
-  did: string = ''
-  handle: string = ''
-  displayName: string = ''
-  description: string = ''
-  avatar: string = ''
-  followsCount: number | undefined
-  followersCount: number | undefined
-  lastProfileStateUpdate = Date.now()
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {rootStore: false, serialize: false, hydrate: false},
-      {autoBind: true},
-    )
-  }
-
-  clear() {
-    this.rootStore.profiles.cache.clear()
-    this.rootStore.posts.cache.clear()
-    this.did = ''
-    this.handle = ''
-    this.displayName = ''
-    this.description = ''
-    this.avatar = ''
-  }
-
-  serialize(): unknown {
-    return {
-      did: this.did,
-      handle: this.handle,
-      displayName: this.displayName,
-      description: this.description,
-      avatar: this.avatar,
-    }
-  }
-
-  hydrate(v: unknown) {
-    if (isObj(v)) {
-      let did, handle, displayName, description, avatar
-      if (hasProp(v, 'did') && typeof v.did === 'string') {
-        did = v.did
-      }
-      if (hasProp(v, 'handle') && typeof v.handle === 'string') {
-        handle = v.handle
-      }
-      if (hasProp(v, 'displayName') && typeof v.displayName === 'string') {
-        displayName = v.displayName
-      }
-      if (hasProp(v, 'description') && typeof v.description === 'string') {
-        description = v.description
-      }
-      if (hasProp(v, 'avatar') && typeof v.avatar === 'string') {
-        avatar = v.avatar
-      }
-      if (did && handle) {
-        this.did = did
-        this.handle = handle
-        this.displayName = displayName || ''
-        this.description = description || ''
-        this.avatar = avatar || ''
-      }
-    }
-  }
-
-  async load() {
-    const sess = this.rootStore.session
-    logger.debug('MeModel:load', {hasSession: sess.hasSession})
-    if (sess.hasSession) {
-      this.did = sess.currentSession?.did || ''
-      await this.fetchProfile()
-      this.rootStore.emitSessionLoaded()
-    } else {
-      this.clear()
-    }
-  }
-
-  async updateIfNeeded() {
-    if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) {
-      logger.debug('Updating me profile information')
-      this.lastProfileStateUpdate = Date.now()
-      await this.fetchProfile()
-    }
-  }
-
-  async fetchProfile() {
-    const profile = await this.rootStore.agent.getProfile({
-      actor: this.did,
-    })
-    runInAction(() => {
-      if (profile?.data) {
-        this.displayName = profile.data.displayName || ''
-        this.description = profile.data.description || ''
-        this.avatar = profile.data.avatar || ''
-        this.handle = profile.data.handle || ''
-        this.followsCount = profile.data.followsCount
-        this.followersCount = profile.data.followersCount
-      } else {
-        this.displayName = ''
-        this.description = ''
-        this.avatar = ''
-        this.followsCount = profile.data.followsCount
-        this.followersCount = undefined
-      }
-    })
-  }
-}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
deleted file mode 100644
index 6d7c2c12e..000000000
--- a/src/state/models/root-store.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * The root store is the base of all modeled state.
- */
-
-import {makeAutoObservable} from 'mobx'
-import {BskyAgent} from '@atproto/api'
-import {createContext, useContext} from 'react'
-import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
-import {z} from 'zod'
-import {isObj, hasProp} from 'lib/type-guards'
-import {SessionModel} from './session'
-import {ShellUiModel} from './ui/shell'
-import {HandleResolutionsCache} from './cache/handle-resolutions'
-import {ProfilesCache} from './cache/profiles-view'
-import {PostsCache} from './cache/posts'
-import {LinkMetasCache} from './cache/link-metas'
-import {MeModel} from './me'
-import {resetToTab} from '../../Navigation'
-import {ImageSizesCache} from './cache/image-sizes'
-import {reset as resetNavigation} from '../../Navigation'
-import {logger} from '#/logger'
-
-// TEMPORARY (APP-700)
-// remove after backend testing finishes
-// -prf
-import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
-
-export const appInfo = z.object({
-  build: z.string(),
-  name: z.string(),
-  namespace: z.string(),
-  version: z.string(),
-})
-export type AppInfo = z.infer<typeof appInfo>
-
-export class RootStoreModel {
-  agent: BskyAgent
-  appInfo?: AppInfo
-  session = new SessionModel(this)
-  shell = new ShellUiModel(this)
-  me = new MeModel(this)
-  handleResolutions = new HandleResolutionsCache()
-  profiles = new ProfilesCache(this)
-  posts = new PostsCache(this)
-  linkMetas = new LinkMetasCache(this)
-  imageSizes = new ImageSizesCache()
-
-  constructor(agent: BskyAgent) {
-    this.agent = agent
-    makeAutoObservable(this, {
-      agent: false,
-      serialize: false,
-      hydrate: false,
-    })
-  }
-
-  setAppInfo(info: AppInfo) {
-    this.appInfo = info
-  }
-
-  serialize(): unknown {
-    return {
-      appInfo: this.appInfo,
-      me: this.me.serialize(),
-    }
-  }
-
-  hydrate(v: unknown) {
-    if (isObj(v)) {
-      if (hasProp(v, 'appInfo')) {
-        const appInfoParsed = appInfo.safeParse(v.appInfo)
-        if (appInfoParsed.success) {
-          this.setAppInfo(appInfoParsed.data)
-        }
-      }
-      if (hasProp(v, 'me')) {
-        this.me.hydrate(v.me)
-      }
-    }
-  }
-
-  /**
-   * Called during init to resume any stored session.
-   */
-  async attemptSessionResumption() {}
-
-  /**
-   * Called by the session model. Refreshes session-oriented state.
-   */
-  async handleSessionChange(
-    agent: BskyAgent,
-    {hadSession}: {hadSession: boolean},
-  ) {
-    logger.debug('RootStoreModel:handleSessionChange')
-    this.agent = agent
-    applyDebugHeader(this.agent)
-    this.me.clear()
-    await this.me.load()
-    if (!hadSession) {
-      await resetNavigation()
-    }
-    this.emitSessionReady()
-  }
-
-  /**
-   * Called by the session model. Handles session drops by informing the user.
-   */
-  async handleSessionDrop() {
-    logger.debug('RootStoreModel:handleSessionDrop')
-    resetToTab('HomeTab')
-    this.me.clear()
-    this.emitSessionDropped()
-  }
-
-  /**
-   * Clears all session-oriented state, previously called on LOGOUT
-   */
-  clearAllSessionState() {
-    logger.debug('RootStoreModel:clearAllSessionState')
-    resetToTab('HomeTab')
-    this.me.clear()
-  }
-
-  /**
-   * Periodic poll for new session state.
-   */
-  async updateSessionState() {
-    if (!this.session.hasSession) {
-      return
-    }
-    try {
-      await this.me.updateIfNeeded()
-    } catch (e: any) {
-      logger.error('Failed to fetch latest state', {error: e})
-    }
-  }
-
-  // global event bus
-  // =
-  // - some events need to be passed around between views and models
-  //   in order to keep state in sync; these methods are for that
-
-  // a post was deleted by the local user
-  onPostDeleted(handler: (uri: string) => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('post-deleted', handler)
-  }
-  emitPostDeleted(uri: string) {
-    DeviceEventEmitter.emit('post-deleted', uri)
-  }
-
-  // a list was deleted by the local user
-  onListDeleted(handler: (uri: string) => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('list-deleted', handler)
-  }
-  emitListDeleted(uri: string) {
-    DeviceEventEmitter.emit('list-deleted', uri)
-  }
-
-  // the session has started and been fully hydrated
-  onSessionLoaded(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('session-loaded', handler)
-  }
-  emitSessionLoaded() {
-    DeviceEventEmitter.emit('session-loaded')
-  }
-
-  // the session has completed all setup; good for post-initialization behaviors like triggering modals
-  onSessionReady(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('session-ready', handler)
-  }
-  emitSessionReady() {
-    DeviceEventEmitter.emit('session-ready')
-  }
-
-  // the session was dropped due to bad/expired refresh tokens
-  onSessionDropped(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('session-dropped', handler)
-  }
-  emitSessionDropped() {
-    DeviceEventEmitter.emit('session-dropped')
-  }
-
-  // the current screen has changed
-  // TODO is this still needed?
-  onNavigation(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('navigation', handler)
-  }
-  emitNavigation() {
-    DeviceEventEmitter.emit('navigation')
-  }
-
-  // a "soft reset" typically means scrolling to top and loading latest
-  // but it can depend on the screen
-  onScreenSoftReset(handler: () => void): EmitterSubscription {
-    return DeviceEventEmitter.addListener('screen-soft-reset', handler)
-  }
-  emitScreenSoftReset() {
-    DeviceEventEmitter.emit('screen-soft-reset')
-  }
-}
-
-const throwawayInst = new RootStoreModel(
-  new BskyAgent({service: 'http://localhost'}),
-) // this will be replaced by the loader, we just need to supply a value at init
-const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
-export const RootStoreProvider = RootStoreContext.Provider
-export const useStores = () => useContext(RootStoreContext)
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
deleted file mode 100644
index cc681ad34..000000000
--- a/src/state/models/session.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  BskyAgent,
-  ComAtprotoServerDescribeServer as DescribeServer,
-} from '@atproto/api'
-import {RootStoreModel} from './root-store'
-
-export type ServiceDescription = DescribeServer.OutputSchema
-
-export class SessionModel {
-  data: any = {}
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(this, {
-      rootStore: false,
-      hasSession: false,
-    })
-  }
-
-  get currentSession(): any {
-    return undefined
-  }
-
-  get hasSession() {
-    return false
-  }
-
-  clear() {}
-
-  /**
-   * Helper to fetch the accounts config settings from an account.
-   */
-  async describeService(service: string): Promise<ServiceDescription> {
-    const agent = new BskyAgent({service})
-    const res = await agent.com.atproto.server.describeServer({})
-    return res.data
-  }
-
-  /**
-   * Reloads the session from the server. Useful when account details change, like the handle.
-   */
-  async reloadFromServer() {}
-}
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
deleted file mode 100644
index 18287c953..000000000
--- a/src/state/models/ui/shell.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {RootStoreModel} from '../root-store'
-import {makeAutoObservable} from 'mobx'
-import {
-  shouldRequestEmailConfirmation,
-  setEmailConfirmationRequested,
-} from '#/state/shell/reminders'
-import {unstable__openModal} from '#/state/modals'
-
-export type ColorMode = 'system' | 'light' | 'dark'
-
-export function isColorMode(v: unknown): v is ColorMode {
-  return v === 'system' || v === 'light' || v === 'dark'
-}
-
-export class ShellUiModel {
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(this, {
-      rootStore: false,
-    })
-
-    this.setupLoginModals()
-  }
-
-  setupLoginModals() {
-    this.rootStore.onSessionReady(() => {
-      if (shouldRequestEmailConfirmation(this.rootStore.session)) {
-        unstable__openModal({name: 'verify-email', showReminder: true})
-        setEmailConfirmationRequested()
-      }
-    })
-  }
-}