Files
swiftapp/swift/Sources/App/MailNotificationActivity.swift
Jürgen Kunz 2fe6b8a6df WIP: local handoff implementation
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>
2026-04-19 16:26:38 +02:00

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