summaryrefslogtreecommitdiff
path: root/Hutch/Views/Inbox
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 15:17:38 -0500
committerChristian Cleberg <[email protected]>2026-03-19 15:17:38 -0500
commit6ba4e967d5dfb5d3c7bb97a0f2662f3180595563 (patch)
tree572bef546fcca19ad37bc1aa7cfb153eb2bf8eb7 /Hutch/Views/Inbox
parent1e3c748119c6e9eec27f02146f17ea0302ff648a (diff)
downloadhutch-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.swift15
-rw-r--r--Hutch/Views/Inbox/InboxViewModel.swift10
-rw-r--r--Hutch/Views/Inbox/ThreadDetailView.swift60
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()
+ }
}
}
}