Files
WRNexus-Mobile-SDKs/Sources/WRNexusInbox/InboxView.swift
T

74 lines
5.7 KiB
Swift

#if canImport(SwiftUI)
import SwiftUI
/// A complete recipient inbox, using organization branding returned by the recipient API.
@available(iOS 15.0, macOS 12.0, *)
public struct InboxView: View {
private let client: InboxClient
private let onAction: (InboxMessage, InboxAction?) -> Void
@State private var snapshot: InboxSnapshot?
@State private var branding: InboxBranding?
@State private var error: String?
@State private var busy: Set<String> = []
@State private var generation=0
@State private var recipientClient: ObjectIdentifier?
@Environment(\.scenePhase) private var scenePhase
public init(client: InboxClient, onAction: @escaping (InboxMessage, InboxAction?) -> Void = {_,_ in}) { self.client=client; self.onAction=onAction }
private var visibleBranding: InboxBranding? { recipientClient == ObjectIdentifier(client) ? branding : nil }
private var brand: Color { color(visibleBranding?.brandColor) ?? Color.accentColor }
public var body: some View {
let currentSnapshot = recipientClient == ObjectIdentifier(client) ? snapshot : nil
VStack(alignment:.leading,spacing:0) {
HStack(spacing:12) {
if let branding=visibleBranding, let url=client.logoURL(branding) { AsyncImage(url:url) { image in image.resizable().scaledToFit() } placeholder: { Color.clear }.frame(width:36,height:36).accessibilityHidden(true) }
VStack(alignment:.leading) { Text(visibleBranding?.displayName ?? "Notifications").font(.title2.bold()); if let snapshot=currentSnapshot { Text("\(snapshot.unreadCount) unread").font(.subheadline).foregroundStyle(.secondary) } }
Spacer()
Button { generation += 1 } label: { Image(systemName:"arrow.clockwise") }.accessibilityLabel("Refresh notifications")
}.padding().overlay(alignment:.bottom) { Rectangle().fill(brand).frame(height:2) }
if let error,recipientClient == ObjectIdentifier(client) { HStack { Text(error).font(.callout); Spacer(); Button("Retry") {generation += 1} }.padding().accessibilityElement(children:.contain) }
if let snapshot=currentSnapshot {
if snapshot.messages.isEmpty { VStack(spacing:8) { Image(systemName:"bell").font(.largeTitle).foregroundStyle(brand); Text("No notifications yet").font(.headline); Text("Your notifications will appear here.").foregroundStyle(.secondary) }.frame(maxWidth:.infinity,maxHeight:.infinity).padding() }
else { List(snapshot.messages) { message in
VStack(alignment:.leading,spacing:8) {
HStack { if message.readAt == nil { Circle().fill(brand).frame(width:8,height:8).accessibilityLabel("Unread") }; Text(message.title).font(.headline); Spacer() }
Text(message.body).font(.body)
Text(message.createdAt).font(.caption).foregroundStyle(.secondary)
HStack {
if message.readAt == nil { Button("Mark read") { mutate(message,archive:false) } }
Button("Archive") { mutate(message,archive:true) }
if let url=InboxClient.safeActionURL(message.url) { Link("Open",destination:url).simultaneousGesture(TapGesture().onEnded { onAction(message,nil) }) }
}.buttonStyle(.borderless).disabled(busy.contains(message.id))
ForEach(message.actions ?? [],id:\.id) { action in
if let url=InboxClient.safeActionURL(action.url) { Link(action.label,destination:url).simultaneousGesture(TapGesture().onEnded {onAction(message,action)}) }
else if action.url == nil { Button(action.label) {onAction(message,action)}.buttonStyle(.borderless) }
}
}.padding(.vertical,8)
}.listStyle(.plain) }
} else if error == nil || recipientClient != ObjectIdentifier(client) { ProgressView("Loading notifications").frame(maxWidth:.infinity,maxHeight:.infinity) }
}.tint(.primary).task(id:"\(ObjectIdentifier(client)):\(generation)") { await load() }
.onChange(of:scenePhase) { _ in generation += 1 }
}
@MainActor private func load() async {
guard scenePhase == .active else {return}
let identity=ObjectIdentifier(client)
if recipientClient != identity {snapshot=nil;branding=nil;busy=[];recipientClient=identity}
error=nil
do { let settings=try await client.preferences();let first=try await client.messages();try Task.checkCancellation();guard recipientClient==identity else{return};branding=settings.branding;snapshot=first; for try await value in client.updates() { try Task.checkCancellation();guard recipientClient==identity else{return};snapshot=value } }
catch { if !Task.isCancelled && recipientClient==identity { self.error=error.localizedDescription } }
}
private func mutate(_ message: InboxMessage, archive: Bool) {
let identity=ObjectIdentifier(client)
busy.insert(message.id)
Task { @MainActor in
defer {if recipientClient==identity {busy.remove(message.id)}}
do { if archive { _=try await client.archive(message.id) } else { _=try await client.markRead(message.id) }; let value=try await client.messages();if recipientClient==identity {snapshot=value} }
catch {if recipientClient==identity {self.error=error.localizedDescription}}
}
}
private func color(_ text: String?) -> Color? {
guard let text,text.range(of:"^#[a-fA-F0-9]{6}$",options:.regularExpression) != nil, let hex=UInt32(text.dropFirst(),radix:16) else {return nil}
return Color(red:Double((hex>>16)&255)/255,green:Double((hex>>8)&255)/255,blue:Double(hex&255)/255)
}
}
#endif