Skip to content

Commit 803a373

Browse files
committed
enhance(mobile): native search view
1 parent 74fa401 commit 803a373

8 files changed

Lines changed: 213 additions & 133 deletions

File tree

ios/App/App/LiquidTabsPlugin.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
1616
public let jsName = "LiquidTabsPlugin"
1717
public let pluginMethods: [CAPPluginMethod] = [
1818
CAPPluginMethod(name: "configureTabs", returnType: CAPPluginReturnPromise),
19-
CAPPluginMethod(name: "selectTab", returnType: CAPPluginReturnPromise)
19+
CAPPluginMethod(name: "selectTab", returnType: CAPPluginReturnPromise),
20+
CAPPluginMethod(name: "updateNativeSearchResults", returnType: CAPPluginReturnPromise),
2021
]
2122

2223
public override func load() {
@@ -74,6 +75,27 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
7475
call.resolve()
7576
}
7677

78+
/// Update native search results list from JS.
79+
/// { results: [{ id, title, subtitle? }] }
80+
@objc func updateNativeSearchResults(_ call: CAPPluginCall) {
81+
guard let resultDicts = call.getArray("results", JSObject.self) else {
82+
call.reject("Missing 'results'")
83+
return
84+
}
85+
86+
let mapped: [NativeSearchResult] = resultDicts.compactMap { dict in
87+
guard let id = dict["id"] as? String,
88+
let title = dict["title"] as? String else {
89+
return nil
90+
}
91+
let subtitle = dict["subtitle"] as? String
92+
return NativeSearchResult(id: id, title: title, subtitle: subtitle)
93+
}
94+
95+
store.updateSearchResults(mapped)
96+
call.resolve()
97+
}
98+
7799
// MARK: - Events to JS
78100

79101
func notifyTabSelected(id: String) {
@@ -88,6 +110,10 @@ public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
88110
notifyListeners("keyboardHackKey", data: ["key": key])
89111
}
90112

