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] {
guard !search.isEmpty else { return store.messages }
return searchCandidates.filter(searchFilter.matches)
let candidates = search.isEmpty ? store.messages : 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] {
@@ -256,7 +262,7 @@ struct ContentView: View {
}
searchCandidates = store.messagesForSearch(
scope: searchScope,
folderID: selectedFolder,
folderID: store.mailboxID(for: selectedFolder),
accountID: currentSearchAccountID
)
}
+53 -5
View File
@@ -8,6 +8,8 @@ final class MailStore {
var recipientHistory: [String] = []
var folders: [UUID: [MailFolder]] = [:]
var messages: [MailMessage] = []
private(set) var pendingHiddenMessageIDs: Set<String> = []
private(set) var pendingReadStates: [String: Bool] = [:]
var isBusy = false
private(set) var sendingAccountIDs: Set<UUID> = []
var unreadCounts: [String: Int] = [:]
@@ -132,6 +134,14 @@ final class MailStore {
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 {
accounts
.flatMap { allFolders(folders[$0.id] ?? []) }
@@ -216,6 +226,7 @@ final class MailStore {
}
unreadCounts = try archive.unreadCounts()
archiveRevision += 1
if let resolvedFolderID = mailboxID(for: folderID) { selectedFolderID = resolvedFolderID }
if let selectedFolderID { try display(folderID: selectedFolderID) }
await MailNotifications.postNewMail(
newInboxMessages,
@@ -242,6 +253,7 @@ final class MailStore {
}
func load(folderID: String?, more: Bool = false) async {
let folderID = mailboxID(for: folderID)
selectedFolderID = folderID
guard let folderID else { messages = []; totalMessages = 0; return }
do {
@@ -439,12 +451,17 @@ final class MailStore {
}
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) {
guard destination.selectable else { throw MailError.message("Bitte einen beschreibbaren Zielordner wählen.") }
for id in Set(ids) {
try await self.moveOne(id: id, to: destination)
}
}
pendingHiddenMessageIDs.subtract(ids)
}
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") {
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 updateLocalPresentation()
if assigned == nil { try await reconcileDestination(destination) }
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)")
}
try storage().applyMessageChange(id: id)
try updateLocalPresentation()
}
private func updateLocalPresentation() throws {
@@ -511,6 +526,7 @@ final class MailStore {
}
func deleteMessages(ids: [String]) async {
optimisticallyHide(ids: ids)
await mutate(refreshAfter: false) {
for id in Set(ids) {
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.") }
try await client.remove(uids: inventory.flags[uid] == nil ? [] : [uid])
try self.storage().applyMessageChange(id: id)
try self.updateLocalPresentation()
} 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.")
@@ -531,6 +546,7 @@ final class MailStore {
}
}
}
pendingHiddenMessageIDs.subtract(ids)
}
func selectionContainsTrash(_ ids: [String]) -> Bool {
@@ -549,6 +565,7 @@ final class MailStore {
}
func restoreToInbox(ids: [String]) async {
optimisticallyHide(ids: ids)
await mutate(refreshAfter: false) {
for id in Set(ids) {
let record = try self.storage().record(id: id)
@@ -561,6 +578,7 @@ final class MailStore {
try await self.moveOne(id: id, to: inbox)
}
}
pendingHiddenMessageIDs.subtract(ids)
}
func createFolder(accountID: UUID, parent: MailFolder?, name: String) async {
@@ -642,6 +660,7 @@ final class MailStore {
}
func setRead(ids: [String], read: Bool) async {
optimisticallySetRead(ids: ids, read: read)
await mutate(refreshAfter: false) {
for id in Set(ids) {
let record = try self.storage().record(id: id)
@@ -656,9 +675,38 @@ final class MailStore {
var flags = (inventory.flags[uid] ?? []).filter { $0.lowercased() != "\\seen" }
if read { flags.append("\\Seen") }
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() {