Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 327 lines 11 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 disableComposerPrompt: z.boolean().optional(), 151 disableLikesMetrics: z.boolean().optional(), 152 disableRepostsMetrics: z.boolean().optional(), 153 disableQuotesMetrics: z.boolean().optional(), 154 disableSavesMetrics: z.boolean().optional(), 155 disableReplyMetrics: z.boolean().optional(), 156 disableFollowersMetrics: z.boolean().optional(), 157 disableFollowingMetrics: z.boolean().optional(), 158 disableFollowedByMetrics: z.boolean().optional(), 159 disablePostsMetrics: z.boolean().optional(), 160 hideSimilarAccountsRecomm: z.boolean().optional(), 161 enableSquareAvatars: z.boolean().optional(), 162 enableSquareButtons: z.boolean().optional(), 163 disableVerifyEmailReminder: z.boolean().optional(), 164 deerVerification: z 165 .object({ 166 enabled: z.boolean(), 167 trusted: z.array(z.string()), 168 }) 169 .optional(), 170 highQualityImages: z.boolean().optional(), 171 hideUnreplyablePosts: z.boolean().optional(), 172 173 showExternalShareButtons: z.boolean().optional(), 174 175 translationServicePreference: z.enum(['google', 'kagi']), 176 177 /** @deprecated */ 178 mutedThreads: z.array(z.string()), 179 trendingDisabled: z.boolean().optional(), 180 trendingVideoDisabled: z.boolean().optional(), 181}) 182export type Schema = z.infer<typeof schema> 183 184export const defaults: Schema = { 185 colorMode: 'system', 186 darkTheme: 'dim', 187 colorScheme: 'witchsky', 188 hue: 0, 189 session: { 190 accounts: [], 191 currentAccount: undefined, 192 }, 193 reminders: { 194 lastEmailConfirm: undefined, 195 }, 196 languagePrefs: { 197 primaryLanguage: deviceLanguageCodes[0] || 'en', 198 contentLanguages: deviceLanguageCodes || [], 199 postLanguage: deviceLanguageCodes[0] || 'en', 200 postLanguageHistory: (deviceLanguageCodes || []) 201 .concat(['en', 'ja', 'pt', 'de']) 202 .slice(0, 6), 203 // try full language tag first, then fallback to language code 204 appLanguage: findSupportedAppLanguage([ 205 deviceLocales.at(0)?.languageTag, 206 deviceLanguageCodes[0], 207 ]), 208 }, 209 requireAltTextEnabled: true, 210 largeAltBadgeEnabled: true, 211 externalEmbeds: {}, 212 mutedThreads: [], 213 invites: { 214 copiedInvites: [], 215 }, 216 onboarding: { 217 step: 'Home', 218 }, 219 hiddenPosts: [], 220 useInAppBrowser: undefined, 221 lastSelectedHomeFeed: undefined, 222 pdsAddressHistory: [], 223 disableHaptics: false, 224 disableAutoplay: PlatformInfo.getIsReducedMotionEnabled(), 225 kawaii: false, 226 hasCheckedForStarterPack: false, 227 subtitlesEnabled: true, 228 trendingDisabled: true, 229 trendingVideoDisabled: true, 230 231 // deer 232 goLinksEnabled: true, 233 constellationEnabled: true, 234 directFetchRecords: false, 235 noAppLabelers: false, 236 noDiscoverFallback: false, 237 repostCarouselEnabled: false, 238 constellationInstance: 'https://constellation.microcosm.blue/', 239 showLinkInHandle: true, 240 hideFeedsPromoTab: false, 241 disableViaRepostNotification: false, 242 disableComposerPrompt: true, 243 disableLikesMetrics: false, 244 disableRepostsMetrics: false, 245 disableQuotesMetrics: false, 246 disableSavesMetrics: false, 247 disableReplyMetrics: false, 248 disableFollowersMetrics: false, 249 disableFollowingMetrics: false, 250 disableFollowedByMetrics: false, 251 disablePostsMetrics: false, 252 hideSimilarAccountsRecomm: true, 253 enableSquareAvatars: true, 254 enableSquareButtons: true, 255 disableVerifyEmailReminder: false, 256 deerVerification: { 257 enabled: false, 258 // https://witchsky.app/profile/did:plc:p2cp5gopk7mgjegy6wadk3ep/post/3lndyqyyr4k2k 259 // using https://bverified.vercel.app/trusted as a source 260 trusted: [ 261 'did:plc:z72i7hdynmk6r22z27h6tvur', 262 'did:plc:b2kutgxqlltwc6lhs724cfwr', 263 'did:plc:inz4fkbbp7ms3ixufw6xuvdi', 264 'did:plc:eclio37ymobqex2ncko63h4r', 265 'did:plc:dzezcmpb3fhcpns4n4xm4ur5', 266 'did:plc:5u54z2qgkq43dh2nzwzdbbhb', 267 'did:plc:wmho6q2uiyktkam3jsvrms3s', 268 'did:plc:sqbswn3lalcc2dlh2k7zdpuw', 269 'did:plc:k5nskatzhyxersjilvtnz4lh', 270 'did:plc:d2jith367s6ybc3ldsusgdae', 271 'did:plc:y3xrmnwvkvsq4tqcsgwch4na', 272 'did:plc:i3fhjvvkbmirhyu4aeihhrnv', 273 'did:plc:fivojrvylkim4nuo3pfqcf3k', 274 'did:plc:ofbkqcjzvm6gtwuufsubnkaf', 275 'did:plc:xwqgusybtrpm67tcwqdfmzvy', 276 'did:plc:oxo226vi7t2btjokm2buusoy', 277 'did:plc:r4ve5hjtfjubdwrvlxcad62e', 278 'did:plc:j4eroku3volozvv6ljsnnfec', 279 'did:plc:6q2thhy2ohzog26mmqm4pffk', 280 'did:plc:rk25gdgk3cnnmtkvlae265nz', 281 ], 282 }, 283 highQualityImages: false, 284 hideUnreplyablePosts: false, 285 showExternalShareButtons: false, 286 translationServicePreference: 'google', 287} 288 289export function tryParse(rawData: string): Schema | undefined { 290 let objData 291 try { 292 objData = JSON.parse(rawData) 293 } catch (e) { 294 logger.error('persisted state: failed to parse root state from storage', { 295 message: e, 296 }) 297 } 298 if (!objData) { 299 return undefined 300 } 301 const parsed = schema.safeParse(objData) 302 if (parsed.success) { 303 return objData 304 } else { 305 const errors = 306 parsed.error?.errors?.map(e => ({ 307 code: e.code, 308 // @ts-ignore exists on some types 309 expected: e?.expected, 310 path: e.path?.join('.'), 311 })) || [] 312 logger.error(`persisted store: data failed validation on read`, {errors}) 313 return undefined 314 } 315} 316 317export function tryStringify(value: Schema): string | undefined { 318 try { 319 schema.parse(value) 320 return JSON.stringify(value) 321 } catch (e) { 322 logger.error(`persisted state: failed stringifying root state`, { 323 message: e, 324 }) 325 return undefined 326 } 327}