import LChatCore import SwiftUI import WebKit // 🤖 A space-wide app's full-pane surface on iOS (docs/native-apps.md phase // 4): the webview seeded with the SPACE thread + its key, wearing the app's // identity — bootEmbed's space flag routes it through the web's own // openSpaceApp path (the space thread has no chat surface). Pushed from the // chat list's space band. struct SpaceAppScreen: View { let store: ThreadStore let app: String var body: some View { Group { if let thread = store.spaceApps.surfaceThread( app: app, title: store.registry.title(app), glyph: store.registry.glyph(app)) { SpaceAppSurface(instance: store.instance, thread: thread, myUserId: store.myUserId, instanceKeyB64: store.instanceKey(), app: app) } else { ContentUnavailableView("This app isn't ready yet", systemImage: "square.grid.2x2", description: Text("Try again in a moment.")) } } .navigationTitle(store.registry.title(app)) .navigationBarTitleDisplayMode(.inline) } } // The webview plus its bridge-observing chrome: the inline app-update offer // (AppUpdateBar) above the pane, so a space app that changed while on screen // asks before reloading like every other app surface. private struct SpaceAppSurface: View { @ObservedObject private var bridge: WebBridge let instance: SavedInstance let thread: ChatThread let myUserId: String let instanceKeyB64: String? let app: String @MainActor init(instance: SavedInstance, thread: ChatThread, myUserId: String, instanceKeyB64: String?, app: String) { self.instance = instance self.thread = thread self.myUserId = myUserId self.instanceKeyB64 = instanceKeyB64 self.app = app bridge = FeedWebCache.shared.bridge(for: instance, thread: thread, myUserId: myUserId, instanceKeyB64: instanceKeyB64, app: app, space: true) } var body: some View { VStack(spacing: 0) { AppUpdateBar(bridge: bridge) SpaceAppWebView(instance: instance, thread: thread, myUserId: myUserId, instanceKeyB64: instanceKeyB64, app: app) .ignoresSafeArea(edges: .bottom) } } } private struct SpaceAppWebView: UIViewRepresentable { let instance: SavedInstance let thread: ChatThread let myUserId: String let instanceKeyB64: String? let app: String func makeUIView(context: Context) -> WKWebView { FeedWebCache.shared.webView(for: instance, thread: thread, myUserId: myUserId, instanceKeyB64: instanceKeyB64, app: app, space: true) } func updateUIView(_ web: WKWebView, context: Context) {} } // 🤖 The chat list's SPACE BAND: one chip per space-wide install with an // openable surface — the iOS shape of the web sidebar's space rows (the // UIKit-backed thread list can't grow SwiftUI rows, so the band rides a // safe-area inset: in the layout, never floating over rows someone is // reading). Its own view so it observes SpaceAppsStore and AppRegistry // directly. struct SpaceBand: View { @ObservedObject var spaceApps: SpaceAppsStore @ObservedObject var registry: AppRegistry let onOpen: (String) -> Void private var apps: [AppManifest] { registry.knownSpace(spaceApps.installs) .compactMap { registry.manifest($0.id) } .filter { $0.surface ?? true } } var body: some View { if !apps.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(apps) { app in Button { onOpen(app.name) } label: { HStack(spacing: 6) { Text(app.glyph).font(.system(size: 15)) Text(app.title) .font(.subheadline.weight(.medium)) .lineLimit(1) } .padding(.horizontal, 12) .padding(.vertical, 7) .floatingGlass(in: Capsule()) .contentShape(Capsule()) } .buttonStyle(.plain) .accessibilityLabel("\(app.title), space app") .accessibilityHint("Opens the app shared with everyone in this space") } } .padding(.horizontal, 14) .padding(.vertical, 8) } } } }