diff --git a/src/components/dms/getSystemMessageInfo.ts b/src/components/dms/getSystemMessageInfo.ts
index c8eb54764a..fe9525d4b2 100644
--- a/src/components/dms/getSystemMessageInfo.ts
+++ b/src/components/dms/getSystemMessageInfo.ts
@@ -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,
diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx
index 450a7104e0..0146d3b8c9 100644
--- a/src/screens/Messages/Conversation.tsx
+++ b/src/screens/Messages/Conversation.tsx
@@ -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 =
+ let footer: React.ReactNode = null
+ if (isDisabled) {
+ footer =
+ } else if (convo && primaryMember && primaryMemberModeration?.blocked) {
+ footer = (
+
+ )
+ } else if (convo?.kind === 'group') {
+ if (convo.details.lockStatus === 'locked') {
+ footer =
+ } else if (convo.details.lockStatus === 'locked-permanently') {
+ footer =
+ }
+ }
+
return (
<>
{IS_LIQUID_GLASS ? (
@@ -280,18 +302,7 @@ function InnerReady({
setHasScrolled={setHasScrolled}
hasAcceptOverride={!!params.accept}
transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0}
- footer={
- isDisabled ? (
-
- ) : convo && primaryMember && primaryMemberModeration?.blocked ? (
-
- ) : null
- }
+ footer={footer}
/>
)}
diff --git a/src/screens/Messages/components/ChatEnded.tsx b/src/screens/Messages/components/ChatEnded.tsx
new file mode 100644
index 0000000000..1ee13b68d3
--- /dev/null
+++ b/src/screens/Messages/components/ChatEnded.tsx
@@ -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
+}) {
+ const t = useTheme()
+ const {t: l} = useLingui()
+
+ const leaveChatPrompt = Prompt.usePromptControl()
+
+ const navigation = useNavigation()
+ 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 (
+
+ {isOwner ? null : (
+ <>
+
+
+ Leave chat
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/src/screens/Messages/components/ChatFooter.tsx b/src/screens/Messages/components/ChatFooter.tsx
new file mode 100644
index 0000000000..97c5e53310
--- /dev/null
+++ b/src/screens/Messages/components/ChatFooter.tsx
@@ -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
+}>) {
+ const t = useTheme()
+
+ return (
+
+
+
+
+
+
+ {heading}
+
+ {subheading ? (
+
+ {subheading}
+
+ ) : null}
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/screens/Messages/components/ChatLocked.tsx b/src/screens/Messages/components/ChatLocked.tsx
new file mode 100644
index 0000000000..7e0d9ebafd
--- /dev/null
+++ b/src/screens/Messages/components/ChatLocked.tsx
@@ -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
+}) {
+ const t = useTheme()
+ const {t: l} = useLingui()
+
+ const leaveChatPrompt = Prompt.usePromptControl()
+
+ const navigation = useNavigation()
+ 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 (
+
+ {isOwner ? (
+ <>
+ lockConvo({lock: false})}>
+
+ Unlock chat
+
+
+ >
+ ) : (
+ <>
+
+
+ Leave chat
+
+
+
+ >
+ )}
+
+ )
+}