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 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-01-10 13:39:13 +02:00
parent a19d652a39
commit ff23ebb58e
9 changed files with 514 additions and 183 deletions
@@ -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({
<UserResult
profile={item.profile}
onChange={onChange}
memberships={memberships}
listMembers={listMembers}
list={list}
moderationOpts={moderationOpts}
/>
)
},
[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({
@@ -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 (
<Dialog.Outer control={control} testID="userAddRemoveListsDialog">
<Dialog.Handle />
<ListsContent
subjectDid={subjectDid}
displayName={displayName}
handle={handle}
onAdd={onAdd}
onRemove={onRemove}
/>
</Dialog.Outer>
)
}
function Empty() {
const t = useTheme()
return (
<View
style={[
a.gap_2xl,
platform({web: {paddingTop: 100}, native: {paddingTop: 64}}),
]}>
<View style={[a.gap_xs, a.align_center]}>
<ListIcon
size="xl"
style={{color: t.atoms.border_contrast_medium.borderColor}}
/>
<Text style={[a.text_center]}>
<Trans>You have no lists.</Trans>
</Text>
</View>
</View>
)
}
function ListsContent({
subjectDid,
displayName,
handle,
onAdd,
onRemove,
}: Omit<UserAddRemoveListsDialogProps, 'control'>) {
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}) => (
<ListItem
listWithMembership={item}
subjectDid={subjectDid}
displayName={displayName}
handle={handle}
onAdd={onAdd}
onRemove={onRemove}
/>
),
[subjectDid, displayName, handle, onAdd, onRemove],
)
const onClose = useCallback(() => {
control.close()
}, [control])
const listHeader = (
<View
style={[
a.justify_between,
a.align_center,
a.flex_row,
a.pb_lg,
native(a.pt_lg),
]}>
<Text style={[a.text_lg, a.font_semi_bold]}>
<Trans>Update {sanitizeDisplayName(displayName)} in Lists</Trans>
</Text>
<Button
label={_(msg`Close`)}
onPress={onClose}
variant="ghost"
color="secondary"
size="small"
shape="round"
style={{margin: -8}}>
<ButtonIcon icon={XIcon} />
</Button>
</View>
)
return (
<Dialog.InnerFlatList
data={isLoading ? [{}] : listItems}
renderItem={
isLoading
? () => (
<View style={[a.align_center, a.py_2xl]}>
<Loader size="xl" />
</View>
)
: renderItem
}
keyExtractor={
isLoading
? () => 'lists_dialog_loader'
: (item: ListWithMembership) => item.list.uri
}
onEndReached={onEndReached}
onEndReachedThreshold={0.1}
ListHeaderComponent={listHeader}
ListEmptyComponent={<Empty />}
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 (
<View
testID={`toggleBtn-${list.name}`}
style={[a.flex_row, a.align_center, a.py_md, a.gap_md]}>
<UserAvatar size={40} avatar={list.avatar} type="list" />
<View style={[a.flex_1]}>
<Text
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
numberOfLines={1}>
{sanitizeDisplayName(list.name)}
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>User list by you</Trans>
) : (
<Trans>
User list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
{list.purpose === 'app.bsky.graph.defs#modlist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>Moderation list by you</Trans>
) : (
<Trans>
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
</Text>
</View>
<Button
testID={`user-${handle}-addBtn`}
label={isMember ? _(msg`Remove`) : _(msg`Add`)}
onPress={handleToggleMembership}
disabled={isPending}
size="tiny"
color={isMember ? 'secondary' : 'primary_subtle'}
variant="solid">
{isPending && <ButtonIcon icon={Loader} />}
<ButtonText>
{isMember ? <Trans>Remove</Trans> : <Trans>Add</Trans>}
</ButtonText>
</Button>
</View>
)
}
+1 -13
View File
@@ -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
+4 -131
View File
@@ -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<ListMembersip[]>({
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<ListMembersip[]>(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<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
@@ -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<ListMembersip[]>(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<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>
>(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => {
+120
View File
@@ -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<AppBskyGraphGetListsWithMembership.OutputSchema>,
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<AppBskyGraphGetListsWithMembership.OutputSchema>
>(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<AppBskyGraphGetListsWithMembership.OutputSchema>
>(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
}),
})),
}
})
}
+18 -9
View File
@@ -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 && (
<UserAddRemoveListsDialog
control={editListsDialogControl}
subjectDid={selectedProfile.did}
displayName={selectedProfile.displayName || selectedProfile.handle}
handle={selectedProfile.handle}
/>
)}
</View>
)
}
+1 -5
View File
@@ -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 = <UserAddRemoveListsModal.Component {...activeModal} />
} else {
{
return null
}
+1 -4
View File
@@ -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 = <UserAddRemoveLists.Component {...modal} />
} else {
{
return null
}
+13 -11
View File
@@ -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<NavigationProp>()
@@ -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}
/>
<UserAddRemoveListsDialog
control={addToListsDialogControl}
subjectDid={profile.did}
displayName={profile.displayName || profile.handle}
handle={profile.handle}
onAdd={invalidateProfileQuery}
onRemove={invalidateProfileQuery}
/>
<ReportDialog
control={reportDialogControl}
subject={{