about summary refs log tree commit diff
path: root/src/state/persisted/legacy.ts
blob: 6bb75ae86774df94e44784ed510bcaae8b45d4e0 (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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import AsyncStorage from '@react-native-async-storage/async-storage'

import {logger} from '#/logger'
import {defaults, Schema, schema} from '#/state/persisted/schema'
import {write, read} from '#/state/persisted/store'

/**
 * The shape of the serialized data from our legacy Mobx store.
 */
export type LegacySchema = {
  shell: {
    colorMode: 'system' | 'light' | 'dark'
  }
  session: {
    data: {
      service: string
      did: `did:plc:${string}`
    } | null
    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: Partial<LegacySchema>): Schema {
  return {
    colorMode: legacy.shell?.colorMode || defaults.colorMode,
    session: {
      accounts: legacy.session?.accounts || defaults.session.accounts,
      currentAccount:
        legacy.session?.accounts?.find(
          a => a.did === legacy.session?.data?.did,
        ) || defaults.session.currentAccount,
    },
    reminders: {
      lastEmailConfirm:
        legacy.reminders?.lastEmailConfirm ||
        defaults.reminders.lastEmailConfirm,
    },
    languagePrefs: {
      primaryLanguage:
        legacy.preferences?.primaryLanguage ||
        defaults.languagePrefs.primaryLanguage,
      contentLanguages:
        legacy.preferences?.contentLanguages ||
        defaults.languagePrefs.contentLanguages,
      postLanguage:
        legacy.preferences?.postLanguage || defaults.languagePrefs.postLanguage,
      postLanguageHistory:
        legacy.preferences?.postLanguageHistory ||
        defaults.languagePrefs.postLanguageHistory,
      appLanguage:
        legacy.preferences?.primaryLanguage ||
        defaults.languagePrefs.appLanguage,
    },
    requireAltTextEnabled:
      legacy.preferences?.requireAltTextEnabled ||
      defaults.requireAltTextEnabled,
    mutedThreads: legacy.mutedThreads?.uris || defaults.mutedThreads,
    invites: {
      copiedInvites:
        legacy.invitedUsers?.copiedInvites || defaults.invites.copiedInvites,
    },
    onboarding: {
      step: legacy.onboarding?.step || defaults.onboarding.step,
    },
    hiddenPosts: defaults.hiddenPosts,
    externalEmbeds: defaults.externalEmbeds,
  }
}

/**
 * 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.info('persisted state: check need to migrate')

  try {
    const rawLegacyData = await AsyncStorage.getItem(
      DEPRECATED_ROOT_STATE_STORAGE_KEY,
    )
    const newData = await read()
    const alreadyMigrated = Boolean(newData)

    if (!alreadyMigrated && rawLegacyData) {
      logger.info('persisted state: migrating legacy storage')

      const legacyData = JSON.parse(rawLegacyData)
      const newData = transform(legacyData)
      const validate = schema.safeParse(newData)

      if (validate.success) {
        await write(newData)
        logger.info('persisted state: migrated legacy storage')
      } else {
        logger.error('persisted state: legacy data failed validation', {
          message: validate.error,
        })
      }
    } else {
      logger.info('persisted state: no migration needed')
    }
  } catch (e: any) {
    logger.error(e, {
      message: 'persisted state: error migrating legacy storage',
    })
  }
}

export async function clearLegacyStorage() {
  try {
    await AsyncStorage.removeItem(DEPRECATED_ROOT_STATE_STORAGE_KEY)
  } catch (e: any) {
    logger.error(`persisted legacy store: failed to clear`, {
      message: e.toString(),
    })
  }
}