From ff23ebb58eba35087e6c569b28de825ab9b3c643 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 10 Jan 2026 13:39:13 +0200 Subject: [PATCH] Replace Add User to Lists modal with modern Dialog component Migrate from deprecated modal system to modern Dialog system using the efficient getListsWithMembership() API instead of the inefficient useDangerousListMembershipsQuery(). Adds optimistic updates for faster UI feedback on add/remove operations. Co-Authored-By: Claude Haiku 4.5 --- .../lists/ListAddRemoveUsersDialog.tsx | 32 +- .../lists/UserAddRemoveListsDialog.tsx | 334 ++++++++++++++++++ src/state/modals/index.tsx | 14 +- src/state/queries/list-memberships.ts | 135 +------ src/state/queries/lists-with-membership.ts | 120 +++++++ src/view/com/lists/ListMembers.tsx | 27 +- src/view/com/modals/Modal.tsx | 6 +- src/view/com/modals/Modal.web.tsx | 5 +- src/view/com/profile/ProfileMenu.tsx | 24 +- 9 files changed, 514 insertions(+), 183 deletions(-) create mode 100644 src/components/dialogs/lists/UserAddRemoveListsDialog.tsx create mode 100644 src/state/queries/lists-with-membership.ts diff --git a/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx b/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx index da96565655..8129e9bdb4 100644 --- a/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx +++ b/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx @@ -7,10 +7,8 @@ import {Trans} from '@lingui/react/macro' import {cleanError} from '#/lib/strings/errors' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useAllListMembersQuery} from '#/state/queries/list-members' import { - getMembership, - type ListMembersip, - useDangerousListMembershipsQuery, useListMembershipAddMutation, useListMembershipRemoveMutation, } from '#/state/queries/list-memberships' @@ -61,7 +59,7 @@ function DialogInner({ }) { const {_} = useLingui() const moderationOpts = useModerationOpts() - const {data: memberships} = useDangerousListMembershipsQuery() + const {data: listMembers} = useAllListMembersQuery(list.uri) const renderProfileCard = useCallback( (item: ProfileItem) => { @@ -69,13 +67,13 @@ function DialogInner({ ) }, - [onChange, memberships, list, moderationOpts], + [onChange, listMembers, list, moderationOpts], ) return ( @@ -86,16 +84,30 @@ function DialogInner({ ) } +/** + * Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record) + */ +function getMembership( + listMembers: AppBskyGraphDefs.ListItemView[] | undefined, + actorDid: string, +): string | false | undefined { + if (!listMembers) { + return undefined + } + const member = listMembers.find(item => item.subject.did === actorDid) + return member ? member.uri : false +} + function UserResult({ profile, list, - memberships, + listMembers, onChange, moderationOpts, }: { profile: bsky.profile.AnyProfileView list: AppBskyGraphDefs.ListView - memberships: ListMembersip[] | undefined + listMembers: AppBskyGraphDefs.ListItemView[] | undefined onChange?: ( type: 'add' | 'remove', profile: bsky.profile.AnyProfileView, @@ -104,8 +116,8 @@ function UserResult({ }) { const {_} = useLingui() const membership = useMemo( - () => getMembership(memberships, list.uri, profile.did), - [memberships, list.uri, profile.did], + () => getMembership(listMembers, profile.did), + [listMembers, profile.did], ) const {mutate: listMembershipAdd, isPending: isAddingPending} = useListMembershipAddMutation({ diff --git a/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx b/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx new file mode 100644 index 0000000000..ad16a445ff --- /dev/null +++ b/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx @@ -0,0 +1,334 @@ +import {useCallback} from 'react' +import {View} from 'react-native' +import {type AppBskyActorDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' + +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {isNetworkError} from '#/lib/strings/errors' +import {sanitizeHandle} from '#/lib/strings/handles' +import {logger} from '#/logger' +import { + useListMembershipAddMutation, + useListMembershipRemoveMutation, +} from '#/state/queries/list-memberships' +import { + type ListWithMembership, + removeListMembershipOptimistically, + updateListMembershipOptimistically, + useListsWithMembershipQuery, +} from '#/state/queries/lists-with-membership' +import {useSession} from '#/state/session' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, native, platform, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {BulletList_Stroke2_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' +import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' +import {Loader} from '#/components/Loader' +import * as Toast from '#/components/Toast' +import {Text} from '#/components/Typography' + +export type UserAddRemoveListsDialogProps = { + control: Dialog.DialogControlProps + subjectDid: string + displayName: string + handle: string + onAdd?: (listUri: string) => void + onRemove?: (listUri: string) => void +} + +export function UserAddRemoveListsDialog({ + control, + subjectDid, + displayName, + handle, + onAdd, + onRemove, +}: UserAddRemoveListsDialogProps) { + return ( + + + + + ) +} + +function Empty() { + const t = useTheme() + + return ( + + + + + You have no lists. + + + + ) +} + +function ListsContent({ + subjectDid, + displayName, + handle, + onAdd, + onRemove, +}: Omit) { + const control = Dialog.useDialogContext() + const {_} = useLingui() + + const { + data, + isError, + isLoading, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useListsWithMembershipQuery({actor: subjectDid}) + + const listItems = data?.pages.flatMap(page => page.listsWithMembership) || [] + + const onEndReached = useCallback(async () => { + if (isFetchingNextPage || !hasNextPage || isError) return + try { + await fetchNextPage() + } catch (err) { + // Error handling is optional since this is just pagination + } + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) + + const renderItem = useCallback( + ({item}: {item: ListWithMembership}) => ( + + ), + [subjectDid, displayName, handle, onAdd, onRemove], + ) + + const onClose = useCallback(() => { + control.close() + }, [control]) + + const listHeader = ( + + + Update {sanitizeDisplayName(displayName)} in Lists + + + + ) + + return ( + ( + + + + ) + : renderItem + } + keyExtractor={ + isLoading + ? () => 'lists_dialog_loader' + : (item: ListWithMembership) => item.list.uri + } + onEndReached={onEndReached} + onEndReachedThreshold={0.1} + ListHeaderComponent={listHeader} + ListEmptyComponent={} + style={platform({ + web: [a.px_2xl, {minHeight: 400}], + native: [a.px_2xl, a.pt_lg], + })} + /> + ) +} + +function ListItem({ + listWithMembership, + subjectDid, + displayName, + handle, + onAdd, + onRemove, +}: { + listWithMembership: ListWithMembership + subjectDid: string + displayName: string + handle: string + onAdd?: (listUri: string) => void + onRemove?: (listUri: string) => void +}) { + const {_} = useLingui() + const t = useTheme() + const queryClient = useQueryClient() + const {currentAccount} = useSession() + + const list = listWithMembership.list + const listItem = listWithMembership.listItem + const isMember = !!listItem + + const {mutate: addMembership, isPending: isPendingAdd} = + useListMembershipAddMutation({ + onSuccess: data => { + Toast.show(_(msg`Added to list`)) + onAdd?.(list.uri) + updateListMembershipOptimistically({ + queryClient, + actor: subjectDid, + listUri: list.uri, + membershipUri: data.uri, + subject: { + did: subjectDid, + handle, + displayName, + } as AppBskyActorDefs.ProfileView, + }) + }, + onError: err => { + if (!isNetworkError(err)) { + logger.error('Failed to add to list', {safeMessage: err}) + } + Toast.show(_(msg`Failed to add to list`), {type: 'error'}) + }, + }) + + const {mutate: removeMembership, isPending: isPendingRemove} = + useListMembershipRemoveMutation({ + onSuccess: () => { + Toast.show(_(msg`Removed from list`)) + onRemove?.(list.uri) + removeListMembershipOptimistically({ + queryClient, + actor: subjectDid, + listUri: list.uri, + }) + }, + onError: err => { + if (!isNetworkError(err)) { + logger.error('Failed to remove from list', {safeMessage: err}) + } + Toast.show(_(msg`Failed to remove from list`), {type: 'error'}) + }, + }) + + const isPending = isPendingAdd || isPendingRemove + + const handleToggleMembership = useCallback(() => { + if (isPending) return + + if (!isMember) { + addMembership({ + listUri: list.uri, + actorDid: subjectDid, + }) + } else { + if (!listItem?.uri) { + console.error('Cannot remove: missing membership URI') + return + } + removeMembership({ + listUri: list.uri, + actorDid: subjectDid, + membershipUri: listItem.uri, + }) + } + }, [ + list.uri, + subjectDid, + isMember, + listItem, + isPending, + addMembership, + removeMembership, + ]) + + return ( + + + + + {sanitizeDisplayName(list.name)} + + + {list.purpose === 'app.bsky.graph.defs#curatelist' && + (list.creator.did === currentAccount?.did ? ( + User list by you + ) : ( + + User list by {sanitizeHandle(list.creator.handle, '@')} + + ))} + {list.purpose === 'app.bsky.graph.defs#modlist' && + (list.creator.did === currentAccount?.did ? ( + Moderation list by you + ) : ( + + Moderation list by {sanitizeHandle(list.creator.handle, '@')} + + ))} + + + + + ) +} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 3c545362b8..d62ba2a7a0 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -3,15 +3,6 @@ import {createContext, useContext, useEffect, useMemo, useState} from 'react' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useHotkeysContext} from '#/lib/hotkeys' -export interface UserAddRemoveListsModal { - name: 'user-add-remove-lists' - subject: string - handle: string - displayName: string - onAdd?: (listUri: string) => void - onRemove?: (listUri: string) => void -} - export interface ContentLanguagesSettingsModal { name: 'content-languages-settings' } @@ -21,10 +12,7 @@ export interface ContentLanguagesSettingsModal { */ export type Modal = // Curation - | ContentLanguagesSettingsModal - - // Lists - | UserAddRemoveListsModal + ContentLanguagesSettingsModal const ModalContext = createContext<{ isModalActive: boolean diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts index dd7fde2482..ab5221a80b 100644 --- a/src/state/queries/list-memberships.ts +++ b/src/state/queries/list-memberships.ts @@ -1,19 +1,3 @@ -/** - * NOTE - * - * This query is a temporary solution to our lack of server API for - * querying user membership in an API. It is extremely inefficient. - * - * THIS SHOULD ONLY BE USED IN MODALS FOR MODIFYING A USER'S LIST MEMBERSHIP! - * Use the list-members query for rendering a list's members. - * - * It works by fetching *all* of the user's list item records and querying - * or manipulating that cache. For users with large lists, it will fall - * down completely, so be very conservative about how you use it. - * - * -prf - */ - import { type AppBskyActorDefs, type AppBskyGraphGetStarterPacksWithMembership, @@ -22,85 +6,14 @@ import { import { type InfiniteData, useMutation, - useQuery, useQueryClient, } from '@tanstack/react-query' -import {STALE} from '#/state/queries' import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members' import {useAgent, useSession} from '#/state/session' import type * as bsky from '#/types/bsky' import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs' -// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records -const SANITY_PAGE_LIMIT = 1000 -const PAGE_SIZE = 100 -// ...which comes 100,000k list members - -const RQKEY_ROOT = 'list-memberships' -export const RQKEY = () => [RQKEY_ROOT] - -export interface ListMembersip { - membershipUri: string - listUri: string - actorDid: string -} - -/** - * This API is dangerous! Read the note above! - */ -export function useDangerousListMembershipsQuery() { - const {currentAccount} = useSession() - const agent = useAgent() - return useQuery({ - staleTime: STALE.MINUTES.FIVE, - queryKey: RQKEY(), - async queryFn() { - if (!currentAccount) { - return [] - } - let cursor - let arr: ListMembersip[] = [] - for (let i = 0; i < SANITY_PAGE_LIMIT; i++) { - const res = await agent.app.bsky.graph.listitem.list({ - repo: currentAccount.did, - limit: PAGE_SIZE, - cursor, - }) - arr = arr.concat( - res.records.map(r => ({ - membershipUri: r.uri, - listUri: r.value.list, - actorDid: r.value.subject, - })), - ) - cursor = res.cursor - if (!cursor) { - break - } - } - return arr - }, - }) -} - -/** - * Returns undefined for pending, false for not a member, and string for a member (the URI of the membership record) - */ -export function getMembership( - memberships: ListMembersip[] | undefined, - list: string, - actor: string, -): string | false | undefined { - if (!memberships) { - return undefined - } - const membership = memberships.find( - m => m.listUri === list && m.actorDid === actor, - ) - return membership ? membership.membershipUri : false -} - export function useListMembershipAddMutation({ subject, onSuccess, @@ -133,43 +46,18 @@ export function useListMembershipAddMutation({ createdAt: new Date().toISOString(), }, ) - // TODO - // we need to wait for appview to update, but there's not an efficient - // query for that, so we use a timeout below - // -prf return res }, onSuccess: (data, variables) => { - // manually update the cache; a refetch is too expensive - let memberships = queryClient.getQueryData(RQKEY()) - if (memberships) { - memberships = memberships - // avoid dups - .filter( - m => - !( - m.actorDid === variables.actorDid && - m.listUri === variables.listUri - ), - ) - .concat([ - { - ...variables, - membershipUri: data.uri, - }, - ]) - queryClient.setQueryData(RQKEY(), memberships) - } // invalidate the members queries (used for rendering the listings) - // use a timeout to wait for the appview (see above) + // use a timeout to wait for the appview setTimeout(() => { queryClient.invalidateQueries({ queryKey: LIST_MEMBERS_RQKEY(variables.listUri), }) }, 1e3) - // update WITH_MEMBERSHIPS query - + // update WITH_MEMBERSHIPS query for starter packs if (subject) { queryClient.setQueryData< InfiniteData @@ -251,32 +139,17 @@ export function useListMembershipRemoveMutation({ repo: currentAccount.did, rkey: membershipUrip.rkey, }) - // TODO - // we need to wait for appview to update, but there's not an efficient - // query for that, so we use a timeout below - // -prf }, onSuccess: (data, variables) => { - // manually update the cache; a refetch is too expensive - let memberships = queryClient.getQueryData(RQKEY()) - if (memberships) { - memberships = memberships.filter( - m => - !( - m.actorDid === variables.actorDid && - m.listUri === variables.listUri - ), - ) - queryClient.setQueryData(RQKEY(), memberships) - } // invalidate the members queries (used for rendering the listings) - // use a timeout to wait for the appview (see above) + // use a timeout to wait for the appview setTimeout(() => { queryClient.invalidateQueries({ queryKey: LIST_MEMBERS_RQKEY(variables.listUri), }) }, 1e3) + // update WITH_MEMBERSHIPS query for starter packs queryClient.setQueryData< InfiniteData >(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => { diff --git a/src/state/queries/lists-with-membership.ts b/src/state/queries/lists-with-membership.ts new file mode 100644 index 0000000000..01fef12ad7 --- /dev/null +++ b/src/state/queries/lists-with-membership.ts @@ -0,0 +1,120 @@ +import { + type AppBskyActorDefs, + type AppBskyGraphGetListsWithMembership, +} from '@atproto/api' +import { + type InfiniteData, + type QueryClient, + type QueryKey, + useInfiniteQuery, +} from '@tanstack/react-query' + +import {useAgent} from '#/state/session' + +export type ListWithMembership = + AppBskyGraphGetListsWithMembership.ListWithMembership + +const RQKEY_ROOT = 'lists-with-membership' +export const RQKEY = (actor: string) => [RQKEY_ROOT, actor] + +export function useListsWithMembershipQuery({ + actor, + enabled = true, +}: { + actor: string + enabled?: boolean +}) { + const agent = useAgent() + + return useInfiniteQuery< + AppBskyGraphGetListsWithMembership.OutputSchema, + Error, + InfiniteData, + QueryKey, + string | undefined + >({ + queryKey: RQKEY(actor), + queryFn: async ({pageParam}: {pageParam?: string}) => { + const res = await agent.app.bsky.graph.getListsWithMembership({ + actor, + limit: 50, + cursor: pageParam, + }) + return res.data + }, + enabled: Boolean(actor) && enabled, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + }) +} + +export function updateListMembershipOptimistically({ + queryClient, + actor, + listUri, + membershipUri, + subject, +}: { + queryClient: QueryClient + actor: string + listUri: string + membershipUri: string + subject: AppBskyActorDefs.ProfileView +}) { + queryClient.setQueryData< + InfiniteData + >(RQKEY(actor), old => { + if (!old) return old + + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + listsWithMembership: page.listsWithMembership.map(lwm => { + if (lwm.list.uri === listUri) { + return { + ...lwm, + listItem: { + uri: membershipUri, + subject, + }, + } + } + return lwm + }), + })), + } + }) +} + +export function removeListMembershipOptimistically({ + queryClient, + actor, + listUri, +}: { + queryClient: QueryClient + actor: string + listUri: string +}) { + queryClient.setQueryData< + InfiniteData + >(RQKEY(actor), old => { + if (!old) return old + + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + listsWithMembership: page.listsWithMembership.map(lwm => { + if (lwm.list.uri === listUri) { + return { + ...lwm, + listItem: undefined, + } + } + return lwm + }), + })), + } + }) +} diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx index a443c49fd4..886d105dc1 100644 --- a/src/view/com/lists/ListMembers.tsx +++ b/src/view/com/lists/ListMembers.tsx @@ -13,7 +13,6 @@ import {Trans} from '@lingui/react/macro' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {useModalControls} from '#/state/modals' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useListMembersQuery} from '#/state/queries/list-members' import {useSession} from '#/state/session' @@ -23,6 +22,8 @@ import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlacehol import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {UserAddRemoveListsDialog} from '#/components/dialogs/lists/UserAddRemoveListsDialog' import {ListFooter} from '#/components/Lists' import * as ProfileCard from '#/components/ProfileCard' import type * as bsky from '#/types/bsky' @@ -58,9 +59,12 @@ export function ListMembers({ const t = useTheme() const {_} = useLingui() const [isRefreshing, setIsRefreshing] = useState(false) - const {openModal} = useModalControls() const {currentAccount} = useSession() const moderationOpts = useModerationOpts() + const editListsDialogControl = useDialogControl() + const [selectedProfile, setSelectedProfile] = React.useState< + bsky.profile.AnyProfileView | undefined + >() const { data, @@ -128,14 +132,10 @@ export function ListMembers({ const onPressEditMembership = useCallback( (e: GestureResponderEvent, profile: bsky.profile.AnyProfileView) => { e.preventDefault() - openModal({ - name: 'user-add-remove-lists', - subject: profile.did, - displayName: profile.displayName || profile.handle, - handle: profile.handle, - }) + setSelectedProfile(profile) + editListsDialogControl.open() }, - [openModal], + [editListsDialogControl], ) // rendering @@ -263,6 +263,15 @@ export function ListMembers({ removeClippedSubviews={true} desktopFixedHeight={desktopFixedHeightOffset || true} /> + + {selectedProfile && ( + + )} ) } diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index fa95661d27..96b04a1428 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -7,7 +7,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {useModalControls, useModals} from '#/state/modals' import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' -import * as UserAddRemoveListsModal from './UserAddRemoveLists' const DEFAULT_SNAPPOINTS = ['90%'] const HANDLE_HEIGHT = 24 @@ -40,10 +39,7 @@ export function ModalsContainer() { let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS let element - if (activeModal?.name === 'user-add-remove-lists') { - snapPoints = UserAddRemoveListsModal.snapPoints - element = - } else { + { return null } diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index dc080bae01..e34999323d 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -6,7 +6,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type Modal as ModalIface} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals' -import * as UserAddRemoveLists from './UserAddRemoveLists' export function ModalsContainer() { const {isModalActive, activeModals} = useModals() @@ -44,9 +43,7 @@ function Modal({modal}: {modal: ModalIface}) { } let element - if (modal.name === 'user-add-remove-lists') { - element = - } else { + { return null } diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index 51edee1903..34e592751a 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -12,7 +12,6 @@ import {type NavigationProp} from '#/lib/routes/types' import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl} from '#/lib/strings/url-helpers' import {type Shadow} from '#/state/cache/types' -import {useModalControls} from '#/state/modals' import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' import { RQKEY as profileQueryKey, @@ -25,6 +24,7 @@ import {EventStopper} from '#/view/com/util/EventStopper' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' +import {UserAddRemoveListsDialog} from '#/components/dialogs/lists/UserAddRemoveListsDialog' import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' @@ -74,7 +74,6 @@ let ProfileMenu = ({ const ax = useAnalytics() const {_} = useLingui() const {currentAccount, hasSession} = useSession() - const {openModal} = useModalControls() const reportDialogControl = useReportDialogControl() const queryClient = useQueryClient() const navigation = useNavigation() @@ -107,6 +106,7 @@ let ProfileMenu = ({ const goLiveDialogControl = useDialogControl() const goLiveDisabledDialogControl = useDialogControl() const addToStarterPacksDialogControl = useDialogControl() + const addToListsDialogControl = useDialogControl() const showLoggedOutWarning = useMemo(() => { return ( @@ -131,15 +131,8 @@ let ProfileMenu = ({ }, [profile]) const onPressAddRemoveLists = useCallback(() => { - openModal({ - name: 'user-add-remove-lists', - subject: profile.did, - handle: profile.handle, - displayName: profile.displayName || profile.handle, - onAdd: invalidateProfileQuery, - onRemove: invalidateProfileQuery, - }) - }, [profile, openModal, invalidateProfileQuery]) + addToListsDialogControl.open() + }, [addToListsDialogControl]) const onPressMuteAccount = useCallback(async () => { if (profile.viewer?.muted) { @@ -530,6 +523,15 @@ let ProfileMenu = ({ targetDid={profile.did} /> + +