Initial social.io Swift app
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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]
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
routeID: String = UUID().uuidString.lowercased(),
|
||||
mailbox: Mailbox,
|
||||
subject: String,
|
||||
participants: [MailPerson],
|
||||
messages: [MailMessage],
|
||||
isUnread: Bool,
|
||||
isStarred: Bool,
|
||||
tags: [String] = []
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
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 = ""
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
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"]
|
||||
),
|
||||
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"]
|
||||
),
|
||||
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"]
|
||||
),
|
||||
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"]
|
||||
),
|
||||
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"]
|
||||
),
|
||||
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"]
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user