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

12 KiB

Implementation plan — chat name on its own line in delete/leave/clear dialogs

Follows the product spec in 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)

 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)

 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)

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

 // 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 */ }
   )
 }
 // 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.

 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:

 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:

 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):

 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):

     showActionSheet(SomeActionSheet(
         actionSheet: ActionSheet(
             title: Text("Confirm contact deletion?"),
+            message: Text(contact.displayName),
             buttons: [ /* unchanged */ ]
         ),
         id: "deleteContactWithoutConversation"
     ))

deleteNotReadyContact (line 1348) — same:

     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.