Files
swiftapp/swift/Sources/Core/Models/MailModels.swift
Jürgen Kunz 549aaa634c Align Mail UI with social.io design handoff
Rewrite the Mail feature to match the Apple-native look from the
handoff spec: lane-split inbox, AI summary card, clean ThreadRow,
Cc/From + format toolbar in Compose. Drop the gradient hero
surfaces and blurred canvas backgrounds the spec calls out as
anti-patterns, and introduce a token-backed design layer so the
lane palette and SIO tint live in the asset catalog.

- Add Assets.xcassets with SIOTint, LaneFeed, LanePaper, LanePeople
  (light + dark variants).
- Add Sources/Core/Design/SIODesign.swift: SIO tokens, Lane enum,
  LaneChip, AvatarView, AISummaryCard, KeyboardHint, button styles,
  and a glass-chrome helper with iOS 26 / material fallback.
- Extend MailThread with lane + summary; custom Codable keeps old
  payloads decodable. Seed mock threads with sensible lanes and
  hand-write summaries on launch-copy, investor-update, roadmap-notes.
- Add lane filtering to AppViewModel (selectedLane, selectLane,
  laneUnreadCount, laneThreadCount).
- Rewrite MailRootView end to end: sidebar with Inbox/lane rows,
  lane filter strip, Apple-native ThreadRow (avatar, unread dot,
  lane chip, summary chip), ThreadReadingView with AI summary +
  floating reply pill, ComposeView with To/Cc/From/Subject and a
  format toolbar.
- Wire Assets.xcassets + SIODesign.swift into project.pbxproj.

Accessibility identifiers preserved byte-identical; new ones
(mailbox.lane.*, lane.chip.*) added only where new.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:22:10 +02:00

151 lines
4.1 KiB
Swift

import Foundation
enum Mailbox: String, CaseIterable, Identifiable, Codable {
case inbox
case starred
case sent
case drafts
case archive
var id: String { rawValue }
var title: String {
switch self {
case .inbox: "Inbox"
case .starred: "Starred"
case .sent: "Sent"
case .drafts: "Drafts"
case .archive: "Archive"
}
}
var systemImage: String {
switch self {
case .inbox: "tray.full"
case .starred: "star"
case .sent: "paperplane"
case .drafts: "doc.text"
case .archive: "archivebox"
}
}
}
struct MailPerson: Identifiable, Hashable, Codable {
let id: UUID
let name: String
let email: String
init(id: UUID = UUID(), name: String, email: String) {
self.id = id
self.name = name
self.email = email
}
}
struct MailMessage: Identifiable, Hashable, Codable {
let id: UUID
let routeID: String
let sender: MailPerson
let recipients: [MailPerson]
let sentAt: Date
let body: String
let isDraft: Bool
init(
id: UUID = UUID(),
routeID: String = UUID().uuidString.lowercased(),
sender: MailPerson,
recipients: [MailPerson],
sentAt: Date,
body: String,
isDraft: Bool = false
) {
self.id = id
self.routeID = routeID
self.sender = sender
self.recipients = recipients
self.sentAt = sentAt
self.body = body
self.isDraft = isDraft
}
}
struct MailThread: Identifiable, Hashable, Codable {
let id: UUID
let routeID: String
var mailbox: Mailbox
var subject: String
var participants: [MailPerson]
var messages: [MailMessage]
var isUnread: Bool
var isStarred: Bool
var tags: [String]
var lane: Lane
var summary: [String]?
init(
id: UUID = UUID(),
routeID: String = UUID().uuidString.lowercased(),
mailbox: Mailbox,
subject: String,
participants: [MailPerson],
messages: [MailMessage],
isUnread: Bool,
isStarred: Bool,
tags: [String] = [],
lane: Lane = .people,
summary: [String]? = nil
) {
self.id = id
self.routeID = routeID
self.mailbox = mailbox
self.subject = subject
self.participants = participants
self.messages = messages.sorted { $0.sentAt < $1.sentAt }
self.isUnread = isUnread
self.isStarred = isStarred
self.tags = tags
self.lane = lane
self.summary = summary
}
enum CodingKeys: String, CodingKey {
case id, routeID, mailbox, subject, participants, messages
case isUnread, isStarred, tags, lane, summary
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.id = try c.decode(UUID.self, forKey: .id)
self.routeID = try c.decode(String.self, forKey: .routeID)
self.mailbox = try c.decode(Mailbox.self, forKey: .mailbox)
self.subject = try c.decode(String.self, forKey: .subject)
self.participants = try c.decode([MailPerson].self, forKey: .participants)
let rawMessages = try c.decode([MailMessage].self, forKey: .messages)
self.messages = rawMessages.sorted { $0.sentAt < $1.sentAt }
self.isUnread = try c.decode(Bool.self, forKey: .isUnread)
self.isStarred = try c.decode(Bool.self, forKey: .isStarred)
self.tags = try c.decodeIfPresent([String].self, forKey: .tags) ?? []
self.lane = try c.decodeIfPresent(Lane.self, forKey: .lane) ?? .people
self.summary = try c.decodeIfPresent([String].self, forKey: .summary)
}
var latestMessage: MailMessage? {
messages.max(by: { $0.sentAt < $1.sentAt })
}
var previewText: String {
latestMessage?.body.replacingOccurrences(of: "\n", with: " ") ?? ""
}
var lastUpdated: Date {
latestMessage?.sentAt ?? .distantPast
}
}
struct ComposeDraft: Equatable {
var to = ""
var subject = ""
var body = ""
}