android, desktop, ios: show chat name in delete / leave / clear confirmation dialogs (#7021)

* android, desktop, ios: show chat name in delete / leave / clear confirmation dialogs

The dialogs confirming delete contact, delete/leave group/channel and
clear chat now show the chat's name on its own line above the existing
warning, so the user can see which chat the destructive action will
affect.

Pure code change: no new translation strings, no signature changes, no
new helpers. The name is read via existing displayName accessors on
GroupInfo / Contact / ChatInfo.

clearNoteFolderDialog is intentionally unchanged - the notes folder is a
single-instance object and its existing warning already identifies it.

* android, desktop, ios: also show chat name when deleting pending connection

deleteContactConnectionAlert was missed in the original inventory.
Same pattern as the other dispatchers - prepend the connection's
displayName on its own line above the existing warning - so a user
who set a custom name on a pending connection can see which one
they are about to delete.

* android: use <br> instead of \n for newline in delete confirmation dialog body

On Android, the alert body goes through HtmlCompat.fromHtml which
treats the input as HTML and collapses literal \n to a single space -
so "Tech Talk\n\nGroup will be deleted..." rendered as
"Tech Talk Group will be deleted...". Switch to <br><br>, which both
HtmlCompat (Android) and the Desktop parser at Utils.desktop.kt:75
correctly render as a newline.

* android, desktop: skip HTML parsing for delete confirmation dialog text

Add parseHtml: Boolean = true parameter to showAlertDialog and
showAlertDialogButtonsColumn; when false, the body text is wrapped as
AnnotatedString and routed through the existing AnnotatedString
AlertContent overload, bypassing escapedHtmlToAnnotatedString entirely.

The 8 dispatchers that embed the user-controlled chat displayName now
opt out (parseHtml = false). This means:
 - displayName is rendered as literal text - no HTML interpretation,
   so a contact whose alias is "<b>X</b>" or "<font color=..." no
   longer renders bold or coloured in the confirmation dialog
 - the platforms align: both iOS and Kotlin now use plain "\n\n" for
   the separator (no more <br><br> Kotlin-only workaround)
 - removes coupling to escapedHtmlToAnnotatedString in this path

* android, desktop: shorten parseHtml comment in AlertManager

* remove extra text

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Narasimha-sc
2026-06-08 12:13:06 +01:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent 589d834f83
commit 503d091ec4
9 changed files with 620 additions and 21 deletions
@@ -577,7 +577,7 @@ struct ChatInfoView: View {
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) {
Task {
await clearChat(chat)
@@ -1185,6 +1185,7 @@ private func deleteContactOrConversationDialog(
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Delete contact?"),
message: Text(contact.displayName),
buttons: [
.destructive(Text("Only delete conversation")) {
deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .messages, dismissToChatList, showAlert)
@@ -1331,6 +1332,7 @@ private func deleteContactWithoutConversation(
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Confirm contact deletion?"),
message: Text(contact.displayName),
buttons: [
.destructive(Text("Delete and notify contact")) {
deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: true), dismissToChatList, showAlert)
@@ -1355,6 +1357,7 @@ private func deleteNotReadyContact(
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Confirm contact deletion?"),
message: Text(contact.displayName),
buttons: [
.destructive(Text("Confirm")) {
deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert)
@@ -845,7 +845,7 @@ struct GroupChatInfoView: View {
let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
return Alert(
title: Text(label),
message: deleteGroupAlertMessage(groupInfo),
message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
@@ -867,7 +867,7 @@ struct GroupChatInfoView: View {
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) {
Task {
await clearChat(chat)
@@ -889,7 +889,7 @@ struct GroupChatInfoView: View {
)
return Alert(
title: Text(titleLabel),
message: Text(messageLabel),
message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel),
primaryButton: .destructive(Text("Leave")) {
Task {
await leaveGroup(chat.chatInfo.apiId)
@@ -568,7 +568,7 @@ struct ChatListNavLink: View {
let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
return Alert(
title: Text(label),
message: deleteGroupAlertMessage(groupInfo),
message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo),
primaryButton: .destructive(Text("Delete")) {
Task { await deleteChat(chat) }
},
@@ -600,7 +600,7 @@ struct ChatListNavLink: View {
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) {
Task { await clearChat(chat) }
},
@@ -630,7 +630,7 @@ struct ChatListNavLink: View {
)
return Alert(
title: Text(titleLabel),
message: Text(messageLabel),
message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel),
primaryButton: .destructive(Text("Leave")) {
Task { await leaveGroup(groupInfo.groupId) }
},
@@ -701,10 +701,10 @@ func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert {
func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert {
Alert(
title: Text("Delete pending connection?"),
message:
contactConnection.initiated
? Text("The contact you shared this link with will NOT be able to connect!")
: Text("The connection you accepted will be cancelled!"),
message: Text(contactConnection.displayName + "\n\n")
+ (contactConnection.initiated
? Text("The contact you shared this link with will NOT be able to connect!")
: Text("The connection you accepted will be cancelled!")),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
@@ -248,6 +248,8 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? =
private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
text = contact.displayName,
parseHtml = false,
buttons = {
Column {
// Only delete conversation
@@ -306,7 +308,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
parseHtml = false,
buttons = {
Column {
// Keep conversation toggle
@@ -361,7 +364,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C
private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.confirm_delete_contact_question),
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
parseHtml = false,
buttons = {
Column {
// Delete and notify contact
@@ -417,7 +421,8 @@ private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, c
private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.confirm_delete_contact_question),
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
parseHtml = false,
buttons = {
// Confirm
SectionItemView({
@@ -492,7 +497,8 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDe
fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.clear_chat_question),
text = generalGetString(MR.strings.clear_chat_warning),
text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}",
parseHtml = false,
confirmText = generalGetString(MR.strings.clear_verb),
onConfirm = { controller.clearChat(chat, close) },
destructive = true,
@@ -199,7 +199,8 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl
}
AlertManager.shared.showAlertDialog(
title = generalGetString(titleId),
text = generalGetString(messageId),
text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
parseHtml = false,
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = {
withBGApi {
@@ -233,7 +234,8 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
MR.strings.you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved
AlertManager.shared.showAlertDialog(
title = generalGetString(titleId),
text = generalGetString(messageId),
text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
parseHtml = false,
confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = {
withLongRunningApi(60_000) {
@@ -772,10 +772,11 @@ fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatMod
fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_pending_connection__question),
text = generalGetString(
text = "${connection.displayName}\n\n" + generalGetString(
if (connection.initiated) MR.strings.contact_you_shared_link_with_wont_be_able_to_connect
else MR.strings.connection_you_accepted_will_be_cancelled
),
parseHtml = false,
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = {
withBGApi {
@@ -75,6 +75,8 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
belowTextContent: @Composable (() -> Unit) = {},
// When false, [text] is rendered as literal text — use for user-controlled content.
parseHtml: Boolean = true,
buttons: @Composable () -> Unit,
) {
showAlert {
@@ -82,8 +84,14 @@ class AlertManager {
onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() },
title = alertTitle(title),
buttons = {
AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) {
buttons()
if (parseHtml) {
AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) {
buttons()
}
} else {
AlertContent(text?.let { AnnotatedString(it) }, hostDevice, extraPadding = true) {
buttons()
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
@@ -122,13 +130,15 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false,
hostDevice: Pair<Long?, String>? = null,
// When false, [text] is rendered as literal text — use for user-controlled content.
parseHtml: Boolean = true,
) {
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
buttons = {
AlertContent(text, hostDevice, true) {
val buttonRow: @Composable () -> Unit = {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween
@@ -149,6 +159,11 @@ class AlertManager {
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
}
if (parseHtml) {
AlertContent(text, hostDevice, true, content = buttonRow)
} else {
AlertContent(text?.let { AnnotatedString(it) }, hostDevice, true, content = buttonRow)
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
@@ -0,0 +1,323 @@
# Implementation plan — chat name on its own line in delete/leave/clear dialogs
Follows the product spec in
[`delete-leave-dialog-with-profile.md`](./delete-leave-dialog-with-profile.md).
Pure code change — zero string additions, zero new helpers, zero
signature changes. Each call site edits one argument: the `text =` /
`message:` value gains `"${displayName}\n\n"` prepended to the
existing localized warning (or, where there is no current body, the
chat name becomes the new body).
One commit per platform.
## Commit 1 — Kotlin
**Files touched:**
- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt`
— adds `parseHtml: Boolean = true` to `showAlertDialog` and
`showAlertDialogButtonsColumn`. When `false`, the body text is wrapped
as `AnnotatedString` and routed through the existing AnnotatedString
`AlertContent` overload, which does NOT call
`escapedHtmlToAnnotatedString`. Default stays `true` so existing
callers are unaffected.
- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt`
- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt`
- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt`
— adds the previously-missed `deleteContactConnectionAlert`
dispatcher to the coverage (pending contact connections).
Every Kotlin call site that prepends the chat name sets
`parseHtml = false`, so `displayName` is never HTML-interpreted.
### 1.1 — `deleteGroupDialog` (`GroupChatInfoView.kt:182`)
```diff
fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
val chatInfo = chat.chatInfo
val titleId = /* unchanged */
val messageId = /* unchanged */
AlertManager.shared.showAlertDialog(
title = generalGetString(titleId),
- text = generalGetString(messageId),
+ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = { /* unchanged */ },
destructive = true,
)
}
```
### 1.2 — `leaveGroupDialog` (`GroupChatInfoView.kt:222`)
```diff
fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
val titleId = /* unchanged */
val messageId = /* unchanged */
AlertManager.shared.showAlertDialog(
title = generalGetString(titleId),
- text = generalGetString(messageId),
+ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = { /* unchanged */ },
destructive = true,
)
}
```
Signature unchanged. No caller updates. `groupInfo.displayName` is
already available on the existing parameter (`ChatModel.kt:2142`).
### 1.3 — `clearChatDialog` (`ChatInfoView.kt:492`)
```diff
fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.clear_chat_question),
- text = generalGetString(MR.strings.clear_chat_warning),
+ text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}",
confirmText = generalGetString(MR.strings.clear_verb),
onConfirm = { controller.clearChat(chat, close) },
destructive = true,
)
}
```
### 1.4 — Contact-delete dispatchers (`ChatInfoView.kt`)
Four functions. `deleteContactOrConversationDialog` (line 248) has
no existing `text =`, so the chat name becomes the new body. The
other three already have a `text =`, so the name is prepended.
All four already have `contact: Contact` as a parameter, so
`contact.displayName` is used directly (same value as
`chat.chatInfo.displayName` for a direct chat, shorter expression).
```diff
// deleteContactOrConversationDialog — line 248
private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
+ text = contact.displayName,
buttons = { /* unchanged */ }
)
}
```
```diff
// deleteActiveContactDialog — line 304
private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) {
val contactDeleteMode = mutableStateOf<ContactDeleteMode>(ContactDeleteMode.Full())
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
- text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
+ text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
buttons = { /* unchanged */ }
)
}
```
Same diff for `deleteContactWithoutConversation` (line 361) and
`deleteNotReadyContact` (line 417) — both use
`delete_contact_cannot_undo_warning`. Neither takes `contact` as
a parameter, so the name is read via `chat.chatInfo.displayName`
(which resolves to `contact.displayName` because these dispatchers
are only reached for `ChatInfo.Direct` chats). Their titles
(`confirm_delete_contact_question`) stay unchanged — the
not-ready / no-conversation paths keep their distinct title.
## Commit 2 — iOS
**Files touched:**
- `apps/ios/Shared/Views/ChatList/ChatListNavLink.swift`
- `apps/ios/Shared/Views/Chat/ChatInfoView.swift`
- `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift`
### 2.1 — `deleteGroupAlert` (two locations)
`Views/Chat/Group/GroupChatInfoView.swift:835` and
`Views/ChatList/ChatListNavLink.swift:567` get the same diff.
`deleteGroupAlertMessage(_:)` already returns a `Text` containing
the localized warning — concatenate to it.
```diff
private func deleteGroupAlert() -> Alert {
let label: LocalizedStringKey = /* unchanged */
return Alert(
title: Text(label),
- message: deleteGroupAlertMessage(groupInfo),
+ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + deleteGroupAlertMessage(groupInfo),
primaryButton: .destructive(Text("Delete")) { /* unchanged */ },
secondaryButton: .cancel()
)
}
```
`Text(chat.chatInfo.displayName)` resolves to `Text(_ content: some StringProtocol)`
(the runtime-string overload — no localization lookup, matches
codebase convention: `ChatView.swift:984`, `ChatInfoToolbar.swift:49`,
`SettingsView.swift:540`). `Text(verbatim: "\n\n")` is the literal
separator, matching the codebase convention that reserves
`verbatim:` for fixed punctuation (`ContextItemView.swift:88` is
the textbook example: `Text(chatLink.displayName) + Text(verbatim: " - ")`).
The third term `Text(messageLabel)` keeps the existing
`LocalizedStringKey` lookup.
### 2.2 — `leaveGroupAlert` (two locations)
`Views/Chat/Group/GroupChatInfoView.swift:872` and
`Views/ChatList/ChatListNavLink.swift:622`:
```diff
private func leaveGroupAlert() -> Alert {
let titleLabel: LocalizedStringKey = /* unchanged */
let messageLabel: LocalizedStringKey = /* unchanged */
return Alert(
title: Text(titleLabel),
- message: Text(messageLabel),
+ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text(messageLabel),
primaryButton: .destructive(Text("Leave")) { /* unchanged */ },
secondaryButton: .cancel()
)
}
```
### 2.3 — `clearChatAlert` (three locations)
`Views/Chat/ChatInfoView.swift:577`,
`Views/Chat/Group/GroupChatInfoView.swift:858`,
`Views/ChatList/ChatListNavLink.swift:600`:
```diff
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
- message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
+ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) { /* unchanged */ },
secondaryButton: .cancel()
)
}
```
### 2.4 — Contact-delete action sheets
Three functions in `Views/Chat/ChatInfoView.swift`. None currently
pass `message:` to `ActionSheet`; we add it. `ActionSheet`'s
`message:` is an optional second parameter that SwiftUI already
supports.
All three functions have `contact: Contact` in scope. Use bare
`Text(contact.displayName)` (resolves to the `StringProtocol`
overload, no localization lookup, matches codebase convention).
Add only the name as `message:` — these ActionSheets had no
message before, so adding any additional warning would be new
behavior beyond the stated goal.
**`deleteContactOrConversationDialog`** (line 1177):
```diff
private func deleteContactOrConversationDialog(
_ chat: Chat, _ contact: Contact, _ dismissToChatList: Bool,
_ showAlert: @escaping (SomeAlert) -> Void,
_ showActionSheet: @escaping (SomeActionSheet) -> Void,
_ showSheetContent: @escaping (SomeSheet<AnyView>) -> Void
) {
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Delete contact?"),
+ message: Text(contact.displayName),
buttons: [ /* unchanged */ ]
),
id: "deleteContactOrConversationDialog"
))
}
```
**`deleteContactWithoutConversation`** (line 1324):
```diff
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Confirm contact deletion?"),
+ message: Text(contact.displayName),
buttons: [ /* unchanged */ ]
),
id: "deleteContactWithoutConversation"
))
```
**`deleteNotReadyContact`** (line 1348) — same:
```diff
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Confirm contact deletion?"),
+ message: Text(contact.displayName),
buttons: [ /* unchanged */ ]
),
id: "deleteNotReadyContact"
))
```
### 2.5 — `DeleteActiveContactDialog` sheet (line 1282) unchanged
The secondary multi-option sheet is reached only after the user
confirms "Delete contact" in the previous action sheet — which now
shows the name. The sheet itself remains as-is.
## Verification
For each platform, exercise every entry point and confirm the
body reads `<chat name>` on its own line followed by the existing
warning:
- Android & Desktop:
- Chat list swipe — direct contact, group, channel, business chat
→ delete / clear / leave. (Note folder's clear dialog is
intentionally unchanged — `clearNoteFolderDialog` excluded.)
- Chat info screens — "Delete contact" / "Delete group" / "Delete
channel" / "Clear conversation" / "Leave …" rows.
- Contact list (`ContactListNavView.kt:148`) — "Delete contact"
action shows the name in entry-point dialog and toggle dialog.
- Multi-option contact-delete path: entry dialog (now has a name
body where it had none) → toggle dialog (name above the
warning) → success.
- iOS:
- Same matrix from chat list swipe and chat info screens.
- Action-sheet contact-delete paths show the name as the
`message:` line on iPhone and iPad.
Edge cases:
- Long chat name — alert containers wrap automatically; the body
occupies 3+ lines. Confirm with a chat renamed to ~40 characters.
- Special characters (emoji, RTL, double quotes) — render literally
via string interpolation, no format-substitution involved.
- Empty `displayName` — does not occur in practice (`NamedChat`
enforces non-empty via `localAlias.ifEmpty { profile.displayName }`).
Diff-level checks:
- `git diff '*strings.xml' '*Localizable.strings'` returns zero
hunks. Pure code change.
- `git diff --stat` shows ~5 files total: two Kotlin dispatcher
files, three iOS view files.
- Cancel/confirm flows behave exactly as before — same API calls,
same model updates, same navigation.
## Out of scope
- Profile picture / avatar in dialogs — excluded by product decision.
- Refactoring the iOS duplication between `ChatListNavLink` and
`GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO` at
`GroupChatInfoView.swift:834`).
- Pre-existing wording divergence between Kotlin's "Clear chat?"
and iOS's "Clear conversation?". Both platforms keep their
titles.
- "Delete invitation" at `ChatListNavLink.swift:236` — has no
confirmation dialog (direct call to `deleteChat(chat)`); nothing
to modify.
- Bolding the chat name. SwiftUI `Text + Text` supports `.bold()`
on the first term, but Jetpack Compose `AlertDialog` text is a
single unstyled string — keeping both unstyled preserves parity.
+249
View File
@@ -0,0 +1,249 @@
# Show chat name in delete / leave / clear confirmation dialogs
## Goal
The current delete-contact, delete-group, delete-channel, leave-group,
leave-channel and clear-chat confirmations are generic. From a long
chat list, swiping on a row and triggering one of these actions opens
a dialog whose title is "Delete group?", "Leave channel?", "Clear
conversation?" — with no indication of *which* chat is the target. A
user can easily act on the wrong chat.
The fix: include the chat's display name in the dialog body, on a line
of its own above the existing warning text. Nothing else changes —
same title, same warning text, same buttons, same colors, same dialog
shape. No profile picture, no layout changes, no new helpers, no new
translation strings.
We deliberately do NOT reuse the open-chat-link alert layout (centered
profile image + name + open-chat button). That layout is the *invite*
flow's identity; repurposing it for destructive confirmations would
confuse the two flows visually. The minimum change that solves the
"which chat?" problem is putting the name in the body text.
## Why body, not title; why no new strings
The title carries the action ("Delete group?", "Leave channel?"). The
body carries the consequences ("Group will be deleted for all
members…"). The chat name belongs with the body — it is the subject
of the consequence, not part of the question.
Adding the name to the title would require new format-string variants
(`delete_group_named_question` etc.) and per-locale re-translation.
Putting the name on its own line in the body is a pure code change —
the existing translated warnings are concatenated with the chat name
in code:
```
Tech Talk
Group will be deleted for all members - this cannot be undone!
```
The display name appears first because the user wants to confirm
*which* chat before reading *what* will happen. The blank line between
the name and the warning makes the name visually distinct.
## Current state
### Multiplatform (Kotlin / Android / Desktop)
All eight dialogs go through `AlertManager.shared.showAlertDialog` or
`showAlertDialogButtonsColumn`:
- `deleteGroupDialog``views/chat/group/GroupChatInfoView.kt:182`
- `leaveGroupDialog``views/chat/group/GroupChatInfoView.kt:222`
- `clearChatDialog``views/chat/ChatInfoView.kt:492`
- `deleteContactOrConversationDialog``views/chat/ChatInfoView.kt:248`
- `deleteActiveContactDialog``views/chat/ChatInfoView.kt:304`
- `deleteContactWithoutConversation``views/chat/ChatInfoView.kt:361`
- `deleteNotReadyContact``views/chat/ChatInfoView.kt:417`
- `deleteContactConnectionAlert``views/chatlist/ChatListNavLinkView.kt:772`
(deletes a pending contact connection; takes a `PendingContactConnection`
whose `displayName` reflects any custom name the user set)
Call sites (chat-info screens, chat-list swipe / overflow, contact
list) funnel through these dispatcher functions.
### iOS (Swift)
Two SwiftUI patterns are used:
- SwiftUI `Alert` with `primaryButton: .destructive` / `.cancel()`:
- `deleteGroupAlert``Views/ChatList/ChatListNavLink.swift:567`,
`Views/Chat/Group/GroupChatInfoView.swift:835`
- `leaveGroupAlert``Views/ChatList/ChatListNavLink.swift:622`,
`Views/Chat/Group/GroupChatInfoView.swift:872`
- `clearChatAlert``Views/ChatList/ChatListNavLink.swift:600`,
`Views/Chat/ChatInfoView.swift:577`,
`Views/Chat/Group/GroupChatInfoView.swift:858`
- SwiftUI `ActionSheet`:
- `deleteContactOrConversationDialog`
`Views/Chat/ChatInfoView.swift:1177`
- `deleteContactWithoutConversation`
`Views/Chat/ChatInfoView.swift:1324`
- `deleteNotReadyContact``Views/Chat/ChatInfoView.swift:1348`
`Alert(message:)` accepts `Text`, and `ActionSheet(message:)` (an
existing optional parameter not used today) accepts `Text` too — so
the name can be added by composing the existing message string with
`"\n\n"` and the chat name. No widget changes.
## Design
| Dialog | Body today | Body after |
|---|---|---|
| Delete group | `Group will be deleted for all members this cannot be undone!` | `Tech Talk` + blank line + existing text |
| Delete channel | `Channel will be deleted for all subscribers this cannot be undone!` | `SimpleX news` + blank line + existing text |
| Leave group | `You will stop receiving messages from this group. …` | `Tech Talk` + blank line + existing text |
| Clear chat | `All messages will be deleted this cannot be undone! …` | `Alice` + blank line + existing text |
| Delete contact (entry sheet) | *(no body today — title only + buttons)* | `Alice` (becomes the body) |
| Delete contact (active variant) | `Contact will be deleted this cannot be undone!` | `Alice` + blank line + existing text |
| Confirm contact deletion (not-ready / no-conversation) | `Contact will be deleted this cannot be undone!` | `Alice` + blank line + existing text |
Title text is unchanged in every case. Existing titles
(`delete_contact_question`, `confirm_delete_contact_question`, etc.)
keep their semantic distinction — the "Confirm contact deletion?"
title still appears for the not-ready / no-conversation paths.
### Which name to use: `displayName`, not `chatViewName`
The chat list row labels chats with `cInfo.chatViewName`
(`ChatPreviewView.kt:87`), defined as:
```kotlin
val chatViewName: String
get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
```
The dialog uses `chatInfo.displayName` (and `groupInfo.displayName`
for the leave dialog). For most chats these are identical:
- If `localAlias` is set, both resolve to the alias.
- If `displayName == fullName` (or `fullName` is empty), both resolve
to `displayName`.
For a contact with distinct display name and full name (no alias),
the row would show `alice / Alice Smith` while the dialog shows
`alice`. Acceptable: `displayName` is the recognizable identifier,
shorter, and the dialog format (single line above the warning)
benefits from concision. Two-part identifiers in the dialog would
crowd the layout.
### `clearNoteFolderDialog` is excluded
The local notes folder is a single-instance object — there is only
one per user — and its existing warning text already names it
unambiguously. Adding the display name on its own line would be
pure redundancy. Skipped.
## Changes
### Multiplatform (Kotlin)
Each dispatcher function changes one argument: the `text =` parameter
passed to `AlertManager.shared.showAlertDialog` /
`showAlertDialogButtonsColumn`. The new value is the chat name + two
newlines + the existing message text:
```kotlin
text = "${chatInfo.displayName}\n\n${generalGetString(messageId)}",
parseHtml = false,
```
`parseHtml = false` is a new boolean parameter added to both alert
helpers. It bypasses `escapedHtmlToAnnotatedString` so the
user-controlled `displayName` is rendered as literal text, never
interpreted as HTML markup (`<b>`, `<font>`, `&`, etc.). The default
remains `true`; only our delete-confirmation dispatchers opt out.
For `leaveGroupDialog` the source is `groupInfo.displayName` (the
function already takes `groupInfo` — no signature change needed,
no caller updates needed).
For `deleteGroupDialog`, also `groupInfo.displayName`, for consistency
with `leaveGroupDialog` (both have `groupInfo` already in scope).
For `deleteContactOrConversationDialog`, which has no `text =`
parameter today, add `text = chatInfo.displayName` (no concatenation
needed — the dialog had no body text before).
### iOS
Each of the eight call sites changes one argument: the `message:`
parameter passed to `Alert(…)` or `ActionSheet(…)`. The new value
composes the chat name with the existing localized message string:
```swift
message: Text("\(chat.chatInfo.displayName)\n\n\(existingMessage)"),
```
For the three `ActionSheet` sites that have no `message:` today, add
`message: Text(chat.chatInfo.displayName)`.
## Out of scope
- Profile picture / avatar in any of these dialogs — excluded by
decision: the open-chat-link alert owns that layout, and reusing
it for destructive confirmations conflates two semantically
different flows.
- The pre-existing wording divergence between Kotlin's
`clear_chat_question` ("Clear chat?") and iOS's "Clear
conversation?". Both platforms keep their existing titles.
- Refactoring the iOS duplication between `ChatListNavLink` and
`GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO reuse
this and clearChatAlert with ChatInfoView` at
`GroupChatInfoView.swift:834`).
- "Delete invitation" at `ChatListNavLink.swift:236` — goes through
`deleteChat(chat)` directly with no confirmation dialog. No dialog
to modify.
- Bolding the chat name on its own line. SwiftUI `Text` concatenation
supports `.bold()`; Jetpack Compose `AlertDialog` text is a single
string. Keep both platforms unstyled for parity.
## Verification
Per platform, exercise every entry point and confirm the dialog body
reads `<chat name>` on its own line followed by a blank line followed
by the existing warning:
- Android & Desktop:
- Chat list swipe — direct contact, group, channel, business chat
→ delete / clear / leave actions. (Note folder's clear dialog
is intentionally unchanged.)
- Chat info screens — "Delete contact" / "Delete group" / "Delete
channel" / "Clear conversation" / "Leave …" rows.
- Contact list (`ContactListNavView.kt:148`) — "Delete contact"
action.
- The multi-option contact-delete path: entry dialog (now has a
name body where it had none) → toggle dialog (name above the
warning) → success.
- iOS:
- Same matrix from chat list swipe and chat info screens.
- Action-sheet contact-delete paths show the name as the
`message:` line.
Edge cases:
- Long chat name (40+ chars) — alert containers wrap automatically;
body now occupies 3+ lines (name on 2, blank line, warning on 1+).
Confirm via a chat renamed to a long string.
- Special characters in name (emoji, RTL text, double quotes) —
render literally because the substitution is string concatenation,
not format expansion. A contact named `Bob "the builder"` displays
as `Bob "the builder"` on its own line. No quoting/escaping issue.
- Empty `displayName` would render an empty first line above the
warning. In practice `displayName` is non-empty (the `NamedChat`
interface enforces it via `localAlias.ifEmpty { profile.displayName }`);
no defensive trimming added.
Diff-level checks:
- `git diff strings.xml` and `git diff '*Localizable.strings'` show
zero hunks. The change is pure code.
- `git diff --stat` shows each platform touched in 24 files:
the dispatcher file(s) on Kotlin (`ChatInfoView.kt`,
`GroupChatInfoView.kt`), and the SwiftUI views holding the
alert/sheet builders on iOS.
- Behavior is unchanged. Cancel returns to the prior screen;
confirm performs the same destructive API call as before.