diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index f734a2f6b8..92010ffd22 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -31,6 +31,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel /// Temporary snapshot image for smooth pop transitions. private var popSnapshotView: UIView? + // Each stack has its own native VC stack, just like paths. + private var stackViewControllerStacks: [String: [UIViewController]] = [:] + // --------------------------------------------------------- // MARK: Helpers // --------------------------------------------------------- @@ -40,14 +43,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel return raw } - private func debugLogStacks(_ label: String) { - #if DEBUG - print("🧭 [\(label)] activeStackId=\(activeStackId)") - print(" pathStack=\(pathStack)") - print(" stackPathStacks=\(stackPathStacks)") - #endif - } - /// Returns the current native path stack for a given logical stack id, /// or initialises a sensible default if none exists yet. private func paths(for stackId: String) -> [String] { @@ -72,6 +67,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel } } + private func setViewControllers(_ vcs: [UIViewController], for stackId: String) { + stackViewControllerStacks[stackId] = vcs + } + + private func viewControllers(for stackId: String) -> [UIViewController] { + stackViewControllerStacks[stackId] ?? [] + } + // --------------------------------------------------------- // MARK: UIApplication lifecycle // --------------------------------------------------------- @@ -198,7 +201,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel SharedWebViewController.instance.clearPlaceholder() SharedWebViewController.instance.attach(to: vc) - debugLogStacks("emptyNavStack") } private func pushIfNeeded(path: String, animated: Bool) { @@ -216,7 +218,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel nav.pushViewController(vc, animated: animated) - debugLogStacks("pushIfNeeded") } private func replaceTop(path: String) { @@ -236,7 +237,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel } nav.setViewControllers(stack, animated: false) - debugLogStacks("replaceTop") } private func popIfNeeded(animated: Bool) { @@ -246,8 +246,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel _ = pathStack.popLast() setPaths(pathStack, for: activeStackId) nav.popViewController(animated: animated) - - debugLogStacks("popIfNeeded") } } @@ -274,12 +272,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel vcs.count < pathStack.count } - #if DEBUG - print("🧭 willShow β€” isPop=\(isPop)") - print(" toVC=\(toVC.targetPath) fromVC=\(String(describing: fromVC?.targetPath))") - debugLogStacks("willShow") - #endif - if isPop { // ----------------------------- // POP β€” update per-stack pathStack, then notify JS. @@ -322,10 +314,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel // πŸ”‘ DO NOT call webView.goBack(). // Tell JS explicitly that native popped. self.ignoreRoutePopCount += 1 - #if DEBUG - print("⬅️ Native POP completed, notifying JS via onNativePop(), ignoreRoutePopCount=\(self.ignoreRoutePopCount)") - debugLogStacks("after native-pop pathStack update") - #endif if let bridge = SharedWebViewController.instance.bridgeController.bridge { let js = "window.LogseqNative && window.LogseqNative.onNativePop && window.LogseqNative.onNativePop();" @@ -395,9 +383,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel private func observeRouteChanges() { NotificationCenter.default.addObserver( - forName: UILocalPlugin.routeChangeNotification, - object: nil, - queue: .main + forName: UILocalPlugin.routeChangeNotification, + object: nil, + queue: .main ) { [weak self] notification in guard let self else { return } guard let nav = self.navController else { return } @@ -408,40 +396,57 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel let stackId = (notification.userInfo?["stack"] as? String) ?? "home" let previousStackId = self.activeStackId - #if DEBUG - print("πŸ“‘ routeDidChange from JS β†’ native") - print(" stackId=\(stackId) navigationType=\(navigationType) path=\(path)") - debugLogStacks("before observeRouteChanges") - #endif + // 🚫 Fast-path: ignore duplicate replace for same stack/path + if stackId == self.activeStackId, + navigationType == "replace", + path == self.pathStack.last { + return + } + + // ⚑️ Fast-path: cancel search β†’ home root. + // We do NOT rebuild nav stack and we do NOT reattach the WebView. + // JS will just navigate the existing shared WKWebView to "/". + if previousStackId == "search", + stackId == "home"{ + + // Just update bookkeeping so future home pushes/pop work correctly. + self.activeStackId = "home" + self.setPaths(["/"], for: "home") + self.setPaths(["/__stack__/search"], for: "search") + + nav.setViewControllers([], animated: false) + self.setViewControllers([], for: "home") + + // πŸ‘ˆ Do NOTHING to nav.viewControllers or SharedWebViewController here. + return + } // ============================================ - // 1️⃣ Stack switch: home ↔ capture ↔ goto ... + // 1️⃣ Stack switch: home ↔ search ↔ capture... // ============================================ if stackId != self.activeStackId { - // Save current native stack paths; drop stale search stack when leaving it. - if previousStackId == "search", stackId != "search" { - self.setPaths(["/__stack__/search"], for: "search") - } else { - self.setPaths(self.pathStack, for: previousStackId) - } + self.setPaths(self.pathStack, for: previousStackId) - // Load (or create) new stack's paths + // Load saved paths for target stack var newPaths = self.paths(for: stackId) - // Ensure the top of the stack matches the path sent by JS - if let last = newPaths.last, last != path { - if newPaths.isEmpty { - newPaths = [path] - } else { - newPaths[newPaths.count - 1] = path - } + // πŸ”‘ Special rules for shaping the new stack + if stackId == "home", path == "/" { + // πŸ‘‰ ALWAYS reset home to a single root VC. + newPaths = ["/"] + } else if newPaths.isEmpty { + // First time visiting this stack + newPaths = [path] + } else if let last = newPaths.last, last != path { + // Same history, but different top path β†’ align the top. + newPaths[newPaths.count - 1] = path } self.activeStackId = stackId self.pathStack = newPaths self.setPaths(newPaths, for: stackId) - // Rebuild the UINavigationController's stack from these paths + // Rebuild native stack for these paths var vcs: [UIViewController] = [] for (idx, p) in newPaths.enumerated() { let vc = NativePageViewController(path: p, push: idx > 0) @@ -449,24 +454,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel } nav.setViewControllers(vcs, animated: false) + self.setViewControllers(vcs, for: stackId) if let lastVC = vcs.last as? NativePageViewController { - SharedWebViewController.instance.attach(to: lastVC) - SharedWebViewController.instance.clearPlaceholder() + // Defer & avoid redundant attach. + DispatchQueue.main.async { + if let bridge = SharedWebViewController.instance.bridgeController.bridge, + let webView = bridge.webView, + webView.isDescendant(of: lastVC.view) { + } else { + SharedWebViewController.instance.attach(to: lastVC) + } + SharedWebViewController.instance.clearPlaceholder() + } } - #if DEBUG - print("πŸ”€ STACK SWITCH to \(stackId)") - debugLogStacks("after stack switch") - #endif - - // For stacks like "capture", default paths are ["__/stack__/capture"], - // so they get a single VC and no back button. return } // ============================================ - // 2️⃣ Navigation *within* the active stack + // 2️⃣ Navigation *within* active stack // ============================================ switch navigationType { case "reset": @@ -478,10 +485,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel case "pop": if self.ignoreRoutePopCount > 0 { self.ignoreRoutePopCount -= 1 - #if DEBUG - print("πŸ™ˆ ignoring JS pop (ignoreRoutePopCountβ†’\(self.ignoreRoutePopCount))") - debugLogStacks("after ignore JS pop") - #endif return } if self.pathStack.count > 1 { @@ -491,12 +494,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel default: self.pushIfNeeded(path: path, animated: true) } - - #if DEBUG - debugLogStacks("after observeRouteChanges switch") - #endif } } + } // --------------------------------------------------------- diff --git a/ios/App/App/LiquidTabsRootView.swift b/ios/App/App/LiquidTabsRootView.swift index 30799167e6..0b8d483af7 100644 --- a/ios/App/App/LiquidTabsRootView.swift +++ b/ios/App/App/LiquidTabsRootView.swift @@ -320,7 +320,6 @@ private struct SearchTabHost26: View { wasSearching = false selectedTab.wrappedValue = .content(0) store.selectedId = firstId - LiquidTabsPlugin.shared?.notifyTabSelected(id: firstId) } } } diff --git a/src/main/mobile/components/app.cljs b/src/main/mobile/components/app.cljs index 660326cb13..ea1715cebb 100644 --- a/src/main/mobile/components/app.cljs +++ b/src/main/mobile/components/app.cljs @@ -158,7 +158,7 @@ route-match (state/sub :route-match)] [:main#app-container-wrapper.ls-fold-button-on-right [:div#app-container {:class (when show-popup? "invisible")} - [:div#main-container.flex.flex-1.overflow-x-hidden.py-4 + [:div#main-container.flex.flex-1.overflow-x-hidden (app current-repo route-match)]] (when show-popup? [:div.ls-layer diff --git a/src/main/mobile/navigation.cljs b/src/main/mobile/navigation.cljs index 72a0791aee..6af519aa6c 100644 --- a/src/main/mobile/navigation.cljs +++ b/src/main/mobile/navigation.cljs @@ -16,30 +16,9 @@ (defonce ^:private pending-navigation (atom nil)) (defonce ^:private hooks-installed? (atom false)) -;; --- DEBUG toggle --- -(def ^:private debug-nav? true) - -(defn- dbg [tag & args] - (when debug-nav? - (let [payload (cond - ;; one map argument β†’ use it directly - (and (= 1 (count args)) - (map? (first args))) - (first args) - - ;; even number of args β†’ treat as k/v pairs - (even? (count args)) - (apply hash-map args) - - ;; odd / weird β†’ just log the raw args - :else - {:args args})] - (log/info tag payload)))) - ;; Track whether the latest change came from a native back gesture / popstate. (.addEventListener js/window "popstate" (fn [_] - (reset! navigation-source :pop) - (dbg :nav/popstate {:source :popstate}))) + (reset! navigation-source :pop))) (defn current-stack [] @@ -48,7 +27,6 @@ (defn set-current-stack! [stack] (when (some? stack) - (dbg :nav/set-current-stack {:from @active-stack :to stack}) (reset! active-stack stack))) (defn- strip-fragment [href] @@ -77,7 +55,6 @@ (defn- record-navigation-intent! [{:keys [type stack]}] (let [stack (or stack @active-stack primary-stack)] - (dbg :nav/record-intent {:type type :stack stack}) (reset! pending-navigation {:type type :stack stack}))) @@ -91,7 +68,6 @@ ([k params query] (record-navigation-intent! {:type :push :stack @active-stack}) - (dbg :nav/push-state {:name k :params params :query query :stack @active-stack}) (orig-push-state k params query))) (defonce orig-replace-state rfe/replace-state) @@ -103,7 +79,6 @@ ([k params query] (record-navigation-intent! {:type :replace :stack @active-stack}) - (dbg :nav/replace-state {:name k :params params :query query :stack @active-stack}) (orig-replace-state k params query))) (defn install-navigation-hooks! @@ -111,7 +86,6 @@ Also tags navigation with the active stack so native can keep per-stack history." [] (when (compare-and-set! hooks-installed? false true) - (dbg :nav/hooks-installed {}) (set! rfe/push-state push-state) (set! rfe/replace-state replace-state))) @@ -132,14 +106,6 @@ [stack] (-> @stack-history (get stack) :history last)) -;; --- DEBUG: watch stack-history changes --- -(add-watch stack-history ::stack-history-debug - (fn [_ _ old new] - (when debug-nav? - (dbg :nav/stack-history - :old (into {} (for [[k v] old] [k (mapv :path (:history v))])) - :new (into {} (for [[k v] new] [k (mapv :path (:history v))])))))) - (defn- remember-route! [stack nav-type route path route-match] (when stack @@ -160,12 +126,6 @@ (conj history entry)) history)))] (when entry - (dbg :nav/remember-route - :stack stack - :nav-type nav-type - :path path - :route-to (or (get-in route [:to]) - (get-in route-match [:data :name]))) (swap! stack-history update stack (fn [{:keys [history] :as st}] {:history (update-history history)})) (swap! initialised-stacks assoc stack true))))) @@ -173,7 +133,6 @@ (defn reset-stack-history! [stack] (when stack - (dbg :nav/reset-stack-history {:stack stack}) (swap! stack-history assoc stack {:history [(stack-defaults stack)]}) (swap! initialised-stacks dissoc stack))) @@ -193,12 +152,7 @@ (true? push) "push" :else "push"))] (reset! navigation-source nil) - (dbg :nav/next-navigation - :src src - :intent intent - :stack stack - :first? first? - :nav-type nav-type) + (when first? (swap! initialised-stacks assoc stack true)) {:navigation-type nav-type @@ -207,7 +161,6 @@ (defn- notify-route-payload! [payload] - (dbg :nav/notify-native payload) (-> (.routeDidChange mobile-util/ui-local (clj->js payload)) (p/catch (fn [err] (log/warn :mobile-native-navigation/route-report-failed @@ -222,12 +175,6 @@ :stack (or stack (current-stack))}) stack (or stack (current-stack)) path (or path (current-path))] - (dbg :nav/notify-route-change - :stack stack - :navigation-type navigation-type - :path path - :route-to (or (:to route) - (get-in route-match [:data :name]))) (set-current-stack! stack) (remember-route! stack navigation-type route path route-match) (when (and (mobile-util/native-ios?) @@ -254,20 +201,16 @@ "Activate a stack and restore its last known route." [stack] (when stack - (let [stack (ensure-stack stack)] - (dbg :nav/switch-stack {:to stack - :current @active-stack - :stack-top (select-keys (stack-top stack) [:path])}) + (let [stack (ensure-stack stack) + current @active-stack] (set-current-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))] - (dbg :nav/switch-stack-apply - :stack stack - :path path - :route-name (or (get-in route [:data :name]) - (get-in route-match [:data :name]))) (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])) @@ -286,23 +229,14 @@ (let [stack (current-stack) {:keys [history]} (get @stack-history stack) history (vec history)] - (if (<= (count history) 1) - (dbg :nav/pop-stack-root {:stack stack - :history (mapv :path history)}) + (when (> (count history) 1) (let [new-history (subvec history 0 (dec (count history))) - {:keys [route route-match path]} (peek new-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])] - (dbg :nav/pop-stack - :stack stack - :old-history (mapv :path history) - :new-history (mapv :path new-history) - :target-path path - :route-name route-name) - (swap! stack-history assoc stack {:history new-history}) ;; Pretend this came from a pop for next-navigation! @@ -315,10 +249,6 @@ (defn ^:export install-native-bridge! [] - (dbg :nav/install-native-bridge {}) (set! (.-LogseqNative js/window) (clj->js - {:onNativePop (fn [] - (dbg :nav/on-native-pop {:stack (current-stack) - :path (current-path)}) - (pop-stack!))}))) + {:onNativePop (fn [] (pop-stack!))})))