diff options
Diffstat (limited to 'src/view/shell/desktop')
-rw-r--r-- | src/view/shell/desktop/Feeds.tsx | 62 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 295 | ||||
-rw-r--r-- | src/view/shell/desktop/RightNav.tsx | 136 | ||||
-rw-r--r-- | src/view/shell/desktop/Search.tsx | 209 |
4 files changed, 445 insertions, 257 deletions
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 3237d2cdd..ff51ffe22 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -1,17 +1,17 @@ import React from 'react' import {View, StyleSheet} from 'react-native' import {useNavigationState} from '@react-navigation/native' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {useDesktopRightNavItems} from 'lib/hooks/useDesktopRightNavItems' import {TextLink} from 'view/com/util/Link' import {getCurrentRoute} from 'lib/routes/helpers' +import {useLingui} from '@lingui/react' +import {msg} from '@lingui/macro' +import {usePinnedFeedsInfos} from '#/state/queries/feed' -export const DesktopFeeds = observer(function DesktopFeeds() { - const store = useStores() +export function DesktopFeeds() { const pal = usePalette('default') - const items = useDesktopRightNavItems(store.preferences.pinnedFeeds) + const {_} = useLingui() + const {feeds} = usePinnedFeedsInfos() const route = useNavigationState(state => { if (!state) { @@ -23,40 +23,40 @@ export const DesktopFeeds = observer(function DesktopFeeds() { return ( <View style={[styles.container, pal.view, pal.border]}> <FeedItem href="/" title="Following" current={route.name === 'Home'} /> - {items.map(item => { - try { - const params = route.params as Record<string, string> - const routeName = - item.collection === 'app.bsky.feed.generator' - ? 'ProfileFeed' - : 'ProfileList' - return ( - <FeedItem - key={item.uri} - href={item.href} - title={item.displayName} - current={ - route.name === routeName && - params.name === item.hostname && - params.rkey === item.rkey - } - /> - ) - } catch { - return null - } - })} + {feeds + .filter(f => f.displayName !== 'Following') + .map(feed => { + try { + const params = route.params as Record<string, string> + const routeName = + feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList' + return ( + <FeedItem + key={feed.uri} + href={feed.route.href} + title={feed.displayName} + current={ + route.name === routeName && + params.name === feed.route.params.name && + params.rkey === feed.route.params.rkey + } + /> + ) + } catch { + return null + } + })} <View style={{paddingTop: 8, paddingBottom: 6}}> <TextLink type="lg" href="/feeds" - text="More feeds" + text={_(msg`More feeds`)} style={[pal.link]} /> </View> </View> ) -}) +} function FeedItem({ title, diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 39271605c..2ed294501 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {PressableWithHover} from 'view/com/util/PressableWithHover' import { @@ -16,7 +15,6 @@ import {UserAvatar} from 'view/com/util/UserAvatar' import {Link} from 'view/com/util/Link' import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {s, colors} from 'lib/styles' import { @@ -39,18 +37,36 @@ import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types' import {router} from '../../../routes' import {makeProfileLink} from 'lib/routes/links' +import {useLingui} from '@lingui/react' +import {Trans, msg} from '@lingui/macro' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import {useComposerControls} from '#/state/shell/composer' +import {useFetchHandle} from '#/state/queries/handle' +import {emitSoftReset} from '#/state/events' +import {useQueryClient} from '@tanstack/react-query' +import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' +import {NavSignupCard} from '#/view/shell/NavSignupCard' +import {truncateAndInvalidate} from '#/state/queries/util' -const ProfileCard = observer(function ProfileCardImpl() { - const store = useStores() +function ProfileCard() { + const {currentAccount} = useSession() + const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did}) const {isDesktop} = useWebMediaQueries() + const {_} = useLingui() const size = 48 - return store.me.handle ? ( + + return !isLoading && profile ? ( <Link - href={makeProfileLink(store.me)} + href={makeProfileLink({ + did: currentAccount!.did, + handle: currentAccount!.handle, + })} style={[styles.profileCard, !isDesktop && styles.profileCardTablet]} - title="My Profile" + title={_(msg`My Profile`)} asAnchor> - <UserAvatar avatar={store.me.avatar} size={size} /> + <UserAvatar avatar={profile.avatar} size={size} /> </Link> ) : ( <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}> @@ -61,12 +77,13 @@ const ProfileCard = observer(function ProfileCardImpl() { /> </View> ) -}) +} function BackBtn() { const {isTablet} = useWebMediaQueries() const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() + const {_} = useLingui() const shouldShow = useNavigationState(state => !isStateAtTabRoot(state)) const onPressBack = React.useCallback(() => { @@ -86,7 +103,7 @@ function BackBtn() { onPress={onPressBack} style={styles.backBtn} accessibilityRole="button" - accessibilityLabel="Go back" + accessibilityLabel={_(msg`Go back`)} accessibilityHint=""> <FontAwesomeIcon size={24} @@ -104,15 +121,10 @@ interface NavItemProps { iconFilled: JSX.Element label: string } -const NavItem = observer(function NavItemImpl({ - count, - href, - icon, - iconFilled, - label, -}: NavItemProps) { +function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { const pal = usePalette('default') - const store = useStores() + const queryClient = useQueryClient() + const {currentAccount} = useSession() const {isDesktop, isTablet} = useWebMediaQueries() const [pathName] = React.useMemo(() => router.matchPath(href), [href]) const currentRouteInfo = useNavigationState(state => { @@ -125,7 +137,7 @@ const NavItem = observer(function NavItemImpl({ currentRouteInfo.name === 'Profile' ? isTab(currentRouteInfo.name, pathName) && (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === - store.me.handle + currentAccount?.handle : isTab(currentRouteInfo.name, pathName) const {onPress} = useLinkProps({to: href}) const onPressWrapped = React.useCallback( @@ -135,12 +147,16 @@ const NavItem = observer(function NavItemImpl({ } e.preventDefault() if (isCurrent) { - store.emitScreenSoftReset() + emitSoftReset() } else { + if (href === '/notifications') { + // fetch new notifs on view + truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) + } onPress() } }, - [onPress, isCurrent, store], + [onPress, isCurrent, queryClient, href], ) return ( @@ -179,12 +195,16 @@ const NavItem = observer(function NavItemImpl({ )} </PressableWithHover> ) -}) +} function ComposeBtn() { - const store = useStores() + const {currentAccount} = useSession() const {getState} = useNavigation() + const {openComposer} = useComposerControls() + const {_} = useLingui() const {isTablet} = useWebMediaQueries() + const [isFetchingHandle, setIsFetchingHandle] = React.useState(false) + const fetchHandle = useFetchHandle() const getProfileHandle = async () => { const {routes} = getState() @@ -196,13 +216,21 @@ function ComposeBtn() { ).name if (handle.startsWith('did:')) { - const cached = await store.profiles.cache.get(handle) - const profile = cached ? cached.data : undefined - // if we can't resolve handle, set to undefined - handle = profile?.handle || undefined + try { + setIsFetchingHandle(true) + handle = await fetchHandle(handle) + } catch (e) { + handle = undefined + } finally { + setIsFetchingHandle(false) + } } - if (!handle || handle === store.me.handle || handle === 'handle.invalid') + if ( + !handle || + handle === currentAccount?.handle || + handle === 'handle.invalid' + ) return undefined return handle @@ -212,17 +240,18 @@ function ComposeBtn() { } const onPressCompose = async () => - store.shell.openComposer({mention: await getProfileHandle()}) + openComposer({mention: await getProfileHandle()}) if (isTablet) { return null } return ( <TouchableOpacity + disabled={isFetchingHandle} style={[styles.newPostBtn]} onPress={onPressCompose} accessibilityRole="button" - accessibilityLabel="New post" + accessibilityLabel={_(msg`New post`)} accessibilityHint=""> <View style={styles.newPostBtnIconWrapper}> <ComposeIcon2 @@ -232,16 +261,18 @@ function ComposeBtn() { /> </View> <Text type="button" style={styles.newPostBtnLabel}> - New Post + <Trans>New Post</Trans> </Text> </TouchableOpacity> ) } -export const DesktopLeftNav = observer(function DesktopLeftNav() { - const store = useStores() +export function DesktopLeftNav() { + const {hasSession, currentAccount} = useSession() const pal = usePalette('default') + const {_} = useLingui() const {isDesktop, isTablet} = useWebMediaQueries() + const numUnread = useUnreadNotifications() return ( <View @@ -251,8 +282,16 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { pal.view, pal.border, ]}> - {store.session.hasSession && <ProfileCard />} + {hasSession ? ( + <ProfileCard /> + ) : isDesktop ? ( + <View style={{paddingHorizontal: 12}}> + <NavSignupCard /> + </View> + ) : null} + <BackBtn /> + <NavItem href="/" icon={<HomeIcon size={isDesktop ? 24 : 28} style={pal.text} />} @@ -263,7 +302,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { style={pal.text} /> } - label="Home" + label={_(msg`Home`)} /> <NavItem href="/search" @@ -281,7 +320,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { style={pal.text} /> } - label="Search" + label={_(msg`Search`)} /> <NavItem href="/feeds" @@ -299,105 +338,109 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { size={isDesktop ? 24 : 28} /> } - label="Feeds" + label={_(msg`Feeds`)} /> - <NavItem - href="/notifications" - count={store.me.notifications.unreadCountLabel} - icon={ - <BellIcon - strokeWidth={2} - size={isDesktop ? 24 : 26} - style={pal.text} + + {hasSession && ( + <> + <NavItem + href="/notifications" + count={numUnread} + icon={ + <BellIcon + strokeWidth={2} + size={isDesktop ? 24 : 26} + style={pal.text} + /> + } + iconFilled={ + <BellIconSolid + strokeWidth={1.5} + size={isDesktop ? 24 : 26} + style={pal.text} + /> + } + label={_(msg`Notifications`)} /> - } - iconFilled={ - <BellIconSolid - strokeWidth={1.5} - size={isDesktop ? 24 : 26} - style={pal.text} + <NavItem + href="/lists" + icon={ + <ListIcon + style={pal.text} + size={isDesktop ? 26 : 30} + strokeWidth={2} + /> + } + iconFilled={ + <ListIcon + style={pal.text} + size={isDesktop ? 26 : 30} + strokeWidth={3} + /> + } + label={_(msg`Lists`)} /> - } - label="Notifications" - /> - <NavItem - href="/lists" - icon={ - <ListIcon - style={pal.text} - size={isDesktop ? 26 : 30} - strokeWidth={2} + <NavItem + href="/moderation" + icon={ + <HandIcon + style={pal.text} + size={isDesktop ? 24 : 27} + strokeWidth={5.5} + /> + } + iconFilled={ + <FontAwesomeIcon + icon="hand" + style={pal.text as FontAwesomeIconStyle} + size={isDesktop ? 20 : 26} + /> + } + label={_(msg`Moderation`)} /> - } - iconFilled={ - <ListIcon - style={pal.text} - size={isDesktop ? 26 : 30} - strokeWidth={3} + <NavItem + href={currentAccount ? makeProfileLink(currentAccount) : '/'} + icon={ + <UserIcon + strokeWidth={1.75} + size={isDesktop ? 28 : 30} + style={pal.text} + /> + } + iconFilled={ + <UserIconSolid + strokeWidth={1.75} + size={isDesktop ? 28 : 30} + style={pal.text} + /> + } + label="Profile" /> - } - label="Lists" - /> - <NavItem - href="/moderation" - icon={ - <HandIcon - style={pal.text} - size={isDesktop ? 24 : 27} - strokeWidth={5.5} + <NavItem + href="/settings" + icon={ + <CogIcon + strokeWidth={1.75} + size={isDesktop ? 28 : 32} + style={pal.text} + /> + } + iconFilled={ + <CogIconSolid + strokeWidth={1.5} + size={isDesktop ? 28 : 32} + style={pal.text} + /> + } + label={_(msg`Settings`)} /> - } - iconFilled={ - <FontAwesomeIcon - icon="hand" - style={pal.text as FontAwesomeIconStyle} - size={isDesktop ? 20 : 26} - /> - } - label="Moderation" - /> - {store.session.hasSession && ( - <NavItem - href={makeProfileLink(store.me)} - icon={ - <UserIcon - strokeWidth={1.75} - size={isDesktop ? 28 : 30} - style={pal.text} - /> - } - iconFilled={ - <UserIconSolid - strokeWidth={1.75} - size={isDesktop ? 28 : 30} - style={pal.text} - /> - } - label="Profile" - /> + + <ComposeBtn /> + </> )} - <NavItem - href="/settings" - icon={ - <CogIcon - strokeWidth={1.75} - size={isDesktop ? 28 : 32} - style={pal.text} - /> - } - iconFilled={ - <CogIconSolid - strokeWidth={1.5} - size={isDesktop ? 28 : 32} - style={pal.text} - /> - } - label="Settings" - /> - {store.session.hasSession && <ComposeBtn />} </View> ) -}) +} const styles = StyleSheet.create({ leftNav: { diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 84d7d7854..9a5186549 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {usePalette} from 'lib/hooks/usePalette' @@ -9,15 +8,19 @@ import {Text} from 'view/com/util/text/Text' import {TextLink} from 'view/com/util/Link' import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants' import {s} from 'lib/styles' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {pluralize} from 'lib/strings/helpers' import {formatCount} from 'view/com/util/numeric/format' +import {useModalControls} from '#/state/modals' +import {useLingui} from '@lingui/react' +import {Plural, Trans, msg, plural} from '@lingui/macro' +import {useSession} from '#/state/session' +import {useInviteCodesQuery} from '#/state/queries/invites' -export const DesktopRightNav = observer(function DesktopRightNavImpl() { - const store = useStores() +export function DesktopRightNav() { const pal = usePalette('default') const palError = usePalette('error') + const {_} = useLingui() + const {isSandbox, hasSession, currentAccount} = useSession() const {isTablet} = useWebMediaQueries() if (isTablet) { @@ -26,10 +29,22 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { return ( <View style={[styles.rightNav, pal.view]}> - {store.session.hasSession && <DesktopSearch />} - {store.session.hasSession && <DesktopFeeds />} - <View style={styles.message}> - {store.session.isSandbox ? ( + <DesktopSearch /> + + {hasSession && ( + <View style={{paddingTop: 18, marginBottom: 18}}> + <DesktopFeeds /> + </View> + )} + + <View + style={[ + styles.message, + { + paddingTop: hasSession ? 0 : 18, + }, + ]}> + {isSandbox ? ( <View style={[palError.view, styles.messageLine, s.p10]}> <Text type="md" style={[palError.text, s.bold]}> SANDBOX. Posts and accounts are not permanent. @@ -37,23 +52,27 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { </View> ) : undefined} <View style={[s.flexRow]}> - <TextLink - type="md" - style={pal.link} - href={FEEDBACK_FORM_URL({ - email: store.session.currentSession?.email, - handle: store.session.currentSession?.handle, - })} - text="Send feedback" - /> - <Text type="md" style={pal.textLight}> - · - </Text> + {hasSession && ( + <> + <TextLink + type="md" + style={pal.link} + href={FEEDBACK_FORM_URL({ + email: currentAccount!.email, + handle: currentAccount!.handle, + })} + text={_(msg`Feedback`)} + /> + <Text type="md" style={pal.textLight}> + · + </Text> + </> + )} <TextLink type="md" style={pal.link} href="https://blueskyweb.xyz/support/privacy-policy" - text="Privacy" + text={_(msg`Privacy`)} /> <Text type="md" style={pal.textLight}> · @@ -62,7 +81,7 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { type="md" style={pal.link} href="https://blueskyweb.xyz/support/tos" - text="Terms" + text={_(msg`Terms`)} /> <Text type="md" style={pal.textLight}> · @@ -71,52 +90,80 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { type="md" style={pal.link} href={HELP_DESK_URL} - text="Help" + text={_(msg`Help`)} /> </View> </View> - <InviteCodes /> + + {hasSession && <InviteCodes />} </View> ) -}) +} -const InviteCodes = observer(function InviteCodesImpl() { - const store = useStores() +function InviteCodes() { const pal = usePalette('default') - - const {invitesAvailable} = store.me + const {openModal} = useModalControls() + const {data: invites} = useInviteCodesQuery() + const invitesAvailable = invites?.available?.length ?? 0 + const {_} = useLingui() const onPress = React.useCallback(() => { - store.shell.openModal({name: 'invite-codes'}) - }, [store]) + openModal({name: 'invite-codes'}) + }, [openModal]) + + if (!invites) { + return null + } + + if (invites?.disabled) { + return ( + <View style={[styles.inviteCodes, pal.border]}> + <FontAwesomeIcon + icon="ticket" + style={[styles.inviteCodesIcon, pal.textLight]} + size={16} + /> + <Text type="md-medium" style={pal.textLight}> + <Trans> + Your invite codes are hidden when logged in using an App Password + </Trans> + </Text> + </View> + ) + } + return ( <TouchableOpacity style={[styles.inviteCodes, pal.border]} onPress={onPress} accessibilityRole="button" - accessibilityLabel={ - invitesAvailable === 1 - ? 'Invite codes: 1 available' - : `Invite codes: ${invitesAvailable} available` - } - accessibilityHint="Opens list of invite codes"> + accessibilityLabel={_( + plural(invitesAvailable, { + one: 'Invite codes: # available', + other: 'Invite codes: # available', + }), + )} + accessibilityHint={_(msg`Opens list of invite codes`)}> <FontAwesomeIcon icon="ticket" style={[ styles.inviteCodesIcon, - store.me.invitesAvailable > 0 ? pal.link : pal.textLight, + invitesAvailable > 0 ? pal.link : pal.textLight, ]} size={16} /> <Text type="md-medium" - style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}> - {formatCount(store.me.invitesAvailable)} invite{' '} - {pluralize(store.me.invitesAvailable, 'code')} available + style={invitesAvailable > 0 ? pal.link : pal.textLight}> + <Plural + value={formatCount(invitesAvailable)} + one="# invite code available" + other="# invite codes available" + /> </Text> </TouchableOpacity> ) -}) +} const styles = StyleSheet.create({ rightNav: { @@ -142,9 +189,10 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingVertical: 12, flexDirection: 'row', - alignItems: 'center', }, inviteCodesIcon: { + marginTop: 2, marginRight: 6, + flexShrink: 0, }, }) diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index caecea4a8..f899431b6 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -1,56 +1,150 @@ import React from 'react' -import {TextInput, View, StyleSheet, TouchableOpacity} from 'react-native' +import { + ViewStyle, + TextInput, + View, + StyleSheet, + TouchableOpacity, + ActivityIndicator, +} from 'react-native' import {useNavigation, StackActions} from '@react-navigation/native' -import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' -import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' +import { + AppBskyActorDefs, + moderateProfile, + ProfileModeration, +} from '@atproto/api' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {s} from '#/lib/styles' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {makeProfileLink} from '#/lib/routes/links' +import {Link} from '#/view/com/util/Link' import {usePalette} from 'lib/hooks/usePalette' import {MagnifyingGlassIcon2} from 'lib/icons' import {NavigationProp} from 'lib/routes/types' -import {ProfileCard} from 'view/com/profile/ProfileCard' import {Text} from 'view/com/util/text/Text' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' +import {useModerationOpts} from '#/state/queries/preferences' -export const DesktopSearch = observer(function DesktopSearch() { - const store = useStores() +export function SearchResultCard({ + profile, + style, + moderation, +}: { + profile: AppBskyActorDefs.ProfileViewBasic + style: ViewStyle + moderation: ProfileModeration +}) { const pal = usePalette('default') - const textInput = React.useRef<TextInput>(null) - const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) - const [query, setQuery] = React.useState<string>('') - const autocompleteView = React.useMemo<UserAutocompleteModel>( - () => new UserAutocompleteModel(store), - [store], + + return ( + <Link + href={makeProfileLink(profile)} + title={profile.handle} + asAnchor + anchorNoUnderline> + <View + style={[ + pal.border, + style, + { + borderTopWidth: 1, + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingVertical: 8, + paddingHorizontal: 12, + }, + ]}> + <UserAvatar + size={40} + avatar={profile.avatar} + moderation={moderation.avatar} + /> + <View style={{flex: 1}}> + <Text + type="lg" + style={[s.bold, pal.text]} + numberOfLines={1} + lineHeight={1.2}> + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.profile, + )} + </Text> + <Text type="md" style={[pal.textLight]} numberOfLines={1}> + {sanitizeHandle(profile.handle, '@')} + </Text> + </View> + </View> + </Link> ) +} + +export function DesktopSearch() { + const {_} = useLingui() + const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() + const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>( + undefined, + ) + const [isActive, setIsActive] = React.useState<boolean>(false) + const [isFetching, setIsFetching] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + const [searchResults, setSearchResults] = React.useState< + AppBskyActorDefs.ProfileViewBasic[] + >([]) - // initial setup - React.useEffect(() => { - if (store.me.did) { - autocompleteView.setup() - } - }, [autocompleteView, store.me.did]) + const moderationOpts = useModerationOpts() + const search = useActorAutocompleteFn() - const onChangeQuery = React.useCallback( - (text: string) => { + const onChangeText = React.useCallback( + async (text: string) => { setQuery(text) - if (text.length > 0 && isInputFocused) { - autocompleteView.setActive(true) - autocompleteView.setPrefix(text) + + if (text.length > 0) { + setIsFetching(true) + setIsActive(true) + + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + + searchDebounceTimeout.current = setTimeout(async () => { + const results = await search({query: text}) + + if (results) { + setSearchResults(results) + setIsFetching(false) + } + }, 300) } else { - autocompleteView.setActive(false) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + setSearchResults([]) + setIsFetching(false) + setIsActive(false) } }, - [setQuery, autocompleteView, isInputFocused], + [setQuery, search, setSearchResults], ) const onPressCancelSearch = React.useCallback(() => { setQuery('') - autocompleteView.setActive(false) - }, [setQuery, autocompleteView]) - + setIsActive(false) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) + }, [setQuery]) const onSubmit = React.useCallback(() => { + setIsActive(false) + if (!query.length) return + setSearchResults([]) + if (searchDebounceTimeout.current) + clearTimeout(searchDebounceTimeout.current) navigation.dispatch(StackActions.push('Search', {q: query})) - autocompleteView.setActive(false) - }, [query, navigation, autocompleteView]) + }, [query, navigation, setSearchResults]) return ( <View style={[styles.container, pal.view]}> @@ -63,19 +157,16 @@ export const DesktopSearch = observer(function DesktopSearch() { /> <TextInput testID="searchTextInput" - ref={textInput} - placeholder="Search" + placeholder={_(msg`Search`)} placeholderTextColor={pal.colors.textLight} selectTextOnFocus returnKeyType="search" value={query} style={[pal.textLight, styles.input]} - onFocus={() => setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - onChangeText={onChangeQuery} + onChangeText={onChangeText} onSubmitEditing={onSubmit} accessibilityRole="search" - accessibilityLabel="Search" + accessibilityLabel={_(msg`Search`)} accessibilityHint="" /> {query ? ( @@ -83,11 +174,11 @@ export const DesktopSearch = observer(function DesktopSearch() { <TouchableOpacity onPress={onPressCancelSearch} accessibilityRole="button" - accessibilityLabel="Cancel search" + accessibilityLabel={_(msg`Cancel search`)} accessibilityHint="Exits inputting search query" onAccessibilityEscape={onPressCancelSearch}> <Text type="lg" style={[pal.link]}> - Cancel + <Trans>Cancel</Trans> </Text> </TouchableOpacity> </View> @@ -95,32 +186,42 @@ export const DesktopSearch = observer(function DesktopSearch() { </View> </View> - {query !== '' && ( + {query !== '' && isActive && moderationOpts && ( <View style={[pal.view, pal.borderDark, styles.resultsContainer]}> - {autocompleteView.suggestions.length ? ( + {isFetching ? ( + <View style={{padding: 8}}> + <ActivityIndicator /> + </View> + ) : ( <> - {autocompleteView.suggestions.map((item, i) => ( - <ProfileCard key={item.did} profile={item} noBorder={i === 0} /> - ))} + {searchResults.length ? ( + searchResults.map((item, i) => ( + <SearchResultCard + key={item.did} + profile={item} + moderation={moderateProfile(item, moderationOpts)} + style={i === 0 ? {borderTopWidth: 0} : {}} + /> + )) + ) : ( + <View> + <Text style={[pal.textLight, styles.noResults]}> + <Trans>No results found for {query}</Trans> + </Text> + </View> + )} </> - ) : ( - <View> - <Text style={[pal.textLight, styles.noResults]}> - No results found for {autocompleteView.prefix} - </Text> - </View> )} </View> )} </View> ) -}) +} const styles = StyleSheet.create({ container: { position: 'relative', width: 300, - paddingBottom: 18, }, search: { paddingHorizontal: 16, @@ -150,15 +251,11 @@ const styles = StyleSheet.create({ paddingVertical: 7, }, resultsContainer: { - // @ts-ignore supported by web - // position: 'fixed', marginTop: 10, - flexDirection: 'column', width: 300, borderWidth: 1, borderRadius: 6, - paddingVertical: 4, }, noResults: { textAlign: 'center', |