Files
simplex-chat/plans/delete-leave-dialog-with-profile-impl.md
T
503d091ec4 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>
2026-06-08 12:13:06 +01:00

324 lines
12 KiB
Markdown

# 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.