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

Configure Feed

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

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