feat: add authenticated durable native Hope chat

This commit is contained in:
snowspeeder committed 2026-10-03 09:06:06 -04:00
1 parent 74d2b63614
commit 7dcd06c911
15 files changed
+1675 -9

No files matched your search

@@ -11,7 +11,8 @@ struct ConversationView: View {
@State private var activeServerURL: String
@State private var pendingServerURL: String?
@State private var isReplacingSession = false
@State private var selectedTab = 0
@State private var selectedTab = 2
@StateObject private var chat: ChatStore
init(settings: SettingsStore, auth: AuthStore, user: AuthUser, bearerToken: String) {
self.auth = auth
@@ -21,6 +22,11 @@ struct ConversationView: View {
let url = settings.trimmedServerURL
_activeServerURL = State(initialValue: url)
_session = State(initialValue: Self.makeSession(url, settings: settings, bearerToken: bearerToken, userID: user.id))
let api: ChatTransport = auth.fixture ? OfflineChatAPI() : ChatAPI(config: auth.config, token: bearerToken)
_chat = StateObject(wrappedValue: ChatStore(api: api))
#if DEBUG
if auth.fixture && !ProcessInfo.processInfo.arguments.contains("--ui-chat") { _selectedTab = State(initialValue: 0) }
#endif
}
private static func makeSession(_ url: String, settings: SettingsStore, bearerToken: String, userID: String) -> VoiceSession {
@@ -49,6 +55,14 @@ struct ConversationView: View {
var body: some View {
TabView(selection: $selectedTab) {
NativeChatView(store: chat, talk: { id in
await session.bindConversation(id)
selectedTab = 0
}, logout: {
await auth.logout { chat.close(); await session.disconnect() }
})
.tabItem { Label("Chats", systemImage: "bubble.left.and.text.bubble.right") }
.tag(2)
NavigationStack {
ConversationContent(settings: settings, session: session, displayName: user.displayName ?? user.username)
.toolbar {
@@ -56,7 +70,7 @@ struct ConversationView: View {
ToolbarItem(placement: .topBarLeading) {
Menu {
Button("Sign out", role: .destructive) {
Task { await auth.logout { await session.disconnect() } }
Task { await auth.logout { chat.close(); await session.disconnect() } }
}
} label: {
Image(systemName: "person.crop.circle")
@@ -72,7 +86,8 @@ struct ConversationView: View {
ActivityTab(taskStore: session.taskStore, settings: settings, session: session)
.tag(1)
}
.tint(Nearlight.rosewood)
.tint(HopeTheme.color(.accent, palette: HopePalette(rawValue: chat.preferences.palette) ?? .opal))
.preferredColorScheme(chat.preferences.appearance == "system" ? nil : (chat.preferences.appearance == "dark" ? .dark : .light))
.onChange(of: settings.textMode) { _, enabled in
Task { await session.applyTextMode(enabled) }
}
@@ -80,10 +95,13 @@ struct ConversationView: View {
if auth.fixture { replaceServer(value) }
}
.onAppear {
auth.disconnectAccount = { [weak session] in await session?.disconnect() }
session.onUnauthorized = { [weak auth, weak session] in
await auth?.logout { await session?.disconnect() }
auth.disconnectAccount = { [weak session, weak chat] in chat?.close(); await session?.disconnect() }
let unauthorized: () async -> Void = { [weak auth, weak session, weak chat] in
await auth?.logout { chat?.close(); await session?.disconnect() }
}
session.onUnauthorized = unauthorized
chat.onUnauthorized = unauthorized
chat.features.onUnauthorized = unauthorized
}
.disabled(auth.isBusy || auth.token == nil)
}