aboutsummaryrefslogtreecommitdiff
path: root/Hutch
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-20 13:04:57 -0500
committerChristian Cleberg <[email protected]>2026-04-20 13:04:57 -0500
commit8b73fd9cd43753b36b3ab3ca5e4dc83a47ea3480 (patch)
tree25658dbdc28775764f5745576c240efd38fd4685 /Hutch
parentd29fbef60abdffaee190b758530dde9212745ed2 (diff)
downloadhutch-8b73fd9cd43753b36b3ab3ca5e4dc83a47ea3480.tar.gz
hutch-8b73fd9cd43753b36b3ab3ca5e4dc83a47ea3480.tar.bz2
hutch-8b73fd9cd43753b36b3ab3ca5e4dc83a47ea3480.zip
fix: add mark all read actions and avoid resending unchanged repo metadata
Diffstat (limited to 'Hutch')
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift14
-rw-r--r--Hutch/Views/Projects/ProjectMailingListView.swift52
-rw-r--r--Hutch/Views/Repositories/RepositorySettingsViewModel.swift19
-rw-r--r--Hutch/Views/Work/WorkView.swift8
4 files changed, 83 insertions, 10 deletions
diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift
index 4bd4f32..6016a11 100644
--- a/Hutch/Views/Home/HomeViewModel.swift
+++ b/Hutch/Views/Home/HomeViewModel.swift
@@ -627,6 +627,20 @@ final class HomeViewModel {
persistNeedsAttentionSnapshot()
}
+ func markAllInboxThreadsRead() {
+ guard !unreadInboxThreads.isEmpty else { return }
+
+ let viewedAt = Date()
+ for thread in unreadInboxThreads {
+ InboxReadStateStore.markViewed(max(viewedAt, thread.lastActivityAt), for: thread.id, defaults: defaults)
+ }
+
+ unreadInboxThreads = []
+ unreadInboxThreadCount = 0
+ hasUnreadInboxThreads = false
+ persistNeedsAttentionSnapshot()
+ }
+
func markInboxThreadUnread(_ thread: InboxThreadSummary) {
InboxReadStateStore.markUnread(for: thread.id, defaults: defaults)
if unreadInboxThreads.contains(where: { $0.id == thread.id }) == false {
diff --git a/Hutch/Views/Projects/ProjectMailingListView.swift b/Hutch/Views/Projects/ProjectMailingListView.swift
index d7c0abc..ca6066c 100644
--- a/Hutch/Views/Projects/ProjectMailingListView.swift
+++ b/Hutch/Views/Projects/ProjectMailingListView.swift
@@ -105,6 +105,39 @@ final class MailingListDetailViewModel {
NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1, accountID: accountID)
}
+ func markAllThreadsRead() {
+ let unreadThreads = threads.filter(\.isUnread)
+ guard !unreadThreads.isEmpty else { return }
+
+ let viewedAt = Date()
+ for thread in unreadThreads {
+ InboxReadStateStore.markViewed(max(viewedAt, thread.lastActivityAt), for: thread.id, defaults: defaults)
+ }
+
+ threads = threads.map { thread in
+ guard thread.isUnread else { return thread }
+ return InboxThreadSummary(
+ rootEmailID: thread.rootEmailID,
+ rootMessageID: thread.rootMessageID,
+ threadRootEmailIDs: thread.threadRootEmailIDs,
+ threadRootMessageIDs: thread.threadRootMessageIDs,
+ listID: thread.listID,
+ listRID: thread.listRID,
+ listName: thread.listName,
+ listOwner: thread.listOwner,
+ subject: thread.subject,
+ latestSender: thread.latestSender,
+ lastActivityAt: thread.lastActivityAt,
+ messageCount: thread.messageCount,
+ repo: thread.repo,
+ containsPatch: thread.containsPatch,
+ isUnread: false
+ )
+ }
+
+ NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -unreadThreads.count, accountID: accountID)
+ }
+
private func makeSummary(from thread: ProjectMailingListThreadPayload) -> InboxThreadSummary {
let normalizedSubject = thread.subject
.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
@@ -241,6 +274,10 @@ struct MailingListDetailView: View {
return HomePinStore.isPinned(.mailingList(mailingList), for: currentUserKey, defaults: appState.accountDefaults)
}
+ private var hasUnreadThreads: Bool {
+ viewModel?.threads.contains(where: \.isUnread) == true
+ }
+
var body: some View {
Group {
if let viewModel {
@@ -252,6 +289,12 @@ struct MailingListDetailView: View {
.navigationTitle(mailingList.name)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("Mark All Read") {
+ viewModel?.markAllThreadsRead()
+ }
+ .disabled(hasUnreadThreads == false)
+ }
if currentUserKey != nil {
ToolbarItem(placement: .topBarTrailing) {
Button {
@@ -299,16 +342,13 @@ struct MailingListDetailView: View {
ThreadDetailView(
thread: thread,
onViewed: {
- InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id, defaults: appState.accountDefaults)
- NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: appState.activeAccountID)
+ viewModel.markThreadRead(thread)
},
onMarkRead: {
- InboxReadStateStore.markViewed(max(Date(), thread.lastActivityAt), for: thread.id, defaults: appState.accountDefaults)
- NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: -1, accountID: appState.activeAccountID)
+ viewModel.markThreadRead(thread)
},
onMarkUnread: {
- InboxReadStateStore.markUnread(for: thread.id, defaults: appState.accountDefaults)
- NeedsAttentionSnapshotStore.adjustUnreadInboxThreads(by: 1, accountID: appState.activeAccountID)
+ viewModel.markThreadUnread(thread)
}
)
} label: {
diff --git a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
index 6233bec..6ad6a78 100644
--- a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
+++ b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
@@ -166,10 +166,7 @@ final class RepositorySettingsViewModel {
defer { isSavingMetadata = false }
error = nil
- let input: [String: any Sendable] = [
- "name": normalizedEditedName,
- "description": normalizedEditedDescription
- ]
+ let input = metadataInputForSave()
do {
return try await updateRepository(with: input)
@@ -295,4 +292,18 @@ final class RepositorySettingsViewModel {
RepositorySummary.displayBranchName(for: $0.name) == normalizedEditedHead
}?.name
}
+
+ func metadataInputForSave() -> [String: any Sendable] {
+ var input: [String: any Sendable] = [:]
+
+ if normalizedEditedName != repository.name {
+ input["name"] = normalizedEditedName
+ }
+
+ if normalizedEditedDescription != (repository.description ?? "") {
+ input["description"] = normalizedEditedDescription.isEmpty ? Optional<String>.none as String? : normalizedEditedDescription
+ }
+
+ return input
+ }
}
diff --git a/Hutch/Views/Work/WorkView.swift b/Hutch/Views/Work/WorkView.swift
index 2326236..8b932dd 100644
--- a/Hutch/Views/Work/WorkView.swift
+++ b/Hutch/Views/Work/WorkView.swift
@@ -26,6 +26,14 @@ struct WorkView: View {
}
.navigationTitle("Work")
.navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("Mark All Read") {
+ viewModel?.markAllInboxThreadsRead()
+ }
+ .disabled(viewModel.map { unreadCount($0) } ?? 0 == 0)
+ }
+ }
.task {
guard let currentUser = appState.currentUser else { return }
await ensureViewModel(currentUser: currentUser).loadDashboard()