mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 15:48:54 +00:00
docs: update plan for iOS invitation notification dialog fix
This commit is contained in:
@@ -1,62 +1,101 @@
|
||||
# Fix: iOS notification for a chat invitation opens a white window instead of switching profile
|
||||
# Fix: iOS notification for a chat invitation opens a white window instead of opening the request
|
||||
|
||||
Date: 2026-07-21
|
||||
Branch: `nd/fix-ios-ntf-invitation-white-window`
|
||||
File changed: `apps/ios/Shared/Model/NtfManager.swift`
|
||||
Files changed:
|
||||
- `apps/ios/Shared/Model/NtfManager.swift`
|
||||
- `apps/ios/Shared/Model/ChatModel.swift`
|
||||
- `apps/ios/Shared/Views/ChatList/ChatListView.swift`
|
||||
- `apps/ios/Shared/Views/ChatList/ChatListNavLink.swift`
|
||||
|
||||
## Symptom
|
||||
|
||||
Reported on iOS, both v6.5 and v7 beta. With more than one user profile, tapping a
|
||||
"chat invitation" (contact request, "X wants to connect!") notification that belongs to a
|
||||
profile other than the active one shows a blank/white window instead of switching to that
|
||||
profile.
|
||||
profile and letting the user act on the request.
|
||||
|
||||
## Root cause (grounded in code)
|
||||
|
||||
`processNotificationResponse` (`NtfManager.swift:54-85`) does two independent things on a tap:
|
||||
`processNotificationResponse` (`NtfManager.swift:54-88`) does two independent things on a tap:
|
||||
|
||||
1. If the notification's `userInfo["userId"]` differs from the active user, it switches
|
||||
profile: `changeActiveUser(userId, viewPwd: nil)` (lines 59-63).
|
||||
profile: `changeActiveUser(userId, viewPwd: nil)`.
|
||||
2. It opens a chat only in the final `else` branch, and only when
|
||||
`content.targetContentIdentifier` is non-nil (lines 77-84).
|
||||
`content.targetContentIdentifier` is non-nil.
|
||||
|
||||
Two facts combine to produce the bug:
|
||||
|
||||
- A contact-request notification is created with `targetContentIdentifier: nil`
|
||||
(`SimpleXChat/Notifications.swift:39`). Its chat id lives in `userInfo["chatId"]` and is
|
||||
consulted only by the "Accept"-button branch (`action == ntfActionAcceptContact`,
|
||||
line 64), never on a plain body tap (default action). So none of the open-chat branches run.
|
||||
- `changeActiveUser` -> `getUserChatData` -> `updateChats` (`ChatModel.swift:628-639`)
|
||||
replaces the `chats` array but never resets `chatModel.chatId`.
|
||||
(`SimpleXChat/Notifications.swift:39`). Its chat id lives in `userInfo["chatId"]` and was
|
||||
consulted only by the "Accept"-button branch (`action == ntfActionAcceptContact`),
|
||||
never on a plain body tap (default action). So none of the open-chat branches ran.
|
||||
- `changeActiveUser` -> `getUserChatData` -> `updateChats` replaces the `chats` array but
|
||||
never resets `chatModel.chatId`.
|
||||
|
||||
Navigation is driven purely by `chatModel.chatId != nil` (`ChatListView.swift:190-198`).
|
||||
So when a chat is open in the previous profile and the user taps an invitation notification
|
||||
for another profile, the profile is switched underneath a ChatView still bound to the old
|
||||
profile's `chatId` (now absent from the new `chats`), and that view renders blank — the
|
||||
"white window". Message notifications avoid this because their `else` branch calls
|
||||
Navigation is driven purely by `chatModel.chatId != nil` (`ChatListView.swift`). So when a
|
||||
chat is open in the previous profile and the user taps an invitation notification for another
|
||||
profile, the profile is switched underneath a ChatView still bound to the old profile's
|
||||
`chatId` (now absent from the new `chats`), and that view renders blank — the "white window".
|
||||
Message notifications avoid this because their `else` branch calls
|
||||
`loadOpenChat(targetContentIdentifier)`, which resets `chatId` to a valid chat.
|
||||
|
||||
This path is unique to notifications: the in-app profile switch (UserPicker) is only
|
||||
reachable from the chat list, where `chatId` is already nil.
|
||||
### Why a contact request cannot just be "opened" like a message
|
||||
|
||||
Unlike a message chat, a contact request has **no openable chat view**. The notification
|
||||
carries the legacy contact-request chat id (`contactRequest.id` = `"<@..."`,
|
||||
`ChatTypes.swift:331`), and that `ChatInfo.contactRequest` row is not navigable via
|
||||
`chatModel.chatId` — it renders as a `ContactRequestView` with an `onTapGesture` that shows an
|
||||
accept/reject confirmation dialog (`ChatListNavLink.swift`). Setting `chatId` to it would
|
||||
render blank. So the fix cannot open a chat; it has to surface that same dialog.
|
||||
|
||||
## Fix
|
||||
|
||||
When a notification tap switches the active user, clear the stale open chat so the UI lands
|
||||
on the switched profile's chat list:
|
||||
Two parts, both scoped to the notification path:
|
||||
|
||||
```swift
|
||||
changeActiveUser(userId, viewPwd: nil)
|
||||
chatModel.chatId = nil
|
||||
```
|
||||
1. **Avoid the blank window.** When a notification tap switches the active user, clear the
|
||||
stale open chat so the UI lands on the switched profile's chat list:
|
||||
|
||||
```swift
|
||||
changeActiveUser(userId, viewPwd: nil)
|
||||
chatModel.chatId = nil
|
||||
```
|
||||
|
||||
2. **Open the request.** A plain body tap (default action) on a contact-request notification
|
||||
now surfaces the accept/reject dialog, instead of doing nothing:
|
||||
|
||||
```swift
|
||||
} else if action == UNNotificationDefaultActionIdentifier,
|
||||
case .contactRequest = chatModel.getChat(chatId)?.chatInfo {
|
||||
chatModel.chatId = nil
|
||||
chatModel.showingContactRequest = NTFContactRequest(chatId: chatId)
|
||||
}
|
||||
```
|
||||
|
||||
- `ChatModel` gains `@Published var showingContactRequest: NTFContactRequest?`.
|
||||
- `ChatListView` presents a model-driven `.confirmationDialog` bound to it.
|
||||
- The dialog buttons (Accept / Accept incognito / Reject) are extracted into a shared
|
||||
`contactRequestDialogButtons(_:)` view builder, reused by the existing chat-list row so
|
||||
the two dialogs cannot drift apart.
|
||||
- The `getChat` guard fires only when the request is already loaded — which
|
||||
`changeActiveUser` guarantees, because it loads the new profile's chats synchronously —
|
||||
so the dialog always has buttons (no empty Cancel-only dialog). The `Accept`-button
|
||||
branch (`ntfActionAcceptContact`) is unchanged and still auto-accepts.
|
||||
|
||||
This also fixes the same-profile case: previously a plain tap on a contact-request
|
||||
notification for the *active* profile did nothing either; it now surfaces the dialog too.
|
||||
|
||||
## Why this is minimal and safe
|
||||
|
||||
- Scoped to the notification handler; `changeActiveUser` is untouched, so the `keepingChatId`
|
||||
flows (e.g. `ContextProfilePickerView`) are unaffected.
|
||||
- No control-flow regression: every contact-request notification now enters the first `if`,
|
||||
but nothing is lost — contact requests always have `targetContentIdentifier == nil` and
|
||||
never match `ntfCallAction`, so the old fall-through `else` was already a no-op for them.
|
||||
Dismiss and other actions remain no-ops as before.
|
||||
- Message notifications still open correctly: their branch re-sets `chatId` via
|
||||
`loadOpenChat` immediately after.
|
||||
- Matches the reported expected behavior ("switch to the profile the notification came from")
|
||||
and aligns iOS with Android/Desktop.
|
||||
- No new/removed translation keys — the dialog reuses the chat-list row's existing strings.
|
||||
|
||||
## Cross-platform status
|
||||
|
||||
@@ -65,9 +104,16 @@ chatModel.chatId = nil
|
||||
(`common/.../platform/NtfManager.kt:59-72`) switches profile, sets `clearOverlays = true`,
|
||||
and only opens `Direct`/`Group` chats, so an invitation falls through to the chat list.
|
||||
|
||||
## Review
|
||||
|
||||
Adversarially reviewed (control-flow, compile/type, SwiftUI presentation, threading, races)
|
||||
across independent passes — no bugs, regressions, compile errors, or races found. The one
|
||||
raised nit (a possible empty dialog when the request is not loaded) is eliminated by the
|
||||
`getChat` guard at set-time.
|
||||
|
||||
## Verification note
|
||||
|
||||
Not runtime-verified here: building/running the iOS app requires Xcode (macOS), unavailable
|
||||
in this Linux environment. Repro to confirm on device: with two profiles, open a chat in
|
||||
profile A, then tap a contact-request notification for profile B — before the fix it shows a
|
||||
blank chat; after, it lands on profile B's chat list.
|
||||
blank chat; after, it lands on profile B's chat list with the accept/reject dialog shown.
|
||||
|
||||
Reference in New Issue
Block a user