113+
func openResult(id: String) {
114+
notifyListeners("openSearchResultBlock", data: ["id": id])
115+
}
116+
91117
private func installKeyboardHackScript() {
92118
guard !keyboardHackScriptInstalled,
93119
let controller = bridge?.webView?.configuration.userContentController else {

ios/App/App/LiquidTabsRootView.swift

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ private struct LiquidTabs26View: View {
113113
@StateObject private var store = LiquidTabsStore.shared
114114
let navController: UINavigationController
115115

116-
@State private var searchText: String = ""
117116
@FocusState private var isSearchFocused: Bool
118117

119118
@State private var hackShowKeyboard: Bool = false
@@ -135,6 +134,13 @@ private struct LiquidTabs26View: View {
135134
)
136135
}
137136

137+
private var searchTextBinding: Binding<String> {
138+
Binding(
139+
get: { store.searchText },
140+
set: { store.searchText = $0 }
141+
)
142+
}
143+
138144
private func handleRetap(on selection: LiquidTabsTabSelection) {
139145
print("User re-tapped tab: \(selection)")
140146
navController.popToRootViewController(animated: true)
@@ -219,10 +225,13 @@ private struct LiquidTabs26View: View {
219225
.ignoresSafeArea()
220226
}
221227
}
222-
.searchable(text: $searchText)
228+
.searchable(text: searchTextBinding)
223229
.searchFocused($isSearchFocused)
224230
.searchToolbarBehavior(.minimize)
225-
.onChange(of: searchText) { query in
231+
.onChange(of: store.searchText) { query in
232+
if query.isEmpty {
233+
store.searchResults = []
234+
}
226235
LiquidTabsPlugin.shared?.notifySearchChanged(query: query)
227236
}
228237
.background(Color.logseqBackground)
@@ -295,35 +304,47 @@ private struct LiquidTabs26View: View {
295304
// Search host for 26+
296305
// Only responsible for cancel behaviour and tab switching.
297306
// It does NOT own the focus anymore.
307+
@available(iOS 26.0, *)
308+
private enum SearchRoute: Hashable {
309+
case result(String)
310+
}
311+
298312
@available(iOS 26.0, *)
299313
private struct SearchTabHost26: View {
300314
let navController: UINavigationController
301315
var selectedTab: Binding<LiquidTabsTabSelection>
302316
let firstTabId: String?
303-
let store: LiquidTabsStore
317+
@ObservedObject var store: LiquidTabsStore
304318

305319
@Environment(\.isSearching) private var isSearching
306320
@State private var wasSearching: Bool = false
307321

308322
var body: some View {
309323
NavigationStack {
310-
NativeNavHost(navController: navController)
311-
.ignoresSafeArea()
312-
.onChange(of: isSearching) { searching in
313-
if searching {
314-
wasSearching = true
315-
} else if wasSearching,
316-
case .search = selectedTab.wrappedValue,
317-
let firstId = firstTabId {
318-
319-
// User tapped Cancel: switch back to first normal tab.
320-
wasSearching = false
321-
selectedTab.wrappedValue = .content(0)
322-
store.selectedId = firstId
323-
}
324-
}
324+
ZStack {
325+
Color.logseqBackground
326+
.ignoresSafeArea()
327+
328+
SearchResultsContent(
329+
navController: navController,
330+
store: store
331+
)
332+
}
325333
}
334+
.onChange(of: isSearching) { searching in
335+
if searching {
336+
wasSearching = true
337+
} else if wasSearching,
338+
case .search = selectedTab.wrappedValue,
339+
let firstId = firstTabId {
340+
341+
wasSearching = false
342+
selectedTab.wrappedValue = .content(0)
343+
store.selectedId = firstId
344+
}
345+
}
326346
}
347+
327348
}
328349

329350
// MARK: - iOS 16–25 implementation
@@ -333,9 +354,15 @@ private struct LiquidTabs16View: View {
333354
@StateObject private var store = LiquidTabsStore.shared
334355
let navController: UINavigationController
335356

336-
@State private var searchText: String = ""
337357
@State private var hackShowKeyboard: Bool = false
338358

359+
private var searchTextBinding: Binding<String> {
360+
Binding(
361+
get: { store.searchText },
362+
set: { store.searchText = $0 }
363+
)
364+
}
365+
339366
var body: some View {
340367
ZStack {
341368
Color.logseqBackground.ignoresSafeArea()
@@ -380,7 +407,8 @@ private struct LiquidTabs16View: View {
380407
// --- 🔍 SEARCH TAB (iOS 16–25) ---
381408
SearchTab16Host(
382409
navController: navController,
383-
searchText: $searchText
410+
searchText: searchTextBinding,
411+
store: store
384412
)
385413
.ignoresSafeArea()
386414
.tabItem {
@@ -425,13 +453,18 @@ private struct LiquidTabs16View: View {
425453
private struct SearchTab16Host: View {
426454
let navController: UINavigationController
427455
@Binding var searchText: String
456+
@ObservedObject var store: LiquidTabsStore
428457

429458
var body: some View {
430459
NavigationStack {
431460
ZStack {
432-
// Main content (fills whole screen)
433-
NativeNavHost(navController: navController)
434-
.ignoresSafeArea()
461+
Color.logseqBackground
462+
.ignoresSafeArea()
463+
464+
SearchResultsContent(
465+
navController: navController,
466+
store: store
467+
)
435468

436469
// Bottom search bar
437470
VStack {
@@ -462,10 +495,36 @@ private struct SearchTab16Host: View {
462495
.padding(.bottom, 12)
463496
}
464497
}
465-
.navigationBarHidden(true)
466498
}
467499
.onChange(of: searchText) { query in
468500
LiquidTabsPlugin.shared?.notifySearchChanged(query: query)
469501
}
470502
}
471503
}
504+
505+
private struct SearchResultsContent: View {
506+
let navController: UINavigationController
507+
@ObservedObject var store: LiquidTabsStore
508+
509+
var body: some View {
510+
List(store.searchResults) { result in
511+
NavigationLink(value: result) {
512+
Text(result.title)
513+
.foregroundColor(.primary)
514+
.padding(.vertical, 8)
515+
.contentShape(Rectangle()) // improves tap area
516+
}
517+
.listRowBackground(Color.clear)
518+
}
519+
.scrollContentBackground(.hidden)
520+
.scrollDismissesKeyboard(.immediately)
521+
.navigationTitle("Search")
522+
.navigationDestination(for: NativeSearchResult.self) { result in
523+
NativeNavHost(navController: navController)
524+
.ignoresSafeArea()
525+
.onAppear {
526+
LiquidTabsPlugin.shared?.openResult(id: result.id)
527+
}
528+
}
529+
}
530+
}

ios/App/App/LiquidTabsStore.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import SwiftUI
22
import Combine
33

4+
struct NativeSearchResult: Identifiable, Hashable {
5+
let id: String // page or block id from JS
6+
let title: String
7+
let subtitle: String? // optional: page path, snippet, etc.
8+
}
9+
410
struct LiquidTab: Identifiable, Equatable {
511
let id: String
612
let title: String
@@ -18,6 +24,10 @@ final class LiquidTabsStore: ObservableObject {
1824

1925
@Published var tabs: [LiquidTab] = []
2026
@Published var selectedId: String?
27+
@Published var searchText: String = ""
28+
29+
// Native-rendered search results supplied by JS.
30+
@Published var searchResults: [NativeSearchResult] = []
2131

2232
// Helper to get a stable selection if JS forgets
2333
func effectiveSelectedId() -> String? {
@@ -30,4 +40,10 @@ final class LiquidTabsStore: ObservableObject {
3040
func tab(for id: String) -> LiquidTab? {
3141
tabs.first(where: { $0.id == id })
3242
}
43+
44+
func updateSearchResults(_ results: [NativeSearchResult]) {
45+
DispatchQueue.main.async {
46+
self.searchResults = results
47+
}
48+
}
3349
}

src/main/mobile/bottom_tabs.cljs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
(:require [cljs-bean.core :as bean]
44
[clojure.string :as string]
55
[frontend.handler.editor :as editor-handler]
6+
[frontend.handler.route :as route-handler]
67
[frontend.state :as state]
78
[frontend.util :as util]
89
[logseq.common.util :as common-util]
9-
[mobile.state :as mobile-state]))
10+
[mobile.search :as mobile-search]
11+
[mobile.state :as mobile-state]
12+
[promesa.core :as p]))
1013

1114
;; Capacitor plugin instance:
1215
;; Make sure the plugin is registered as `LiquidTabs` on the native side.
@@ -32,6 +35,12 @@
3235
liquid-tabs
3336
#js {:id id}))
3437

38+
(defn update-native-search-results!
39+
"Send native search result list to the iOS plugin."
40+
[results]
41+
(when (and (util/capacitor?) liquid-tabs (.-updateNativeSearchResults liquid-tabs))
42+
(.updateNativeSearchResults liquid-tabs (clj->js {:results results}))))
43+
3544
(defn add-tab-selected-listener!
3645
"Listen to native tab selection.
3746
@@ -60,6 +69,16 @@
6069
;; data is like { query: string }
6170
(f (.-query data)))))
6271

72+
(defn add-search-result-item-listener!
73+
[]
74+
(.addListener
75+
liquid-tabs
76+
"openSearchResultBlock"
77+
(fn [data]
78+
(when-let [id (.-id data)]
79+
(when-not (string/blank? id)
80+
(route-handler/redirect-to-page! id {:push false}))))))
81+
6382
(defn add-keyboard-hack-listener!
6483
"Listen for Backspace or Enter while the invisible keyboard field is focused."
6584
[]
@@ -97,9 +116,9 @@
97116
(js/console.log "Native search query" q)
98117
(reset! mobile-state/*search-input q)
99118
(reset! mobile-state/*search-last-input-at (common-util/time-ms))
100-
(comment
101-
(when (= :page (state/get-current-route))
102-
(mobile-nav/reset-route!)))))
119+
(p/let [result (mobile-search/search q)]
120+
(update-native-search-results! result))))
121+
(add-search-result-item-listener!)
103122
(add-keyboard-hack-listener!)))
104123

105124
(defn configure

src/main/mobile/components/app.cljs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
[mobile.components.graphs :as graphs]
2222
[mobile.components.header :as mobile-header]
2323
[mobile.components.popup :as popup]
24-
[mobile.components.search :as search]
2524
[mobile.components.selection-toolbar :as selection-toolbar]
2625
[mobile.components.ui :as ui-component]
2726
[mobile.state :as mobile-state]
@@ -106,7 +105,7 @@
106105
(cond
107106
(= tab "graphs") (graphs/page)
108107
(= tab "go to") (favorites/favorites)
109-
(= tab "search") (search/search)
108+
(= tab "search") nil
110109
(= tab "capture") (capture)))]))
111110

112111
(rum/defc main-content < rum/static

src/main/mobile/components/header.cljs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
(defn- configure-native-top-bar!
144144
[repo {:keys [tab title route-name route-view sync-color favorited?]}]
145145
(when (mobile-util/native-ios?)
146-
(let [hidden? false
146+
(let [hidden? (= tab "search")
147147
rtc-indicator? (and repo
148148
(ldb/get-graph-rtc-uuid (db/get-db))
149149
(user-handler/logged-in?))

0 commit comments

Comments
 (0)