eleven / messenger Public

ios: refresh the chat list on foreground and on incoming pushes (#126)

The native thread list only fetched /api/threads on first appear or pull-to-refresh, and creating a thread sends no push (only messages do) — so a chat someone started with you never showed up until a manual refresh or a relaunch. Re-fetch the active server's list when the app returns to the foreground, and live when a push lands for a chat other than the one on-screen. Both are guarded against stacking on an in-flight load (the cold-launch task, a pull-to-refresh, or an overlapping trigger).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paul Campbell dcfd2aafd41a42c90e16f1bf36dc4fbbb8ac8e3a parent 6331bea
2 files changed, +25 −0
  • ios/LChat/LChatApp.swift +15 −0
  • ios/LChat/PushManager.swift +10 −0
diff --git a/ios/LChat/LChatApp.swift b/ios/LChat/LChatApp.swift
index 634ce00..9893675 100644
--- a/ios/LChat/LChatApp.swift
+++ b/ios/LChat/LChatApp.swift
@@ -71,6 +71,7 @@ final class NavModel: ObservableObject {
struct RootView: View {
@EnvironmentObject var app: AppState
@EnvironmentObject private var push: PushManager
+ @Environment(\.scenePhase) private var scenePhase
@StateObject private var nav = NavModel()
@State private var path: [String] = [] // pushed thread ids
@State private var showServers = false
@@ -132,6 +133,20 @@ struct RootView: View {
// A tap that arrives while we're already up: the published id changes and
// this fires. (onAppear covers the cold-launch case where it was set first.)
.onChange(of: push.pendingThreadID) { _, _ in routeNotificationTap() }
+ // Coming back to the app re-fetches the active server's chats, so a chat
+ // started with us while we were away (thread creation sends no push) is
+ // there when we look. The `.task(id:)` in ThreadListView only fires on first
+ // appear / server switch, so foregrounding otherwise never refreshed.
+ .onChange(of: scenePhase) { _, phase in refreshInbox(when: phase == .active) }
+ // A push landing while we're open (see PushManager.inboxPing) refreshes live.
+ .onChange(of: push.inboxPing) { _, _ in refreshInbox(when: true) }
+ }
+
+ // Re-fetch the active server's thread list, unless one is already in flight (the
+ // cold-launch `.task` load, a pull-to-refresh, or an overlapping trigger).
+ private func refreshInbox(when: Bool) {
+ guard when, let store = nav.store, !store.loading else { return }
+ Task { await store.refresh() }
}
// A tapped notification lands in its chat: the shared key store knows which
diff --git a/ios/LChat/PushManager.swift b/ios/LChat/PushManager.swift
index 0e652f7..02352ac 100644
--- a/ios/LChat/PushManager.swift
+++ b/ios/LChat/PushManager.swift
@@ -21,6 +21,12 @@ final class PushManager: NSObject, ObservableObject, UIApplicationDelegate, UNUs
// appear, so the tap routes whether it arrives before or after the view is up.
@Published var pendingThreadID: String?
+ // Bumped whenever a push lands while the app is open, for a thread other than
+ // the one on-screen. RootView watches it and re-fetches the active thread list,
+ // so a chat someone *just* started with us (or a message in another chat) shows
+ // up live instead of only after a pull-to-refresh or relaunch.
+ @Published var inboxPing = 0
+
// A thread that should jump straight to its newest message the next time it
// appears — set on a notification tap, so "you have a new message" always lands
// on that message even if the parked feed had been scrolled up into history.
@@ -96,6 +102,10 @@ final class PushManager: NSObject, ObservableObject, UIApplicationDelegate, UNUs
completionHandler([])
return
}
+ // A push for a chat we're not reading — the list may be stale (a brand-new
+ // chat isn't in it at all). Nudge RootView to re-fetch. (UN callbacks run on
+ // the main thread, so touching @Published here is safe.)
+ inboxPing &+= 1
completionHandler([.banner, .sound])
}