From 719d7b7a57c96663292d886adb6f19e283e309e0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 17 Apr 2025 19:11:46 +0300 Subject: Use `SearchablePeopleList` for add user to list dialog, replace old modal (#8212) * move to dialogs dir * make searchable people list more generic * new list-add-remove-users dialog * update header text * fix header on android * delete old modal * reduce spacing on items --- .../dms/dialogs/SearchablePeopleList.tsx | 516 --------------------- 1 file changed, 516 deletions(-) delete mode 100644 src/components/dms/dialogs/SearchablePeopleList.tsx (limited to 'src/components/dms/dialogs/SearchablePeopleList.tsx') diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx deleted file mode 100644 index 05d6f723e..000000000 --- a/src/components/dms/dialogs/SearchablePeopleList.tsx +++ /dev/null @@ -1,516 +0,0 @@ -import React, { - useCallback, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react' -import {TextInput, View} from 'react-native' -import {moderateProfile, ModerationOpts} from '@atproto/api' -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 {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' -import {useListConvosQuery} from '#/state/queries/messages/list-conversations' -import {useProfileFollowsQuery} from '#/state/queries/profile-follows' -import {useSession} from '#/state/session' -import {ListMethods} from '#/view/com/util/List' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, native, useTheme, web} from '#/alf' -import {Button, ButtonIcon} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {canBeMessaged} from '#/components/dms/util' -import {useInteractionState} from '#/components/hooks/useInteractionState' -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' -import * as bsky from '#/types/bsky' - -type Item = - | { - type: 'profile' - key: string - enabled: boolean - profile: bsky.profile.AnyProfileView - } - | { - type: 'empty' - key: string - message: string - } - | { - type: 'placeholder' - key: string - } - | { - type: 'error' - key: string - } - -export function SearchablePeopleList({ - title, - onSelectChat, - showRecentConvos, -}: { - title: string - onSelectChat: (did: string) => void - showRecentConvos?: boolean -}) { - const t = useTheme() - const {_} = useLingui() - const moderationOpts = useModerationOpts() - const control = Dialog.useDialogContext() - const [headerHeight, setHeaderHeight] = useState(0) - 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 {data: convos} = useListConvosQuery({ - enabled: showRecentConvos, - status: 'accepted', - }) - - 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(item => { - // @ts-ignore - return item.enabled ? -1 : 1 - }) - } - } else { - const placeholders: Item[] = Array(10) - .fill(0) - .map((__, i) => ({ - type: 'placeholder', - key: i + '', - })) - - if (showRecentConvos) { - if (convos && follows) { - const usedDids = new Set() - - for (const page of convos.pages) { - for (const convo of page.convos) { - const profiles = convo.members.filter( - m => m.did !== currentAccount?.did, - ) - - for (const profile of profiles) { - if (usedDids.has(profile.did)) continue - - usedDids.add(profile.did) - - _items.push({ - type: 'profile', - key: profile.did, - enabled: true, - profile, - }) - } - } - } - - let followsItems: typeof _items = [] - - for (const page of follows.pages) { - for (const profile of page.follows) { - if (usedDids.has(profile.did)) continue - - followsItems.push({ - type: 'profile', - key: profile.did, - enabled: canBeMessaged(profile), - profile, - }) - } - } - - // only sort follows - followsItems = followsItems.sort(item => { - // @ts-ignore - return item.enabled ? -1 : 1 - }) - - // then append - _items.push(...followsItems) - } else { - _items.push(...placeholders) - } - } 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(item => { - // @ts-ignore - return item.enabled ? -1 : 1 - }) - } else { - _items.push(...placeholders) - } - } - - return _items - }, [ - _, - searchText, - results, - isError, - currentAccount?.did, - follows, - convos, - showRecentConvos, - ]) - - 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 ( - setHeaderHeight(evt.nativeEvent.layout.height)} - style={[ - a.relative, - web(a.pt_lg), - native(a.pt_4xl), - a.pb_xs, - a.px_lg, - a.border_b, - t.atoms.border_contrast_low, - t.atoms.bg, - ]}> - - - {title} - - {isWeb ? ( - - ) : null} - - - - { - 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, - _, - title, - searchText, - control, - ]) - - return ( - item.key} - style={[ - web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), - native({height: '100%'}), - ]} - webInnerContentContainerStyle={a.py_0} - webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} - scrollIndicatorInsets={{top: headerHeight}} - keyboardDismissMode="on-drag" - /> - ) -} - -function ProfileCard({ - enabled, - profile, - moderationOpts, - onPress, -}: { - enabled: boolean - profile: bsky.profile.AnyProfileView - 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`Searches for profiles`)} - /> - - ) -} -- cgit 1.4.1