Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Remove deprecated models and mobx usage (react-query refactor) (#1934)

* Update login page to use service query

* Update modal to use session instead of store

* Move image sizes cache off store

* Update settings to no longer use store

* Update link-meta fetch to use agent instead of rootstore

* Remove deprecated resolveName()

* Delete deprecated link-metas cache

* Delete deprecated posts cache

* Delete all remaining mobx models, including the root store

* Strip out unused mobx observer wrappers

authored by

Paul Frazee and committed by
GitHub
54faa7e1 e637798e

+1044 -1901
+12 -25
src/App.native.tsx
··· 5 5 import {RootSiblingParent} from 'react-native-root-siblings' 6 6 import * as SplashScreen from 'expo-splash-screen' 7 7 import {GestureHandlerRootView} from 'react-native-gesture-handler' 8 - import {observer} from 'mobx-react-lite' 9 8 import {QueryClientProvider} from '@tanstack/react-query' 10 9 11 10 import 'view/icons' ··· 16 15 import {useColorMode} from 'state/shell' 17 16 import {ThemeProvider} from 'lib/ThemeContext' 18 17 import {s} from 'lib/styles' 19 - import {RootStoreModel, setupState, RootStoreProvider} from './state' 20 18 import {Shell} from 'view/shell' 21 19 import * as notifications from 'lib/notifications/notifications' 22 20 import * as analytics from 'lib/analytics/analytics' ··· 44 42 45 43 SplashScreen.preventAutoHideAsync() 46 44 47 - const InnerApp = observer(function AppImpl() { 45 + function InnerApp() { 48 46 const colorMode = useColorMode() 49 47 const {isInitialLoad} = useSession() 50 48 const {resumeSession} = useSessionApi() 51 - const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( 52 - undefined, 53 - ) 54 49 55 50 // init 56 51 useEffect(() => { 57 - setupState().then(store => { 58 - setRootStore(store) 59 - }) 60 - }, []) 61 - 62 - useEffect(() => { 63 52 initReminders() 64 53 analytics.init() 65 54 notifications.init(queryClient) ··· 72 61 }, [resumeSession]) 73 62 74 63 // show nothing prior to init 75 - if (!rootStore || isInitialLoad) { 64 + if (isInitialLoad) { 76 65 // TODO add a loading state 77 66 return null 78 67 } ··· 85 74 <UnreadNotifsProvider> 86 75 <ThemeProvider theme={colorMode}> 87 76 <analytics.Provider> 88 - <RootStoreProvider value={rootStore}> 89 - <I18nProvider i18n={i18n}> 90 - {/* All components should be within this provider */} 91 - <RootSiblingParent> 92 - <GestureHandlerRootView style={s.h100pct}> 93 - <TestCtrls /> 94 - <Shell /> 95 - </GestureHandlerRootView> 96 - </RootSiblingParent> 97 - </I18nProvider> 98 - </RootStoreProvider> 77 + <I18nProvider i18n={i18n}> 78 + {/* All components should be within this provider */} 79 + <RootSiblingParent> 80 + <GestureHandlerRootView style={s.h100pct}> 81 + <TestCtrls /> 82 + <Shell /> 83 + </GestureHandlerRootView> 84 + </RootSiblingParent> 85 + </I18nProvider> 99 86 </analytics.Provider> 100 87 </ThemeProvider> 101 88 </UnreadNotifsProvider> 102 89 ) 103 - }) 90 + } 104 91 105 92 function App() { 106 93 const [isReady, setReady] = useState(false)
+12 -25
src/App.web.tsx
··· 1 1 import 'lib/sentry' // must be near top 2 2 3 3 import React, {useState, useEffect} from 'react' 4 - import {observer} from 'mobx-react-lite' 5 4 import {QueryClientProvider} from '@tanstack/react-query' 6 5 import {SafeAreaProvider} from 'react-native-safe-area-context' 7 6 import {RootSiblingParent} from 'react-native-root-siblings' ··· 12 11 import {init as initReminders} from '#/state/shell/reminders' 13 12 import {useColorMode} from 'state/shell' 14 13 import * as analytics from 'lib/analytics/analytics' 15 - import {RootStoreModel, setupState, RootStoreProvider} from './state' 16 14 import {Shell} from 'view/shell/index' 17 15 import {ToastContainer} from 'view/com/util/Toast.web' 18 16 import {ThemeProvider} from 'lib/ThemeContext' ··· 34 32 import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' 35 33 import * as persisted from '#/state/persisted' 36 34 37 - const InnerApp = observer(function AppImpl() { 35 + function InnerApp() { 38 36 const {isInitialLoad} = useSession() 39 37 const {resumeSession} = useSessionApi() 40 38 const colorMode = useColorMode() 41 - const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( 42 - undefined, 43 - ) 44 39 45 40 // init 46 41 useEffect(() => { 47 - setupState().then(store => { 48 - setRootStore(store) 49 - }) 50 - }, []) 51 - 52 - useEffect(() => { 53 42 initReminders() 54 43 analytics.init() 55 44 dynamicActivate(defaultLocale) // async import of locale data ··· 59 48 }, [resumeSession]) 60 49 61 50 // show nothing prior to init 62 - if (!rootStore || isInitialLoad) { 51 + if (isInitialLoad) { 63 52 // TODO add a loading state 64 53 return null 65 54 } ··· 72 61 <UnreadNotifsProvider> 73 62 <ThemeProvider theme={colorMode}> 74 63 <analytics.Provider> 75 - <RootStoreProvider value={rootStore}> 76 - <I18nProvider i18n={i18n}> 77 - {/* All components should be within this provider */} 78 - <RootSiblingParent> 79 - <SafeAreaProvider> 80 - <Shell /> 81 - </SafeAreaProvider> 82 - </RootSiblingParent> 83 - </I18nProvider> 84 - <ToastContainer /> 85 - </RootStoreProvider> 64 + <I18nProvider i18n={i18n}> 65 + {/* All components should be within this provider */} 66 + <RootSiblingParent> 67 + <SafeAreaProvider> 68 + <Shell /> 69 + </SafeAreaProvider> 70 + </RootSiblingParent> 71 + </I18nProvider> 72 + <ToastContainer /> 86 73 </analytics.Provider> 87 74 </ThemeProvider> 88 75 </UnreadNotifsProvider> 89 76 ) 90 - }) 77 + } 91 78 92 79 function App() { 93 80 const [isReady, setReady] = useState(false)
+15 -3
src/lib/analytics/analytics.tsx
··· 7 7 useAnalytics as useAnalyticsOrig, 8 8 ClientMethods, 9 9 } from '@segment/analytics-react-native' 10 - import {AppInfo} from 'state/models/root-store' 10 + import {z} from 'zod' 11 11 import {useSession} from '#/state/session' 12 12 import {sha256} from 'js-sha256' 13 13 import {ScreenEvent, TrackEvent} from './types' 14 14 import {logger} from '#/logger' 15 15 import {listenSessionLoaded} from '#/state/events' 16 + 17 + export const appInfo = z.object({ 18 + build: z.string().optional(), 19 + name: z.string().optional(), 20 + namespace: z.string().optional(), 21 + version: z.string().optional(), 22 + }) 23 + export type AppInfo = z.infer<typeof appInfo> 16 24 17 25 const segmentClient = createClient({ 18 26 writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', ··· 128 136 await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value)) 129 137 } 130 138 131 - async function readAppInfo(): Promise<Partial<AppInfo> | undefined> { 139 + async function readAppInfo(): Promise<AppInfo | undefined> { 132 140 const rawData = await AsyncStorage.getItem('BSKY_APP_INFO') 133 - return rawData ? JSON.parse(rawData) : undefined 141 + const obj = rawData ? JSON.parse(rawData) : undefined 142 + if (!obj || typeof obj !== 'object') { 143 + return undefined 144 + } 145 + return obj 134 146 }
-28
src/lib/api/index.ts
··· 10 10 RichText, 11 11 } from '@atproto/api' 12 12 import {AtUri} from '@atproto/api' 13 - import {RootStoreModel} from 'state/models/root-store' 14 13 import {isNetworkError} from 'lib/strings/errors' 15 14 import {LinkMeta} from '../link-meta/link-meta' 16 15 import {isWeb} from 'platform/detection' ··· 24 23 meta?: LinkMeta 25 24 embed?: AppBskyEmbedRecord.Main 26 25 localThumb?: ImageModel 27 - } 28 - 29 - export async function resolveName(store: RootStoreModel, didOrHandle: string) { 30 - if (!didOrHandle) { 31 - throw new Error('Invalid handle: ""') 32 - } 33 - if (didOrHandle.startsWith('did:')) { 34 - return didOrHandle 35 - } 36 - 37 - // we run the resolution always to ensure freshness 38 - const promise = store.agent 39 - .resolveHandle({ 40 - handle: didOrHandle, 41 - }) 42 - .then(res => { 43 - store.handleResolutions.cache.set(didOrHandle, res.data.did) 44 - return res.data.did 45 - }) 46 - 47 - // but we can return immediately if it's cached 48 - const cached = store.handleResolutions.cache.get(didOrHandle) 49 - if (cached) { 50 - return cached 51 - } 52 - 53 - return promise 54 26 } 55 27 56 28 export async function uploadBlob(
-68
src/lib/async/revertible.ts
··· 1 - import {runInAction} from 'mobx' 2 - import {deepObserve} from 'mobx-utils' 3 - import set from 'lodash.set' 4 - 5 - const ongoingActions = new Set<any>() 6 - 7 - /** 8 - * This is a TypeScript function that optimistically updates data on the client-side before sending a 9 - * request to the server and rolling back changes if the request fails. 10 - * @param {T} model - The object or record that needs to be updated optimistically. 11 - * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It 12 - * can be used to perform any necessary actions or updates on the model or UI before the server update 13 - * is initiated. 14 - * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server 15 - * update operation. This function is called after the previous state of the model has been recorded 16 - * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate` 17 - * function is called with the result 18 - * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the 19 - * server update is successful. It takes in the response from the server update as its parameter. If 20 - * this parameter is not provided, nothing will happen after the server update. 21 - * @returns A Promise that resolves to `void`. 22 - */ 23 - export const updateDataOptimistically = async < 24 - T extends Record<string, any>, 25 - U, 26 - >( 27 - model: T, 28 - preUpdate: () => void, 29 - serverUpdate: () => Promise<U>, 30 - postUpdate?: (res: U) => void, 31 - ): Promise<void> => { 32 - if (ongoingActions.has(model)) { 33 - return 34 - } 35 - ongoingActions.add(model) 36 - 37 - const prevState: Map<string, any> = new Map<string, any>() 38 - const dispose = deepObserve(model, (change, path) => { 39 - if (change.observableKind === 'object') { 40 - if (change.type === 'update') { 41 - prevState.set( 42 - [path, change.name].filter(Boolean).join('.'), 43 - change.oldValue, 44 - ) 45 - } else if (change.type === 'add') { 46 - prevState.set([path, change.name].filter(Boolean).join('.'), undefined) 47 - } 48 - } 49 - }) 50 - preUpdate() 51 - dispose() 52 - 53 - try { 54 - const res = await serverUpdate() 55 - runInAction(() => { 56 - postUpdate?.(res) 57 - }) 58 - } catch (error) { 59 - runInAction(() => { 60 - prevState.forEach((value, path) => { 61 - set(model, path, value) 62 - }) 63 - }) 64 - throw error 65 - } finally { 66 - ongoingActions.delete(model) 67 - } 68 - }
+34
src/lib/media/image-sizes.ts
··· 1 + import {Image} from 'react-native' 2 + import type {Dimensions} from 'lib/media/types' 3 + 4 + const sizes: Map<string, Dimensions> = new Map() 5 + const activeRequests: Map<string, Promise<Dimensions>> = new Map() 6 + 7 + export function get(uri: string): Dimensions | undefined { 8 + return sizes.get(uri) 9 + } 10 + 11 + export async function fetch(uri: string): Promise<Dimensions> { 12 + const Dimensions = sizes.get(uri) 13 + if (Dimensions) { 14 + return Dimensions 15 + } 16 + 17 + const prom = 18 + activeRequests.get(uri) || 19 + new Promise<Dimensions>(resolve => { 20 + Image.getSize( 21 + uri, 22 + (width: number, height: number) => resolve({width, height}), 23 + (err: any) => { 24 + console.error('Failed to fetch image dimensions for', uri, err) 25 + resolve({width: 0, height: 0}) 26 + }, 27 + ) 28 + }) 29 + activeRequests.set(uri, prom) 30 + const res = await prom 31 + activeRequests.delete(uri) 32 + sizes.set(uri, res) 33 + return res 34 + }
+1 -1
src/lib/strings/url-helpers.ts
··· 1 1 import {AtUri} from '@atproto/api' 2 - import {PROD_SERVICE} from 'state/index' 2 + import {PROD_SERVICE} from 'lib/constants' 3 3 import TLDs from 'tlds' 4 4 import psl from 'psl' 5 5
-54
src/state/index.ts
··· 1 - import {autorun} from 'mobx' 2 - import {AppState, Platform} from 'react-native' 3 - import {BskyAgent} from '@atproto/api' 4 - import {RootStoreModel} from './models/root-store' 5 - import * as apiPolyfill from 'lib/api/api-polyfill' 6 - import * as storage from 'lib/storage' 7 - import {logger} from '#/logger' 8 - 9 - export const LOCAL_DEV_SERVICE = 10 - Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' 11 - export const STAGING_SERVICE = 'https://staging.bsky.dev' 12 - export const PROD_SERVICE = 'https://bsky.social' 13 - export const DEFAULT_SERVICE = PROD_SERVICE 14 - const ROOT_STATE_STORAGE_KEY = 'root' 15 - const STATE_FETCH_INTERVAL = 15e3 16 - 17 - export async function setupState(serviceUri = DEFAULT_SERVICE) { 18 - let rootStore: RootStoreModel 19 - let data: any 20 - 21 - apiPolyfill.doPolyfill() 22 - 23 - rootStore = new RootStoreModel(new BskyAgent({service: serviceUri})) 24 - try { 25 - data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} 26 - logger.debug('Initial hydrate', {hasSession: !!data.session}) 27 - rootStore.hydrate(data) 28 - } catch (e: any) { 29 - logger.error('Failed to load state from storage', {error: e}) 30 - } 31 - rootStore.attemptSessionResumption() 32 - 33 - // track changes & save to storage 34 - autorun(() => { 35 - const snapshot = rootStore.serialize() 36 - storage.save(ROOT_STATE_STORAGE_KEY, snapshot) 37 - }) 38 - 39 - // periodic state fetch 40 - setInterval(() => { 41 - // NOTE 42 - // this must ONLY occur when the app is active, as the bg-fetch handler 43 - // will wake up the thread and cause this interval to fire, which in 44 - // turn schedules a bunch of work at a poor time 45 - // -prf 46 - if (AppState.currentState === 'active') { 47 - rootStore.updateSessionState() 48 - } 49 - }, STATE_FETCH_INTERVAL) 50 - 51 - return rootStore 52 - } 53 - 54 - export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store'
-5
src/state/models/cache/handle-resolutions.ts
··· 1 - import {LRUMap} from 'lru_map' 2 - 3 - export class HandleResolutionsCache { 4 - cache: LRUMap<string, string> = new LRUMap(500) 5 - }
-38
src/state/models/cache/image-sizes.ts
··· 1 - import {Image} from 'react-native' 2 - import type {Dimensions} from 'lib/media/types' 3 - 4 - export class ImageSizesCache { 5 - sizes: Map<string, Dimensions> = new Map() 6 - activeRequests: Map<string, Promise<Dimensions>> = new Map() 7 - 8 - constructor() {} 9 - 10 - get(uri: string): Dimensions | undefined { 11 - return this.sizes.get(uri) 12 - } 13 - 14 - async fetch(uri: string): Promise<Dimensions> { 15 - const Dimensions = this.sizes.get(uri) 16 - if (Dimensions) { 17 - return Dimensions 18 - } 19 - 20 - const prom = 21 - this.activeRequests.get(uri) || 22 - new Promise<Dimensions>(resolve => { 23 - Image.getSize( 24 - uri, 25 - (width: number, height: number) => resolve({width, height}), 26 - (err: any) => { 27 - console.error('Failed to fetch image dimensions for', uri, err) 28 - resolve({width: 0, height: 0}) 29 - }, 30 - ) 31 - }) 32 - this.activeRequests.set(uri, prom) 33 - const res = await prom 34 - this.activeRequests.delete(uri) 35 - this.sizes.set(uri, res) 36 - return res 37 - } 38 - }
-70
src/state/models/cache/posts.ts
··· 1 - import {LRUMap} from 'lru_map' 2 - import {RootStoreModel} from '../root-store' 3 - import { 4 - AppBskyFeedDefs, 5 - AppBskyEmbedRecord, 6 - AppBskyEmbedRecordWithMedia, 7 - AppBskyFeedPost, 8 - } from '@atproto/api' 9 - 10 - type PostView = AppBskyFeedDefs.PostView 11 - 12 - export class PostsCache { 13 - cache: LRUMap<string, PostView> = new LRUMap(500) 14 - 15 - constructor(public rootStore: RootStoreModel) {} 16 - 17 - set(uri: string, postView: PostView) { 18 - this.cache.set(uri, postView) 19 - if (postView.author.handle) { 20 - this.rootStore.handleResolutions.cache.set( 21 - postView.author.handle, 22 - postView.author.did, 23 - ) 24 - } 25 - } 26 - 27 - fromFeedItem(feedItem: AppBskyFeedDefs.FeedViewPost) { 28 - this.set(feedItem.post.uri, feedItem.post) 29 - if ( 30 - feedItem.reply?.parent && 31 - AppBskyFeedDefs.isPostView(feedItem.reply?.parent) 32 - ) { 33 - this.set(feedItem.reply.parent.uri, feedItem.reply.parent) 34 - } 35 - const embed = feedItem.post.embed 36 - if ( 37 - AppBskyEmbedRecord.isView(embed) && 38 - AppBskyEmbedRecord.isViewRecord(embed.record) && 39 - AppBskyFeedPost.isRecord(embed.record.value) && 40 - AppBskyFeedPost.validateRecord(embed.record.value).success 41 - ) { 42 - this.set(embed.record.uri, embedViewToPostView(embed.record)) 43 - } 44 - if ( 45 - AppBskyEmbedRecordWithMedia.isView(embed) && 46 - AppBskyEmbedRecord.isViewRecord(embed.record?.record) && 47 - AppBskyFeedPost.isRecord(embed.record.record.value) && 48 - AppBskyFeedPost.validateRecord(embed.record.record.value).success 49 - ) { 50 - this.set( 51 - embed.record.record.uri, 52 - embedViewToPostView(embed.record.record), 53 - ) 54 - } 55 - } 56 - } 57 - 58 - function embedViewToPostView( 59 - embedView: AppBskyEmbedRecord.ViewRecord, 60 - ): PostView { 61 - return { 62 - $type: 'app.bsky.feed.post#view', 63 - uri: embedView.uri, 64 - cid: embedView.cid, 65 - author: embedView.author, 66 - record: embedView.value, 67 - indexedAt: embedView.indexedAt, 68 - labels: embedView.labels, 69 - } 70 - }
-50
src/state/models/cache/profiles-view.ts
··· 1 - import {makeAutoObservable} from 'mobx' 2 - import {LRUMap} from 'lru_map' 3 - import {RootStoreModel} from '../root-store' 4 - import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' 5 - 6 - type CacheValue = Promise<GetProfile.Response> | GetProfile.Response 7 - export class ProfilesCache { 8 - cache: LRUMap<string, CacheValue> = new LRUMap(100) 9 - 10 - constructor(public rootStore: RootStoreModel) { 11 - makeAutoObservable( 12 - this, 13 - { 14 - rootStore: false, 15 - cache: false, 16 - }, 17 - {autoBind: true}, 18 - ) 19 - } 20 - 21 - // public api 22 - // = 23 - 24 - async getProfile(did: string) { 25 - const cached = this.cache.get(did) 26 - if (cached) { 27 - try { 28 - return await cached 29 - } catch (e) { 30 - // ignore, we'll try again 31 - } 32 - } 33 - try { 34 - const promise = this.rootStore.agent.getProfile({ 35 - actor: did, 36 - }) 37 - this.cache.set(did, promise) 38 - const res = await promise 39 - this.cache.set(did, res) 40 - return res 41 - } catch (e) { 42 - this.cache.delete(did) 43 - throw e 44 - } 45 - } 46 - 47 - overwrite(did: string, res: GetProfile.Response) { 48 - this.cache.set(did, res) 49 - } 50 - }
-115
src/state/models/me.ts
··· 1 - import {makeAutoObservable, runInAction} from 'mobx' 2 - import {RootStoreModel} from './root-store' 3 - import {isObj, hasProp} from 'lib/type-guards' 4 - import {logger} from '#/logger' 5 - 6 - const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min 7 - 8 - export class MeModel { 9 - did: string = '' 10 - handle: string = '' 11 - displayName: string = '' 12 - description: string = '' 13 - avatar: string = '' 14 - followsCount: number | undefined 15 - followersCount: number | undefined 16 - lastProfileStateUpdate = Date.now() 17 - 18 - constructor(public rootStore: RootStoreModel) { 19 - makeAutoObservable( 20 - this, 21 - {rootStore: false, serialize: false, hydrate: false}, 22 - {autoBind: true}, 23 - ) 24 - } 25 - 26 - clear() { 27 - this.rootStore.profiles.cache.clear() 28 - this.rootStore.posts.cache.clear() 29 - this.did = '' 30 - this.handle = '' 31 - this.displayName = '' 32 - this.description = '' 33 - this.avatar = '' 34 - } 35 - 36 - serialize(): unknown { 37 - return { 38 - did: this.did, 39 - handle: this.handle, 40 - displayName: this.displayName, 41 - description: this.description, 42 - avatar: this.avatar, 43 - } 44 - } 45 - 46 - hydrate(v: unknown) { 47 - if (isObj(v)) { 48 - let did, handle, displayName, description, avatar 49 - if (hasProp(v, 'did') && typeof v.did === 'string') { 50 - did = v.did 51 - } 52 - if (hasProp(v, 'handle') && typeof v.handle === 'string') { 53 - handle = v.handle 54 - } 55 - if (hasProp(v, 'displayName') && typeof v.displayName === 'string') { 56 - displayName = v.displayName 57 - } 58 - if (hasProp(v, 'description') && typeof v.description === 'string') { 59 - description = v.description 60 - } 61 - if (hasProp(v, 'avatar') && typeof v.avatar === 'string') { 62 - avatar = v.avatar 63 - } 64 - if (did && handle) { 65 - this.did = did 66 - this.handle = handle 67 - this.displayName = displayName || '' 68 - this.description = description || '' 69 - this.avatar = avatar || '' 70 - } 71 - } 72 - } 73 - 74 - async load() { 75 - const sess = this.rootStore.session 76 - logger.debug('MeModel:load', {hasSession: sess.hasSession}) 77 - if (sess.hasSession) { 78 - this.did = sess.currentSession?.did || '' 79 - await this.fetchProfile() 80 - this.rootStore.emitSessionLoaded() 81 - } else { 82 - this.clear() 83 - } 84 - } 85 - 86 - async updateIfNeeded() { 87 - if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) { 88 - logger.debug('Updating me profile information') 89 - this.lastProfileStateUpdate = Date.now() 90 - await this.fetchProfile() 91 - } 92 - } 93 - 94 - async fetchProfile() { 95 - const profile = await this.rootStore.agent.getProfile({ 96 - actor: this.did, 97 - }) 98 - runInAction(() => { 99 - if (profile?.data) { 100 - this.displayName = profile.data.displayName || '' 101 - this.description = profile.data.description || '' 102 - this.avatar = profile.data.avatar || '' 103 - this.handle = profile.data.handle || '' 104 - this.followsCount = profile.data.followsCount 105 - this.followersCount = profile.data.followersCount 106 - } else { 107 - this.displayName = '' 108 - this.description = '' 109 - this.avatar = '' 110 - this.followsCount = profile.data.followsCount 111 - this.followersCount = undefined 112 - } 113 - }) 114 - } 115 - }
-207
src/state/models/root-store.ts
··· 1 - /** 2 - * The root store is the base of all modeled state. 3 - */ 4 - 5 - import {makeAutoObservable} from 'mobx' 6 - import {BskyAgent} from '@atproto/api' 7 - import {createContext, useContext} from 'react' 8 - import {DeviceEventEmitter, EmitterSubscription} from 'react-native' 9 - import {z} from 'zod' 10 - import {isObj, hasProp} from 'lib/type-guards' 11 - import {SessionModel} from './session' 12 - import {ShellUiModel} from './ui/shell' 13 - import {HandleResolutionsCache} from './cache/handle-resolutions' 14 - import {ProfilesCache} from './cache/profiles-view' 15 - import {PostsCache} from './cache/posts' 16 - import {LinkMetasCache} from './cache/link-metas' 17 - import {MeModel} from './me' 18 - import {resetToTab} from '../../Navigation' 19 - import {ImageSizesCache} from './cache/image-sizes' 20 - import {reset as resetNavigation} from '../../Navigation' 21 - import {logger} from '#/logger' 22 - 23 - // TEMPORARY (APP-700) 24 - // remove after backend testing finishes 25 - // -prf 26 - import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header' 27 - 28 - export const appInfo = z.object({ 29 - build: z.string(), 30 - name: z.string(), 31 - namespace: z.string(), 32 - version: z.string(), 33 - }) 34 - export type AppInfo = z.infer<typeof appInfo> 35 - 36 - export class RootStoreModel { 37 - agent: BskyAgent 38 - appInfo?: AppInfo 39 - session = new SessionModel(this) 40 - shell = new ShellUiModel(this) 41 - me = new MeModel(this) 42 - handleResolutions = new HandleResolutionsCache() 43 - profiles = new ProfilesCache(this) 44 - posts = new PostsCache(this) 45 - linkMetas = new LinkMetasCache(this) 46 - imageSizes = new ImageSizesCache() 47 - 48 - constructor(agent: BskyAgent) { 49 - this.agent = agent 50 - makeAutoObservable(this, { 51 - agent: false, 52 - serialize: false, 53 - hydrate: false, 54 - }) 55 - } 56 - 57 - setAppInfo(info: AppInfo) { 58 - this.appInfo = info 59 - } 60 - 61 - serialize(): unknown { 62 - return { 63 - appInfo: this.appInfo, 64 - me: this.me.serialize(), 65 - } 66 - } 67 - 68 - hydrate(v: unknown) { 69 - if (isObj(v)) { 70 - if (hasProp(v, 'appInfo')) { 71 - const appInfoParsed = appInfo.safeParse(v.appInfo) 72 - if (appInfoParsed.success) { 73 - this.setAppInfo(appInfoParsed.data) 74 - } 75 - } 76 - if (hasProp(v, 'me')) { 77 - this.me.hydrate(v.me) 78 - } 79 - } 80 - } 81 - 82 - /** 83 - * Called during init to resume any stored session. 84 - */ 85 - async attemptSessionResumption() {} 86 - 87 - /** 88 - * Called by the session model. Refreshes session-oriented state. 89 - */ 90 - async handleSessionChange( 91 - agent: BskyAgent, 92 - {hadSession}: {hadSession: boolean}, 93 - ) { 94 - logger.debug('RootStoreModel:handleSessionChange') 95 - this.agent = agent 96 - applyDebugHeader(this.agent) 97 - this.me.clear() 98 - await this.me.load() 99 - if (!hadSession) { 100 - await resetNavigation() 101 - } 102 - this.emitSessionReady() 103 - } 104 - 105 - /** 106 - * Called by the session model. Handles session drops by informing the user. 107 - */ 108 - async handleSessionDrop() { 109 - logger.debug('RootStoreModel:handleSessionDrop') 110 - resetToTab('HomeTab') 111 - this.me.clear() 112 - this.emitSessionDropped() 113 - } 114 - 115 - /** 116 - * Clears all session-oriented state, previously called on LOGOUT 117 - */ 118 - clearAllSessionState() { 119 - logger.debug('RootStoreModel:clearAllSessionState') 120 - resetToTab('HomeTab') 121 - this.me.clear() 122 - } 123 - 124 - /** 125 - * Periodic poll for new session state. 126 - */ 127 - async updateSessionState() { 128 - if (!this.session.hasSession) { 129 - return 130 - } 131 - try { 132 - await this.me.updateIfNeeded() 133 - } catch (e: any) { 134 - logger.error('Failed to fetch latest state', {error: e}) 135 - } 136 - } 137 - 138 - // global event bus 139 - // = 140 - // - some events need to be passed around between views and models 141 - // in order to keep state in sync; these methods are for that 142 - 143 - // a post was deleted by the local user 144 - onPostDeleted(handler: (uri: string) => void): EmitterSubscription { 145 - return DeviceEventEmitter.addListener('post-deleted', handler) 146 - } 147 - emitPostDeleted(uri: string) { 148 - DeviceEventEmitter.emit('post-deleted', uri) 149 - } 150 - 151 - // a list was deleted by the local user 152 - onListDeleted(handler: (uri: string) => void): EmitterSubscription { 153 - return DeviceEventEmitter.addListener('list-deleted', handler) 154 - } 155 - emitListDeleted(uri: string) { 156 - DeviceEventEmitter.emit('list-deleted', uri) 157 - } 158 - 159 - // the session has started and been fully hydrated 160 - onSessionLoaded(handler: () => void): EmitterSubscription { 161 - return DeviceEventEmitter.addListener('session-loaded', handler) 162 - } 163 - emitSessionLoaded() { 164 - DeviceEventEmitter.emit('session-loaded') 165 - } 166 - 167 - // the session has completed all setup; good for post-initialization behaviors like triggering modals 168 - onSessionReady(handler: () => void): EmitterSubscription { 169 - return DeviceEventEmitter.addListener('session-ready', handler) 170 - } 171 - emitSessionReady() { 172 - DeviceEventEmitter.emit('session-ready') 173 - } 174 - 175 - // the session was dropped due to bad/expired refresh tokens 176 - onSessionDropped(handler: () => void): EmitterSubscription { 177 - return DeviceEventEmitter.addListener('session-dropped', handler) 178 - } 179 - emitSessionDropped() { 180 - DeviceEventEmitter.emit('session-dropped') 181 - } 182 - 183 - // the current screen has changed 184 - // TODO is this still needed? 185 - onNavigation(handler: () => void): EmitterSubscription { 186 - return DeviceEventEmitter.addListener('navigation', handler) 187 - } 188 - emitNavigation() { 189 - DeviceEventEmitter.emit('navigation') 190 - } 191 - 192 - // a "soft reset" typically means scrolling to top and loading latest 193 - // but it can depend on the screen 194 - onScreenSoftReset(handler: () => void): EmitterSubscription { 195 - return DeviceEventEmitter.addListener('screen-soft-reset', handler) 196 - } 197 - emitScreenSoftReset() { 198 - DeviceEventEmitter.emit('screen-soft-reset') 199 - } 200 - } 201 - 202 - const throwawayInst = new RootStoreModel( 203 - new BskyAgent({service: 'http://localhost'}), 204 - ) // this will be replaced by the loader, we just need to supply a value at init 205 - const RootStoreContext = createContext<RootStoreModel>(throwawayInst) 206 - export const RootStoreProvider = RootStoreContext.Provider 207 - export const useStores = () => useContext(RootStoreContext)
-43
src/state/models/session.ts
··· 1 - import {makeAutoObservable} from 'mobx' 2 - import { 3 - BskyAgent, 4 - ComAtprotoServerDescribeServer as DescribeServer, 5 - } from '@atproto/api' 6 - import {RootStoreModel} from './root-store' 7 - 8 - export type ServiceDescription = DescribeServer.OutputSchema 9 - 10 - export class SessionModel { 11 - data: any = {} 12 - 13 - constructor(public rootStore: RootStoreModel) { 14 - makeAutoObservable(this, { 15 - rootStore: false, 16 - hasSession: false, 17 - }) 18 - } 19 - 20 - get currentSession(): any { 21 - return undefined 22 - } 23 - 24 - get hasSession() { 25 - return false 26 - } 27 - 28 - clear() {} 29 - 30 - /** 31 - * Helper to fetch the accounts config settings from an account. 32 - */ 33 - async describeService(service: string): Promise<ServiceDescription> { 34 - const agent = new BskyAgent({service}) 35 - const res = await agent.com.atproto.server.describeServer({}) 36 - return res.data 37 - } 38 - 39 - /** 40 - * Reloads the session from the server. Useful when account details change, like the handle. 41 - */ 42 - async reloadFromServer() {} 43 - }
-32
src/state/models/ui/shell.ts
··· 1 - import {RootStoreModel} from '../root-store' 2 - import {makeAutoObservable} from 'mobx' 3 - import { 4 - shouldRequestEmailConfirmation, 5 - setEmailConfirmationRequested, 6 - } from '#/state/shell/reminders' 7 - import {unstable__openModal} from '#/state/modals' 8 - 9 - export type ColorMode = 'system' | 'light' | 'dark' 10 - 11 - export function isColorMode(v: unknown): v is ColorMode { 12 - return v === 'system' || v === 'light' || v === 'dark' 13 - } 14 - 15 - export class ShellUiModel { 16 - constructor(public rootStore: RootStoreModel) { 17 - makeAutoObservable(this, { 18 - rootStore: false, 19 - }) 20 - 21 - this.setupLoginModals() 22 - } 23 - 24 - setupLoginModals() { 25 - this.rootStore.onSessionReady(() => { 26 - if (shouldRequestEmailConfirmation(this.rootStore.session)) { 27 - unstable__openModal({name: 'verify-email', showReminder: true}) 28 - setEmailConfirmationRequested() 29 - } 30 - }) 31 - } 32 - }
+1 -3
src/state/shell/reminders.e2e.ts
··· 1 - import {SessionModel} from '../models/session' 2 - 3 - export function shouldRequestEmailConfirmation(_session: SessionModel) { 1 + export function shouldRequestEmailConfirmation() { 4 2 return false 5 3 } 6 4
+2 -3
src/view/com/auth/LoggedOut.tsx
··· 1 1 import React from 'react' 2 2 import {SafeAreaView} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {Login} from 'view/com/auth/login/Login' 5 4 import {CreateAccount} from 'view/com/auth/create/CreateAccount' 6 5 import {ErrorBoundary} from 'view/com/util/ErrorBoundary' ··· 16 15 S_CreateAccount, 17 16 } 18 17 19 - export const LoggedOut = observer(function LoggedOutImpl() { 18 + export function LoggedOut() { 20 19 const pal = usePalette('default') 21 20 const setMinimalShellMode = useSetMinimalShellMode() 22 21 const {screen} = useAnalytics() ··· 58 57 </ErrorBoundary> 59 58 </SafeAreaView> 60 59 ) 61 - }) 60 + }
+2 -3
src/view/com/auth/Onboarding.tsx
··· 1 1 import React from 'react' 2 2 import {SafeAreaView} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 5 4 import {s} from 'lib/styles' 6 5 import {usePalette} from 'lib/hooks/usePalette' ··· 10 9 import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' 11 10 import {useOnboardingState, useOnboardingDispatch} from '#/state/shell' 12 11 13 - export const Onboarding = observer(function OnboardingImpl() { 12 + export function Onboarding() { 14 13 const pal = usePalette('default') 15 14 const setMinimalShellMode = useSetMinimalShellMode() 16 15 const onboardingState = useOnboardingState() ··· 38 37 </ErrorBoundary> 39 38 </SafeAreaView> 40 39 ) 41 - }) 40 + }
+3 -1
src/view/com/auth/create/Policies.tsx
··· 4 4 FontAwesomeIcon, 5 5 FontAwesomeIconStyle, 6 6 } from '@fortawesome/react-native-fontawesome' 7 + import {ComAtprotoServerDescribeServer} from '@atproto/api' 7 8 import {TextLink} from '../../util/Link' 8 9 import {Text} from '../../util/text/Text' 9 10 import {s, colors} from 'lib/styles' 10 - import {ServiceDescription} from 'state/models/session' 11 11 import {usePalette} from 'lib/hooks/usePalette' 12 + 13 + type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 12 14 13 15 export const Policies = ({ 14 16 serviceDescription,
+1 -1
src/view/com/auth/create/Step1.tsx
··· 13 13 import {msg, Trans} from '@lingui/macro' 14 14 import {useLingui} from '@lingui/react' 15 15 16 - import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index' 16 + import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants' 17 17 import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' 18 18 19 19 /** STEP 1: Your hosting provider
+3 -1
src/view/com/auth/login/ForgotPasswordForm.tsx
··· 9 9 FontAwesomeIcon, 10 10 FontAwesomeIconStyle, 11 11 } from '@fortawesome/react-native-fontawesome' 12 + import {ComAtprotoServerDescribeServer} from '@atproto/api' 12 13 import * as EmailValidator from 'email-validator' 13 14 import {BskyAgent} from '@atproto/api' 14 15 import {useAnalytics} from 'lib/analytics/analytics' 15 16 import {Text} from '../../util/text/Text' 16 17 import {s} from 'lib/styles' 17 18 import {toNiceDomain} from 'lib/strings/url-helpers' 18 - import {ServiceDescription} from 'state/models/session' 19 19 import {isNetworkError} from 'lib/strings/errors' 20 20 import {usePalette} from 'lib/hooks/usePalette' 21 21 import {useTheme} from 'lib/ThemeContext' ··· 25 25 import {useLingui} from '@lingui/react' 26 26 import {styles} from './styles' 27 27 import {useModalControls} from '#/state/modals' 28 + 29 + type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 28 30 29 31 export const ForgotPasswordForm = ({ 30 32 error,
+20 -34
src/view/com/auth/login/Login.tsx
··· 2 2 import {KeyboardAvoidingView} from 'react-native' 3 3 import {useAnalytics} from 'lib/analytics/analytics' 4 4 import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' 5 - import {useStores, DEFAULT_SERVICE} from 'state/index' 6 - import {ServiceDescription} from 'state/models/session' 5 + import {DEFAULT_SERVICE} from '#/lib/constants' 7 6 import {usePalette} from 'lib/hooks/usePalette' 8 7 import {logger} from '#/logger' 9 8 import {ChooseAccountForm} from './ChooseAccountForm' ··· 14 13 import {useLingui} from '@lingui/react' 15 14 import {msg} from '@lingui/macro' 16 15 import {useSession, SessionAccount} from '#/state/session' 16 + import {useServiceQuery} from '#/state/queries/service' 17 17 18 18 enum Forms { 19 19 Login, ··· 25 25 26 26 export const Login = ({onPressBack}: {onPressBack: () => void}) => { 27 27 const pal = usePalette('default') 28 - const store = useStores() 29 28 const {accounts} = useSession() 30 29 const {track} = useAnalytics() 31 30 const {_} = useLingui() 32 31 const [error, setError] = useState<string>('') 33 - const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({}) 34 32 const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE) 35 - const [serviceDescription, setServiceDescription] = useState< 36 - ServiceDescription | undefined 37 - >(undefined) 38 33 const [initialHandle, setInitialHandle] = useState<string>('') 39 34 const [currentForm, setCurrentForm] = useState<Forms>( 40 35 accounts.length ? Forms.ChooseAccount : Forms.Login, 41 36 ) 37 + const { 38 + data: serviceDescription, 39 + error: serviceError, 40 + refetch: refetchService, 41 + } = useServiceQuery(serviceUrl) 42 42 43 43 const onSelectAccount = (account?: SessionAccount) => { 44 44 if (account?.service) { ··· 54 54 } 55 55 56 56 useEffect(() => { 57 - let aborted = false 58 - setError('') 59 - store.session.describeService(serviceUrl).then( 60 - desc => { 61 - if (aborted) { 62 - return 63 - } 64 - setServiceDescription(desc) 65 - }, 66 - err => { 67 - if (aborted) { 68 - return 69 - } 70 - logger.warn(`Failed to fetch service description for ${serviceUrl}`, { 71 - error: err, 72 - }) 73 - setError( 74 - _( 75 - msg`Unable to contact your service. Please check your Internet connection.`, 76 - ), 77 - ) 78 - }, 79 - ) 80 - return () => { 81 - aborted = true 57 + if (serviceError) { 58 + setError( 59 + _( 60 + msg`Unable to contact your service. Please check your Internet connection.`, 61 + ), 62 + ) 63 + logger.warn(`Failed to fetch service description for ${serviceUrl}`, { 64 + error: String(serviceError), 65 + }) 66 + } else { 67 + setError('') 82 68 } 83 - }, [store.session, serviceUrl, retryDescribeTrigger, _]) 69 + }, [serviceError, serviceUrl, _]) 84 70 85 - const onPressRetryConnect = () => setRetryDescribeTrigger({}) 71 + const onPressRetryConnect = () => refetchService() 86 72 const onPressForgotPassword = () => { 87 73 track('Signin:PressedForgotPassword') 88 74 setCurrentForm(Forms.ForgotPassword)
+3 -1
src/view/com/auth/login/LoginForm.tsx
··· 10 10 FontAwesomeIcon, 11 11 FontAwesomeIconStyle, 12 12 } from '@fortawesome/react-native-fontawesome' 13 + import {ComAtprotoServerDescribeServer} from '@atproto/api' 13 14 import {useAnalytics} from 'lib/analytics/analytics' 14 15 import {Text} from '../../util/text/Text' 15 16 import {s} from 'lib/styles' 16 17 import {createFullHandle} from 'lib/strings/handles' 17 18 import {toNiceDomain} from 'lib/strings/url-helpers' 18 - import {ServiceDescription} from 'state/models/session' 19 19 import {isNetworkError} from 'lib/strings/errors' 20 20 import {usePalette} from 'lib/hooks/usePalette' 21 21 import {useTheme} from 'lib/ThemeContext' ··· 26 26 import {styles} from './styles' 27 27 import {useLingui} from '@lingui/react' 28 28 import {useModalControls} from '#/state/modals' 29 + 30 + type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 29 31 30 32 export const LoginForm = ({ 31 33 error,
+2 -5
src/view/com/auth/onboarding/RecommendedFeeds.tsx
··· 1 1 import React from 'react' 2 2 import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints' 6 5 import {Text} from 'view/com/util/text/Text' ··· 16 15 type Props = { 17 16 next: () => void 18 17 } 19 - export const RecommendedFeeds = observer(function RecommendedFeedsImpl({ 20 - next, 21 - }: Props) { 18 + export function RecommendedFeeds({next}: Props) { 22 19 const pal = usePalette('default') 23 20 const {isTabletOrMobile} = useWebMediaQueries() 24 21 const {isLoading, data} = useSuggestedFeedsQuery() ··· 146 143 </Mobile> 147 144 </> 148 145 ) 149 - }) 146 + } 150 147 151 148 const tdStyles = StyleSheet.create({ 152 149 container: {
+2 -3
src/view/com/auth/onboarding/RecommendedFeedsItem.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {AppBskyFeedDefs, RichText as BskRichText} from '@atproto/api' 6 5 import {Text} from 'view/com/util/text/Text' ··· 19 18 } from '#/state/queries/preferences' 20 19 import {logger} from '#/logger' 21 20 22 - export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({ 21 + export function RecommendedFeedsItem({ 23 22 item, 24 23 }: { 25 24 item: AppBskyFeedDefs.GeneratorView ··· 164 163 </View> 165 164 </View> 166 165 ) 167 - }) 166 + }
+2 -5
src/view/com/auth/onboarding/RecommendedFollows.tsx
··· 1 1 import React from 'react' 2 2 import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {AppBskyActorDefs, moderateProfile} from '@atproto/api' 6 5 import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints' ··· 19 18 type Props = { 20 19 next: () => void 21 20 } 22 - export const RecommendedFollows = observer(function RecommendedFollowsImpl({ 23 - next, 24 - }: Props) { 21 + export function RecommendedFollows({next}: Props) { 25 22 const pal = usePalette('default') 26 23 const {isTabletOrMobile} = useWebMediaQueries() 27 24 const {data: suggestedFollows, dataUpdatedAt} = useSuggestedFollowsQuery() ··· 211 208 </Mobile> 212 209 </> 213 210 ) 214 - }) 211 + } 215 212 216 213 const tdStyles = StyleSheet.create({ 217 214 container: {
+2 -5
src/view/com/auth/onboarding/WelcomeDesktop.tsx
··· 7 7 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 8 8 import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' 9 9 import {Button} from 'view/com/util/forms/Button' 10 - import {observer} from 'mobx-react-lite' 11 10 12 11 type Props = { 13 12 next: () => void 14 13 skip: () => void 15 14 } 16 15 17 - export const WelcomeDesktop = observer(function WelcomeDesktopImpl({ 18 - next, 19 - }: Props) { 16 + export function WelcomeDesktop({next}: Props) { 20 17 const pal = usePalette('default') 21 18 const horizontal = useMediaQuery({minWidth: 1300}) 22 19 const title = ( ··· 105 102 </View> 106 103 </TitleColumnLayout> 107 104 ) 108 - }) 105 + } 109 106 110 107 const styles = StyleSheet.create({ 111 108 row: {
+2 -6
src/view/com/auth/onboarding/WelcomeMobile.tsx
··· 5 5 import {usePalette} from 'lib/hooks/usePalette' 6 6 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 7 7 import {Button} from 'view/com/util/forms/Button' 8 - import {observer} from 'mobx-react-lite' 9 8 import {ViewHeader} from 'view/com/util/ViewHeader' 10 9 import {Trans} from '@lingui/macro' 11 10 ··· 14 13 skip: () => void 15 14 } 16 15 17 - export const WelcomeMobile = observer(function WelcomeMobileImpl({ 18 - next, 19 - skip, 20 - }: Props) { 16 + export function WelcomeMobile({next, skip}: Props) { 21 17 const pal = usePalette('default') 22 18 23 19 return ( ··· 102 98 /> 103 99 </View> 104 100 ) 105 - }) 101 + } 106 102 107 103 const styles = StyleSheet.create({ 108 104 container: {
+2 -3
src/view/com/auth/withAuthRequired.tsx
··· 5 5 StyleSheet, 6 6 TouchableOpacity, 7 7 } from 'react-native' 8 - import {observer} from 'mobx-react-lite' 9 8 import {CenteredView} from '../util/Views' 10 9 import {LoggedOut} from './LoggedOut' 11 10 import {Onboarding} from './Onboarding' ··· 18 17 export const withAuthRequired = <P extends object>( 19 18 Component: React.ComponentType<P>, 20 19 ): React.FC<P> => 21 - observer(function AuthRequired(props: P) { 20 + function AuthRequired(props: P) { 22 21 const {isInitialLoad, hasSession} = useSession() 23 22 const onboardingState = useOnboardingState() 24 23 if (isInitialLoad) { ··· 31 30 return <Onboarding /> 32 31 } 33 32 return <Component {...props} /> 34 - }) 33 + } 35 34 36 35 function Loading() { 37 36 const pal = usePalette('default')
+2 -3
src/view/com/composer/labels/LabelsBtn.tsx
··· 1 1 import React from 'react' 2 2 import {Keyboard, StyleSheet} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {Button} from 'view/com/util/forms/Button' 5 4 import {usePalette} from 'lib/hooks/usePalette' 6 5 import {ShieldExclamation} from 'lib/icons' ··· 11 10 import {msg} from '@lingui/macro' 12 11 import {useModalControls} from '#/state/modals' 13 12 14 - export const LabelsBtn = observer(function LabelsBtn({ 13 + export function LabelsBtn({ 15 14 labels, 16 15 hasMedia, 17 16 onChange, ··· 49 48 ) : null} 50 49 </Button> 51 50 ) 52 - }) 51 + } 53 52 54 53 const styles = StyleSheet.create({ 55 54 button: {
+2 -3
src/view/com/composer/select-language/SelectLangBtn.tsx
··· 1 1 import React, {useCallback, useMemo} from 'react' 2 2 import {StyleSheet, Keyboard} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import { 5 4 FontAwesomeIcon, 6 5 FontAwesomeIconStyle, ··· 24 23 import {t, msg} from '@lingui/macro' 25 24 import {useLingui} from '@lingui/react' 26 25 27 - export const SelectLangBtn = observer(function SelectLangBtn() { 26 + export function SelectLangBtn() { 28 27 const pal = usePalette('default') 29 28 const {_} = useLingui() 30 29 const {openModal} = useModalControls() ··· 117 116 )} 118 117 </DropdownButton> 119 118 ) 120 - }) 119 + } 121 120 122 121 const styles = StyleSheet.create({ 123 122 button: {
+2 -3
src/view/com/composer/text-input/mobile/Autocomplete.tsx
··· 1 1 import React, {useEffect, useRef} from 'react' 2 2 import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' 5 4 import {usePalette} from 'lib/hooks/usePalette' 6 5 import {Text} from 'view/com/util/text/Text' ··· 10 9 import {Trans} from '@lingui/macro' 11 10 import {AppBskyActorDefs} from '@atproto/api' 12 11 13 - export const Autocomplete = observer(function AutocompleteImpl({ 12 + export function Autocomplete({ 14 13 prefix, 15 14 onSelect, 16 15 }: { ··· 103 102 ) : null} 104 103 </Animated.View> 105 104 ) 106 - }) 105 + } 107 106 108 107 const styles = StyleSheet.create({ 109 108 container: {
+6 -6
src/view/com/composer/useExternalLinkFetch.ts
··· 1 1 import {useState, useEffect} from 'react' 2 - import {useStores} from 'state/index' 3 2 import {ImageModel} from 'state/models/media/image' 4 3 import * as apilib from 'lib/api/index' 5 4 import {getLinkMeta} from 'lib/link-meta/link-meta' ··· 17 16 import {ComposerOpts} from 'state/shell/composer' 18 17 import {POST_IMG_MAX} from 'lib/constants' 19 18 import {logger} from '#/logger' 19 + import {useSession} from '#/state/session' 20 20 import {useGetPost} from '#/state/queries/post' 21 21 22 22 export function useExternalLinkFetch({ ··· 24 24 }: { 25 25 setQuote: (opts: ComposerOpts['quote']) => void 26 26 }) { 27 - const store = useStores() 27 + const {agent} = useSession() 28 28 const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>( 29 29 undefined, 30 30 ) ··· 56 56 }, 57 57 ) 58 58 } else if (isBskyCustomFeedUrl(extLink.uri)) { 59 - getFeedAsEmbed(store, extLink.uri).then( 59 + getFeedAsEmbed(agent, extLink.uri).then( 60 60 ({embed, meta}) => { 61 61 if (aborted) { 62 62 return ··· 74 74 }, 75 75 ) 76 76 } else if (isBskyListUrl(extLink.uri)) { 77 - getListAsEmbed(store, extLink.uri).then( 77 + getListAsEmbed(agent, extLink.uri).then( 78 78 ({embed, meta}) => { 79 79 if (aborted) { 80 80 return ··· 92 92 }, 93 93 ) 94 94 } else { 95 - getLinkMeta(store, extLink.uri).then(meta => { 95 + getLinkMeta(agent, extLink.uri).then(meta => { 96 96 if (aborted) { 97 97 return 98 98 } ··· 134 134 }) 135 135 } 136 136 return cleanup 137 - }, [store, extLink, setQuote, getPost]) 137 + }, [agent, extLink, setQuote, getPost]) 138 138 139 139 return {extLink, setExtLink} 140 140 }
-1
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
··· 315 315 <GestureDetector gesture={composedGesture}> 316 316 <AnimatedImage 317 317 contentFit="contain" 318 - // NOTE: Don't pass imageSrc={imageSrc} or MobX will break. 319 318 source={{uri: imageSrc.uri}} 320 319 style={[styles.image, animatedStyle]} 321 320 accessibilityLabel={imageSrc.alt}
-1
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
··· 139 139 {(!loaded || !imageDimensions) && <ImageLoading />} 140 140 <AnimatedImage 141 141 contentFit="contain" 142 - // NOTE: Don't pass imageSrc={imageSrc} or MobX will break. 143 142 source={{uri: imageSrc.uri}} 144 143 style={[styles.image, animatedStyle]} 145 144 accessibilityLabel={imageSrc.alt}
+2 -3
src/view/com/modals/BirthDateSettings.tsx
··· 5 5 TouchableOpacity, 6 6 View, 7 7 } from 'react-native' 8 - import {observer} from 'mobx-react-lite' 9 8 import {Text} from '../util/text/Text' 10 9 import {DateInput} from '../util/forms/DateInput' 11 10 import {ErrorMessage} from '../util/error/ErrorMessage' ··· 103 102 ) 104 103 } 105 104 106 - export const Component = observer(function Component({}: {}) { 105 + export function Component({}: {}) { 107 106 const {data: preferences} = usePreferencesQuery() 108 107 109 108 return !preferences ? ( ··· 111 110 ) : ( 112 111 <Inner preferences={preferences} /> 113 112 ) 114 - }) 113 + } 115 114 116 115 const styles = StyleSheet.create({ 117 116 container: {
+2 -3
src/view/com/modals/ChangeEmail.tsx
··· 1 1 import React, {useState} from 'react' 2 2 import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native' 3 3 import {ScrollView, TextInput} from './util' 4 - import {observer} from 'mobx-react-lite' 5 4 import {Text} from '../util/text/Text' 6 5 import {Button} from '../util/forms/Button' 7 6 import {ErrorMessage} from '../util/error/ErrorMessage' ··· 24 23 25 24 export const snapPoints = ['90%'] 26 25 27 - export const Component = observer(function Component({}: {}) { 26 + export function Component() { 28 27 const pal = usePalette('default') 29 28 const {agent, currentAccount} = useSession() 30 29 const {updateCurrentAccount} = useSessionApi() ··· 226 225 </ScrollView> 227 226 </SafeAreaView> 228 227 ) 229 - }) 228 + } 230 229 231 230 const styles = StyleSheet.create({ 232 231 titleSection: {
+71 -74
src/view/com/modals/ContentFilteringSettings.tsx
··· 2 2 import {LabelPreference} from '@atproto/api' 3 3 import {StyleSheet, Pressable, View} from 'react-native' 4 4 import LinearGradient from 'react-native-linear-gradient' 5 - import {observer} from 'mobx-react-lite' 6 5 import {ScrollView} from './util' 7 6 import {s, colors, gradients} from 'lib/styles' 8 7 import {Text} from '../util/text/Text' ··· 28 27 29 28 export const snapPoints = ['90%'] 30 29 31 - export const Component = observer( 32 - function ContentFilteringSettingsImpl({}: {}) { 33 - const {isMobile} = useWebMediaQueries() 34 - const pal = usePalette('default') 35 - const {_} = useLingui() 36 - const {closeModal} = useModalControls() 37 - const {data: preferences} = usePreferencesQuery() 30 + export function Component({}: {}) { 31 + const {isMobile} = useWebMediaQueries() 32 + const pal = usePalette('default') 33 + const {_} = useLingui() 34 + const {closeModal} = useModalControls() 35 + const {data: preferences} = usePreferencesQuery() 38 36 39 - const onPressDone = React.useCallback(() => { 40 - closeModal() 41 - }, [closeModal]) 37 + const onPressDone = React.useCallback(() => { 38 + closeModal() 39 + }, [closeModal]) 42 40 43 - return ( 44 - <View testID="contentFilteringModal" style={[pal.view, styles.container]}> 45 - <Text style={[pal.text, styles.title]}> 46 - <Trans>Content Filtering</Trans> 47 - </Text> 41 + return ( 42 + <View testID="contentFilteringModal" style={[pal.view, styles.container]}> 43 + <Text style={[pal.text, styles.title]}> 44 + <Trans>Content Filtering</Trans> 45 + </Text> 48 46 49 - <ScrollView style={styles.scrollContainer}> 50 - <AdultContentEnabledPref /> 51 - <ContentLabelPref 52 - preferences={preferences} 53 - labelGroup="nsfw" 54 - disabled={!preferences?.adultContentEnabled} 55 - /> 56 - <ContentLabelPref 57 - preferences={preferences} 58 - labelGroup="nudity" 59 - disabled={!preferences?.adultContentEnabled} 60 - /> 61 - <ContentLabelPref 62 - preferences={preferences} 63 - labelGroup="suggestive" 64 - disabled={!preferences?.adultContentEnabled} 65 - /> 66 - <ContentLabelPref 67 - preferences={preferences} 68 - labelGroup="gore" 69 - disabled={!preferences?.adultContentEnabled} 70 - /> 71 - <ContentLabelPref preferences={preferences} labelGroup="hate" /> 72 - <ContentLabelPref preferences={preferences} labelGroup="spam" /> 73 - <ContentLabelPref 74 - preferences={preferences} 75 - labelGroup="impersonation" 76 - /> 77 - <View style={{height: isMobile ? 60 : 0}} /> 78 - </ScrollView> 47 + <ScrollView style={styles.scrollContainer}> 48 + <AdultContentEnabledPref /> 49 + <ContentLabelPref 50 + preferences={preferences} 51 + labelGroup="nsfw" 52 + disabled={!preferences?.adultContentEnabled} 53 + /> 54 + <ContentLabelPref 55 + preferences={preferences} 56 + labelGroup="nudity" 57 + disabled={!preferences?.adultContentEnabled} 58 + /> 59 + <ContentLabelPref 60 + preferences={preferences} 61 + labelGroup="suggestive" 62 + disabled={!preferences?.adultContentEnabled} 63 + /> 64 + <ContentLabelPref 65 + preferences={preferences} 66 + labelGroup="gore" 67 + disabled={!preferences?.adultContentEnabled} 68 + /> 69 + <ContentLabelPref preferences={preferences} labelGroup="hate" /> 70 + <ContentLabelPref preferences={preferences} labelGroup="spam" /> 71 + <ContentLabelPref 72 + preferences={preferences} 73 + labelGroup="impersonation" 74 + /> 75 + <View style={{height: isMobile ? 60 : 0}} /> 76 + </ScrollView> 79 77 80 - <View 81 - style={[ 82 - styles.btnContainer, 83 - isMobile && styles.btnContainerMobile, 84 - pal.borderDark, 85 - ]}> 86 - <Pressable 87 - testID="sendReportBtn" 88 - onPress={onPressDone} 89 - accessibilityRole="button" 90 - accessibilityLabel={_(msg`Done`)} 91 - accessibilityHint=""> 92 - <LinearGradient 93 - colors={[gradients.blueLight.start, gradients.blueLight.end]} 94 - start={{x: 0, y: 0}} 95 - end={{x: 1, y: 1}} 96 - style={[styles.btn]}> 97 - <Text style={[s.white, s.bold, s.f18]}> 98 - <Trans>Done</Trans> 99 - </Text> 100 - </LinearGradient> 101 - </Pressable> 102 - </View> 78 + <View 79 + style={[ 80 + styles.btnContainer, 81 + isMobile && styles.btnContainerMobile, 82 + pal.borderDark, 83 + ]}> 84 + <Pressable 85 + testID="sendReportBtn" 86 + onPress={onPressDone} 87 + accessibilityRole="button" 88 + accessibilityLabel={_(msg`Done`)} 89 + accessibilityHint=""> 90 + <LinearGradient 91 + colors={[gradients.blueLight.start, gradients.blueLight.end]} 92 + start={{x: 0, y: 0}} 93 + end={{x: 1, y: 1}} 94 + style={[styles.btn]}> 95 + <Text style={[s.white, s.bold, s.f18]}> 96 + <Trans>Done</Trans> 97 + </Text> 98 + </LinearGradient> 99 + </Pressable> 103 100 </View> 104 - ) 105 - }, 106 - ) 101 + </View> 102 + ) 103 + } 107 104 108 105 function AdultContentEnabledPref() { 109 106 const pal = usePalette('default') ··· 171 168 } 172 169 173 170 // TODO: Refactor this component to pass labels down to each tab 174 - const ContentLabelPref = observer(function ContentLabelPrefImpl({ 171 + function ContentLabelPref({ 175 172 preferences, 176 173 labelGroup, 177 174 disabled, ··· 217 214 )} 218 215 </View> 219 216 ) 220 - }) 217 + } 221 218 222 219 interface SelectGroupProps { 223 220 current: LabelPreference
+2 -3
src/view/com/modals/InviteCodes.tsx
··· 5 5 View, 6 6 ActivityIndicator, 7 7 } from 'react-native' 8 - import {observer} from 'mobx-react-lite' 9 8 import {ComAtprotoServerDefs} from '@atproto/api' 10 9 import { 11 10 FontAwesomeIcon, ··· 129 128 ) 130 129 } 131 130 132 - const InviteCode = observer(function InviteCodeImpl({ 131 + function InviteCode({ 133 132 testID, 134 133 invite, 135 134 used, ··· 211 210 ) : null} 212 211 </View> 213 212 ) 214 - }) 213 + } 215 214 216 215 const styles = StyleSheet.create({ 217 216 container: {
+2 -9
src/view/com/modals/LinkWarning.tsx
··· 1 1 import React from 'react' 2 2 import {Linking, SafeAreaView, StyleSheet, View} from 'react-native' 3 3 import {ScrollView} from './util' 4 - import {observer} from 'mobx-react-lite' 5 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 6 5 import {Text} from '../util/text/Text' 7 6 import {Button} from '../util/forms/Button' ··· 16 15 17 16 export const snapPoints = ['50%'] 18 17 19 - export const Component = observer(function Component({ 20 - text, 21 - href, 22 - }: { 23 - text: string 24 - href: string 25 - }) { 18 + export function Component({text, href}: {text: string; href: string}) { 26 19 const pal = usePalette('default') 27 20 const {closeModal} = useModalControls() 28 21 const {isMobile} = useWebMediaQueries() ··· 97 90 </ScrollView> 98 91 </SafeAreaView> 99 92 ) 100 - }) 93 + } 101 94 102 95 function LinkBox({href}: {href: string}) { 103 96 const pal = usePalette('default')
+2 -3
src/view/com/modals/Modal.tsx
··· 1 1 import React, {useRef, useEffect} from 'react' 2 2 import {StyleSheet} from 'react-native' 3 3 import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' 4 - import {observer} from 'mobx-react-lite' 5 4 import BottomSheet from '@gorhom/bottom-sheet' 6 5 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 7 6 import {usePalette} from 'lib/hooks/usePalette' ··· 40 39 const DEFAULT_SNAPPOINTS = ['90%'] 41 40 const HANDLE_HEIGHT = 24 42 41 43 - export const ModalsContainer = observer(function ModalsContainer() { 42 + export function ModalsContainer() { 44 43 const {isModalActive, activeModals} = useModals() 45 44 const {closeModal} = useModalControls() 46 45 const bottomSheetRef = useRef<BottomSheet>(null) ··· 198 197 {element} 199 198 </BottomSheet> 200 199 ) 201 - }) 200 + } 202 201 203 202 const styles = StyleSheet.create({ 204 203 handle: {
+2 -3
src/view/com/modals/Modal.web.tsx
··· 1 1 import React from 'react' 2 2 import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {usePalette} from 'lib/hooks/usePalette' 5 4 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 6 5 import type {Modal as ModalIface} from '#/state/modals' ··· 33 32 import * as ChangeEmailModal from './ChangeEmail' 34 33 import * as LinkWarningModal from './LinkWarning' 35 34 36 - export const ModalsContainer = observer(function ModalsContainer() { 35 + export function ModalsContainer() { 37 36 const {isModalActive, activeModals} = useModals() 38 37 39 38 if (!isModalActive) { ··· 47 46 ))} 48 47 </> 49 48 ) 50 - }) 49 + } 51 50 52 51 function Modal({modal}: {modal: ModalIface}) { 53 52 const {isModalActive} = useModals()
+2 -3
src/view/com/modals/SelfLabel.tsx
··· 1 1 import React, {useState} from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {Text} from '../util/text/Text' 5 4 import {s, colors} from 'lib/styles' 6 5 import {usePalette} from 'lib/hooks/usePalette' ··· 17 16 18 17 export const snapPoints = ['50%'] 19 18 20 - export const Component = observer(function Component({ 19 + export function Component({ 21 20 labels, 22 21 hasMedia, 23 22 onChange, ··· 161 160 </View> 162 161 </View> 163 162 ) 164 - }) 163 + } 165 164 166 165 const styles = StyleSheet.create({ 167 166 container: {
+1 -1
src/view/com/modals/ServerInput.tsx
··· 9 9 import {s, colors} from 'lib/styles' 10 10 import {usePalette} from 'lib/hooks/usePalette' 11 11 import {useTheme} from 'lib/ThemeContext' 12 - import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index' 12 + import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants' 13 13 import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' 14 14 import {Trans, msg} from '@lingui/macro' 15 15 import {useLingui} from '@lingui/react'
+2 -7
src/view/com/modals/VerifyEmail.tsx
··· 8 8 } from 'react-native' 9 9 import {Svg, Circle, Path} from 'react-native-svg' 10 10 import {ScrollView, TextInput} from './util' 11 - import {observer} from 'mobx-react-lite' 12 11 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 13 12 import {Text} from '../util/text/Text' 14 13 import {Button} from '../util/forms/Button' ··· 32 31 ConfirmCode, 33 32 } 34 33 35 - export const Component = observer(function Component({ 36 - showReminder, 37 - }: { 38 - showReminder?: boolean 39 - }) { 34 + export function Component({showReminder}: {showReminder?: boolean}) { 40 35 const pal = usePalette('default') 41 36 const {agent, currentAccount} = useSession() 42 37 const {updateCurrentAccount} = useSessionApi() ··· 244 239 </ScrollView> 245 240 </SafeAreaView> 246 241 ) 247 - }) 242 + } 248 243 249 244 function ReminderIllustration() { 250 245 const pal = usePalette('default')
+2 -3
src/view/com/modals/lang-settings/LanguageToggle.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet} from 'react-native' 3 3 import {usePalette} from 'lib/hooks/usePalette' 4 - import {observer} from 'mobx-react-lite' 5 4 import {ToggleButton} from 'view/com/util/forms/ToggleButton' 6 5 import {useLanguagePrefs, toPostLanguages} from '#/state/preferences/languages' 7 6 8 - export const LanguageToggle = observer(function LanguageToggleImpl({ 7 + export function LanguageToggle({ 9 8 code2, 10 9 name, 11 10 onPress, ··· 39 38 style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]} 40 39 /> 41 40 ) 42 - }) 41 + } 43 42 44 43 const styles = StyleSheet.create({ 45 44 languageToggle: {
+2 -3
src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {ScrollView} from '../util' 5 4 import {Text} from '../../util/text/Text' 6 5 import {usePalette} from 'lib/hooks/usePalette' ··· 19 18 20 19 export const snapPoints = ['100%'] 21 20 22 - export const Component = observer(function PostLanguagesSettingsImpl() { 21 + export function Component() { 23 22 const {closeModal} = useModalControls() 24 23 const langPrefs = useLanguagePrefs() 25 24 const setLangPrefs = useLanguagePrefsApi() ··· 111 110 <ConfirmLanguagesButton onPress={onPressDone} /> 112 111 </View> 113 112 ) 114 - }) 113 + } 115 114 116 115 const styles = StyleSheet.create({ 117 116 container: {
+3 -3
src/view/com/modals/report/Modal.tsx
··· 2 2 import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {ScrollView} from 'react-native-gesture-handler' 4 4 import {AtUri} from '@atproto/api' 5 - import {useStores} from 'state/index' 6 5 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 7 6 import {s} from 'lib/styles' 8 7 import {Text} from '../../util/text/Text' ··· 17 16 import {Trans, msg} from '@lingui/macro' 18 17 import {useLingui} from '@lingui/react' 19 18 import {useModalControls} from '#/state/modals' 19 + import {useSession} from '#/state/session' 20 20 21 21 const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright' 22 22 ··· 39 39 } 40 40 41 41 export function Component(content: ReportComponentProps) { 42 - const store = useStores() 42 + const {agent} = useSession() 43 43 const {closeModal} = useModalControls() 44 44 const pal = usePalette('default') 45 45 const {isMobile} = useWebMediaQueries() ··· 70 70 const $type = !isAccountReport 71 71 ? 'com.atproto.repo.strongRef' 72 72 : 'com.atproto.admin.defs#repoRef' 73 - await store.agent.createModerationReport({ 73 + await agent.createModerationReport({ 74 74 reasonType: issue, 75 75 subject: { 76 76 $type,
+4 -5
src/view/com/pager/FeedsTabBar.web.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet} from 'react-native' 3 3 import Animated from 'react-native-reanimated' 4 - import {observer} from 'mobx-react-lite' 5 4 import {TabBar} from 'view/com/pager/TabBar' 6 5 import {RenderTabBarFnProps} from 'view/com/pager/Pager' 7 6 import {usePalette} from 'lib/hooks/usePalette' ··· 11 10 import {useShellLayout} from '#/state/shell/shell-layout' 12 11 import {usePinnedFeedsInfos} from '#/state/queries/feed' 13 12 14 - export const FeedsTabBar = observer(function FeedsTabBarImpl( 13 + export function FeedsTabBar( 15 14 props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 16 15 ) { 17 16 const {isMobile, isTablet} = useWebMediaQueries() ··· 22 21 } else { 23 22 return null 24 23 } 25 - }) 24 + } 26 25 27 - const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl( 26 + function FeedsTabBarTablet( 28 27 props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 29 28 ) { 30 29 const feeds = usePinnedFeedsInfos() ··· 48 47 /> 49 48 </Animated.View> 50 49 ) 51 - }) 50 + } 52 51 53 52 const styles = StyleSheet.create({ 54 53 tabBar: {
+2 -3
src/view/com/pager/FeedsTabBarMobile.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {TabBar} from 'view/com/pager/TabBar' 5 4 import {RenderTabBarFnProps} from 'view/com/pager/Pager' 6 5 import {usePalette} from 'lib/hooks/usePalette' ··· 20 19 import {useSession} from '#/state/session' 21 20 import {usePinnedFeedsInfos} from '#/state/queries/feed' 22 21 23 - export const FeedsTabBar = observer(function FeedsTabBarImpl( 22 + export function FeedsTabBar( 24 23 props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, 25 24 ) { 26 25 const pal = usePalette('default') ··· 88 87 /> 89 88 </Animated.View> 90 89 ) 91 - }) 90 + } 92 91 93 92 const styles = StyleSheet.create({ 94 93 tabBar: {
+2 -3
src/view/com/posts/FeedSlice.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FeedPostSlice} from '#/state/queries/post-feed' 5 4 import {AtUri, moderatePost, ModerationOpts} from '@atproto/api' 6 5 import {Link} from '../util/Link' ··· 10 9 import {usePalette} from 'lib/hooks/usePalette' 11 10 import {makeProfileLink} from 'lib/routes/links' 12 11 13 - export const FeedSlice = observer(function FeedSliceImpl({ 12 + export function FeedSlice({ 14 13 slice, 15 14 dataUpdatedAt, 16 15 ignoreFilterFor, ··· 94 93 ))} 95 94 </> 96 95 ) 97 - }) 96 + } 98 97 99 98 function ViewFullThread({slice}: {slice: FeedPostSlice}) { 100 99 const pal = usePalette('default')
+2 -3
src/view/com/profile/ProfileCard.tsx
··· 1 1 import * as React from 'react' 2 2 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import { 5 4 AppBskyActorDefs, 6 5 moderateProfile, ··· 152 151 ) 153 152 } 154 153 155 - const FollowersList = observer(function FollowersListImpl({ 154 + function FollowersList({ 156 155 followers, 157 156 }: { 158 157 followers?: AppBskyActorDefs.ProfileView[] | undefined ··· 196 195 ))} 197 196 </View> 198 197 ) 199 - }) 198 + } 200 199 201 200 export function ProfileCardWithFollowBtn({ 202 201 profile,
+2 -5
src/view/com/profile/ProfileSubpageHeader.tsx
··· 1 1 import React from 'react' 2 2 import {Pressable, StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {useNavigation} from '@react-navigation/native' 6 5 import {usePalette} from 'lib/hooks/usePalette' ··· 12 11 import {CenteredView} from '../util/Views' 13 12 import {sanitizeHandle} from 'lib/strings/handles' 14 13 import {makeProfileLink} from 'lib/routes/links' 15 - import {useStores} from 'state/index' 16 14 import {NavigationProp} from 'lib/routes/types' 17 15 import {BACK_HITSLOP} from 'lib/constants' 18 16 import {isNative} from 'platform/detection' ··· 22 20 import {useSetDrawerOpen} from '#/state/shell' 23 21 import {emitSoftReset} from '#/state/events' 24 22 25 - export const ProfileSubpageHeader = observer(function HeaderImpl({ 23 + export function ProfileSubpageHeader({ 26 24 isLoading, 27 25 href, 28 26 title, ··· 45 43 | undefined 46 44 avatarType: UserAvatarType 47 45 }>) { 48 - const store = useStores() 49 46 const setDrawerOpen = useSetDrawerOpen() 50 47 const navigation = useNavigation<NavigationProp>() 51 48 const {_} = useLingui() ··· 183 180 </View> 184 181 </CenteredView> 185 182 ) 186 - }) 183 + } 187 184 188 185 const styles = StyleSheet.create({ 189 186 backBtn: {
+2 -3
src/view/com/util/PostMeta.tsx
··· 6 6 import {usePalette} from 'lib/hooks/usePalette' 7 7 import {TypographyVariant} from 'lib/ThemeContext' 8 8 import {UserAvatar} from './UserAvatar' 9 - import {observer} from 'mobx-react-lite' 10 9 import {sanitizeDisplayName} from 'lib/strings/display-names' 11 10 import {sanitizeHandle} from 'lib/strings/handles' 12 11 import {isAndroid} from 'platform/detection' ··· 30 29 style?: StyleProp<ViewStyle> 31 30 } 32 31 33 - export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) { 32 + export function PostMeta(opts: PostMetaOpts) { 34 33 const pal = usePalette('default') 35 34 const displayName = opts.author.displayName || opts.author.handle 36 35 const handle = opts.author.handle ··· 92 91 </TimeElapsed> 93 92 </View> 94 93 ) 95 - }) 94 + } 96 95 97 96 const styles = StyleSheet.create({ 98 97 container: {
+2 -3
src/view/com/util/SimpleViewHeader.tsx
··· 1 1 import React from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import { 4 3 StyleProp, 5 4 StyleSheet, ··· 18 17 19 18 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 20 19 21 - export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({ 20 + export function SimpleViewHeader({ 22 21 showBackButton = true, 23 22 style, 24 23 children, ··· 76 75 {children} 77 76 </Container> 78 77 ) 79 - }) 78 + } 80 79 81 80 const styles = StyleSheet.create({ 82 81 header: {
+4 -5
src/view/com/util/ViewHeader.tsx
··· 1 1 import React from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {useNavigation} from '@react-navigation/native' ··· 15 14 16 15 const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} 17 16 18 - export const ViewHeader = observer(function ViewHeaderImpl({ 17 + export function ViewHeader({ 19 18 title, 20 19 canGoBack, 21 20 showBackButton = true, ··· 108 107 </Container> 109 108 ) 110 109 } 111 - }) 110 + } 112 111 113 112 function DesktopWebHeader({ 114 113 title, ··· 140 139 ) 141 140 } 142 141 143 - const Container = observer(function ContainerImpl({ 142 + function Container({ 144 143 children, 145 144 hideOnScroll, 146 145 showBorder, ··· 178 177 {children} 179 178 </Animated.View> 180 179 ) 181 - }) 180 + } 182 181 183 182 const styles = StyleSheet.create({ 184 183 header: {
+2 -7
src/view/com/util/fab/FABInner.tsx
··· 1 1 import React, {ComponentProps} from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import {StyleSheet, TouchableWithoutFeedback} from 'react-native' 4 3 import LinearGradient from 'react-native-linear-gradient' 5 4 import {gradients} from 'lib/styles' ··· 15 14 icon: JSX.Element 16 15 } 17 16 18 - export const FABInner = observer(function FABInnerImpl({ 19 - testID, 20 - icon, 21 - ...props 22 - }: FABProps) { 17 + export function FABInner({testID, icon, ...props}: FABProps) { 23 18 const insets = useSafeAreaInsets() 24 19 const {isMobile, isTablet} = useWebMediaQueries() 25 20 const {fabMinimalShellTransform} = useMinimalShellMode() ··· 55 50 </Animated.View> 56 51 </TouchableWithoutFeedback> 57 52 ) 58 - }) 53 + } 59 54 60 55 const styles = StyleSheet.create({ 61 56 sizeRegular: {
+4 -5
src/view/com/util/images/AutoSizedImage.tsx
··· 2 2 import {StyleProp, StyleSheet, Pressable, View, ViewStyle} from 'react-native' 3 3 import {Image} from 'expo-image' 4 4 import {clamp} from 'lib/numbers' 5 - import {useStores} from 'state/index' 6 5 import {Dimensions} from 'lib/media/types' 6 + import * as imageSizes from 'lib/media/image-sizes' 7 7 8 8 const MIN_ASPECT_RATIO = 0.33 // 1/3 9 9 const MAX_ASPECT_RATIO = 5 // 5/1 ··· 29 29 style, 30 30 children = null, 31 31 }: Props) { 32 - const store = useStores() 33 32 const [dim, setDim] = React.useState<Dimensions | undefined>( 34 - dimensionsHint || store.imageSizes.get(uri), 33 + dimensionsHint || imageSizes.get(uri), 35 34 ) 36 35 const [aspectRatio, setAspectRatio] = React.useState<number>( 37 36 dim ? calc(dim) : 1, ··· 41 40 if (dim) { 42 41 return 43 42 } 44 - store.imageSizes.fetch(uri).then(newDim => { 43 + imageSizes.fetch(uri).then(newDim => { 45 44 if (aborted) { 46 45 return 47 46 } 48 47 setDim(newDim) 49 48 setAspectRatio(calc(newDim)) 50 49 }) 51 - }, [dim, setDim, setAspectRatio, store, uri]) 50 + }, [dim, setDim, setAspectRatio, uri]) 52 51 53 52 if (onPress || onLongPress || onPressIn) { 54 53 return (
+2 -3
src/view/com/util/load-latest/LoadLatestBtn.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {usePalette} from 'lib/hooks/usePalette' 6 5 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' ··· 12 11 Animated.createAnimatedComponent(TouchableOpacity) 13 12 import {isWeb} from 'platform/detection' 14 13 15 - export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ 14 + export function LoadLatestBtn({ 16 15 onPress, 17 16 label, 18 17 showIndicator, ··· 44 43 {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} 45 44 </AnimatedTouchableOpacity> 46 45 ) 47 - }) 46 + } 48 47 49 48 const styles = StyleSheet.create({ 50 49 loadLatest: {
+2 -3
src/view/com/util/post-embeds/ListEmbed.tsx
··· 1 1 import React from 'react' 2 2 import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 3 import {usePalette} from 'lib/hooks/usePalette' 4 - import {observer} from 'mobx-react-lite' 5 4 import {ListCard} from 'view/com/lists/ListCard' 6 5 import {AppBskyGraphDefs} from '@atproto/api' 7 6 import {s} from 'lib/styles' 8 7 9 - export const ListEmbed = observer(function ListEmbedImpl({ 8 + export function ListEmbed({ 10 9 item, 11 10 style, 12 11 }: { ··· 20 19 <ListCard list={item} style={[style, styles.card]} /> 21 20 </View> 22 21 ) 23 - }) 22 + } 24 23 25 24 const styles = StyleSheet.create({ 26 25 container: {
+104 -109
src/view/screens/Home.tsx
··· 1 1 import React from 'react' 2 2 import {useFocusEffect} from '@react-navigation/native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' 5 4 import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' 6 5 import {withAuthRequired} from 'view/com/auth/withAuthRequired' ··· 15 14 import {emitSoftReset} from '#/state/events' 16 15 17 16 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> 18 - export const HomeScreen = withAuthRequired( 19 - observer(function HomeScreenImpl({}: Props) { 20 - const setMinimalShellMode = useSetMinimalShellMode() 21 - const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() 22 - const pagerRef = React.useRef<PagerRef>(null) 23 - const [selectedPage, setSelectedPage] = React.useState(0) 24 - const [customFeeds, setCustomFeeds] = React.useState<FeedDescriptor[]>([]) 25 - const {data: preferences} = usePreferencesQuery() 17 + export const HomeScreen = withAuthRequired(function HomeScreenImpl({}: Props) { 18 + const setMinimalShellMode = useSetMinimalShellMode() 19 + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() 20 + const pagerRef = React.useRef<PagerRef>(null) 21 + const [selectedPage, setSelectedPage] = React.useState(0) 22 + const [customFeeds, setCustomFeeds] = React.useState<FeedDescriptor[]>([]) 23 + const {data: preferences} = usePreferencesQuery() 26 24 27 - React.useEffect(() => { 28 - if (!preferences?.feeds?.pinned) return 25 + React.useEffect(() => { 26 + if (!preferences?.feeds?.pinned) return 29 27 30 - const pinned = preferences.feeds.pinned 28 + const pinned = preferences.feeds.pinned 31 29 32 - const feeds: FeedDescriptor[] = [] 30 + const feeds: FeedDescriptor[] = [] 33 31 34 - for (const uri of pinned) { 35 - if (uri.includes('app.bsky.feed.generator')) { 36 - feeds.push(`feedgen|${uri}`) 37 - } else if (uri.includes('app.bsky.graph.list')) { 38 - feeds.push(`list|${uri}`) 39 - } 32 + for (const uri of pinned) { 33 + if (uri.includes('app.bsky.feed.generator')) { 34 + feeds.push(`feedgen|${uri}`) 35 + } else if (uri.includes('app.bsky.graph.list')) { 36 + feeds.push(`list|${uri}`) 40 37 } 38 + } 39 + 40 + setCustomFeeds(feeds) 41 41 42 - setCustomFeeds(feeds) 42 + pagerRef.current?.setPage(0) 43 + }, [preferences?.feeds?.pinned, setCustomFeeds, pagerRef]) 43 44 44 - pagerRef.current?.setPage(0) 45 - }, [preferences?.feeds?.pinned, setCustomFeeds, pagerRef]) 45 + const homeFeedParams = React.useMemo<FeedParams>(() => { 46 + if (!preferences) return {} 46 47 47 - const homeFeedParams = React.useMemo<FeedParams>(() => { 48 - if (!preferences) return {} 48 + return { 49 + mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled), 50 + mergeFeedSources: preferences.feeds.saved, 51 + } 52 + }, [preferences]) 49 53 50 - return { 51 - mergeFeedEnabled: Boolean( 52 - preferences.feedViewPrefs.lab_mergeFeedEnabled, 53 - ), 54 - mergeFeedSources: preferences.feeds.saved, 54 + useFocusEffect( 55 + React.useCallback(() => { 56 + setMinimalShellMode(false) 57 + setDrawerSwipeDisabled(selectedPage > 0) 58 + return () => { 59 + setDrawerSwipeDisabled(false) 55 60 } 56 - }, [preferences]) 61 + }, [setDrawerSwipeDisabled, selectedPage, setMinimalShellMode]), 62 + ) 63 + 64 + const onPageSelected = React.useCallback( 65 + (index: number) => { 66 + setMinimalShellMode(false) 67 + setSelectedPage(index) 68 + setDrawerSwipeDisabled(index > 0) 69 + }, 70 + [setDrawerSwipeDisabled, setSelectedPage, setMinimalShellMode], 71 + ) 72 + 73 + const onPressSelected = React.useCallback(() => { 74 + emitSoftReset() 75 + }, []) 57 76 58 - useFocusEffect( 59 - React.useCallback(() => { 77 + const onPageScrollStateChanged = React.useCallback( 78 + (state: 'idle' | 'dragging' | 'settling') => { 79 + if (state === 'dragging') { 60 80 setMinimalShellMode(false) 61 - setDrawerSwipeDisabled(selectedPage > 0) 62 - return () => { 63 - setDrawerSwipeDisabled(false) 64 - } 65 - }, [setDrawerSwipeDisabled, selectedPage, setMinimalShellMode]), 66 - ) 81 + } 82 + }, 83 + [setMinimalShellMode], 84 + ) 67 85 68 - const onPageSelected = React.useCallback( 69 - (index: number) => { 70 - setMinimalShellMode(false) 71 - setSelectedPage(index) 72 - setDrawerSwipeDisabled(index > 0) 73 - }, 74 - [setDrawerSwipeDisabled, setSelectedPage, setMinimalShellMode], 75 - ) 86 + const renderTabBar = React.useCallback( 87 + (props: RenderTabBarFnProps) => { 88 + return ( 89 + <FeedsTabBar 90 + key="FEEDS_TAB_BAR" 91 + selectedPage={props.selectedPage} 92 + onSelect={props.onSelect} 93 + testID="homeScreenFeedTabs" 94 + onPressSelected={onPressSelected} 95 + /> 96 + ) 97 + }, 98 + [onPressSelected], 99 + ) 76 100 77 - const onPressSelected = React.useCallback(() => { 78 - emitSoftReset() 79 - }, []) 101 + const renderFollowingEmptyState = React.useCallback(() => { 102 + return <FollowingEmptyState /> 103 + }, []) 80 104 81 - const onPageScrollStateChanged = React.useCallback( 82 - (state: 'idle' | 'dragging' | 'settling') => { 83 - if (state === 'dragging') { 84 - setMinimalShellMode(false) 85 - } 86 - }, 87 - [setMinimalShellMode], 88 - ) 105 + const renderCustomFeedEmptyState = React.useCallback(() => { 106 + return <CustomFeedEmptyState /> 107 + }, []) 89 108 90 - const renderTabBar = React.useCallback( 91 - (props: RenderTabBarFnProps) => { 109 + return ( 110 + <Pager 111 + ref={pagerRef} 112 + testID="homeScreen" 113 + onPageSelected={onPageSelected} 114 + onPageScrollStateChanged={onPageScrollStateChanged} 115 + renderTabBar={renderTabBar} 116 + tabBarPosition="top"> 117 + <FeedPage 118 + key="1" 119 + testID="followingFeedPage" 120 + isPageFocused={selectedPage === 0} 121 + feed="home" 122 + feedParams={homeFeedParams} 123 + renderEmptyState={renderFollowingEmptyState} 124 + renderEndOfFeed={FollowingEndOfFeed} 125 + /> 126 + {customFeeds.map((f, index) => { 92 127 return ( 93 - <FeedsTabBar 94 - key="FEEDS_TAB_BAR" 95 - selectedPage={props.selectedPage} 96 - onSelect={props.onSelect} 97 - testID="homeScreenFeedTabs" 98 - onPressSelected={onPressSelected} 128 + <FeedPage 129 + key={f} 130 + testID="customFeedPage" 131 + isPageFocused={selectedPage === 1 + index} 132 + feed={f} 133 + renderEmptyState={renderCustomFeedEmptyState} 99 134 /> 100 135 ) 101 - }, 102 - [onPressSelected], 103 - ) 104 - 105 - const renderFollowingEmptyState = React.useCallback(() => { 106 - return <FollowingEmptyState /> 107 - }, []) 108 - 109 - const renderCustomFeedEmptyState = React.useCallback(() => { 110 - return <CustomFeedEmptyState /> 111 - }, []) 112 - 113 - return ( 114 - <Pager 115 - ref={pagerRef} 116 - testID="homeScreen" 117 - onPageSelected={onPageSelected} 118 - onPageScrollStateChanged={onPageScrollStateChanged} 119 - renderTabBar={renderTabBar} 120 - tabBarPosition="top"> 121 - <FeedPage 122 - key="1" 123 - testID="followingFeedPage" 124 - isPageFocused={selectedPage === 0} 125 - feed="home" 126 - feedParams={homeFeedParams} 127 - renderEmptyState={renderFollowingEmptyState} 128 - renderEndOfFeed={FollowingEndOfFeed} 129 - /> 130 - {customFeeds.map((f, index) => { 131 - return ( 132 - <FeedPage 133 - key={f} 134 - testID="customFeedPage" 135 - isPageFocused={selectedPage === 1 + index} 136 - feed={f} 137 - renderEmptyState={renderCustomFeedEmptyState} 138 - /> 139 - ) 140 - })} 141 - </Pager> 142 - ) 143 - }), 144 - ) 136 + })} 137 + </Pager> 138 + ) 139 + })
+2 -5
src/view/screens/LanguageSettings.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {Text} from '../com/util/text/Text' 5 4 import {s} from 'lib/styles' 6 5 import {usePalette} from 'lib/hooks/usePalette' ··· 23 22 24 23 type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> 25 24 26 - export const LanguageSettingsScreen = observer(function LanguageSettingsImpl( 27 - _: Props, 28 - ) { 25 + export function LanguageSettingsScreen(_: Props) { 29 26 const pal = usePalette('default') 30 27 const langPrefs = useLanguagePrefs() 31 28 const setLangPrefs = useLanguagePrefsApi() ··· 192 189 </View> 193 190 </CenteredView> 194 191 ) 195 - }) 192 + } 196 193 197 194 const styles = StyleSheet.create({ 198 195 container: {
+2 -3
src/view/screens/Log.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 3 3 import {useFocusEffect} from '@react-navigation/native' 4 - import {observer} from 'mobx-react-lite' 5 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 6 5 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 7 6 import {ScrollView} from '../com/util/Views' ··· 15 14 import {msg} from '@lingui/macro' 16 15 import {useSetMinimalShellMode} from '#/state/shell' 17 16 18 - export const LogScreen = observer(function Log({}: NativeStackScreenProps< 17 + export function LogScreen({}: NativeStackScreenProps< 19 18 CommonNavigatorParams, 20 19 'Log' 21 20 >) { ··· 88 87 </ScrollView> 89 88 </View> 90 89 ) 91 - }) 90 + } 92 91 93 92 const styles = StyleSheet.create({ 94 93 entry: {
+2 -3
src/view/screens/Moderation.tsx
··· 5 5 FontAwesomeIcon, 6 6 FontAwesomeIconStyle, 7 7 } from '@fortawesome/react-native-fontawesome' 8 - import {observer} from 'mobx-react-lite' 9 8 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 10 9 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 11 10 import {s} from 'lib/styles' ··· 21 20 22 21 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'> 23 22 export const ModerationScreen = withAuthRequired( 24 - observer(function Moderation({}: Props) { 23 + function Moderation({}: Props) { 25 24 const pal = usePalette('default') 26 25 const setMinimalShellMode = useSetMinimalShellMode() 27 26 const {screen, track} = useAnalytics() ··· 111 110 </Link> 112 111 </CenteredView> 113 112 ) 114 - }), 113 + }, 115 114 ) 116 115 117 116 const styles = StyleSheet.create({
+71 -72
src/view/screens/PostThread.tsx
··· 3 3 import Animated from 'react-native-reanimated' 4 4 import {useFocusEffect} from '@react-navigation/native' 5 5 import {useQueryClient} from '@tanstack/react-query' 6 - import {observer} from 'mobx-react-lite' 7 6 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 8 7 import {makeRecordUri} from 'lib/strings/url-helpers' 9 8 import {withAuthRequired} from 'view/com/auth/withAuthRequired' ··· 26 25 import {useComposerControls} from '#/state/shell/composer' 27 26 28 27 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> 29 - export const PostThreadScreen = withAuthRequired( 30 - observer(function PostThreadScreenImpl({route}: Props) { 31 - const queryClient = useQueryClient() 32 - const {fabMinimalShellTransform} = useMinimalShellMode() 33 - const setMinimalShellMode = useSetMinimalShellMode() 34 - const {openComposer} = useComposerControls() 35 - const safeAreaInsets = useSafeAreaInsets() 36 - const {name, rkey} = route.params 37 - const {isMobile} = useWebMediaQueries() 38 - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 39 - const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) 28 + export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({ 29 + route, 30 + }: Props) { 31 + const queryClient = useQueryClient() 32 + const {fabMinimalShellTransform} = useMinimalShellMode() 33 + const setMinimalShellMode = useSetMinimalShellMode() 34 + const {openComposer} = useComposerControls() 35 + const safeAreaInsets = useSafeAreaInsets() 36 + const {name, rkey} = route.params 37 + const {isMobile} = useWebMediaQueries() 38 + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 39 + const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) 40 40 41 - useFocusEffect( 42 - React.useCallback(() => { 43 - setMinimalShellMode(false) 44 - }, [setMinimalShellMode]), 45 - ) 41 + useFocusEffect( 42 + React.useCallback(() => { 43 + setMinimalShellMode(false) 44 + }, [setMinimalShellMode]), 45 + ) 46 46 47 - const onPressReply = React.useCallback(() => { 48 - if (!resolvedUri) { 49 - return 50 - } 51 - const thread = queryClient.getQueryData<ThreadNode>( 52 - POST_THREAD_RQKEY(resolvedUri.uri), 53 - ) 54 - if (thread?.type !== 'post') { 55 - return 56 - } 57 - openComposer({ 58 - replyTo: { 59 - uri: thread.post.uri, 60 - cid: thread.post.cid, 61 - text: thread.record.text, 62 - author: { 63 - handle: thread.post.author.handle, 64 - displayName: thread.post.author.displayName, 65 - avatar: thread.post.author.avatar, 66 - }, 47 + const onPressReply = React.useCallback(() => { 48 + if (!resolvedUri) { 49 + return 50 + } 51 + const thread = queryClient.getQueryData<ThreadNode>( 52 + POST_THREAD_RQKEY(resolvedUri.uri), 53 + ) 54 + if (thread?.type !== 'post') { 55 + return 56 + } 57 + openComposer({ 58 + replyTo: { 59 + uri: thread.post.uri, 60 + cid: thread.post.cid, 61 + text: thread.record.text, 62 + author: { 63 + handle: thread.post.author.handle, 64 + displayName: thread.post.author.displayName, 65 + avatar: thread.post.author.avatar, 67 66 }, 68 - onPost: () => 69 - queryClient.invalidateQueries({ 70 - queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), 71 - }), 72 - }) 73 - }, [openComposer, queryClient, resolvedUri]) 67 + }, 68 + onPost: () => 69 + queryClient.invalidateQueries({ 70 + queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), 71 + }), 72 + }) 73 + }, [openComposer, queryClient, resolvedUri]) 74 74 75 - return ( 76 - <View style={s.hContentRegion}> 77 - {isMobile && <ViewHeader title="Post" />} 78 - <View style={s.flex1}> 79 - {uriError ? ( 80 - <CenteredView> 81 - <ErrorMessage message={String(uriError)} /> 82 - </CenteredView> 83 - ) : ( 84 - <PostThreadComponent 85 - uri={resolvedUri?.uri} 86 - onPressReply={onPressReply} 87 - /> 88 - )} 89 - </View> 90 - {isMobile && ( 91 - <Animated.View 92 - style={[ 93 - styles.prompt, 94 - fabMinimalShellTransform, 95 - { 96 - bottom: clamp(safeAreaInsets.bottom, 15, 30), 97 - }, 98 - ]}> 99 - <ComposePrompt onPressCompose={onPressReply} /> 100 - </Animated.View> 75 + return ( 76 + <View style={s.hContentRegion}> 77 + {isMobile && <ViewHeader title="Post" />} 78 + <View style={s.flex1}> 79 + {uriError ? ( 80 + <CenteredView> 81 + <ErrorMessage message={String(uriError)} /> 82 + </CenteredView> 83 + ) : ( 84 + <PostThreadComponent 85 + uri={resolvedUri?.uri} 86 + onPressReply={onPressReply} 87 + /> 101 88 )} 102 89 </View> 103 - ) 104 - }), 105 - ) 90 + {isMobile && ( 91 + <Animated.View 92 + style={[ 93 + styles.prompt, 94 + fabMinimalShellTransform, 95 + { 96 + bottom: clamp(safeAreaInsets.bottom, 15, 30), 97 + }, 98 + ]}> 99 + <ComposePrompt onPressCompose={onPressReply} /> 100 + </Animated.View> 101 + )} 102 + </View> 103 + ) 104 + }) 106 105 107 106 const styles = StyleSheet.create({ 108 107 prompt: {
+2 -5
src/view/screens/PreferencesHomeFeed.tsx
··· 1 1 import React, {useState} from 'react' 2 2 import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' 3 - import {observer} from 'mobx-react-lite' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {Slider} from '@miblanchard/react-native-slider' 6 5 import {Text} from '../com/util/text/Text' ··· 72 71 CommonNavigatorParams, 73 72 'PreferencesHomeFeed' 74 73 > 75 - export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ 76 - navigation, 77 - }: Props) { 74 + export function PreferencesHomeFeed({navigation}: Props) { 78 75 const pal = usePalette('default') 79 76 const {_} = useLingui() 80 77 const {isTabletOrDesktop} = useWebMediaQueries() ··· 308 305 </View> 309 306 </CenteredView> 310 307 ) 311 - }) 308 + } 312 309 313 310 const styles = StyleSheet.create({ 314 311 container: {
+2 -5
src/view/screens/PreferencesThreads.tsx
··· 6 6 TouchableOpacity, 7 7 View, 8 8 } from 'react-native' 9 - import {observer} from 'mobx-react-lite' 10 9 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11 10 import {Text} from '../com/util/text/Text' 12 11 import {s, colors} from 'lib/styles' ··· 25 24 } from '#/state/queries/preferences' 26 25 27 26 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'> 28 - export const PreferencesThreads = observer(function PreferencesThreadsImpl({ 29 - navigation, 30 - }: Props) { 27 + export function PreferencesThreads({navigation}: Props) { 31 28 const pal = usePalette('default') 32 29 const {_} = useLingui() 33 30 const {isTabletOrDesktop} = useWebMediaQueries() ··· 162 159 </View> 163 160 </CenteredView> 164 161 ) 165 - }) 162 + } 166 163 167 164 const styles = StyleSheet.create({ 168 165 container: {
+5 -6
src/view/screens/ProfileFeed.tsx
··· 15 15 import {CommonNavigatorParams} from 'lib/routes/types' 16 16 import {makeRecordUri} from 'lib/strings/url-helpers' 17 17 import {colors, s} from 'lib/styles' 18 - import {observer} from 'mobx-react-lite' 19 18 import {FeedDescriptor} from '#/state/queries/post-feed' 20 19 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 21 20 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' ··· 71 70 72 71 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> 73 72 export const ProfileFeedScreen = withAuthRequired( 74 - observer(function ProfileFeedScreenImpl(props: Props) { 73 + function ProfileFeedScreenImpl(props: Props) { 75 74 const {rkey, name: handleOrDid} = props.route.params 76 75 77 76 const pal = usePalette('default') ··· 129 128 </View> 130 129 </CenteredView> 131 130 ) 132 - }), 131 + }, 133 132 ) 134 133 135 134 function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { ··· 154 153 ) 155 154 } 156 155 157 - export const ProfileFeedScreenInner = function ProfileFeedScreenInnerImpl({ 156 + export function ProfileFeedScreenInner({ 158 157 preferences, 159 158 feedInfo, 160 159 }: { ··· 485 484 }, 486 485 ) 487 486 488 - const AboutSection = observer(function AboutPageImpl({ 487 + function AboutSection({ 489 488 feedOwnerDid, 490 489 feedRkey, 491 490 feedInfo, ··· 606 605 </View> 607 606 </ScrollView> 608 607 ) 609 - }) 608 + } 610 609 611 610 const styles = StyleSheet.create({ 612 611 btn: {
+2 -3
src/view/screens/SavedFeeds.tsx
··· 14 14 import {useAnalytics} from 'lib/analytics/analytics' 15 15 import {usePalette} from 'lib/hooks/usePalette' 16 16 import {CommonNavigatorParams} from 'lib/routes/types' 17 - import {observer} from 'mobx-react-lite' 18 17 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 19 18 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 20 19 import {ViewHeader} from 'view/com/util/ViewHeader' ··· 146 145 ) 147 146 }) 148 147 149 - const ListItem = observer(function ListItemImpl({ 148 + function ListItem({ 150 149 feedUri, 151 150 isPinned, 152 151 }: { ··· 269 268 </TouchableOpacity> 270 269 </Pressable> 271 270 ) 272 - }) 271 + } 273 272 274 273 const styles = StyleSheet.create({ 275 274 desktopContainer: {
+560 -571
src/view/screens/Settings.tsx
··· 19 19 FontAwesomeIcon, 20 20 FontAwesomeIconStyle, 21 21 } from '@fortawesome/react-native-fontawesome' 22 - import {observer} from 'mobx-react-lite' 23 22 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 24 23 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 25 24 import * as AppInfo from 'lib/app-info' 26 - import {useStores} from 'state/index' 27 25 import {s, colors} from 'lib/styles' 28 26 import {ScrollView} from '../com/util/Views' 29 27 import {ViewHeader} from '../com/util/ViewHeader' ··· 45 43 import Clipboard from '@react-native-clipboard/clipboard' 46 44 import {makeProfileLink} from 'lib/routes/links' 47 45 import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' 48 - import {logger} from '#/logger' 46 + import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' 49 47 import {useModalControls} from '#/state/modals' 50 48 import { 51 49 useSetMinimalShellMode, ··· 69 67 import {STATUS_PAGE_URL} from 'lib/constants' 70 68 import {Trans, msg} from '@lingui/macro' 71 69 import {useLingui} from '@lingui/react' 70 + import {useQueryClient} from '@tanstack/react-query' 72 71 73 72 function SettingsAccountCard({account}: {account: SessionAccount}) { 74 73 const pal = usePalette('default') ··· 135 134 } 136 135 137 136 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> 138 - export const SettingsScreen = withAuthRequired( 139 - observer(function Settings({}: Props) { 140 - const colorMode = useColorMode() 141 - const setColorMode = useSetColorMode() 142 - const pal = usePalette('default') 143 - const store = useStores() 144 - const {_} = useLingui() 145 - const setMinimalShellMode = useSetMinimalShellMode() 146 - const requireAltTextEnabled = useRequireAltTextEnabled() 147 - const setRequireAltTextEnabled = useSetRequireAltTextEnabled() 148 - const onboardingDispatch = useOnboardingDispatch() 149 - const navigation = useNavigation<NavigationProp>() 150 - const {isMobile} = useWebMediaQueries() 151 - const {screen, track} = useAnalytics() 152 - const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting( 153 - store.agent, 154 - ) 155 - const {openModal} = useModalControls() 156 - const {isSwitchingAccounts, accounts, currentAccount} = useSession() 157 - const {clearCurrentAccount} = useSessionApi() 158 - const {mutate: clearPreferences} = useClearPreferencesMutation() 159 - const {data: invites} = useInviteCodesQuery() 160 - const invitesAvailable = invites?.available?.length ?? 0 137 + export const SettingsScreen = withAuthRequired(function Settings({}: Props) { 138 + const queryClient = useQueryClient() 139 + const colorMode = useColorMode() 140 + const setColorMode = useSetColorMode() 141 + const pal = usePalette('default') 142 + const {_} = useLingui() 143 + const setMinimalShellMode = useSetMinimalShellMode() 144 + const requireAltTextEnabled = useRequireAltTextEnabled() 145 + const setRequireAltTextEnabled = useSetRequireAltTextEnabled() 146 + const onboardingDispatch = useOnboardingDispatch() 147 + const navigation = useNavigation<NavigationProp>() 148 + const {isMobile} = useWebMediaQueries() 149 + const {screen, track} = useAnalytics() 150 + const {openModal} = useModalControls() 151 + const {isSwitchingAccounts, accounts, currentAccount, agent} = useSession() 152 + const {clearCurrentAccount} = useSessionApi() 153 + const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(agent) 154 + const {mutate: clearPreferences} = useClearPreferencesMutation() 155 + const {data: invites} = useInviteCodesQuery() 156 + const invitesAvailable = invites?.available?.length ?? 0 161 157 162 - const primaryBg = useCustomPalette<ViewStyle>({ 163 - light: {backgroundColor: colors.blue0}, 164 - dark: {backgroundColor: colors.blue6}, 165 - }) 166 - const primaryText = useCustomPalette<TextStyle>({ 167 - light: {color: colors.blue3}, 168 - dark: {color: colors.blue2}, 169 - }) 158 + const primaryBg = useCustomPalette<ViewStyle>({ 159 + light: {backgroundColor: colors.blue0}, 160 + dark: {backgroundColor: colors.blue6}, 161 + }) 162 + const primaryText = useCustomPalette<TextStyle>({ 163 + light: {color: colors.blue3}, 164 + dark: {color: colors.blue2}, 165 + }) 170 166 171 - const dangerBg = useCustomPalette<ViewStyle>({ 172 - light: {backgroundColor: colors.red1}, 173 - dark: {backgroundColor: colors.red7}, 174 - }) 175 - const dangerText = useCustomPalette<TextStyle>({ 176 - light: {color: colors.red4}, 177 - dark: {color: colors.red2}, 178 - }) 167 + const dangerBg = useCustomPalette<ViewStyle>({ 168 + light: {backgroundColor: colors.red1}, 169 + dark: {backgroundColor: colors.red7}, 170 + }) 171 + const dangerText = useCustomPalette<TextStyle>({ 172 + light: {color: colors.red4}, 173 + dark: {color: colors.red2}, 174 + }) 179 175 180 - useFocusEffect( 181 - React.useCallback(() => { 182 - screen('Settings') 183 - setMinimalShellMode(false) 184 - }, [screen, setMinimalShellMode]), 185 - ) 176 + useFocusEffect( 177 + React.useCallback(() => { 178 + screen('Settings') 179 + setMinimalShellMode(false) 180 + }, [screen, setMinimalShellMode]), 181 + ) 186 182 187 - const onPressAddAccount = React.useCallback(() => { 188 - track('Settings:AddAccountButtonClicked') 189 - navigation.navigate('HomeTab') 190 - navigation.dispatch(StackActions.popToTop()) 191 - clearCurrentAccount() 192 - }, [track, navigation, clearCurrentAccount]) 183 + const onPressAddAccount = React.useCallback(() => { 184 + track('Settings:AddAccountButtonClicked') 185 + navigation.navigate('HomeTab') 186 + navigation.dispatch(StackActions.popToTop()) 187 + clearCurrentAccount() 188 + }, [track, navigation, clearCurrentAccount]) 193 189 194 - const onPressChangeHandle = React.useCallback(() => { 195 - track('Settings:ChangeHandleButtonClicked') 196 - openModal({ 197 - name: 'change-handle', 198 - onChanged() { 199 - store.session.reloadFromServer().then( 200 - () => { 201 - Toast.show('Your handle has been updated') 202 - }, 203 - err => { 204 - logger.error('Failed to reload from server after handle update', { 205 - error: err, 206 - }) 207 - }, 208 - ) 209 - }, 210 - }) 211 - }, [track, store, openModal]) 190 + const onPressChangeHandle = React.useCallback(() => { 191 + track('Settings:ChangeHandleButtonClicked') 192 + openModal({ 193 + name: 'change-handle', 194 + onChanged() { 195 + if (currentAccount) { 196 + // refresh my profile 197 + queryClient.invalidateQueries({ 198 + queryKey: RQKEY_PROFILE(currentAccount.did), 199 + }) 200 + } 201 + }, 202 + }) 203 + }, [track, queryClient, openModal, currentAccount]) 212 204 213 - const onPressInviteCodes = React.useCallback(() => { 214 - track('Settings:InvitecodesButtonClicked') 215 - openModal({name: 'invite-codes'}) 216 - }, [track, openModal]) 205 + const onPressInviteCodes = React.useCallback(() => { 206 + track('Settings:InvitecodesButtonClicked') 207 + openModal({name: 'invite-codes'}) 208 + }, [track, openModal]) 217 209 218 - const onPressLanguageSettings = React.useCallback(() => { 219 - navigation.navigate('LanguageSettings') 220 - }, [navigation]) 210 + const onPressLanguageSettings = React.useCallback(() => { 211 + navigation.navigate('LanguageSettings') 212 + }, [navigation]) 221 213 222 - const onPressDeleteAccount = React.useCallback(() => { 223 - openModal({name: 'delete-account'}) 224 - }, [openModal]) 214 + const onPressDeleteAccount = React.useCallback(() => { 215 + openModal({name: 'delete-account'}) 216 + }, [openModal]) 225 217 226 - const onPressResetPreferences = React.useCallback(async () => { 227 - clearPreferences() 228 - }, [clearPreferences]) 218 + const onPressResetPreferences = React.useCallback(async () => { 219 + clearPreferences() 220 + }, [clearPreferences]) 229 221 230 - const onPressResetOnboarding = React.useCallback(async () => { 231 - onboardingDispatch({type: 'start'}) 232 - Toast.show('Onboarding reset') 233 - }, [onboardingDispatch]) 222 + const onPressResetOnboarding = React.useCallback(async () => { 223 + onboardingDispatch({type: 'start'}) 224 + Toast.show('Onboarding reset') 225 + }, [onboardingDispatch]) 234 226 235 - const onPressBuildInfo = React.useCallback(() => { 236 - Clipboard.setString( 237 - `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, 238 - ) 239 - Toast.show('Copied build version to clipboard') 240 - }, []) 227 + const onPressBuildInfo = React.useCallback(() => { 228 + Clipboard.setString( 229 + `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, 230 + ) 231 + Toast.show('Copied build version to clipboard') 232 + }, []) 241 233 242 - const openHomeFeedPreferences = React.useCallback(() => { 243 - navigation.navigate('PreferencesHomeFeed') 244 - }, [navigation]) 234 + const openHomeFeedPreferences = React.useCallback(() => { 235 + navigation.navigate('PreferencesHomeFeed') 236 + }, [navigation]) 245 237 246 - const openThreadsPreferences = React.useCallback(() => { 247 - navigation.navigate('PreferencesThreads') 248 - }, [navigation]) 238 + const openThreadsPreferences = React.useCallback(() => { 239 + navigation.navigate('PreferencesThreads') 240 + }, [navigation]) 249 241 250 - const onPressAppPasswords = React.useCallback(() => { 251 - navigation.navigate('AppPasswords') 252 - }, [navigation]) 242 + const onPressAppPasswords = React.useCallback(() => { 243 + navigation.navigate('AppPasswords') 244 + }, [navigation]) 253 245 254 - const onPressSystemLog = React.useCallback(() => { 255 - navigation.navigate('Log') 256 - }, [navigation]) 246 + const onPressSystemLog = React.useCallback(() => { 247 + navigation.navigate('Log') 248 + }, [navigation]) 257 249 258 - const onPressStorybook = React.useCallback(() => { 259 - navigation.navigate('Debug') 260 - }, [navigation]) 250 + const onPressStorybook = React.useCallback(() => { 251 + navigation.navigate('Debug') 252 + }, [navigation]) 261 253 262 - const onPressSavedFeeds = React.useCallback(() => { 263 - navigation.navigate('SavedFeeds') 264 - }, [navigation]) 254 + const onPressSavedFeeds = React.useCallback(() => { 255 + navigation.navigate('SavedFeeds') 256 + }, [navigation]) 265 257 266 - const onPressStatusPage = React.useCallback(() => { 267 - Linking.openURL(STATUS_PAGE_URL) 268 - }, []) 258 + const onPressStatusPage = React.useCallback(() => { 259 + Linking.openURL(STATUS_PAGE_URL) 260 + }, []) 269 261 270 - return ( 271 - <View style={[s.hContentRegion]} testID="settingsScreen"> 272 - <ViewHeader title="Settings" /> 273 - <ScrollView 274 - style={[s.hContentRegion]} 275 - contentContainerStyle={isMobile && pal.viewLight} 276 - scrollIndicatorInsets={{right: 1}}> 277 - <View style={styles.spacer20} /> 278 - {currentAccount ? ( 279 - <> 280 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 281 - <Trans>Account</Trans> 262 + return ( 263 + <View style={[s.hContentRegion]} testID="settingsScreen"> 264 + <ViewHeader title="Settings" /> 265 + <ScrollView 266 + style={[s.hContentRegion]} 267 + contentContainerStyle={isMobile && pal.viewLight} 268 + scrollIndicatorInsets={{right: 1}}> 269 + <View style={styles.spacer20} /> 270 + {currentAccount ? ( 271 + <> 272 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 273 + <Trans>Account</Trans> 274 + </Text> 275 + <View style={[styles.infoLine]}> 276 + <Text type="lg-medium" style={pal.text}> 277 + Email:{' '} 282 278 </Text> 283 - <View style={[styles.infoLine]}> 284 - <Text type="lg-medium" style={pal.text}> 285 - Email:{' '} 279 + {currentAccount.emailConfirmed && ( 280 + <> 281 + <FontAwesomeIcon 282 + icon="check" 283 + size={10} 284 + style={{color: colors.green3, marginRight: 2}} 285 + /> 286 + </> 287 + )} 288 + <Text type="lg" style={pal.text}> 289 + {currentAccount.email}{' '} 290 + </Text> 291 + <Link onPress={() => openModal({name: 'change-email'})}> 292 + <Text type="lg" style={pal.link}> 293 + <Trans>Change</Trans> 286 294 </Text> 287 - {currentAccount.emailConfirmed && ( 288 - <> 289 - <FontAwesomeIcon 290 - icon="check" 291 - size={10} 292 - style={{color: colors.green3, marginRight: 2}} 293 - /> 294 - </> 295 - )} 296 - <Text type="lg" style={pal.text}> 297 - {currentAccount.email}{' '} 298 - </Text> 299 - <Link onPress={() => openModal({name: 'change-email'})}> 300 - <Text type="lg" style={pal.link}> 301 - <Trans>Change</Trans> 302 - </Text> 303 - </Link> 304 - </View> 305 - <View style={[styles.infoLine]}> 306 - <Text type="lg-medium" style={pal.text}> 307 - <Trans>Birthday:</Trans>{' '} 295 + </Link> 296 + </View> 297 + <View style={[styles.infoLine]}> 298 + <Text type="lg-medium" style={pal.text}> 299 + <Trans>Birthday:</Trans>{' '} 300 + </Text> 301 + <Link onPress={() => openModal({name: 'birth-date-settings'})}> 302 + <Text type="lg" style={pal.link}> 303 + <Trans>Show</Trans> 308 304 </Text> 309 - <Link onPress={() => openModal({name: 'birth-date-settings'})}> 310 - <Text type="lg" style={pal.link}> 311 - <Trans>Show</Trans> 312 - </Text> 313 - </Link> 314 - </View> 315 - <View style={styles.spacer20} /> 305 + </Link> 306 + </View> 307 + <View style={styles.spacer20} /> 308 + 309 + {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} 310 + </> 311 + ) : null} 312 + <View style={[s.flexRow, styles.heading]}> 313 + <Text type="xl-bold" style={pal.text}> 314 + <Trans>Signed in as</Trans> 315 + </Text> 316 + <View style={s.flex1} /> 317 + </View> 316 318 317 - {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} 318 - </> 319 - ) : null} 320 - <View style={[s.flexRow, styles.heading]}> 321 - <Text type="xl-bold" style={pal.text}> 322 - <Trans>Signed in as</Trans> 323 - </Text> 324 - <View style={s.flex1} /> 319 + {isSwitchingAccounts ? ( 320 + <View style={[pal.view, styles.linkCard]}> 321 + <ActivityIndicator /> 325 322 </View> 323 + ) : ( 324 + <SettingsAccountCard account={currentAccount!} /> 325 + )} 326 326 327 - {isSwitchingAccounts ? ( 328 - <View style={[pal.view, styles.linkCard]}> 329 - <ActivityIndicator /> 330 - </View> 331 - ) : ( 332 - <SettingsAccountCard account={currentAccount!} /> 333 - )} 327 + {accounts 328 + .filter(a => a.did !== currentAccount?.did) 329 + .map(account => ( 330 + <SettingsAccountCard key={account.did} account={account} /> 331 + ))} 334 332 335 - {accounts 336 - .filter(a => a.did !== currentAccount?.did) 337 - .map(account => ( 338 - <SettingsAccountCard key={account.did} account={account} /> 339 - ))} 333 + <TouchableOpacity 334 + testID="switchToNewAccountBtn" 335 + style={[ 336 + styles.linkCard, 337 + pal.view, 338 + isSwitchingAccounts && styles.dimmed, 339 + ]} 340 + onPress={isSwitchingAccounts ? undefined : onPressAddAccount} 341 + accessibilityRole="button" 342 + accessibilityLabel={_(msg`Add account`)} 343 + accessibilityHint="Create a new Bluesky account"> 344 + <View style={[styles.iconContainer, pal.btn]}> 345 + <FontAwesomeIcon 346 + icon="plus" 347 + style={pal.text as FontAwesomeIconStyle} 348 + /> 349 + </View> 350 + <Text type="lg" style={pal.text}> 351 + <Trans>Add account</Trans> 352 + </Text> 353 + </TouchableOpacity> 340 354 341 - <TouchableOpacity 342 - testID="switchToNewAccountBtn" 355 + <View style={styles.spacer20} /> 356 + 357 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 358 + <Trans>Invite a Friend</Trans> 359 + </Text> 360 + 361 + <TouchableOpacity 362 + testID="inviteFriendBtn" 363 + style={[ 364 + styles.linkCard, 365 + pal.view, 366 + isSwitchingAccounts && styles.dimmed, 367 + ]} 368 + onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} 369 + accessibilityRole="button" 370 + accessibilityLabel={_(msg`Invite`)} 371 + accessibilityHint="Opens invite code list"> 372 + <View 343 373 style={[ 344 - styles.linkCard, 345 - pal.view, 346 - isSwitchingAccounts && styles.dimmed, 347 - ]} 348 - onPress={isSwitchingAccounts ? undefined : onPressAddAccount} 349 - accessibilityRole="button" 350 - accessibilityLabel={_(msg`Add account`)} 351 - accessibilityHint="Create a new Bluesky account"> 352 - <View style={[styles.iconContainer, pal.btn]}> 353 - <FontAwesomeIcon 354 - icon="plus" 355 - style={pal.text as FontAwesomeIconStyle} 356 - /> 357 - </View> 358 - <Text type="lg" style={pal.text}> 359 - <Trans>Add account</Trans> 360 - </Text> 361 - </TouchableOpacity> 374 + styles.iconContainer, 375 + invitesAvailable > 0 ? primaryBg : pal.btn, 376 + ]}> 377 + <FontAwesomeIcon 378 + icon="ticket" 379 + style={ 380 + (invitesAvailable > 0 381 + ? primaryText 382 + : pal.text) as FontAwesomeIconStyle 383 + } 384 + /> 385 + </View> 386 + <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}> 387 + {formatCount(invitesAvailable)} invite{' '} 388 + {pluralize(invitesAvailable, 'code')} available 389 + </Text> 390 + </TouchableOpacity> 362 391 363 - <View style={styles.spacer20} /> 392 + <View style={styles.spacer20} /> 364 393 365 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 366 - <Trans>Invite a Friend</Trans> 367 - </Text> 394 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 395 + <Trans>Accessibility</Trans> 396 + </Text> 397 + <View style={[pal.view, styles.toggleCard]}> 398 + <ToggleButton 399 + type="default-light" 400 + label="Require alt text before posting" 401 + labelType="lg" 402 + isSelected={requireAltTextEnabled} 403 + onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} 404 + /> 405 + </View> 368 406 369 - <TouchableOpacity 370 - testID="inviteFriendBtn" 371 - style={[ 372 - styles.linkCard, 373 - pal.view, 374 - isSwitchingAccounts && styles.dimmed, 375 - ]} 376 - onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} 377 - accessibilityRole="button" 378 - accessibilityLabel={_(msg`Invite`)} 379 - accessibilityHint="Opens invite code list"> 380 - <View 381 - style={[ 382 - styles.iconContainer, 383 - invitesAvailable > 0 ? primaryBg : pal.btn, 384 - ]}> 385 - <FontAwesomeIcon 386 - icon="ticket" 387 - style={ 388 - (invitesAvailable > 0 389 - ? primaryText 390 - : pal.text) as FontAwesomeIconStyle 391 - } 392 - /> 393 - </View> 394 - <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}> 395 - {formatCount(invitesAvailable)} invite{' '} 396 - {pluralize(invitesAvailable, 'code')} available 397 - </Text> 398 - </TouchableOpacity> 407 + <View style={styles.spacer20} /> 399 408 400 - <View style={styles.spacer20} /> 409 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 410 + <Trans>Appearance</Trans> 411 + </Text> 412 + <View> 413 + <View style={[styles.linkCard, pal.view, styles.selectableBtns]}> 414 + <SelectableBtn 415 + selected={colorMode === 'system'} 416 + label="System" 417 + left 418 + onSelect={() => setColorMode('system')} 419 + accessibilityHint="Set color theme to system setting" 420 + /> 421 + <SelectableBtn 422 + selected={colorMode === 'light'} 423 + label="Light" 424 + onSelect={() => setColorMode('light')} 425 + accessibilityHint="Set color theme to light" 426 + /> 427 + <SelectableBtn 428 + selected={colorMode === 'dark'} 429 + label="Dark" 430 + right 431 + onSelect={() => setColorMode('dark')} 432 + accessibilityHint="Set color theme to dark" 433 + /> 434 + </View> 435 + </View> 436 + <View style={styles.spacer20} /> 401 437 402 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 403 - <Trans>Accessibility</Trans> 438 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 439 + <Trans>Basics</Trans> 440 + </Text> 441 + <TouchableOpacity 442 + testID="preferencesHomeFeedButton" 443 + style={[ 444 + styles.linkCard, 445 + pal.view, 446 + isSwitchingAccounts && styles.dimmed, 447 + ]} 448 + onPress={openHomeFeedPreferences} 449 + accessibilityRole="button" 450 + accessibilityHint="" 451 + accessibilityLabel={_(msg`Opens the home feed preferences`)}> 452 + <View style={[styles.iconContainer, pal.btn]}> 453 + <FontAwesomeIcon 454 + icon="sliders" 455 + style={pal.text as FontAwesomeIconStyle} 456 + /> 457 + </View> 458 + <Text type="lg" style={pal.text}> 459 + <Trans>Home Feed Preferences</Trans> 460 + </Text> 461 + </TouchableOpacity> 462 + <TouchableOpacity 463 + testID="preferencesThreadsButton" 464 + style={[ 465 + styles.linkCard, 466 + pal.view, 467 + isSwitchingAccounts && styles.dimmed, 468 + ]} 469 + onPress={openThreadsPreferences} 470 + accessibilityRole="button" 471 + accessibilityHint="" 472 + accessibilityLabel={_(msg`Opens the threads preferences`)}> 473 + <View style={[styles.iconContainer, pal.btn]}> 474 + <FontAwesomeIcon 475 + icon={['far', 'comments']} 476 + style={pal.text as FontAwesomeIconStyle} 477 + size={18} 478 + /> 479 + </View> 480 + <Text type="lg" style={pal.text}> 481 + <Trans>Thread Preferences</Trans> 482 + </Text> 483 + </TouchableOpacity> 484 + <TouchableOpacity 485 + testID="savedFeedsBtn" 486 + style={[ 487 + styles.linkCard, 488 + pal.view, 489 + isSwitchingAccounts && styles.dimmed, 490 + ]} 491 + accessibilityHint="My Saved Feeds" 492 + accessibilityLabel={_(msg`Opens screen with all saved feeds`)} 493 + onPress={onPressSavedFeeds}> 494 + <View style={[styles.iconContainer, pal.btn]}> 495 + <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> 496 + </View> 497 + <Text type="lg" style={pal.text}> 498 + <Trans>My Saved Feeds</Trans> 404 499 </Text> 405 - <View style={[pal.view, styles.toggleCard]}> 406 - <ToggleButton 407 - type="default-light" 408 - label="Require alt text before posting" 409 - labelType="lg" 410 - isSelected={requireAltTextEnabled} 411 - onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} 500 + </TouchableOpacity> 501 + <TouchableOpacity 502 + testID="languageSettingsBtn" 503 + style={[ 504 + styles.linkCard, 505 + pal.view, 506 + isSwitchingAccounts && styles.dimmed, 507 + ]} 508 + onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} 509 + accessibilityRole="button" 510 + accessibilityHint="Language settings" 511 + accessibilityLabel={_(msg`Opens configurable language settings`)}> 512 + <View style={[styles.iconContainer, pal.btn]}> 513 + <FontAwesomeIcon 514 + icon="language" 515 + style={pal.text as FontAwesomeIconStyle} 412 516 /> 413 517 </View> 518 + <Text type="lg" style={pal.text}> 519 + <Trans>Languages</Trans> 520 + </Text> 521 + </TouchableOpacity> 522 + <TouchableOpacity 523 + testID="moderationBtn" 524 + style={[ 525 + styles.linkCard, 526 + pal.view, 527 + isSwitchingAccounts && styles.dimmed, 528 + ]} 529 + onPress={ 530 + isSwitchingAccounts 531 + ? undefined 532 + : () => navigation.navigate('Moderation') 533 + } 534 + accessibilityRole="button" 535 + accessibilityHint="" 536 + accessibilityLabel={_(msg`Opens moderation settings`)}> 537 + <View style={[styles.iconContainer, pal.btn]}> 538 + <HandIcon style={pal.text} size={18} strokeWidth={6} /> 539 + </View> 540 + <Text type="lg" style={pal.text}> 541 + <Trans>Moderation</Trans> 542 + </Text> 543 + </TouchableOpacity> 544 + <View style={styles.spacer20} /> 414 545 415 - <View style={styles.spacer20} /> 416 - 417 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 418 - <Trans>Appearance</Trans> 546 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 547 + <Trans>Advanced</Trans> 548 + </Text> 549 + <TouchableOpacity 550 + testID="appPasswordBtn" 551 + style={[ 552 + styles.linkCard, 553 + pal.view, 554 + isSwitchingAccounts && styles.dimmed, 555 + ]} 556 + onPress={onPressAppPasswords} 557 + accessibilityRole="button" 558 + accessibilityHint="Open app password settings" 559 + accessibilityLabel={_(msg`Opens the app password settings page`)}> 560 + <View style={[styles.iconContainer, pal.btn]}> 561 + <FontAwesomeIcon 562 + icon="lock" 563 + style={pal.text as FontAwesomeIconStyle} 564 + /> 565 + </View> 566 + <Text type="lg" style={pal.text}> 567 + <Trans>App passwords</Trans> 419 568 </Text> 420 - <View> 421 - <View style={[styles.linkCard, pal.view, styles.selectableBtns]}> 422 - <SelectableBtn 423 - selected={colorMode === 'system'} 424 - label="System" 425 - left 426 - onSelect={() => setColorMode('system')} 427 - accessibilityHint="Set color theme to system setting" 428 - /> 429 - <SelectableBtn 430 - selected={colorMode === 'light'} 431 - label="Light" 432 - onSelect={() => setColorMode('light')} 433 - accessibilityHint="Set color theme to light" 434 - /> 435 - <SelectableBtn 436 - selected={colorMode === 'dark'} 437 - label="Dark" 438 - right 439 - onSelect={() => setColorMode('dark')} 440 - accessibilityHint="Set color theme to dark" 441 - /> 442 - </View> 569 + </TouchableOpacity> 570 + <TouchableOpacity 571 + testID="changeHandleBtn" 572 + style={[ 573 + styles.linkCard, 574 + pal.view, 575 + isSwitchingAccounts && styles.dimmed, 576 + ]} 577 + onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} 578 + accessibilityRole="button" 579 + accessibilityLabel={_(msg`Change handle`)} 580 + accessibilityHint="Choose a new Bluesky username or create"> 581 + <View style={[styles.iconContainer, pal.btn]}> 582 + <FontAwesomeIcon 583 + icon="at" 584 + style={pal.text as FontAwesomeIconStyle} 585 + /> 586 + </View> 587 + <Text type="lg" style={pal.text} numberOfLines={1}> 588 + <Trans>Change handle</Trans> 589 + </Text> 590 + </TouchableOpacity> 591 + <View style={styles.spacer20} /> 592 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 593 + <Trans>Danger Zone</Trans> 594 + </Text> 595 + <TouchableOpacity 596 + style={[pal.view, styles.linkCard]} 597 + onPress={onPressDeleteAccount} 598 + accessible={true} 599 + accessibilityRole="button" 600 + accessibilityLabel={_(msg`Delete account`)} 601 + accessibilityHint="Opens modal for account deletion confirmation. Requires email code."> 602 + <View style={[styles.iconContainer, dangerBg]}> 603 + <FontAwesomeIcon 604 + icon={['far', 'trash-can']} 605 + style={dangerText as FontAwesomeIconStyle} 606 + size={18} 607 + /> 443 608 </View> 444 - <View style={styles.spacer20} /> 445 - 446 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 447 - <Trans>Basics</Trans> 609 + <Text type="lg" style={dangerText}> 610 + <Trans>Delete my account…</Trans> 611 + </Text> 612 + </TouchableOpacity> 613 + <View style={styles.spacer20} /> 614 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 615 + <Trans>Developer Tools</Trans> 616 + </Text> 617 + <TouchableOpacity 618 + style={[pal.view, styles.linkCardNoIcon]} 619 + onPress={onPressSystemLog} 620 + accessibilityRole="button" 621 + accessibilityHint="Open system log" 622 + accessibilityLabel={_(msg`Opens the system log page`)}> 623 + <Text type="lg" style={pal.text}> 624 + <Trans>System log</Trans> 448 625 </Text> 626 + </TouchableOpacity> 627 + {__DEV__ ? ( 628 + <ToggleButton 629 + type="default-light" 630 + label="Experiment: Use AppView Proxy" 631 + isSelected={debugHeaderEnabled} 632 + onPress={toggleDebugHeader} 633 + /> 634 + ) : null} 635 + {__DEV__ ? ( 636 + <> 637 + <TouchableOpacity 638 + style={[pal.view, styles.linkCardNoIcon]} 639 + onPress={onPressStorybook} 640 + accessibilityRole="button" 641 + accessibilityHint="Open storybook page" 642 + accessibilityLabel={_(msg`Opens the storybook page`)}> 643 + <Text type="lg" style={pal.text}> 644 + <Trans>Storybook</Trans> 645 + </Text> 646 + </TouchableOpacity> 647 + <TouchableOpacity 648 + style={[pal.view, styles.linkCardNoIcon]} 649 + onPress={onPressResetPreferences} 650 + accessibilityRole="button" 651 + accessibilityHint="Reset preferences" 652 + accessibilityLabel={_(msg`Resets the preferences state`)}> 653 + <Text type="lg" style={pal.text}> 654 + <Trans>Reset preferences state</Trans> 655 + </Text> 656 + </TouchableOpacity> 657 + <TouchableOpacity 658 + style={[pal.view, styles.linkCardNoIcon]} 659 + onPress={onPressResetOnboarding} 660 + accessibilityRole="button" 661 + accessibilityHint="Reset onboarding" 662 + accessibilityLabel={_(msg`Resets the onboarding state`)}> 663 + <Text type="lg" style={pal.text}> 664 + <Trans>Reset onboarding state</Trans> 665 + </Text> 666 + </TouchableOpacity> 667 + </> 668 + ) : null} 669 + <View style={[styles.footer]}> 449 670 <TouchableOpacity 450 - testID="preferencesHomeFeedButton" 451 - style={[ 452 - styles.linkCard, 453 - pal.view, 454 - isSwitchingAccounts && styles.dimmed, 455 - ]} 456 - onPress={openHomeFeedPreferences} 457 671 accessibilityRole="button" 458 - accessibilityHint="" 459 - accessibilityLabel={_(msg`Opens the home feed preferences`)}> 460 - <View style={[styles.iconContainer, pal.btn]}> 461 - <FontAwesomeIcon 462 - icon="sliders" 463 - style={pal.text as FontAwesomeIconStyle} 464 - /> 465 - </View> 466 - <Text type="lg" style={pal.text}> 467 - <Trans>Home Feed Preferences</Trans> 672 + onPress={onPressBuildInfo}> 673 + <Text type="sm" style={[styles.buildInfo, pal.textLight]}> 674 + <Trans> 675 + Build version {AppInfo.appVersion} {AppInfo.updateChannel} 676 + </Trans> 468 677 </Text> 469 678 </TouchableOpacity> 679 + <Text type="sm" style={[pal.textLight]}> 680 + &middot; &nbsp; 681 + </Text> 470 682 <TouchableOpacity 471 - testID="preferencesThreadsButton" 472 - style={[ 473 - styles.linkCard, 474 - pal.view, 475 - isSwitchingAccounts && styles.dimmed, 476 - ]} 477 - onPress={openThreadsPreferences} 478 683 accessibilityRole="button" 479 - accessibilityHint="" 480 - accessibilityLabel={_(msg`Opens the threads preferences`)}> 481 - <View style={[styles.iconContainer, pal.btn]}> 482 - <FontAwesomeIcon 483 - icon={['far', 'comments']} 484 - style={pal.text as FontAwesomeIconStyle} 485 - size={18} 486 - /> 487 - </View> 488 - <Text type="lg" style={pal.text}> 489 - <Trans>Thread Preferences</Trans> 684 + onPress={onPressStatusPage}> 685 + <Text type="sm" style={[styles.buildInfo, pal.textLight]}> 686 + <Trans>Status page</Trans> 490 687 </Text> 491 688 </TouchableOpacity> 492 - <TouchableOpacity 493 - testID="savedFeedsBtn" 689 + </View> 690 + <View style={s.footerSpacer} /> 691 + </ScrollView> 692 + </View> 693 + ) 694 + }) 695 + 696 + function EmailConfirmationNotice() { 697 + const pal = usePalette('default') 698 + const palInverted = usePalette('inverted') 699 + const {_} = useLingui() 700 + const {isMobile} = useWebMediaQueries() 701 + const {openModal} = useModalControls() 702 + 703 + return ( 704 + <View style={{marginBottom: 20}}> 705 + <Text type="xl-bold" style={[pal.text, styles.heading]}> 706 + <Trans>Verify email</Trans> 707 + </Text> 708 + <View 709 + style={[ 710 + { 711 + paddingVertical: isMobile ? 12 : 0, 712 + paddingHorizontal: 18, 713 + }, 714 + pal.view, 715 + ]}> 716 + <View style={{flexDirection: 'row', marginBottom: 8}}> 717 + <Pressable 494 718 style={[ 495 - styles.linkCard, 496 - pal.view, 497 - isSwitchingAccounts && styles.dimmed, 498 - ]} 499 - accessibilityHint="My Saved Feeds" 500 - accessibilityLabel={_(msg`Opens screen with all saved feeds`)} 501 - onPress={onPressSavedFeeds}> 502 - <View style={[styles.iconContainer, pal.btn]}> 503 - <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> 504 - </View> 505 - <Text type="lg" style={pal.text}> 506 - <Trans>My Saved Feeds</Trans> 507 - </Text> 508 - </TouchableOpacity> 509 - <TouchableOpacity 510 - testID="languageSettingsBtn" 511 - style={[ 512 - styles.linkCard, 513 - pal.view, 514 - isSwitchingAccounts && styles.dimmed, 719 + palInverted.view, 720 + { 721 + flexDirection: 'row', 722 + gap: 6, 723 + borderRadius: 6, 724 + paddingHorizontal: 12, 725 + paddingVertical: 10, 726 + alignItems: 'center', 727 + }, 728 + isMobile && {flex: 1}, 515 729 ]} 516 - onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} 517 730 accessibilityRole="button" 518 - accessibilityHint="Language settings" 519 - accessibilityLabel={_(msg`Opens configurable language settings`)}> 520 - <View style={[styles.iconContainer, pal.btn]}> 521 - <FontAwesomeIcon 522 - icon="language" 523 - style={pal.text as FontAwesomeIconStyle} 524 - /> 525 - </View> 526 - <Text type="lg" style={pal.text}> 527 - <Trans>Languages</Trans> 528 - </Text> 529 - </TouchableOpacity> 530 - <TouchableOpacity 531 - testID="moderationBtn" 532 - style={[ 533 - styles.linkCard, 534 - pal.view, 535 - isSwitchingAccounts && styles.dimmed, 536 - ]} 537 - onPress={ 538 - isSwitchingAccounts 539 - ? undefined 540 - : () => navigation.navigate('Moderation') 541 - } 542 - accessibilityRole="button" 731 + accessibilityLabel={_(msg`Verify my email`)} 543 732 accessibilityHint="" 544 - accessibilityLabel={_(msg`Opens moderation settings`)}> 545 - <View style={[styles.iconContainer, pal.btn]}> 546 - <HandIcon style={pal.text} size={18} strokeWidth={6} /> 547 - </View> 548 - <Text type="lg" style={pal.text}> 549 - <Trans>Moderation</Trans> 550 - </Text> 551 - </TouchableOpacity> 552 - <View style={styles.spacer20} /> 553 - 554 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 555 - <Trans>Advanced</Trans> 556 - </Text> 557 - <TouchableOpacity 558 - testID="appPasswordBtn" 559 - style={[ 560 - styles.linkCard, 561 - pal.view, 562 - isSwitchingAccounts && styles.dimmed, 563 - ]} 564 - onPress={onPressAppPasswords} 565 - accessibilityRole="button" 566 - accessibilityHint="Open app password settings" 567 - accessibilityLabel={_(msg`Opens the app password settings page`)}> 568 - <View style={[styles.iconContainer, pal.btn]}> 569 - <FontAwesomeIcon 570 - icon="lock" 571 - style={pal.text as FontAwesomeIconStyle} 572 - /> 573 - </View> 574 - <Text type="lg" style={pal.text}> 575 - <Trans>App passwords</Trans> 576 - </Text> 577 - </TouchableOpacity> 578 - <TouchableOpacity 579 - testID="changeHandleBtn" 580 - style={[ 581 - styles.linkCard, 582 - pal.view, 583 - isSwitchingAccounts && styles.dimmed, 584 - ]} 585 - onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} 586 - accessibilityRole="button" 587 - accessibilityLabel={_(msg`Change handle`)} 588 - accessibilityHint="Choose a new Bluesky username or create"> 589 - <View style={[styles.iconContainer, pal.btn]}> 590 - <FontAwesomeIcon 591 - icon="at" 592 - style={pal.text as FontAwesomeIconStyle} 593 - /> 594 - </View> 595 - <Text type="lg" style={pal.text} numberOfLines={1}> 596 - <Trans>Change handle</Trans> 597 - </Text> 598 - </TouchableOpacity> 599 - <View style={styles.spacer20} /> 600 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 601 - <Trans>Danger Zone</Trans> 602 - </Text> 603 - <TouchableOpacity 604 - style={[pal.view, styles.linkCard]} 605 - onPress={onPressDeleteAccount} 606 - accessible={true} 607 - accessibilityRole="button" 608 - accessibilityLabel={_(msg`Delete account`)} 609 - accessibilityHint="Opens modal for account deletion confirmation. Requires email code."> 610 - <View style={[styles.iconContainer, dangerBg]}> 611 - <FontAwesomeIcon 612 - icon={['far', 'trash-can']} 613 - style={dangerText as FontAwesomeIconStyle} 614 - size={18} 615 - /> 616 - </View> 617 - <Text type="lg" style={dangerText}> 618 - <Trans>Delete my account…</Trans> 619 - </Text> 620 - </TouchableOpacity> 621 - <View style={styles.spacer20} /> 622 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 623 - <Trans>Developer Tools</Trans> 624 - </Text> 625 - <TouchableOpacity 626 - style={[pal.view, styles.linkCardNoIcon]} 627 - onPress={onPressSystemLog} 628 - accessibilityRole="button" 629 - accessibilityHint="Open system log" 630 - accessibilityLabel={_(msg`Opens the system log page`)}> 631 - <Text type="lg" style={pal.text}> 632 - <Trans>System log</Trans> 633 - </Text> 634 - </TouchableOpacity> 635 - {__DEV__ ? ( 636 - <ToggleButton 637 - type="default-light" 638 - label="Experiment: Use AppView Proxy" 639 - isSelected={debugHeaderEnabled} 640 - onPress={toggleDebugHeader} 733 + onPress={() => openModal({name: 'verify-email'})}> 734 + <FontAwesomeIcon 735 + icon="envelope" 736 + color={palInverted.colors.text} 737 + size={16} 641 738 /> 642 - ) : null} 643 - {__DEV__ ? ( 644 - <> 645 - <TouchableOpacity 646 - style={[pal.view, styles.linkCardNoIcon]} 647 - onPress={onPressStorybook} 648 - accessibilityRole="button" 649 - accessibilityHint="Open storybook page" 650 - accessibilityLabel={_(msg`Opens the storybook page`)}> 651 - <Text type="lg" style={pal.text}> 652 - <Trans>Storybook</Trans> 653 - </Text> 654 - </TouchableOpacity> 655 - <TouchableOpacity 656 - style={[pal.view, styles.linkCardNoIcon]} 657 - onPress={onPressResetPreferences} 658 - accessibilityRole="button" 659 - accessibilityHint="Reset preferences" 660 - accessibilityLabel={_(msg`Resets the preferences state`)}> 661 - <Text type="lg" style={pal.text}> 662 - <Trans>Reset preferences state</Trans> 663 - </Text> 664 - </TouchableOpacity> 665 - <TouchableOpacity 666 - style={[pal.view, styles.linkCardNoIcon]} 667 - onPress={onPressResetOnboarding} 668 - accessibilityRole="button" 669 - accessibilityHint="Reset onboarding" 670 - accessibilityLabel={_(msg`Resets the onboarding state`)}> 671 - <Text type="lg" style={pal.text}> 672 - <Trans>Reset onboarding state</Trans> 673 - </Text> 674 - </TouchableOpacity> 675 - </> 676 - ) : null} 677 - <View style={[styles.footer]}> 678 - <TouchableOpacity 679 - accessibilityRole="button" 680 - onPress={onPressBuildInfo}> 681 - <Text type="sm" style={[styles.buildInfo, pal.textLight]}> 682 - <Trans> 683 - Build version {AppInfo.appVersion} {AppInfo.updateChannel} 684 - </Trans> 685 - </Text> 686 - </TouchableOpacity> 687 - <Text type="sm" style={[pal.textLight]}> 688 - &middot; &nbsp; 739 + <Text type="button" style={palInverted.text}> 740 + <Trans>Verify My Email</Trans> 689 741 </Text> 690 - <TouchableOpacity 691 - accessibilityRole="button" 692 - onPress={onPressStatusPage}> 693 - <Text type="sm" style={[styles.buildInfo, pal.textLight]}> 694 - <Trans>Status page</Trans> 695 - </Text> 696 - </TouchableOpacity> 697 - </View> 698 - <View style={s.footerSpacer} /> 699 - </ScrollView> 700 - </View> 701 - ) 702 - }), 703 - ) 704 - 705 - const EmailConfirmationNotice = observer( 706 - function EmailConfirmationNoticeImpl() { 707 - const pal = usePalette('default') 708 - const palInverted = usePalette('inverted') 709 - const {_} = useLingui() 710 - const {isMobile} = useWebMediaQueries() 711 - const {openModal} = useModalControls() 712 - 713 - return ( 714 - <View style={{marginBottom: 20}}> 715 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 716 - <Trans>Verify email</Trans> 742 + </Pressable> 743 + </View> 744 + <Text style={pal.textLight}> 745 + <Trans>Protect your account by verifying your email.</Trans> 717 746 </Text> 718 - <View 719 - style={[ 720 - { 721 - paddingVertical: isMobile ? 12 : 0, 722 - paddingHorizontal: 18, 723 - }, 724 - pal.view, 725 - ]}> 726 - <View style={{flexDirection: 'row', marginBottom: 8}}> 727 - <Pressable 728 - style={[ 729 - palInverted.view, 730 - { 731 - flexDirection: 'row', 732 - gap: 6, 733 - borderRadius: 6, 734 - paddingHorizontal: 12, 735 - paddingVertical: 10, 736 - alignItems: 'center', 737 - }, 738 - isMobile && {flex: 1}, 739 - ]} 740 - accessibilityRole="button" 741 - accessibilityLabel={_(msg`Verify my email`)} 742 - accessibilityHint="" 743 - onPress={() => openModal({name: 'verify-email'})}> 744 - <FontAwesomeIcon 745 - icon="envelope" 746 - color={palInverted.colors.text} 747 - size={16} 748 - /> 749 - <Text type="button" style={palInverted.text}> 750 - <Trans>Verify My Email</Trans> 751 - </Text> 752 - </Pressable> 753 - </View> 754 - <Text style={pal.textLight}> 755 - <Trans>Protect your account by verifying your email.</Trans> 756 - </Text> 757 - </View> 758 747 </View> 759 - ) 760 - }, 761 - ) 748 + </View> 749 + ) 750 + } 762 751 763 752 const styles = StyleSheet.create({ 764 753 dimmed: {
+4 -9
src/view/shell/Drawer.tsx
··· 10 10 ViewStyle, 11 11 } from 'react-native' 12 12 import {useNavigation, StackActions} from '@react-navigation/native' 13 - import {observer} from 'mobx-react-lite' 14 13 import { 15 14 FontAwesomeIcon, 16 15 FontAwesomeIconStyle, ··· 101 100 ) 102 101 } 103 102 104 - export const DrawerContent = observer(function DrawerContentImpl() { 103 + export function DrawerContent() { 105 104 const theme = useTheme() 106 105 const pal = usePalette('default') 107 106 const {_} = useLingui() ··· 404 403 </SafeAreaView> 405 404 </View> 406 405 ) 407 - }) 406 + } 408 407 409 408 interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> { 410 409 icon: JSX.Element ··· 458 457 ) 459 458 } 460 459 461 - const InviteCodes = observer(function InviteCodesImpl({ 462 - style, 463 - }: { 464 - style?: StyleProp<ViewStyle> 465 - }) { 460 + function InviteCodes({style}: {style?: StyleProp<ViewStyle>}) { 466 461 const {track} = useAnalytics() 467 462 const setDrawerOpen = useSetDrawerOpen() 468 463 const pal = usePalette('default') ··· 502 497 </Text> 503 498 </TouchableOpacity> 504 499 ) 505 - }) 500 + } 506 501 507 502 const styles = StyleSheet.create({ 508 503 view: {
+2 -5
src/view/shell/bottom-bar/BottomBar.tsx
··· 4 4 import {StackActions} from '@react-navigation/native' 5 5 import {BottomTabBarProps} from '@react-navigation/bottom-tabs' 6 6 import {useSafeAreaInsets} from 'react-native-safe-area-context' 7 - import {observer} from 'mobx-react-lite' 8 7 import {Text} from 'view/com/util/text/Text' 9 8 import {useAnalytics} from 'lib/analytics/analytics' 10 9 import {clamp} from 'lib/numbers' ··· 34 33 35 34 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' 36 35 37 - export const BottomBar = observer(function BottomBarImpl({ 38 - navigation, 39 - }: BottomTabBarProps) { 36 + export function BottomBar({navigation}: BottomTabBarProps) { 40 37 const {openModal} = useModalControls() 41 38 const {currentAccount} = useSession() 42 39 const pal = usePalette('default') ··· 231 228 /> 232 229 </Animated.View> 233 230 ) 234 - }) 231 + } 235 232 236 233 interface BtnProps 237 234 extends Pick<
+2 -3
src/view/shell/bottom-bar/BottomBarWeb.tsx
··· 1 1 import React from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import {usePalette} from 'lib/hooks/usePalette' 4 3 import {useNavigationState} from '@react-navigation/native' 5 4 import Animated from 'react-native-reanimated' ··· 24 23 import {CommonNavigatorParams} from 'lib/routes/types' 25 24 import {useSession} from '#/state/session' 26 25 27 - export const BottomBarWeb = observer(function BottomBarWebImpl() { 26 + export function BottomBarWeb() { 28 27 const {currentAccount} = useSession() 29 28 const pal = usePalette('default') 30 29 const safeAreaInsets = useSafeAreaInsets() ··· 111 110 </NavItem> 112 111 </Animated.View> 113 112 ) 114 - }) 113 + } 115 114 116 115 const NavItem: React.FC<{ 117 116 children: (props: {isActive: boolean}) => React.ReactChild
+2 -3
src/view/shell/desktop/Feeds.tsx
··· 1 1 import React from 'react' 2 2 import {View, StyleSheet} from 'react-native' 3 3 import {useNavigationState} from '@react-navigation/native' 4 - import {observer} from 'mobx-react-lite' 5 4 import {usePalette} from 'lib/hooks/usePalette' 6 5 import {TextLink} from 'view/com/util/Link' 7 6 import {getCurrentRoute} from 'lib/routes/helpers' 8 7 import {usePinnedFeedsInfos} from '#/state/queries/feed' 9 8 10 - export const DesktopFeeds = observer(function DesktopFeeds() { 9 + export function DesktopFeeds() { 11 10 const pal = usePalette('default') 12 11 const feeds = usePinnedFeedsInfos() 13 12 ··· 54 53 </View> 55 54 </View> 56 55 ) 57 - }) 56 + } 58 57 59 58 function FeedItem({ 60 59 title,
+6 -13
src/view/shell/desktop/LeftNav.tsx
··· 1 1 import React from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 4 3 import {PressableWithHover} from 'view/com/util/PressableWithHover' 5 4 import { ··· 47 46 import {useFetchHandle} from '#/state/queries/handle' 48 47 import {emitSoftReset} from '#/state/events' 49 48 50 - const ProfileCard = observer(function ProfileCardImpl() { 49 + function ProfileCard() { 51 50 const {currentAccount} = useSession() 52 51 const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did}) 53 52 const {isDesktop} = useWebMediaQueries() ··· 73 72 /> 74 73 </View> 75 74 ) 76 - }) 75 + } 77 76 78 77 function BackBtn() { 79 78 const {isTablet} = useWebMediaQueries() ··· 117 116 iconFilled: JSX.Element 118 117 label: string 119 118 } 120 - const NavItem = observer(function NavItemImpl({ 121 - count, 122 - href, 123 - icon, 124 - iconFilled, 125 - label, 126 - }: NavItemProps) { 119 + function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { 127 120 const pal = usePalette('default') 128 121 const {currentAccount} = useSession() 129 122 const {isDesktop, isTablet} = useWebMediaQueries() ··· 192 185 )} 193 186 </PressableWithHover> 194 187 ) 195 - }) 188 + } 196 189 197 190 function ComposeBtn() { 198 191 const {currentAccount} = useSession() ··· 264 257 ) 265 258 } 266 259 267 - export const DesktopLeftNav = observer(function DesktopLeftNav() { 260 + export function DesktopLeftNav() { 268 261 const {currentAccount} = useSession() 269 262 const pal = usePalette('default') 270 263 const {isDesktop, isTablet} = useWebMediaQueries() ··· 422 415 <ComposeBtn /> 423 416 </View> 424 417 ) 425 - }) 418 + } 426 419 427 420 const styles = StyleSheet.create({ 428 421 leftNav: {
+4 -5
src/view/shell/desktop/RightNav.tsx
··· 1 1 import React from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import {StyleSheet, TouchableOpacity, View} from 'react-native' 4 3 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {usePalette} from 'lib/hooks/usePalette' ··· 16 15 import {useSession} from '#/state/session' 17 16 import {useInviteCodesQuery} from '#/state/queries/invites' 18 17 19 - export const DesktopRightNav = observer(function DesktopRightNavImpl() { 18 + export function DesktopRightNav() { 20 19 const pal = usePalette('default') 21 20 const palError = usePalette('error') 22 21 const {isSandbox, hasSession, currentAccount} = useSession() ··· 80 79 <InviteCodes /> 81 80 </View> 82 81 ) 83 - }) 82 + } 84 83 85 - const InviteCodes = observer(function InviteCodesImpl() { 84 + function InviteCodes() { 86 85 const pal = usePalette('default') 87 86 const {openModal} = useModalControls() 88 87 const {data: invites} = useInviteCodesQuery() ··· 118 117 </Text> 119 118 </TouchableOpacity> 120 119 ) 121 - }) 120 + } 122 121 123 122 const styles = StyleSheet.create({ 124 123 rightNav: {
+2 -3
src/view/shell/index.web.tsx
··· 1 1 import React, {useEffect} from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import {View, StyleSheet, TouchableOpacity} from 'react-native' 4 3 import {DesktopLeftNav} from './desktop/LeftNav' 5 4 import {DesktopRightNav} from './desktop/RightNav' ··· 76 75 ) 77 76 } 78 77 79 - export const Shell: React.FC = observer(function ShellImpl() { 78 + export const Shell: React.FC = function ShellImpl() { 80 79 const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) 81 80 return ( 82 81 <View style={[s.hContentRegion, pageBg]}> ··· 85 84 </RoutesContainer> 86 85 </View> 87 86 ) 88 - }) 87 + } 89 88 90 89 const styles = StyleSheet.create({ 91 90 bgLight: {