Files
swiftapp/swift/Sources/App/AppNavigationCommand.swift
Jürgen Kunz ad059e9b8d Add MailRootView and related components for mail functionality
- Implement MailRootView with navigation and sidebar for mail management.
- Create MailSidebarView, ThreadListView, and ThreadDetailView for displaying mail content.
- Introduce ComposeView for composing new messages.
- Add MailTheme for consistent styling across mail components.
- Implement adaptive layouts for iOS and macOS.
- Create unit tests for AppNavigationCommand and AppViewModel to ensure correct functionality.
2026-04-19 01:00:32 +02:00

208 lines
6.2 KiB
Swift

import Foundation
enum AppNavigationCommand: Equatable {
case mailbox(mailbox: Mailbox, search: String?, unreadOnly: Bool?)
case thread(
threadRouteID: String,
mailbox: Mailbox?,
messageRouteID: String?,
search: String?,
unreadOnly: Bool?
)
case compose(draft: ComposeDraft)
static let routeEnvironmentKey = "SOCIALIO_ROUTE"
static let jsonEnvironmentKey = "SOCIALIO_COMMAND_JSON"
static func from(environment: [String: String]) -> AppNavigationCommand? {
if let json = environment[jsonEnvironmentKey] {
return from(json: json)
}
if let route = environment[routeEnvironmentKey] {
return parse(route)
}
return nil
}
static func parse(_ rawValue: String) -> AppNavigationCommand? {
let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
if trimmed.hasPrefix("{") {
return from(json: trimmed)
}
guard let url = URL(string: trimmed) else { return nil }
return from(url: url)
}
static func from(url: URL) -> AppNavigationCommand? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return nil
}
let host = (components.host ?? "").lowercased()
let pathComponents = url.path
.split(separator: "/")
.map(String.init)
let queryItems = components.queryItems ?? []
let mailbox = queryItems.value(named: "mailbox").flatMap(Mailbox.init(rawValue:))
let threadRouteID = queryItems.value(named: "thread")
let messageRouteID = queryItems.value(named: "message")
let search = queryItems.value(named: "search")
let unreadOnly = queryItems.value(named: "unreadOnly").flatMap(Bool.init)
switch host {
case "mailbox":
guard let mailboxID = pathComponents.first, let mailbox = Mailbox(rawValue: mailboxID) else {
return nil
}
return .mailbox(mailbox: mailbox, search: search, unreadOnly: unreadOnly)
case "thread":
guard let routeID = pathComponents.first else { return nil }
return .thread(
threadRouteID: routeID,
mailbox: mailbox,
messageRouteID: messageRouteID,
search: search,
unreadOnly: unreadOnly
)
case "compose":
return .compose(
draft: ComposeDraft(
to: queryItems.value(named: "to") ?? "",
subject: queryItems.value(named: "subject") ?? "",
body: queryItems.value(named: "body") ?? ""
)
)
case "open", "":
if let threadRouteID {
return .thread(
threadRouteID: threadRouteID,
mailbox: mailbox,
messageRouteID: messageRouteID,
search: search,
unreadOnly: unreadOnly
)
}
if let mailbox {
return .mailbox(mailbox: mailbox, search: search, unreadOnly: unreadOnly)
}
if queryItems.value(named: "to") != nil ||
queryItems.value(named: "subject") != nil ||
queryItems.value(named: "body") != nil {
return .compose(
draft: ComposeDraft(
to: queryItems.value(named: "to") ?? "",
subject: queryItems.value(named: "subject") ?? "",
body: queryItems.value(named: "body") ?? ""
)
)
}
return nil
default:
return nil
}
}
static func from(json: String) -> AppNavigationCommand? {
guard let data = json.data(using: .utf8) else { return nil }
let decoder = JSONDecoder()
do {
let payload = try decoder.decode(AppNavigationPayload.self, from: data)
return payload.command
} catch {
return nil
}
}
}
private struct AppNavigationPayload: Decodable {
enum Kind: String, Decodable {
case mailbox
case thread
case compose
}
let kind: Kind?
let mailbox: Mailbox?
let threadID: String?
let messageID: String?
let search: String?
let unreadOnly: Bool?
let to: String?
let subject: String?
let body: String?
var command: AppNavigationCommand? {
switch kind {
case .mailbox:
guard let mailbox else { return nil }
return .mailbox(mailbox: mailbox, search: search, unreadOnly: unreadOnly)
case .thread:
guard let threadID else { return nil }
return .thread(
threadRouteID: threadID,
mailbox: mailbox,
messageRouteID: messageID,
search: search,
unreadOnly: unreadOnly
)
case .compose:
return .compose(
draft: ComposeDraft(
to: to ?? "",
subject: subject ?? "",
body: body ?? ""
)
)
case nil:
if let threadID {
return .thread(
threadRouteID: threadID,
mailbox: mailbox,
messageRouteID: messageID,
search: search,
unreadOnly: unreadOnly
)
}
if let mailbox {
return .mailbox(mailbox: mailbox, search: search, unreadOnly: unreadOnly)
}
if to != nil || subject != nil || body != nil {
return .compose(
draft: ComposeDraft(
to: to ?? "",
subject: subject ?? "",
body: body ?? ""
)
)
}
return nil
}
}
}
private extension [URLQueryItem] {
func value(named name: String) -> String? {
first(where: { $0.name == name })?.value
}
}