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