diff options
| author | Christian Cleberg <[email protected]> | 2026-03-19 16:35:41 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-19 16:35:41 -0500 |
| commit | 9065ef6e92245a44390e336cc1ae515ae706cdd7 (patch) | |
| tree | 413a9d930bd8159205afe567fe1e65fbfd8733c2 | |
| parent | 165d97fd2867cf5923aeb1c5fc00f6e60bcaaa6e (diff) | |
| download | hutch-9065ef6e92245a44390e336cc1ae515ae706cdd7.tar.gz hutch-9065ef6e92245a44390e336cc1ae515ae706cdd7.tar.bz2 hutch-9065ef6e92245a44390e336cc1ae515ae706cdd7.zip | |
fix: inbox thread handling bug
| -rw-r--r-- | Hutch/Views/Inbox/InboxView.swift | 87 | ||||
| -rw-r--r-- | Hutch/Views/Inbox/InboxViewModel.swift | 4 | ||||
| -rw-r--r-- | Hutch/Views/Inbox/ThreadDetailView.swift | 7 |
3 files changed, 94 insertions, 4 deletions
diff --git a/Hutch/Views/Inbox/InboxView.swift b/Hutch/Views/Inbox/InboxView.swift index 304e62d..2b92738 100644 --- a/Hutch/Views/Inbox/InboxView.swift +++ b/Hutch/Views/Inbox/InboxView.swift @@ -1,8 +1,14 @@ import SwiftUI +import os + +private let inboxNavigationLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxNavigation") struct InboxView: View { @Environment(AppState.self) private var appState @State private var viewModel: InboxViewModel? + @State private var selectedThreadID: InboxThreadSummary.ID? + @State private var selectedThreadSnapshot: InboxThreadSummary? + @State private var isShowingThreadDetail = false var body: some View { Group { @@ -28,9 +34,12 @@ struct InboxView: View { List { ForEach(viewModel.threads) { thread in - NavigationLink(value: thread) { + Button { + selectThread(thread) + } label: { InboxThreadRow(thread: thread) } + .buttonStyle(.plain) .swipeActions(edge: .leading, allowsFullSwipe: true) { readStateAction(for: thread, in: viewModel) } @@ -64,9 +73,37 @@ struct InboxView: View { .refreshable { await viewModel.loadThreads() } - .navigationDestination(for: InboxThreadSummary.self) { thread in - ThreadDetailView(thread: thread) { - viewModel.markThreadRead(thread) + .onChange(of: viewModel.threads) { _, threads in + syncSelectedThreadSnapshot(with: threads) + } + .navigationDestination(isPresented: Binding( + get: { isShowingThreadDetail && selectedThread(for: viewModel) != nil }, + set: { isPresented in + if !isPresented { + clearSelection() + } + isShowingThreadDetail = isPresented + } + )) { + if let thread = selectedThread(for: viewModel) { + ThreadDetailView(thread: thread) { + viewModel.markThreadRead(thread) + } + .onAppear { + cacheSelectedThread(thread) + } + .onDisappear { + handleThreadDetailDisappear(for: thread.id) + } + } else { + ContentUnavailableView( + "Thread Unavailable", + systemImage: "tray", + description: Text("This thread could not be restored.") + ) + .onAppear { + inboxNavigationLogger.error("Inbox navigation destination missing thread snapshot") + } } } } @@ -82,6 +119,48 @@ struct InboxView: View { } .tint(.blue) } + + private func selectThread(_ thread: InboxThreadSummary) { + cacheSelectedThread(thread) + isShowingThreadDetail = true + inboxNavigationLogger.debug( + "Inbox navigation triggered: threadID=\(thread.id, privacy: .public) subject=\(thread.subject, privacy: .public)" + ) + } + + private func cacheSelectedThread(_ thread: InboxThreadSummary) { + selectedThreadID = thread.id + selectedThreadSnapshot = thread + } + + private func selectedThread(for viewModel: InboxViewModel) -> InboxThreadSummary? { + guard let selectedThreadID else { return selectedThreadSnapshot } + return viewModel.thread(withID: selectedThreadID) ?? (selectedThreadSnapshot?.id == selectedThreadID ? selectedThreadSnapshot : nil) + } + + private func handleThreadDetailDisappear(for threadID: String) { + let isActiveSelection = selectedThreadID == threadID + inboxNavigationLogger.debug( + "Inbox thread detail disappeared: threadID=\(threadID, privacy: .public) activeSelection=\(isActiveSelection, privacy: .public)" + ) + guard isActiveSelection else { return } + clearSelection() + } + + private func syncSelectedThreadSnapshot(with threads: [InboxThreadSummary]) { + guard let selectedThreadID else { return } + guard let updatedThread = threads.first(where: { $0.id == selectedThreadID }) else { return } + selectedThreadSnapshot = updatedThread + } + + private func clearSelection() { + if let selectedThreadID { + inboxNavigationLogger.debug("Inbox selection cleared: threadID=\(selectedThreadID, privacy: .public)") + } + selectedThreadID = nil + selectedThreadSnapshot = nil + isShowingThreadDetail = false + } } struct InboxThreadRow: View { diff --git a/Hutch/Views/Inbox/InboxViewModel.swift b/Hutch/Views/Inbox/InboxViewModel.swift index 9c1ef45..e4a0664 100644 --- a/Hutch/Views/Inbox/InboxViewModel.swift +++ b/Hutch/Views/Inbox/InboxViewModel.swift @@ -166,6 +166,10 @@ final class InboxViewModel { } } + func thread(withID id: InboxThreadSummary.ID) -> InboxThreadSummary? { + threads.first(where: { $0.id == id }) + } + private func fetchSubscriptions() async throws -> [InboxActivitySubscription] { var subscriptions: [InboxActivitySubscription] = [] var cursor: String? diff --git a/Hutch/Views/Inbox/ThreadDetailView.swift b/Hutch/Views/Inbox/ThreadDetailView.swift index 6fe835c..c5d26a5 100644 --- a/Hutch/Views/Inbox/ThreadDetailView.swift +++ b/Hutch/Views/Inbox/ThreadDetailView.swift @@ -4,6 +4,7 @@ import SwiftUI import UIKit private let inboxReplyLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxReply") +private let inboxThreadNavigationLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxThreadNavigation") struct ThreadDetailView: View { let thread: InboxThreadSummary @@ -52,12 +53,18 @@ struct ThreadDetailView: View { isUnread = thread.isUnread await vm.loadThread() } + .onAppear { + inboxThreadNavigationLogger.debug("Inbox thread detail appeared: threadID=\(thread.id, privacy: .public)") + } .onChange(of: viewModel?.thread?.id) { _, threadID in guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return } hasMarkedCurrentThreadViewed = true isUnread = false onViewed() } + .onDisappear { + inboxThreadNavigationLogger.debug("Inbox thread detail view disappeared: threadID=\(thread.id, privacy: .public)") + } .sheet(item: Binding( get: { viewModel?.composeDraft }, set: { _ in viewModel?.dismissReply() } |
