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

Configure Feed

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

at theme-changes 425 lines 13 kB view raw
1import {z} from 'zod' 2 3import {DEFAULT_ALT_TEXT_AI_MODEL} from '#/lib/constants' 4import {deviceLanguageCodes, deviceLocales} from '#/locale/deviceLocales' 5import {findSupportedAppLanguage} from '#/locale/helpers' 6import {logger} from '#/logger' 7import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army' 8 9const externalEmbedOptions = ['show', 'hide'] as const 10 11/** 12 * A account persisted to storage. Stored in the `accounts[]` array. Contains 13 * base account info and access tokens. 14 */ 15const accountSchema = z.object({ 16 service: z.string(), 17 did: z.string(), 18 handle: z.string(), 19 addedAt: z.string().optional(), 20 lastActiveAt: z.string().optional(), 21 email: z.string().optional(), 22 emailConfirmed: z.boolean().optional(), 23 emailAuthFactor: z.boolean().optional(), 24 refreshJwt: z.string().optional(), // optional because it can expire 25 accessJwt: z.string().optional(), // optional because it can expire 26 signupQueued: z.boolean().optional(), 27 active: z.boolean().optional(), // optional for backwards compat 28 /** 29 * Known values: takendown, suspended, deactivated 30 * @see https://github.com/bluesky-social/atproto/blob/5441fbde9ed3b22463e91481ec80cb095643e141/lexicons/com/atproto/server/getSession.json 31 */ 32 status: z.string().optional(), 33 pdsUrl: z.string().optional(), 34 isSelfHosted: z.boolean().optional(), 35 isOauthSession: z.boolean().optional(), 36}) 37export type PersistedAccount = z.infer<typeof accountSchema> 38 39/** 40 * The current account. Stored in the `currentAccount` field. 41 * 42 * In previous versions, this included tokens and other info. Now, it's used 43 * only to reference the `did` field, and all other fields are marked as 44 * optional. They should be considered deprecated and not used, but are kept 45 * here for backwards compat. 46 */ 47const currentAccountSchema = accountSchema.extend({ 48 service: z.string().optional(), 49 handle: z.string().optional(), 50}) 51export type PersistedCurrentAccount = z.infer<typeof currentAccountSchema> 52 53const schema = z.object({ 54 colorMode: z.enum(['system', 'light', 'dark']), 55 darkTheme: z.enum(['dim', 'dark']).optional(), 56 colorScheme: z.enum([ 57 'witchsky', 58 'bluesky', 59 'blacksky', 60 'deer', 61 'zeppelin', 62 'kitty', 63 'reddwarf', 64 'catppuccin', 65 'evergarden', 66 'cyan base', 67 'material3', 68 ]), 69 hue: z.number(), 70 material3Accent: z 71 .string() 72 .regex(/^#[0-9a-fA-F]{6}$/, { 73 message: 74 'Invalid color format. Must be a 7-character hex code (e.g., #RRGGBB).', 75 }) 76 .default('#ee6300'), 77 material3Style: z 78 .enum([ 79 'SPRITZ', 80 'TONAL_SPOT', 81 'VIBRANT', 82 'EXPRESSIVE', 83 'RAINBOW', 84 'FRUIT_SALAD', 85 'CONTENT', 86 'MONOCHROMATIC', 87 ]) 88 .default('TONAL_SPOT'), 89 session: z.object({ 90 accounts: z.array(accountSchema), 91 currentAccount: currentAccountSchema.optional(), 92 }), 93 reminders: z.object({ 94 lastEmailConfirm: z.string().optional(), 95 }), 96 languagePrefs: z.object({ 97 /** 98 * The target language for translating posts. 99 * 100 * BCP-47 2-letter language code without region. 101 */ 102 primaryLanguage: z.string(), 103 /** 104 * The languages the user can read, passed to feeds. 105 * 106 * BCP-47 2-letter language codes without region. 107 */ 108 contentLanguages: z.array(z.string()), 109 /** 110 * The language(s) the user is currently posting in, configured within the 111 * composer. Multiple languages are separated by commas. 112 * 113 * BCP-47 2-letter language code without region. 114 */ 115 postLanguage: z.string(), 116 /** 117 * The user's post language history, used to pre-populate the post language 118 * selector in the composer. Within each value, multiple languages are separated 119 * by commas. 120 * 121 * BCP-47 2-letter language codes without region. 122 */ 123 postLanguageHistory: z.array(z.string()), 124 /** 125 * The language for UI translations in the app. 126 * 127 * BCP-47 2-letter language code with or without region, 128 * to match with {@link AppLanguage}. 129 */ 130 appLanguage: z.string(), 131 }), 132 requireAltTextEnabled: z.boolean(), // should move to server 133 largeAltBadgeEnabled: z.boolean().optional(), 134 externalEmbeds: z 135 .object({ 136 giphy: z.enum(externalEmbedOptions).optional(), 137 tenor: z.enum(externalEmbedOptions).optional(), 138 klipy: z.enum(externalEmbedOptions).optional(), 139 youtube: z.enum(externalEmbedOptions).optional(), 140 youtubeShorts: z.enum(externalEmbedOptions).optional(), 141 twitch: z.enum(externalEmbedOptions).optional(), 142 vimeo: z.enum(externalEmbedOptions).optional(), 143 spotify: z.enum(externalEmbedOptions).optional(), 144 appleMusic: z.enum(externalEmbedOptions).optional(), 145 soundcloud: z.enum(externalEmbedOptions).optional(), 146 flickr: z.enum(externalEmbedOptions).optional(), 147 bandcamp: z.enum(externalEmbedOptions).optional(), 148 streamplace: z.enum(externalEmbedOptions).optional(), 149 }) 150 .optional(), 151 invites: z.object({ 152 copiedInvites: z.array(z.string()), 153 }), 154 onboarding: z.object({ 155 step: z.string(), 156 }), 157 hiddenPosts: z.array(z.string()).optional(), // should move to server 158 useInAppBrowser: z.boolean().optional(), 159 /** @deprecated */ 160 lastSelectedHomeFeed: z.string().optional(), 161 pdsAddressHistory: z.array(z.string()).optional(), 162 disableHaptics: z.boolean().optional(), 163 disableAutoplay: z.boolean().optional(), 164 kawaii: z.boolean().optional(), 165 hasCheckedForStarterPack: z.boolean().optional(), 166 subtitlesEnabled: z.boolean().optional(), 167 168 // deer 169 goLinksEnabled: z.boolean().optional(), 170 constellationEnabled: z.boolean().optional(), 171 directFetchRecords: z.boolean().optional(), 172 noAppLabelers: z.boolean().optional(), 173 noDiscoverFallback: z.boolean().optional(), 174 repostCarouselEnabled: z.boolean().optional(), 175 alsoLikedFeedEnabled: z.boolean().optional(), 176 constellationInstance: z.string().optional(), 177 showLinkInHandle: z.boolean().optional(), 178 showLinkInHandleOnlyOnWorkingLinks: z.boolean().optional(), 179 hideFeedsPromoTab: z.boolean().optional(), 180 disableViaRepostNotification: z.boolean().optional(), 181 disableComposerPrompt: z.boolean().optional(), 182 disableTopOfFeedButton: z.boolean().optional(), 183 disableLikesMetrics: z.boolean().optional(), 184 disableRepostsMetrics: z.boolean().optional(), 185 disableQuotesMetrics: z.boolean().optional(), 186 disableSavesMetrics: z.boolean().optional(), 187 disableReplyMetrics: z.boolean().optional(), 188 disableFollowersMetrics: z.boolean().optional(), 189 disableFollowingMetrics: z.boolean().optional(), 190 disableFollowedByMetrics: z.boolean().optional(), 191 disablePostsMetrics: z.boolean().optional(), 192 showFollowsYouBadge: z.boolean().optional(), 193 hideSimilarAccountsRecomm: z.boolean().optional(), 194 hideScaryFollowButtons: z.boolean().optional(), 195 discoverContextEnabled: z.boolean().optional(), 196 enableSquareAvatars: z.boolean().optional(), 197 enableSquareButtons: z.boolean().optional(), 198 disableVerifyEmailReminder: z.boolean().optional(), 199 showViaClient: z.boolean().optional(), 200 deerVerification: z 201 .object({ 202 enabled: z.boolean(), 203 trusted: z.array(z.string()), 204 }) 205 .optional(), 206 highQualityImages: z.boolean().optional(), 207 imageCdnHost: z.string().optional(), 208 plcDirectory: z.string().optional(), 209 hideUnreplyablePosts: z.boolean().optional(), 210 pdsLabel: z 211 .object({ 212 enabled: z.boolean(), 213 hideBskyPds: z.boolean(), 214 }) 215 .optional(), 216 faviconService: z.string().optional(), 217 218 postReplacement: z.object({ 219 enabled: z.boolean().optional(), 220 postName: z.string().optional(), 221 postsName: z.string().optional(), 222 }), 223 224 showExternalShareButtons: z.boolean().optional(), 225 226 translationServicePreference: z.enum([ 227 'google', 228 'kagi', 229 'papago', 230 'libreTranslate', 231 ]), 232 libreTranslateInstance: z.string().optional(), 233 234 openRouterApiKey: z.string().optional(), 235 openRouterModel: z.string().optional(), 236 openRouterPrompt: z.string().optional(), 237 238 useHandleInLinks: z.boolean().optional(), 239 240 /** @deprecated */ 241 mutedThreads: z.array(z.string()), 242 trendingDisabled: z.boolean().optional(), 243 trendingVideoDisabled: z.boolean().optional(), 244 245 autoLikeOnRepost: z.boolean().optional(), 246 omitViaField: z.boolean().optional(), 247}) 248export type Schema = z.infer<typeof schema> 249 250export const defaults: Schema = { 251 colorMode: 'system', 252 darkTheme: 'dim', 253 colorScheme: 'witchsky', 254 hue: 0, 255 material3Accent: '#ee6300', 256 material3Style: 'TONAL_SPOT', 257 session: { 258 accounts: [], 259 currentAccount: undefined, 260 }, 261 reminders: { 262 lastEmailConfirm: undefined, 263 }, 264 languagePrefs: { 265 primaryLanguage: deviceLanguageCodes[0] || 'en', 266 contentLanguages: [], 267 postLanguage: deviceLanguageCodes[0] || 'en', 268 postLanguageHistory: (deviceLanguageCodes || []) 269 .concat(['en', 'ja', 'pt', 'de']) 270 .slice(0, 6), 271 // try full language tag first, then fallback to language code 272 appLanguage: findSupportedAppLanguage([ 273 deviceLocales.at(0)?.languageTag, 274 deviceLanguageCodes[0], 275 ]), 276 }, 277 requireAltTextEnabled: true, 278 largeAltBadgeEnabled: false, 279 externalEmbeds: {}, 280 mutedThreads: [], 281 invites: { 282 copiedInvites: [], 283 }, 284 onboarding: { 285 step: 'Home', 286 }, 287 hiddenPosts: [], 288 useInAppBrowser: undefined, 289 lastSelectedHomeFeed: undefined, 290 pdsAddressHistory: [], 291 disableHaptics: false, 292 disableAutoplay: PlatformInfo.getIsReducedMotionEnabled(), 293 kawaii: false, 294 hasCheckedForStarterPack: false, 295 subtitlesEnabled: true, 296 trendingDisabled: true, 297 trendingVideoDisabled: true, 298 299 // deer 300 goLinksEnabled: true, 301 constellationEnabled: true, 302 directFetchRecords: true, 303 noAppLabelers: false, 304 noDiscoverFallback: false, 305 repostCarouselEnabled: false, 306 alsoLikedFeedEnabled: true, 307 constellationInstance: 'https://constellation.microcosm.blue/', 308 showLinkInHandle: true, 309 showLinkInHandleOnlyOnWorkingLinks: true, 310 hideFeedsPromoTab: false, 311 disableViaRepostNotification: false, 312 disableComposerPrompt: true, 313 disableTopOfFeedButton: false, 314 disableLikesMetrics: false, 315 disableRepostsMetrics: false, 316 disableQuotesMetrics: false, 317 disableSavesMetrics: false, 318 disableReplyMetrics: false, 319 disableFollowersMetrics: false, 320 disableFollowingMetrics: false, 321 disableFollowedByMetrics: false, 322 disablePostsMetrics: false, 323 showFollowsYouBadge: false, 324 hideSimilarAccountsRecomm: true, 325 hideScaryFollowButtons: false, 326 discoverContextEnabled: false, 327 enableSquareAvatars: true, 328 enableSquareButtons: true, 329 disableVerifyEmailReminder: false, 330 showViaClient: true, 331 deerVerification: { 332 enabled: false, 333 // https://witchsky.app/profile/did:plc:p2cp5gopk7mgjegy6wadk3ep/post/3lndyqyyr4k2k 334 // using https://bverified.vercel.app/trusted as a source 335 trusted: [ 336 'did:plc:z72i7hdynmk6r22z27h6tvur', 337 'did:plc:b2kutgxqlltwc6lhs724cfwr', 338 'did:plc:inz4fkbbp7ms3ixufw6xuvdi', 339 'did:plc:eclio37ymobqex2ncko63h4r', 340 'did:plc:dzezcmpb3fhcpns4n4xm4ur5', 341 'did:plc:5u54z2qgkq43dh2nzwzdbbhb', 342 'did:plc:wmho6q2uiyktkam3jsvrms3s', 343 'did:plc:sqbswn3lalcc2dlh2k7zdpuw', 344 'did:plc:k5nskatzhyxersjilvtnz4lh', 345 'did:plc:d2jith367s6ybc3ldsusgdae', 346 'did:plc:y3xrmnwvkvsq4tqcsgwch4na', 347 'did:plc:i3fhjvvkbmirhyu4aeihhrnv', 348 'did:plc:fivojrvylkim4nuo3pfqcf3k', 349 'did:plc:ofbkqcjzvm6gtwuufsubnkaf', 350 'did:plc:xwqgusybtrpm67tcwqdfmzvy', 351 'did:plc:oxo226vi7t2btjokm2buusoy', 352 'did:plc:r4ve5hjtfjubdwrvlxcad62e', 353 'did:plc:j4eroku3volozvv6ljsnnfec', 354 'did:plc:6q2thhy2ohzog26mmqm4pffk', 355 'did:plc:rk25gdgk3cnnmtkvlae265nz', 356 ], 357 }, 358 highQualityImages: false, 359 imageCdnHost: 'https://cdn.bsky.app', 360 plcDirectory: 'https://plc.directory', 361 hideUnreplyablePosts: false, 362 pdsLabel: { 363 enabled: true, 364 hideBskyPds: true, 365 }, 366 faviconService: 'https://twenty-icons.com/(pds)', 367 showExternalShareButtons: false, 368 translationServicePreference: 'google', 369 libreTranslateInstance: 'https://libretranslate.com/', 370 371 openRouterApiKey: undefined, 372 openRouterModel: DEFAULT_ALT_TEXT_AI_MODEL, 373 openRouterPrompt: undefined, 374 375 useHandleInLinks: false, 376 377 postReplacement: { 378 enabled: false, 379 postName: 'skeet', 380 postsName: 'skeets', 381 }, 382 383 autoLikeOnRepost: false, 384 omitViaField: false, 385} 386 387export function tryParse(rawData: string): Schema | undefined { 388 let objData 389 try { 390 objData = JSON.parse(rawData) 391 } catch (e) { 392 logger.error('persisted state: failed to parse root state from storage', { 393 message: e, 394 }) 395 } 396 if (!objData) { 397 return undefined 398 } 399 const parsed = schema.safeParse(objData) 400 if (parsed.success) { 401 return objData 402 } else { 403 const errors = 404 parsed.error?.errors?.map(e => ({ 405 code: e.code, 406 // @ts-ignore exists on some types 407 expected: e?.expected, 408 path: e.path?.join('.'), 409 })) || [] 410 logger.error(`persisted store: data failed validation on read`, {errors}) 411 return undefined 412 } 413} 414 415export function tryStringify(value: Schema): string | undefined { 416 try { 417 schema.parse(value) 418 return JSON.stringify(value) 419 } catch (e) { 420 logger.error(`persisted state: failed stringifying root state`, { 421 message: e, 422 }) 423 return undefined 424 } 425}