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 } }