#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 = [] @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