about summary refs log tree commit diff
path: root/src/state/models/root-store.ts
blob: 73f1c452f718ed110393555883bc44c55b64444c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
 * The root store is the base of all modeled state.
 */

import {makeAutoObservable} from 'mobx'
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
import {createContext, useContext} from 'react'
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
import {isObj, hasProp} from '../lib/type-guards'
import {LogModel} from './log'
import {SessionModel} from './session'
import {NavigationModel} from './navigation'
import {ShellUiModel} from './shell-ui'
import {ProfilesViewModel} from './profiles-view'
import {LinkMetasViewModel} from './link-metas-view'
import {MeModel} from './me'
import {OnboardModel} from './onboard'
import {isNetworkError} from '../../lib/errors'

export class RootStoreModel {
  log = new LogModel()
  session = new SessionModel(this)
  nav = new NavigationModel()
  shell = new ShellUiModel()
  me = new MeModel(this)
  onboard = new OnboardModel()
  profiles = new ProfilesViewModel(this)
  linkMetas = new LinkMetasViewModel(this)

  constructor(public api: SessionServiceClient) {
    makeAutoObservable(this, {
      api: false,
      resolveName: false,
      serialize: false,
      hydrate: false,
    })
  }

  async resolveName(didOrHandle: string) {
    if (!didOrHandle) {
      throw new Error('Invalid handle: ""')
    }
    if (didOrHandle.startsWith('did:')) {
      return didOrHandle
    }
    const res = await this.api.com.atproto.handle.resolve({handle: didOrHandle})
    return res.data.did
  }

  async fetchStateUpdate() {
    if (!this.session.hasSession) {
      return
    }
    try {
      if (!this.session.online) {
        await this.session.connect()
      }
      await this.me.fetchStateUpdate()
    } catch (e: any) {
      if (isNetworkError(e)) {
        this.session.setOnline(false) // connection lost
      }
      this.log.error('Failed to fetch latest state', e)
    }
  }

  serialize(): unknown {
    return {
      log: this.log.serialize(),
      session: this.session.serialize(),
      me: this.me.serialize(),
      nav: this.nav.serialize(),
      onboard: this.onboard.serialize(),
      shell: this.shell.serialize(),
    }
  }

  hydrate(v: unknown) {
    if (isObj(v)) {
      if (hasProp(v, 'log')) {
        this.log.hydrate(v.log)
      }
      if (hasProp(v, 'me')) {
        this.me.hydrate(v.me)
      }
      if (hasProp(v, 'nav')) {
        this.nav.hydrate(v.nav)
      }
      if (hasProp(v, 'onboard')) {
        this.onboard.hydrate(v.onboard)
      }
      if (hasProp(v, 'session')) {
        this.session.hydrate(v.session)
      }
      if (hasProp(v, 'shell')) {
        this.shell.hydrate(v.shell)
      }
    }
  }

  clearAll() {
    this.session.clear()
    this.nav.clear()
    this.me.clear()
  }

  onPostDeleted(handler: (uri: string) => void): EmitterSubscription {
    return DeviceEventEmitter.addListener('post-deleted', handler)
  }

  emitPostDeleted(uri: string) {
    console.log('emit')
    DeviceEventEmitter.emit('post-deleted', uri)
  }
}

const throwawayInst = new RootStoreModel(AtpApi.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)