ios: add pull to refresh

This commit is contained in:
Sybil Codex
2026-05-03 19:27:06 +00:00
parent bca408c971
commit 9572d0320f
6 changed files with 78 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ struct SybilChatTranscriptView: View {
var messages: [Message] var messages: [Message]
var isLoading: Bool var isLoading: Bool
var isSending: Bool var isSending: Bool
var onRefresh: (() async -> Void)? = nil
@State private var hasHandledInitialTranscriptScroll = false @State private var hasHandledInitialTranscriptScroll = false
private var hasPendingAssistant: Bool { private var hasPendingAssistant: Bool {
@@ -51,6 +52,10 @@ struct SybilChatTranscriptView: View {
.padding(.vertical, 18) .padding(.vertical, 18)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.refreshable {
await onRefresh?()
}
.tint(SybilTheme.primary)
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
.onAppear { .onAppear {
scrollToBottom(with: proxy, animated: false) scrollToBottom(with: proxy, animated: false)

View File

@@ -132,6 +132,10 @@ private struct SybilPhoneSidebarRoot: View {
} }
.padding(10) .padding(10)
} }
.refreshable {
await viewModel.refreshCollectionsFromUser()
}
.tint(SybilTheme.primary)
} }
} }
.background(SybilTheme.panelGradient) .background(SybilTheme.panelGradient)

View File

@@ -6,6 +6,7 @@ struct SybilSearchResultsView: View {
var isLoading: Bool var isLoading: Bool
var isRunning: Bool var isRunning: Bool
var isStartingChat: Bool = false var isStartingChat: Bool = false
var onRefresh: (() async -> Void)? = nil
var onStartChat: (() -> Void)? = nil var onStartChat: (() -> Void)? = nil
var body: some View { var body: some View {
@@ -100,6 +101,10 @@ struct SybilSearchResultsView: View {
.padding(.horizontal, 14) .padding(.horizontal, 14)
.padding(.vertical, 20) .padding(.vertical, 20)
} }
.refreshable {
await onRefresh?()
}
.tint(SybilTheme.primary)
.scrollDismissesKeyboard(.interactively) .scrollDismissesKeyboard(.interactively)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }

View File

@@ -149,6 +149,10 @@ struct SybilSidebarView: View {
} }
.padding(10) .padding(10)
} }
.refreshable {
await viewModel.refreshCollectionsFromUser()
}
.tint(SybilTheme.primary)
} }
} }

View File

@@ -471,6 +471,34 @@ final class SybilViewModel {
select(nextSelection) select(nextSelection)
} }
func refreshCollectionsFromUser() async {
guard isAuthenticated else {
return
}
errorMessage = nil
guard draftKind == nil else {
await refreshCollectionsPreservingDraft()
return
}
await refreshCollections(preferredSelection: selectedItem)
}
func refreshSelectionFromUser() async {
guard isAuthenticated, !isSending, !isCreatingSearchChat else {
return
}
guard selectedItem != nil, draftKind == nil else {
return
}
errorMessage = nil
await refreshSelectionIfNeeded()
}
func deleteItem(_ selection: SidebarSelection) async { func deleteItem(_ selection: SidebarSelection) async {
guard isAuthenticated else { guard isAuthenticated else {
return return
@@ -710,6 +738,30 @@ final class SybilViewModel {
settings.persist() settings.persist()
} }
private func refreshCollectionsPreservingDraft() async {
isLoadingCollections = true
do {
let client = try client()
async let chatsValue = client.listChats()
async let searchesValue = client.listSearches()
let (nextChats, nextSearches) = try await (chatsValue, searchesValue)
chats = nextChats
searches = nextSearches
SybilLog.info(
SybilLog.app,
"Refreshed collections for draft: \(nextChats.count) chats, \(nextSearches.count) searches"
)
} catch {
errorMessage = normalizeAPIError(error)
SybilLog.error(SybilLog.app, "Refresh draft collections failed", error: error)
}
isLoadingCollections = false
}
private func refreshCollections( private func refreshCollections(
preferredSelection: SidebarSelection?, preferredSelection: SidebarSelection?,
refreshSelection: Bool = true refreshSelection: Bool = true

View File

@@ -127,7 +127,10 @@ struct SybilWorkspaceView: View {
search: viewModel.selectedSearch, search: viewModel.selectedSearch,
isLoading: viewModel.isLoadingSelection, isLoading: viewModel.isLoadingSelection,
isRunning: viewModel.isSending, isRunning: viewModel.isSending,
isStartingChat: viewModel.isCreatingSearchChat isStartingChat: viewModel.isCreatingSearchChat,
onRefresh: {
await viewModel.refreshSelectionFromUser()
}
) { ) {
Task { Task {
await viewModel.startChatFromSelectedSearch() await viewModel.startChatFromSelectedSearch()
@@ -137,7 +140,10 @@ struct SybilWorkspaceView: View {
SybilChatTranscriptView( SybilChatTranscriptView(
messages: viewModel.displayedMessages, messages: viewModel.displayedMessages,
isLoading: viewModel.isLoadingSelection, isLoading: viewModel.isLoadingSelection,
isSending: viewModel.isSending isSending: viewModel.isSending,
onRefresh: {
await viewModel.refreshSelectionFromUser()
}
) )
.id(transcriptScrollContextID) .id(transcriptScrollContextID)
} }