import React, { useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react' import type {TextInput as TextInputType} from 'react-native' import {View} from 'react-native' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useSession} from '#/state/session' import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme, web} from '#/alf' import {Button} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {TextInput} from '#/components/dms/dialogs/TextInput' import {canBeMessaged} from '#/components/dms/util' import {useInteractionState} from '#/components/hooks/useInteractionState' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Text} from '#/components/Typography' type Item = | { type: 'profile' key: string enabled: boolean profile: AppBskyActorDefs.ProfileView } | { type: 'empty' key: string message: string } | { type: 'placeholder' key: string } | { type: 'error' key: string } export function SearchablePeopleList({ title, onSelectChat, }: { title: string onSelectChat: (did: string) => void }) { const t = useTheme() const {_} = useLingui() const moderationOpts = useModerationOpts() const control = Dialog.useDialogContext() const listRef = useRef(null) const {currentAccount} = useSession() const inputRef = useRef(null) const [searchText, setSearchText] = useState('') const { data: results, isError, isFetching, } = useActorAutocompleteQuery(searchText, true, 12) const {data: follows} = useProfileFollowsQuery(currentAccount?.did) const items = useMemo(() => { let _items: Item[] = [] if (isError) { _items.push({ type: 'empty', key: 'empty', message: _(msg`We're having network issues, try again`), }) } else if (searchText.length) { if (results?.length) { for (const profile of results) { if (profile.did === currentAccount?.did) continue _items.push({ type: 'profile', key: profile.did, enabled: canBeMessaged(profile), profile, }) } _items = _items.sort(a => { // @ts-ignore return a.enabled ? -1 : 1 }) } } else { if (follows) { for (const page of follows.pages) { for (const profile of page.follows) { _items.push({ type: 'profile', key: profile.did, enabled: canBeMessaged(profile), profile, }) } } _items = _items.sort(a => { // @ts-ignore return a.enabled ? -1 : 1 }) } else { Array(10) .fill(0) .forEach((_, i) => { _items.push({ type: 'placeholder', key: i + '', }) }) } } return _items }, [_, searchText, results, isError, currentAccount?.did, follows]) if (searchText && !isFetching && !items.length && !isError) { items.push({type: 'empty', key: 'empty', message: _(msg`No results`)}) } const renderItems = useCallback( ({item}: {item: Item}) => { switch (item.type) { case 'profile': { return ( ) } case 'placeholder': { return } case 'empty': { return } default: return null } }, [moderationOpts, onSelectChat], ) useLayoutEffect(() => { if (isWeb) { setImmediate(() => { inputRef?.current?.focus() }) } }, []) const listHeader = useMemo(() => { return ( {title} { setSearchText(text) listRef.current?.scrollToOffset({offset: 0, animated: false}) }} onEscape={control.close} /> ) }, [ t.atoms.border_contrast_low, t.atoms.bg, t.atoms.text_contrast_high, t.palette.contrast_500, _, title, searchText, control, ]) return ( item.key} style={[ web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), native({ height: '100%', paddingHorizontal: 0, marginTop: 0, paddingTop: 0, borderTopLeftRadius: 40, borderTopRightRadius: 40, }), ]} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} keyboardDismissMode="on-drag" /> ) } function ProfileCard({ enabled, profile, moderationOpts, onPress, }: { enabled: boolean profile: AppBskyActorDefs.ProfileView moderationOpts: ModerationOpts onPress: (did: string) => void }) { const t = useTheme() const {_} = useLingui() const moderation = moderateProfile(profile, moderationOpts) const handle = sanitizeHandle(profile.handle, '@') const displayName = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), ) const handleOnPress = useCallback(() => { onPress(profile.did) }, [onPress, profile.did]) return ( ) } function ProfileCardSkeleton() { const t = useTheme() return ( ) } function Empty({message}: {message: string}) { const t = useTheme() return ( {message} (╯°□°)╯︵ ┻━┻ ) } function SearchInput({ value, onChangeText, onEscape, inputRef, }: { value: string onChangeText: (text: string) => void onEscape: () => void inputRef: React.RefObject }) { const t = useTheme() const {_} = useLingui() const { state: hovered, onIn: onMouseEnter, onOut: onMouseLeave, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const interacted = hovered || focused return ( { if (nativeEvent.key === 'Escape') { onEscape() } }} autoCorrect={false} autoComplete="off" autoCapitalize="none" autoFocus accessibilityLabel={_(msg`Search profiles`)} accessibilityHint={_(msg`Search profiles`)} /> ) }