Local work on the social.io handoff before merging the claude worktree branch. Includes the full per-spec Sources/Core/Design module (8 files), watchOS target under WatchApp/, Live Activity + widget extension, entitlements, scheme, and asset catalog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
5.6 KiB
Swift
169 lines
5.6 KiB
Swift
#if os(iOS) && canImport(ActivityKit) && canImport(AppIntents) && canImport(WidgetKit)
|
|
import ActivityKit
|
|
import AppIntents
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
struct MailNotificationActivityAttributes: ActivityAttributes {
|
|
struct ContentState: Codable, Hashable {
|
|
var sender: String
|
|
var initials: String
|
|
var subject: String
|
|
var preview: String
|
|
var route: String
|
|
}
|
|
|
|
var threadRouteID: String
|
|
}
|
|
|
|
struct OpenMailLiveActivityIntent: LiveActivityIntent {
|
|
static var title: LocalizedStringResource = "Open"
|
|
static var openAppWhenRun = true
|
|
|
|
@Parameter(title: "Route")
|
|
var route: String
|
|
|
|
init() {
|
|
route = "socialio://mailbox/inbox"
|
|
}
|
|
|
|
init(route: String) {
|
|
self.route = route
|
|
}
|
|
|
|
func perform() async throws -> some IntentResult {
|
|
.result()
|
|
}
|
|
}
|
|
|
|
struct SnoozeMailLiveActivityIntent: LiveActivityIntent {
|
|
static var title: LocalizedStringResource = "Snooze"
|
|
|
|
func perform() async throws -> some IntentResult {
|
|
.result()
|
|
}
|
|
}
|
|
|
|
struct MailNotificationLiveActivity: Widget {
|
|
var body: some WidgetConfiguration {
|
|
ActivityConfiguration(for: MailNotificationActivityAttributes.self) { context in
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
header(context: context)
|
|
|
|
Text(context.state.preview)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
|
|
HStack(spacing: 10) {
|
|
Button(intent: OpenMailLiveActivityIntent(route: context.state.route)) {
|
|
Text("Open")
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
Button(intent: SnoozeMailLiveActivityIntent()) {
|
|
Text("Snooze")
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
}
|
|
.padding(.vertical, 6)
|
|
} dynamicIsland: { context in
|
|
DynamicIsland {
|
|
DynamicIslandExpandedRegion(.leading) {
|
|
Image(systemName: "envelope.fill")
|
|
.foregroundStyle(SIO.tint)
|
|
}
|
|
|
|
DynamicIslandExpandedRegion(.trailing) {
|
|
Text(context.state.initials)
|
|
.font(.headline.weight(.semibold))
|
|
}
|
|
|
|
DynamicIslandExpandedRegion(.center) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
header(context: context)
|
|
Text(context.state.preview)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(2)
|
|
}
|
|
}
|
|
|
|
DynamicIslandExpandedRegion(.bottom) {
|
|
HStack(spacing: 10) {
|
|
Button(intent: OpenMailLiveActivityIntent(route: context.state.route)) {
|
|
Text("Open")
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
Button(intent: SnoozeMailLiveActivityIntent()) {
|
|
Text("Snooze")
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
}
|
|
} compactLeading: {
|
|
Image(systemName: "envelope.fill")
|
|
} compactTrailing: {
|
|
Text(context.state.initials)
|
|
.font(.caption2.weight(.bold))
|
|
} minimal: {
|
|
Image(systemName: "envelope.fill")
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func header(context: ActivityViewContext<MailNotificationActivityAttributes>) -> some View {
|
|
HStack(spacing: 10) {
|
|
Text(context.state.initials)
|
|
.font(.caption.weight(.bold))
|
|
.foregroundStyle(SIO.tint)
|
|
.frame(width: 28, height: 28)
|
|
.background(SIO.tint.opacity(0.12), in: Circle())
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(context.state.sender)
|
|
.font(.subheadline.weight(.semibold))
|
|
Text(context.state.subject)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if !WIDGET_EXTENSION
|
|
enum MailNotificationActivityController {
|
|
static func startIfNeeded(with thread: MailThread) async {
|
|
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
|
|
|
let attributes = MailNotificationActivityAttributes(threadRouteID: thread.routeID)
|
|
let state = MailNotificationActivityAttributes.ContentState(
|
|
sender: thread.latestMessage?.sender.name ?? thread.participants.first?.name ?? "social.io",
|
|
initials: initials(from: thread.latestMessage?.sender.name ?? thread.participants.first?.name ?? "SI"),
|
|
subject: thread.subject,
|
|
preview: thread.previewText,
|
|
route: "socialio://open?thread=\(thread.routeID)"
|
|
)
|
|
|
|
if let existing = Activity<MailNotificationActivityAttributes>.activities.first {
|
|
await existing.update(ActivityContent(state: state, staleDate: nil))
|
|
return
|
|
}
|
|
|
|
_ = try? Activity<MailNotificationActivityAttributes>.request(
|
|
attributes: attributes,
|
|
content: ActivityContent(state: state, staleDate: nil)
|
|
)
|
|
}
|
|
|
|
private static func initials(from name: String) -> String {
|
|
String(name.split(separator: " ").prefix(2).compactMap { $0.first }).uppercased()
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|