From 767213cdc1da8ade3541ad93467ec581834c39f8 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Fri, 30 Dec 2022 14:12:07 +0000 Subject: [PATCH] Mobile: Add support for realtime search --- .eslintignore | 6 ++ .gitignore | 6 ++ packages/app-desktop/plugins/GotoAnything.tsx | 16 +---- .../app-mobile/components/ScreenHeader.tsx | 8 +-- .../screens/{search.js => search.tsx} | 72 ++++++++++--------- .../searchengine/SearchEngineUtils.ts | 2 +- .../searchengine/gotoAnythingStyleQuery.ts | 14 ++++ 7 files changed, 72 insertions(+), 52 deletions(-) rename packages/app-mobile/components/screens/{search.js => search.tsx} (77%) create mode 100644 packages/lib/services/searchengine/gotoAnythingStyleQuery.ts diff --git a/.eslintignore b/.eslintignore index cdf031a20..6642ce56c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1005,6 +1005,9 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map packages/app-mobile/components/screens/encryption-config.d.ts packages/app-mobile/components/screens/encryption-config.js packages/app-mobile/components/screens/encryption-config.js.map +packages/app-mobile/components/screens/search.d.ts +packages/app-mobile/components/screens/search.js +packages/app-mobile/components/screens/search.js.map packages/app-mobile/components/side-menu-content.d.ts packages/app-mobile/components/side-menu-content.js packages/app-mobile/components/side-menu-content.js.map @@ -1878,6 +1881,9 @@ packages/lib/services/searchengine/filterParser.js.map packages/lib/services/searchengine/filterParser.test.d.ts packages/lib/services/searchengine/filterParser.test.js packages/lib/services/searchengine/filterParser.test.js.map +packages/lib/services/searchengine/gotoAnythingStyleQuery.d.ts +packages/lib/services/searchengine/gotoAnythingStyleQuery.js +packages/lib/services/searchengine/gotoAnythingStyleQuery.js.map packages/lib/services/searchengine/queryBuilder.d.ts packages/lib/services/searchengine/queryBuilder.js packages/lib/services/searchengine/queryBuilder.js.map diff --git a/.gitignore b/.gitignore index 4b48b4b93..0a903fca3 100644 --- a/.gitignore +++ b/.gitignore @@ -993,6 +993,9 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map packages/app-mobile/components/screens/encryption-config.d.ts packages/app-mobile/components/screens/encryption-config.js packages/app-mobile/components/screens/encryption-config.js.map +packages/app-mobile/components/screens/search.d.ts +packages/app-mobile/components/screens/search.js +packages/app-mobile/components/screens/search.js.map packages/app-mobile/components/side-menu-content.d.ts packages/app-mobile/components/side-menu-content.js packages/app-mobile/components/side-menu-content.js.map @@ -1866,6 +1869,9 @@ packages/lib/services/searchengine/filterParser.js.map packages/lib/services/searchengine/filterParser.test.d.ts packages/lib/services/searchengine/filterParser.test.js packages/lib/services/searchengine/filterParser.test.js.map +packages/lib/services/searchengine/gotoAnythingStyleQuery.d.ts +packages/lib/services/searchengine/gotoAnythingStyleQuery.js +packages/lib/services/searchengine/gotoAnythingStyleQuery.js.map packages/lib/services/searchengine/queryBuilder.d.ts packages/lib/services/searchengine/queryBuilder.js packages/lib/services/searchengine/queryBuilder.js.map diff --git a/packages/app-desktop/plugins/GotoAnything.tsx b/packages/app-desktop/plugins/GotoAnything.tsx index 18186d0cd..b1f763ccb 100644 --- a/packages/app-desktop/plugins/GotoAnything.tsx +++ b/packages/app-desktop/plugins/GotoAnything.tsx @@ -8,6 +8,7 @@ const { connect } = require('react-redux'); const { _ } = require('@joplin/lib/locale'); const { themeStyle } = require('@joplin/lib/theme'); import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine'; +import gotoAnythingStyleQuery from '@joplin/lib/services/searchengine/gotoAnythingStyleQuery'; import BaseModel from '@joplin/lib/BaseModel'; import Tag from '@joplin/lib/models/Tag'; import Folder from '@joplin/lib/models/Folder'; @@ -242,19 +243,6 @@ class Dialog extends React.PureComponent { }, 100); } - makeSearchQuery(query: string) { - const output = []; - const splitted = query.split(' '); - - for (let i = 0; i < splitted.length; i++) { - const s = splitted[i].trim(); - if (!s) continue; - output.push(`${s}*`); - } - - return output.join(' '); - } - async keywords(searchQuery: string) { const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery); return SearchEngine.instance().allParsedQueryTerms(parsedQuery); @@ -321,7 +309,7 @@ class Dialog extends React.PureComponent { } } else { // Note TITLE or BODY listType = BaseModel.TYPE_NOTE; - searchQuery = this.makeSearchQuery(this.state.query); + searchQuery = gotoAnythingStyleQuery(this.state.query); results = await SearchEngine.instance().search(searchQuery); resultsInBody = !!results.find((row: any) => row.fields.includes('body')); diff --git a/packages/app-mobile/components/ScreenHeader.tsx b/packages/app-mobile/components/ScreenHeader.tsx index 4753f87aa..6475975c8 100644 --- a/packages/app-mobile/components/ScreenHeader.tsx +++ b/packages/app-mobile/components/ScreenHeader.tsx @@ -1,7 +1,7 @@ const React = require('react'); import { connect } from 'react-redux'; -import { PureComponent, Component } from 'react'; +import { PureComponent } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions, ViewStyle } from 'react-native'; const Icon = require('react-native-vector-icons/Ionicons').default; const { BackButtonService } = require('../services/back-button.js'); @@ -46,7 +46,7 @@ type DispatchCommandType=(event: { type: string })=> void; interface ScreenHeaderProps { selectedNoteIds: string[]; noteSelectionEnabled: boolean; - parentComponent: Component; + parentComponent: any; showUndoButton: boolean; undoButtonDisabled?: boolean; showRedoButton: boolean; @@ -55,8 +55,8 @@ interface ScreenHeaderProps { folders: FolderEntity[]; folderPickerOptions?: { enabled: boolean; - selectedFolderId: string; - onValueChange: OnValueChangedListener; + selectedFolderId?: string; + onValueChange?: OnValueChangedListener; mustSelect?: boolean; }; diff --git a/packages/app-mobile/components/screens/search.js b/packages/app-mobile/components/screens/search.tsx similarity index 77% rename from packages/app-mobile/components/screens/search.js rename to packages/app-mobile/components/screens/search.tsx index 50b8a4806..21f6f9ed8 100644 --- a/packages/app-mobile/components/screens/search.js +++ b/packages/app-mobile/components/screens/search.tsx @@ -1,23 +1,31 @@ const React = require('react'); -const { StyleSheet, View, TextInput, FlatList, TouchableHighlight } = require('react-native'); +import { StyleSheet, View, TextInput, FlatList, TouchableHighlight } from 'react-native'; const { connect } = require('react-redux'); -const { ScreenHeader } = require('../ScreenHeader'); +import ScreenHeader from '../ScreenHeader'; const Icon = require('react-native-vector-icons/Ionicons').default; -const { _ } = require('@joplin/lib/locale'); -const Note = require('@joplin/lib/models/Note').default; +import { _ } from '@joplin/lib/locale'; +import Note from '@joplin/lib/models/Note'; +import gotoAnythingStyleQuery from '@joplin/lib/services/searchengine/gotoAnythingStyleQuery'; const { NoteItem } = require('../note-item.js'); const { BaseScreenComponent } = require('../base-screen.js'); const { themeStyle } = require('../global-style.js'); const DialogBox = require('react-native-dialogbox').default; -const SearchEngineUtils = require('@joplin/lib/services/searchengine/SearchEngineUtils').default; -const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine').default; +import SearchEngineUtils from '@joplin/lib/services/searchengine/SearchEngineUtils'; +import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine'; +import { AppState } from '../../utils/types'; Icon.loadFont(); class SearchScreenComponent extends BaseScreenComponent { + + private state: any = null; + private isMounted_ = false; + private styles_: any = {}; + private scheduleSearchTimer_: any = null; + static navigationOptions() { - return { header: null }; + return { header: null } as any; } constructor() { @@ -26,8 +34,6 @@ class SearchScreenComponent extends BaseScreenComponent { query: '', notes: [], }; - this.isMounted_ = false; - this.styles_ = {}; } styles() { @@ -36,7 +42,7 @@ class SearchScreenComponent extends BaseScreenComponent { if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId]; this.styles_ = {}; - const styles = { + const styles: any = { body: { flex: 1, }, @@ -65,7 +71,7 @@ class SearchScreenComponent extends BaseScreenComponent { componentDidMount() { this.setState({ query: this.props.query }); - this.refreshSearch(this.props.query); + void this.refreshSearch(this.props.query); this.isMounted_ = true; } @@ -73,19 +79,6 @@ class SearchScreenComponent extends BaseScreenComponent { this.isMounted_ = false; } - searchTextInput_submit() { - const query = this.state.query.trim(); - if (!query) return; - - this.props.dispatch({ - type: 'SEARCH_QUERY', - query: query, - }); - - this.setState({ query: query }); - this.refreshSearch(query); - } - clearButton_press() { this.props.dispatch({ type: 'SEARCH_QUERY', @@ -93,13 +86,13 @@ class SearchScreenComponent extends BaseScreenComponent { }); this.setState({ query: '' }); - this.refreshSearch(''); + void this.refreshSearch(''); } - async refreshSearch(query = null) { + async refreshSearch(query: string = null) { if (!this.props.visible) return; - query = query === null ? this.state.query.trim : query.trim(); + query = gotoAnythingStyleQuery(query); let notes = []; @@ -134,8 +127,24 @@ class SearchScreenComponent extends BaseScreenComponent { this.setState({ notes: notes }); } - searchTextInput_changeText(text) { + scheduleSearch() { + if (this.scheduleSearchTimer_) clearTimeout(this.scheduleSearchTimer_); + + this.scheduleSearchTimer_ = setTimeout(() => { + this.scheduleSearchTimer_ = null; + void this.refreshSearch(this.state.query); + }, 200); + } + + searchTextInput_changeText(text: string) { this.setState({ query: text }); + + this.props.dispatch({ + type: 'SEARCH_QUERY', + query: text, + }); + + this.scheduleSearch(); } render() { @@ -172,9 +181,6 @@ class SearchScreenComponent extends BaseScreenComponent { style={this.styles().searchTextInput} autoFocus={this.props.visible} underlineColorAndroid="#ffffff00" - onSubmitEditing={() => { - this.searchTextInput_submit(); - }} onChangeText={text => this.searchTextInput_changeText(text)} value={this.state.query} selectionColor={theme.textSelectionColor} @@ -188,7 +194,7 @@ class SearchScreenComponent extends BaseScreenComponent { item.id} renderItem={event => } /> { + ref={(dialogbox: any) => { this.dialogbox = dialogbox; }} /> @@ -197,7 +203,7 @@ class SearchScreenComponent extends BaseScreenComponent { } } -const SearchScreen = connect(state => { +const SearchScreen = connect((state: AppState) => { return { query: state.searchQuery, themeId: state.settings.theme, diff --git a/packages/lib/services/searchengine/SearchEngineUtils.ts b/packages/lib/services/searchengine/SearchEngineUtils.ts index 0c547972f..d8823bb3c 100644 --- a/packages/lib/services/searchengine/SearchEngineUtils.ts +++ b/packages/lib/services/searchengine/SearchEngineUtils.ts @@ -3,7 +3,7 @@ import Note from '../../models/Note'; import Setting from '../../models/Setting'; export default class SearchEngineUtils { - static async notesForQuery(query: string, applyUserSettings: boolean, options: any = null, searchEngine: SearchEngine = null) { + public static async notesForQuery(query: string, applyUserSettings: boolean, options: any = null, searchEngine: SearchEngine = null) { if (!options) options = {}; if (!searchEngine) { diff --git a/packages/lib/services/searchengine/gotoAnythingStyleQuery.ts b/packages/lib/services/searchengine/gotoAnythingStyleQuery.ts new file mode 100644 index 000000000..0d09f0ee1 --- /dev/null +++ b/packages/lib/services/searchengine/gotoAnythingStyleQuery.ts @@ -0,0 +1,14 @@ +export default (query: string) => { + if (!query) return ''; + + const output = []; + const splitted = query.split(' '); + + for (let i = 0; i < splitted.length; i++) { + const s = splitted[i].trim(); + if (!s) continue; + output.push(`${s}*`); + } + + return output.join(' '); +};