diff options
Diffstat (limited to 'src/view/screens')
-rw-r--r-- | src/view/screens/DebugMod.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 30 | ||||
-rw-r--r-- | src/view/screens/ModerationBlockedAccounts.tsx | 6 | ||||
-rw-r--r-- | src/view/screens/ModerationMutedAccounts.tsx | 6 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 80 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 24 | ||||
-rw-r--r-- | src/view/screens/Search/Search.tsx | 511 | ||||
-rw-r--r-- | src/view/screens/Settings/index.tsx | 153 | ||||
-rw-r--r-- | src/view/screens/Storybook/ListContained.tsx | 104 | ||||
-rw-r--r-- | src/view/screens/Storybook/index.tsx | 164 |
10 files changed, 619 insertions, 461 deletions
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx index f88d500f9..442e33fd3 100644 --- a/src/view/screens/DebugMod.tsx +++ b/src/view/screens/DebugMod.tsx @@ -20,12 +20,12 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' +import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts' import {FeedNotification} from '#/state/queries/notifications/types' import { groupNotifications, shouldFilterNotif, } from '#/state/queries/notifications/util' -import {moderationOptsOverrideContext} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {CenteredView, ScrollView} from '#/view/com/util/Views' diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 3eaa1b875..665400f14 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -119,22 +119,24 @@ function HomeScreenReady({ const gate = useGate() const mode = useMinimalShellMode() const {isMobile} = useWebMediaQueries() - React.useEffect(() => { - const listener = AppState.addEventListener('change', nextAppState => { - if (nextAppState === 'active') { - if ( - isMobile && - mode.value === 1 && - gate('disable_min_shell_on_foregrounding_v2') - ) { - setMinimalShellMode(false) + useFocusEffect( + React.useCallback(() => { + const listener = AppState.addEventListener('change', nextAppState => { + if (nextAppState === 'active') { + if ( + isMobile && + mode.value === 1 && + gate('disable_min_shell_on_foregrounding_v3') + ) { + setMinimalShellMode(false) + } } + }) + return () => { + listener.remove() } - }) - return () => { - listener.remove() - } - }, [setMinimalShellMode, mode, isMobile, gate]) + }, [setMinimalShellMode, mode, isMobile, gate]), + ) const onPageSelected = React.useCallback( (index: number) => { diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index b7ce8cdd0..ebd9bb23e 100644 --- a/src/view/screens/ModerationBlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -20,8 +20,6 @@ import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {CommonNavigatorParams} from 'lib/routes/types' -import {useGate} from 'lib/statsig/statsig' -import {isWeb} from 'platform/detection' import {ProfileCard} from 'view/com/profile/ProfileCard' import {CenteredView} from 'view/com/util/Views' import {ErrorScreen} from '../com/util/error/ErrorScreen' @@ -38,7 +36,6 @@ export function ModerationBlockedAccounts({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() - const gate = useGate() const [isPTRing, setIsPTRing] = React.useState(false) const { @@ -168,9 +165,6 @@ export function ModerationBlockedAccounts({}: Props) { )} // @ts-ignore our .web version only -prf desktopFixedHeight - showsVerticalScrollIndicator={ - isWeb || !gate('hide_vertical_scroll_indicators') - } /> )} </CenteredView> diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index 4d7ca6294..e395a3a5b 100644 --- a/src/view/screens/ModerationMutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -20,8 +20,6 @@ import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {CommonNavigatorParams} from 'lib/routes/types' -import {useGate} from 'lib/statsig/statsig' -import {isWeb} from 'platform/detection' import {ProfileCard} from 'view/com/profile/ProfileCard' import {CenteredView} from 'view/com/util/Views' import {ErrorScreen} from '../com/util/error/ErrorScreen' @@ -38,7 +36,6 @@ export function ModerationMutedAccounts({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop} = useWebMediaQueries() const {screen} = useAnalytics() - const gate = useGate() const [isPTRing, setIsPTRing] = React.useState(false) const { @@ -167,9 +164,6 @@ export function ModerationMutedAccounts({}: Props) { )} // @ts-ignore our .web version only -prf desktopFixedHeight - showsVerticalScrollIndicator={ - isWeb || !gate('hide_vertical_scroll_indicators') - } /> )} </CenteredView> diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index eb9979823..4fa46a4cf 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo} from 'react' +import React, {useMemo} from 'react' import {StyleSheet} from 'react-native' import { AppBskyActorDefs, @@ -11,12 +11,11 @@ import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' -import {logEvent, useGate} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useLabelerInfoQuery} from '#/state/queries/labeler' import {resetProfilePostsQueries} from '#/state/queries/post-feed' -import {useModerationOpts} from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useAgent, useSession} from '#/state/session' @@ -466,7 +465,6 @@ function ProfileScreenLoaded({ accessibilityHint="" /> )} - <TestGates /> </ScreenHider> ) } @@ -525,77 +523,3 @@ const styles = StyleSheet.create({ textAlign: 'center', }, }) - -const shouldExposeToGate2 = Math.random() < 0.2 - -// --- Temporary: we're testing our Statsig setup --- -let TestGates = React.memo(function TestGates() { - const gate = useGate() - - useEffect(() => { - logEvent('test:all:always', {}) - if (Math.random() < 0.2) { - logEvent('test:all:sometimes', {}) - } - if (Math.random() < 0.1) { - logEvent('test:all:boosted_by_gate1', { - reason: 'base', - }) - } - if (Math.random() < 0.1) { - logEvent('test:all:boosted_by_gate2', { - reason: 'base', - }) - } - if (Math.random() < 0.1) { - logEvent('test:all:boosted_by_both', { - reason: 'base', - }) - } - }, []) - - return [ - gate('test_gate_1') ? <TestGate1 /> : null, - shouldExposeToGate2 && gate('test_gate_2') ? <TestGate2 /> : null, - ] -}) - -function TestGate1() { - useEffect(() => { - logEvent('test:gate1:always', {}) - if (Math.random() < 0.2) { - logEvent('test:gate1:sometimes', {}) - } - if (Math.random() < 0.5) { - logEvent('test:all:boosted_by_gate1', { - reason: 'gate1', - }) - } - if (Math.random() < 0.5) { - logEvent('test:all:boosted_by_both', { - reason: 'gate1', - }) - } - }, []) - return null -} - -function TestGate2() { - useEffect(() => { - logEvent('test:gate2:always', {}) - if (Math.random() < 0.2) { - logEvent('test:gate2:sometimes', {}) - } - if (Math.random() < 0.5) { - logEvent('test:all:boosted_by_gate2', { - reason: 'gate2', - }) - } - if (Math.random() < 0.5) { - logEvent('test:all:boosted_by_both', { - reason: 'gate2', - }) - } - }, []) - return null -} diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 1d93a9fd7..2902ccf5e 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -454,33 +454,29 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { }, }) } - if (isCurateList) { + if (isCurateList && (isBlocking || isMuting)) { items.push({label: 'separator'}) - if (!isBlocking) { + if (isMuting) { items.push({ testID: 'listHeaderDropdownMuteBtn', - label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`), - onPress: isMuting - ? onUnsubscribeMute - : subscribeMutePromptControl.open, + label: _(msg`Un-mute list`), + onPress: onUnsubscribeMute, icon: { ios: { - name: isMuting ? 'eye' : 'eye.slash', + name: 'eye', }, android: '', - web: isMuting ? 'eye' : ['far', 'eye-slash'], + web: 'eye', }, }) } - if (!isMuting) { + if (isBlocking) { items.push({ testID: 'listHeaderDropdownBlockBtn', - label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`), - onPress: isBlocking - ? onUnsubscribeBlock - : subscribeBlockPromptControl.open, + label: _(msg`Un-block list`), + onPress: onUnsubscribeBlock, icon: { ios: { name: 'person.fill.xmark', @@ -508,9 +504,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { isBlocking, isMuting, onUnsubscribeMute, - subscribeMutePromptControl.open, onUnsubscribeBlock, - subscribeBlockPromptControl.open, ]) const subscribeDropdownItems: DropdownItem[] = useMemo(() => { diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index ee9e69433..9dd1c397f 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -25,11 +25,11 @@ import {NavigationProp} from '#/lib/routes/types' import {augmentSearchQuery} from '#/lib/strings/helpers' import {s} from '#/lib/styles' import {logger} from '#/logger' -import {isNative, isWeb} from '#/platform/detection' +import {isIOS, isNative, isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' +import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorSearch} from '#/state/queries/actor-search' -import {useModerationOpts} from '#/state/queries/preferences' import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' import {useSession} from '#/state/session' @@ -49,11 +49,7 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {List} from '#/view/com/util/List' import {Text} from '#/view/com/util/text/Text' import {CenteredView, ScrollView} from '#/view/com/util/Views' -import { - MATCH_HANDLE, - SearchLinkCard, - SearchProfileCard, -} from '#/view/shell/desktop/Search' +import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {atoms as a} from '#/alf' @@ -156,7 +152,7 @@ function useSuggestedFollows(): [ return [items, onEndReached] } -function SearchScreenSuggestedFollows() { +let SearchScreenSuggestedFollows = (_props: {}): React.ReactNode => { const pal = usePalette('default') const [suggestions, onEndReached] = useSuggestedFollows() @@ -180,6 +176,7 @@ function SearchScreenSuggestedFollows() { </CenteredView> ) } +SearchScreenSuggestedFollows = React.memo(SearchScreenSuggestedFollows) type SearchResultSlice = | { @@ -192,7 +189,7 @@ type SearchResultSlice = key: string } -function SearchScreenPostResults({ +let SearchScreenPostResults = ({ query, sort, active, @@ -200,7 +197,7 @@ function SearchScreenPostResults({ query: string sort?: 'top' | 'latest' active: boolean -}) { +}): React.ReactNode => { const {_} = useLingui() const {currentAccount} = useSession() const [isPTR, setIsPTR] = React.useState(false) @@ -298,14 +295,15 @@ function SearchScreenPostResults({ </> ) } +SearchScreenPostResults = React.memo(SearchScreenPostResults) -function SearchScreenUserResults({ +let SearchScreenUserResults = ({ query, active, }: { query: string active: boolean -}) { +}): React.ReactNode => { const {_} = useLingui() const {data: results, isFetched} = useActorSearch({ @@ -334,8 +332,9 @@ function SearchScreenUserResults({ <Loader /> ) } +SearchScreenUserResults = React.memo(SearchScreenUserResults) -export function SearchScreenInner({query}: {query?: string}) { +let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() @@ -467,18 +466,17 @@ export function SearchScreenInner({query}: {query?: string}) { </CenteredView> ) } +SearchScreenInner = React.memo(SearchScreenInner) export function SearchScreen( props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, ) { const navigation = useNavigation<NavigationProp>() - const theme = useTheme() const textInput = React.useRef<TextInput>(null) const {_} = useLingui() const pal = usePalette('default') const {track} = useAnalytics() const setDrawerOpen = useSetDrawerOpen() - const moderationOpts = useModerationOpts() const setMinimalShellMode = useSetMinimalShellMode() const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries() @@ -527,23 +525,10 @@ export function SearchScreen( const onPressCancelSearch = React.useCallback(() => { scrollToTopWeb() - - if (showAutocomplete) { - textInput.current?.blur() - setShowAutocomplete(false) - setSearchText(queryParam) - } else { - // If we just `setParams` and set `q` to an empty string, the URL still displays `q=`, which isn't pretty. - // However, `.replace()` on native has a "push" animation that we don't want. So we need to handle these - // differently. - if (isWeb) { - navigation.replace('Search', {}) - } else { - setSearchText('') - navigation.setParams({q: ''}) - } - } - }, [showAutocomplete, navigation, queryParam]) + textInput.current?.blur() + setShowAutocomplete(false) + setSearchText(queryParam) + }, [queryParam]) const onChangeText = React.useCallback(async (text: string) => { scrollToTopWeb() @@ -597,20 +582,31 @@ export function SearchScreen( navigateToItem(searchText) }, [navigateToItem, searchText]) - const handleHistoryItemClick = (item: string) => { - setSearchText(item) - navigateToItem(item) - } + const onAutocompleteResultPress = React.useCallback(() => { + if (isWeb) { + setShowAutocomplete(false) + } else { + textInput.current?.blur() + } + }, []) - const onSoftReset = React.useCallback(() => { - scrollToTopWeb() - onPressCancelSearch() - }, [onPressCancelSearch]) + const handleHistoryItemClick = React.useCallback( + (item: string) => { + setSearchText(item) + navigateToItem(item) + }, + [navigateToItem], + ) - const queryMaybeHandle = React.useMemo(() => { - const match = MATCH_HANDLE.exec(queryParam) - return match && match[1] - }, [queryParam]) + const onSoftReset = React.useCallback(() => { + if (isWeb) { + // Empty params resets the URL to be /search rather than /search?q= + navigation.replace('Search', {}) + } else { + setSearchText('') + navigation.setParams({q: ''}) + } + }, [navigation]) useFocusEffect( React.useCallback(() => { @@ -619,15 +615,19 @@ export function SearchScreen( }, [onSoftReset, setMinimalShellMode]), ) - const handleRemoveHistoryItem = (itemToRemove: string) => { - const updatedHistory = searchHistory.filter(item => item !== itemToRemove) - setSearchHistory(updatedHistory) - AsyncStorage.setItem('searchHistory', JSON.stringify(updatedHistory)).catch( - e => { + const handleRemoveHistoryItem = React.useCallback( + (itemToRemove: string) => { + const updatedHistory = searchHistory.filter(item => item !== itemToRemove) + setSearchHistory(updatedHistory) + AsyncStorage.setItem( + 'searchHistory', + JSON.stringify(updatedHistory), + ).catch(e => { logger.error('Failed to update search history', {message: e}) - }, - ) - } + }) + }, + [searchHistory], + ) return ( <View style={isWeb ? null : {flex: 1}}> @@ -655,175 +655,269 @@ export function SearchScreen( /> </Pressable> )} - - <View - style={[ - {backgroundColor: pal.colors.backgroundLight}, - styles.headerSearchContainer, - ]}> - <MagnifyingGlassIcon - style={[pal.icon, styles.headerSearchIcon]} - size={21} - /> - <TextInput - testID="searchTextInput" - ref={textInput} - placeholder={_(msg`Search`)} - placeholderTextColor={pal.colors.textLight} - selectTextOnFocus={isNative} - returnKeyType="search" - value={searchText} - style={[pal.text, styles.headerSearchInput]} - keyboardAppearance={theme.colorScheme} - onFocus={() => { - if (isWeb) { - // Prevent a jump on iPad by ensuring that - // the initial focused render has no result list. - requestAnimationFrame(() => { - setShowAutocomplete(true) - }) - } else { - setShowAutocomplete(true) - } - }} - onChangeText={onChangeText} - onSubmitEditing={onSubmit} - autoFocus={false} - accessibilityRole="search" - accessibilityLabel={_(msg`Search`)} - accessibilityHint="" - autoCorrect={false} - autoComplete="off" - autoCapitalize="none" - /> - {showAutocomplete ? ( - <Pressable - testID="searchTextInputClearBtn" - onPress={onPressClearQuery} - accessibilityRole="button" - accessibilityLabel={_(msg`Clear search query`)} - accessibilityHint="" - hitSlop={HITSLOP_10}> - <FontAwesomeIcon - icon="xmark" - size={16} - style={pal.textLight as FontAwesomeIconStyle} - /> - </Pressable> - ) : undefined} - </View> - - {(queryParam || showAutocomplete) && ( - <View style={styles.headerCancelBtn}> + <SearchInputBox + textInput={textInput} + searchText={searchText} + showAutocomplete={showAutocomplete} + setShowAutocomplete={setShowAutocomplete} + onChangeText={onChangeText} + onSubmit={onSubmit} + onPressClearQuery={onPressClearQuery} + /> + {showAutocomplete && ( + <View style={[styles.headerCancelBtn]}> <Pressable onPress={onPressCancelSearch} accessibilityRole="button" hitSlop={HITSLOP_10}> - <Text style={[pal.text]}> + <Text style={pal.text}> <Trans>Cancel</Trans> </Text> </Pressable> </View> )} </CenteredView> + <View + style={{ + display: showAutocomplete ? 'flex' : 'none', + flex: 1, + }}> + {searchText.length > 0 ? ( + <AutocompleteResults + isAutocompleteFetching={isAutocompleteFetching} + autocompleteData={autocompleteData} + searchText={searchText} + onSubmit={onSubmit} + onResultPress={onAutocompleteResultPress} + /> + ) : ( + <SearchHistory + searchHistory={searchHistory} + onItemClick={handleHistoryItemClick} + onRemoveItemClick={handleRemoveHistoryItem} + /> + )} + </View> + <View + style={{ + display: showAutocomplete ? 'none' : 'flex', + flex: 1, + }}> + <SearchScreenInner query={queryParam} /> + </View> + </View> + ) +} - {showAutocomplete && searchText.length > 0 ? ( - <> - {(isAutocompleteFetching && !autocompleteData?.length) || - !moderationOpts ? ( - <Loader /> - ) : ( - <ScrollView - style={{height: '100%'}} - // @ts-ignore web only -prf - dataSet={{stableGutters: '1'}} - keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag"> - <SearchLinkCard - label={_(msg`Search for "${searchText}"`)} - onPress={isNative ? onSubmit : undefined} - to={ - isNative - ? undefined - : `/search?q=${encodeURIComponent(searchText)}` - } - style={{borderBottomWidth: 1}} - /> - - {queryMaybeHandle ? ( - <SearchLinkCard - label={_(msg`Go to @${queryMaybeHandle}`)} - to={`/profile/${queryMaybeHandle}`} - /> - ) : null} - - {autocompleteData?.map(item => ( - <SearchProfileCard - key={item.did} - profile={item} - moderation={moderateProfile(item, moderationOpts)} - onPress={() => { - if (isWeb) { - setShowAutocomplete(false) - } else { - textInput.current?.blur() - } - }} - /> - ))} - - <View style={{height: 200}} /> - </ScrollView> - )} - </> - ) : !queryParam && showAutocomplete ? ( - <CenteredView - sideBorders={isTabletOrDesktop} +let SearchInputBox = ({ + textInput, + searchText, + showAutocomplete, + setShowAutocomplete, + onChangeText, + onSubmit, + onPressClearQuery, +}: { + textInput: React.RefObject<TextInput> + searchText: string + showAutocomplete: boolean + setShowAutocomplete: (show: boolean) => void + onChangeText: (text: string) => void + onSubmit: () => void + onPressClearQuery: () => void +}): React.ReactNode => { + const pal = usePalette('default') + const {_} = useLingui() + const theme = useTheme() + return ( + <Pressable + // This only exists only for extra hitslop so don't expose it to the a11y tree. + accessible={false} + focusable={false} + // @ts-ignore web-only + tabIndex={-1} + style={[ + {backgroundColor: pal.colors.backgroundLight}, + styles.headerSearchContainer, + isWeb && { + // @ts-ignore web only + cursor: 'default', + }, + ]} + onPress={() => { + textInput.current?.focus() + }}> + <MagnifyingGlassIcon + style={[pal.icon, styles.headerSearchIcon]} + size={21} + /> + <TextInput + testID="searchTextInput" + ref={textInput} + placeholder={_(msg`Search`)} + placeholderTextColor={pal.colors.textLight} + returnKeyType="search" + value={searchText} + style={[pal.text, styles.headerSearchInput]} + keyboardAppearance={theme.colorScheme} + selectTextOnFocus={isNative} + onFocus={() => { + if (isWeb) { + // Prevent a jump on iPad by ensuring that + // the initial focused render has no result list. + requestAnimationFrame(() => { + setShowAutocomplete(true) + }) + } else { + setShowAutocomplete(true) + if (isIOS) { + // We rely on selectTextOnFocus, but it's broken on iOS: + // https://github.com/facebook/react-native/issues/41988 + textInput.current?.setSelection(0, searchText.length) + // We still rely on selectTextOnFocus for it to be instant on Android. + } + } + }} + onChangeText={onChangeText} + onSubmitEditing={onSubmit} + autoFocus={false} + accessibilityRole="search" + accessibilityLabel={_(msg`Search`)} + accessibilityHint="" + autoCorrect={false} + autoComplete="off" + autoCapitalize="none" + /> + {showAutocomplete && searchText.length > 0 && ( + <Pressable + testID="searchTextInputClearBtn" + onPress={onPressClearQuery} + accessibilityRole="button" + accessibilityLabel={_(msg`Clear search query`)} + accessibilityHint="" + hitSlop={HITSLOP_10}> + <FontAwesomeIcon + icon="xmark" + size={16} + style={pal.textLight as FontAwesomeIconStyle} + /> + </Pressable> + )} + </Pressable> + ) +} +SearchInputBox = React.memo(SearchInputBox) + +let AutocompleteResults = ({ + isAutocompleteFetching, + autocompleteData, + searchText, + onSubmit, + onResultPress, +}: { + isAutocompleteFetching: boolean + autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined + searchText: string + onSubmit: () => void + onResultPress: () => void +}): React.ReactNode => { + const moderationOpts = useModerationOpts() + const {_} = useLingui() + return ( + <> + {(isAutocompleteFetching && !autocompleteData?.length) || + !moderationOpts ? ( + <Loader /> + ) : ( + <ScrollView + style={{height: '100%'}} // @ts-ignore web only -prf - style={{ - height: isWeb ? '100vh' : undefined, - }}> - <View style={styles.searchHistoryContainer}> - {searchHistory.length > 0 && ( - <View style={styles.searchHistoryContent}> - <Text style={[pal.text, styles.searchHistoryTitle]}> - <Trans>Recent Searches</Trans> - </Text> - {searchHistory.map((historyItem, index) => ( - <View - key={index} - style={[ - a.flex_row, - a.mt_md, - a.justify_center, - a.justify_between, - ]}> - <Pressable - accessibilityRole="button" - onPress={() => handleHistoryItemClick(historyItem)} - style={[a.flex_1, a.py_sm]}> - <Text style={pal.text}>{historyItem}</Text> - </Pressable> - <Pressable - accessibilityRole="button" - onPress={() => handleRemoveHistoryItem(historyItem)} - style={[a.px_md, a.py_xs, a.justify_center]}> - <FontAwesomeIcon - icon="xmark" - size={16} - style={pal.textLight as FontAwesomeIconStyle} - /> - </Pressable> - </View> - ))} + dataSet={{stableGutters: '1'}} + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag"> + <SearchLinkCard + label={_(msg`Search for "${searchText}"`)} + onPress={isNative ? onSubmit : undefined} + to={ + isNative + ? undefined + : `/search?q=${encodeURIComponent(searchText)}` + } + style={{borderBottomWidth: 1}} + /> + {autocompleteData?.map(item => ( + <SearchProfileCard + key={item.did} + profile={item} + moderation={moderateProfile(item, moderationOpts)} + onPress={onResultPress} + /> + ))} + <View style={{height: 200}} /> + </ScrollView> + )} + </> + ) +} +AutocompleteResults = React.memo(AutocompleteResults) + +function SearchHistory({ + searchHistory, + onItemClick, + onRemoveItemClick, +}: { + searchHistory: string[] + onItemClick: (item: string) => void + onRemoveItemClick: (item: string) => void +}) { + const {isTabletOrDesktop} = useWebMediaQueries() + const pal = usePalette('default') + return ( + <CenteredView + sideBorders={isTabletOrDesktop} + // @ts-ignore web only -prf + style={{ + height: isWeb ? '100vh' : undefined, + }}> + <View style={styles.searchHistoryContainer}> + {searchHistory.length > 0 && ( + <View style={styles.searchHistoryContent}> + <Text style={[pal.text, styles.searchHistoryTitle]}> + <Trans>Recent Searches</Trans> + </Text> + {searchHistory.map((historyItem, index) => ( + <View + key={index} + style={[ + a.flex_row, + a.mt_md, + a.justify_center, + a.justify_between, + ]}> + <Pressable + accessibilityRole="button" + onPress={() => onItemClick(historyItem)} + hitSlop={HITSLOP_10} + style={[a.flex_1, a.py_sm]}> + <Text style={pal.text}>{historyItem}</Text> + </Pressable> + <Pressable + accessibilityRole="button" + onPress={() => onRemoveItemClick(historyItem)} + hitSlop={HITSLOP_10} + style={[a.px_md, a.py_xs, a.justify_center]}> + <FontAwesomeIcon + icon="xmark" + size={16} + style={pal.textLight as FontAwesomeIconStyle} + /> + </Pressable> </View> - )} + ))} </View> - </CenteredView> - ) : ( - <SearchScreenInner query={queryParam} /> - )} - </View> + )} + </View> + </CenteredView> ) } @@ -874,6 +968,9 @@ const styles = StyleSheet.create({ }, headerCancelBtn: { paddingLeft: 10, + alignSelf: 'center', + zIndex: -1, + elevation: -1, // For Android }, tabBarContainer: { // @ts-ignore web only diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 6b5390c29..c3864e5a9 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -1,7 +1,5 @@ import React from 'react' import { - ActivityIndicator, - Linking, Platform, Pressable, StyleSheet, @@ -41,7 +39,7 @@ import { import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {useAnalytics} from 'lib/analytics/analytics' -import * as AppInfo from 'lib/app-info' +import {appVersion, BUNDLE_DATE, bundleInfo} from 'lib/app-info' import {STATUS_PAGE_URL} from 'lib/constants' import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher' import {useCustomPalette} from 'lib/hooks/useCustomPalette' @@ -61,23 +59,40 @@ import {Text} from 'view/com/util/text/Text' import * as Toast from 'view/com/util/Toast' import {UserAvatar} from 'view/com/util/UserAvatar' import {ScrollView} from 'view/com/util/Views' +import {useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' import {navigate, resetToTab} from '#/Navigation' import {Email2FAToggle} from './Email2FAToggle' import {ExportCarDialog} from './ExportCarDialog' -function SettingsAccountCard({account}: {account: SessionAccount}) { +function SettingsAccountCard({ + account, + pendingDid, + onPressSwitchAccount, +}: { + account: SessionAccount + pendingDid: string | null + onPressSwitchAccount: ( + account: SessionAccount, + logContext: 'Settings', + ) => void +}) { const pal = usePalette('default') const {_} = useLingui() - const {isSwitchingAccounts, currentAccount} = useSession() + const t = useTheme() + const {currentAccount} = useSession() const {logout} = useSessionApi() const {data: profile} = useProfileQuery({did: account.did}) const isCurrentAccount = account.did === currentAccount?.did - const {onPressSwitchAccount} = useAccountSwitcher() const contents = ( - <View style={[pal.view, styles.linkCard]}> + <View + style={[ + pal.view, + styles.linkCard, + account.did === pendingDid && t.atoms.bg_contrast_25, + ]}> <View style={styles.avi}> <UserAvatar size={40} @@ -109,7 +124,8 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { }} accessibilityRole="button" accessibilityLabel={_(msg`Sign out`)} - accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}> + accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`} + activeOpacity={0.8}> <Text type="lg" style={pal.link}> <Trans>Sign out</Trans> </Text> @@ -135,13 +151,12 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { testID={`switchToAccountBtn-${account.handle}`} key={account.did} onPress={ - isSwitchingAccounts - ? undefined - : () => onPressSwitchAccount(account, 'Settings') + pendingDid ? undefined : () => onPressSwitchAccount(account, 'Settings') } accessibilityRole="button" accessibilityLabel={_(msg`Switch to ${account.handle}`)} - accessibilityHint={_(msg`Switches the account you are logged in to`)}> + accessibilityHint={_(msg`Switches the account you are logged in to`)} + activeOpacity={0.8}> {contents} </TouchableOpacity> ) @@ -162,12 +177,14 @@ export function SettingsScreen({}: Props) { const {isMobile} = useWebMediaQueries() const {screen, track} = useAnalytics() const {openModal} = useModalControls() - const {isSwitchingAccounts, accounts, currentAccount} = useSession() + const {accounts, currentAccount} = useSession() const {mutate: clearPreferences} = useClearPreferencesMutation() const {setShowLoggedOut} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() const exportCarControl = useDialogControl() const birthdayControl = useDialogControl() + const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() + const isSwitchingAccounts = !!pendingDid // const primaryBg = useCustomPalette<ViewStyle>({ // light: {backgroundColor: colors.blue0}, @@ -238,7 +255,7 @@ export function SettingsScreen({}: Props) { const onPressBuildInfo = React.useCallback(() => { setStringAsync( - `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, + `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}`, ) Toast.show(_(msg`Copied build version to clipboard`)) }, [_]) @@ -275,10 +292,6 @@ export function SettingsScreen({}: Props) { navigation.navigate('AccessibilitySettings') }, [navigation]) - const onPressStatusPage = React.useCallback(() => { - Linking.openURL(STATUS_PAGE_URL) - }, []) - const onPressBirthday = React.useCallback(() => { birthdayControl.open() }, [birthdayControl]) @@ -363,50 +376,53 @@ export function SettingsScreen({}: Props) { <View style={styles.spacer20} /> {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} + + <View style={[s.flexRow, styles.heading]}> + <Text type="xl-bold" style={pal.text}> + <Trans>Signed in as</Trans> + </Text> + <View style={s.flex1} /> + </View> + <View pointerEvents={pendingDid ? 'none' : 'auto'}> + <SettingsAccountCard + account={currentAccount} + onPressSwitchAccount={onPressSwitchAccount} + pendingDid={pendingDid} + /> + </View> </> ) : 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!} /> - )} + <View pointerEvents={pendingDid ? 'none' : 'auto'}> + {accounts + .filter(a => a.did !== currentAccount?.did) + .map(account => ( + <SettingsAccountCard + key={account.did} + account={account} + onPressSwitchAccount={onPressSwitchAccount} + pendingDid={pendingDid} + /> + ))} - {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={_(msg`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]} + onPress={isSwitchingAccounts ? undefined : onPressAddAccount} + accessibilityRole="button" + accessibilityLabel={_(msg`Add account`)} + accessibilityHint={_(msg`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> <View style={styles.spacer20} /> @@ -849,17 +865,9 @@ export function SettingsScreen({}: Props) { accessibilityRole="button" onPress={onPressBuildInfo}> <Text type="sm" style={[styles.buildInfo, pal.textLight]}> - <Trans>Version {AppInfo.appVersion}</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> + <Trans> + Version {appVersion} {bundleInfo} + </Trans> </Text> </TouchableOpacity> </View> @@ -881,6 +889,12 @@ export function SettingsScreen({}: Props) { href="https://bsky.social/about/support/privacy-policy" text={_(msg`Privacy Policy`)} /> + <TextLink + type="md" + style={pal.link} + href={STATUS_PAGE_URL} + text={_(msg`Status Page`)} + /> </View> <View style={s.footerSpacer} /> </ScrollView> @@ -1026,7 +1040,6 @@ const styles = StyleSheet.create({ footer: { flex: 1, flexDirection: 'row', - alignItems: 'center', paddingLeft: 18, }, }) diff --git a/src/view/screens/Storybook/ListContained.tsx b/src/view/screens/Storybook/ListContained.tsx new file mode 100644 index 000000000..b3ea091f4 --- /dev/null +++ b/src/view/screens/Storybook/ListContained.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import {FlatList, View} from 'react-native' + +import {ScrollProvider} from 'lib/ScrollContext' +import {List} from 'view/com/util/List' +import {Button, ButtonText} from '#/components/Button' +import * as Toggle from '#/components/forms/Toggle' +import {Text} from '#/components/Typography' + +export function ListContained() { + const [animated, setAnimated] = React.useState(false) + const ref = React.useRef<FlatList>(null) + + const data = React.useMemo(() => { + return Array.from({length: 100}, (_, i) => ({ + id: i, + text: `Message ${i}`, + })) + }, []) + + return ( + <> + <View style={{width: '100%', height: 300}}> + <ScrollProvider + onScroll={e => { + 'worklet' + console.log( + JSON.stringify({ + contentOffset: e.contentOffset, + layoutMeasurement: e.layoutMeasurement, + contentSize: e.contentSize, + }), + ) + }}> + <List + data={data} + renderItem={item => { + return ( + <View + style={{ + padding: 10, + borderBottomWidth: 1, + borderBottomColor: 'rgba(0,0,0,0.1)', + }}> + <Text>{item.item.text}</Text> + </View> + ) + }} + keyExtractor={item => item.id.toString()} + containWeb={true} + style={{flex: 1}} + onStartReached={() => { + console.log('Start Reached') + }} + onEndReached={() => { + console.log('End Reached (threshold of 2)') + }} + onEndReachedThreshold={2} + ref={ref} + disableVirtualization={true} + /> + </ScrollProvider> + </View> + + <View style={{flexDirection: 'row', gap: 10, alignItems: 'center'}}> + <Toggle.Item + name="a" + label="Click me" + value={animated} + onChange={() => setAnimated(prev => !prev)}> + <Toggle.Checkbox /> + <Toggle.LabelText>Animated Scrolling</Toggle.LabelText> + </Toggle.Item> + </View> + + <Button + variant="solid" + color="primary" + size="large" + label="Scroll to End" + onPress={() => ref.current?.scrollToOffset({animated, offset: 0})}> + <ButtonText>Scroll to Top</ButtonText> + </Button> + + <Button + variant="solid" + color="primary" + size="large" + label="Scroll to End" + onPress={() => ref.current?.scrollToEnd({animated})}> + <ButtonText>Scroll to End</ButtonText> + </Button> + + <Button + variant="solid" + color="primary" + size="large" + label="Scroll to Offset 100" + onPress={() => ref.current?.scrollToOffset({animated, offset: 500})}> + <ButtonText>Scroll to Offset 500</ButtonText> + </Button> + </> + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index 35a666601..282b3ff5c 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -1,8 +1,10 @@ import React from 'react' -import {View} from 'react-native' +import {ScrollView, View} from 'react-native' import {useSetThemePrefs} from '#/state/shell' -import {CenteredView, ScrollView} from '#/view/com/util/Views' +import {isWeb} from 'platform/detection' +import {CenteredView} from '#/view/com/util/Views' +import {ListContained} from 'view/screens/Storybook/ListContained' import {atoms as a, ThemeProvider, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {Breakpoints} from './Breakpoints' @@ -18,77 +20,111 @@ import {Theming} from './Theming' import {Typography} from './Typography' export function Storybook() { + if (isWeb) return <StorybookInner /> + + return ( + <ScrollView> + <StorybookInner /> + </ScrollView> + ) +} + +function StorybookInner() { const t = useTheme() const {setColorMode, setDarkTheme} = useSetThemePrefs() + const [showContainedList, setShowContainedList] = React.useState(false) return ( - <ScrollView> - <CenteredView style={[t.atoms.bg]}> - <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}> - <View style={[a.flex_row, a.align_start, a.gap_md]}> - <Button - variant="outline" - color="primary" - size="small" - label='Set theme to "system"' - onPress={() => setColorMode('system')}> - <ButtonText>System</ButtonText> - </Button> - <Button - variant="solid" - color="secondary" - size="small" - label='Set theme to "light"' - onPress={() => setColorMode('light')}> - <ButtonText>Light</ButtonText> - </Button> + <CenteredView style={[t.atoms.bg]}> + <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}> + {!showContainedList ? ( + <> + <View style={[a.flex_row, a.align_start, a.gap_md]}> + <Button + variant="outline" + color="primary" + size="small" + label='Set theme to "system"' + onPress={() => setColorMode('system')}> + <ButtonText>System</ButtonText> + </Button> + <Button + variant="solid" + color="secondary" + size="small" + label='Set theme to "light"' + onPress={() => setColorMode('light')}> + <ButtonText>Light</ButtonText> + </Button> + <Button + variant="solid" + color="secondary" + size="small" + label='Set theme to "dim"' + onPress={() => { + setColorMode('dark') + setDarkTheme('dim') + }}> + <ButtonText>Dim</ButtonText> + </Button> + <Button + variant="solid" + color="secondary" + size="small" + label='Set theme to "dark"' + onPress={() => { + setColorMode('dark') + setDarkTheme('dark') + }}> + <ButtonText>Dark</ButtonText> + </Button> + </View> + + <Dialogs /> + <ThemeProvider theme="light"> + <Theming /> + </ThemeProvider> + <ThemeProvider theme="dim"> + <Theming /> + </ThemeProvider> + <ThemeProvider theme="dark"> + <Theming /> + </ThemeProvider> + + <Typography /> + <Spacing /> + <Shadows /> + <Buttons /> + <Icons /> + <Links /> + <Forms /> + <Dialogs /> + <Menus /> + <Breakpoints /> + <Button variant="solid" - color="secondary" - size="small" - label='Set theme to "dim"' - onPress={() => { - setColorMode('dark') - setDarkTheme('dim') - }}> - <ButtonText>Dim</ButtonText> + color="primary" + size="large" + label="Switch to Contained List" + onPress={() => setShowContainedList(true)}> + <ButtonText>Switch to Contained List</ButtonText> </Button> + </> + ) : ( + <> <Button variant="solid" - color="secondary" - size="small" - label='Set theme to "dark"' - onPress={() => { - setColorMode('dark') - setDarkTheme('dark') - }}> - <ButtonText>Dark</ButtonText> + color="primary" + size="large" + label="Switch to Storybook" + onPress={() => setShowContainedList(false)}> + <ButtonText>Switch to Storybook</ButtonText> </Button> - </View> - - <Dialogs /> - <ThemeProvider theme="light"> - <Theming /> - </ThemeProvider> - <ThemeProvider theme="dim"> - <Theming /> - </ThemeProvider> - <ThemeProvider theme="dark"> - <Theming /> - </ThemeProvider> - - <Typography /> - <Spacing /> - <Shadows /> - <Buttons /> - <Icons /> - <Links /> - <Forms /> - <Dialogs /> - <Menus /> - <Breakpoints /> - </View> - </CenteredView> - </ScrollView> + <ListContained /> + </> + )} + </View> + </CenteredView> ) } |