diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-04-03 03:21:15 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-02 17:21:15 -0700 |
commit | 87da619aaa92e0ec762e68c13b24e58a25da10a8 (patch) | |
tree | 4da902d3ca43a226f6da8e5c090ab33c2df3297a /src/view/screens | |
parent | 8d1f97b5ffac5d86762f1d4e9384ff3097acbc52 (diff) | |
download | voidsky-87da619aaa92e0ec762e68c13b24e58a25da10a8.tar.zst |
[Explore] Base (#8053)
* migrate to #/screens * rm unneeded import * block drawer gesture on recent profiles * rm recommendations (#8056) * [Explore] Disable Trending videos (#8054) * remove giant header * disable * [Explore] Dynamic module ordering (#8066) * Dynamic module ordering * [Explore] New headers, metrics (#8067) * new sticky headers * improve spacing between modules * view metric on modules * update metrics names * [Explore] Suggested accounts module (#8072) * use modern profile card, update load more * add tab bar * tabbed suggested accounts * [Explore] Discover feeds module (#8073) * cap number of feeds to 3 * change feed pin button * Apply suggestions from code review Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * restore statsig to log events * filter out followed profiles, make suer enough are loaded (#8090) * [Explore] Trending topics (#8055) * redesigned trending topics * rm borders on web * get post count / age / ranking from api * spacing tweaks * fetch more topics then slice * use api data for avis/category * rm top border * Integrate new SDK, part out components * Clean up * Use status field * Bump SDK * Send up interests and langs --------- Co-authored-by: Eric Bailey <git@esb.lol> * Clean up module spacing and borders (cherry picked from commit 63d19b6c2d67e226e0e14709b1047a1f88b3ce1c) (cherry picked from commit 62d7d394ab1dc31b40b9c2cf59075adbf94737a1) * Switch back border ordering (cherry picked from commit 34e3789f8b410132c1390df3c2bb8257630ebdd9) * [Explore] Starter Packs (#8095) * Temp WIP (cherry picked from commit 43b5d7b1e64b3adb1ed162262d0310e0bf026c18) * New SP card * Load state * Revert change * Cleanup * Interests and caching * Count total * Format * Caching * [Explore] Feed previews module (#8075) * wip new hook * get fetching working, maybe * get feed previews rendering! * fix header height * working pin button * extract out FeedLink * add loader * only make preview:header sticky * Fix headers * Header tweaks * Fix moderation filter * Fix threading --------- Co-authored-by: Eric Bailey <git@esb.lol> * Space it out * Fix query key * Mock new endpoint, filter saved feeds * Make sure we're pinning, lower cache time * add news category * Remove log * Improve suggested accounts load state * Integrate new app view endpoint * fragment * Update src/screens/Search/modules/ExploreTrendingTopics.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/screens/Search/modules/ExploreTrendingTopics.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * lint * maybe fix this --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view/screens')
-rw-r--r-- | src/view/screens/Search/Explore.tsx | 641 | ||||
-rw-r--r-- | src/view/screens/Search/Search.tsx | 1165 | ||||
-rw-r--r-- | src/view/screens/Search/index.tsx | 1 |
3 files changed, 0 insertions, 1807 deletions
diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx deleted file mode 100644 index 520e103a4..000000000 --- a/src/view/screens/Search/Explore.tsx +++ /dev/null @@ -1,641 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import { - AppBskyActorDefs, - AppBskyFeedDefs, - moderateProfile, - ModerationDecision, - ModerationOpts, -} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {logEvent} from '#/lib/statsig/statsig' -import {cleanError} from '#/lib/strings/errors' -import {logger} from '#/logger' -import {isNative, isWeb} from '#/platform/detection' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useGetPopularFeedsQuery} from '#/state/queries/feed' -import {usePreferencesQuery} from '#/state/queries/preferences' -import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' -import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' -import {List} from '#/view/com/util/List' -import { - FeedFeedLoadingPlaceholder, - ProfileCardFeedLoadingPlaceholder, -} from '#/view/com/util/LoadingPlaceholder' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations' -import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics' -import {ExploreTrendingVideos} from '#/screens/Search/components/ExploreTrendingVideos' -import {atoms as a, useTheme, ViewStyleProp} from '#/alf' -import {Button} from '#/components/Button' -import * as FeedCard from '#/components/FeedCard' -import {ArrowBottom_Stroke2_Corner0_Rounded as ArrowBottom} from '#/components/icons/Arrow' -import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {Props as SVGIconProps} from '#/components/icons/common' -import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle' -import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' - -function SuggestedItemsHeader({ - title, - description, - style, - icon: Icon, -}: { - title: string - description: string - icon: React.ComponentType<SVGIconProps> -} & ViewStyleProp) { - const t = useTheme() - - return ( - <View - style={[ - isWeb - ? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md] - : [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md], - a.border_b, - t.atoms.border_contrast_low, - style, - ]}> - <View style={[a.flex_1, a.gap_sm]}> - <View style={[a.flex_row, a.align_center, a.gap_sm]}> - <Icon - size="lg" - fill={t.palette.primary_500} - style={{marginLeft: -2}} - /> - <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>{title}</Text> - </View> - <Text style={[t.atoms.text_contrast_high, a.leading_snug]}> - {description} - </Text> - </View> - </View> - ) -} - -type LoadMoreItem = - | { - type: 'profile' - key: string - avatar: string | undefined - moderation: ModerationDecision - } - | { - type: 'feed' - key: string - avatar: string | undefined - moderation: undefined - } - -function LoadMore({ - item, - moderationOpts, -}: { - item: ExploreScreenItems & {type: 'loadMore'} - moderationOpts?: ModerationOpts -}) { - const t = useTheme() - const {_} = useLingui() - const items: LoadMoreItem[] = React.useMemo(() => { - return item.items - .map(_item => { - let loadMoreItem: LoadMoreItem | undefined - if (_item.type === 'profile') { - loadMoreItem = { - type: 'profile', - key: _item.profile.did, - avatar: _item.profile.avatar, - moderation: moderateProfile(_item.profile, moderationOpts!), - } - } else if (_item.type === 'feed') { - loadMoreItem = { - type: 'feed', - key: _item.feed.uri, - avatar: _item.feed.avatar, - moderation: undefined, - } - } - return loadMoreItem - }) - .filter(n => !!n) - }, [item.items, moderationOpts]) - - if (items.length === 0) return null - - const type = items[0].type - - return ( - <View style={[]}> - <Button - label={_(msg`Load more`)} - onPress={item.onLoadMore} - style={[a.relative, a.w_full]}> - {({hovered, pressed}) => ( - <View - style={[ - a.flex_1, - a.flex_row, - a.align_center, - a.px_lg, - a.py_md, - (hovered || pressed) && t.atoms.bg_contrast_25, - ]}> - <View - style={[ - a.relative, - { - height: 32, - width: 32 + 15 * items.length, - }, - ]}> - <View - style={[ - a.align_center, - a.justify_center, - t.atoms.bg_contrast_25, - a.absolute, - { - width: 30, - height: 30, - left: 0, - borderWidth: 1, - backgroundColor: t.palette.primary_500, - borderColor: t.atoms.bg.backgroundColor, - borderRadius: type === 'profile' ? 999 : 4, - zIndex: 4, - }, - ]}> - <ArrowBottom fill={t.palette.white} /> - </View> - {items.map((_item, i) => { - return ( - <View - key={_item.key} - style={[ - t.atoms.bg_contrast_25, - a.absolute, - { - width: 30, - height: 30, - left: (i + 1) * 15, - borderWidth: 1, - borderColor: t.atoms.bg.backgroundColor, - borderRadius: _item.type === 'profile' ? 999 : 4, - zIndex: 3 - i, - }, - ]}> - {moderationOpts && ( - <> - {_item.type === 'profile' ? ( - <UserAvatar - size={28} - avatar={_item.avatar} - moderation={_item.moderation.ui('avatar')} - type="user" - /> - ) : _item.type === 'feed' ? ( - <UserAvatar - size={28} - avatar={_item.avatar} - type="algo" - /> - ) : null} - </> - )} - </View> - ) - })} - </View> - - <Text - style={[ - a.pl_sm, - a.leading_snug, - hovered ? t.atoms.text : t.atoms.text_contrast_medium, - ]}> - {type === 'profile' ? ( - <Trans>Load more suggested follows</Trans> - ) : ( - <Trans>Load more suggested feeds</Trans> - )} - </Text> - - <View style={[a.flex_1, a.align_end]}> - {item.isLoadingMore && <Loader size="lg" />} - </View> - </View> - )} - </Button> - </View> - ) -} - -type ExploreScreenItems = - | { - type: 'header' - key: string - title: string - description: string - style?: ViewStyleProp['style'] - icon: React.ComponentType<SVGIconProps> - } - | { - type: 'trendingTopics' - key: string - } - | { - type: 'trendingVideos' - key: string - } - | { - type: 'recommendations' - key: string - } - | { - type: 'profile' - key: string - profile: AppBskyActorDefs.ProfileView - recId?: number - } - | { - type: 'feed' - key: string - feed: AppBskyFeedDefs.GeneratorView - } - | { - type: 'loadMore' - key: string - isLoadingMore: boolean - onLoadMore: () => void - items: ExploreScreenItems[] - } - | { - type: 'profilePlaceholder' - key: string - } - | { - type: 'feedPlaceholder' - key: string - } - | { - type: 'error' - key: string - message: string - error: string - } - -export function Explore() { - const {_} = useLingui() - const t = useTheme() - const {data: preferences, error: preferencesError} = usePreferencesQuery() - const moderationOpts = useModerationOpts() - const { - data: profiles, - hasNextPage: hasNextProfilesPage, - isLoading: isLoadingProfiles, - isFetchingNextPage: isFetchingNextProfilesPage, - error: profilesError, - fetchNextPage: fetchNextProfilesPage, - } = useSuggestedFollowsQuery({limit: 6, subsequentPageLimit: 10}) - const { - data: feeds, - hasNextPage: hasNextFeedsPage, - isLoading: isLoadingFeeds, - isFetchingNextPage: isFetchingNextFeedsPage, - error: feedsError, - fetchNextPage: fetchNextFeedsPage, - } = useGetPopularFeedsQuery({limit: 10}) - - const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles - const onLoadMoreProfiles = React.useCallback(async () => { - if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError) - return - try { - await fetchNextProfilesPage() - } catch (err) { - logger.error('Failed to load more suggested follows', {message: err}) - } - }, [ - isFetchingNextProfilesPage, - hasNextProfilesPage, - profilesError, - fetchNextProfilesPage, - ]) - - const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds - const onLoadMoreFeeds = React.useCallback(async () => { - if (isFetchingNextFeedsPage || !hasNextFeedsPage || feedsError) return - try { - await fetchNextFeedsPage() - } catch (err) { - logger.error('Failed to load more suggested follows', {message: err}) - } - }, [ - isFetchingNextFeedsPage, - hasNextFeedsPage, - feedsError, - fetchNextFeedsPage, - ]) - - const items = React.useMemo<ExploreScreenItems[]>(() => { - const i: ExploreScreenItems[] = [] - - i.push({ - type: 'trendingTopics', - key: `trending-topics`, - }) - - if (isNative) { - i.push({ - type: 'trendingVideos', - key: `trending-videos`, - }) - } - - i.push({ - type: 'recommendations', - key: `recommendations`, - }) - - i.push({ - type: 'header', - key: 'suggested-follows-header', - title: _(msg`Suggested accounts`), - description: _( - msg`Follow more accounts to get connected to your interests and build your network.`, - ), - icon: Person, - }) - - if (profiles) { - // Currently the responses contain duplicate items. - // Needs to be fixed on backend, but let's dedupe to be safe. - let seen = new Set() - const profileItems: ExploreScreenItems[] = [] - for (const page of profiles.pages) { - for (const actor of page.actors) { - if (!seen.has(actor.did)) { - seen.add(actor.did) - profileItems.push({ - type: 'profile', - key: actor.did, - profile: actor, - recId: page.recId, - }) - } - } - } - - if (hasNextProfilesPage) { - // splice off 3 as previews if we have a next page - const previews = profileItems.splice(-3) - // push remainder - i.push(...profileItems) - i.push({ - type: 'loadMore', - key: 'loadMoreProfiles', - isLoadingMore: isLoadingMoreProfiles, - onLoadMore: onLoadMoreProfiles, - items: previews, - }) - } else { - i.push(...profileItems) - } - } else { - if (profilesError) { - i.push({ - type: 'error', - key: 'profilesError', - message: _(msg`Failed to load suggested follows`), - error: cleanError(profilesError), - }) - } else { - i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) - } - } - - i.push({ - type: 'header', - key: 'suggested-feeds-header', - title: _(msg`Discover new feeds`), - description: _( - msg`Choose your own timeline! Feeds built by the community help you find content you love.`, - ), - style: [a.pt_5xl], - icon: ListSparkle, - }) - - if (feeds && preferences) { - // Currently the responses contain duplicate items. - // Needs to be fixed on backend, but let's dedupe to be safe. - let seen = new Set() - const feedItems: ExploreScreenItems[] = [] - for (const page of feeds.pages) { - for (const feed of page.feeds) { - if (!seen.has(feed.uri)) { - seen.add(feed.uri) - feedItems.push({ - type: 'feed', - key: feed.uri, - feed, - }) - } - } - } - - // feeds errors can occur during pagination, so feeds is truthy - if (feedsError) { - i.push({ - type: 'error', - key: 'feedsError', - message: _(msg`Failed to load suggested feeds`), - error: cleanError(feedsError), - }) - } else if (preferencesError) { - i.push({ - type: 'error', - key: 'preferencesError', - message: _(msg`Failed to load feeds preferences`), - error: cleanError(preferencesError), - }) - } else if (hasNextFeedsPage) { - const preview = feedItems.splice(-3) - i.push(...feedItems) - i.push({ - type: 'loadMore', - key: 'loadMoreFeeds', - isLoadingMore: isLoadingMoreFeeds, - onLoadMore: onLoadMoreFeeds, - items: preview, - }) - } else { - i.push(...feedItems) - } - } else { - if (feedsError) { - i.push({ - type: 'error', - key: 'feedsError', - message: _(msg`Failed to load suggested feeds`), - error: cleanError(feedsError), - }) - } else if (preferencesError) { - i.push({ - type: 'error', - key: 'preferencesError', - message: _(msg`Failed to load feeds preferences`), - error: cleanError(preferencesError), - }) - } else { - i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'}) - } - } - - return i - }, [ - _, - profiles, - feeds, - preferences, - onLoadMoreFeeds, - onLoadMoreProfiles, - isLoadingMoreProfiles, - isLoadingMoreFeeds, - profilesError, - feedsError, - preferencesError, - hasNextProfilesPage, - hasNextFeedsPage, - ]) - - const renderItem = React.useCallback( - ({item, index}: {item: ExploreScreenItems; index: number}) => { - switch (item.type) { - case 'header': { - return ( - <SuggestedItemsHeader - title={item.title} - description={item.description} - style={item.style} - icon={item.icon} - /> - ) - } - case 'trendingTopics': { - return <ExploreTrendingTopics /> - } - case 'trendingVideos': { - return <ExploreTrendingVideos /> - } - case 'recommendations': { - return <ExploreRecommendations /> - } - case 'profile': { - return ( - <View style={[a.border_b, t.atoms.border_contrast_low]}> - <ProfileCardWithFollowBtn - profile={item.profile} - noBg - noBorder - showKnownFollowers - onPress={() => { - logEvent('suggestedUser:press', { - logContext: 'Explore', - recId: item.recId, - position: index, - }) - }} - onFollow={() => { - logEvent('suggestedUser:follow', { - logContext: 'Explore', - location: 'Card', - recId: item.recId, - position: index, - }) - }} - /> - </View> - ) - } - case 'feed': { - return ( - <View - style={[ - a.border_b, - t.atoms.border_contrast_low, - a.px_lg, - a.py_lg, - ]}> - <FeedCard.Default view={item.feed} /> - </View> - ) - } - case 'loadMore': { - return <LoadMore item={item} moderationOpts={moderationOpts} /> - } - case 'profilePlaceholder': { - return <ProfileCardFeedLoadingPlaceholder /> - } - case 'feedPlaceholder': { - return <FeedFeedLoadingPlaceholder /> - } - case 'error': { - return ( - <View - style={[ - a.border_t, - a.pt_md, - a.px_md, - t.atoms.border_contrast_low, - ]}> - <View - style={[ - a.flex_row, - a.gap_md, - a.p_lg, - a.rounded_sm, - t.atoms.bg_contrast_25, - ]}> - <CircleInfo size="md" fill={t.palette.negative_400} /> - <View style={[a.flex_1, a.gap_sm]}> - <Text style={[a.font_bold, a.leading_snug]}> - {item.message} - </Text> - <Text - style={[ - a.italic, - a.leading_snug, - t.atoms.text_contrast_medium, - ]}> - {item.error} - </Text> - </View> - </View> - </View> - ) - } - } - }, - [t, moderationOpts], - ) - - // note: actually not a screen, instead it's nested within - // the search screen. so we don't need Layout.Screen - return ( - <List - data={items} - renderItem={renderItem} - keyExtractor={item => item.key} - // @ts-ignore web only -prf - desktopFixedHeight - contentContainerStyle={{paddingBottom: 100}} - keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag" - /> - ) -} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx deleted file mode 100644 index 785e1872e..000000000 --- a/src/view/screens/Search/Search.tsx +++ /dev/null @@ -1,1165 +0,0 @@ -import React, {useCallback, useLayoutEffect, useMemo} from 'react' -import { - ActivityIndicator, - Pressable, - StyleProp, - StyleSheet, - TextInput, - View, - ViewStyle, -} from 'react-native' -import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler' -import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect, useNavigation, useRoute} from '@react-navigation/native' -import {useQueryClient} from '@tanstack/react-query' - -import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' -import {createHitslop, HITSLOP_20} from '#/lib/constants' -import {HITSLOP_10} from '#/lib/constants' -import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {MagnifyingGlassIcon} from '#/lib/icons' -import {makeProfileLink} from '#/lib/routes/links' -import {NavigationProp} from '#/lib/routes/types' -import { - NativeStackScreenProps, - SearchTabNavigatorParams, -} from '#/lib/routes/types' -import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {augmentSearchQuery} from '#/lib/strings/helpers' -import {languageName} from '#/locale/helpers' -import {isNative, isWeb} from '#/platform/detection' -import {listenSoftReset} from '#/state/events' -import {useLanguagePrefs} from '#/state/preferences/languages' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' -import {useActorSearch} from '#/state/queries/actor-search' -import {usePopularFeedsSearch} from '#/state/queries/feed' -import { - unstableCacheProfileView, - useProfilesQuery, -} from '#/state/queries/profile' -import {useSearchPostsQuery} from '#/state/queries/search-posts' -import {useSession} from '#/state/session' -import {useSetMinimalShellMode} from '#/state/shell' -import {Pager} from '#/view/com/pager/Pager' -import {TabBar} from '#/view/com/pager/TabBar' -import {Post} from '#/view/com/post/Post' -import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' -import {Link} from '#/view/com/util/Link' -import {List} from '#/view/com/util/List' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {Explore} from '#/view/screens/Search/Explore' -import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' -import {makeSearchQuery, Params, parseSearchQuery} from '#/screens/Search/utils' -import { - atoms as a, - native, - platform, - tokens, - useBreakpoints, - useTheme, - web, -} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as FeedCard from '#/components/FeedCard' -import {SearchInput} from '#/components/forms/SearchInput' -import { - ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, - ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon, -} from '#/components/icons/Chevron' -import {Earth_Stroke2_Corner0_Rounded as EarthIcon} from '#/components/icons/Globe' -import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' -import * as Layout from '#/components/Layout' -import * as Menu from '#/components/Menu' -import {Text} from '#/components/Typography' -import {account, useStorage} from '#/storage' -import * as bsky from '#/types/bsky' - -function Loader() { - return ( - <Layout.Content> - <View style={[a.py_xl]}> - <ActivityIndicator /> - </View> - </Layout.Content> - ) -} - -function EmptyState({message, error}: {message: string; error?: string}) { - const t = useTheme() - - return ( - <Layout.Content> - <View style={[a.p_xl]}> - <View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}> - <Text style={[a.text_md]}>{message}</Text> - - {error && ( - <> - <View - style={[ - { - marginVertical: 12, - height: 1, - width: '100%', - backgroundColor: t.atoms.text.color, - opacity: 0.2, - }, - ]} - /> - - <Text style={[t.atoms.text_contrast_medium]}> - <Trans>Error:</Trans> {error} - </Text> - </> - )} - </View> - </View> - </Layout.Content> - ) -} - -type SearchResultSlice = - | { - type: 'post' - key: string - post: AppBskyFeedDefs.PostView - } - | { - type: 'loadingMore' - key: string - } - -let SearchScreenPostResults = ({ - query, - sort, - active, -}: { - query: string - sort?: 'top' | 'latest' - active: boolean -}): React.ReactNode => { - const {_} = useLingui() - const {currentAccount} = useSession() - const [isPTR, setIsPTR] = React.useState(false) - - const augmentedQuery = React.useMemo(() => { - return augmentSearchQuery(query || '', {did: currentAccount?.did}) - }, [query, currentAccount]) - - const { - isFetched, - data: results, - isFetching, - error, - refetch, - fetchNextPage, - isFetchingNextPage, - hasNextPage, - } = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active}) - - const onPullToRefresh = React.useCallback(async () => { - setIsPTR(true) - await refetch() - setIsPTR(false) - }, [setIsPTR, refetch]) - const onEndReached = React.useCallback(() => { - if (isFetching || !hasNextPage || error) return - fetchNextPage() - }, [isFetching, error, hasNextPage, fetchNextPage]) - - const posts = React.useMemo(() => { - return results?.pages.flatMap(page => page.posts) || [] - }, [results]) - const items = React.useMemo(() => { - let temp: SearchResultSlice[] = [] - - const seenUris = new Set() - for (const post of posts) { - if (seenUris.has(post.uri)) { - continue - } - temp.push({ - type: 'post', - key: post.uri, - post, - }) - seenUris.add(post.uri) - } - - if (isFetchingNextPage) { - temp.push({ - type: 'loadingMore', - key: 'loadingMore', - }) - } - - return temp - }, [posts, isFetchingNextPage]) - - return error ? ( - <EmptyState - message={_( - msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`, - )} - error={error.toString()} - /> - ) : ( - <> - {isFetched ? ( - <> - {posts.length ? ( - <List - data={items} - renderItem={({item}) => { - if (item.type === 'post') { - return <Post post={item.post} /> - } else { - return null - } - }} - keyExtractor={item => item.key} - refreshing={isPTR} - onRefresh={onPullToRefresh} - onEndReached={onEndReached} - desktopFixedHeight - contentContainerStyle={{paddingBottom: 100}} - /> - ) : ( - <EmptyState message={_(msg`No results found for ${query}`)} /> - )} - </> - ) : ( - <Loader /> - )} - </> - ) -} -SearchScreenPostResults = React.memo(SearchScreenPostResults) - -let SearchScreenUserResults = ({ - query, - active, -}: { - query: string - active: boolean -}): React.ReactNode => { - const {_} = useLingui() - - const {data: results, isFetched} = useActorSearch({ - query, - enabled: active, - }) - - return isFetched && results ? ( - <> - {results.length ? ( - <List - data={results} - renderItem={({item}) => ( - <ProfileCardWithFollowBtn profile={item} noBg /> - )} - keyExtractor={item => item.did} - desktopFixedHeight - contentContainerStyle={{paddingBottom: 100}} - /> - ) : ( - <EmptyState message={_(msg`No results found for ${query}`)} /> - )} - </> - ) : ( - <Loader /> - ) -} -SearchScreenUserResults = React.memo(SearchScreenUserResults) - -let SearchScreenFeedsResults = ({ - query, - active, -}: { - query: string - active: boolean -}): React.ReactNode => { - const t = useTheme() - const {_} = useLingui() - - const {data: results, isFetched} = usePopularFeedsSearch({ - query, - enabled: active, - }) - - return isFetched && results ? ( - <> - {results.length ? ( - <List - data={results} - renderItem={({item}) => ( - <View - style={[ - a.border_b, - t.atoms.border_contrast_low, - a.px_lg, - a.py_lg, - ]}> - <FeedCard.Default view={item} /> - </View> - )} - keyExtractor={item => item.uri} - desktopFixedHeight - contentContainerStyle={{paddingBottom: 100}} - /> - ) : ( - <EmptyState message={_(msg`No results found for ${query}`)} /> - )} - </> - ) : ( - <Loader /> - ) -} -SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults) - -function SearchLanguageDropdown({ - value, - onChange, -}: { - value: string - onChange(value: string): void -}) { - const {_} = useLingui() - const {appLanguage, contentLanguages} = useLanguagePrefs() - - const languages = useMemo(() => { - return LANGUAGES.filter( - (lang, index, self) => - Boolean(lang.code2) && // reduce to the code2 varieties - index === self.findIndex(t => t.code2 === lang.code2), // remove dupes (which will happen) - ) - .map(l => ({ - label: languageName(l, appLanguage), - value: l.code2, - key: l.code2 + l.code3, - })) - .sort((a, b) => { - // prioritize user's languages - const aIsUser = contentLanguages.includes(a.value) - const bIsUser = contentLanguages.includes(b.value) - if (aIsUser && !bIsUser) return -1 - if (bIsUser && !aIsUser) return 1 - // prioritize "common" langs in the network - const aIsCommon = !!APP_LANGUAGES.find( - al => - // skip `ast`, because it uses a 3-letter code which conflicts with `as` - // it begins with `a` anyway so still is top of the list - al.code2 !== 'ast' && al.code2.startsWith(a.value), - ) - const bIsCommon = !!APP_LANGUAGES.find( - al => - // ditto - al.code2 !== 'ast' && al.code2.startsWith(b.value), - ) - if (aIsCommon && !bIsCommon) return -1 - if (bIsCommon && !aIsCommon) return 1 - // fall back to alphabetical - return a.label.localeCompare(b.label) - }) - }, [appLanguage, contentLanguages]) - - const currentLanguageLabel = - languages.find(lang => lang.value === value)?.label ?? _(msg`All languages`) - - return ( - <Menu.Root> - <Menu.Trigger - label={_( - msg`Filter search by language (currently: ${currentLanguageLabel})`, - )}> - {({props}) => ( - <Button - {...props} - label={props.accessibilityLabel} - size="small" - color={platform({native: 'primary', default: 'secondary'})} - variant={platform({native: 'ghost', default: 'solid'})} - style={native([ - a.py_sm, - a.px_sm, - {marginRight: tokens.space.sm * -1}, - ])}> - <ButtonIcon icon={EarthIcon} /> - <ButtonText>{currentLanguageLabel}</ButtonText> - <ButtonIcon - icon={platform({ - native: ChevronUpDownIcon, - default: ChevronDownIcon, - })} - /> - </Button> - )} - </Menu.Trigger> - <Menu.Outer> - <Menu.LabelText> - <Trans>Filter search by language</Trans> - </Menu.LabelText> - <Menu.Item label={_(msg`All languages`)} onPress={() => onChange('')}> - <Menu.ItemText> - <Trans>All languages</Trans> - </Menu.ItemText> - <Menu.ItemRadio selected={value === ''} /> - </Menu.Item> - <Menu.Divider /> - <Menu.Group> - {languages.map(lang => ( - <Menu.Item - key={lang.key} - label={lang.label} - onPress={() => onChange(lang.value)}> - <Menu.ItemText>{lang.label}</Menu.ItemText> - <Menu.ItemRadio selected={value === lang.value} /> - </Menu.Item> - ))} - </Menu.Group> - </Menu.Outer> - </Menu.Root> - ) -} - -function useQueryManager({ - initialQuery, - fixedParams, -}: { - initialQuery: string - fixedParams?: Params -}) { - const {query, params: initialParams} = React.useMemo(() => { - return parseSearchQuery(initialQuery || '') - }, [initialQuery]) - const [prevInitialQuery, setPrevInitialQuery] = React.useState(initialQuery) - const [lang, setLang] = React.useState(initialParams.lang || '') - - if (initialQuery !== prevInitialQuery) { - // handle new queryParam change (from manual search entry) - setPrevInitialQuery(initialQuery) - setLang(initialParams.lang || '') - } - - const params = React.useMemo( - () => ({ - // default stuff - ...initialParams, - // managed stuff - lang, - ...fixedParams, - }), - [lang, initialParams, fixedParams], - ) - const handlers = React.useMemo( - () => ({ - setLang, - }), - [setLang], - ) - - return React.useMemo(() => { - return { - query, - queryWithParams: makeSearchQuery(query, params), - params: { - ...params, - ...handlers, - }, - } - }, [query, params, handlers]) -} - -let SearchScreenInner = ({ - query, - queryWithParams, - headerHeight, -}: { - query: string - queryWithParams: string - headerHeight: number -}): React.ReactNode => { - const t = useTheme() - const setMinimalShellMode = useSetMinimalShellMode() - const {hasSession} = useSession() - const {gtTablet} = useBreakpoints() - const [activeTab, setActiveTab] = React.useState(0) - const {_} = useLingui() - - const onPageSelected = React.useCallback( - (index: number) => { - setMinimalShellMode(false) - setActiveTab(index) - }, - [setMinimalShellMode], - ) - - const sections = React.useMemo(() => { - if (!queryWithParams) return [] - const noParams = queryWithParams === query - return [ - { - title: _(msg`Top`), - component: ( - <SearchScreenPostResults - query={queryWithParams} - sort="top" - active={activeTab === 0} - /> - ), - }, - { - title: _(msg`Latest`), - component: ( - <SearchScreenPostResults - query={queryWithParams} - sort="latest" - active={activeTab === 1} - /> - ), - }, - noParams && { - title: _(msg`People`), - component: ( - <SearchScreenUserResults query={query} active={activeTab === 2} /> - ), - }, - noParams && { - title: _(msg`Feeds`), - component: ( - <SearchScreenFeedsResults query={query} active={activeTab === 3} /> - ), - }, - ].filter(Boolean) as { - title: string - component: React.ReactNode - }[] - }, [_, query, queryWithParams, activeTab]) - - return queryWithParams ? ( - <Pager - onPageSelected={onPageSelected} - renderTabBar={props => ( - <Layout.Center style={[a.z_10, web([a.sticky, {top: headerHeight}])]}> - <TabBar items={sections.map(section => section.title)} {...props} /> - </Layout.Center> - )} - initialPage={0}> - {sections.map((section, i) => ( - <View key={i}>{section.component}</View> - ))} - </Pager> - ) : hasSession ? ( - <Explore /> - ) : ( - <Layout.Center> - <View style={a.flex_1}> - {gtTablet && ( - <View - style={[ - a.border_b, - t.atoms.border_contrast_low, - a.px_lg, - a.pt_sm, - a.pb_lg, - ]}> - <Text style={[a.text_2xl, a.font_heavy]}> - <Trans>Search</Trans> - </Text> - </View> - )} - - <View style={[a.align_center, a.justify_center, a.py_4xl, a.gap_lg]}> - <MagnifyingGlassIcon - strokeWidth={3} - size={60} - style={t.atoms.text_contrast_medium as StyleProp<ViewStyle>} - /> - <Text style={[t.atoms.text_contrast_medium, a.text_md]}> - <Trans>Find posts, users, and feeds on Bluesky</Trans> - </Text> - </View> - </View> - </Layout.Center> - ) -} -SearchScreenInner = React.memo(SearchScreenInner) - -export function SearchScreen( - props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, -) { - const queryParam = props.route?.params?.q ?? '' - - return <SearchScreenShell queryParam={queryParam} testID="searchScreen" /> -} - -export function SearchScreenShell({ - queryParam, - testID, - fixedParams, - navButton = 'menu', - inputPlaceholder, -}: { - queryParam: string - testID: string - fixedParams?: Params - navButton?: 'back' | 'menu' - inputPlaceholder?: string -}) { - const t = useTheme() - const {gtMobile} = useBreakpoints() - const navigation = useNavigation<NavigationProp>() - const route = useRoute() - const textInput = React.useRef<TextInput>(null) - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const {currentAccount} = useSession() - const queryClient = useQueryClient() - - // Query terms - const [searchText, setSearchText] = React.useState<string>(queryParam) - const {data: autocompleteData, isFetching: isAutocompleteFetching} = - useActorAutocompleteQuery(searchText, true) - - const [showAutocomplete, setShowAutocomplete] = React.useState(false) - - const [termHistory = [], setTermHistory] = useStorage(account, [ - currentAccount?.did ?? 'pwi', - 'searchTermHistory', - ] as const) - const [accountHistory = [], setAccountHistory] = useStorage(account, [ - currentAccount?.did ?? 'pwi', - 'searchAccountHistory', - ]) - - const {data: accountHistoryProfiles} = useProfilesQuery({ - handles: accountHistory, - maintainData: true, - }) - - const updateSearchHistory = useCallback( - async (item: string) => { - if (!item) return - const newSearchHistory = [ - item, - ...termHistory.filter(search => search !== item), - ].slice(0, 6) - setTermHistory(newSearchHistory) - }, - [termHistory, setTermHistory], - ) - - const updateProfileHistory = useCallback( - async (item: bsky.profile.AnyProfileView) => { - const newAccountHistory = [ - item.did, - ...accountHistory.filter(p => p !== item.did), - ].slice(0, 5) - setAccountHistory(newAccountHistory) - }, - [accountHistory, setAccountHistory], - ) - - const deleteSearchHistoryItem = useCallback( - async (item: string) => { - setTermHistory(termHistory.filter(search => search !== item)) - }, - [termHistory, setTermHistory], - ) - const deleteProfileHistoryItem = useCallback( - async (item: AppBskyActorDefs.ProfileViewDetailed) => { - setAccountHistory(accountHistory.filter(p => p !== item.did)) - }, - [accountHistory, setAccountHistory], - ) - - const {params, query, queryWithParams} = useQueryManager({ - initialQuery: queryParam, - fixedParams, - }) - const showFilters = Boolean(queryWithParams && !showAutocomplete) - - // web only - measure header height for sticky positioning - const [headerHeight, setHeaderHeight] = React.useState(0) - const headerRef = React.useRef(null) - useLayoutEffect(() => { - if (isWeb) { - if (!headerRef.current) return - const measurement = (headerRef.current as Element).getBoundingClientRect() - setHeaderHeight(measurement.height) - } - }, []) - - useFocusEffect( - useNonReactiveCallback(() => { - if (isWeb) { - setSearchText(queryParam) - } - }), - ) - - const onPressClearQuery = React.useCallback(() => { - scrollToTopWeb() - setSearchText('') - textInput.current?.focus() - }, []) - - const onChangeText = React.useCallback(async (text: string) => { - scrollToTopWeb() - setSearchText(text) - }, []) - - const navigateToItem = React.useCallback( - (item: string) => { - scrollToTopWeb() - setShowAutocomplete(false) - updateSearchHistory(item) - - if (isWeb) { - // @ts-expect-error route is not typesafe - navigation.push(route.name, {...route.params, q: item}) - } else { - textInput.current?.blur() - navigation.setParams({q: item}) - } - }, - [updateSearchHistory, navigation, route], - ) - - const onPressCancelSearch = React.useCallback(() => { - scrollToTopWeb() - textInput.current?.blur() - setShowAutocomplete(false) - if (isWeb) { - // Empty params resets the URL to be /search rather than /search?q= - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {q: _q, ...parameters} = (route.params ?? {}) as { - [key: string]: string - } - // @ts-expect-error route is not typesafe - navigation.replace(route.name, parameters) - } else { - setSearchText('') - navigation.setParams({q: ''}) - } - }, [setShowAutocomplete, setSearchText, navigation, route.params, route.name]) - - const onSubmit = React.useCallback(() => { - navigateToItem(searchText) - }, [navigateToItem, searchText]) - - const onAutocompleteResultPress = React.useCallback(() => { - if (isWeb) { - setShowAutocomplete(false) - } else { - textInput.current?.blur() - } - }, []) - - const handleHistoryItemClick = React.useCallback( - (item: string) => { - setSearchText(item) - navigateToItem(item) - }, - [navigateToItem], - ) - - const handleProfileClick = React.useCallback( - (profile: bsky.profile.AnyProfileView) => { - unstableCacheProfileView(queryClient, profile) - // Slight delay to avoid updating during push nav animation. - setTimeout(() => { - updateProfileHistory(profile) - }, 400) - }, - [updateProfileHistory, queryClient], - ) - - const onSoftReset = React.useCallback(() => { - if (isWeb) { - // Empty params resets the URL to be /search rather than /search?q= - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {q: _q, ...parameters} = (route.params ?? {}) as { - [key: string]: string - } - // @ts-expect-error route is not typesafe - navigation.replace(route.name, parameters) - } else { - setSearchText('') - navigation.setParams({q: ''}) - textInput.current?.focus() - } - }, [navigation, route]) - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - return listenSoftReset(onSoftReset) - }, [onSoftReset, setMinimalShellMode]), - ) - - const onSearchInputFocus = React.useCallback(() => { - if (isWeb) { - // Prevent a jump on iPad by ensuring that - // the initial focused render has no result list. - requestAnimationFrame(() => { - setShowAutocomplete(true) - }) - } else { - setShowAutocomplete(true) - } - }, [setShowAutocomplete]) - - const showHeader = !gtMobile || navButton !== 'menu' - - return ( - <Layout.Screen testID={testID}> - <View - ref={headerRef} - onLayout={evt => { - if (isWeb) setHeaderHeight(evt.nativeEvent.layout.height) - }} - style={[ - a.relative, - a.z_10, - web({ - position: 'sticky', - top: 0, - }), - ]}> - <Layout.Center style={t.atoms.bg}> - {showHeader && ( - <View - // HACK: shift up search input. we can't remove the top padding - // on the search input because it messes up the layout animation - // if we add it only when the header is hidden - style={{marginBottom: tokens.space.xs * -1}}> - <Layout.Header.Outer noBottomBorder> - {navButton === 'menu' ? ( - <Layout.Header.MenuButton /> - ) : ( - <Layout.Header.BackButton /> - )} - <Layout.Header.Content align="left"> - <Layout.Header.TitleText> - <Trans>Search</Trans> - </Layout.Header.TitleText> - </Layout.Header.Content> - {showFilters ? ( - <SearchLanguageDropdown - value={params.lang} - onChange={params.setLang} - /> - ) : ( - <Layout.Header.Slot /> - )} - </Layout.Header.Outer> - </View> - )} - <View style={[a.px_md, a.pt_sm, a.pb_sm, a.overflow_hidden]}> - <View style={[a.gap_sm]}> - <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}> - <View style={[a.flex_1]}> - <SearchInput - ref={textInput} - value={searchText} - onFocus={onSearchInputFocus} - onChangeText={onChangeText} - onClearText={onPressClearQuery} - onSubmitEditing={onSubmit} - placeholder={ - inputPlaceholder ?? - _(msg`Search for posts, users, or feeds`) - } - hitSlop={{...HITSLOP_20, top: 0}} - /> - </View> - {showAutocomplete && ( - <Button - label={_(msg`Cancel search`)} - size="large" - variant="ghost" - color="secondary" - style={[a.px_sm]} - onPress={onPressCancelSearch} - hitSlop={HITSLOP_10}> - <ButtonText> - <Trans>Cancel</Trans> - </ButtonText> - </Button> - )} - </View> - - {showFilters && !showHeader && ( - <View - style={[ - a.flex_row, - a.align_center, - a.justify_between, - a.gap_sm, - ]}> - <SearchLanguageDropdown - value={params.lang} - onChange={params.setLang} - /> - </View> - )} - </View> - </View> - </Layout.Center> - </View> - - <View - style={{ - display: showAutocomplete && !fixedParams ? 'flex' : 'none', - flex: 1, - }}> - {searchText.length > 0 ? ( - <AutocompleteResults - isAutocompleteFetching={isAutocompleteFetching} - autocompleteData={autocompleteData} - searchText={searchText} - onSubmit={onSubmit} - onResultPress={onAutocompleteResultPress} - onProfileClick={handleProfileClick} - /> - ) : ( - <SearchHistory - searchHistory={termHistory} - selectedProfiles={accountHistoryProfiles?.profiles || []} - onItemClick={handleHistoryItemClick} - onProfileClick={handleProfileClick} - onRemoveItemClick={deleteSearchHistoryItem} - onRemoveProfileClick={deleteProfileHistoryItem} - /> - )} - </View> - <View - style={{ - display: showAutocomplete ? 'none' : 'flex', - flex: 1, - }}> - <SearchScreenInner - query={query} - queryWithParams={queryWithParams} - headerHeight={headerHeight} - /> - </View> - </Layout.Screen> - ) -} - -let AutocompleteResults = ({ - isAutocompleteFetching, - autocompleteData, - searchText, - onSubmit, - onResultPress, - onProfileClick, -}: { - isAutocompleteFetching: boolean - autocompleteData: AppBskyActorDefs.ProfileViewBasic[] | undefined - searchText: string - onSubmit: () => void - onResultPress: () => void - onProfileClick: (profile: AppBskyActorDefs.ProfileViewBasic) => void -}): React.ReactNode => { - const moderationOpts = useModerationOpts() - const {_} = useLingui() - return ( - <> - {(isAutocompleteFetching && !autocompleteData?.length) || - !moderationOpts ? ( - <Loader /> - ) : ( - <Layout.Content - 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={() => { - onProfileClick(item) - onResultPress() - }} - /> - ))} - <View style={{height: 200}} /> - </Layout.Content> - )} - </> - ) -} -AutocompleteResults = React.memo(AutocompleteResults) - -function SearchHistory({ - searchHistory, - selectedProfiles, - onItemClick, - onProfileClick, - onRemoveItemClick, - onRemoveProfileClick, -}: { - searchHistory: string[] - selectedProfiles: AppBskyActorDefs.ProfileViewDetailed[] - onItemClick: (item: string) => void - onProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void - onRemoveItemClick: (item: string) => void - onRemoveProfileClick: (profile: AppBskyActorDefs.ProfileViewDetailed) => void -}) { - const {gtMobile} = useBreakpoints() - const t = useTheme() - const {_} = useLingui() - - return ( - <Layout.Content - keyboardDismissMode="interactive" - keyboardShouldPersistTaps="handled"> - <View style={[a.w_full, a.px_md]}> - {(searchHistory.length > 0 || selectedProfiles.length > 0) && ( - <Text style={[a.text_md, a.font_bold, a.p_md]}> - <Trans>Recent Searches</Trans> - </Text> - )} - {selectedProfiles.length > 0 && ( - <View - style={[ - styles.selectedProfilesContainer, - !gtMobile && styles.selectedProfilesContainerMobile, - ]}> - <RNGHScrollView - keyboardShouldPersistTaps="handled" - horizontal={true} - style={[ - a.flex_row, - a.flex_nowrap, - {marginHorizontal: tokens.space._2xl * -1}, - ]} - contentContainerStyle={[a.px_2xl, a.border_0]}> - {selectedProfiles.slice(0, 5).map((profile, index) => ( - <View - key={index} - style={[ - styles.profileItem, - !gtMobile && styles.profileItemMobile, - ]}> - <Link - href={makeProfileLink(profile)} - title={profile.handle} - asAnchor - anchorNoUnderline - onBeforePress={() => onProfileClick(profile)} - style={[a.align_center, a.w_full]}> - <UserAvatar - avatar={profile.avatar} - type={profile.associated?.labeler ? 'labeler' : 'user'} - size={60} - /> - <Text - emoji - style={[a.text_xs, a.text_center, styles.profileName]} - numberOfLines={1}> - {sanitizeDisplayName( - profile.displayName || profile.handle, - )} - </Text> - </Link> - <Pressable - accessibilityRole="button" - accessibilityLabel={_(msg`Remove profile`)} - accessibilityHint={_( - msg`Removes profile from search history`, - )} - onPress={() => onRemoveProfileClick(profile)} - hitSlop={createHitslop(6)} - style={styles.profileRemoveBtn}> - <XIcon size="xs" style={t.atoms.text_contrast_low} /> - </Pressable> - </View> - ))} - </RNGHScrollView> - </View> - )} - {searchHistory.length > 0 && ( - <View style={[a.pl_md, a.pr_xs, a.mt_md]}> - {searchHistory.slice(0, 5).map((historyItem, index) => ( - <View key={index} style={[a.flex_row, a.align_center, a.mt_xs]}> - <Pressable - accessibilityRole="button" - onPress={() => onItemClick(historyItem)} - hitSlop={HITSLOP_10} - style={[a.flex_1, a.py_md]}> - <Text style={[a.text_md]}>{historyItem}</Text> - </Pressable> - <Button - label={_(msg`Remove ${historyItem}`)} - onPress={() => onRemoveItemClick(historyItem)} - size="small" - variant="ghost" - color="secondary" - shape="round"> - <ButtonIcon icon={XIcon} /> - </Button> - </View> - ))} - </View> - )} - </View> - </Layout.Content> - ) -} - -function scrollToTopWeb() { - if (isWeb) { - window.scrollTo(0, 0) - } -} - -const styles = StyleSheet.create({ - selectedProfilesContainer: { - marginTop: 10, - paddingHorizontal: 12, - height: 80, - }, - selectedProfilesContainerMobile: { - height: 100, - }, - profileItem: { - alignItems: 'center', - marginRight: 15, - width: 78, - }, - profileItemMobile: { - width: 70, - }, - profileName: { - width: 78, - marginTop: 6, - }, - profileRemoveBtn: { - position: 'absolute', - top: 0, - right: 5, - backgroundColor: 'white', - borderRadius: 10, - width: 18, - height: 18, - alignItems: 'center', - justifyContent: 'center', - }, -}) diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx deleted file mode 100644 index f6c0eca26..000000000 --- a/src/view/screens/Search/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {SearchScreen} from '#/view/screens/Search/Search' |