Files
MikeMail/MikeMail/MessageActions.swift
T
2026-11-08 22:00:47 +01:00

127 lines
5.5 KiB
Swift

import SwiftUI
enum MessageCommand {
case open, reply, forward, delete, read, unread
}
struct ComposeDraft: Identifiable {
let id = UUID()
var accountID: UUID?
var recipient = ""
var cc = ""
var bcc = ""
var subject = ""
var body = ""
var htmlBody: String?
var attachments: [MailAttachment] = []
var isReply = false
static func saved(_ message: MailMessage, accountID: UUID?) -> ComposeDraft {
ComposeDraft(
accountID: accountID,
recipient: message.recipient,
cc: message.cc,
bcc: message.bcc,
subject: message.subject,
body: message.body,
htmlBody: message.htmlBody,
attachments: message.attachments
)
}
static func from(_ message: MailMessage, accountID: UUID?, forwarding: Bool) -> ComposeDraft {
let source = message.replyTo.isEmpty ? message.sender : message.replyTo
let recipient: String
if let start = source.firstIndex(of: "<"), let end = source[start...].firstIndex(of: ">") {
recipient = String(source[source.index(after: start)..<end])
} else { recipient = source }
return ComposeDraft(accountID: accountID, recipient: forwarding ? "" : recipient,
subject: (forwarding ? "Fwd: " : "Re: ") + message.subject,
body: "\n\n" + (forwarding ? "Weitergeleitete Nachricht" : "Am \(message.date.formatted()) schrieb \(message.sender):") +
"\nVon: \(message.sender)\nAn: \(message.recipient)\nBetreff: \(message.subject)\n\n" +
message.body.components(separatedBy: "\n").map { forwarding ? $0 : "> " + $0 }.joined(separator: "\n"),
attachments: forwarding ? message.attachments : [], isReply: !forwarding)
}
}
struct MessageActionButtons: View {
let action: (MessageCommand) -> Void
var body: some View {
Button("Löschen…", systemImage: "trash") { action(.delete) }
Button("Antworten…", systemImage: "arrowshape.turn.up.left") { action(.reply) }
Button("Weiterleiten", systemImage: "arrowshape.turn.up.right") { action(.forward) }
}
}
struct MessageDetailWindow: View {
let store: MailStore
let messageID: String?
@Environment(\.openWindow) private var openWindow
@State private var confirmingDelete = false
@AppStorage("mailDeleteConfirmation") private var deleteConfirmation: MailDeleteConfirmation = .always
@AppStorage("markReadImmediately") private var markReadImmediately = false
@AppStorage("markReadDelaySeconds") private var markReadDelaySeconds = 1
private var message: MailMessage? {
_ = store.archiveRevision
return messageID.flatMap { store.message(id: $0) }
}
var body: some View {
MessageReader(message: message)
.frame(minWidth: 600, minHeight: 450)
.navigationTitle(message?.subject ?? "Nachricht")
.toolbar {
Button("Drucken …", systemImage: "printer") {
guard let message else { return }
MailPrinter.print(message)
}
.disabled(message == nil)
MessageActionButtons { command in
guard let message else { return }
switch command {
case .reply, .forward:
let draft = ComposeDraft.from(message, accountID: store.accountID(for: message.id), forwarding: command == .forward)
openWindow(id: "compose", value: store.prepareCompose(draft))
case .delete:
if deleteConfirmation.isRequired(count: 1) { confirmingDelete = true }
else { Task { await store.deleteMessages(ids: [message.id]) } }
default: break
}
}.disabled(message == nil)
}
.alert("Nachricht löschen?", isPresented: $confirmingDelete) {
Button("Abbrechen", role: .cancel) {}
Button("Löschen", role: .destructive) {
if let messageID { Task { await store.deleteMessages(ids: [messageID]) } }
}
} message: {
Text(store.selectionContainsTrash([messageID ?? ""])
? "Die Nachricht wird auch auf dem Server endgültig gelöscht."
: "Die Nachricht wird auch auf dem Server in den Papierkorb verschoben.")
}
.task(id: AutoReadRequest(ids: Set([messageID].compactMap { $0 }), immediately: markReadImmediately, seconds: markReadDelaySeconds)) {
guard let message, !message.isRead else { return }
do {
try await AutoReadRequest(ids: [message.id], immediately: markReadImmediately, seconds: markReadDelaySeconds).wait()
await store.setRead(ids: [message.id], read: true)
} catch { /* The message window closed before the read delay elapsed. */ }
}
.alert("MikeMail", isPresented: Binding(get: { store.error != nil }, set: { if !$0 { store.error = nil } })) {
Button("OK") { store.error = nil }
} message: { Text(store.error ?? "") }
}
}
struct UnreadBadge: View {
let count: Int
var body: some View {
if count > 0 {
Text(count.formatted()).font(.caption.bold()).monospacedDigit()
.padding(.horizontal, 6).padding(.vertical, 2)
.background(.quaternary, in: Capsule())
.accessibilityLabel("\(count) ungelesen")
}
}
}