diff options
| author | Christian Cleberg <[email protected]> | 2026-03-19 15:17:38 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-19 15:17:38 -0500 |
| commit | 6ba4e967d5dfb5d3c7bb97a0f2662f3180595563 (patch) | |
| tree | 572bef546fcca19ad37bc1aa7cfb153eb2bf8eb7 /Hutch/Views/Inbox | |
| parent | 1e3c748119c6e9eec27f02146f17ea0302ff648a (diff) | |
| download | hutch-6ba4e967d5dfb5d3c7bb97a0f2662f3180595563.tar.gz hutch-6ba4e967d5dfb5d3c7bb97a0f2662f3180595563.tar.bz2 hutch-6ba4e967d5dfb5d3c7bb97a0f2662f3180595563.zip | |
feat: implement support for projects, lists, and pastes
Diffstat (limited to 'Hutch/Views/Inbox')
| -rw-r--r-- | Hutch/Views/Inbox/InboxView.swift | 15 | ||||
| -rw-r--r-- | Hutch/Views/Inbox/InboxViewModel.swift | 10 | ||||
| -rw-r--r-- | Hutch/Views/Inbox/ThreadDetailView.swift | 60 |
3 files changed, 65 insertions, 20 deletions
diff --git a/Hutch/Views/Inbox/InboxView.swift b/Hutch/Views/Inbox/InboxView.swift index 602f8d9..304e62d 100644 --- a/Hutch/Views/Inbox/InboxView.swift +++ b/Hutch/Views/Inbox/InboxView.swift @@ -51,9 +51,9 @@ struct InboxView: View { ) } else if viewModel.threads.isEmpty, viewModel.error == nil { ContentUnavailableView( - "No Threads", + "Inbox Zero", systemImage: "tray", - description: Text("Patch threads will appear here.") + description: Text("Unread threads will appear here.") ) } } @@ -75,19 +75,16 @@ struct InboxView: View { private func readStateAction(for thread: InboxThreadSummary, in viewModel: InboxViewModel) -> some View { Button { withAnimation(.easeInOut(duration: 0.2)) { - viewModel.toggleThreadReadState(thread) + viewModel.markThreadRead(thread) } } label: { - Label( - thread.isUnread ? "Mark as Read" : "Mark as Unread", - systemImage: thread.isUnread ? "envelope.open" : "envelope.badge" - ) + Label("Mark as Read", systemImage: "envelope.open") } - .tint(thread.isUnread ? .blue : .gray) + .tint(.blue) } } -private struct InboxThreadRow: View { +struct InboxThreadRow: View { let thread: InboxThreadSummary var body: some View { diff --git a/Hutch/Views/Inbox/InboxViewModel.swift b/Hutch/Views/Inbox/InboxViewModel.swift index 9211b40..9c1ef45 100644 --- a/Hutch/Views/Inbox/InboxViewModel.swift +++ b/Hutch/Views/Inbox/InboxViewModel.swift @@ -127,7 +127,9 @@ final class InboxViewModel { let subscriptions = try await fetchSubscriptions() let mailingLists = deduplicateMailingLists(subscriptions.compactMap(\.list)) let fetchedThreads = try await fetchThreads(for: mailingLists) - threads = fetchedThreads.sorted { lhs, rhs in + threads = fetchedThreads + .filter(\.isUnread) + .sorted { lhs, rhs in if lhs.lastActivityAt == rhs.lastActivityAt { return lhs.subject.localizedCaseInsensitiveCompare(rhs.subject) == .orderedAscending } @@ -145,7 +147,7 @@ final class InboxViewModel { inboxListLogger.debug( "Inbox mark read: key=\(thread.id, privacy: .public) latestActivityAt=\(thread.lastActivityAt.ISO8601Format(), privacy: .public) storedLastViewedAt=\(viewedAt.ISO8601Format(), privacy: .public)" ) - updateThread(thread, isUnread: false) + threads.removeAll { $0.id == thread.id } } func markThreadUnread(_ thread: InboxThreadSummary) { @@ -329,6 +331,10 @@ final class InboxViewModel { private func updateThread(_ thread: InboxThreadSummary, isUnread: Bool) { guard let index = threads.firstIndex(where: { $0.id == thread.id }) else { return } let current = threads[index] + if !isUnread { + threads.remove(at: index) + return + } threads[index] = InboxThreadSummary( rootEmailID: current.rootEmailID, rootMessageID: current.rootMessageID, diff --git a/Hutch/Views/Inbox/ThreadDetailView.swift b/Hutch/Views/Inbox/ThreadDetailView.swift index 7677698..6fe835c 100644 --- a/Hutch/Views/Inbox/ThreadDetailView.swift +++ b/Hutch/Views/Inbox/ThreadDetailView.swift @@ -8,10 +8,29 @@ private let inboxReplyLogger = Logger(subsystem: "net.cleberg.Hutch", category: struct ThreadDetailView: View { let thread: InboxThreadSummary let onViewed: () -> Void + var onMarkRead: (() -> Void)? = nil + var onMarkUnread: (() -> Void)? = nil @Environment(AppState.self) private var appState @State private var viewModel: ThreadViewModel? @State private var replySuccessMessage: String? + @State private var loadedThreadID: String? + @State private var hasMarkedCurrentThreadViewed = false + @State private var suppressAutoMarkViewed = false + @State private var isUnread: Bool + + init( + thread: InboxThreadSummary, + onViewed: @escaping () -> Void, + onMarkRead: (() -> Void)? = nil, + onMarkUnread: (() -> Void)? = nil + ) { + self.thread = thread + self.onViewed = onViewed + self.onMarkRead = onMarkRead + self.onMarkUnread = onMarkUnread + self._isUnread = State(initialValue: thread.isUnread) + } var body: some View { Group { @@ -23,13 +42,21 @@ struct ThreadDetailView: View { } .navigationTitle("Thread") .navigationBarTitleDisplayMode(.inline) - .task { - if viewModel == nil { - onViewed() - let vm = ThreadViewModel(summary: thread, client: appState.client) - viewModel = vm - await vm.loadThread() - } + .task(id: thread.id) { + guard loadedThreadID != thread.id else { return } + let vm = ThreadViewModel(summary: thread, client: appState.client) + viewModel = vm + loadedThreadID = thread.id + hasMarkedCurrentThreadViewed = false + suppressAutoMarkViewed = false + isUnread = thread.isUnread + await vm.loadThread() + } + .onChange(of: viewModel?.thread?.id) { _, threadID in + guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return } + hasMarkedCurrentThreadViewed = true + isUnread = false + onViewed() } .sheet(item: Binding( get: { viewModel?.composeDraft }, @@ -109,8 +136,23 @@ struct ThreadDetailView: View { .listStyle(.plain) .toolbar { ToolbarItem(placement: .topBarTrailing) { - Button("Reply") { - viewModel.prepareReply() + HStack { + if onMarkRead != nil || onMarkUnread != nil { + Button(isUnread ? "Mark Read" : "Mark Unread") { + suppressAutoMarkViewed = !isUnread + if isUnread { + onMarkRead?() + isUnread = false + } else { + onMarkUnread?() + isUnread = true + } + } + } + + Button("Reply") { + viewModel.prepareReply() + } } } } |
