Show message when chat is locked or ended (#10413)

This commit is contained in:
DS Boyce
2026-05-06 10:42:54 -07:00
committed by GitHub
parent 945d8b229d
commit c91eae3720
5 changed files with 287 additions and 20 deletions
+1 -8
View File
@@ -15,7 +15,6 @@ import {
Unlock_Stroke2_Corner2_Rounded as UnlockIcon,
} from '#/components/icons/Lock'
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
export type SystemMessageInfo = {
message: MessageDescriptor
@@ -69,13 +68,7 @@ export function getSystemMessageInfo(
} else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) {
return {Icon: UnlockIcon, message: msg`Chat unlocked`}
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) {
const name = getReferredDisplayName(data.lockedBy, relatedProfiles)
return {
Icon: XIcon,
message: name
? msg`${name} ended the group chat`
: msg`This group chat was ended`,
}
return {Icon: LockIcon, message: msg`Chat ended`}
} else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
return {
Icon: PencilIcon,
+23 -12
View File
@@ -51,6 +51,8 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_INTERNAL, IS_LIQUID_GLASS, IS_WEB} from '#/env'
import {ChatDisabled} from './components/ChatDisabled'
import {ChatEnded} from './components/ChatEnded'
import {ChatLocked} from './components/ChatLocked'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
@@ -262,6 +264,26 @@ function InnerReady({
const header = <MessagesListHeader convo={convo} />
let footer: React.ReactNode = null
if (isDisabled) {
footer = <ChatDisabled />
} else if (convo && primaryMember && primaryMemberModeration?.blocked) {
footer = (
<MessagesListBlockedFooter
recipient={primaryMember}
convoId={convo.view.id}
hasMessages={hasMessages}
moderation={primaryMemberModeration}
/>
)
} else if (convo?.kind === 'group') {
if (convo.details.lockStatus === 'locked') {
footer = <ChatLocked convo={convo} />
} else if (convo.details.lockStatus === 'locked-permanently') {
footer = <ChatEnded convo={convo} />
}
}
return (
<>
{IS_LIQUID_GLASS ? (
@@ -280,18 +302,7 @@ function InnerReady({
setHasScrolled={setHasScrolled}
hasAcceptOverride={!!params.accept}
transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0}
footer={
isDisabled ? (
<ChatDisabled />
) : convo && primaryMember && primaryMemberModeration?.blocked ? (
<MessagesListBlockedFooter
recipient={primaryMember}
convoId={convo.view.id}
hasMessages={hasMessages}
moderation={primaryMemberModeration}
/>
) : null
}
footer={footer}
/>
)}
@@ -0,0 +1,79 @@
import {Pressable} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {StackActions, useNavigation} from '@react-navigation/native'
import {HITSLOP_10} from '#/lib/constants'
import {type NavigationProp} from '#/lib/routes/types'
import {logger} from '#/logger'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {type ConvoWithDetails} from '#/components/dms/util'
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {LeaveChatPrompt} from '../ConversationSettings/prompts'
import {ChatFooter} from './ChatFooter'
export function ChatEnded({
convo,
}: {
convo: Extract<ConvoWithDetails, {kind: 'group'}>
}) {
const t = useTheme()
const {t: l} = useLingui()
const leaveChatPrompt = Prompt.usePromptControl()
const navigation = useNavigation<NavigationProp>()
const {currentAccount} = useSession()
const primaryMember = convo?.primaryMember
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, {
onSuccess: () => {
// Settings > Chat > Chat list
navigation.dispatch(StackActions.pop(2))
},
onError: e => {
logger.error('Failed to leave group chat', {message: e})
Toast.show(l({message: 'Failed to leave group chat', context: 'toast'}), {
type: 'error',
})
},
})
return (
<ChatFooter heading={l`This chat has ended`} icon={CircleXIcon}>
{isOwner ? null : (
<>
<Pressable
accessibilityRole="button"
hitSlop={HITSLOP_10}
style={[a.mx_md]}
onPress={leaveChatPrompt.open}>
<Text
numberOfLines={1}
style={[
a.text_sm,
a.font_semi_bold,
a.leading_snug,
{
color: t.palette.negative_500,
},
]}>
<Trans>Leave chat</Trans>
</Text>
</Pressable>
<LeaveChatPrompt
control={leaveChatPrompt}
groupName={convo.details.name}
onConfirm={leaveConvo}
/>
</>
)}
</ChatFooter>
)
}
@@ -0,0 +1,70 @@
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
export function ChatFooter({
children,
heading,
subheading,
icon: Icon,
}: React.PropsWithChildren<{
heading: string
subheading?: string
icon: React.ComponentType<SVGIconProps>
}>) {
const t = useTheme()
return (
<View style={[a.p_lg]}>
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.p_md,
a.rounded_full,
t.atoms.bg_contrast_50,
]}>
<View
style={[
a.flex_row,
a.align_center,
{
minHeight: 32,
},
]}>
<Icon
size="md"
fill={t.atoms.text_contrast_medium.color}
style={[a.mr_sm]}
/>
<View>
<Text
numberOfLines={1}
style={[
a.text_sm,
a.font_semi_bold,
t.atoms.text_contrast_medium,
]}>
{heading}
</Text>
{subheading ? (
<Text
numberOfLines={1}
style={[
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{subheading}
</Text>
) : null}
</View>
</View>
{children}
</View>
</View>
)
}
@@ -0,0 +1,114 @@
import {Pressable} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {StackActions, useNavigation} from '@react-navigation/native'
import {HITSLOP_10} from '#/lib/constants'
import {type NavigationProp} from '#/lib/routes/types'
import {logger} from '#/logger'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {useLockConvo} from '#/state/queries/messages/lock-conversation'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {type ConvoWithDetails} from '#/components/dms/util'
import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {LeaveChatPrompt} from '../ConversationSettings/prompts'
import {ChatFooter} from './ChatFooter'
export function ChatLocked({
convo,
}: {
convo: Extract<ConvoWithDetails, {kind: 'group'}>
}) {
const t = useTheme()
const {t: l} = useLingui()
const leaveChatPrompt = Prompt.usePromptControl()
const navigation = useNavigation<NavigationProp>()
const {currentAccount} = useSession()
const primaryMember = convo?.primaryMember
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
const {mutate: lockConvo} = useLockConvo(convo.view.id, {
onSuccess: () => {
Toast.show(l({message: 'Group chat unlocked', context: 'toast'}))
},
onError: e => {
logger.error('Failed to unlock group chat', {message: e})
Toast.show(l`Failed to unlock group chat`, {type: 'error'})
},
})
const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, {
onSuccess: () => {
// Settings > Chat > Chat list
navigation.dispatch(StackActions.pop(2))
},
onError: e => {
logger.error('Failed to leave group chat', {message: e})
Toast.show(l({message: 'Failed to leave group chat', context: 'toast'}), {
type: 'error',
})
},
})
return (
<ChatFooter
heading={l`This chat is locked`}
subheading={l`No one can send messages`}
icon={LockIcon}>
{isOwner ? (
<>
<Pressable
accessibilityRole="button"
hitSlop={HITSLOP_10}
style={[a.mx_md]}
onPress={() => lockConvo({lock: false})}>
<Text
numberOfLines={1}
style={[
a.text_sm,
a.font_semi_bold,
a.leading_snug,
{
color: t.palette.negative_500,
},
]}>
<Trans>Unlock chat</Trans>
</Text>
</Pressable>
</>
) : (
<>
<Pressable
accessibilityRole="button"
hitSlop={HITSLOP_10}
style={[a.mx_md]}
onPress={leaveChatPrompt.open}>
<Text
numberOfLines={1}
style={[
a.text_sm,
a.font_semi_bold,
a.leading_snug,
{
color: t.palette.negative_500,
},
]}>
<Trans>Leave chat</Trans>
</Text>
</Pressable>
<LeaveChatPrompt
control={leaveChatPrompt}
groupName={convo.details.name}
onConfirm={leaveConvo}
/>
</>
)}
</ChatFooter>
)
}