This commit is contained in:
2026-11-09 22:32:51 +01:00
committed by michael300679
parent 469a9a11d2
commit 49138503dd
2 changed files with 62 additions and 8 deletions
+9 -3
View File
@@ -66,8 +66,14 @@ struct ContentView: View {
} }
private var displayedMessages: [MailMessage] { private var displayedMessages: [MailMessage] {
guard !search.isEmpty else { return store.messages } let candidates = search.isEmpty ? store.messages : searchCandidates.filter(searchFilter.matches)
return searchCandidates.filter(searchFilter.matches) return candidates.compactMap { message in
guard !store.pendingHiddenMessageIDs.contains(message.id) else { return nil }
guard let read = store.pendingReadStates[message.id] else { return message }
var updated = message
updated.isRead = read
return updated
}
} }
private var accountIndicators: [UUID: AccountIndicator] { private var accountIndicators: [UUID: AccountIndicator] {
@@ -256,7 +262,7 @@ struct ContentView: View {
} }
searchCandidates = store.messagesForSearch( searchCandidates = store.messagesForSearch(
scope: searchScope, scope: searchScope,
folderID: selectedFolder, folderID: store.mailboxID(for: selectedFolder),
accountID: currentSearchAccountID accountID: currentSearchAccountID
) )
} }
+53 -5
View File
@@ -8,6 +8,8 @@ final class MailStore {
var recipientHistory: [String] = [] var recipientHistory: [String] = []
var folders: [UUID: [MailFolder]] = [:] var folders: [UUID: [MailFolder]] = [:]
var messages: [MailMessage] = [] var messages: [MailMessage] = []
private(set) var pendingHiddenMessageIDs: Set<String> = []
private(set) var pendingReadStates: [String: Bool] = [:]
var isBusy = false var isBusy = false
private(set) var sendingAccountIDs: Set<UUID> = [] private(set) var sendingAccountIDs: Set<UUID> = []
var unreadCounts: [String: Int] = [:] var unreadCounts: [String: Int] = [:]
@@ -132,6 +134,14 @@ final class MailStore {
items.flatMap { [$0] + allFolders($0.children ?? []) } items.flatMap { [$0] + allFolders($0.children ?? []) }
} }
func mailboxID(for sidebarID: String?) -> String? {
guard let sidebarID, sidebarID.hasPrefix("account:"),
let accountID = UUID(uuidString: String(sidebarID.dropFirst("account:".count))) else {
return sidebarID
}
return allFolders(folders[accountID] ?? []).first(where: \.isInbox)?.id
}
var totalUnreadCount: Int { var totalUnreadCount: Int {
accounts accounts
.flatMap { allFolders(folders[$0.id] ?? []) } .flatMap { allFolders(folders[$0.id] ?? []) }
@@ -216,6 +226,7 @@ final class MailStore {
} }
unreadCounts = try archive.unreadCounts() unreadCounts = try archive.unreadCounts()
archiveRevision += 1 archiveRevision += 1
if let resolvedFolderID = mailboxID(for: folderID) { selectedFolderID = resolvedFolderID }
if let selectedFolderID { try display(folderID: selectedFolderID) } if let selectedFolderID { try display(folderID: selectedFolderID) }
await MailNotifications.postNewMail( await MailNotifications.postNewMail(
newInboxMessages, newInboxMessages,
@@ -242,6 +253,7 @@ final class MailStore {
} }
func load(folderID: String?, more: Bool = false) async { func load(folderID: String?, more: Bool = false) async {
let folderID = mailboxID(for: folderID)
selectedFolderID = folderID selectedFolderID = folderID
guard let folderID else { messages = []; totalMessages = 0; return } guard let folderID else { messages = []; totalMessages = 0; return }
do { do {
@@ -439,12 +451,17 @@ final class MailStore {
} }
func moveMessages(ids: [String], to destination: MailFolder) async { func moveMessages(ids: [String], to destination: MailFolder) async {
guard destination.selectable else {
error = "Bitte einen beschreibbaren Zielordner wählen."
return
}
optimisticallyHide(ids: ids)
await mutate(refreshAfter: false) { await mutate(refreshAfter: false) {
guard destination.selectable else { throw MailError.message("Bitte einen beschreibbaren Zielordner wählen.") }
for id in Set(ids) { for id in Set(ids) {
try await self.moveOne(id: id, to: destination) try await self.moveOne(id: id, to: destination)
} }
} }
pendingHiddenMessageIDs.subtract(ids)
} }
private func moveOne(id: String, to destination: MailFolder) async throws { private func moveOne(id: String, to destination: MailFolder) async throws {
@@ -463,7 +480,6 @@ final class MailStore {
if source.accountID == destination.accountID, capabilities.contains("MOVE") { if source.accountID == destination.accountID, capabilities.contains("MOVE") {
let assigned = try await sourceClient.move(uid: uid, to: destination.path) let assigned = try await sourceClient.move(uid: uid, to: destination.path)
try storage().applyMessageChange(id: id, destination: destination.id, assigned: assigned, flags: inventory.flags[uid]) try storage().applyMessageChange(id: id, destination: destination.id, assigned: assigned, flags: inventory.flags[uid])
try updateLocalPresentation()
if assigned == nil { try await reconcileDestination(destination) } if assigned == nil { try await reconcileDestination(destination) }
return return
} }
@@ -485,7 +501,6 @@ final class MailStore {
throw MailError.message("Die Nachricht wurde ins Ziel kopiert, konnte aber nicht aus der Quelle entfernt werden. Beide Kopien bleiben erhalten. \(error.localizedDescription)") throw MailError.message("Die Nachricht wurde ins Ziel kopiert, konnte aber nicht aus der Quelle entfernt werden. Beide Kopien bleiben erhalten. \(error.localizedDescription)")
} }
try storage().applyMessageChange(id: id) try storage().applyMessageChange(id: id)
try updateLocalPresentation()
} }
private func updateLocalPresentation() throws { private func updateLocalPresentation() throws {
@@ -511,6 +526,7 @@ final class MailStore {
} }
func deleteMessages(ids: [String]) async { func deleteMessages(ids: [String]) async {
optimisticallyHide(ids: ids)
await mutate(refreshAfter: false) { await mutate(refreshAfter: false) {
for id in Set(ids) { for id in Set(ids) {
let record = try self.storage().record(id: id) let record = try self.storage().record(id: id)
@@ -522,7 +538,6 @@ final class MailStore {
guard inventory.validity == record.validity else { throw MailError.message("Ordnerbestand geändert. Bitte erneut abrufen.") } guard inventory.validity == record.validity else { throw MailError.message("Ordnerbestand geändert. Bitte erneut abrufen.") }
try await client.remove(uids: inventory.flags[uid] == nil ? [] : [uid]) try await client.remove(uids: inventory.flags[uid] == nil ? [] : [uid])
try self.storage().applyMessageChange(id: id) try self.storage().applyMessageChange(id: id)
try self.updateLocalPresentation()
} else { } else {
guard let trash = self.allFolders(self.folders[source.accountID] ?? []).first(where: \.isTrash) else { guard let trash = self.allFolders(self.folders[source.accountID] ?? []).first(where: \.isTrash) else {
throw MailError.message("Für dieses Konto wurde kein Papierkorb gefunden. Bitte zuerst einen Ordner „Trash“ erstellen.") throw MailError.message("Für dieses Konto wurde kein Papierkorb gefunden. Bitte zuerst einen Ordner „Trash“ erstellen.")
@@ -531,6 +546,7 @@ final class MailStore {
} }
} }
} }
pendingHiddenMessageIDs.subtract(ids)
} }
func selectionContainsTrash(_ ids: [String]) -> Bool { func selectionContainsTrash(_ ids: [String]) -> Bool {
@@ -549,6 +565,7 @@ final class MailStore {
} }
func restoreToInbox(ids: [String]) async { func restoreToInbox(ids: [String]) async {
optimisticallyHide(ids: ids)
await mutate(refreshAfter: false) { await mutate(refreshAfter: false) {
for id in Set(ids) { for id in Set(ids) {
let record = try self.storage().record(id: id) let record = try self.storage().record(id: id)
@@ -561,6 +578,7 @@ final class MailStore {
try await self.moveOne(id: id, to: inbox) try await self.moveOne(id: id, to: inbox)
} }
} }
pendingHiddenMessageIDs.subtract(ids)
} }
func createFolder(accountID: UUID, parent: MailFolder?, name: String) async { func createFolder(accountID: UUID, parent: MailFolder?, name: String) async {
@@ -642,6 +660,7 @@ final class MailStore {
} }
func setRead(ids: [String], read: Bool) async { func setRead(ids: [String], read: Bool) async {
optimisticallySetRead(ids: ids, read: read)
await mutate(refreshAfter: false) { await mutate(refreshAfter: false) {
for id in Set(ids) { for id in Set(ids) {
let record = try self.storage().record(id: id) let record = try self.storage().record(id: id)
@@ -656,9 +675,38 @@ final class MailStore {
var flags = (inventory.flags[uid] ?? []).filter { $0.lowercased() != "\\seen" } var flags = (inventory.flags[uid] ?? []).filter { $0.lowercased() != "\\seen" }
if read { flags.append("\\Seen") } if read { flags.append("\\Seen") }
try self.storage().applyMessageChange(id: id, flags: flags, removed: false) try self.storage().applyMessageChange(id: id, flags: flags, removed: false)
try self.updateLocalPresentation()
} }
} }
for id in ids { pendingReadStates[id] = nil }
}
private func optimisticallyHide(ids: [String]) {
let hidden = Set(ids)
guard !hidden.isEmpty else { return }
pendingHiddenMessageIDs.formUnion(hidden)
messages.removeAll { hidden.contains($0.id) }
totalMessages = messages.count
status = "Änderung wird im Hintergrund synchronisiert …"
}
private func optimisticallySetRead(ids: [String], read: Bool) {
let changed = Set(ids)
guard !changed.isEmpty else { return }
for id in changed { pendingReadStates[id] = read }
messages = messages.map { message in
guard changed.contains(message.id) else { return message }
var updated = message
updated.isRead = read
return updated
}
for id in changed {
guard let record = try? storage().record(id: id) else { continue }
let wasRead = record.flags.contains { $0.lowercased() == "\\seen" }
guard wasRead != read else { continue }
unreadCounts[record.folderID, default: 0] = max(0,
unreadCounts[record.folderID, default: 0] + (read ? -1 : 1))
}
status = "Änderung wird im Hintergrund synchronisiert …"
} }
func configureIdle() { func configureIdle() {