From 3852d1882227d77073daae090102541a22e79faf Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 19 May 2026 05:54:42 +0800 Subject: [PATCH] fix: mask native tab transition briefly --- ios/App/App/LiquidTabsPlugin.swift | 13 +++++++++++++ ios/App/App/LiquidTabsRootView.swift | 24 ++++++++++++++++++++++++ ios/App/App/LiquidTabsStore.swift | 26 ++++++++++++++++++++++++++ src/main/mobile/bottom_tabs.cljs | 5 +++++ src/main/mobile/components/app.cljs | 12 ++++++++++++ 5 files changed, 80 insertions(+) diff --git a/ios/App/App/LiquidTabsPlugin.swift b/ios/App/App/LiquidTabsPlugin.swift index ae1d44195f..54df3514c4 100644 --- a/ios/App/App/LiquidTabsPlugin.swift +++ b/ios/App/App/LiquidTabsPlugin.swift @@ -19,6 +19,7 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "selectTab", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "updateNativeSearchResults", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "updateNativeGraphs", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "markTabContentReady", returnType: CAPPluginReturnPromise), ] public override func load() { @@ -214,6 +215,18 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin { call.resolve() } + /// Clear the native transition cover after JS has rendered a WebView-backed tab. + /// { id: string } + @objc func markTabContentReady(_ call: CAPPluginCall) { + guard let id = call.getString("id") else { + call.reject("Missing 'id'") + return + } + + store.markWebTabReady(id) + call.resolve() + } + func openGraph(_ graph: NativeGraphItem) { notifyListeners("nativeGraphAction", data: [ "action": "open", diff --git a/ios/App/App/LiquidTabsRootView.swift b/ios/App/App/LiquidTabsRootView.swift index c41bb773d3..015672e29b 100644 --- a/ios/App/App/LiquidTabsRootView.swift +++ b/ios/App/App/LiquidTabsRootView.swift @@ -183,7 +183,25 @@ private struct LiquidTabs26View: View { isSearchFocused = true } + private func webBackedTabId(for selection: LiquidTabsTabSelection) -> String? { + guard let id = store.tabId(for: selection), + id != "graphs", + id != "search" else { + return nil + } + + return id + } + + private func waitForWebBackedTab(_ selection: LiquidTabsTabSelection) { + if let id = webBackedTabId(for: selection) { + store.waitForWebTab(id) + } + } + private func prepareForSelectionChange(to selection: LiquidTabsTabSelection) { + waitForWebBackedTab(selection) + switch selection { case .search: store.suppressSearchNotifications = true @@ -279,6 +297,12 @@ private struct LiquidTabs26View: View { SearchFocusBridge(isActive: selectedTab == .search) .frame(width: 0, height: 0) } + .overlay { + if store.pendingWebTabId != nil { + Color.logseqBackground + .ignoresSafeArea() + } + } } .onAppear { diff --git a/ios/App/App/LiquidTabsStore.swift b/ios/App/App/LiquidTabsStore.swift index 471714817b..487bc95b7c 100644 --- a/ios/App/App/LiquidTabsStore.swift +++ b/ios/App/App/LiquidTabsStore.swift @@ -78,6 +78,8 @@ final class LiquidTabsStore: ObservableObject { @Published var graphSections: [NativeGraphSection] = [] @Published var graphLabels = NativeGraphLabels() @Published var nativeGraphsVisible = true + @Published var pendingWebTabId: String? + private var pendingWebTabRequestId = 0 // Helper to get a stable selection if JS forgets func effectiveSelectedId() -> String? { @@ -107,4 +109,28 @@ final class LiquidTabsStore: ObservableObject { } } + func waitForWebTab(_ id: String) { + DispatchQueue.main.async { + self.pendingWebTabRequestId += 1 + let requestId = self.pendingWebTabRequestId + self.pendingWebTabId = id + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { + guard self.pendingWebTabRequestId == requestId, + self.pendingWebTabId == id else { + return + } + + self.pendingWebTabId = nil + } + } + } + + func markWebTabReady(_ id: String) { + DispatchQueue.main.async { + guard self.pendingWebTabId == id else { return } + self.pendingWebTabRequestId += 1 + self.pendingWebTabId = nil + } + } } diff --git a/src/main/mobile/bottom_tabs.cljs b/src/main/mobile/bottom_tabs.cljs index 21a989fe9d..74b0214062 100644 --- a/src/main/mobile/bottom_tabs.cljs +++ b/src/main/mobile/bottom_tabs.cljs @@ -54,6 +54,11 @@ (when (and (mobile-util/native-ios?) liquid-tabs (.-updateNativeGraphs liquid-tabs)) (.updateNativeGraphs liquid-tabs (clj->js payload)))) +(defn mark-tab-content-ready! + [id] + (when (and (mobile-util/native-ios?) liquid-tabs (.-markTabContentReady liquid-tabs)) + (.markTabContentReady liquid-tabs #js {:id id}))) + (defn add-tab-selected-listener! "Listen to native tab selection. diff --git a/src/main/mobile/components/app.cljs b/src/main/mobile/components/app.cljs index f8590c892a..36c19b717c 100644 --- a/src/main/mobile/components/app.cljs +++ b/src/main/mobile/components/app.cljs @@ -294,6 +294,18 @@ [theme] (frum/use-atom-in state/state :ui/theme)] (use-screen-size-effects!) (use-theme-effects! current-repo theme) + (hooks/use-effect! + (fn [] + (when (mobile-util/native-ios?) + (.requestAnimationFrame + js/window + (fn [] + (.requestAnimationFrame + js/window + (fn [] + (bottom-tabs/mark-tab-content-ready! tab)))))) + nil) + [tab route-match]) (hooks/use-effect! (fn [] (when-let [element (util/app-scroll-container-node)]