Add list of mutual chats to block dialog (#10727)
Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,7 @@ import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {BlockDialog} from '#/components/moderation/BlockDialog'
|
||||
import {
|
||||
ReportDialog,
|
||||
useReportDialogControl,
|
||||
@@ -845,13 +846,10 @@ let PostMenuItems = ({
|
||||
onConfirm={() => void onToggleReplyVisibility()}
|
||||
confirmButtonCta={l`Yes, hide`}
|
||||
/>
|
||||
<Prompt.Basic
|
||||
<BlockDialog
|
||||
control={blockPromptControl}
|
||||
title={l`Block Account?`}
|
||||
description={l`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`}
|
||||
onConfirm={() => void onBlockAuthor()}
|
||||
confirmButtonCta={l`Block`}
|
||||
confirmButtonColor="negative"
|
||||
profile={postAuthor}
|
||||
onBlock={onBlockAuthor}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type ChatBskyConvoDefs,
|
||||
ChatBskyConvoLeaveConvo,
|
||||
ChatBskyGroupRemoveMembers,
|
||||
} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||
import {
|
||||
createListMutualGroupsQueryKey,
|
||||
useListMutualGroupsQuery,
|
||||
} from '#/state/queries/messages/list-mutual-groups'
|
||||
import {useRemoveFromGroupChat} from '#/state/queries/messages/remove-from-group'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import {parseConvoView} from '#/components/dms/util'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type AnyProfileView} from '#/types/bsky/profile'
|
||||
|
||||
type Item = ChatBskyConvoDefs.ConvoView
|
||||
|
||||
type BlockDialogProps = {
|
||||
control: DialogControlProps
|
||||
profile: Shadow<AnyProfileView>
|
||||
onBlock: () => Promise<void>
|
||||
currentConvoId?: string
|
||||
}
|
||||
|
||||
export function BlockDialog({
|
||||
control,
|
||||
profile,
|
||||
onBlock,
|
||||
currentConvoId,
|
||||
}: BlockDialogProps) {
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<View style={[a.relative]}>
|
||||
<Dialog.Handle />
|
||||
<BlockDialogInner
|
||||
control={control}
|
||||
profile={profile}
|
||||
onBlock={onBlock}
|
||||
currentConvoId={currentConvoId}
|
||||
/>
|
||||
<Dialog.Close />
|
||||
</View>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function BlockDialogInner({
|
||||
control,
|
||||
profile,
|
||||
onBlock,
|
||||
currentConvoId,
|
||||
}: {
|
||||
control: DialogControlProps
|
||||
profile: Shadow<AnyProfileView>
|
||||
onBlock: () => Promise<void>
|
||||
currentConvoId?: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
const [footerHeight, setFooterHeight] = useState(0)
|
||||
|
||||
/*
|
||||
* Optimistically hide convos the viewer has left or removed the profile
|
||||
* from, before the query refetches. We don't expect many items here, so a
|
||||
* simple filter is fine.
|
||||
*/
|
||||
const [removedConvoIds, setRemovedConvoIds] = useState<Set<string>>(
|
||||
() => new Set(),
|
||||
)
|
||||
const onOptimisticallyRemoveConvo = (convoId: string) => {
|
||||
setRemovedConvoIds(prev => {
|
||||
const next = new Set(prev)
|
||||
next.add(convoId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
const onRestoreConvo = (convoId: string) => {
|
||||
setRemovedConvoIds(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(convoId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const {data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage} =
|
||||
useListMutualGroupsQuery({
|
||||
subject: profile.did,
|
||||
enabled: !profile.viewer?.blocking,
|
||||
})
|
||||
const items: Item[] = (data?.pages.flatMap(page => page.convos) ?? []).filter(
|
||||
item => !removedConvoIds.has(item.id),
|
||||
)
|
||||
const hasMutualGroupChats = items.length > 0
|
||||
|
||||
const onEndReached = async () => {
|
||||
if (isFetchingNextPage || !hasNextPage) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more mutual group chats', {message: err})
|
||||
}
|
||||
}
|
||||
|
||||
const renderItems = ({item}: {item: Item}) => {
|
||||
return (
|
||||
<MutualGroupChat
|
||||
view={item}
|
||||
profileDid={profile.did}
|
||||
currentConvoId={currentConvoId}
|
||||
onOptimisticallyRemoveConvo={onOptimisticallyRemoveConvo}
|
||||
onRestoreConvo={onRestoreConvo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const listHeader = (
|
||||
<View
|
||||
style={[t.atoms.bg]}
|
||||
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
|
||||
<View
|
||||
style={[
|
||||
hasMutualGroupChats && native([a.pt_2xl, a.px_2xl]),
|
||||
a.pb_lg,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<Text style={[a.text_2xl, a.font_bold, t.atoms.text]}>
|
||||
{profile.viewer?.blocking ? (
|
||||
<Trans>Unblock account?</Trans>
|
||||
) : (
|
||||
<Trans>Block account?</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
{profile.viewer?.blocking ? (
|
||||
<Trans>
|
||||
The account will be able to interact with you after unblocking.
|
||||
</Trans>
|
||||
) : profile.associated?.labeler ? (
|
||||
<Trans>
|
||||
Blocking will not prevent labels from being applied on your
|
||||
account, but it will stop this account from replying in your
|
||||
threads or interacting with you.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Blocked accounts cannot reply in your threads, mention you, or
|
||||
otherwise interact with you.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
{hasMutualGroupChats ? (
|
||||
<View style={[web(a.pt_sm), native(a.px_2xl), a.pb_xs, t.atoms.bg]}>
|
||||
<Text
|
||||
style={[a.text_sm, a.font_semi_bold, t.atoms.text_contrast_high]}>
|
||||
<Trans>Mutual group chats</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
|
||||
const footer = (
|
||||
<View style={[a.w_full, a.gap_sm, a.justify_end]}>
|
||||
<Button
|
||||
color={profile.viewer?.blocking ? undefined : 'negative'}
|
||||
size="large"
|
||||
label={profile.viewer?.blocking ? l`Unblock` : l`Block`}
|
||||
onPress={() => control.close(() => void onBlock())}>
|
||||
<ButtonText>
|
||||
{profile.viewer?.blocking ? (
|
||||
<Trans>Unblock</Trans>
|
||||
) : (
|
||||
<Trans>Block</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={l`Close dialog`}
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
|
||||
if (isLoading || !hasMutualGroupChats) {
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={profile.viewer?.blocking ? l`Unblock` : l`Block`}
|
||||
style={[web([{maxWidth: 420}])]}>
|
||||
{listHeader}
|
||||
{isLoading ? (
|
||||
<View style={[a.pb_2xl, a.align_center, a.justify_center]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : null}
|
||||
{footer}
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.InnerFlatList
|
||||
data={items}
|
||||
renderItem={renderItems}
|
||||
ListHeaderComponent={listHeader}
|
||||
stickyHeaderIndices={[0]}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? (
|
||||
<View style={[a.py_lg, a.align_center, a.justify_center]}>
|
||||
<Loader size="lg" />
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
footer={
|
||||
<Dialog.FlatListFooter
|
||||
onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)}>
|
||||
{footer}
|
||||
</Dialog.FlatListFooter>
|
||||
}
|
||||
contentContainerStyle={[a.gap_0, {paddingBottom: footerHeight}]}
|
||||
scrollIndicatorInsets={{top: headerHeight, bottom: footerHeight}}
|
||||
onEndReached={() => void onEndReached()}
|
||||
onEndReachedThreshold={0.5}
|
||||
style={[web([{maxWidth: 420}])]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MutualGroupChat({
|
||||
view,
|
||||
profileDid,
|
||||
currentConvoId,
|
||||
onOptimisticallyRemoveConvo,
|
||||
onRestoreConvo,
|
||||
}: {
|
||||
view: ChatBskyConvoDefs.ConvoView
|
||||
profileDid: string
|
||||
currentConvoId?: string
|
||||
onOptimisticallyRemoveConvo: (convoId: string) => void
|
||||
onRestoreConvo: (convoId: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const convo = parseConvoView(view, currentAccount?.did)
|
||||
|
||||
const {mutate: leaveConvo, isPending: isLeavePending} = useLeaveConvo(
|
||||
convo?.view.id,
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.show(l`Left group chat.`)
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: createListMutualGroupsQueryKey({subject: profileDid}),
|
||||
})
|
||||
},
|
||||
onError: error => {
|
||||
onRestoreConvo(view.id)
|
||||
logger.error('Error leaving group chat', {message: error})
|
||||
let errorMessage = l`Could not leave chat.`
|
||||
if (isNetworkError(error)) {
|
||||
errorMessage = l`A network error occurred. Please check your internet connection.`
|
||||
} else if (error instanceof ChatBskyConvoLeaveConvo.InvalidConvoError) {
|
||||
errorMessage = l`Chat not found.`
|
||||
} else if (
|
||||
error instanceof ChatBskyConvoLeaveConvo.OwnerCannotLeaveError
|
||||
) {
|
||||
errorMessage = l`Chat owners cannot leave a group chat.`
|
||||
}
|
||||
Toast.show(errorMessage, {type: 'error'})
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const {mutate: removeMembers, isPending: isRemovePending} =
|
||||
useRemoveFromGroupChat(convo?.view.id, {
|
||||
onSuccess: () => {
|
||||
Toast.show(l`Member removed from group chat.`)
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: createListMutualGroupsQueryKey({subject: profileDid}),
|
||||
})
|
||||
},
|
||||
onError: error => {
|
||||
onRestoreConvo(view.id)
|
||||
logger.error('Error removing group chat member', {message: error})
|
||||
let errorMessage = l`Could not remove member.`
|
||||
if (isNetworkError(error)) {
|
||||
errorMessage = l`A network error occurred. Please check your internet connection.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRemoveMembers.InvalidConvoError
|
||||
) {
|
||||
errorMessage = l`Chat not found.`
|
||||
} else if (
|
||||
error instanceof ChatBskyGroupRemoveMembers.InsufficientRoleError
|
||||
) {
|
||||
errorMessage = l`You must be a chat owner to remove a member.`
|
||||
}
|
||||
Toast.show(errorMessage, {type: 'error'})
|
||||
},
|
||||
})
|
||||
|
||||
if (!convo || convo.kind !== 'group') return null
|
||||
|
||||
const owner = convo.primaryMember
|
||||
const isViewerOwner = owner?.did != null && owner.did === currentAccount?.did
|
||||
const isProfileOwner = owner?.did != null && owner.did === profileDid
|
||||
const isCurrentConvo = view.id === currentConvoId
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
a.justify_between,
|
||||
a.py_sm,
|
||||
native(a.px_2xl),
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
<AvatarBubbles profiles={convo.members} size={40} />
|
||||
<View>
|
||||
<Text
|
||||
style={[a.text_md, a.font_semi_bold, a.leading_snug, t.atoms.text]}>
|
||||
{convo.details.name}
|
||||
</Text>
|
||||
{isViewerOwner ? (
|
||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||
<Trans>You own this chat</Trans>
|
||||
</Text>
|
||||
) : isProfileOwner ? (
|
||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||
<Trans>They own this chat</Trans>
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
{isViewerOwner ? (
|
||||
<Button
|
||||
color="negative_subtle"
|
||||
disabled={isRemovePending}
|
||||
label={l`Kick member`}
|
||||
size="small"
|
||||
onPress={() => {
|
||||
onOptimisticallyRemoveConvo(view.id)
|
||||
removeMembers({members: [profileDid]})
|
||||
}}>
|
||||
<ButtonText>
|
||||
<Trans>Kick member</Trans>
|
||||
</ButtonText>
|
||||
{isRemovePending ? <ButtonIcon icon={Loader} /> : null}
|
||||
</Button>
|
||||
) : isCurrentConvo ? (
|
||||
<Text style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Current chat</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={isLeavePending}
|
||||
label={l`Leave chat`}
|
||||
size="small"
|
||||
onPress={() => {
|
||||
onOptimisticallyRemoveConvo(view.id)
|
||||
leaveConvo()
|
||||
}}>
|
||||
<ButtonText>
|
||||
<Trans>Leave chat</Trans>
|
||||
</ButtonText>
|
||||
{isLeavePending ? <ButtonIcon icon={Loader} /> : null}
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -22,11 +22,12 @@ import {
|
||||
PersonX_Stroke2_Corner0_Rounded as PersonXIcon,
|
||||
} from '#/components/icons/Person'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {BlockDialog} from '#/components/moderation/BlockDialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {BlockMemberPrompt, RemoveMemberPrompt} from './prompts'
|
||||
import {RemoveMemberPrompt} from './prompts'
|
||||
import {StatusBadge} from './StatusBadge'
|
||||
|
||||
export function MemberMenu({
|
||||
@@ -238,9 +239,11 @@ export function MemberMenu({
|
||||
</Menu.Group>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
<BlockMemberPrompt
|
||||
<BlockDialog
|
||||
control={blockMemberPrompt}
|
||||
onConfirm={() => void handleBlockMember()}
|
||||
profile={profile}
|
||||
onBlock={handleBlockMember}
|
||||
currentConvoId={convoId}
|
||||
/>
|
||||
<RemoveMemberPrompt
|
||||
control={removeMemberPrompt}
|
||||
|
||||
@@ -174,24 +174,3 @@ export function RemoveMemberPrompt({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function BlockMemberPrompt({
|
||||
control,
|
||||
onConfirm,
|
||||
}: {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
onConfirm: () => void
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<Prompt.Basic
|
||||
control={control}
|
||||
title={l`Block account?`}
|
||||
description={l`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`}
|
||||
onConfirm={onConfirm}
|
||||
confirmButtonCta={l`Block`}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {useInfiniteQuery} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const listMutualGroupsQueryKeyRoot = 'list-mutual-groups'
|
||||
|
||||
export const createListMutualGroupsQueryKey = (args: {subject: string}) =>
|
||||
createQueryKey(listMutualGroupsQueryKeyRoot, args)
|
||||
|
||||
export function useListMutualGroupsQuery({
|
||||
subject,
|
||||
enabled,
|
||||
limit = 20,
|
||||
}: {
|
||||
subject: string | undefined
|
||||
enabled?: boolean
|
||||
limit?: number
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const isEnabled = enabled !== false && !!subject
|
||||
|
||||
return useInfiniteQuery({
|
||||
gcTime: 0,
|
||||
staleTime: 0,
|
||||
enabled: isEnabled,
|
||||
queryKey: createListMutualGroupsQueryKey({subject: subject ?? ''}),
|
||||
queryFn: async ({pageParam}) => {
|
||||
const {data} = await agent.chat.bsky.group.listMutualGroups(
|
||||
{subject: subject!, cursor: pageParam, limit},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
return data
|
||||
},
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: page => page.cursor,
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import {memo, useCallback, useMemo} from 'react'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -46,6 +44,7 @@ import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import {StarterPack} from '#/components/icons/StarterPack'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {BlockDialog} from '#/components/moderation/BlockDialog'
|
||||
import {
|
||||
ReportDialog,
|
||||
useReportDialogControl,
|
||||
@@ -72,7 +71,7 @@ let ProfileMenu = ({
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const ax = useAnalytics()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const {openModal} = useModalControls()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
@@ -116,7 +115,7 @@ let ProfileMenu = ({
|
||||
}, [currentAccount, profile])
|
||||
|
||||
const invalidateProfileQuery = useCallback(() => {
|
||||
queryClient.invalidateQueries({
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: profileQueryKey(profile.did),
|
||||
})
|
||||
}, [queryClient, profile.did])
|
||||
@@ -124,10 +123,10 @@ let ProfileMenu = ({
|
||||
const onPressAddToStarterPacks = useCallback(() => {
|
||||
ax.metric('profile:addToStarterPack', {})
|
||||
addToStarterPacksDialogControl.open()
|
||||
}, [addToStarterPacksDialogControl])
|
||||
}, [addToStarterPacksDialogControl, ax])
|
||||
|
||||
const onPressShare = useCallback(() => {
|
||||
shareUrl(toShareUrl(makeProfileLink(profile)))
|
||||
void shareUrl(toShareUrl(makeProfileLink(profile)))
|
||||
}, [profile])
|
||||
|
||||
const onPressAddRemoveLists = useCallback(() => {
|
||||
@@ -145,11 +144,12 @@ let ProfileMenu = ({
|
||||
if (profile.viewer?.muted) {
|
||||
try {
|
||||
await queueUnmute()
|
||||
Toast.show(_(msg({message: 'Account unmuted', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
Toast.show(l({message: 'Account unmuted', context: 'toast'}))
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
ax.logger.error('Failed to unmute account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -157,27 +157,29 @@ let ProfileMenu = ({
|
||||
} else {
|
||||
try {
|
||||
await queueMute()
|
||||
Toast.show(_(msg({message: 'Account muted', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
Toast.show(l({message: 'Account muted', context: 'toast'}))
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
ax.logger.error('Failed to mute account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ax, profile.viewer?.muted, queueUnmute, _, queueMute])
|
||||
}, [ax, profile.viewer?.muted, queueUnmute, l, queueMute])
|
||||
|
||||
const blockAccount = useCallback(async () => {
|
||||
if (profile.viewer?.blocking) {
|
||||
try {
|
||||
await queueUnblock()
|
||||
Toast.show(_(msg({message: 'Account unblocked', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
Toast.show(l({message: 'Account unblocked', context: 'toast'}))
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
ax.logger.error('Failed to unblock account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -185,56 +187,59 @@ let ProfileMenu = ({
|
||||
} else {
|
||||
try {
|
||||
await queueBlock()
|
||||
Toast.show(_(msg({message: 'Account blocked', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
Toast.show(l({message: 'Account blocked', context: 'toast'}))
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
ax.logger.error('Failed to block account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ax, profile.viewer?.blocking, _, queueUnblock, queueBlock])
|
||||
}, [ax, profile.viewer?.blocking, l, queueUnblock, queueBlock])
|
||||
|
||||
const onPressFollowAccount = useCallback(async () => {
|
||||
try {
|
||||
await queueFollow()
|
||||
Toast.show(_(msg({message: 'Account followed', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
Toast.show(l({message: 'Account followed', context: 'toast'}))
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
ax.logger.error('Failed to follow account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [_, ax, queueFollow])
|
||||
}, [l, ax, queueFollow])
|
||||
|
||||
const onPressUnfollowAccount = useCallback(async () => {
|
||||
try {
|
||||
await queueUnfollow()
|
||||
Toast.show(_(msg({message: 'Account unfollowed', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
Toast.show(l({message: 'Account unfollowed', context: 'toast'}))
|
||||
} catch (err) {
|
||||
const e = err as Error
|
||||
if (e?.name !== 'AbortError') {
|
||||
ax.logger.error('Failed to unfollow account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), {
|
||||
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [_, ax, queueUnfollow])
|
||||
}, [l, ax, queueUnfollow])
|
||||
|
||||
const onPressReportAccount = useCallback(() => {
|
||||
reportDialogControl.open()
|
||||
}, [reportDialogControl])
|
||||
|
||||
const onPressShareATUri = useCallback(() => {
|
||||
shareText(`at://${profile.did}`)
|
||||
void shareText(`at://${profile.did}`)
|
||||
}, [profile.did])
|
||||
|
||||
const onPressShareDID = useCallback(() => {
|
||||
shareText(profile.did)
|
||||
void shareText(profile.did)
|
||||
}, [profile.did])
|
||||
|
||||
const onPressSearch = useCallback(() => {
|
||||
@@ -251,14 +256,14 @@ let ProfileMenu = ({
|
||||
return (
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`More options`)}>
|
||||
<Menu.Trigger label={l`More options`}>
|
||||
{({props}) => {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
{...props}
|
||||
testID="profileHeaderDropdownBtn"
|
||||
label={_(msg`More options`)}
|
||||
label={l`More options`}
|
||||
hitSlop={HITSLOP_20}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
@@ -267,7 +272,6 @@ let ProfileMenu = ({
|
||||
{statusNudgeActive && <Gradient style={[a.rounded_full]} />}
|
||||
<ButtonIcon icon={Ellipsis} size="sm" />
|
||||
</Button>
|
||||
|
||||
{statusNudgeActive && <Dot top={1} right={1} />}
|
||||
</>
|
||||
)
|
||||
@@ -278,9 +282,7 @@ let ProfileMenu = ({
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownShareBtn"
|
||||
label={
|
||||
IS_WEB ? _(msg`Copy link to profile`) : _(msg`Share via...`)
|
||||
}
|
||||
label={IS_WEB ? l`Copy link to profile` : l`Share via...`}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
loggedOutWarningPromptControl.open()
|
||||
@@ -301,7 +303,7 @@ let ProfileMenu = ({
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownSearchBtn"
|
||||
label={_(msg`Search posts`)}
|
||||
label={l`Search posts`}
|
||||
onPress={onPressSearch}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Search posts</Trans>
|
||||
@@ -320,14 +322,12 @@ let ProfileMenu = ({
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownFollowBtn"
|
||||
label={
|
||||
isFollowing
|
||||
? _(msg`Unfollow account`)
|
||||
: _(msg`Follow account`)
|
||||
isFollowing ? l`Unfollow account` : l`Follow account`
|
||||
}
|
||||
onPress={
|
||||
isFollowing
|
||||
? onPressUnfollowAccount
|
||||
: onPressFollowAccount
|
||||
? () => void onPressUnfollowAccount()
|
||||
: () => void onPressFollowAccount()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{isFollowing ? (
|
||||
@@ -343,7 +343,7 @@ let ProfileMenu = ({
|
||||
)}
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownStarterPackAddRemoveBtn"
|
||||
label={_(msg`Add to starter packs`)}
|
||||
label={l`Add to starter packs`}
|
||||
onPress={onPressAddToStarterPacks}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Add to starter packs</Trans>
|
||||
@@ -352,7 +352,7 @@ let ProfileMenu = ({
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||
label={_(msg`Add to lists`)}
|
||||
label={l`Add to lists`}
|
||||
onPress={onPressAddRemoveLists}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Add to lists</Trans>
|
||||
@@ -364,10 +364,10 @@ let ProfileMenu = ({
|
||||
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||
label={
|
||||
status.isDisabled
|
||||
? _(msg`Go live (disabled)`)
|
||||
? l`Go live (disabled)`
|
||||
: status.isActive
|
||||
? _(msg`Edit live status`)
|
||||
: _(msg`Go live`)
|
||||
? l`Edit live status`
|
||||
: l`Go live`
|
||||
}
|
||||
onPress={() => {
|
||||
if (status.isDisabled) {
|
||||
@@ -418,7 +418,7 @@ let ProfileMenu = ({
|
||||
(verification.viewer.hasIssuedVerification ? (
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownVerificationRemoveButton"
|
||||
label={_(msg`Remove verification`)}
|
||||
label={l`Remove verification`}
|
||||
onPress={() => verificationRemovePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Remove verification</Trans>
|
||||
@@ -428,7 +428,7 @@ let ProfileMenu = ({
|
||||
) : (
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownVerificationCreateButton"
|
||||
label={_(msg`Verify account`)}
|
||||
label={l`Verify account`}
|
||||
onPress={() => verificationCreatePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Verify account</Trans>
|
||||
@@ -444,10 +444,10 @@ let ProfileMenu = ({
|
||||
testID="profileHeaderDropdownMuteBtn"
|
||||
label={
|
||||
profile.viewer?.muted
|
||||
? _(msg`Unmute account`)
|
||||
: _(msg`Mute account`)
|
||||
? l`Unmute account`
|
||||
: l`Mute account`
|
||||
}
|
||||
onPress={onPressMuteAccount}>
|
||||
onPress={() => void onPressMuteAccount()}>
|
||||
<Menu.ItemText>
|
||||
{profile.viewer?.muted ? (
|
||||
<Trans>Unmute account</Trans>
|
||||
@@ -464,9 +464,9 @@ let ProfileMenu = ({
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownBlockBtn"
|
||||
label={
|
||||
profile.viewer
|
||||
? _(msg`Unblock account`)
|
||||
: _(msg`Block account`)
|
||||
profile.viewer?.blocking
|
||||
? l`Unblock account`
|
||||
: l`Block account`
|
||||
}
|
||||
onPress={() => blockPromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
@@ -485,7 +485,7 @@ let ProfileMenu = ({
|
||||
)}
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownReportBtn"
|
||||
label={_(msg`Report account`)}
|
||||
label={l`Report account`}
|
||||
onPress={onPressReportAccount}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Report account</Trans>
|
||||
@@ -503,7 +503,7 @@ let ProfileMenu = ({
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownShareATURIBtn"
|
||||
label={_(msg`Copy at:// URI`)}
|
||||
label={l`Copy at:// URI`}
|
||||
onPress={onPressShareATUri}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Copy at:// URI</Trans>
|
||||
@@ -512,7 +512,7 @@ let ProfileMenu = ({
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownShareDIDBtn"
|
||||
label={_(msg`Copy DID`)}
|
||||
label={l`Copy DID`}
|
||||
onPress={onPressShareDID}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Copy DID</Trans>
|
||||
@@ -524,12 +524,10 @@ let ProfileMenu = ({
|
||||
) : null}
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
|
||||
<StarterPackDialog
|
||||
control={addToStarterPacksDialogControl}
|
||||
targetDid={profile.did}
|
||||
/>
|
||||
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
subject={{
|
||||
@@ -537,44 +535,18 @@ let ProfileMenu = ({
|
||||
$type: 'app.bsky.actor.defs#profileViewDetailed',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
<BlockDialog
|
||||
control={blockPromptControl}
|
||||
title={
|
||||
profile.viewer?.blocking
|
||||
? _(msg`Unblock Account?`)
|
||||
: _(msg`Block Account?`)
|
||||
}
|
||||
description={
|
||||
profile.viewer?.blocking
|
||||
? _(
|
||||
msg`The account will be able to interact with you after unblocking.`,
|
||||
)
|
||||
: profile.associated?.labeler
|
||||
? _(
|
||||
msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`,
|
||||
)
|
||||
: _(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
)
|
||||
}
|
||||
onConfirm={blockAccount}
|
||||
confirmButtonCta={
|
||||
profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
|
||||
}
|
||||
confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'}
|
||||
profile={profile}
|
||||
onBlock={blockAccount}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={loggedOutWarningPromptControl}
|
||||
title={_(msg`Note about sharing`)}
|
||||
description={_(
|
||||
msg`This profile is only visible to logged-in users. It won't be visible to people who aren't signed in.`,
|
||||
)}
|
||||
title={l`Note about sharing`}
|
||||
description={l`This profile is only visible to logged-in users. It won't be visible to people who aren't signed in.`}
|
||||
onConfirm={onPressShare}
|
||||
confirmButtonCta={_(msg`Share anyway`)}
|
||||
confirmButtonCta={l`Share anyway`}
|
||||
/>
|
||||
|
||||
<VerificationCreatePrompt
|
||||
control={verificationCreatePromptControl}
|
||||
profile={profile}
|
||||
@@ -584,7 +556,6 @@ let ProfileMenu = ({
|
||||
profile={profile}
|
||||
verifications={currentAccountVerifications}
|
||||
/>
|
||||
|
||||
{status.isDisabled ? (
|
||||
<GoLiveDisabledDialog
|
||||
control={goLiveDisabledDialogControl}
|
||||
|
||||
Reference in New Issue
Block a user