Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RxCodeMobile/State/MobileAppState+Inbound.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ extension MobileAppState {
} else if messagesBySession[active] == nil {
messagesBySession[active] = []
}
loadingThreadMessageSessions.remove(active)
activeSessionID = active
}
hasReceivedInitialSnapshot = true
Expand Down Expand Up @@ -362,6 +363,9 @@ extension MobileAppState {
if loadingMoreSessions.remove(previous) != nil {
loadingMoreSessions.insert(update.sessionID)
}
if loadingThreadMessageSessions.remove(previous) != nil {
loadingThreadMessageSessions.insert(update.sessionID)
}
if activeSessionID == previous {
activeSessionID = update.sessionID
}
Expand Down
21 changes: 20 additions & 1 deletion RxCodeMobile/State/MobileAppState+Intents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ extension MobileAppState {
loadingMoreSessions.contains(sessionID)
}

/// Whether the initial message window for a newly opened thread is still
/// being refreshed from the desktop.
func isLoadingThreadMessages(sessionID: String) -> Bool {
loadingThreadMessageSessions.contains(sessionID)
}

/// Mirror of the desktop's per-session queue, surfaced via `SessionSummary`.
func queuedMessages(sessionID: String) -> [QueuedUserMessage] {
sessions.first(where: { $0.id == sessionID })?.queuedMessages ?? []
Expand Down Expand Up @@ -187,6 +193,7 @@ extension MobileAppState {
/// Archive a thread. Optimistically flips `isArchived` so the row drops out
/// of the active list right away.
func archiveThread(sessionID: String) async {
loadingThreadMessageSessions.remove(sessionID)
replaceSession(sessionID: sessionID) { current in
SessionSummary(
id: current.id,
Expand All @@ -210,6 +217,7 @@ extension MobileAppState {
messagesBySession.removeValue(forKey: sessionID)
sessionsWithMoreMessages.remove(sessionID)
loadingMoreSessions.remove(sessionID)
loadingThreadMessageSessions.remove(sessionID)
if activeSessionID == sessionID { activeSessionID = nil }
await sendThreadAction(sessionID: sessionID, action: .delete)
}
Expand Down Expand Up @@ -276,7 +284,18 @@ extension MobileAppState {
func subscribe(to sessionID: String?) async {
activeSessionID = sessionID
guard isPaired else { return }
if let sessionID {
loadingThreadMessageSessions = [sessionID]
} else {
loadingThreadMessageSessions.removeAll()
}
let payload = SubscribeSessionPayload(sessionID: sessionID)
try? await client.send(.subscribeSession(payload), toHex: pairedDesktopPubkey)
do {
try await client.send(.subscribeSession(payload), toHex: pairedDesktopPubkey)
} catch {
if let sessionID {
loadingThreadMessageSessions.remove(sessionID)
}
}
}
}
3 changes: 3 additions & 0 deletions RxCodeMobile/State/MobileAppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ final class MobileAppState: ObservableObject {
@Published var sessionsWithMoreMessages: Set<String> = []
/// Sessions with an in-flight `load_more_messages` request.
@Published var loadingMoreSessions: Set<String> = []
/// Sessions whose initial message page is being refreshed after opening the
/// chat detail view.
@Published var loadingThreadMessageSessions: Set<String> = []
/// Maps an outstanding load-more request ID to its session, so a late
/// `more_messages` reply lands on the right thread.
var pendingLoadMoreRequests: [UUID: String] = [:]
Expand Down
4 changes: 3 additions & 1 deletion RxCodeMobile/Views/MobileBriefingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct GroupedBriefing: Identifiable {
struct MobileBriefingView: View {
@EnvironmentObject private var state: MobileAppState
@Namespace private var glassNamespace
var onCloseChat: () -> Void = {}

/// Selected project ids for filtering. Empty = show every project.
@State private var selectedProjectIds: Set<UUID> = []
Expand Down Expand Up @@ -84,8 +85,9 @@ struct MobileBriefingView: View {
MobileBriefingDetailView(groupKey: key)
}
.navigationDestination(for: String.self) { sessionID in
MobileChatView(sessionID: sessionID)
MobileChatView(sessionID: sessionID, onClose: onCloseChat)
.id(sessionID)
.toolbar(.hidden, for: .tabBar)
.task(id: sessionID) {
if !MobileDraftSessionID.isDraft(sessionID) {
await state.subscribe(to: sessionID)
Expand Down
36 changes: 36 additions & 0 deletions RxCodeMobile/Views/MobileChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ struct MobileChatView: View {
/// and should be pinned to the top of the viewport.
@State private var awaitingSentUserMessage = false
@State private var distanceFromBottom: CGFloat = 0
@State private var minimumThreadLoadElapsed = false

private static let bottomAnchorID = "message-list-bottom"
/// Distance from the bottom past which the "scroll to bottom" button shows.
Expand All @@ -83,9 +84,13 @@ struct MobileChatView: View {
.animation(.easeInOut(duration: 0.2), value: queuedMessages.count)
.animation(.easeInOut(duration: 0.2), value: sessionQuestions.count)
.animation(.easeInOut(duration: 0.2), value: pendingPlans.count)
.animation(.easeInOut(duration: 0.25), value: shouldShowThreadLoading)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(title)
.toolbar { threadActionsToolbar }
.task(id: sessionID) {
await runThreadLoadingGate()
}
.sheet(isPresented: $showingQueueSheet) {
QueuedMessagesSheet(
messages: queuedMessages,
Expand Down Expand Up @@ -488,6 +493,17 @@ struct MobileChatView: View {
} action: { newValue in
composerMinY = newValue
}

if shouldShowThreadLoading {
MobileThreadLoadingOverlay()
.transition(
.asymmetric(
insertion: .opacity.combined(with: .scale(scale: 0.98)),
removal: .opacity.combined(with: .move(edge: .bottom))
)
)
.zIndex(3)
}
}
}

Expand Down Expand Up @@ -540,6 +556,11 @@ struct MobileChatView: View {
state.isLoadingMoreMessages(sessionID: sessionID)
}

private var shouldShowThreadLoading: Bool {
!MobileDraftSessionID.isDraft(sessionID)
&& (!minimumThreadLoadElapsed || state.isLoadingThreadMessages(sessionID: sessionID))
}

private var queuedMessages: [QueuedUserMessage] {
state.queuedMessages(sessionID: sessionID)
}
Expand All @@ -560,6 +581,21 @@ struct MobileChatView: View {
sessionPlans.filter { !$0.isDecided && !$0.isStreaming }
}

// MARK: - Initial loading

private func runThreadLoadingGate() async {
minimumThreadLoadElapsed = false
do {
try await Task.sleep(for: .seconds(1))
} catch {
return
}
guard !Task.isCancelled else { return }
withAnimation(.easeInOut(duration: 0.25)) {
minimumThreadLoadElapsed = true
}
}

// MARK: - Message paging

/// Spinner shown at the top of the list while an older page is loading.
Expand Down
Loading