diff --git a/ios/App/App/LiquidTabsRootView.swift b/ios/App/App/LiquidTabsRootView.swift index ba268ffdf6..53007cf4db 100644 --- a/ios/App/App/LiquidTabsRootView.swift +++ b/ios/App/App/LiquidTabsRootView.swift @@ -5,9 +5,8 @@ struct LiquidTabsRootView: View { let navController: UINavigationController @State private var searchText: String = "" - @FocusState private var isSearchFocused: Bool - // Convenience helpers: first two tabs from CLJS, rest ignored + // Convenience helpers: first three tabs from CLJS, rest ignored private var firstTab: LiquidTab? { store.tabs.first } @@ -22,106 +21,98 @@ struct LiquidTabsRootView: View { var body: some View { if #available(iOS 26.0, *) { - // iOS 26+: static TabView with a dedicated search tab (role: .search) + // iOS 26+: TabView with dedicated search tab role if store.tabs.isEmpty { NativeNavHost(navController: navController) - .ignoresSafeArea() + .ignoresSafeArea() } else { - TabView { - // ---- Tab 1 (normal) ---- - if let tab = firstTab { - Tab { - NativeNavHost(navController: navController) - .ignoresSafeArea() - .onAppear { - store.selectedId = tab.id - LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) - } - } label: { - Label(tab.title, systemImage: tab.systemImage) - } - } - - // ---- Tab 2 (normal) ---- - if let tab = secondTab { - Tab { - NativeNavHost(navController: navController) - .ignoresSafeArea() - .onAppear { - store.selectedId = tab.id - LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) - } - } label: { - Label(tab.title, systemImage: tab.systemImage) - } - } - - // ---- Tab 3 (normal) ---- - if let tab = thirdTab { - Tab { - NativeNavHost(navController: navController) - .ignoresSafeArea() - .onAppear { - store.selectedId = tab.id - LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) - } - } label: { - Label(tab.title, systemImage: tab.systemImage) - } - } - - // ---- Search tab: same webview, CLJS shows search page ---- - Tab(role: .search) { + TabView { + // ---- Tab 1 (normal) ---- + if let tab = firstTab { + Tab { NativeNavHost(navController: navController) - .ignoresSafeArea() - .onAppear { - store.selectedId = "search" - LiquidTabsPlugin.shared?.notifyTabSelected(id: "search") - - // Focus the native search field when entering search tab - DispatchQueue.main.async { - isSearchFocused = true - } - } - .onDisappear { - isSearchFocused = false - } + .ignoresSafeArea() + .onAppear { + store.selectedId = tab.id + LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) + } } label: { - Label("Search", systemImage: "magnifyingglass") + Label(tab.title, systemImage: tab.systemImage) } - } - // 👇 Key part: toolbar placement ties search to the bottom “liquid” bar - .searchable( - text: $searchText, - placement: .toolbar, - prompt: "Search" - ) - .searchFocused($isSearchFocused) - .searchToolbarBehavior(.minimize) - .onChange(of: searchText) { newValue in - LiquidTabsPlugin.shared?.notifySearchChanged(query: newValue) - } + // ---- Tab 2 (normal) ---- + if let tab = secondTab { + Tab { + NativeNavHost(navController: navController) + .ignoresSafeArea() + .onAppear { + store.selectedId = tab.id + LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) + } + } label: { + Label(tab.title, systemImage: tab.systemImage) + } + } + + // ---- Tab 3 (normal) ---- + if let tab = thirdTab { + Tab { + NativeNavHost(navController: navController) + .ignoresSafeArea() + .onAppear { + store.selectedId = tab.id + LiquidTabsPlugin.shared?.notifyTabSelected(id: tab.id) + } + } label: { + Label(tab.title, systemImage: tab.systemImage) + } + } + + // ---- Search tab (special role) ---- + Tab(role: .search) { + // 👇 Apple requires search tab content inside NavigationStack + NavigationStack { + NativeNavHost(navController: navController) + .ignoresSafeArea() + .onAppear { + // Tell CLJS to show the search page + store.selectedId = "search" + LiquidTabsPlugin.shared?.notifyTabSelected(id: "search") + } + } + } label: { + Label("Search", systemImage: "magnifyingglass") + } + } + // 👇 This is the key combo for “search tab → search field” UX: + // - Tab(role: .search) above + // - .searchable on the TabView + .searchable(text: $searchText) + .searchToolbarBehavior(.minimize) + .onChange(of: searchText) { newValue in + // Forward query to JS/CLJS + LiquidTabsPlugin.shared?.notifySearchChanged(query: newValue) + } } } else { - // iOS < 26: fall back to the old dynamic tabItem-based tabs + // iOS < 26: old dynamic tabItem-based tabs TabView(selection: Binding( - get: { store.selectedId ?? firstTab?.id }, - set: { newValue in - guard let id = newValue else { return } - store.selectedId = id - LiquidTabsPlugin.shared?.notifyTabSelected(id: id) - } - )) { + get: { store.selectedId ?? firstTab?.id }, + set: { newValue in + guard let id = newValue else { return } + store.selectedId = id + LiquidTabsPlugin.shared?.notifyTabSelected(id: id) + } + )) { ForEach(store.tabs) { tab in NativeNavHost(navController: navController) - .ignoresSafeArea() - .tabItem { - Label(tab.title, systemImage: tab.systemImage) - } - .tag(tab.id as String?) + .ignoresSafeArea() + .tabItem { + Label(tab.title, systemImage: tab.systemImage) + } + .tag(tab.id as String?) } } } diff --git a/src/main/frontend/handler/notification.cljs b/src/main/frontend/handler/notification.cljs index 40f2a8cf4b..ce93529179 100644 --- a/src/main/frontend/handler/notification.cljs +++ b/src/main/frontend/handler/notification.cljs @@ -27,14 +27,15 @@ (show! content status clear? uid timeout nil)) ([content status clear? uid timeout close-cb] (assert (keyword? status) "status should be a keyword") - (let [contents (state/get-notification-contents) - uid (or uid (keyword (util/unique-id)))] - (state/set-state! :notification/contents (assoc contents - uid {:content content - :status status - :close-cb close-cb})) + ;; (let [contents (state/get-notification-contents) + ;; uid (or uid (keyword (util/unique-id)))] + ;; (state/set-state! :notification/contents (assoc contents + ;; uid {:content content + ;; :status status + ;; :close-cb close-cb})) - (when (and (not= status :error) (not (false? clear?))) - (js/setTimeout #(clear! uid) (or timeout 2000))) + ;; (when (and (not= status :error) (not (false? clear?))) + ;; (js/setTimeout #(clear! uid) (or timeout 2000))) - uid))) + ;; uid) + )) diff --git a/src/main/mobile/bottom_tabs.cljs b/src/main/mobile/bottom_tabs.cljs index a22c7b96ab..68d5c5c41c 100644 --- a/src/main/mobile/bottom_tabs.cljs +++ b/src/main/mobile/bottom_tabs.cljs @@ -64,9 +64,7 @@ (configure-tabs [{:id "home" :title "Home" :systemImage "house" :role "normal"} {:id "quick-add" :title "Capture" :systemImage "plus" :role "normal"} - {:id "settings" :title "Settings" :systemImage "gear" :role "normal"} - ;; {:id "search" :title "Search" :systemImage "magnifyingglass" :role "search"} - ]) + {:id "settings" :title "Settings" :systemImage "gear" :role "normal"}]) (add-tab-selected-listener! (fn [tab] (when-not (= tab "quick-add") @@ -76,8 +74,6 @@ (do (route-handler/redirect-to-home!) (util/scroll-to-top false)) - "search" - (route-handler/redirect! {:to :search}) "quick-add" (editor-handler/show-quick-add) ;; TODO: support longPress detection @@ -88,7 +84,8 @@ (add-search-listener! (fn [q] ;; wire up search handler - (js/console.log "Native search query" q))))) + (js/console.log "Native search query" q) + (reset! mobile-state/*search-input q))))) (defn hide! [] diff --git a/src/main/mobile/components/header.cljs b/src/main/mobile/components/header.cljs index e5023af55a..af5dcb29a2 100644 --- a/src/main/mobile/components/header.cljs +++ b/src/main/mobile/components/header.cljs @@ -144,9 +144,13 @@ (reset! native-top-bar-listener? true))) (defn- configure-native-top-bar! - [{:keys [tab title hidden?]}] + [{:keys [tab title route-name]}] (when (mobile-util/native-ios?) - (let [base {:title title + (let [hidden? (and (= tab "search") + (not= route-name :page)) + skip? (and (= tab "home") + (not= route-name :home)) + base {:title title :tintColor "#1f2937" :hidden (boolean hidden?)} left-buttons (when (= tab "home") @@ -163,8 +167,9 @@ left-buttons (assoc :leftButtons left-buttons) right-buttons (assoc :rightButtons right-buttons) (= tab "home") (assoc :titleClickable true))] - (.configure mobile-util/native-top-bar - (clj->js header))))) + (when-not skip? + (.configure mobile-util/native-top-bar + (clj->js header)))))) (rum/defc rtc-indicator-btn [] @@ -175,8 +180,8 @@ (user-handler/logged-in?)) (rtc-indicator/indicator))])) -(rum/defc header - [tab login?] +(rum/defc header-inner + [tab route-name] (let [current-repo (state/get-current-repo) short-repo-name (if current-repo (db-conn/get-short-repo-name current-repo) @@ -189,8 +194,16 @@ {:tab tab :title (if (= tab "home") short-repo-name - (string/capitalize tab))})) + (string/capitalize tab)) + :hidden? (and (= tab "search") + (not= route-name :page)) + :route-name route-name})) nil) - [tab short-repo-name]) + [tab short-repo-name route-name]) [:<>])) + +(rum/defc header < rum/reactive + [tab] + (let [route-match (state/sub :route-match)] + (header-inner tab (get-in route-match [:data :name])))) diff --git a/src/main/mobile/components/search.cljs b/src/main/mobile/components/search.cljs index e28cb49b63..be069e81c4 100644 --- a/src/main/mobile/components/search.cljs +++ b/src/main/mobile/components/search.cljs @@ -31,7 +31,7 @@ (rum/defc ^:large-vars/cleanup-todo search [] - (let [[input set-input!] (hooks/use-state "") + (let [[input set-input!] (mobile-state/use-search-input) [search-result set-search-result!] (hooks/use-state nil) [last-input-at set-last-input-at!] (hooks/use-state nil) [recents set-recents!] (hooks/use-state (search-handler/get-recents)) @@ -56,47 +56,45 @@ (js/clearTimeout timeout))))) [(hooks/use-debounced-value input 150)]) - [:div.app-silk-search-page - [:div.bd - (when (and (string/blank? input) (seq recents)) - [:div.mb-4 - [:div.px-4.text-sm.font-medium.text-muted-foreground - [:div.flex.flex-item.items-center.justify-between.mt-2 - "Recent" - (shui/button - {:variant :text - :size :sm - :class "text-muted-foreground flex justify-end pr-1" - :on-click (fn [] - (search-handler/clear-recents!) - (set-recents! nil))} - "Clear")]] + [:div.app-search + (when (and (string/blank? input) (seq recents)) + [:div.mb-4 + [:div.px-4.text-sm.font-medium.text-muted-foreground + [:div.flex.flex-item.items-center.justify-between.mt-2 + "Recent" + (shui/button + {:variant :text + :size :sm + :class "text-muted-foreground flex justify-end pr-1" + :on-click (fn [] + (search-handler/clear-recents!) + (set-recents! nil))} + "Clear")]] - (for [item recents] - [:div.px-2 - (ui/menu-link - {:on-click #(set-input! item)} - item)])]) - - (if (seq result) - [:ul.px-3 - {:class (when (and (not (string/blank? input)) - (seq search-result)) - "as-results")} - (for [{:keys [page? icon text header source-block]} result] - (let [block source-block] - [:li.flex.gap-1 - {:on-click (fn [] - (when-let [id (:block/uuid block)] - (route-handler/redirect-to-page! (str id))))} - [:div.flex.flex-col.gap-1.py-1 - (when header - [:div.opacity-60.text-sm - header]) - [:div.flex.flex-row.items-start.gap-1 - (when (and page? icon) (ui/icon icon {:size 15 - :class "text-muted-foreground mt-1"})) - [:div text]]]]))] - (when-not (string/blank? input) - [:div.px-4.text-muted-foreground - "No results"]))]])) + (for [item recents] + [:div.px-2 + (ui/menu-link + {:on-click #(set-input! item)} + item)])]) + (if (seq result) + [:ul.px-3 + {:class (when (and (not (string/blank? input)) + (seq search-result)) + "as-results")} + (for [{:keys [page? icon text header source-block]} result] + (let [block source-block] + [:li.flex.gap-1 + {:on-click (fn [] + (when-let [id (:block/uuid block)] + (route-handler/redirect-to-page! (str id))))} + [:div.flex.flex-col.gap-1.py-1 + (when header + [:div.opacity-60.text-sm + header]) + [:div.flex.flex-row.items-start.gap-1 + (when (and page? icon) (ui/icon icon {:size 15 + :class "text-muted-foreground mt-1"})) + [:div text]]]]))] + (when-not (string/blank? input) + [:div.px-4.text-muted-foreground + "No results"]))])) diff --git a/src/main/mobile/routes.cljs b/src/main/mobile/routes.cljs index 04f0158c4c..9a27e13c6e 100644 --- a/src/main/mobile/routes.cljs +++ b/src/main/mobile/routes.cljs @@ -1,7 +1,6 @@ (ns mobile.routes (:require [frontend.components.page :as page] - [mobile.components.left-sidebar :as mobile-left-sidebar] - [mobile.components.search :as search])) + [mobile.components.left-sidebar :as mobile-left-sidebar])) (def routes [["/" @@ -14,10 +13,6 @@ {:name :page :view (fn [route-match] (page/page-cp route-match))}] - ["/search" - {:name :search - :view (fn [] - (search/search))}] ["/graphs" {:name :graphs}] ["/import" diff --git a/src/main/mobile/state.cljs b/src/main/mobile/state.cljs index 122a592b3a..47e7edfb7d 100644 --- a/src/main/mobile/state.cljs +++ b/src/main/mobile/state.cljs @@ -6,6 +6,9 @@ (defonce *tab (atom "home")) (defn set-tab! [tab] (reset! *tab tab)) (defn use-tab [] (r/use-atom *tab)) +(defonce *search-input (atom "")) +(defn use-search-input [] + (r/use-atom *search-input)) (defonce *modal-blocks (atom [])) (defonce *blocks-navigation-history (atom []))