Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 82f42e734c50b34de31e8aff1e7ced248ab6e96f 219 lines 7.0 kB view raw
1import {z} from 'zod' 2 3import {deviceLanguageCodes, deviceLocales} from '#/locale/deviceLocales' 4import {findSupportedAppLanguage} from '#/locale/helpers' 5import {logger} from '#/logger' 6import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army' 7 8const externalEmbedOptions = ['show', 'hide'] as const 9 10/** 11 * A account persisted to storage. Stored in the `accounts[]` array. Contains 12 * base account info and access tokens. 13 */ 14const accountSchema = z.object({ 15 service: z.string(), 16 did: z.string(), 17 handle: z.string(), 18 email: z.string().optional(), 19 emailConfirmed: z.boolean().optional(), 20 emailAuthFactor: z.boolean().optional(), 21 refreshJwt: z.string().optional(), // optional because it can expire 22 accessJwt: z.string().optional(), // optional because it can expire 23 signupQueued: z.boolean().optional(), 24 active: z.boolean().optional(), // optional for backwards compat 25 /** 26 * Known values: takendown, suspended, deactivated 27 * @see https://github.com/bluesky-social/atproto/blob/5441fbde9ed3b22463e91481ec80cb095643e141/lexicons/com/atproto/server/getSession.json 28 */ 29 status: z.string().optional(), 30 pdsUrl: z.string().optional(), 31 isSelfHosted: z.boolean().optional(), 32}) 33export type PersistedAccount = z.infer<typeof accountSchema> 34 35/** 36 * The current account. Stored in the `currentAccount` field. 37 * 38 * In previous versions, this included tokens and other info. Now, it's used 39 * only to reference the `did` field, and all other fields are marked as 40 * optional. They should be considered deprecated and not used, but are kept 41 * here for backwards compat. 42 */ 43const currentAccountSchema = accountSchema.extend({ 44 service: z.string().optional(), 45 handle: z.string().optional(), 46}) 47export type PersistedCurrentAccount = z.infer<typeof currentAccountSchema> 48 49const schema = z.object({ 50 colorMode: z.enum(['system', 'light', 'dark']), 51 darkTheme: z.enum(['dim', 'dark']).optional(), 52 session: z.object({ 53 accounts: z.array(accountSchema), 54 currentAccount: currentAccountSchema.optional(), 55 }), 56 reminders: z.object({ 57 lastEmailConfirm: z.string().optional(), 58 }), 59 languagePrefs: z.object({ 60 /** 61 * The target language for translating posts. 62 * 63 * BCP-47 2-letter language code without region. 64 */ 65 primaryLanguage: z.string(), 66 /** 67 * The languages the user can read, passed to feeds. 68 * 69 * BCP-47 2-letter language codes without region. 70 */ 71 contentLanguages: z.array(z.string()), 72 /** 73 * The language(s) the user is currently posting in, configured within the 74 * composer. Multiple languages are separated by commas. 75 * 76 * BCP-47 2-letter language code without region. 77 */ 78 postLanguage: z.string(), 79 /** 80 * The user's post language history, used to pre-populate the post language 81 * selector in the composer. Within each value, multiple languages are separated 82 * by commas. 83 * 84 * BCP-47 2-letter language codes without region. 85 */ 86 postLanguageHistory: z.array(z.string()), 87 /** 88 * The language for UI translations in the app. 89 * 90 * BCP-47 2-letter language code with or without region, 91 * to match with {@link AppLanguage}. 92 */ 93 appLanguage: z.string(), 94 }), 95 requireAltTextEnabled: z.boolean(), // should move to server 96 largeAltBadgeEnabled: z.boolean().optional(), 97 externalEmbeds: z 98 .object({ 99 giphy: z.enum(externalEmbedOptions).optional(), 100 tenor: z.enum(externalEmbedOptions).optional(), 101 youtube: z.enum(externalEmbedOptions).optional(), 102 youtubeShorts: z.enum(externalEmbedOptions).optional(), 103 twitch: z.enum(externalEmbedOptions).optional(), 104 vimeo: z.enum(externalEmbedOptions).optional(), 105 spotify: z.enum(externalEmbedOptions).optional(), 106 appleMusic: z.enum(externalEmbedOptions).optional(), 107 soundcloud: z.enum(externalEmbedOptions).optional(), 108 flickr: z.enum(externalEmbedOptions).optional(), 109 bandcamp: z.enum(externalEmbedOptions).optional(), 110 }) 111 .optional(), 112 invites: z.object({ 113 copiedInvites: z.array(z.string()), 114 }), 115 onboarding: z.object({ 116 step: z.string(), 117 }), 118 hiddenPosts: z.array(z.string()).optional(), // should move to server 119 useInAppBrowser: z.boolean().optional(), 120 /** @deprecated */ 121 lastSelectedHomeFeed: z.string().optional(), 122 pdsAddressHistory: z.array(z.string()).optional(), 123 disableHaptics: z.boolean().optional(), 124 disableAutoplay: z.boolean().optional(), 125 kawaii: z.boolean().optional(), 126 hasCheckedForStarterPack: z.boolean().optional(), 127 subtitlesEnabled: z.boolean().optional(), 128 /** @deprecated */ 129 mutedThreads: z.array(z.string()), 130 trendingDisabled: z.boolean().optional(), 131 trendingVideoDisabled: z.boolean().optional(), 132}) 133export type Schema = z.infer<typeof schema> 134 135export const defaults: Schema = { 136 colorMode: 'system', 137 darkTheme: 'dim', 138 session: { 139 accounts: [], 140 currentAccount: undefined, 141 }, 142 reminders: { 143 lastEmailConfirm: undefined, 144 }, 145 languagePrefs: { 146 primaryLanguage: deviceLanguageCodes[0] || 'en', 147 contentLanguages: deviceLanguageCodes || [], 148 postLanguage: deviceLanguageCodes[0] || 'en', 149 postLanguageHistory: (deviceLanguageCodes || []) 150 .concat(['en', 'ja', 'pt', 'de']) 151 .slice(0, 6), 152 // try full language tag first, then fallback to language code 153 appLanguage: findSupportedAppLanguage([ 154 deviceLocales.at(0)?.languageTag, 155 deviceLanguageCodes[0], 156 ]), 157 }, 158 requireAltTextEnabled: false, 159 largeAltBadgeEnabled: false, 160 externalEmbeds: {}, 161 mutedThreads: [], 162 invites: { 163 copiedInvites: [], 164 }, 165 onboarding: { 166 step: 'Home', 167 }, 168 hiddenPosts: [], 169 useInAppBrowser: undefined, 170 lastSelectedHomeFeed: undefined, 171 pdsAddressHistory: [], 172 disableHaptics: false, 173 disableAutoplay: PlatformInfo.getIsReducedMotionEnabled(), 174 kawaii: false, 175 hasCheckedForStarterPack: false, 176 subtitlesEnabled: true, 177 trendingDisabled: false, 178 trendingVideoDisabled: false, 179} 180 181export function tryParse(rawData: string): Schema | undefined { 182 let objData 183 try { 184 objData = JSON.parse(rawData) 185 } catch (e) { 186 logger.error('persisted state: failed to parse root state from storage', { 187 message: e, 188 }) 189 } 190 if (!objData) { 191 return undefined 192 } 193 const parsed = schema.safeParse(objData) 194 if (parsed.success) { 195 return objData 196 } else { 197 const errors = 198 parsed.error?.errors?.map(e => ({ 199 code: e.code, 200 // @ts-ignore exists on some types 201 expected: e?.expected, 202 path: e.path?.join('.'), 203 })) || [] 204 logger.error(`persisted store: data failed validation on read`, {errors}) 205 return undefined 206 } 207} 208 209export function tryStringify(value: Schema): string | undefined { 210 try { 211 schema.parse(value) 212 return JSON.stringify(value) 213 } catch (e) { 214 logger.error(`persisted state: failed stringifying root state`, { 215 message: e, 216 }) 217 return undefined 218 } 219}