summaryrefslogtreecommitdiff
path: root/Hutch/Views/Inbox
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 16:35:41 -0500
committerChristian Cleberg <[email protected]>2026-03-19 16:35:41 -0500
commit9065ef6e92245a44390e336cc1ae515ae706cdd7 (patch)
tree413a9d930bd8159205afe567fe1e65fbfd8733c2 /Hutch/Views/Inbox
parent165d97fd2867cf5923aeb1c5fc00f6e60bcaaa6e (diff)
downloadhutch-9065ef6e92245a44390e336cc1ae515ae706cdd7.tar.gz
hutch-9065ef6e92245a44390e336cc1ae515ae706cdd7.tar.bz2
hutch-9065ef6e92245a44390e336cc1ae515ae706cdd7.zip
fix: inbox thread handling bug
Diffstat (limited to 'Hutch/Views/Inbox')
-rw-r--r--Hutch/Views/Inbox/InboxView.swift87
-rw-r--r--Hutch/Views/Inbox/InboxViewModel.swift4
-rw-r--r--Hutch/Views/Inbox/ThreadDetailView.swift7
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() }