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

View File

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

View File

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

View File

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

View File

@@ -471,6 +471,34 @@ final class SybilViewModel {
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 {
guard isAuthenticated else {
return
@@ -710,6 +738,30 @@ final class SybilViewModel {
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(
preferredSelection: SidebarSelection?,
refreshSelection: Bool = true

View File

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