Files
swiftapp/swift/Sources/Core/Services/MockMailService.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

209 lines
8.6 KiB
Swift

import Foundation
protocol MailServicing {
func loadThreads() async throws -> [MailThread]
func send(draft: ComposeDraft) async throws -> MailThread
}
struct MockMailService: MailServicing {
private let me = MailPerson(name: "Phil Kunz", email: "phil@social.io")
func loadThreads() async throws -> [MailThread] {
try await Task.sleep(for: .milliseconds(150))
return seededThreads.sorted { $0.lastUpdated > $1.lastUpdated }
}
func send(draft: ComposeDraft) async throws -> MailThread {
try await Task.sleep(for: .milliseconds(120))
let threadRouteID = "sent-\(UUID().uuidString.lowercased())"
let messageRouteID = "\(threadRouteID)-message"
let recipientNames = draft.to
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
let recipients = recipientNames.map { raw in
MailPerson(
name: raw.components(separatedBy: "@").first?.capitalized ?? raw,
email: raw
)
}
let message = MailMessage(
routeID: messageRouteID,
sender: me,
recipients: recipients,
sentAt: .now,
body: draft.body
)
return MailThread(
routeID: threadRouteID,
mailbox: .sent,
subject: draft.subject.isEmpty ? "(No Subject)" : draft.subject,
participants: recipients + [me],
messages: [message],
isUnread: false,
isStarred: false,
tags: ["Sent"]
)
}
private var seededThreads: [MailThread] {
let alex = MailPerson(name: "Alex Rivera", email: "alex@social.io")
let nora = MailPerson(name: "Nora Chen", email: "nora@social.io")
let tanya = MailPerson(name: "Tanya Hall", email: "tanya@design.social.io")
let ops = MailPerson(name: "Ops Bot", email: "ops@social.io")
let investor = MailPerson(name: "Mina Park", email: "mina@northshore.vc")
return [
MailThread(
routeID: "launch-copy",
mailbox: .inbox,
subject: "Launch copy for the onboarding flow",
participants: [tanya, me],
messages: [
MailMessage(
routeID: "launch-copy-1",
sender: tanya,
recipients: [me],
sentAt: .now.addingTimeInterval(-3600 * 2),
body: "I tightened the onboarding copy and added a warmer empty-state tone. If you're good with it, I can hand the strings to the app team today."
),
MailMessage(
routeID: "launch-copy-2",
sender: me,
recipients: [tanya],
sentAt: .now.addingTimeInterval(-3600),
body: "Looks strong. Let's keep the playful language on iPhone and simplify the desktop first-run copy a bit."
)
],
isUnread: true,
isStarred: true,
tags: ["Design", "Launch"],
lane: .people,
summary: [
"Tanya tightened the onboarding copy and softened the empty-state tone.",
"Phil signed off with a note to keep playful language on iPhone.",
"Desktop first-run copy still needs a lighter pass before handoff."
]
),
MailThread(
routeID: "daily-sync-status",
mailbox: .inbox,
subject: "Daily inbox sync status",
participants: [ops, me],
messages: [
MailMessage(
routeID: "daily-sync-status-1",
sender: ops,
recipients: [me],
sentAt: .now.addingTimeInterval(-3600 * 4),
body: "Mock sync complete. 1,284 messages mirrored from staging. Attachment fetch remains disabled in the sandbox profile."
)
],
isUnread: false,
isStarred: false,
tags: ["System"],
lane: .feed
),
MailThread(
routeID: "investor-update",
mailbox: .inbox,
subject: "Investor update before next Friday",
participants: [investor, me],
messages: [
MailMessage(
routeID: "investor-update-1",
sender: investor,
recipients: [me],
sentAt: .now.addingTimeInterval(-3600 * 26),
body: "Could you send a concise product update and a few screenshots of the new mail experience before Friday? Interested in how you are differentiating from commodity inboxes."
)
],
isUnread: true,
isStarred: false,
tags: ["External"],
lane: .paper,
summary: [
"Mina wants a concise product update before Friday.",
"She's specifically asking for screenshots of the new mail experience.",
"Interested in how we differentiate from commodity inboxes."
]
),
MailThread(
routeID: "search-ranking-polish",
mailbox: .sent,
subject: "Re: Search ranking polish",
participants: [alex, me],
messages: [
MailMessage(
routeID: "search-ranking-polish-1",
sender: alex,
recipients: [me],
sentAt: .now.addingTimeInterval(-3600 * 30),
body: "The current search sort is useful, but I still feel too much recency over intent."
),
MailMessage(
routeID: "search-ranking-polish-2",
sender: me,
recipients: [alex],
sentAt: .now.addingTimeInterval(-3600 * 28),
body: "Agreed. I want us to bias toward active collaborators and threads with lightweight action language like review, approve, or ship."
)
],
isUnread: false,
isStarred: false,
tags: ["Search"],
lane: .people
),
MailThread(
routeID: "welcome-to-socialio",
mailbox: .drafts,
subject: "Welcome to social.io mail",
participants: [me, nora],
messages: [
MailMessage(
routeID: "welcome-to-socialio-1",
sender: me,
recipients: [nora],
sentAt: .now.addingTimeInterval(-3600 * 6),
body: "Thanks for joining the early design partner group. Here is a quick overview of our calm, collaborative take on email...",
isDraft: true
)
],
isUnread: false,
isStarred: false,
tags: ["Draft"],
lane: .people
),
MailThread(
routeID: "roadmap-notes",
mailbox: .archive,
subject: "Roadmap notes from product sync",
participants: [nora, alex, me],
messages: [
MailMessage(
routeID: "roadmap-notes-1",
sender: nora,
recipients: [alex, me],
sentAt: .now.addingTimeInterval(-3600 * 72),
body: "Captured the big roadmap themes: faster triage, identity-rich threads, and calmer notifications. I archived the raw notes after cleanup."
)
],
isUnread: false,
isStarred: true,
tags: ["Product"],
lane: .paper,
summary: [
"Three roadmap themes: faster triage, identity-rich threads, calmer notifications.",
"Raw notes were cleaned up and archived by Nora.",
"Owner split still needs to be reconciled with eng capacity."
]
)
]
}
}