Bluesky app fork with some witchin' additions 馃挮
at post-text-option 317 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 lastSelectedHomeFeed: z.string().optional(), 131 pdsAddressHistory: z.array(z.string()).optional(), 132 disableHaptics: z.boolean().optional(), 133 disableAutoplay: z.boolean().optional(), 134 kawaii: z.boolean().optional(), 135 hasCheckedForStarterPack: z.boolean().optional(), 136 subtitlesEnabled: z.boolean().optional(), 137 138 // deer 139 goLinksEnabled: z.boolean().optional(), 140 constellationEnabled: z.boolean().optional(), 141 directFetchRecords: z.boolean().optional(), 142 noAppLabelers: z.boolean().optional(), 143 noDiscoverFallback: z.boolean().optional(), 144 repostCarouselEnabled: z.boolean().optional(), 145 constellationInstance: z.string().optional(), 146 showLinkInHandle: z.boolean().optional(), 147 hideFeedsPromoTab: z.boolean().optional(), 148 disableViaRepostNotification: z.boolean().optional(), 149 disableLikesMetrics: z.boolean().optional(), 150 disableRepostsMetrics: z.boolean().optional(), 151 disableQuotesMetrics: z.boolean().optional(), 152 disableSavesMetrics: z.boolean().optional(), 153 disableReplyMetrics: z.boolean().optional(), 154 disableFollowersMetrics: z.boolean().optional(), 155 disableFollowingMetrics: z.boolean().optional(), 156 disableFollowedByMetrics: z.boolean().optional(), 157 disablePostsMetrics: z.boolean().optional(), 158 hideSimilarAccountsRecomm: z.boolean().optional(), 159 enableSquareAvatars: z.boolean().optional(), 160 enableSquareButtons: z.boolean().optional(), 161 disableVerifyEmailReminder: z.boolean().optional(), 162 deerVerification: z 163 .object({ 164 enabled: z.boolean(), 165 trusted: z.array(z.string()), 166 }) 167 .optional(), 168 highQualityImages: z.boolean().optional(), 169 hideUnreplyablePosts: z.boolean().optional(), 170 171 showExternalShareButtons: z.boolean().optional(), 172 terminologyPreference: z.enum(['skeet', 'post', 'spell']).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: false, 250 enableSquareButtons: false, 251 disableVerifyEmailReminder: false, 252 deerVerification: { 253 enabled: false, 254 // https://witchsky.app/profile/did:plc:p2cp5gopk7mgjegy6wadk3ep/post/3lndyqyyr4k2k 255 // using https://bverified.vercel.app/ 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 ], 272 }, 273 highQualityImages: false, 274 hideUnreplyablePosts: false, 275 showExternalShareButtons: false, 276 terminologyPreference: 'skeet', 277} 278 279export function tryParse(rawData: string): Schema | undefined { 280 let objData 281 try { 282 objData = JSON.parse(rawData) 283 } catch (e) { 284 logger.error('persisted state: failed to parse root state from storage', { 285 message: e, 286 }) 287 } 288 if (!objData) { 289 return undefined 290 } 291 const parsed = schema.safeParse(objData) 292 if (parsed.success) { 293 return objData 294 } else { 295 const errors = 296 parsed.error?.errors?.map(e => ({ 297 code: e.code, 298 // @ts-ignore exists on some types 299 expected: e?.expected, 300 path: e.path?.join('.'), 301 })) || [] 302 logger.error(`persisted store: data failed validation on read`, {errors}) 303 return undefined 304 } 305} 306 307export function tryStringify(value: Schema): string | undefined { 308 try { 309 schema.parse(value) 310 return JSON.stringify(value) 311 } catch (e) { 312 logger.error(`persisted state: failed stringifying root state`, { 313 message: e, 314 }) 315 return undefined 316 } 317}