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 --- src/components/ProfileCard.tsx | 48 +- src/components/dialogs/SearchablePeopleList.tsx | 538 +++++++++++++++++++++ .../dialogs/lists/ListAddRemoveUsersDialog.tsx | 180 +++++++ src/components/dms/dialogs/NewChatDialog.tsx | 3 +- .../dms/dialogs/SearchablePeopleList.tsx | 516 -------------------- src/components/dms/dialogs/ShareViaChatDialog.tsx | 3 +- src/state/modals/index.tsx | 14 +- src/state/queries/list-memberships.ts | 24 +- src/view/com/modals/ListAddRemoveUsers.tsx | 316 ------------ src/view/com/modals/Modal.tsx | 4 - src/view/com/modals/Modal.web.tsx | 5 +- src/view/screens/ProfileList.tsx | 52 +- 12 files changed, 808 insertions(+), 895 deletions(-) create mode 100644 src/components/dialogs/SearchablePeopleList.tsx create mode 100644 src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx delete mode 100644 src/components/dms/dialogs/SearchablePeopleList.tsx delete mode 100644 src/view/com/modals/ListAddRemoveUsers.tsx (limited to 'src') diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 394ff9946..1a64c51d5 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -166,29 +166,47 @@ export function NameAndHandle({ profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts }) { - const t = useTheme() + return ( + + + + + ) +} + +export function Name({ + profile, + moderationOpts, +}: { + profile: bsky.profile.AnyProfileView + moderationOpts: ModerationOpts +}) { const moderation = moderateProfile(profile, moderationOpts) const name = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), ) + return ( + + {name} + + ) +} + +export function Handle({profile}: {profile: bsky.profile.AnyProfileView}) { + const t = useTheme() const handle = sanitizeHandle(profile.handle, '@') return ( - - - {name} - - - {handle} - - + + {handle} + ) } diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx new file mode 100644 index 000000000..26e20db57 --- /dev/null +++ b/src/components/dialogs/SearchablePeopleList.tsx @@ -0,0 +1,538 @@ +import { + Fragment, + useCallback, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' +import {TextInput, View} from 'react-native' +import {moderateProfile, type 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 {type ListMethods} from '#/view/com/util/List' +import {android, 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 * as ProfileCard from '#/components/ProfileCard' +import {Text} from '#/components/Typography' +import type * as bsky from '#/types/bsky' + +export type ProfileItem = { + type: 'profile' + key: string + profile: bsky.profile.AnyProfileView +} + +type EmptyItem = { + type: 'empty' + key: string + message: string +} + +type PlaceholderItem = { + type: 'placeholder' + key: string +} + +type ErrorItem = { + type: 'error' + key: string +} + +type Item = ProfileItem | EmptyItem | PlaceholderItem | ErrorItem + +export function SearchablePeopleList({ + title, + showRecentConvos, + sortByMessageDeclaration, + onSelectChat, + renderProfileCard, +}: { + title: string + showRecentConvos?: boolean + sortByMessageDeclaration?: boolean +} & ( + | { + renderProfileCard: (item: ProfileItem) => React.ReactNode + onSelectChat?: undefined + } + | { + onSelectChat: (did: string) => void + renderProfileCard?: undefined + } +)) { + 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, + profile, + }) + } + + if (sortByMessageDeclaration) { + _items = _items.sort(item => { + return item.type === 'profile' && canBeMessaged(item.profile) + ? -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, + profile, + }) + } + } + } + + let followsItems: ProfileItem[] = [] + + 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, + profile, + }) + } + } + + if (sortByMessageDeclaration) { + // only sort follows + followsItems = followsItems.sort(item => { + return canBeMessaged(item.profile) ? -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, + profile, + }) + } + } + + if (sortByMessageDeclaration) { + _items = _items.sort(item => { + return item.type === 'profile' && canBeMessaged(item.profile) + ? -1 + : 1 + }) + } + } else { + _items.push(...placeholders) + } + } + + return _items + }, [ + _, + searchText, + results, + isError, + currentAccount?.did, + follows, + convos, + showRecentConvos, + sortByMessageDeclaration, + ]) + + 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': { + if (renderProfileCard) { + return {renderProfileCard(item)} + } else { + return ( + + ) + } + } + case 'placeholder': { + return + } + case 'empty': { + return + } + default: + return null + } + }, + [moderationOpts, onSelectChat, renderProfileCard], + ) + + 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), + android({ + borderTopLeftRadius: a.rounded_md.borderRadius, + borderTopRightRadius: a.rounded_md.borderRadius, + }), + 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 DefaultProfileCard({ + profile, + moderationOpts, + onPress, +}: { + profile: bsky.profile.AnyProfileView + moderationOpts: ModerationOpts + onPress: (did: string) => void +}) { + const t = useTheme() + const {_} = useLingui() + const enabled = canBeMessaged(profile) + 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`)} + /> + + ) +} diff --git a/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx b/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx new file mode 100644 index 000000000..d975c89ed --- /dev/null +++ b/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx @@ -0,0 +1,180 @@ +import {useCallback, useMemo} from 'react' +import {View} from 'react-native' +import {type AppBskyGraphDefs, type ModerationOpts} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import { + getMembership, + type ListMembersip, + useDangerousListMembershipsQuery, + useListMembershipAddMutation, + useListMembershipRemoveMutation, +} from '#/state/queries/list-memberships' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import { + type ProfileItem, + SearchablePeopleList, +} from '#/components/dialogs/SearchablePeopleList' +import {Loader} from '#/components/Loader' +import * as ProfileCard from '#/components/ProfileCard' +import type * as bsky from '#/types/bsky' + +export function ListAddRemoveUsersDialog({ + control, + list, + onChange, +}: { + control: Dialog.DialogControlProps + list: AppBskyGraphDefs.ListView + onChange?: ( + type: 'add' | 'remove', + profile: bsky.profile.AnyProfileView, + ) => void | undefined +}) { + return ( + + + + + ) +} + +function DialogInner({ + list, + onChange, +}: { + list: AppBskyGraphDefs.ListView + onChange?: ( + type: 'add' | 'remove', + profile: bsky.profile.AnyProfileView, + ) => void | undefined +}) { + const {_} = useLingui() + const moderationOpts = useModerationOpts() + const {data: memberships} = useDangerousListMembershipsQuery() + + const renderProfileCard = useCallback( + (item: ProfileItem) => { + return ( + + ) + }, + [onChange, memberships, list, moderationOpts], + ) + + return ( + + ) +} + +function UserResult({ + profile, + list, + memberships, + onChange, + moderationOpts, +}: { + profile: bsky.profile.AnyProfileView + list: AppBskyGraphDefs.ListView + memberships: ListMembersip[] | undefined + onChange?: ( + type: 'add' | 'remove', + profile: bsky.profile.AnyProfileView, + ) => void | undefined + moderationOpts?: ModerationOpts +}) { + const {_} = useLingui() + const membership = useMemo( + () => getMembership(memberships, list.uri, profile.did), + [memberships, list.uri, profile.did], + ) + const {mutate: listMembershipAdd, isPending: isAddingPending} = + useListMembershipAddMutation({ + onSuccess: () => { + Toast.show(_(msg`Added to list`)) + onChange?.('add', profile) + }, + onError: e => Toast.show(cleanError(e), 'xmark'), + }) + const {mutate: listMembershipRemove, isPending: isRemovingPending} = + useListMembershipRemoveMutation({ + onSuccess: () => { + Toast.show(_(msg`Removed from list`)) + onChange?.('remove', profile) + }, + onError: e => Toast.show(cleanError(e), 'xmark'), + }) + const isMutating = isAddingPending || isRemovingPending + + const onToggleMembership = useCallback(() => { + if (typeof membership === 'undefined') { + return + } + if (membership === false) { + listMembershipAdd({ + listUri: list.uri, + actorDid: profile.did, + }) + } else { + listMembershipRemove({ + listUri: list.uri, + actorDid: profile.did, + membershipUri: membership, + }) + } + }, [list, profile, membership, listMembershipAdd, listMembershipRemove]) + + if (!moderationOpts) return null + + return ( + + + + + + + + {membership !== undefined && ( + + )} + + + ) +} diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index c7fedb488..a5ba793fb 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -11,9 +11,9 @@ import * as Toast from '#/view/com/util/Toast' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog' +import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList' import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {SearchablePeopleList} from './SearchablePeopleList' export function NewChat({ control, @@ -71,6 +71,7 @@ export function NewChat({ 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`)} - /> - - ) -} diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx index 4bb27ae69..97897bc28 100644 --- a/src/components/dms/dialogs/ShareViaChatDialog.tsx +++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx @@ -7,7 +7,7 @@ import {logger} from '#/logger' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import * as Toast from '#/view/com/util/Toast' import * as Dialog from '#/components/Dialog' -import {SearchablePeopleList} from './SearchablePeopleList' +import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList' export function SendViaChatDialog({ control, @@ -62,6 +62,7 @@ function SendViaChatDialogInner({ title={_(msg`Send post to...`)} onSelectChat={onCreateChat} showRecentConvos + sortByMessageDeclaration /> ) } diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 483de99e4..1709f0288 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -1,6 +1,6 @@ import React from 'react' -import {Image as RNImage} from 'react-native-image-crop-picker' -import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' +import {type Image as RNImage} from 'react-native-image-crop-picker' +import {type AppBskyActorDefs, type AppBskyGraphDefs} from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -26,15 +26,6 @@ export interface UserAddRemoveListsModal { onRemove?: (listUri: string) => void } -export interface ListAddRemoveUsersModal { - name: 'list-add-remove-users' - list: AppBskyGraphDefs.ListView - onChange?: ( - type: 'add' | 'remove', - profile: AppBskyActorDefs.ProfileViewBasic, - ) => void -} - export interface CropImageModal { name: 'crop-image' uri: string @@ -107,7 +98,6 @@ export type Modal = // Lists | CreateOrEditListModal | UserAddRemoveListsModal - | ListAddRemoveUsersModal // Posts | CropImageModal diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts index b93dc059d..410a613ad 100644 --- a/src/state/queries/list-memberships.ts +++ b/src/state/queries/list-memberships.ts @@ -90,7 +90,13 @@ export function getMembership( return membership ? membership.membershipUri : false } -export function useListMembershipAddMutation() { +export function useListMembershipAddMutation({ + onSuccess, + onError, +}: { + onSuccess?: (data: {uri: string; cid: string}) => void + onError?: (error: Error) => void +} = {}) { const {currentAccount} = useSession() const agent = useAgent() const queryClient = useQueryClient() @@ -117,7 +123,7 @@ export function useListMembershipAddMutation() { // -prf return res }, - onSuccess(data, variables) { + onSuccess: (data, variables) => { // manually update the cache; a refetch is too expensive let memberships = queryClient.getQueryData(RQKEY()) if (memberships) { @@ -145,11 +151,19 @@ export function useListMembershipAddMutation() { queryKey: LIST_MEMBERS_RQKEY(variables.listUri), }) }, 1e3) + onSuccess?.(data) }, + onError, }) } -export function useListMembershipRemoveMutation() { +export function useListMembershipRemoveMutation({ + onSuccess, + onError, +}: { + onSuccess?: (data: void) => void + onError?: (error: Error) => void +} = {}) { const {currentAccount} = useSession() const agent = useAgent() const queryClient = useQueryClient() @@ -172,7 +186,7 @@ export function useListMembershipRemoveMutation() { // query for that, so we use a timeout below // -prf }, - onSuccess(data, variables) { + onSuccess: (data, variables) => { // manually update the cache; a refetch is too expensive let memberships = queryClient.getQueryData(RQKEY()) if (memberships) { @@ -192,6 +206,8 @@ export function useListMembershipRemoveMutation() { queryKey: LIST_MEMBERS_RQKEY(variables.listUri), }) }, 1e3) + onSuccess?.(data) }, + onError, }) } diff --git a/src/view/com/modals/ListAddRemoveUsers.tsx b/src/view/com/modals/ListAddRemoveUsers.tsx deleted file mode 100644 index 5285d4a15..000000000 --- a/src/view/com/modals/ListAddRemoveUsers.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import React, {useCallback, useState} from 'react' -import { - ActivityIndicator, - Pressable, - SafeAreaView, - StyleSheet, - View, -} from 'react-native' -import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {HITSLOP_20} from '#/lib/constants' -import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {cleanError} from '#/lib/strings/errors' -import {sanitizeHandle} from '#/lib/strings/handles' -import {colors, s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' -import { - getMembership, - ListMembersip, - useDangerousListMembershipsQuery, - useListMembershipAddMutation, - useListMembershipRemoveMutation, -} from '#/state/queries/list-memberships' -import {Button} from '../util/forms/Button' -import {Text} from '../util/text/Text' -import * as Toast from '../util/Toast' -import {UserAvatar} from '../util/UserAvatar' -import {ScrollView, TextInput} from './util' - -export const snapPoints = ['90%'] - -export function Component({ - list, - onChange, -}: { - list: AppBskyGraphDefs.ListView - onChange?: ( - type: 'add' | 'remove', - profile: AppBskyActorDefs.ProfileViewBasic, - ) => void -}) { - const pal = usePalette('default') - const {_} = useLingui() - const {closeModal} = useModalControls() - const {isMobile} = useWebMediaQueries() - const [query, setQuery] = useState('') - const autocomplete = useActorAutocompleteQuery(query) - const {data: memberships} = useDangerousListMembershipsQuery() - const [isKeyboardVisible] = useIsKeyboardVisible() - - const onPressCancelSearch = useCallback(() => setQuery(''), [setQuery]) - - return ( - - - - - - {query ? ( - - - - ) : undefined} - - - {autocomplete.isLoading ? ( - - - - ) : autocomplete.data?.length ? ( - <> - {autocomplete.data.slice(0, 40).map((item, i) => ( - - ))} - - ) : ( - - No results found for {query} - - )} - - -