diff options
Diffstat (limited to 'src/view/screens')
-rw-r--r-- | src/view/screens/Home.tsx | 231 | ||||
-rw-r--r-- | src/view/screens/LanguageSettings.tsx | 7 | ||||
-rw-r--r-- | src/view/screens/Log.tsx | 5 | ||||
-rw-r--r-- | src/view/screens/Moderation.tsx | 5 | ||||
-rw-r--r-- | src/view/screens/PostThread.tsx | 143 | ||||
-rw-r--r-- | src/view/screens/PreferencesHomeFeed.tsx | 7 | ||||
-rw-r--r-- | src/view/screens/PreferencesThreads.tsx | 7 | ||||
-rw-r--r-- | src/view/screens/ProfileFeed.tsx | 11 | ||||
-rw-r--r-- | src/view/screens/SavedFeeds.tsx | 5 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 1163 |
10 files changed, 777 insertions, 807 deletions
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: { |