| 1 | import Foundation |
| 2 | |
| 3 | // 🤖 Navigation push guard (issue #580, iOS threading). |
| 4 | // |
| 5 | // The chat list pushes a chat with `path.append(.thread(id))`. That append is |
| 6 | // unguarded, and a `NavigationStack` path is an ARRAY — it holds duplicates |
| 7 | // happily. When the main thread stalls (the layout-loop / socket-churn hangs |
| 8 | // we're chasing), a member's taps on a row don't visually commit, so they tap |
| 9 | // again… and every tap already ran `append`. When the stack finally commits it |
| 10 | // holds two (or more) identical `.thread(id)` entries, and "back" surfaces a |
| 11 | // phantom second copy of the same chat. |
| 12 | // |
| 13 | // The fix is idempotence: only push when the target chat isn't already on top |
| 14 | // of the stack, collapsing queued taps to a single push. This mirrors the guard |
| 15 | // the notification-tap path already uses (`path.last != .thread(id)` in |
| 16 | // RootView.routeNotificationTap). Kept as a pure helper so it's unit-testable |
| 17 | // without the SwiftUI/UIKit runtime the bug actually lives in. |
| 18 | public enum ChatNavGuard { |
| 19 | /// Whether tapping into `threadID` should push a new chat screen, given the |
| 20 | /// thread currently on top of the navigation stack (`nil` = the top isn't a |
| 21 | /// chat, e.g. the chat list itself). |
| 22 | public static func shouldPush(threadID: String, topThreadID: String?) -> Bool { |
| 23 | topThreadID != threadID |
| 24 | } |
| 25 | } |
| 26 | |