From 6e92a32c45c68e177ebe44a44a68664a91ecbef4 Mon Sep 17 00:00:00 2001 From: charlie Date: Wed, 13 May 2026 16:54:51 +0800 Subject: [PATCH] refactor: simplify navigation handling and improve event logging --- .../main/java/com/logseq/app/ComposeHost.kt | 22 +-- .../java/com/logseq/app/LiquidTabsPlugin.kt | 14 +- .../java/com/logseq/app/MainActivity.java | 64 +++------ src/main/mobile/navigation.cljs | 135 ++++++++++-------- src/main/mobile/state.cljs | 15 +- 5 files changed, 114 insertions(+), 136 deletions(-) diff --git a/android/app/src/main/java/com/logseq/app/ComposeHost.kt b/android/app/src/main/java/com/logseq/app/ComposeHost.kt index 944999c644..8f42075a0f 100644 --- a/android/app/src/main/java/com/logseq/app/ComposeHost.kt +++ b/android/app/src/main/java/com/logseq/app/ComposeHost.kt @@ -7,7 +7,6 @@ import android.util.Log import android.view.ViewGroup import android.webkit.WebView import android.widget.FrameLayout -import androidx.activity.compose.BackHandler import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -34,7 +33,6 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -55,14 +53,14 @@ object ComposeHost { fun applyNavigation(navigationType: String?, path: String?) { val type = (navigationType ?: "push").lowercase() val safePath = path?.takeIf { it.isNotBlank() } ?: "/" - navEvents.tryEmit(NavigationEvent(type, safePath)) + if (!navEvents.tryEmit(NavigationEvent(type, safePath))) { + Log.w("ComposeHost", "Dropped navigation event: type=$type path=$safePath") + } } fun renderWithSystemInsets( activity: Activity, - webView: WebView, - onBackRequested: () -> Unit, - onExit: () -> Unit = { activity.finish() } + webView: WebView ) { WebViewSnapshotManager.registerWindow(activity.window) val root = activity.findViewById(android.R.id.content) @@ -76,9 +74,7 @@ object ComposeHost { setContent { ComposeNavigationHost( navEvents = navEvents, - webView = webView, - onBackRequested = onBackRequested, - onExit = onExit + webView = webView ) } } @@ -105,9 +101,7 @@ private fun routeFor(path: String): String = @Composable private fun ComposeNavigationHost( navEvents: SharedFlow, - webView: WebView, - onBackRequested: () -> Unit, - onExit: () -> Unit + webView: WebView ) { val navController = rememberNavController() @@ -273,9 +267,7 @@ private fun HandleNavigationEvents( } "pop" -> { - if (!navController.popBackStack()) { - // Already at root; nothing to pop. - } + navController.popBackStack() } "reset" -> { diff --git a/android/app/src/main/java/com/logseq/app/LiquidTabsPlugin.kt b/android/app/src/main/java/com/logseq/app/LiquidTabsPlugin.kt index cfbb420aab..dcfa42d060 100644 --- a/android/app/src/main/java/com/logseq/app/LiquidTabsPlugin.kt +++ b/android/app/src/main/java/com/logseq/app/LiquidTabsPlugin.kt @@ -86,7 +86,7 @@ class LiquidTabsPlugin : Plugin() { ensureNav() currentTabId?.let { id -> tabsState.find { it.id == id }?.let { tab -> - handleSelection(tab, reselected = false) + handleSelection(tab, reselected = false, notify = false) } } ?: hideSearchUi() adjustWebViewPadding() @@ -112,7 +112,7 @@ class LiquidTabsPlugin : Plugin() { } nav.post { val reselected = currentTabId == tab.id - handleSelection(tab, reselected) + handleSelection(tab, reselected, notify = false) call.resolve() } } @@ -169,7 +169,7 @@ class LiquidTabsPlugin : Plugin() { currentId = currentTabId, onSelect = { tab -> val reselected = tab.id == currentTabId - handleSelection(tab, reselected) + handleSelection(tab, reselected, notify = true) } ) } @@ -178,7 +178,7 @@ class LiquidTabsPlugin : Plugin() { return nav } - private fun handleSelection(tab: TabSpec, reselected: Boolean) { + private fun handleSelection(tab: TabSpec, reselected: Boolean, notify: Boolean) { currentTabId = tab.id if (tab.role == "search") { showSearchUi() @@ -186,7 +186,9 @@ class LiquidTabsPlugin : Plugin() { hideSearchUi() } - notifyListeners("tabSelected", JSObject().put("id", tab.id).put("reselected", reselected)) + if (notify) { + notifyListeners("tabSelected", JSObject().put("id", tab.id).put("reselected", reselected)) + } } private fun adjustWebViewPadding() { @@ -539,7 +541,7 @@ class LiquidTabsPlugin : Plugin() { val id = obj.optString("id", "") if (id.isBlank()) continue val title = obj.optString("title", "") - val subtitle = obj.optString("subtitle", null) + val subtitle = if (obj.has("subtitle") && !obj.isNull("subtitle")) obj.optString("subtitle") else null result.add(SearchResult(id, title, subtitle)) } return result diff --git a/android/app/src/main/java/com/logseq/app/MainActivity.java b/android/app/src/main/java/com/logseq/app/MainActivity.java index 13cac8ca97..55d9be6734 100644 --- a/android/app/src/main/java/com/logseq/app/MainActivity.java +++ b/android/app/src/main/java/com/logseq/app/MainActivity.java @@ -6,24 +6,20 @@ import android.content.Context; import android.content.IntentFilter; import android.content.res.Configuration; import android.os.Bundle; -import android.webkit.ValueCallback; import android.webkit.WebView; +import androidx.activity.OnBackPressedCallback; import androidx.activity.EdgeToEdge; +import androidx.core.content.ContextCompat; import androidx.core.view.WindowInsetsControllerCompat; -import com.getcapacitor.PluginCall; -import com.getcapacitor.JSObject; import com.getcapacitor.BridgeActivity; -import androidx.activity.OnBackPressedDispatcher; import android.util.Log; import android.view.View; import java.util.Timer; import java.util.TimerTask; -import ee.forgr.capacitor_navigation_bar.CapgoNavigationBarPlugin; - public class MainActivity extends BridgeActivity { - private NavigationCoordinator navigationCoordinator = new NavigationCoordinator(); + private final NavigationCoordinator navigationCoordinator = new NavigationCoordinator(); private BroadcastReceiver routeChangeReceiver; @Override @@ -46,13 +42,15 @@ public class MainActivity extends BridgeActivity { applyLogseqTheme(); - // Let Compose host the WebView with system bar padding for safe areas and handle back. - ComposeHost.INSTANCE.renderWithSystemInsets(this, webView, () -> { - sendJsBack(webView); - return null; - }, () -> { - finish(); - return null; + // Let Compose host the WebView with system bar padding for safe areas. + // Android back is still delegated to JS from the Activity back dispatcher. + ComposeHost.INSTANCE.renderWithSystemInsets(this, webView); + + getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + handleNativeBack(); + } }); routeChangeReceiver = new BroadcastReceiver() { @@ -66,23 +64,11 @@ public class MainActivity extends BridgeActivity { } }; IntentFilter filter = new IntentFilter(UILocal.ACTION_ROUTE_CHANGED); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { - registerReceiver(routeChangeReceiver, filter, Context.RECEIVER_NOT_EXPORTED); - } else { - registerReceiver(routeChangeReceiver, filter); - } - - // initNavigationBarBgColor(); - + ContextCompat.registerReceiver(this, routeChangeReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED); new Timer().schedule(new TimerTask() { @Override public void run() { - bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", new ValueCallback() { - @Override - public void onReceiveValue(String s) { - // - } - }); + bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", s -> {}); } }, 5000); } @@ -139,23 +125,13 @@ public class MainActivity extends BridgeActivity { } } - public void initNavigationBarBgColor() { - CapgoNavigationBarPlugin navigationBarPlugin = new CapgoNavigationBarPlugin(); - JSObject data = new JSObject(); - data.put("color", "transparent"); - - PluginCall call = new PluginCall(null, null, null, "t", data); - navigationBarPlugin.setNavigationBarColor(call); - } - @Override public void onPause() { overridePendingTransition(0, R.anim.byebye); super.onPause(); } - @Override - public void onBackPressed() { + private void handleNativeBack() { Log.d("onBackPressed", "Debug"); WebView webView = getBridge().getWebView(); @@ -165,9 +141,10 @@ public class MainActivity extends BridgeActivity { sendJsBack(webView); } else { // Fallback if for some reason there is no webview - super.onBackPressed(); + finish(); } } + @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -175,12 +152,7 @@ public class MainActivity extends BridgeActivity { String type = intent.getType(); if (Intent.ACTION_SEND.equals(action) && type != null) { bridge.getActivity().setIntent(intent); - bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", new ValueCallback() { - @Override - public void onReceiveValue(String s) { - // - } - }); + bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", s -> {}); } } diff --git a/src/main/mobile/navigation.cljs b/src/main/mobile/navigation.cljs index d89fb6d849..3208452658 100644 --- a/src/main/mobile/navigation.cljs +++ b/src/main/mobile/navigation.cljs @@ -5,6 +5,7 @@ [frontend.handler.route :as route-handler] [frontend.mobile.util :as mobile-util] [frontend.state :as state] + [goog.object :as gobj] [lambdaisland.glogi :as log] [logseq.shui.dialog.core :as shui-dialog] [promesa.core :as p] @@ -19,6 +20,8 @@ (defonce ^:private pending-navigation (atom nil)) (defonce ^:private hooks-installed? (atom false)) +(declare notify-route-payload!) + ;; Track whether the latest change came from a native back gesture / popstate. (.addEventListener js/window "popstate" (fn [_] (reset! navigation-source :pop))) @@ -109,6 +112,57 @@ [stack] (-> @stack-history (get stack) :history last)) +(defn- native-route-payload! + [navigation-type stack path route] + (when (and (mobile-util/native-platform?) + mobile-util/ui-local) + (let [payload (cond-> {:navigationType navigation-type + :push (= navigation-type "push") + :stack stack + :path path} + route (assoc :route route))] + (notify-route-payload! payload)))) + +(defn- prepare-browser-route-restore! + [navigation-type stack] + (case navigation-type + "pop" (reset! navigation-source :pop) + "replace" (record-navigation-intent! {:type :replace :stack stack}) + "push" (record-navigation-intent! {:type :push :stack stack}) + nil)) + +(defn- restore-stack-entry! + "Restore a stack entry into route state and native navigation. + + If the entry has a real reitit route, update the browser hash and let the + router callback report to native. If the entry is a virtual tab root + (`graphs`, `capture`, `search`, etc.), there is no valid reitit route to + generate, so update route state and notify native directly." + [stack {:keys [path route route-match] :as _entry} navigation-type] + (let [route-match (or route-match (:route-match (stack-defaults stack))) + route (or route (:route (stack-defaults stack))) + path (or path (current-path)) + path (if (string/blank? path) "/" path) + route-name (:to route)] + (set-current-stack! stack) + (route-handler/set-route-match! route-match) + (swap! initialised-stacks assoc stack true) + (if route-name + (if (= path (current-path)) + ;; No router callback will fire if the hash is already at the target. + ;; Still keep the native animation/mirror stack in sync. + (do + (reset! navigation-source nil) + (reset! pending-navigation nil) + (native-route-payload! navigation-type stack path route)) + (do + (prepare-browser-route-restore! navigation-type stack) + (orig-replace-state route-name (:path-params route) (:query-params route)))) + (do + (reset! navigation-source nil) + (reset! pending-navigation nil) + (native-route-payload! navigation-type stack path nil))))) + (defn- remember-route! [stack nav-type route path route-match] (when stack @@ -213,26 +267,12 @@ "Activate a stack and restore its last known route." [stack] (when stack - (let [stack (ensure-stack stack) - current @active-stack] - (set-current-stack! stack) + (let [stack (ensure-stack stack)] (when-let [{:keys [path route route-match]} (stack-top stack)] - (let [route-match (or route-match (:route-match (stack-defaults stack))) - path (or path (current-path))] - (route-handler/set-route-match! route-match) - (when (= current "search") - ;; reset to :home - (orig-replace-state :home nil nil)) - (notify-route-change! - {:route {:to (or (get-in route [:data :name]) - (get-in route-match [:data :name])) - :path-params (or (:path-params route) - (get-in route-match [:parameters :path])) - :query-params (or (:query-params route) - (get-in route-match [:parameters :query]))} - :path path - :stack stack - :push false})))))) + (restore-stack-entry! + stack + {:path path :route route :route-match route-match} + "replace"))))) (defn pop-modal! [] @@ -259,32 +299,19 @@ (let [stack (current-stack) {:keys [history]} (get @stack-history stack) history (vec history)] - ;; back to search root - (when (and - (mobile-util/native-android?) - (= stack "search") - (= (count history) 2)) - (.showSearchUiNative ^js (.. js/Capacitor -Plugins -LiquidTabsPlugin))) - (when (>= (count history) 1) - (let [root-history? (= (count history) 1) - new-history (if root-history? - history - (subvec history 0 (dec (count history)))) - {:keys [route-match]} (peek new-history) - route-match (or route-match (:route-match (stack-defaults stack))) - route-name (get-in route-match [:data :name]) - path-params (get-in route-match [:parameters :path]) - query-params (get-in route-match [:parameters :query])] - + (when (> (count history) 1) + ;; Back to the Android native search root. + (when (and (mobile-util/native-android?) + (= stack "search") + (= (count history) 2)) + (when-let [^js plugin (some-> js/Capacitor .-Plugins .-LiquidTabsPlugin)] + (when (gobj/get plugin "showSearchUiNative") + (.showSearchUiNative plugin)))) + (let [new-history (subvec history 0 (dec (count history))) + entry (peek new-history)] (swap! stack-history assoc stack {:history new-history}) - - ;; Pretend this came from a pop for next-navigation! - (reset! navigation-source :pop) - - ;; Use *original* replace-state to avoid recording a :replace intent. - (orig-replace-state route-name path-params query-params) - - (route-handler/set-route-match! route-match))))) + (restore-stack-entry! stack entry "pop") + true)))) (defn pop-to-root! "Pop current or given stack back to its root entry and notify navigation." @@ -293,25 +320,9 @@ (when stack (let [{:keys [history]} (get @stack-history stack) root (or (first history) (stack-defaults stack)) - {:keys [route route-match path]} root - route-match (or route-match (:route-match (stack-defaults stack))) - path (or path (current-path)) - route (or route {:to (get-in route-match [:data :name]) - :path-params (get-in route-match [:parameters :path]) - :query-params (get-in route-match [:parameters :query])})] + root (merge (stack-defaults stack) root)] (swap! stack-history assoc stack {:history [root]}) - (set-current-stack! stack) - ;; Use original replace-state to avoid recording a push intent. - (orig-replace-state (get-in route-match [:data :name]) - (get-in route-match [:parameters :path]) - (get-in route-match [:parameters :query])) - (route-handler/set-route-match! route-match) - (notify-route-change! - {:route route - :route-match route-match - :path path - :stack stack - :push false}))))) + (restore-stack-entry! stack root "replace"))))) (defn ^:export install-native-bridge! [] diff --git a/src/main/mobile/state.cljs b/src/main/mobile/state.cljs index b319a9c5e9..5e8b8fa103 100644 --- a/src/main/mobile/state.cljs +++ b/src/main/mobile/state.cljs @@ -9,13 +9,14 @@ (defonce *tab (atom "home")) (defn set-tab! [tab] (let [prev @*tab] - ;; When leaving the search tab, clear its stack so reopening starts fresh. - (when (and (= prev "search") - (not= tab "search")) - (reset! *search-input "") - (mobile-nav/reset-stack-history! "search")) - (reset! *tab tab) - (mobile-nav/switch-stack! tab))) + (when-not (= prev tab) + ;; When leaving the search tab, clear its stack so reopening starts fresh. + (when (and (= prev "search") + (not= tab "search")) + (reset! *search-input "") + (mobile-nav/reset-stack-history! "search")) + (reset! *tab tab) + (mobile-nav/switch-stack! tab)))) (defn use-tab [] (r/use-atom *tab)) (defonce *popup-data (atom nil))