diff options
Diffstat (limited to 'src')
81 files changed, 1069 insertions, 1926 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index 3f49eb11f..af4c3be89 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -5,7 +5,6 @@ import React, {useState, useEffect} from 'react' import {RootSiblingParent} from 'react-native-root-siblings' import * as SplashScreen from 'expo-splash-screen' import {GestureHandlerRootView} from 'react-native-gesture-handler' -import {observer} from 'mobx-react-lite' import {QueryClientProvider} from '@tanstack/react-query' import 'view/icons' @@ -16,7 +15,6 @@ import {listenSessionDropped} from './state/events' import {useColorMode} from 'state/shell' import {ThemeProvider} from 'lib/ThemeContext' import {s} from 'lib/styles' -import {RootStoreModel, setupState, RootStoreProvider} from './state' import {Shell} from 'view/shell' import * as notifications from 'lib/notifications/notifications' import * as analytics from 'lib/analytics/analytics' @@ -44,22 +42,13 @@ i18n.activate('en') SplashScreen.preventAutoHideAsync() -const InnerApp = observer(function AppImpl() { +function InnerApp() { const colorMode = useColorMode() const {isInitialLoad} = useSession() const {resumeSession} = useSessionApi() - const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( - undefined, - ) // init useEffect(() => { - setupState().then(store => { - setRootStore(store) - }) - }, []) - - useEffect(() => { initReminders() analytics.init() notifications.init(queryClient) @@ -72,7 +61,7 @@ const InnerApp = observer(function AppImpl() { }, [resumeSession]) // show nothing prior to init - if (!rootStore || isInitialLoad) { + if (isInitialLoad) { // TODO add a loading state return null } @@ -85,22 +74,20 @@ const InnerApp = observer(function AppImpl() { <UnreadNotifsProvider> <ThemeProvider theme={colorMode}> <analytics.Provider> - <RootStoreProvider value={rootStore}> - <I18nProvider i18n={i18n}> - {/* All components should be within this provider */} - <RootSiblingParent> - <GestureHandlerRootView style={s.h100pct}> - <TestCtrls /> - <Shell /> - </GestureHandlerRootView> - </RootSiblingParent> - </I18nProvider> - </RootStoreProvider> + <I18nProvider i18n={i18n}> + {/* All components should be within this provider */} + <RootSiblingParent> + <GestureHandlerRootView style={s.h100pct}> + <TestCtrls /> + <Shell /> + </GestureHandlerRootView> + </RootSiblingParent> + </I18nProvider> </analytics.Provider> </ThemeProvider> </UnreadNotifsProvider> ) -}) +} function App() { const [isReady, setReady] = useState(false) diff --git a/src/App.web.tsx b/src/App.web.tsx index e1f4e8030..67fa3dcc3 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,7 +1,6 @@ import 'lib/sentry' // must be near top import React, {useState, useEffect} from 'react' -import {observer} from 'mobx-react-lite' import {QueryClientProvider} from '@tanstack/react-query' import {SafeAreaProvider} from 'react-native-safe-area-context' import {RootSiblingParent} from 'react-native-root-siblings' @@ -12,7 +11,6 @@ import {init as initPersistedState} from '#/state/persisted' import {init as initReminders} from '#/state/shell/reminders' import {useColorMode} from 'state/shell' import * as analytics from 'lib/analytics/analytics' -import {RootStoreModel, setupState, RootStoreProvider} from './state' import {Shell} from 'view/shell/index' import {ToastContainer} from 'view/com/util/Toast.web' import {ThemeProvider} from 'lib/ThemeContext' @@ -34,22 +32,13 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' -const InnerApp = observer(function AppImpl() { +function InnerApp() { const {isInitialLoad} = useSession() const {resumeSession} = useSessionApi() const colorMode = useColorMode() - const [rootStore, setRootStore] = useState<RootStoreModel | undefined>( - undefined, - ) // init useEffect(() => { - setupState().then(store => { - setRootStore(store) - }) - }, []) - - useEffect(() => { initReminders() analytics.init() dynamicActivate(defaultLocale) // async import of locale data @@ -59,7 +48,7 @@ const InnerApp = observer(function AppImpl() { }, [resumeSession]) // show nothing prior to init - if (!rootStore || isInitialLoad) { + if (isInitialLoad) { // TODO add a loading state return null } @@ -72,22 +61,20 @@ const InnerApp = observer(function AppImpl() { <UnreadNotifsProvider> <ThemeProvider theme={colorMode}> <analytics.Provider> - <RootStoreProvider value={rootStore}> - <I18nProvider i18n={i18n}> - {/* All components should be within this provider */} - <RootSiblingParent> - <SafeAreaProvider> - <Shell /> - </SafeAreaProvider> - </RootSiblingParent> - </I18nProvider> - <ToastContainer /> - </RootStoreProvider> + <I18nProvider i18n={i18n}> + {/* All components should be within this provider */} + <RootSiblingParent> + <SafeAreaProvider> + <Shell /> + </SafeAreaProvider> + </RootSiblingParent> + </I18nProvider> + <ToastContainer /> </analytics.Provider> </ThemeProvider> </UnreadNotifsProvider> ) -}) +} function App() { const [isReady, setReady] = useState(false) diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx index 4b955b365..3a8254eb1 100644 --- a/src/lib/analytics/analytics.tsx +++ b/src/lib/analytics/analytics.tsx @@ -7,13 +7,21 @@ import { useAnalytics as useAnalyticsOrig, ClientMethods, } from '@segment/analytics-react-native' -import {AppInfo} from 'state/models/root-store' +import {z} from 'zod' import {useSession} from '#/state/session' import {sha256} from 'js-sha256' import {ScreenEvent, TrackEvent} from './types' import {logger} from '#/logger' import {listenSessionLoaded} from '#/state/events' +export const appInfo = z.object({ + build: z.string().optional(), + name: z.string().optional(), + namespace: z.string().optional(), + version: z.string().optional(), +}) +export type AppInfo = z.infer<typeof appInfo> + const segmentClient = createClient({ writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', trackAppLifecycleEvents: false, @@ -128,7 +136,11 @@ async function writeAppInfo(value: AppInfo) { await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value)) } -async function readAppInfo(): Promise<Partial<AppInfo> | undefined> { +async function readAppInfo(): Promise<AppInfo | undefined> { const rawData = await AsyncStorage.getItem('BSKY_APP_INFO') - return rawData ? JSON.parse(rawData) : undefined + const obj = rawData ? JSON.parse(rawData) : undefined + if (!obj || typeof obj !== 'object') { + return undefined + } + return obj } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 92620c459..a78abcacd 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -10,7 +10,6 @@ import { RichText, } from '@atproto/api' import {AtUri} from '@atproto/api' -import {RootStoreModel} from 'state/models/root-store' import {isNetworkError} from 'lib/strings/errors' import {LinkMeta} from '../link-meta/link-meta' import {isWeb} from 'platform/detection' @@ -26,33 +25,6 @@ export interface ExternalEmbedDraft { localThumb?: ImageModel } -export async function resolveName(store: RootStoreModel, didOrHandle: string) { - if (!didOrHandle) { - throw new Error('Invalid handle: ""') - } - if (didOrHandle.startsWith('did:')) { - return didOrHandle - } - - // we run the resolution always to ensure freshness - const promise = store.agent - .resolveHandle({ - handle: didOrHandle, - }) - .then(res => { - store.handleResolutions.cache.set(didOrHandle, res.data.did) - return res.data.did - }) - - // but we can return immediately if it's cached - const cached = store.handleResolutions.cache.get(didOrHandle) - if (cached) { - return cached - } - - return promise -} - export async function uploadBlob( agent: BskyAgent, blob: string, diff --git a/src/lib/async/revertible.ts b/src/lib/async/revertible.ts deleted file mode 100644 index 43383b61e..000000000 --- a/src/lib/async/revertible.ts +++ /dev/null @@ -1,68 +0,0 @@ -import {runInAction} from 'mobx' -import {deepObserve} from 'mobx-utils' -import set from 'lodash.set' - -const ongoingActions = new Set<any>() - -/** - * This is a TypeScript function that optimistically updates data on the client-side before sending a - * request to the server and rolling back changes if the request fails. - * @param {T} model - The object or record that needs to be updated optimistically. - * @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It - * can be used to perform any necessary actions or updates on the model or UI before the server update - * is initiated. - * @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server - * update operation. This function is called after the previous state of the model has been recorded - * and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate` - * function is called with the result - * @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the - * server update is successful. It takes in the response from the server update as its parameter. If - * this parameter is not provided, nothing will happen after the server update. - * @returns A Promise that resolves to `void`. - */ -export const updateDataOptimistically = async < - T extends Record<string, any>, - U, ->( - model: T, - preUpdate: () => void, - serverUpdate: () => Promise<U>, - postUpdate?: (res: U) => void, -): Promise<void> => { - if (ongoingActions.has(model)) { - return - } - ongoingActions.add(model) - - const prevState: Map<string, any> = new Map<string, any>() - const dispose = deepObserve(model, (change, path) => { - if (change.observableKind === 'object') { - if (change.type === 'update') { - prevState.set( - [path, change.name].filter(Boolean).join('.'), - change.oldValue, - ) - } else if (change.type === 'add') { - prevState.set([path, change.name].filter(Boolean).join('.'), undefined) - } - } - }) - preUpdate() - dispose() - - try { - const res = await serverUpdate() - runInAction(() => { - postUpdate?.(res) - }) - } catch (error) { - runInAction(() => { - prevState.forEach((value, path) => { - set(model, path, value) - }) - }) - throw error - } finally { - ongoingActions.delete(model) - } -} diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index 78d755331..322b02332 100644 --- a/src/lib/link-meta/bsky.ts +++ b/src/lib/link-meta/bsky.ts @@ -1,9 +1,8 @@ -import {AppBskyFeedPost} from '@atproto/api' +import {AppBskyFeedPost, BskyAgent} from '@atproto/api' import * as apilib from 'lib/api/index' import {LikelyType, LinkMeta} from './link-meta' // import {match as matchRoute} from 'view/routes' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' -import {RootStoreModel} from 'state/index' import {ComposerOptsQuote} from 'state/shell/composer' import {useGetPost} from '#/state/queries/post' @@ -23,7 +22,7 @@ import {useGetPost} from '#/state/queries/post' // remove once that's implemented // -prf export async function extractBskyMeta( - store: RootStoreModel, + agent: BskyAgent, url: string, ): Promise<LinkMeta> { url = convertBskyAppUrlIfNeeded(url) @@ -120,13 +119,13 @@ export async function getPostAsQuote( } export async function getFeedAsEmbed( - store: RootStoreModel, + agent: BskyAgent, url: string, ): Promise<apilib.ExternalEmbedDraft> { url = convertBskyAppUrlIfNeeded(url) const [_0, user, _1, rkey] = url.split('/').filter(Boolean) const feed = makeRecordUri(user, 'app.bsky.feed.generator', rkey) - const res = await store.agent.app.bsky.feed.getFeedGenerator({feed}) + const res = await agent.app.bsky.feed.getFeedGenerator({feed}) return { isLoading: false, uri: feed, @@ -146,13 +145,13 @@ export async function getFeedAsEmbed( } export async function getListAsEmbed( - store: RootStoreModel, + agent: BskyAgent, url: string, ): Promise<apilib.ExternalEmbedDraft> { url = convertBskyAppUrlIfNeeded(url) const [_0, user, _1, rkey] = url.split('/').filter(Boolean) const list = makeRecordUri(user, 'app.bsky.graph.list', rkey) - const res = await store.agent.app.bsky.graph.getList({list}) + const res = await agent.app.bsky.graph.getList({list}) return { isLoading: false, uri: list, diff --git a/src/lib/link-meta/link-meta.ts b/src/lib/link-meta/link-meta.ts index c490fa292..c17dee51f 100644 --- a/src/lib/link-meta/link-meta.ts +++ b/src/lib/link-meta/link-meta.ts @@ -1,5 +1,5 @@ +import {BskyAgent} from '@atproto/api' import {isBskyAppUrl} from '../strings/url-helpers' -import {RootStoreModel} from 'state/index' import {extractBskyMeta} from './bsky' import {LINK_META_PROXY} from 'lib/constants' @@ -23,12 +23,12 @@ export interface LinkMeta { } export async function getLinkMeta( - store: RootStoreModel, + agent: BskyAgent, url: string, timeout = 5e3, ): Promise<LinkMeta> { if (isBskyAppUrl(url)) { - return extractBskyMeta(store, url) + return extractBskyMeta(agent, url) } let urlp @@ -55,9 +55,9 @@ export async function getLinkMeta( const to = setTimeout(() => controller.abort(), timeout || 5e3) const response = await fetch( - `${LINK_META_PROXY( - store.session.currentSession?.service || '', - )}${encodeURIComponent(url)}`, + `${LINK_META_PROXY(agent.service.toString() || '')}${encodeURIComponent( + url, + )}`, {signal: controller.signal}, ) diff --git a/src/lib/media/image-sizes.ts b/src/lib/media/image-sizes.ts new file mode 100644 index 000000000..4ea95ea23 --- /dev/null +++ b/src/lib/media/image-sizes.ts @@ -0,0 +1,34 @@ +import {Image} from 'react-native' +import type {Dimensions} from 'lib/media/types' + +const sizes: Map<string, Dimensions> = new Map() +const activeRequests: Map<string, Promise<Dimensions>> = new Map() + +export function get(uri: string): Dimensions | undefined { + return sizes.get(uri) +} + +export async function fetch(uri: string): Promise<Dimensions> { + const Dimensions = sizes.get(uri) + if (Dimensions) { + return Dimensions + } + + const prom = + activeRequests.get(uri) || + new Promise<Dimensions>(resolve => { + Image.getSize( + uri, + (width: number, height: number) => resolve({width, height}), + (err: any) => { + console.error('Failed to fetch image dimensions for', uri, err) + resolve({width: 0, height: 0}) + }, + ) + }) + activeRequests.set(uri, prom) + const res = await prom + activeRequests.delete(uri) + sizes.set(uri, res) + return res +} diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 106d2ca31..d06bd8028 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,5 +1,5 @@ import {AtUri} from '@atproto/api' -import {PROD_SERVICE} from 'state/index' +import {PROD_SERVICE} from 'lib/constants' import TLDs from 'tlds' import psl from 'psl' diff --git a/src/state/index.ts b/src/state/index.ts deleted file mode 100644 index 55dcae6d6..000000000 --- a/src/state/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {autorun} from 'mobx' -import {AppState, Platform} from 'react-native' -import {BskyAgent} from '@atproto/api' -import {RootStoreModel} from './models/root-store' -import * as apiPolyfill from 'lib/api/api-polyfill' -import * as storage from 'lib/storage' -import {logger} from '#/logger' - -export const LOCAL_DEV_SERVICE = - Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' -export const STAGING_SERVICE = 'https://staging.bsky.dev' -export const PROD_SERVICE = 'https://bsky.social' -export const DEFAULT_SERVICE = PROD_SERVICE -const ROOT_STATE_STORAGE_KEY = 'root' -const STATE_FETCH_INTERVAL = 15e3 - -export async function setupState(serviceUri = DEFAULT_SERVICE) { - let rootStore: RootStoreModel - let data: any - - apiPolyfill.doPolyfill() - - rootStore = new RootStoreModel(new BskyAgent({service: serviceUri})) - try { - data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} - logger.debug('Initial hydrate', {hasSession: !!data.session}) - rootStore.hydrate(data) - } catch (e: any) { - logger.error('Failed to load state from storage', {error: e}) - } - rootStore.attemptSessionResumption() - - // track changes & save to storage - autorun(() => { - const snapshot = rootStore.serialize() - storage.save(ROOT_STATE_STORAGE_KEY, snapshot) - }) - - // periodic state fetch - setInterval(() => { - // NOTE - // this must ONLY occur when the app is active, as the bg-fetch handler - // will wake up the thread and cause this interval to fire, which in - // turn schedules a bunch of work at a poor time - // -prf - if (AppState.currentState === 'active') { - rootStore.updateSessionState() - } - }, STATE_FETCH_INTERVAL) - - return rootStore -} - -export {useStores, RootStoreModel, RootStoreProvider} from './models/root-store' diff --git a/src/state/models/cache/handle-resolutions.ts b/src/state/models/cache/handle-resolutions.ts deleted file mode 100644 index 2e2b69661..000000000 --- a/src/state/models/cache/handle-resolutions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {LRUMap} from 'lru_map' - -export class HandleResolutionsCache { - cache: LRUMap<string, string> = new LRUMap(500) -} diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts deleted file mode 100644 index c30a68f4d..000000000 --- a/src/state/models/cache/image-sizes.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {Image} from 'react-native' -import type {Dimensions} from 'lib/media/types' - -export class ImageSizesCache { - sizes: Map<string, Dimensions> = new Map() - activeRequests: Map<string, Promise<Dimensions>> = new Map() - - constructor() {} - - get(uri: string): Dimensions | undefined { - return this.sizes.get(uri) - } - - async fetch(uri: string): Promise<Dimensions> { - const Dimensions = this.sizes.get(uri) - if (Dimensions) { - return Dimensions - } - - const prom = - this.activeRequests.get(uri) || - new Promise<Dimensions>(resolve => { - Image.getSize( - uri, - (width: number, height: number) => resolve({width, height}), - (err: any) => { - console.error('Failed to fetch image dimensions for', uri, err) - resolve({width: 0, height: 0}) - }, - ) - }) - this.activeRequests.set(uri, prom) - const res = await prom - this.activeRequests.delete(uri) - this.sizes.set(uri, res) - return res - } -} diff --git a/src/state/models/cache/link-metas.ts b/src/state/models/cache/link-metas.ts deleted file mode 100644 index 607968c80..000000000 --- a/src/state/models/cache/link-metas.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {LRUMap} from 'lru_map' -import {RootStoreModel} from '../root-store' -import {LinkMeta, getLinkMeta} from 'lib/link-meta/link-meta' - -type CacheValue = Promise<LinkMeta> | LinkMeta -export class LinkMetasCache { - cache: LRUMap<string, CacheValue> = new LRUMap(100) - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - cache: false, - }, - {autoBind: true}, - ) - } - - // public api - // = - - async getLinkMeta(url: string) { - const cached = this.cache.get(url) - if (cached) { - try { - return await cached - } catch (e) { - // ignore, we'll try again - } - } - try { - const promise = getLinkMeta(this.rootStore, url) - this.cache.set(url, promise) - const res = await promise - this.cache.set(url, res) - return res - } catch (e) { - this.cache.delete(url) - throw e - } - } -} diff --git a/src/state/models/cache/posts.ts b/src/state/models/cache/posts.ts deleted file mode 100644 index d3632f436..000000000 --- a/src/state/models/cache/posts.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {LRUMap} from 'lru_map' -import {RootStoreModel} from '../root-store' -import { - AppBskyFeedDefs, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyFeedPost, -} from '@atproto/api' - -type PostView = AppBskyFeedDefs.PostView - -export class PostsCache { - cache: LRUMap<string, PostView> = new LRUMap(500) - - constructor(public rootStore: RootStoreModel) {} - - set(uri: string, postView: PostView) { - this.cache.set(uri, postView) - if (postView.author.handle) { - this.rootStore.handleResolutions.cache.set( - postView.author.handle, - postView.author.did, - ) - } - } - - fromFeedItem(feedItem: AppBskyFeedDefs.FeedViewPost) { - this.set(feedItem.post.uri, feedItem.post) - if ( - feedItem.reply?.parent && - AppBskyFeedDefs.isPostView(feedItem.reply?.parent) - ) { - this.set(feedItem.reply.parent.uri, feedItem.reply.parent) - } - const embed = feedItem.post.embed - if ( - AppBskyEmbedRecord.isView(embed) && - AppBskyEmbedRecord.isViewRecord(embed.record) && - AppBskyFeedPost.isRecord(embed.record.value) && - AppBskyFeedPost.validateRecord(embed.record.value).success - ) { - this.set(embed.record.uri, embedViewToPostView(embed.record)) - } - if ( - AppBskyEmbedRecordWithMedia.isView(embed) && - AppBskyEmbedRecord.isViewRecord(embed.record?.record) && - AppBskyFeedPost.isRecord(embed.record.record.value) && - AppBskyFeedPost.validateRecord(embed.record.record.value).success - ) { - this.set( - embed.record.record.uri, - embedViewToPostView(embed.record.record), - ) - } - } -} - -function embedViewToPostView( - embedView: AppBskyEmbedRecord.ViewRecord, -): PostView { - return { - $type: 'app.bsky.feed.post#view', - uri: embedView.uri, - cid: embedView.cid, - author: embedView.author, - record: embedView.value, - indexedAt: embedView.indexedAt, - labels: embedView.labels, - } -} diff --git a/src/state/models/cache/profiles-view.ts b/src/state/models/cache/profiles-view.ts deleted file mode 100644 index e5a9be587..000000000 --- a/src/state/models/cache/profiles-view.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {LRUMap} from 'lru_map' -import {RootStoreModel} from '../root-store' -import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' - -type CacheValue = Promise<GetProfile.Response> | GetProfile.Response -export class ProfilesCache { - cache: LRUMap<string, CacheValue> = new LRUMap(100) - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - cache: false, - }, - {autoBind: true}, - ) - } - - // public api - // = - - async getProfile(did: string) { - const cached = this.cache.get(did) - if (cached) { - try { - return await cached - } catch (e) { - // ignore, we'll try again - } - } - try { - const promise = this.rootStore.agent.getProfile({ - actor: did, - }) - this.cache.set(did, promise) - const res = await promise - this.cache.set(did, res) - return res - } catch (e) { - this.cache.delete(did) - throw e - } - } - - overwrite(did: string, res: GetProfile.Response) { - this.cache.set(did, res) - } -} diff --git a/src/state/models/me.ts b/src/state/models/me.ts deleted file mode 100644 index 1e802fb78..000000000 --- a/src/state/models/me.ts +++ /dev/null @@ -1,115 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {RootStoreModel} from './root-store' -import {isObj, hasProp} from 'lib/type-guards' -import {logger} from '#/logger' - -const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min - -export class MeModel { - did: string = '' - handle: string = '' - displayName: string = '' - description: string = '' - avatar: string = '' - followsCount: number | undefined - followersCount: number | undefined - lastProfileStateUpdate = Date.now() - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - {rootStore: false, serialize: false, hydrate: false}, - {autoBind: true}, - ) - } - - clear() { - this.rootStore.profiles.cache.clear() - this.rootStore.posts.cache.clear() - this.did = '' - this.handle = '' - this.displayName = '' - this.description = '' - this.avatar = '' - } - - serialize(): unknown { - return { - did: this.did, - handle: this.handle, - displayName: this.displayName, - description: this.description, - avatar: this.avatar, - } - } - - hydrate(v: unknown) { - if (isObj(v)) { - let did, handle, displayName, description, avatar - if (hasProp(v, 'did') && typeof v.did === 'string') { - did = v.did - } - if (hasProp(v, 'handle') && typeof v.handle === 'string') { - handle = v.handle - } - if (hasProp(v, 'displayName') && typeof v.displayName === 'string') { - displayName = v.displayName - } - if (hasProp(v, 'description') && typeof v.description === 'string') { - description = v.description - } - if (hasProp(v, 'avatar') && typeof v.avatar === 'string') { - avatar = v.avatar - } - if (did && handle) { - this.did = did - this.handle = handle - this.displayName = displayName || '' - this.description = description || '' - this.avatar = avatar || '' - } - } - } - - async load() { - const sess = this.rootStore.session - logger.debug('MeModel:load', {hasSession: sess.hasSession}) - if (sess.hasSession) { - this.did = sess.currentSession?.did || '' - await this.fetchProfile() - this.rootStore.emitSessionLoaded() - } else { - this.clear() - } - } - - async updateIfNeeded() { - if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) { - logger.debug('Updating me profile information') - this.lastProfileStateUpdate = Date.now() - await this.fetchProfile() - } - } - - async fetchProfile() { - const profile = await this.rootStore.agent.getProfile({ - actor: this.did, - }) - runInAction(() => { - if (profile?.data) { - this.displayName = profile.data.displayName || '' - this.description = profile.data.description || '' - this.avatar = profile.data.avatar || '' - this.handle = profile.data.handle || '' - this.followsCount = profile.data.followsCount - this.followersCount = profile.data.followersCount - } else { - this.displayName = '' - this.description = '' - this.avatar = '' - this.followsCount = profile.data.followsCount - this.followersCount = undefined - } - }) - } -} diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts deleted file mode 100644 index 6d7c2c12e..000000000 --- a/src/state/models/root-store.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * The root store is the base of all modeled state. - */ - -import {makeAutoObservable} from 'mobx' -import {BskyAgent} from '@atproto/api' -import {createContext, useContext} from 'react' -import {DeviceEventEmitter, EmitterSubscription} from 'react-native' -import {z} from 'zod' -import {isObj, hasProp} from 'lib/type-guards' -import {SessionModel} from './session' -import {ShellUiModel} from './ui/shell' -import {HandleResolutionsCache} from './cache/handle-resolutions' -import {ProfilesCache} from './cache/profiles-view' -import {PostsCache} from './cache/posts' -import {LinkMetasCache} from './cache/link-metas' -import {MeModel} from './me' -import {resetToTab} from '../../Navigation' -import {ImageSizesCache} from './cache/image-sizes' -import {reset as resetNavigation} from '../../Navigation' -import {logger} from '#/logger' - -// TEMPORARY (APP-700) -// remove after backend testing finishes -// -prf -import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header' - -export const appInfo = z.object({ - build: z.string(), - name: z.string(), - namespace: z.string(), - version: z.string(), -}) -export type AppInfo = z.infer<typeof appInfo> - -export class RootStoreModel { - agent: BskyAgent - appInfo?: AppInfo - session = new SessionModel(this) - shell = new ShellUiModel(this) - me = new MeModel(this) - handleResolutions = new HandleResolutionsCache() - profiles = new ProfilesCache(this) - posts = new PostsCache(this) - linkMetas = new LinkMetasCache(this) - imageSizes = new ImageSizesCache() - - constructor(agent: BskyAgent) { - this.agent = agent - makeAutoObservable(this, { - agent: false, - serialize: false, - hydrate: false, - }) - } - - setAppInfo(info: AppInfo) { - this.appInfo = info - } - - serialize(): unknown { - return { - appInfo: this.appInfo, - me: this.me.serialize(), - } - } - - hydrate(v: unknown) { - if (isObj(v)) { - if (hasProp(v, 'appInfo')) { - const appInfoParsed = appInfo.safeParse(v.appInfo) - if (appInfoParsed.success) { - this.setAppInfo(appInfoParsed.data) - } - } - if (hasProp(v, 'me')) { - this.me.hydrate(v.me) - } - } - } - - /** - * Called during init to resume any stored session. - */ - async attemptSessionResumption() {} - - /** - * Called by the session model. Refreshes session-oriented state. - */ - async handleSessionChange( - agent: BskyAgent, - {hadSession}: {hadSession: boolean}, - ) { - logger.debug('RootStoreModel:handleSessionChange') - this.agent = agent - applyDebugHeader(this.agent) - this.me.clear() - await this.me.load() - if (!hadSession) { - await resetNavigation() - } - this.emitSessionReady() - } - - /** - * Called by the session model. Handles session drops by informing the user. - */ - async handleSessionDrop() { - logger.debug('RootStoreModel:handleSessionDrop') - resetToTab('HomeTab') - this.me.clear() - this.emitSessionDropped() - } - - /** - * Clears all session-oriented state, previously called on LOGOUT - */ - clearAllSessionState() { - logger.debug('RootStoreModel:clearAllSessionState') - resetToTab('HomeTab') - this.me.clear() - } - - /** - * Periodic poll for new session state. - */ - async updateSessionState() { - if (!this.session.hasSession) { - return - } - try { - await this.me.updateIfNeeded() - } catch (e: any) { - logger.error('Failed to fetch latest state', {error: e}) - } - } - - // global event bus - // = - // - some events need to be passed around between views and models - // in order to keep state in sync; these methods are for that - - // a post was deleted by the local user - onPostDeleted(handler: (uri: string) => void): EmitterSubscription { - return DeviceEventEmitter.addListener('post-deleted', handler) - } - emitPostDeleted(uri: string) { - DeviceEventEmitter.emit('post-deleted', uri) - } - - // a list was deleted by the local user - onListDeleted(handler: (uri: string) => void): EmitterSubscription { - return DeviceEventEmitter.addListener('list-deleted', handler) - } - emitListDeleted(uri: string) { - DeviceEventEmitter.emit('list-deleted', uri) - } - - // the session has started and been fully hydrated - onSessionLoaded(handler: () => void): EmitterSubscription { - return DeviceEventEmitter.addListener('session-loaded', handler) - } - emitSessionLoaded() { - DeviceEventEmitter.emit('session-loaded') - } - - // the session has completed all setup; good for post-initialization behaviors like triggering modals - onSessionReady(handler: () => void): EmitterSubscription { - return DeviceEventEmitter.addListener('session-ready', handler) - } - emitSessionReady() { - DeviceEventEmitter.emit('session-ready') - } - - // the session was dropped due to bad/expired refresh tokens - onSessionDropped(handler: () => void): EmitterSubscription { - return DeviceEventEmitter.addListener('session-dropped', handler) - } - emitSessionDropped() { - DeviceEventEmitter.emit('session-dropped') - } - - // the current screen has changed - // TODO is this still needed? - onNavigation(handler: () => void): EmitterSubscription { - return DeviceEventEmitter.addListener('navigation', handler) - } - emitNavigation() { - DeviceEventEmitter.emit('navigation') - } - - // a "soft reset" typically means scrolling to top and loading latest - // but it can depend on the screen - onScreenSoftReset(handler: () => void): EmitterSubscription { - return DeviceEventEmitter.addListener('screen-soft-reset', handler) - } - emitScreenSoftReset() { - DeviceEventEmitter.emit('screen-soft-reset') - } -} - -const throwawayInst = new RootStoreModel( - new BskyAgent({service: 'http://localhost'}), -) // this will be replaced by the loader, we just need to supply a value at init -const RootStoreContext = createContext<RootStoreModel>(throwawayInst) -export const RootStoreProvider = RootStoreContext.Provider -export const useStores = () => useContext(RootStoreContext) diff --git a/src/state/models/session.ts b/src/state/models/session.ts deleted file mode 100644 index cc681ad34..000000000 --- a/src/state/models/session.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import { - BskyAgent, - ComAtprotoServerDescribeServer as DescribeServer, -} from '@atproto/api' -import {RootStoreModel} from './root-store' - -export type ServiceDescription = DescribeServer.OutputSchema - -export class SessionModel { - data: any = {} - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, { - rootStore: false, - hasSession: false, - }) - } - - get currentSession(): any { - return undefined - } - - get hasSession() { - return false - } - - clear() {} - - /** - * Helper to fetch the accounts config settings from an account. - */ - async describeService(service: string): Promise<ServiceDescription> { - const agent = new BskyAgent({service}) - const res = await agent.com.atproto.server.describeServer({}) - return res.data - } - - /** - * Reloads the session from the server. Useful when account details change, like the handle. - */ - async reloadFromServer() {} -} diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts deleted file mode 100644 index 18287c953..000000000 --- a/src/state/models/ui/shell.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {RootStoreModel} from '../root-store' -import {makeAutoObservable} from 'mobx' -import { - shouldRequestEmailConfirmation, - setEmailConfirmationRequested, -} from '#/state/shell/reminders' -import {unstable__openModal} from '#/state/modals' - -export type ColorMode = 'system' | 'light' | 'dark' - -export function isColorMode(v: unknown): v is ColorMode { - return v === 'system' || v === 'light' || v === 'dark' -} - -export class ShellUiModel { - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, { - rootStore: false, - }) - - this.setupLoginModals() - } - - setupLoginModals() { - this.rootStore.onSessionReady(() => { - if (shouldRequestEmailConfirmation(this.rootStore.session)) { - unstable__openModal({name: 'verify-email', showReminder: true}) - setEmailConfirmationRequested() - } - }) - } -} diff --git a/src/state/shell/reminders.e2e.ts b/src/state/shell/reminders.e2e.ts index d7703e9e0..4bd11bf0a 100644 --- a/src/state/shell/reminders.e2e.ts +++ b/src/state/shell/reminders.e2e.ts @@ -1,6 +1,4 @@ -import {SessionModel} from '../models/session' - -export function shouldRequestEmailConfirmation(_session: SessionModel) { +export function shouldRequestEmailConfirmation() { return false } diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 0d8172964..3505e86af 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -1,6 +1,5 @@ import React from 'react' import {SafeAreaView} from 'react-native' -import {observer} from 'mobx-react-lite' import {Login} from 'view/com/auth/login/Login' import {CreateAccount} from 'view/com/auth/create/CreateAccount' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' @@ -16,7 +15,7 @@ enum ScreenState { S_CreateAccount, } -export const LoggedOut = observer(function LoggedOutImpl() { +export function LoggedOut() { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const {screen} = useAnalytics() @@ -58,4 +57,4 @@ export const LoggedOut = observer(function LoggedOutImpl() { </ErrorBoundary> </SafeAreaView> ) -}) +} diff --git a/src/view/com/auth/Onboarding.tsx b/src/view/com/auth/Onboarding.tsx index 994f4c149..3a17eb4f8 100644 --- a/src/view/com/auth/Onboarding.tsx +++ b/src/view/com/auth/Onboarding.tsx @@ -1,6 +1,5 @@ import React from 'react' import {SafeAreaView} from 'react-native' -import {observer} from 'mobx-react-lite' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -10,7 +9,7 @@ import {RecommendedFollows} from './onboarding/RecommendedFollows' import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' import {useOnboardingState, useOnboardingDispatch} from '#/state/shell' -export const Onboarding = observer(function OnboardingImpl() { +export function Onboarding() { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const onboardingState = useOnboardingState() @@ -38,4 +37,4 @@ export const Onboarding = observer(function OnboardingImpl() { </ErrorBoundary> </SafeAreaView> ) -}) +} diff --git a/src/view/com/auth/create/Policies.tsx b/src/view/com/auth/create/Policies.tsx index 7d10f32fc..a52f07531 100644 --- a/src/view/com/auth/create/Policies.tsx +++ b/src/view/com/auth/create/Policies.tsx @@ -4,12 +4,14 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import {ComAtprotoServerDescribeServer} from '@atproto/api' import {TextLink} from '../../util/Link' import {Text} from '../../util/text/Text' import {s, colors} from 'lib/styles' -import {ServiceDescription} from 'state/models/session' import {usePalette} from 'lib/hooks/usePalette' +type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema + export const Policies = ({ serviceDescription, needsGuardian, diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index ab47b411f..c9d19e868 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -13,7 +13,7 @@ import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index' +import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants' import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' /** STEP 1: Your hosting provider diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx index a794665c9..215c393d9 100644 --- a/src/view/com/auth/login/ForgotPasswordForm.tsx +++ b/src/view/com/auth/login/ForgotPasswordForm.tsx @@ -9,13 +9,13 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import {ComAtprotoServerDescribeServer} from '@atproto/api' import * as EmailValidator from 'email-validator' import {BskyAgent} from '@atproto/api' import {useAnalytics} from 'lib/analytics/analytics' import {Text} from '../../util/text/Text' import {s} from 'lib/styles' import {toNiceDomain} from 'lib/strings/url-helpers' -import {ServiceDescription} from 'state/models/session' import {isNetworkError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' @@ -26,6 +26,8 @@ import {useLingui} from '@lingui/react' import {styles} from './styles' import {useModalControls} from '#/state/modals' +type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema + export const ForgotPasswordForm = ({ error, serviceUrl, diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index 27d08812c..a2f924f95 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -2,8 +2,7 @@ import React, {useState, useEffect} from 'react' import {KeyboardAvoidingView} from 'react-native' import {useAnalytics} from 'lib/analytics/analytics' import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout' -import {useStores, DEFAULT_SERVICE} from 'state/index' -import {ServiceDescription} from 'state/models/session' +import {DEFAULT_SERVICE} from '#/lib/constants' import {usePalette} from 'lib/hooks/usePalette' import {logger} from '#/logger' import {ChooseAccountForm} from './ChooseAccountForm' @@ -14,6 +13,7 @@ import {PasswordUpdatedForm} from './PasswordUpdatedForm' import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' import {useSession, SessionAccount} from '#/state/session' +import {useServiceQuery} from '#/state/queries/service' enum Forms { Login, @@ -25,20 +25,20 @@ enum Forms { export const Login = ({onPressBack}: {onPressBack: () => void}) => { const pal = usePalette('default') - const store = useStores() const {accounts} = useSession() const {track} = useAnalytics() const {_} = useLingui() const [error, setError] = useState<string>('') - const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({}) const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE) - const [serviceDescription, setServiceDescription] = useState< - ServiceDescription | undefined - >(undefined) const [initialHandle, setInitialHandle] = useState<string>('') const [currentForm, setCurrentForm] = useState<Forms>( accounts.length ? Forms.ChooseAccount : Forms.Login, ) + const { + data: serviceDescription, + error: serviceError, + refetch: refetchService, + } = useServiceQuery(serviceUrl) const onSelectAccount = (account?: SessionAccount) => { if (account?.service) { @@ -54,35 +54,21 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } useEffect(() => { - let aborted = false - setError('') - store.session.describeService(serviceUrl).then( - desc => { - if (aborted) { - return - } - setServiceDescription(desc) - }, - err => { - if (aborted) { - return - } - logger.warn(`Failed to fetch service description for ${serviceUrl}`, { - error: err, - }) - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - }, - ) - return () => { - aborted = true + if (serviceError) { + setError( + _( + msg`Unable to contact your service. Please check your Internet connection.`, + ), + ) + logger.warn(`Failed to fetch service description for ${serviceUrl}`, { + error: String(serviceError), + }) + } else { + setError('') } - }, [store.session, serviceUrl, retryDescribeTrigger, _]) + }, [serviceError, serviceUrl, _]) - const onPressRetryConnect = () => setRetryDescribeTrigger({}) + const onPressRetryConnect = () => refetchService() const onPressForgotPassword = () => { track('Signin:PressedForgotPassword') setCurrentForm(Forms.ForgotPassword) diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx index be3a95131..365f2e253 100644 --- a/src/view/com/auth/login/LoginForm.tsx +++ b/src/view/com/auth/login/LoginForm.tsx @@ -10,12 +10,12 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import {ComAtprotoServerDescribeServer} from '@atproto/api' import {useAnalytics} from 'lib/analytics/analytics' import {Text} from '../../util/text/Text' import {s} from 'lib/styles' import {createFullHandle} from 'lib/strings/handles' import {toNiceDomain} from 'lib/strings/url-helpers' -import {ServiceDescription} from 'state/models/session' import {isNetworkError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' @@ -27,6 +27,8 @@ import {styles} from './styles' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' +type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema + export const LoginForm = ({ error, serviceUrl, diff --git a/src/view/com/auth/onboarding/RecommendedFeeds.tsx b/src/view/com/auth/onboarding/RecommendedFeeds.tsx index d134dae9f..941671bf8 100644 --- a/src/view/com/auth/onboarding/RecommendedFeeds.tsx +++ b/src/view/com/auth/onboarding/RecommendedFeeds.tsx @@ -1,6 +1,5 @@ import React from 'react' import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints' import {Text} from 'view/com/util/text/Text' @@ -16,9 +15,7 @@ import {useSuggestedFeedsQuery} from '#/state/queries/suggested-feeds' type Props = { next: () => void } -export const RecommendedFeeds = observer(function RecommendedFeedsImpl({ - next, -}: Props) { +export function RecommendedFeeds({next}: Props) { const pal = usePalette('default') const {isTabletOrMobile} = useWebMediaQueries() const {isLoading, data} = useSuggestedFeedsQuery() @@ -146,7 +143,7 @@ export const RecommendedFeeds = observer(function RecommendedFeedsImpl({ </Mobile> </> ) -}) +} const tdStyles = StyleSheet.create({ container: { diff --git a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx index 2eaf3cf2d..7417e5b06 100644 --- a/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx +++ b/src/view/com/auth/onboarding/RecommendedFeedsItem.tsx @@ -1,6 +1,5 @@ import React from 'react' import {View} from 'react-native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {AppBskyFeedDefs, RichText as BskRichText} from '@atproto/api' import {Text} from 'view/com/util/text/Text' @@ -19,7 +18,7 @@ import { } from '#/state/queries/preferences' import {logger} from '#/logger' -export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({ +export function RecommendedFeedsItem({ item, }: { item: AppBskyFeedDefs.GeneratorView @@ -164,4 +163,4 @@ export const RecommendedFeedsItem = observer(function RecommendedFeedsItemImpl({ </View> </View> ) -}) +} diff --git a/src/view/com/auth/onboarding/RecommendedFollows.tsx b/src/view/com/auth/onboarding/RecommendedFollows.tsx index aa528cbd8..c327a7168 100644 --- a/src/view/com/auth/onboarding/RecommendedFollows.tsx +++ b/src/view/com/auth/onboarding/RecommendedFollows.tsx @@ -1,6 +1,5 @@ import React from 'react' import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {AppBskyActorDefs, moderateProfile} from '@atproto/api' import {TabletOrDesktop, Mobile} from 'view/com/util/layouts/Breakpoints' @@ -19,9 +18,7 @@ import {logger} from '#/logger' type Props = { next: () => void } -export const RecommendedFollows = observer(function RecommendedFollowsImpl({ - next, -}: Props) { +export function RecommendedFollows({next}: Props) { const pal = usePalette('default') const {isTabletOrMobile} = useWebMediaQueries() const {data: suggestedFollows, dataUpdatedAt} = useSuggestedFollowsQuery() @@ -211,7 +208,7 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({ </Mobile> </> ) -}) +} const tdStyles = StyleSheet.create({ container: { diff --git a/src/view/com/auth/onboarding/WelcomeDesktop.tsx b/src/view/com/auth/onboarding/WelcomeDesktop.tsx index c066e9bd5..1a30c17f9 100644 --- a/src/view/com/auth/onboarding/WelcomeDesktop.tsx +++ b/src/view/com/auth/onboarding/WelcomeDesktop.tsx @@ -7,16 +7,13 @@ import {usePalette} from 'lib/hooks/usePalette' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' import {Button} from 'view/com/util/forms/Button' -import {observer} from 'mobx-react-lite' type Props = { next: () => void skip: () => void } -export const WelcomeDesktop = observer(function WelcomeDesktopImpl({ - next, -}: Props) { +export function WelcomeDesktop({next}: Props) { const pal = usePalette('default') const horizontal = useMediaQuery({minWidth: 1300}) const title = ( @@ -105,7 +102,7 @@ export const WelcomeDesktop = observer(function WelcomeDesktopImpl({ </View> </TitleColumnLayout> ) -}) +} const styles = StyleSheet.create({ row: { diff --git a/src/view/com/auth/onboarding/WelcomeMobile.tsx b/src/view/com/auth/onboarding/WelcomeMobile.tsx index ef70a1fe3..2717bff3f 100644 --- a/src/view/com/auth/onboarding/WelcomeMobile.tsx +++ b/src/view/com/auth/onboarding/WelcomeMobile.tsx @@ -5,7 +5,6 @@ import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Button} from 'view/com/util/forms/Button' -import {observer} from 'mobx-react-lite' import {ViewHeader} from 'view/com/util/ViewHeader' import {Trans} from '@lingui/macro' @@ -14,10 +13,7 @@ type Props = { skip: () => void } -export const WelcomeMobile = observer(function WelcomeMobileImpl({ - next, - skip, -}: Props) { +export function WelcomeMobile({next, skip}: Props) { const pal = usePalette('default') return ( @@ -102,7 +98,7 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({ /> </View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx index 4b8b31d6c..5fd89a8bd 100644 --- a/src/view/com/auth/withAuthRequired.tsx +++ b/src/view/com/auth/withAuthRequired.tsx @@ -5,7 +5,6 @@ import { StyleSheet, TouchableOpacity, } from 'react-native' -import {observer} from 'mobx-react-lite' import {CenteredView} from '../util/Views' import {LoggedOut} from './LoggedOut' import {Onboarding} from './Onboarding' @@ -18,7 +17,7 @@ import {useSession} from '#/state/session' export const withAuthRequired = <P extends object>( Component: React.ComponentType<P>, ): React.FC<P> => - observer(function AuthRequired(props: P) { + function AuthRequired(props: P) { const {isInitialLoad, hasSession} = useSession() const onboardingState = useOnboardingState() if (isInitialLoad) { @@ -31,7 +30,7 @@ export const withAuthRequired = <P extends object>( return <Onboarding /> } return <Component {...props} /> - }) + } function Loading() { const pal = usePalette('default') diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx index 7d90774f2..a10684691 100644 --- a/src/view/com/composer/labels/LabelsBtn.tsx +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -1,6 +1,5 @@ import React from 'react' import {Keyboard, StyleSheet} from 'react-native' -import {observer} from 'mobx-react-lite' import {Button} from 'view/com/util/forms/Button' import {usePalette} from 'lib/hooks/usePalette' import {ShieldExclamation} from 'lib/icons' @@ -11,7 +10,7 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' import {useModalControls} from '#/state/modals' -export const LabelsBtn = observer(function LabelsBtn({ +export function LabelsBtn({ labels, hasMedia, onChange, @@ -49,7 +48,7 @@ export const LabelsBtn = observer(function LabelsBtn({ ) : null} </Button> ) -}) +} const styles = StyleSheet.create({ button: { diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx index 7ea63ed62..78b1e9ba2 100644 --- a/src/view/com/composer/select-language/SelectLangBtn.tsx +++ b/src/view/com/composer/select-language/SelectLangBtn.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useMemo} from 'react' import {StyleSheet, Keyboard} from 'react-native' -import {observer} from 'mobx-react-lite' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -24,7 +23,7 @@ import { import {t, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -export const SelectLangBtn = observer(function SelectLangBtn() { +export function SelectLangBtn() { const pal = usePalette('default') const {_} = useLingui() const {openModal} = useModalControls() @@ -117,7 +116,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() { )} </DropdownButton> ) -}) +} const styles = StyleSheet.create({ button: { diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index bb54a2042..c400aa48d 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -1,6 +1,5 @@ import React, {useEffect, useRef} from 'react' import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {usePalette} from 'lib/hooks/usePalette' import {Text} from 'view/com/util/text/Text' @@ -10,7 +9,7 @@ import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {Trans} from '@lingui/macro' import {AppBskyActorDefs} from '@atproto/api' -export const Autocomplete = observer(function AutocompleteImpl({ +export function Autocomplete({ prefix, onSelect, }: { @@ -103,7 +102,7 @@ export const Autocomplete = observer(function AutocompleteImpl({ ) : null} </Animated.View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index 0c96e4727..af6042306 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -1,5 +1,4 @@ import {useState, useEffect} from 'react' -import {useStores} from 'state/index' import {ImageModel} from 'state/models/media/image' import * as apilib from 'lib/api/index' import {getLinkMeta} from 'lib/link-meta/link-meta' @@ -17,6 +16,7 @@ import { import {ComposerOpts} from 'state/shell/composer' import {POST_IMG_MAX} from 'lib/constants' import {logger} from '#/logger' +import {useSession} from '#/state/session' import {useGetPost} from '#/state/queries/post' export function useExternalLinkFetch({ @@ -24,7 +24,7 @@ export function useExternalLinkFetch({ }: { setQuote: (opts: ComposerOpts['quote']) => void }) { - const store = useStores() + const {agent} = useSession() const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>( undefined, ) @@ -56,7 +56,7 @@ export function useExternalLinkFetch({ }, ) } else if (isBskyCustomFeedUrl(extLink.uri)) { - getFeedAsEmbed(store, extLink.uri).then( + getFeedAsEmbed(agent, extLink.uri).then( ({embed, meta}) => { if (aborted) { return @@ -74,7 +74,7 @@ export function useExternalLinkFetch({ }, ) } else if (isBskyListUrl(extLink.uri)) { - getListAsEmbed(store, extLink.uri).then( + getListAsEmbed(agent, extLink.uri).then( ({embed, meta}) => { if (aborted) { return @@ -92,7 +92,7 @@ export function useExternalLinkFetch({ }, ) } else { - getLinkMeta(store, extLink.uri).then(meta => { + getLinkMeta(agent, extLink.uri).then(meta => { if (aborted) { return } @@ -134,7 +134,7 @@ export function useExternalLinkFetch({ }) } return cleanup - }, [store, extLink, setQuote, getPost]) + }, [agent, extLink, setQuote, getPost]) return {extLink, setExtLink} } diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 7c7ad0616..ea740ec91 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -315,7 +315,6 @@ const ImageItem = ({ <GestureDetector gesture={composedGesture}> <AnimatedImage contentFit="contain" - // NOTE: Don't pass imageSrc={imageSrc} or MobX will break. source={{uri: imageSrc.uri}} style={[styles.image, animatedStyle]} accessibilityLabel={imageSrc.alt} diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index f73f355ac..2b0b0b149 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -139,7 +139,6 @@ const ImageItem = ({imageSrc, onTap, onZoom, onRequestClose}: Props) => { {(!loaded || !imageDimensions) && <ImageLoading />} <AnimatedImage contentFit="contain" - // NOTE: Don't pass imageSrc={imageSrc} or MobX will break. source={{uri: imageSrc.uri}} style={[styles.image, animatedStyle]} accessibilityLabel={imageSrc.alt} diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx index 9996c5641..c78f06ed4 100644 --- a/src/view/com/modals/BirthDateSettings.tsx +++ b/src/view/com/modals/BirthDateSettings.tsx @@ -5,7 +5,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {observer} from 'mobx-react-lite' import {Text} from '../util/text/Text' import {DateInput} from '../util/forms/DateInput' import {ErrorMessage} from '../util/error/ErrorMessage' @@ -103,7 +102,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { ) } -export const Component = observer(function Component({}: {}) { +export function Component({}: {}) { const {data: preferences} = usePreferencesQuery() return !preferences ? ( @@ -111,7 +110,7 @@ export const Component = observer(function Component({}: {}) { ) : ( <Inner preferences={preferences} /> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx index 6f7a92102..829188b51 100644 --- a/src/view/com/modals/ChangeEmail.tsx +++ b/src/view/com/modals/ChangeEmail.tsx @@ -1,7 +1,6 @@ import React, {useState} from 'react' import {ActivityIndicator, SafeAreaView, StyleSheet, View} from 'react-native' import {ScrollView, TextInput} from './util' -import {observer} from 'mobx-react-lite' import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {ErrorMessage} from '../util/error/ErrorMessage' @@ -24,7 +23,7 @@ enum Stages { export const snapPoints = ['90%'] -export const Component = observer(function Component({}: {}) { +export function Component() { const pal = usePalette('default') const {agent, currentAccount} = useSession() const {updateCurrentAccount} = useSessionApi() @@ -226,7 +225,7 @@ export const Component = observer(function Component({}: {}) { </ScrollView> </SafeAreaView> ) -}) +} const styles = StyleSheet.create({ titleSection: { diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index 8dc3311fc..8b42e1b1d 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -2,7 +2,6 @@ import React from 'react' import {LabelPreference} from '@atproto/api' import {StyleSheet, Pressable, View} from 'react-native' import LinearGradient from 'react-native-linear-gradient' -import {observer} from 'mobx-react-lite' import {ScrollView} from './util' import {s, colors, gradients} from 'lib/styles' import {Text} from '../util/text/Text' @@ -28,82 +27,80 @@ import { export const snapPoints = ['90%'] -export const Component = observer( - function ContentFilteringSettingsImpl({}: {}) { - const {isMobile} = useWebMediaQueries() - const pal = usePalette('default') - const {_} = useLingui() - const {closeModal} = useModalControls() - const {data: preferences} = usePreferencesQuery() +export function Component({}: {}) { + const {isMobile} = useWebMediaQueries() + const pal = usePalette('default') + const {_} = useLingui() + const {closeModal} = useModalControls() + const {data: preferences} = usePreferencesQuery() - const onPressDone = React.useCallback(() => { - closeModal() - }, [closeModal]) + const onPressDone = React.useCallback(() => { + closeModal() + }, [closeModal]) - return ( - <View testID="contentFilteringModal" style={[pal.view, styles.container]}> - <Text style={[pal.text, styles.title]}> - <Trans>Content Filtering</Trans> - </Text> + return ( + <View testID="contentFilteringModal" style={[pal.view, styles.container]}> + <Text style={[pal.text, styles.title]}> + <Trans>Content Filtering</Trans> + </Text> - <ScrollView style={styles.scrollContainer}> - <AdultContentEnabledPref /> - <ContentLabelPref - preferences={preferences} - labelGroup="nsfw" - disabled={!preferences?.adultContentEnabled} - /> - <ContentLabelPref - preferences={preferences} - labelGroup="nudity" - disabled={!preferences?.adultContentEnabled} - /> - <ContentLabelPref - preferences={preferences} - labelGroup="suggestive" - disabled={!preferences?.adultContentEnabled} - /> - <ContentLabelPref - preferences={preferences} - labelGroup="gore" - disabled={!preferences?.adultContentEnabled} - /> - <ContentLabelPref preferences={preferences} labelGroup="hate" /> - <ContentLabelPref preferences={preferences} labelGroup="spam" /> - <ContentLabelPref - preferences={preferences} - labelGroup="impersonation" - /> - <View style={{height: isMobile ? 60 : 0}} /> - </ScrollView> + <ScrollView style={styles.scrollContainer}> + <AdultContentEnabledPref /> + <ContentLabelPref + preferences={preferences} + labelGroup="nsfw" + disabled={!preferences?.adultContentEnabled} + /> + <ContentLabelPref + preferences={preferences} + labelGroup="nudity" + disabled={!preferences?.adultContentEnabled} + /> + <ContentLabelPref + preferences={preferences} + labelGroup="suggestive" + disabled={!preferences?.adultContentEnabled} + /> + <ContentLabelPref + preferences={preferences} + labelGroup="gore" + disabled={!preferences?.adultContentEnabled} + /> + <ContentLabelPref preferences={preferences} labelGroup="hate" /> + <ContentLabelPref preferences={preferences} labelGroup="spam" /> + <ContentLabelPref + preferences={preferences} + labelGroup="impersonation" + /> + <View style={{height: isMobile ? 60 : 0}} /> + </ScrollView> - <View - style={[ - styles.btnContainer, - isMobile && styles.btnContainerMobile, - pal.borderDark, - ]}> - <Pressable - testID="sendReportBtn" - onPress={onPressDone} - accessibilityRole="button" - accessibilityLabel={_(msg`Done`)} - accessibilityHint=""> - <LinearGradient - colors={[gradients.blueLight.start, gradients.blueLight.end]} - start={{x: 0, y: 0}} - end={{x: 1, y: 1}} - style={[styles.btn]}> - <Text style={[s.white, s.bold, s.f18]}> - <Trans>Done</Trans> - </Text> - </LinearGradient> - </Pressable> - </View> + <View + style={[ + styles.btnContainer, + isMobile && styles.btnContainerMobile, + pal.borderDark, + ]}> + <Pressable + testID="sendReportBtn" + onPress={onPressDone} + accessibilityRole="button" + accessibilityLabel={_(msg`Done`)} + accessibilityHint=""> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[styles.btn]}> + <Text style={[s.white, s.bold, s.f18]}> + <Trans>Done</Trans> + </Text> + </LinearGradient> + </Pressable> </View> - ) - }, -) + </View> + ) +} function AdultContentEnabledPref() { const pal = usePalette('default') @@ -171,7 +168,7 @@ function AdultContentEnabledPref() { } // TODO: Refactor this component to pass labels down to each tab -const ContentLabelPref = observer(function ContentLabelPrefImpl({ +function ContentLabelPref({ preferences, labelGroup, disabled, @@ -217,7 +214,7 @@ const ContentLabelPref = observer(function ContentLabelPrefImpl({ )} </View> ) -}) +} interface SelectGroupProps { current: LabelPreference diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index 973c7c3a7..82a826aca 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -5,7 +5,6 @@ import { View, ActivityIndicator, } from 'react-native' -import {observer} from 'mobx-react-lite' import {ComAtprotoServerDefs} from '@atproto/api' import { FontAwesomeIcon, @@ -129,7 +128,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) { ) } -const InviteCode = observer(function InviteCodeImpl({ +function InviteCode({ testID, invite, used, @@ -211,7 +210,7 @@ const InviteCode = observer(function InviteCodeImpl({ ) : null} </View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx index cb86387a0..c63bc2a87 100644 --- a/src/view/com/modals/LinkWarning.tsx +++ b/src/view/com/modals/LinkWarning.tsx @@ -1,7 +1,6 @@ import React from 'react' import {Linking, SafeAreaView, StyleSheet, View} from 'react-native' import {ScrollView} from './util' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' @@ -16,13 +15,7 @@ import {useModalControls} from '#/state/modals' export const snapPoints = ['50%'] -export const Component = observer(function Component({ - text, - href, -}: { - text: string - href: string -}) { +export function Component({text, href}: {text: string; href: string}) { const pal = usePalette('default') const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() @@ -97,7 +90,7 @@ export const Component = observer(function Component({ </ScrollView> </SafeAreaView> ) -}) +} function LinkBox({href}: {href: string}) { const pal = usePalette('default') diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 38c8bc7ba..d834db40b 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -1,7 +1,6 @@ import React, {useRef, useEffect} from 'react' import {StyleSheet} from 'react-native' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' -import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {usePalette} from 'lib/hooks/usePalette' @@ -40,7 +39,7 @@ import * as LinkWarningModal from './LinkWarning' const DEFAULT_SNAPPOINTS = ['90%'] const HANDLE_HEIGHT = 24 -export const ModalsContainer = observer(function ModalsContainer() { +export function ModalsContainer() { const {isModalActive, activeModals} = useModals() const {closeModal} = useModalControls() const bottomSheetRef = useRef<BottomSheet>(null) @@ -198,7 +197,7 @@ export const ModalsContainer = observer(function ModalsContainer() { {element} </BottomSheet> ) -}) +} const styles = StyleSheet.create({ handle: { diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 28f6c36c9..74aa7b1a9 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -1,6 +1,5 @@ import React from 'react' import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import type {Modal as ModalIface} from '#/state/modals' @@ -33,7 +32,7 @@ import * as VerifyEmailModal from './VerifyEmail' import * as ChangeEmailModal from './ChangeEmail' import * as LinkWarningModal from './LinkWarning' -export const ModalsContainer = observer(function ModalsContainer() { +export function ModalsContainer() { const {isModalActive, activeModals} = useModals() if (!isModalActive) { @@ -47,7 +46,7 @@ export const ModalsContainer = observer(function ModalsContainer() { ))} </> ) -}) +} function Modal({modal}: {modal: ModalIface}) { const {isModalActive} = useModals() diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx index f8b64085d..092dd2d32 100644 --- a/src/view/com/modals/SelfLabel.tsx +++ b/src/view/com/modals/SelfLabel.tsx @@ -1,6 +1,5 @@ import React, {useState} from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {Text} from '../util/text/Text' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -17,7 +16,7 @@ const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] export const snapPoints = ['50%'] -export const Component = observer(function Component({ +export function Component({ labels, hasMedia, onChange, @@ -161,7 +160,7 @@ export const Component = observer(function Component({ </View> </View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx index 09a460315..b30293859 100644 --- a/src/view/com/modals/ServerInput.tsx +++ b/src/view/com/modals/ServerInput.tsx @@ -9,7 +9,7 @@ import {Text} from '../util/text/Text' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' -import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index' +import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants' import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx index 106e05b87..e7a4537c5 100644 --- a/src/view/com/modals/VerifyEmail.tsx +++ b/src/view/com/modals/VerifyEmail.tsx @@ -8,7 +8,6 @@ import { } from 'react-native' import {Svg, Circle, Path} from 'react-native-svg' import {ScrollView, TextInput} from './util' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' @@ -32,11 +31,7 @@ enum Stages { ConfirmCode, } -export const Component = observer(function Component({ - showReminder, -}: { - showReminder?: boolean -}) { +export function Component({showReminder}: {showReminder?: boolean}) { const pal = usePalette('default') const {agent, currentAccount} = useSession() const {updateCurrentAccount} = useSessionApi() @@ -244,7 +239,7 @@ export const Component = observer(function Component({ </ScrollView> </SafeAreaView> ) -}) +} function ReminderIllustration() { const pal = usePalette('default') diff --git a/src/view/com/modals/lang-settings/LanguageToggle.tsx b/src/view/com/modals/lang-settings/LanguageToggle.tsx index 86e38a4d2..45b100f20 100644 --- a/src/view/com/modals/lang-settings/LanguageToggle.tsx +++ b/src/view/com/modals/lang-settings/LanguageToggle.tsx @@ -1,11 +1,10 @@ import React from 'react' import {StyleSheet} from 'react-native' import {usePalette} from 'lib/hooks/usePalette' -import {observer} from 'mobx-react-lite' import {ToggleButton} from 'view/com/util/forms/ToggleButton' import {useLanguagePrefs, toPostLanguages} from '#/state/preferences/languages' -export const LanguageToggle = observer(function LanguageToggleImpl({ +export function LanguageToggle({ code2, name, onPress, @@ -39,7 +38,7 @@ export const LanguageToggle = observer(function LanguageToggleImpl({ style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]} /> ) -}) +} const styles = StyleSheet.create({ languageToggle: { diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx index 7fcb24d58..05cfb8115 100644 --- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx +++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx @@ -1,6 +1,5 @@ import React from 'react' import {StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {ScrollView} from '../util' import {Text} from '../../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' @@ -19,7 +18,7 @@ import { export const snapPoints = ['100%'] -export const Component = observer(function PostLanguagesSettingsImpl() { +export function Component() { const {closeModal} = useModalControls() const langPrefs = useLanguagePrefs() const setLangPrefs = useLanguagePrefsApi() @@ -111,7 +110,7 @@ export const Component = observer(function PostLanguagesSettingsImpl() { <ConfirmLanguagesButton onPress={onPressDone} /> </View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx index 5183bdc10..06075547f 100644 --- a/src/view/com/modals/report/Modal.tsx +++ b/src/view/com/modals/report/Modal.tsx @@ -2,7 +2,6 @@ import React, {useState, useMemo} from 'react' import {Linking, StyleSheet, TouchableOpacity, View} from 'react-native' import {ScrollView} from 'react-native-gesture-handler' import {AtUri} from '@atproto/api' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {s} from 'lib/styles' import {Text} from '../../util/text/Text' @@ -17,6 +16,7 @@ import {CollectionId} from './types' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' +import {useSession} from '#/state/session' const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright' @@ -39,7 +39,7 @@ type ReportComponentProps = } export function Component(content: ReportComponentProps) { - const store = useStores() + const {agent} = useSession() const {closeModal} = useModalControls() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() @@ -70,7 +70,7 @@ export function Component(content: ReportComponentProps) { const $type = !isAccountReport ? 'com.atproto.repo.strongRef' : 'com.atproto.admin.defs#repoRef' - await store.agent.createModerationReport({ + await agent.createModerationReport({ reasonType: issue, subject: { $type, diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index b48690a66..298372893 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -1,7 +1,6 @@ import React from 'react' import {StyleSheet} from 'react-native' import Animated from 'react-native-reanimated' -import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {usePalette} from 'lib/hooks/usePalette' @@ -11,7 +10,7 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useShellLayout} from '#/state/shell/shell-layout' import {usePinnedFeedsInfos} from '#/state/queries/feed' -export const FeedsTabBar = observer(function FeedsTabBarImpl( +export function FeedsTabBar( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const {isMobile, isTablet} = useWebMediaQueries() @@ -22,9 +21,9 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( } else { return null } -}) +} -const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl( +function FeedsTabBarTablet( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const feeds = usePinnedFeedsInfos() @@ -48,7 +47,7 @@ const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl( /> </Animated.View> ) -}) +} const styles = StyleSheet.create({ tabBar: { diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index de985fb7c..e0c0be1ef 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -1,6 +1,5 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {usePalette} from 'lib/hooks/usePalette' @@ -20,7 +19,7 @@ import {useShellLayout} from '#/state/shell/shell-layout' import {useSession} from '#/state/session' import {usePinnedFeedsInfos} from '#/state/queries/feed' -export const FeedsTabBar = observer(function FeedsTabBarImpl( +export function FeedsTabBar( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const pal = usePalette('default') @@ -88,7 +87,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( /> </Animated.View> ) -}) +} const styles = StyleSheet.create({ tabBar: { diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index fad9f9b4e..99ee38536 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -1,6 +1,5 @@ import React from 'react' import {StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {FeedPostSlice} from '#/state/queries/post-feed' import {AtUri, moderatePost, ModerationOpts} from '@atproto/api' import {Link} from '../util/Link' @@ -10,7 +9,7 @@ import {FeedItem} from './FeedItem' import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' -export const FeedSlice = observer(function FeedSliceImpl({ +export function FeedSlice({ slice, dataUpdatedAt, ignoreFilterFor, @@ -94,7 +93,7 @@ export const FeedSlice = observer(function FeedSliceImpl({ ))} </> ) -}) +} function ViewFullThread({slice}: {slice: FeedPostSlice}) { const pal = usePalette('default') diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 2f359018f..cd9855456 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -1,6 +1,5 @@ import * as React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {observer} from 'mobx-react-lite' import { AppBskyActorDefs, moderateProfile, @@ -152,7 +151,7 @@ function ProfileCardPills({ ) } -const FollowersList = observer(function FollowersListImpl({ +function FollowersList({ followers, }: { followers?: AppBskyActorDefs.ProfileView[] | undefined @@ -196,7 +195,7 @@ const FollowersList = observer(function FollowersListImpl({ ))} </View> ) -}) +} export function ProfileCardWithFollowBtn({ profile, diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index ef128e877..0e245f0f4 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -1,6 +1,5 @@ import React from 'react' import {Pressable, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' import {usePalette} from 'lib/hooks/usePalette' @@ -12,7 +11,6 @@ import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {CenteredView} from '../util/Views' import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' -import {useStores} from 'state/index' import {NavigationProp} from 'lib/routes/types' import {BACK_HITSLOP} from 'lib/constants' import {isNative} from 'platform/detection' @@ -22,7 +20,7 @@ import {msg} from '@lingui/macro' import {useSetDrawerOpen} from '#/state/shell' import {emitSoftReset} from '#/state/events' -export const ProfileSubpageHeader = observer(function HeaderImpl({ +export function ProfileSubpageHeader({ isLoading, href, title, @@ -45,7 +43,6 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({ | undefined avatarType: UserAvatarType }>) { - const store = useStores() const setDrawerOpen = useSetDrawerOpen() const navigation = useNavigation<NavigationProp>() const {_} = useLingui() @@ -183,7 +180,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({ </View> </CenteredView> ) -}) +} const styles = StyleSheet.create({ backBtn: { diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index c5e438f8d..fa5f12f6b 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -6,7 +6,6 @@ import {niceDate} from 'lib/strings/time' import {usePalette} from 'lib/hooks/usePalette' import {TypographyVariant} from 'lib/ThemeContext' import {UserAvatar} from './UserAvatar' -import {observer} from 'mobx-react-lite' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {isAndroid} from 'platform/detection' @@ -30,7 +29,7 @@ interface PostMetaOpts { style?: StyleProp<ViewStyle> } -export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) { +export function PostMeta(opts: PostMetaOpts) { const pal = usePalette('default') const displayName = opts.author.displayName || opts.author.handle const handle = opts.author.handle @@ -92,7 +91,7 @@ export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) { </TimeElapsed> </View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx index c871d9404..e86e37565 100644 --- a/src/view/com/util/SimpleViewHeader.tsx +++ b/src/view/com/util/SimpleViewHeader.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import { StyleProp, StyleSheet, @@ -18,7 +17,7 @@ import {useSetDrawerOpen} from '#/state/shell' const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} -export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({ +export function SimpleViewHeader({ showBackButton = true, style, children, @@ -76,7 +75,7 @@ export const SimpleViewHeader = observer(function SimpleViewHeaderImpl({ {children} </Container> ) -}) +} const styles = StyleSheet.create({ header: { diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index adf2e4f08..082cae59c 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' @@ -15,7 +14,7 @@ import {useSetDrawerOpen} from '#/state/shell' const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} -export const ViewHeader = observer(function ViewHeaderImpl({ +export function ViewHeader({ title, canGoBack, showBackButton = true, @@ -108,7 +107,7 @@ export const ViewHeader = observer(function ViewHeaderImpl({ </Container> ) } -}) +} function DesktopWebHeader({ title, @@ -140,7 +139,7 @@ function DesktopWebHeader({ ) } -const Container = observer(function ContainerImpl({ +function Container({ children, hideOnScroll, showBorder, @@ -178,7 +177,7 @@ const Container = observer(function ContainerImpl({ {children} </Animated.View> ) -}) +} const styles = StyleSheet.create({ header: { diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 5b1d5d888..9787d92fb 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -1,5 +1,4 @@ import React, {ComponentProps} from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableWithoutFeedback} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {gradients} from 'lib/styles' @@ -15,11 +14,7 @@ export interface FABProps icon: JSX.Element } -export const FABInner = observer(function FABInnerImpl({ - testID, - icon, - ...props -}: FABProps) { +export function FABInner({testID, icon, ...props}: FABProps) { const insets = useSafeAreaInsets() const {isMobile, isTablet} = useWebMediaQueries() const {fabMinimalShellTransform} = useMinimalShellMode() @@ -55,7 +50,7 @@ export const FABInner = observer(function FABInnerImpl({ </Animated.View> </TouchableWithoutFeedback> ) -}) +} const styles = StyleSheet.create({ sizeRegular: { diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 6cbcddc32..b5b6c1b52 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -2,8 +2,8 @@ import React from 'react' import {StyleProp, StyleSheet, Pressable, View, ViewStyle} from 'react-native' import {Image} from 'expo-image' import {clamp} from 'lib/numbers' -import {useStores} from 'state/index' import {Dimensions} from 'lib/media/types' +import * as imageSizes from 'lib/media/image-sizes' const MIN_ASPECT_RATIO = 0.33 // 1/3 const MAX_ASPECT_RATIO = 5 // 5/1 @@ -29,9 +29,8 @@ export function AutoSizedImage({ style, children = null, }: Props) { - const store = useStores() const [dim, setDim] = React.useState<Dimensions | undefined>( - dimensionsHint || store.imageSizes.get(uri), + dimensionsHint || imageSizes.get(uri), ) const [aspectRatio, setAspectRatio] = React.useState<number>( dim ? calc(dim) : 1, @@ -41,14 +40,14 @@ export function AutoSizedImage({ if (dim) { return } - store.imageSizes.fetch(uri).then(newDim => { + imageSizes.fetch(uri).then(newDim => { if (aborted) { return } setDim(newDim) setAspectRatio(calc(newDim)) }) - }, [dim, setDim, setAspectRatio, store, uri]) + }, [dim, setDim, setAspectRatio, uri]) if (onPress || onLongPress || onPressIn) { return ( diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index f9a9387bb..970d3a73a 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -1,6 +1,5 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -12,7 +11,7 @@ const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) import {isWeb} from 'platform/detection' -export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ +export function LoadLatestBtn({ onPress, label, showIndicator, @@ -44,7 +43,7 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} </AnimatedTouchableOpacity> ) -}) +} const styles = StyleSheet.create({ loadLatest: { diff --git a/src/view/com/util/post-embeds/ListEmbed.tsx b/src/view/com/util/post-embeds/ListEmbed.tsx index dbf350039..fc5ad270f 100644 --- a/src/view/com/util/post-embeds/ListEmbed.tsx +++ b/src/view/com/util/post-embeds/ListEmbed.tsx @@ -1,12 +1,11 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {usePalette} from 'lib/hooks/usePalette' -import {observer} from 'mobx-react-lite' import {ListCard} from 'view/com/lists/ListCard' import {AppBskyGraphDefs} from '@atproto/api' import {s} from 'lib/styles' -export const ListEmbed = observer(function ListEmbedImpl({ +export function ListEmbed({ item, style, }: { @@ -20,7 +19,7 @@ export const ListEmbed = observer(function ListEmbedImpl({ <ListCard list={item} style={[style, styles.card]} /> </View> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 8c80b0fd5..50db7e81f 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,6 +1,5 @@ import React from 'react' import {useFocusEffect} from '@react-navigation/native' -import {observer} from 'mobx-react-lite' import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed' import {withAuthRequired} from 'view/com/auth/withAuthRequired' @@ -15,130 +14,126 @@ import {usePreferencesQuery} from '#/state/queries/preferences' import {emitSoftReset} from '#/state/events' type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> -export const HomeScreen = withAuthRequired( - observer(function HomeScreenImpl({}: Props) { - const setMinimalShellMode = useSetMinimalShellMode() - const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() - const pagerRef = React.useRef<PagerRef>(null) - const [selectedPage, setSelectedPage] = React.useState(0) - const [customFeeds, setCustomFeeds] = React.useState<FeedDescriptor[]>([]) - const {data: preferences} = usePreferencesQuery() - - React.useEffect(() => { - if (!preferences?.feeds?.pinned) return - - const pinned = preferences.feeds.pinned - - const feeds: FeedDescriptor[] = [] - - for (const uri of pinned) { - if (uri.includes('app.bsky.feed.generator')) { - feeds.push(`feedgen|${uri}`) - } else if (uri.includes('app.bsky.graph.list')) { - feeds.push(`list|${uri}`) - } +export const HomeScreen = withAuthRequired(function HomeScreenImpl({}: Props) { + const setMinimalShellMode = useSetMinimalShellMode() + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() + const pagerRef = React.useRef<PagerRef>(null) + const [selectedPage, setSelectedPage] = React.useState(0) + const [customFeeds, setCustomFeeds] = React.useState<FeedDescriptor[]>([]) + const {data: preferences} = usePreferencesQuery() + + React.useEffect(() => { + if (!preferences?.feeds?.pinned) return + + const pinned = preferences.feeds.pinned + + const feeds: FeedDescriptor[] = [] + + for (const uri of pinned) { + if (uri.includes('app.bsky.feed.generator')) { + feeds.push(`feedgen|${uri}`) + } else if (uri.includes('app.bsky.graph.list')) { + feeds.push(`list|${uri}`) } + } - setCustomFeeds(feeds) + setCustomFeeds(feeds) - pagerRef.current?.setPage(0) - }, [preferences?.feeds?.pinned, setCustomFeeds, pagerRef]) + pagerRef.current?.setPage(0) + }, [preferences?.feeds?.pinned, setCustomFeeds, pagerRef]) - const homeFeedParams = React.useMemo<FeedParams>(() => { - if (!preferences) return {} + const homeFeedParams = React.useMemo<FeedParams>(() => { + if (!preferences) return {} - return { - mergeFeedEnabled: Boolean( - preferences.feedViewPrefs.lab_mergeFeedEnabled, - ), - mergeFeedSources: preferences.feeds.saved, - } - }, [preferences]) + return { + mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled), + mergeFeedSources: preferences.feeds.saved, + } + }, [preferences]) - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - setDrawerSwipeDisabled(selectedPage > 0) - return () => { - setDrawerSwipeDisabled(false) - } - }, [setDrawerSwipeDisabled, selectedPage, setMinimalShellMode]), - ) - - const onPageSelected = React.useCallback( - (index: number) => { + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + setDrawerSwipeDisabled(selectedPage > 0) + return () => { + setDrawerSwipeDisabled(false) + } + }, [setDrawerSwipeDisabled, selectedPage, setMinimalShellMode]), + ) + + const onPageSelected = React.useCallback( + (index: number) => { + setMinimalShellMode(false) + setSelectedPage(index) + setDrawerSwipeDisabled(index > 0) + }, + [setDrawerSwipeDisabled, setSelectedPage, setMinimalShellMode], + ) + + const onPressSelected = React.useCallback(() => { + emitSoftReset() + }, []) + + const onPageScrollStateChanged = React.useCallback( + (state: 'idle' | 'dragging' | 'settling') => { + if (state === 'dragging') { setMinimalShellMode(false) - setSelectedPage(index) - setDrawerSwipeDisabled(index > 0) - }, - [setDrawerSwipeDisabled, setSelectedPage, setMinimalShellMode], - ) - - const onPressSelected = React.useCallback(() => { - emitSoftReset() - }, []) - - const onPageScrollStateChanged = React.useCallback( - (state: 'idle' | 'dragging' | 'settling') => { - if (state === 'dragging') { - setMinimalShellMode(false) - } - }, - [setMinimalShellMode], - ) - - const renderTabBar = React.useCallback( - (props: RenderTabBarFnProps) => { + } + }, + [setMinimalShellMode], + ) + + const renderTabBar = React.useCallback( + (props: RenderTabBarFnProps) => { + return ( + <FeedsTabBar + key="FEEDS_TAB_BAR" + selectedPage={props.selectedPage} + onSelect={props.onSelect} + testID="homeScreenFeedTabs" + onPressSelected={onPressSelected} + /> + ) + }, + [onPressSelected], + ) + + const renderFollowingEmptyState = React.useCallback(() => { + return <FollowingEmptyState /> + }, []) + + const renderCustomFeedEmptyState = React.useCallback(() => { + return <CustomFeedEmptyState /> + }, []) + + return ( + <Pager + ref={pagerRef} + testID="homeScreen" + onPageSelected={onPageSelected} + onPageScrollStateChanged={onPageScrollStateChanged} + renderTabBar={renderTabBar} + tabBarPosition="top"> + <FeedPage + key="1" + testID="followingFeedPage" + isPageFocused={selectedPage === 0} + feed="home" + feedParams={homeFeedParams} + renderEmptyState={renderFollowingEmptyState} + renderEndOfFeed={FollowingEndOfFeed} + /> + {customFeeds.map((f, index) => { return ( - <FeedsTabBar - key="FEEDS_TAB_BAR" - selectedPage={props.selectedPage} - onSelect={props.onSelect} - testID="homeScreenFeedTabs" - onPressSelected={onPressSelected} + <FeedPage + key={f} + testID="customFeedPage" + isPageFocused={selectedPage === 1 + index} + feed={f} + renderEmptyState={renderCustomFeedEmptyState} /> ) - }, - [onPressSelected], - ) - - const renderFollowingEmptyState = React.useCallback(() => { - return <FollowingEmptyState /> - }, []) - - const renderCustomFeedEmptyState = React.useCallback(() => { - return <CustomFeedEmptyState /> - }, []) - - return ( - <Pager - ref={pagerRef} - testID="homeScreen" - onPageSelected={onPageSelected} - onPageScrollStateChanged={onPageScrollStateChanged} - renderTabBar={renderTabBar} - tabBarPosition="top"> - <FeedPage - key="1" - testID="followingFeedPage" - isPageFocused={selectedPage === 0} - feed="home" - feedParams={homeFeedParams} - renderEmptyState={renderFollowingEmptyState} - renderEndOfFeed={FollowingEndOfFeed} - /> - {customFeeds.map((f, index) => { - return ( - <FeedPage - key={f} - testID="customFeedPage" - isPageFocused={selectedPage === 1 + index} - feed={f} - renderEmptyState={renderCustomFeedEmptyState} - /> - ) - })} - </Pager> - ) - }), -) + })} + </Pager> + ) +}) diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx index 677451526..649daea0e 100644 --- a/src/view/screens/LanguageSettings.tsx +++ b/src/view/screens/LanguageSettings.tsx @@ -1,6 +1,5 @@ import React from 'react' import {StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {Text} from '../com/util/text/Text' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' @@ -23,9 +22,7 @@ import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> -export const LanguageSettingsScreen = observer(function LanguageSettingsImpl( - _: Props, -) { +export function LanguageSettingsScreen(_: Props) { const pal = usePalette('default') const langPrefs = useLanguagePrefs() const setLangPrefs = useLanguagePrefsApi() @@ -192,7 +189,7 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl( </View> </CenteredView> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index 69c07edae..8680b851b 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -1,7 +1,6 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {ScrollView} from '../com/util/Views' @@ -15,7 +14,7 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' import {useSetMinimalShellMode} from '#/state/shell' -export const LogScreen = observer(function Log({}: NativeStackScreenProps< +export function LogScreen({}: NativeStackScreenProps< CommonNavigatorParams, 'Log' >) { @@ -88,7 +87,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps< </ScrollView> </View> ) -}) +} const styles = StyleSheet.create({ entry: { diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx index eb952c068..37eecf22d 100644 --- a/src/view/screens/Moderation.tsx +++ b/src/view/screens/Moderation.tsx @@ -5,7 +5,6 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {observer} from 'mobx-react-lite' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {s} from 'lib/styles' @@ -21,7 +20,7 @@ import {useModalControls} from '#/state/modals' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'> export const ModerationScreen = withAuthRequired( - observer(function Moderation({}: Props) { + function Moderation({}: Props) { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const {screen, track} = useAnalytics() @@ -111,7 +110,7 @@ export const ModerationScreen = withAuthRequired( </Link> </CenteredView> ) - }), + }, ) const styles = StyleSheet.create({ diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 752f78dce..844a96d11 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -3,7 +3,6 @@ import {StyleSheet, View} from 'react-native' import Animated from 'react-native-reanimated' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' -import {observer} from 'mobx-react-lite' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {makeRecordUri} from 'lib/strings/url-helpers' import {withAuthRequired} from 'view/com/auth/withAuthRequired' @@ -26,83 +25,83 @@ import {CenteredView} from '../com/util/Views' import {useComposerControls} from '#/state/shell/composer' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> -export const PostThreadScreen = withAuthRequired( - observer(function PostThreadScreenImpl({route}: Props) { - const queryClient = useQueryClient() - const {fabMinimalShellTransform} = useMinimalShellMode() - const setMinimalShellMode = useSetMinimalShellMode() - const {openComposer} = useComposerControls() - const safeAreaInsets = useSafeAreaInsets() - const {name, rkey} = route.params - const {isMobile} = useWebMediaQueries() - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) +export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({ + route, +}: Props) { + const queryClient = useQueryClient() + const {fabMinimalShellTransform} = useMinimalShellMode() + const setMinimalShellMode = useSetMinimalShellMode() + const {openComposer} = useComposerControls() + const safeAreaInsets = useSafeAreaInsets() + const {name, rkey} = route.params + const {isMobile} = useWebMediaQueries() + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - const onPressReply = React.useCallback(() => { - if (!resolvedUri) { - return - } - const thread = queryClient.getQueryData<ThreadNode>( - POST_THREAD_RQKEY(resolvedUri.uri), - ) - if (thread?.type !== 'post') { - return - } - openComposer({ - replyTo: { - uri: thread.post.uri, - cid: thread.post.cid, - text: thread.record.text, - author: { - handle: thread.post.author.handle, - displayName: thread.post.author.displayName, - avatar: thread.post.author.avatar, - }, + const onPressReply = React.useCallback(() => { + if (!resolvedUri) { + return + } + const thread = queryClient.getQueryData<ThreadNode>( + POST_THREAD_RQKEY(resolvedUri.uri), + ) + if (thread?.type !== 'post') { + return + } + openComposer({ + replyTo: { + uri: thread.post.uri, + cid: thread.post.cid, + text: thread.record.text, + author: { + handle: thread.post.author.handle, + displayName: thread.post.author.displayName, + avatar: thread.post.author.avatar, }, - onPost: () => - queryClient.invalidateQueries({ - queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), - }), - }) - }, [openComposer, queryClient, resolvedUri]) + }, + onPost: () => + queryClient.invalidateQueries({ + queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), + }), + }) + }, [openComposer, queryClient, resolvedUri]) - return ( - <View style={s.hContentRegion}> - {isMobile && <ViewHeader title="Post" />} - <View style={s.flex1}> - {uriError ? ( - <CenteredView> - <ErrorMessage message={String(uriError)} /> - </CenteredView> - ) : ( - <PostThreadComponent - uri={resolvedUri?.uri} - onPressReply={onPressReply} - /> - )} - </View> - {isMobile && ( - <Animated.View - style={[ - styles.prompt, - fabMinimalShellTransform, - { - bottom: clamp(safeAreaInsets.bottom, 15, 30), - }, - ]}> - <ComposePrompt onPressCompose={onPressReply} /> - </Animated.View> + return ( + <View style={s.hContentRegion}> + {isMobile && <ViewHeader title="Post" />} + <View style={s.flex1}> + {uriError ? ( + <CenteredView> + <ErrorMessage message={String(uriError)} /> + </CenteredView> + ) : ( + <PostThreadComponent + uri={resolvedUri?.uri} + onPressReply={onPressReply} + /> )} </View> - ) - }), -) + {isMobile && ( + <Animated.View + style={[ + styles.prompt, + fabMinimalShellTransform, + { + bottom: clamp(safeAreaInsets.bottom, 15, 30), + }, + ]}> + <ComposePrompt onPressCompose={onPressReply} /> + </Animated.View> + )} + </View> + ) +}) const styles = StyleSheet.create({ prompt: { diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx index 7b240ded0..2fd0eff37 100644 --- a/src/view/screens/PreferencesHomeFeed.tsx +++ b/src/view/screens/PreferencesHomeFeed.tsx @@ -1,6 +1,5 @@ import React, {useState} from 'react' import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Slider} from '@miblanchard/react-native-slider' import {Text} from '../com/util/text/Text' @@ -72,9 +71,7 @@ type Props = NativeStackScreenProps< CommonNavigatorParams, 'PreferencesHomeFeed' > -export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ - navigation, -}: Props) { +export function PreferencesHomeFeed({navigation}: Props) { const pal = usePalette('default') const {_} = useLingui() const {isTabletOrDesktop} = useWebMediaQueries() @@ -308,7 +305,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({ </View> </CenteredView> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx index 2386f6445..7bd87b712 100644 --- a/src/view/screens/PreferencesThreads.tsx +++ b/src/view/screens/PreferencesThreads.tsx @@ -6,7 +6,6 @@ import { TouchableOpacity, View, } from 'react-native' -import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../com/util/text/Text' import {s, colors} from 'lib/styles' @@ -25,9 +24,7 @@ import { } from '#/state/queries/preferences' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'> -export const PreferencesThreads = observer(function PreferencesThreadsImpl({ - navigation, -}: Props) { +export function PreferencesThreads({navigation}: Props) { const pal = usePalette('default') const {_} = useLingui() const {isTabletOrDesktop} = useWebMediaQueries() @@ -162,7 +159,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({ </View> </CenteredView> ) -}) +} const styles = StyleSheet.create({ container: { diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index f62790be6..62f5f1b36 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -15,7 +15,6 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {CommonNavigatorParams} from 'lib/routes/types' import {makeRecordUri} from 'lib/strings/url-helpers' import {colors, s} from 'lib/styles' -import {observer} from 'mobx-react-lite' import {FeedDescriptor} from '#/state/queries/post-feed' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' @@ -71,7 +70,7 @@ interface SectionRef { type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> export const ProfileFeedScreen = withAuthRequired( - observer(function ProfileFeedScreenImpl(props: Props) { + function ProfileFeedScreenImpl(props: Props) { const {rkey, name: handleOrDid} = props.route.params const pal = usePalette('default') @@ -129,7 +128,7 @@ export const ProfileFeedScreen = withAuthRequired( </View> </CenteredView> ) - }), + }, ) function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { @@ -154,7 +153,7 @@ function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { ) } -export const ProfileFeedScreenInner = function ProfileFeedScreenInnerImpl({ +export function ProfileFeedScreenInner({ preferences, feedInfo, }: { @@ -485,7 +484,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( }, ) -const AboutSection = observer(function AboutPageImpl({ +function AboutSection({ feedOwnerDid, feedRkey, feedInfo, @@ -606,7 +605,7 @@ const AboutSection = observer(function AboutPageImpl({ </View> </ScrollView> ) -}) +} const styles = StyleSheet.create({ btn: { diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index c7abcf090..e3e50ca24 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -14,7 +14,6 @@ import {track} from '#/lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {CommonNavigatorParams} from 'lib/routes/types' -import {observer} from 'mobx-react-lite' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from 'view/com/util/ViewHeader' @@ -146,7 +145,7 @@ export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) { ) }) -const ListItem = observer(function ListItemImpl({ +function ListItem({ feedUri, isPinned, }: { @@ -269,7 +268,7 @@ const ListItem = observer(function ListItemImpl({ </TouchableOpacity> </Pressable> ) -}) +} const styles = StyleSheet.create({ desktopContainer: { diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 3f7ef146a..916fd2a53 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -19,11 +19,9 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {observer} from 'mobx-react-lite' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import * as AppInfo from 'lib/app-info' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {ScrollView} from '../com/util/Views' import {ViewHeader} from '../com/util/ViewHeader' @@ -45,7 +43,7 @@ import {formatCount} from 'view/com/util/numeric/format' import Clipboard from '@react-native-clipboard/clipboard' import {makeProfileLink} from 'lib/routes/links' import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' -import {logger} from '#/logger' +import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' import {useModalControls} from '#/state/modals' import { useSetMinimalShellMode, @@ -69,6 +67,7 @@ import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header' import {STATUS_PAGE_URL} from 'lib/constants' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' function SettingsAccountCard({account}: {account: SessionAccount}) { const pal = usePalette('default') @@ -135,630 +134,620 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> -export const SettingsScreen = withAuthRequired( - observer(function Settings({}: Props) { - const colorMode = useColorMode() - const setColorMode = useSetColorMode() - const pal = usePalette('default') - const store = useStores() - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const requireAltTextEnabled = useRequireAltTextEnabled() - const setRequireAltTextEnabled = useSetRequireAltTextEnabled() - const onboardingDispatch = useOnboardingDispatch() - const navigation = useNavigation<NavigationProp>() - const {isMobile} = useWebMediaQueries() - const {screen, track} = useAnalytics() - const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting( - store.agent, - ) - const {openModal} = useModalControls() - const {isSwitchingAccounts, accounts, currentAccount} = useSession() - const {clearCurrentAccount} = useSessionApi() - const {mutate: clearPreferences} = useClearPreferencesMutation() - const {data: invites} = useInviteCodesQuery() - const invitesAvailable = invites?.available?.length ?? 0 - - const primaryBg = useCustomPalette<ViewStyle>({ - light: {backgroundColor: colors.blue0}, - dark: {backgroundColor: colors.blue6}, - }) - const primaryText = useCustomPalette<TextStyle>({ - light: {color: colors.blue3}, - dark: {color: colors.blue2}, - }) +export const SettingsScreen = withAuthRequired(function Settings({}: Props) { + const queryClient = useQueryClient() + const colorMode = useColorMode() + const setColorMode = useSetColorMode() + const pal = usePalette('default') + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const requireAltTextEnabled = useRequireAltTextEnabled() + const setRequireAltTextEnabled = useSetRequireAltTextEnabled() + const onboardingDispatch = useOnboardingDispatch() + const navigation = useNavigation<NavigationProp>() + const {isMobile} = useWebMediaQueries() + const {screen, track} = useAnalytics() + const {openModal} = useModalControls() + const {isSwitchingAccounts, accounts, currentAccount, agent} = useSession() + const {clearCurrentAccount} = useSessionApi() + const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(agent) + const {mutate: clearPreferences} = useClearPreferencesMutation() + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 + + const primaryBg = useCustomPalette<ViewStyle>({ + light: {backgroundColor: colors.blue0}, + dark: {backgroundColor: colors.blue6}, + }) + const primaryText = useCustomPalette<TextStyle>({ + light: {color: colors.blue3}, + dark: {color: colors.blue2}, + }) + + const dangerBg = useCustomPalette<ViewStyle>({ + light: {backgroundColor: colors.red1}, + dark: {backgroundColor: colors.red7}, + }) + const dangerText = useCustomPalette<TextStyle>({ + light: {color: colors.red4}, + dark: {color: colors.red2}, + }) + + useFocusEffect( + React.useCallback(() => { + screen('Settings') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) - const dangerBg = useCustomPalette<ViewStyle>({ - light: {backgroundColor: colors.red1}, - dark: {backgroundColor: colors.red7}, - }) - const dangerText = useCustomPalette<TextStyle>({ - light: {color: colors.red4}, - dark: {color: colors.red2}, + const onPressAddAccount = React.useCallback(() => { + track('Settings:AddAccountButtonClicked') + navigation.navigate('HomeTab') + navigation.dispatch(StackActions.popToTop()) + clearCurrentAccount() + }, [track, navigation, clearCurrentAccount]) + + const onPressChangeHandle = React.useCallback(() => { + track('Settings:ChangeHandleButtonClicked') + openModal({ + name: 'change-handle', + onChanged() { + if (currentAccount) { + // refresh my profile + queryClient.invalidateQueries({ + queryKey: RQKEY_PROFILE(currentAccount.did), + }) + } + }, }) + }, [track, queryClient, openModal, currentAccount]) - useFocusEffect( - React.useCallback(() => { - screen('Settings') - setMinimalShellMode(false) - }, [screen, setMinimalShellMode]), - ) + const onPressInviteCodes = React.useCallback(() => { + track('Settings:InvitecodesButtonClicked') + openModal({name: 'invite-codes'}) + }, [track, openModal]) + + const onPressLanguageSettings = React.useCallback(() => { + navigation.navigate('LanguageSettings') + }, [navigation]) - const onPressAddAccount = React.useCallback(() => { - track('Settings:AddAccountButtonClicked') - navigation.navigate('HomeTab') - navigation.dispatch(StackActions.popToTop()) - clearCurrentAccount() - }, [track, navigation, clearCurrentAccount]) - - const onPressChangeHandle = React.useCallback(() => { - track('Settings:ChangeHandleButtonClicked') - openModal({ - name: 'change-handle', - onChanged() { - store.session.reloadFromServer().then( - () => { - Toast.show('Your handle has been updated') - }, - err => { - logger.error('Failed to reload from server after handle update', { - error: err, - }) - }, - ) - }, - }) - }, [track, store, openModal]) - - const onPressInviteCodes = React.useCallback(() => { - track('Settings:InvitecodesButtonClicked') - openModal({name: 'invite-codes'}) - }, [track, openModal]) - - const onPressLanguageSettings = React.useCallback(() => { - navigation.navigate('LanguageSettings') - }, [navigation]) - - const onPressDeleteAccount = React.useCallback(() => { - openModal({name: 'delete-account'}) - }, [openModal]) - - const onPressResetPreferences = React.useCallback(async () => { - clearPreferences() - }, [clearPreferences]) - - const onPressResetOnboarding = React.useCallback(async () => { - onboardingDispatch({type: 'start'}) - Toast.show('Onboarding reset') - }, [onboardingDispatch]) - - const onPressBuildInfo = React.useCallback(() => { - Clipboard.setString( - `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, - ) - Toast.show('Copied build version to clipboard') - }, []) - - const openHomeFeedPreferences = React.useCallback(() => { - navigation.navigate('PreferencesHomeFeed') - }, [navigation]) - - const openThreadsPreferences = React.useCallback(() => { - navigation.navigate('PreferencesThreads') - }, [navigation]) - - const onPressAppPasswords = React.useCallback(() => { - navigation.navigate('AppPasswords') - }, [navigation]) - - const onPressSystemLog = React.useCallback(() => { - navigation.navigate('Log') - }, [navigation]) - - const onPressStorybook = React.useCallback(() => { - navigation.navigate('Debug') - }, [navigation]) - - const onPressSavedFeeds = React.useCallback(() => { - navigation.navigate('SavedFeeds') - }, [navigation]) - - const onPressStatusPage = React.useCallback(() => { - Linking.openURL(STATUS_PAGE_URL) - }, []) - - return ( - <View style={[s.hContentRegion]} testID="settingsScreen"> - <ViewHeader title="Settings" /> - <ScrollView - style={[s.hContentRegion]} - contentContainerStyle={isMobile && pal.viewLight} - scrollIndicatorInsets={{right: 1}}> - <View style={styles.spacer20} /> - {currentAccount ? ( - <> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Account</Trans> + const onPressDeleteAccount = React.useCallback(() => { + openModal({name: 'delete-account'}) + }, [openModal]) + + const onPressResetPreferences = React.useCallback(async () => { + clearPreferences() + }, [clearPreferences]) + + const onPressResetOnboarding = React.useCallback(async () => { + onboardingDispatch({type: 'start'}) + Toast.show('Onboarding reset') + }, [onboardingDispatch]) + + const onPressBuildInfo = React.useCallback(() => { + Clipboard.setString( + `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, + ) + Toast.show('Copied build version to clipboard') + }, []) + + const openHomeFeedPreferences = React.useCallback(() => { + navigation.navigate('PreferencesHomeFeed') + }, [navigation]) + + const openThreadsPreferences = React.useCallback(() => { + navigation.navigate('PreferencesThreads') + }, [navigation]) + + const onPressAppPasswords = React.useCallback(() => { + navigation.navigate('AppPasswords') + }, [navigation]) + + const onPressSystemLog = React.useCallback(() => { + navigation.navigate('Log') + }, [navigation]) + + const onPressStorybook = React.useCallback(() => { + navigation.navigate('Debug') + }, [navigation]) + + const onPressSavedFeeds = React.useCallback(() => { + navigation.navigate('SavedFeeds') + }, [navigation]) + + const onPressStatusPage = React.useCallback(() => { + Linking.openURL(STATUS_PAGE_URL) + }, []) + + return ( + <View style={[s.hContentRegion]} testID="settingsScreen"> + <ViewHeader title="Settings" /> + <ScrollView + style={[s.hContentRegion]} + contentContainerStyle={isMobile && pal.viewLight} + scrollIndicatorInsets={{right: 1}}> + <View style={styles.spacer20} /> + {currentAccount ? ( + <> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Account</Trans> + </Text> + <View style={[styles.infoLine]}> + <Text type="lg-medium" style={pal.text}> + Email:{' '} </Text> - <View style={[styles.infoLine]}> - <Text type="lg-medium" style={pal.text}> - Email:{' '} - </Text> - {currentAccount.emailConfirmed && ( - <> - <FontAwesomeIcon - icon="check" - size={10} - style={{color: colors.green3, marginRight: 2}} - /> - </> - )} - <Text type="lg" style={pal.text}> - {currentAccount.email}{' '} + {currentAccount.emailConfirmed && ( + <> + <FontAwesomeIcon + icon="check" + size={10} + style={{color: colors.green3, marginRight: 2}} + /> + </> + )} + <Text type="lg" style={pal.text}> + {currentAccount.email}{' '} + </Text> + <Link onPress={() => openModal({name: 'change-email'})}> + <Text type="lg" style={pal.link}> + <Trans>Change</Trans> </Text> - <Link onPress={() => openModal({name: 'change-email'})}> - <Text type="lg" style={pal.link}> - <Trans>Change</Trans> - </Text> - </Link> - </View> - <View style={[styles.infoLine]}> - <Text type="lg-medium" style={pal.text}> - <Trans>Birthday:</Trans>{' '} + </Link> + </View> + <View style={[styles.infoLine]}> + <Text type="lg-medium" style={pal.text}> + <Trans>Birthday:</Trans>{' '} + </Text> + <Link onPress={() => openModal({name: 'birth-date-settings'})}> + <Text type="lg" style={pal.link}> + <Trans>Show</Trans> </Text> - <Link onPress={() => openModal({name: 'birth-date-settings'})}> - <Text type="lg" style={pal.link}> - <Trans>Show</Trans> - </Text> - </Link> - </View> - <View style={styles.spacer20} /> - - {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} - </> - ) : null} - <View style={[s.flexRow, styles.heading]}> - <Text type="xl-bold" style={pal.text}> - <Trans>Signed in as</Trans> - </Text> - <View style={s.flex1} /> + </Link> + </View> + <View style={styles.spacer20} /> + + {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} + </> + ) : null} + <View style={[s.flexRow, styles.heading]}> + <Text type="xl-bold" style={pal.text}> + <Trans>Signed in as</Trans> + </Text> + <View style={s.flex1} /> + </View> + + {isSwitchingAccounts ? ( + <View style={[pal.view, styles.linkCard]}> + <ActivityIndicator /> </View> + ) : ( + <SettingsAccountCard account={currentAccount!} /> + )} - {isSwitchingAccounts ? ( - <View style={[pal.view, styles.linkCard]}> - <ActivityIndicator /> - </View> - ) : ( - <SettingsAccountCard account={currentAccount!} /> - )} + {accounts + .filter(a => a.did !== currentAccount?.did) + .map(account => ( + <SettingsAccountCard key={account.did} account={account} /> + ))} - {accounts - .filter(a => a.did !== currentAccount?.did) - .map(account => ( - <SettingsAccountCard key={account.did} account={account} /> - ))} + <TouchableOpacity + testID="switchToNewAccountBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressAddAccount} + accessibilityRole="button" + accessibilityLabel={_(msg`Add account`)} + accessibilityHint="Create a new Bluesky account"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="plus" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Add account</Trans> + </Text> + </TouchableOpacity> - <TouchableOpacity - testID="switchToNewAccountBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressAddAccount} - accessibilityRole="button" - accessibilityLabel={_(msg`Add account`)} - accessibilityHint="Create a new Bluesky account"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="plus" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Add account</Trans> - </Text> - </TouchableOpacity> + <View style={styles.spacer20} /> - <View style={styles.spacer20} /> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Invite a Friend</Trans> + </Text> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Invite a Friend</Trans> + <TouchableOpacity + testID="inviteFriendBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} + accessibilityRole="button" + accessibilityLabel={_(msg`Invite`)} + accessibilityHint="Opens invite code list"> + <View + style={[ + styles.iconContainer, + invitesAvailable > 0 ? primaryBg : pal.btn, + ]}> + <FontAwesomeIcon + icon="ticket" + style={ + (invitesAvailable > 0 + ? primaryText + : pal.text) as FontAwesomeIconStyle + } + /> + </View> + <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}> + {formatCount(invitesAvailable)} invite{' '} + {pluralize(invitesAvailable, 'code')} available </Text> + </TouchableOpacity> - <TouchableOpacity - testID="inviteFriendBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} - accessibilityRole="button" - accessibilityLabel={_(msg`Invite`)} - accessibilityHint="Opens invite code list"> - <View - style={[ - styles.iconContainer, - invitesAvailable > 0 ? primaryBg : pal.btn, - ]}> - <FontAwesomeIcon - icon="ticket" - style={ - (invitesAvailable > 0 - ? primaryText - : pal.text) as FontAwesomeIconStyle - } - /> - </View> - <Text type="lg" style={invitesAvailable > 0 ? pal.link : pal.text}> - {formatCount(invitesAvailable)} invite{' '} - {pluralize(invitesAvailable, 'code')} available - </Text> - </TouchableOpacity> + <View style={styles.spacer20} /> - <View style={styles.spacer20} /> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Accessibility</Trans> + </Text> + <View style={[pal.view, styles.toggleCard]}> + <ToggleButton + type="default-light" + label="Require alt text before posting" + labelType="lg" + isSelected={requireAltTextEnabled} + onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} + /> + </View> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Accessibility</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label="Require alt text before posting" - labelType="lg" - isSelected={requireAltTextEnabled} - onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} + <View style={styles.spacer20} /> + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Appearance</Trans> + </Text> + <View> + <View style={[styles.linkCard, pal.view, styles.selectableBtns]}> + <SelectableBtn + selected={colorMode === 'system'} + label="System" + left + onSelect={() => setColorMode('system')} + accessibilityHint="Set color theme to system setting" + /> + <SelectableBtn + selected={colorMode === 'light'} + label="Light" + onSelect={() => setColorMode('light')} + accessibilityHint="Set color theme to light" + /> + <SelectableBtn + selected={colorMode === 'dark'} + label="Dark" + right + onSelect={() => setColorMode('dark')} + accessibilityHint="Set color theme to dark" /> </View> + </View> + <View style={styles.spacer20} /> - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Appearance</Trans> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Basics</Trans> + </Text> + <TouchableOpacity + testID="preferencesHomeFeedButton" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={openHomeFeedPreferences} + accessibilityRole="button" + accessibilityHint="" + accessibilityLabel={_(msg`Opens the home feed preferences`)}> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="sliders" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Home Feed Preferences</Trans> </Text> - <View> - <View style={[styles.linkCard, pal.view, styles.selectableBtns]}> - <SelectableBtn - selected={colorMode === 'system'} - label="System" - left - onSelect={() => setColorMode('system')} - accessibilityHint="Set color theme to system setting" - /> - <SelectableBtn - selected={colorMode === 'light'} - label="Light" - onSelect={() => setColorMode('light')} - accessibilityHint="Set color theme to light" - /> - <SelectableBtn - selected={colorMode === 'dark'} - label="Dark" - right - onSelect={() => setColorMode('dark')} - accessibilityHint="Set color theme to dark" - /> - </View> + </TouchableOpacity> + <TouchableOpacity + testID="preferencesThreadsButton" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={openThreadsPreferences} + accessibilityRole="button" + accessibilityHint="" + accessibilityLabel={_(msg`Opens the threads preferences`)}> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon={['far', 'comments']} + style={pal.text as FontAwesomeIconStyle} + size={18} + /> </View> - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Basics</Trans> + <Text type="lg" style={pal.text}> + <Trans>Thread Preferences</Trans> </Text> - <TouchableOpacity - testID="preferencesHomeFeedButton" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={openHomeFeedPreferences} - accessibilityRole="button" - accessibilityHint="" - accessibilityLabel={_(msg`Opens the home feed preferences`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="sliders" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Home Feed Preferences</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="preferencesThreadsButton" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={openThreadsPreferences} - accessibilityRole="button" - accessibilityHint="" - accessibilityLabel={_(msg`Opens the threads preferences`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'comments']} - style={pal.text as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Thread Preferences</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="savedFeedsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - accessibilityHint="My Saved Feeds" - accessibilityLabel={_(msg`Opens screen with all saved feeds`)} - onPress={onPressSavedFeeds}> - <View style={[styles.iconContainer, pal.btn]}> - <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> - </View> - <Text type="lg" style={pal.text}> - <Trans>My Saved Feeds</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="languageSettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} - accessibilityRole="button" - accessibilityHint="Language settings" - accessibilityLabel={_(msg`Opens configurable language settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="language" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Languages</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="moderationBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts - ? undefined - : () => navigation.navigate('Moderation') - } - accessibilityRole="button" - accessibilityHint="" - accessibilityLabel={_(msg`Opens moderation settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <HandIcon style={pal.text} size={18} strokeWidth={6} /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Moderation</Trans> - </Text> - </TouchableOpacity> - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Advanced</Trans> + </TouchableOpacity> + <TouchableOpacity + testID="savedFeedsBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + accessibilityHint="My Saved Feeds" + accessibilityLabel={_(msg`Opens screen with all saved feeds`)} + onPress={onPressSavedFeeds}> + <View style={[styles.iconContainer, pal.btn]}> + <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> + </View> + <Text type="lg" style={pal.text}> + <Trans>My Saved Feeds</Trans> </Text> - <TouchableOpacity - testID="appPasswordBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={onPressAppPasswords} - accessibilityRole="button" - accessibilityHint="Open app password settings" - accessibilityLabel={_(msg`Opens the app password settings page`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="lock" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>App passwords</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="changeHandleBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} - accessibilityRole="button" - accessibilityLabel={_(msg`Change handle`)} - accessibilityHint="Choose a new Bluesky username or create"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="at" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text} numberOfLines={1}> - <Trans>Change handle</Trans> - </Text> - </TouchableOpacity> - <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Danger Zone</Trans> + </TouchableOpacity> + <TouchableOpacity + testID="languageSettingsBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} + accessibilityRole="button" + accessibilityHint="Language settings" + accessibilityLabel={_(msg`Opens configurable language settings`)}> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="language" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Languages</Trans> </Text> - <TouchableOpacity - style={[pal.view, styles.linkCard]} - onPress={onPressDeleteAccount} - accessible={true} - accessibilityRole="button" - accessibilityLabel={_(msg`Delete account`)} - accessibilityHint="Opens modal for account deletion confirmation. Requires email code."> - <View style={[styles.iconContainer, dangerBg]}> - <FontAwesomeIcon - icon={['far', 'trash-can']} - style={dangerText as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={dangerText}> - <Trans>Delete my account…</Trans> - </Text> - </TouchableOpacity> - <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Developer Tools</Trans> + </TouchableOpacity> + <TouchableOpacity + testID="moderationBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={ + isSwitchingAccounts + ? undefined + : () => navigation.navigate('Moderation') + } + accessibilityRole="button" + accessibilityHint="" + accessibilityLabel={_(msg`Opens moderation settings`)}> + <View style={[styles.iconContainer, pal.btn]}> + <HandIcon style={pal.text} size={18} strokeWidth={6} /> + </View> + <Text type="lg" style={pal.text}> + <Trans>Moderation</Trans> </Text> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressSystemLog} - accessibilityRole="button" - accessibilityHint="Open system log" - accessibilityLabel={_(msg`Opens the system log page`)}> - <Text type="lg" style={pal.text}> - <Trans>System log</Trans> - </Text> - </TouchableOpacity> - {__DEV__ ? ( - <ToggleButton - type="default-light" - label="Experiment: Use AppView Proxy" - isSelected={debugHeaderEnabled} - onPress={toggleDebugHeader} + </TouchableOpacity> + <View style={styles.spacer20} /> + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Advanced</Trans> + </Text> + <TouchableOpacity + testID="appPasswordBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={onPressAppPasswords} + accessibilityRole="button" + accessibilityHint="Open app password settings" + accessibilityLabel={_(msg`Opens the app password settings page`)}> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="lock" + style={pal.text as FontAwesomeIconStyle} /> - ) : null} - {__DEV__ ? ( - <> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressStorybook} - accessibilityRole="button" - accessibilityHint="Open storybook page" - accessibilityLabel={_(msg`Opens the storybook page`)}> - <Text type="lg" style={pal.text}> - <Trans>Storybook</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressResetPreferences} - accessibilityRole="button" - accessibilityHint="Reset preferences" - accessibilityLabel={_(msg`Resets the preferences state`)}> - <Text type="lg" style={pal.text}> - <Trans>Reset preferences state</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressResetOnboarding} - accessibilityRole="button" - accessibilityHint="Reset onboarding" - accessibilityLabel={_(msg`Resets the onboarding state`)}> - <Text type="lg" style={pal.text}> - <Trans>Reset onboarding state</Trans> - </Text> - </TouchableOpacity> - </> - ) : null} - <View style={[styles.footer]}> + </View> + <Text type="lg" style={pal.text}> + <Trans>App passwords</Trans> + </Text> + </TouchableOpacity> + <TouchableOpacity + testID="changeHandleBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} + accessibilityRole="button" + accessibilityLabel={_(msg`Change handle`)} + accessibilityHint="Choose a new Bluesky username or create"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="at" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text} numberOfLines={1}> + <Trans>Change handle</Trans> + </Text> + </TouchableOpacity> + <View style={styles.spacer20} /> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Danger Zone</Trans> + </Text> + <TouchableOpacity + style={[pal.view, styles.linkCard]} + onPress={onPressDeleteAccount} + accessible={true} + accessibilityRole="button" + accessibilityLabel={_(msg`Delete account`)} + accessibilityHint="Opens modal for account deletion confirmation. Requires email code."> + <View style={[styles.iconContainer, dangerBg]}> + <FontAwesomeIcon + icon={['far', 'trash-can']} + style={dangerText as FontAwesomeIconStyle} + size={18} + /> + </View> + <Text type="lg" style={dangerText}> + <Trans>Delete my account…</Trans> + </Text> + </TouchableOpacity> + <View style={styles.spacer20} /> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Developer Tools</Trans> + </Text> + <TouchableOpacity + style={[pal.view, styles.linkCardNoIcon]} + onPress={onPressSystemLog} + accessibilityRole="button" + accessibilityHint="Open system log" + accessibilityLabel={_(msg`Opens the system log page`)}> + <Text type="lg" style={pal.text}> + <Trans>System log</Trans> + </Text> + </TouchableOpacity> + {__DEV__ ? ( + <ToggleButton + type="default-light" + label="Experiment: Use AppView Proxy" + isSelected={debugHeaderEnabled} + onPress={toggleDebugHeader} + /> + ) : null} + {__DEV__ ? ( + <> <TouchableOpacity + style={[pal.view, styles.linkCardNoIcon]} + onPress={onPressStorybook} accessibilityRole="button" - onPress={onPressBuildInfo}> - <Text type="sm" style={[styles.buildInfo, pal.textLight]}> - <Trans> - Build version {AppInfo.appVersion} {AppInfo.updateChannel} - </Trans> + accessibilityHint="Open storybook page" + accessibilityLabel={_(msg`Opens the storybook page`)}> + <Text type="lg" style={pal.text}> + <Trans>Storybook</Trans> </Text> </TouchableOpacity> - <Text type="sm" style={[pal.textLight]}> - · - </Text> <TouchableOpacity + style={[pal.view, styles.linkCardNoIcon]} + onPress={onPressResetPreferences} accessibilityRole="button" - onPress={onPressStatusPage}> - <Text type="sm" style={[styles.buildInfo, pal.textLight]}> - <Trans>Status page</Trans> + accessibilityHint="Reset preferences" + accessibilityLabel={_(msg`Resets the preferences state`)}> + <Text type="lg" style={pal.text}> + <Trans>Reset preferences state</Trans> </Text> </TouchableOpacity> - </View> - <View style={s.footerSpacer} /> - </ScrollView> - </View> - ) - }), -) - -const EmailConfirmationNotice = observer( - function EmailConfirmationNoticeImpl() { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - const {openModal} = useModalControls() - - return ( - <View style={{marginBottom: 20}}> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Verify email</Trans> - </Text> - <View - style={[ - { - paddingVertical: isMobile ? 12 : 0, - paddingHorizontal: 18, - }, - pal.view, - ]}> - <View style={{flexDirection: 'row', marginBottom: 8}}> - <Pressable - style={[ - palInverted.view, - { - flexDirection: 'row', - gap: 6, - borderRadius: 6, - paddingHorizontal: 12, - paddingVertical: 10, - alignItems: 'center', - }, - isMobile && {flex: 1}, - ]} + <TouchableOpacity + style={[pal.view, styles.linkCardNoIcon]} + onPress={onPressResetOnboarding} accessibilityRole="button" - accessibilityLabel={_(msg`Verify my email`)} - accessibilityHint="" - onPress={() => openModal({name: 'verify-email'})}> - <FontAwesomeIcon - icon="envelope" - color={palInverted.colors.text} - size={16} - /> - <Text type="button" style={palInverted.text}> - <Trans>Verify My Email</Trans> + accessibilityHint="Reset onboarding" + accessibilityLabel={_(msg`Resets the onboarding state`)}> + <Text type="lg" style={pal.text}> + <Trans>Reset onboarding state</Trans> </Text> - </Pressable> - </View> - <Text style={pal.textLight}> - <Trans>Protect your account by verifying your email.</Trans> + </TouchableOpacity> + </> + ) : null} + <View style={[styles.footer]}> + <TouchableOpacity + accessibilityRole="button" + onPress={onPressBuildInfo}> + <Text type="sm" style={[styles.buildInfo, pal.textLight]}> + <Trans> + Build version {AppInfo.appVersion} {AppInfo.updateChannel} + </Trans> + </Text> + </TouchableOpacity> + <Text type="sm" style={[pal.textLight]}> + · </Text> + <TouchableOpacity + accessibilityRole="button" + onPress={onPressStatusPage}> + <Text type="sm" style={[styles.buildInfo, pal.textLight]}> + <Trans>Status page</Trans> + </Text> + </TouchableOpacity> </View> + <View style={s.footerSpacer} /> + </ScrollView> + </View> + ) +}) + +function EmailConfirmationNotice() { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const {_} = useLingui() + const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() + + return ( + <View style={{marginBottom: 20}}> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Verify email</Trans> + </Text> + <View + style={[ + { + paddingVertical: isMobile ? 12 : 0, + paddingHorizontal: 18, + }, + pal.view, + ]}> + <View style={{flexDirection: 'row', marginBottom: 8}}> + <Pressable + style={[ + palInverted.view, + { + flexDirection: 'row', + gap: 6, + borderRadius: 6, + paddingHorizontal: 12, + paddingVertical: 10, + alignItems: 'center', + }, + isMobile && {flex: 1}, + ]} + accessibilityRole="button" + accessibilityLabel={_(msg`Verify my email`)} + accessibilityHint="" + onPress={() => openModal({name: 'verify-email'})}> + <FontAwesomeIcon + icon="envelope" + color={palInverted.colors.text} + size={16} + /> + <Text type="button" style={palInverted.text}> + <Trans>Verify My Email</Trans> + </Text> + </Pressable> + </View> + <Text style={pal.textLight}> + <Trans>Protect your account by verifying your email.</Trans> + </Text> </View> - ) - }, -) + </View> + ) +} const styles = StyleSheet.create({ dimmed: { diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 1ee359be0..a67a105bb 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -10,7 +10,6 @@ import { ViewStyle, } from 'react-native' import {useNavigation, StackActions} from '@react-navigation/native' -import {observer} from 'mobx-react-lite' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -101,7 +100,7 @@ export function DrawerProfileCard({ ) } -export const DrawerContent = observer(function DrawerContentImpl() { +export function DrawerContent() { const theme = useTheme() const pal = usePalette('default') const {_} = useLingui() @@ -404,7 +403,7 @@ export const DrawerContent = observer(function DrawerContentImpl() { </SafeAreaView> </View> ) -}) +} interface MenuItemProps extends ComponentProps<typeof TouchableOpacity> { icon: JSX.Element @@ -458,11 +457,7 @@ function MenuItem({ ) } -const InviteCodes = observer(function InviteCodesImpl({ - style, -}: { - style?: StyleProp<ViewStyle> -}) { +function InviteCodes({style}: {style?: StyleProp<ViewStyle>}) { const {track} = useAnalytics() const setDrawerOpen = useSetDrawerOpen() const pal = usePalette('default') @@ -502,7 +497,7 @@ const InviteCodes = observer(function InviteCodesImpl({ </Text> </TouchableOpacity> ) -}) +} const styles = StyleSheet.create({ view: { diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index a1502d2e9..ed800679d 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -4,7 +4,6 @@ import Animated from 'react-native-reanimated' import {StackActions} from '@react-navigation/native' import {BottomTabBarProps} from '@react-navigation/bottom-tabs' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {observer} from 'mobx-react-lite' import {Text} from 'view/com/util/text/Text' import {useAnalytics} from 'lib/analytics/analytics' import {clamp} from 'lib/numbers' @@ -34,9 +33,7 @@ import {useProfileQuery} from '#/state/queries/profile' type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' -export const BottomBar = observer(function BottomBarImpl({ - navigation, -}: BottomTabBarProps) { +export function BottomBar({navigation}: BottomTabBarProps) { const {openModal} = useModalControls() const {currentAccount} = useSession() const pal = usePalette('default') @@ -231,7 +228,7 @@ export const BottomBar = observer(function BottomBarImpl({ /> </Animated.View> ) -}) +} interface BtnProps extends Pick< diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index d58580502..32fa1943a 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {usePalette} from 'lib/hooks/usePalette' import {useNavigationState} from '@react-navigation/native' import Animated from 'react-native-reanimated' @@ -24,7 +23,7 @@ import {makeProfileLink} from 'lib/routes/links' import {CommonNavigatorParams} from 'lib/routes/types' import {useSession} from '#/state/session' -export const BottomBarWeb = observer(function BottomBarWebImpl() { +export function BottomBarWeb() { const {currentAccount} = useSession() const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() @@ -111,7 +110,7 @@ export const BottomBarWeb = observer(function BottomBarWebImpl() { </NavItem> </Animated.View> ) -}) +} const NavItem: React.FC<{ children: (props: {isActive: boolean}) => React.ReactChild diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 9cb10517e..dc5e311f4 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -1,13 +1,12 @@ import React from 'react' import {View, StyleSheet} from 'react-native' import {useNavigationState} from '@react-navigation/native' -import {observer} from 'mobx-react-lite' import {usePalette} from 'lib/hooks/usePalette' import {TextLink} from 'view/com/util/Link' import {getCurrentRoute} from 'lib/routes/helpers' import {usePinnedFeedsInfos} from '#/state/queries/feed' -export const DesktopFeeds = observer(function DesktopFeeds() { +export function DesktopFeeds() { const pal = usePalette('default') const feeds = usePinnedFeedsInfos() @@ -54,7 +53,7 @@ export const DesktopFeeds = observer(function DesktopFeeds() { </View> </View> ) -}) +} function FeedItem({ title, diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 3ec68872e..841a49d38 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {PressableWithHover} from 'view/com/util/PressableWithHover' import { @@ -47,7 +46,7 @@ import {useComposerControls} from '#/state/shell/composer' import {useFetchHandle} from '#/state/queries/handle' import {emitSoftReset} from '#/state/events' -const ProfileCard = observer(function ProfileCardImpl() { +function ProfileCard() { const {currentAccount} = useSession() const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did}) const {isDesktop} = useWebMediaQueries() @@ -73,7 +72,7 @@ const ProfileCard = observer(function ProfileCardImpl() { /> </View> ) -}) +} function BackBtn() { const {isTablet} = useWebMediaQueries() @@ -117,13 +116,7 @@ interface NavItemProps { iconFilled: JSX.Element label: string } -const NavItem = observer(function NavItemImpl({ - count, - href, - icon, - iconFilled, - label, -}: NavItemProps) { +function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { const pal = usePalette('default') const {currentAccount} = useSession() const {isDesktop, isTablet} = useWebMediaQueries() @@ -192,7 +185,7 @@ const NavItem = observer(function NavItemImpl({ )} </PressableWithHover> ) -}) +} function ComposeBtn() { const {currentAccount} = useSession() @@ -264,7 +257,7 @@ function ComposeBtn() { ) } -export const DesktopLeftNav = observer(function DesktopLeftNav() { +export function DesktopLeftNav() { const {currentAccount} = useSession() const pal = usePalette('default') const {isDesktop, isTablet} = useWebMediaQueries() @@ -422,7 +415,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { <ComposeBtn /> </View> ) -}) +} const styles = StyleSheet.create({ leftNav: { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 9e17cdcd9..3b94c12ef 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' @@ -16,7 +15,7 @@ import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' import {useInviteCodesQuery} from '#/state/queries/invites' -export const DesktopRightNav = observer(function DesktopRightNavImpl() { +export function DesktopRightNav() { const pal = usePalette('default') const palError = usePalette('error') const {isSandbox, hasSession, currentAccount} = useSession() @@ -80,9 +79,9 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { <InviteCodes /> </View> ) -}) +} -const InviteCodes = observer(function InviteCodesImpl() { +function InviteCodes() { const pal = usePalette('default') const {openModal} = useModalControls() const {data: invites} = useInviteCodesQuery() @@ -118,7 +117,7 @@ const InviteCodes = observer(function InviteCodesImpl() { </Text> </TouchableOpacity> ) -}) +} const styles = StyleSheet.create({ rightNav: { diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index c0eed0787..09f8ba983 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -1,5 +1,4 @@ import React, {useEffect} from 'react' -import {observer} from 'mobx-react-lite' import {View, StyleSheet, TouchableOpacity} from 'react-native' import {DesktopLeftNav} from './desktop/LeftNav' import {DesktopRightNav} from './desktop/RightNav' @@ -76,7 +75,7 @@ function ShellInner() { ) } -export const Shell: React.FC = observer(function ShellImpl() { +export const Shell: React.FC = function ShellImpl() { const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) return ( <View style={[s.hContentRegion, pageBg]}> @@ -85,7 +84,7 @@ export const Shell: React.FC = observer(function ShellImpl() { </RoutesContainer> </View> ) -}) +} const styles = StyleSheet.create({ bgLight: { |