Bluesky app fork with some witchin' additions 馃挮
at main 322 lines 10 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 colorScheme: z.enum([ 53 'witchsky', 54 'bluesky', 55 'blacksky', 56 'deer', 57 'zeppelin', 58 'kitty', 59 'reddwarf', 60 ]), 61 hue: z.number(), 62 session: z.object({ 63 accounts: z.array(accountSchema), 64 currentAccount: currentAccountSchema.optional(), 65 }), 66 reminders: z.object({ 67 lastEmailConfirm: z.string().optional(), 68 }), 69 languagePrefs: z.object({ 70 /** 71 * The target language for translating posts. 72 * 73 * BCP-47 2-letter language code without region. 74 */ 75 primaryLanguage: z.string(), 76 /** 77 * The languages the user can read, passed to feeds. 78 * 79 * BCP-47 2-letter language codes without region. 80 */ 81 contentLanguages: z.array(z.string()), 82 /** 83 * The language(s) the user is currently posting in, configured within the 84 * composer. Multiple languages are separated by commas. 85 * 86 * BCP-47 2-letter language code without region. 87 */ 88 postLanguage: z.string(), 89 /** 90 * The user's post language history, used to pre-populate the post language 91 * selector in the composer. Within each value, multiple languages are separated 92 * by commas. 93 * 94 * BCP-47 2-letter language codes without region. 95 */ 96 postLanguageHistory: z.array(z.string()), 97 /** 98 * The language for UI translations in the app. 99 * 100 * BCP-47 2-letter language code with or without region, 101 * to match with {@link AppLanguage}. 102 */ 103 appLanguage: z.string(), 104 }), 105 requireAltTextEnabled: z.boolean(), // should move to server 106 largeAltBadgeEnabled: z.boolean().optional(), 107 externalEmbeds: z 108 .object({ 109 giphy: z.enum(externalEmbedOptions).optional(), 110 tenor: z.enum(externalEmbedOptions).optional(), 111 youtube: z.enum(externalEmbedOptions).optional(), 112 youtubeShorts: z.enum(externalEmbedOptions).optional(), 113 twitch: z.enum(externalEmbedOptions).optional(), 114 vimeo: z.enum(externalEmbedOptions).optional(), 115 spotify: z.enum(externalEmbedOptions).optional(), 116 appleMusic: z.enum(externalEmbedOptions).optional(), 117 soundcloud: z.enum(externalEmbedOptions).optional(), 118 flickr: z.enum(externalEmbedOptions).optional(), 119 streamplace: z.enum(externalEmbedOptions).optional(), 120 }) 121 .optional(), 122 invites: z.object({ 123 copiedInvites: z.array(z.string()), 124 }), 125 onboarding: z.object({ 126 step: z.string(), 127 }), 128 hiddenPosts: z.array(z.string()).optional(), // should move to server 129 useInAppBrowser: z.boolean().optional(), 130 /** @deprecated */ 131 lastSelectedHomeFeed: z.string().optional(), 132 pdsAddressHistory: z.array(z.string()).optional(), 133 disableHaptics: z.boolean().optional(), 134 disableAutoplay: z.boolean().optional(), 135 kawaii: z.boolean().optional(), 136 hasCheckedForStarterPack: z.boolean().optional(), 137 subtitlesEnabled: z.boolean().optional(), 138 139 // deer 140 goLinksEnabled: z.boolean().optional(), 141 constellationEnabled: z.boolean().optional(), 142 directFetchRecords: z.boolean().optional(), 143 noAppLabelers: z.boolean().optional(), 144 noDiscoverFallback: z.boolean().optional(), 145 repostCarouselEnabled: z.boolean().optional(), 146 constellationInstance: z.string().optional(), 147 showLinkInHandle: z.boolean().optional(), 148 hideFeedsPromoTab: z.boolean().optional(), 149 disableViaRepostNotification: z.boolean().optional(), 150 disableLikesMetrics: z.boolean().optional(), 151 disableRepostsMetrics: z.boolean().optional(), 152 disableQuotesMetrics: z.boolean().optional(), 153 disableSavesMetrics: z.boolean().optional(), 154 disableReplyMetrics: z.boolean().optional(), 155 disableFollowersMetrics: z.boolean().optional(), 156 disableFollowingMetrics: z.boolean().optional(), 157 disableFollowedByMetrics: z.boolean().optional(), 158 disablePostsMetrics: z.boolean().optional(), 159 hideSimilarAccountsRecomm: z.boolean().optional(), 160 enableSquareAvatars: z.boolean().optional(), 161 enableSquareButtons: z.boolean().optional(), 162 disableVerifyEmailReminder: z.boolean().optional(), 163 deerVerification: z 164 .object({ 165 enabled: z.boolean(), 166 trusted: z.array(z.string()), 167 }) 168 .optional(), 169 highQualityImages: z.boolean().optional(), 170 hideUnreplyablePosts: z.boolean().optional(), 171 172 showExternalShareButtons: z.boolean().optional(), 173 174 /** @deprecated */ 175 mutedThreads: z.array(z.string()), 176 trendingDisabled: z.boolean().optional(), 177 trendingVideoDisabled: z.boolean().optional(), 178}) 179export type Schema = z.infer<typeof schema> 180 181export const defaults: Schema = { 182 colorMode: 'system', 183 darkTheme: 'dim', 184 colorScheme: 'witchsky', 185 hue: 0, 186 session: { 187 accounts: [], 188 currentAccount: undefined, 189 }, 190 reminders: { 191 lastEmailConfirm: undefined, 192 }, 193 languagePrefs: { 194 primaryLanguage: deviceLanguageCodes[0] || 'en', 195 contentLanguages: deviceLanguageCodes || [], 196 postLanguage: deviceLanguageCodes[0] || 'en', 197 postLanguageHistory: (deviceLanguageCodes || []) 198 .concat(['en', 'ja', 'pt', 'de']) 199 .slice(0, 6), 200 // try full language tag first, then fallback to language code 201 appLanguage: findSupportedAppLanguage([ 202 deviceLocales.at(0)?.languageTag, 203 deviceLanguageCodes[0], 204 ]), 205 }, 206 requireAltTextEnabled: true, 207 largeAltBadgeEnabled: true, 208 externalEmbeds: {}, 209 mutedThreads: [], 210 invites: { 211 copiedInvites: [], 212 }, 213 onboarding: { 214 step: 'Home', 215 }, 216 hiddenPosts: [], 217 useInAppBrowser: undefined, 218 lastSelectedHomeFeed: undefined, 219 pdsAddressHistory: [], 220 disableHaptics: false, 221 disableAutoplay: PlatformInfo.getIsReducedMotionEnabled(), 222 kawaii: false, 223 hasCheckedForStarterPack: false, 224 subtitlesEnabled: true, 225 trendingDisabled: true, 226 trendingVideoDisabled: true, 227 228 // deer 229 goLinksEnabled: true, 230 constellationEnabled: true, 231 directFetchRecords: false, 232 noAppLabelers: false, 233 noDiscoverFallback: false, 234 repostCarouselEnabled: false, 235 constellationInstance: 'https://constellation.microcosm.blue/', 236 showLinkInHandle: true, 237 hideFeedsPromoTab: false, 238 disableViaRepostNotification: false, 239 disableLikesMetrics: false, 240 disableRepostsMetrics: false, 241 disableQuotesMetrics: false, 242 disableSavesMetrics: false, 243 disableReplyMetrics: false, 244 disableFollowersMetrics: false, 245 disableFollowingMetrics: false, 246 disableFollowedByMetrics: false, 247 disablePostsMetrics: false, 248 hideSimilarAccountsRecomm: true, 249 enableSquareAvatars: true, 250 enableSquareButtons: true, 251 disableVerifyEmailReminder: false, 252 deerVerification: { 253 enabled: false, 254 // https://witchsky.app/profile/did:plc:p2cp5gopk7mgjegy6wadk3ep/post/3lndyqyyr4k2k 255 // using https://bverified.vercel.app/trusted as a source 256 trusted: [ 257 'did:plc:z72i7hdynmk6r22z27h6tvur', 258 'did:plc:b2kutgxqlltwc6lhs724cfwr', 259 'did:plc:inz4fkbbp7ms3ixufw6xuvdi', 260 'did:plc:eclio37ymobqex2ncko63h4r', 261 'did:plc:dzezcmpb3fhcpns4n4xm4ur5', 262 'did:plc:5u54z2qgkq43dh2nzwzdbbhb', 263 'did:plc:wmho6q2uiyktkam3jsvrms3s', 264 'did:plc:sqbswn3lalcc2dlh2k7zdpuw', 265 'did:plc:k5nskatzhyxersjilvtnz4lh', 266 'did:plc:d2jith367s6ybc3ldsusgdae', 267 'did:plc:y3xrmnwvkvsq4tqcsgwch4na', 268 'did:plc:i3fhjvvkbmirhyu4aeihhrnv', 269 'did:plc:fivojrvylkim4nuo3pfqcf3k', 270 'did:plc:ofbkqcjzvm6gtwuufsubnkaf', 271 'did:plc:xwqgusybtrpm67tcwqdfmzvy', 272 'did:plc:oxo226vi7t2btjokm2buusoy', 273 'did:plc:r4ve5hjtfjubdwrvlxcad62e', 274 'did:plc:j4eroku3volozvv6ljsnnfec', 275 'did:plc:6q2thhy2ohzog26mmqm4pffk', 276 'did:plc:rk25gdgk3cnnmtkvlae265nz', 277 ], 278 }, 279 highQualityImages: false, 280 hideUnreplyablePosts: false, 281 showExternalShareButtons: false, 282} 283 284export function tryParse(rawData: string): Schema | undefined { 285 let objData 286 try { 287 objData = JSON.parse(rawData) 288 } catch (e) { 289 logger.error('persisted state: failed to parse root state from storage', { 290 message: e, 291 }) 292 } 293 if (!objData) { 294 return undefined 295 } 296 const parsed = schema.safeParse(objData) 297 if (parsed.success) { 298 return objData 299 } else { 300 const errors = 301 parsed.error?.errors?.map(e => ({ 302 code: e.code, 303 // @ts-ignore exists on some types 304 expected: e?.expected, 305 path: e.path?.join('.'), 306 })) || [] 307 logger.error(`persisted store: data failed validation on read`, {errors}) 308 return undefined 309 } 310} 311 312export function tryStringify(value: Schema): string | undefined { 313 try { 314 schema.parse(value) 315 return JSON.stringify(value) 316 } catch (e) { 317 logger.error(`persisted state: failed stringifying root state`, { 318 message: e, 319 }) 320 return undefined 321 } 322}