Alpha 8
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user