From 18e86a7ba3bda6f0241ef939c3bf33bd6b946c5c Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 16 Nov 2023 04:17:03 -0800 Subject: [PATCH] Mobile: Resolves #9294: Implement settings search (#9320) --- .../app-mobile/components/ScreenHeader.tsx | 7 +- .../screens/ConfigScreen/ConfigScreen.tsx | 283 ++++++++++++------ .../ExportDebugReportButton.tsx | 4 +- .../NoteExportSection/ExportProfileButton.tsx | 4 +- .../NoteExportSection/NoteExportButton.tsx | 8 +- 5 files changed, 202 insertions(+), 104 deletions(-) diff --git a/packages/app-mobile/components/ScreenHeader.tsx b/packages/app-mobile/components/ScreenHeader.tsx index a983e3aa2..39f999af8 100644 --- a/packages/app-mobile/components/ScreenHeader.tsx +++ b/packages/app-mobile/components/ScreenHeader.tsx @@ -70,6 +70,7 @@ interface ScreenHeaderProps { onRedoButtonPress: OnPressCallback; onSaveButtonPress: OnPressCallback; sortButton_press?: OnPressCallback; + onSearchButtonPress?: OnPressCallback; showSideMenuButton?: boolean; showSearchButton?: boolean; @@ -242,7 +243,11 @@ class ScreenHeaderComponent extends PureComponent(); @@ -128,6 +134,21 @@ class ConfigScreenComponent extends BaseScreenComponent { + this.setShowSearch_(!this.state.searching); + }; + + private onSearchUpdate_ = (newQuery: string) => { + this.setState({ searchQuery: newQuery }); + }; + private updateSidebarWidth = () => { const windowWidth = Dimensions.get('window').width; @@ -150,10 +171,13 @@ class ConfigScreenComponent extends BaseScreenComponent windowWidth / 2; } - private switchSectionPress_ = (section: string) => { + private onJumpToSection_ = (section: string) => { const label = Setting.sectionNameToLabel(section); AccessibilityInfo.announceForAccessibility(_('Opening section %s', label)); - this.setState({ selectedSectionName: section }); + this.setState({ + selectedSectionName: section, + searching: false, + }); }; private showSectionNavigation_ = () => { @@ -245,6 +269,12 @@ class ConfigScreenComponent extends BaseScreenComponent { + let searchThrough; + if (Array.isArray(relatedText)) { + searchThrough = relatedText.join('\n'); + } else { + searchThrough = relatedText; + } + searchThrough = searchThrough.toLocaleLowerCase(); + + const searchQuery = this.state.searchQuery.toLocaleLowerCase().trim(); + + const hasSearchMatches = + headerTitle.toLocaleLowerCase() === searchQuery + || searchThrough.includes(searchQuery); + + // Don't show results when the search input is empty + return this.state.searchQuery.length > 0 && hasSearchMatches; + }; + + const addSettingComponent = (component: ReactElement, relatedText: string|string[]) => { + const hiddenBySearch = this.state.searching && !matchesSearchQuery(relatedText); + if (component && !hiddenBySearch) { + settingComps.push(component); + } + }; + + const addSettingButton = (key: string, title: string, clickHandler: ()=> void, options: any = null) => { + const relatedText = [title]; + if (typeof options === 'object' && options?.description) { + relatedText.push(options.description); + } + addSettingComponent(this.renderButton(key, title, clickHandler, options), relatedText); + }; const styleSheet = this.styles().styleSheet; + const addSettingLink = (key: string, title: string, target: string) => { + const component = ( + + { + void Linking.openURL(target); + }} + accessibilityRole='link' + > + + {title} + + + + ); + + addSettingComponent(component, title); + }; + + const addSettingText = (key: string, text: string) => { + addSettingComponent( + + {text} + , + text, + ); + }; for (let i = 0; i < section.metadatas.length; i++) { const md = section.metadatas[i]; @@ -322,24 +415,29 @@ class ConfigScreenComponent extends BaseScreenComponent ); - settingComps.push(this.renderButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp })); + addSettingButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp }); } } const settingComp = this.settingToComponent(md.key, settings[md.key]); - settingComps.push(settingComp); + const relatedText = [md.label?.() ?? '', md.description?.() ?? '']; + addSettingComponent( + settingComp, + relatedText, + ); } if (section.name === 'sync') { - settingComps.push(this.renderButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_)); + addSettingButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_); } if (section.name === 'joplinCloud') { + const label = _('Email to note'); const description = _('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook'); - settingComps.push( + addSettingComponent( - {_('Email to note')} + {label} {this.props.settings['sync.10.inboxEmail']} { @@ -351,20 +449,30 @@ class ConfigScreenComponent extends BaseScreenComponent, + [label, description], ); } if (section.name === 'tools') { - settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_)); - settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_)); - settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_)); - settingComps.push(this.renderButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') })); + addSettingButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_); + addSettingButton('status_button', _('Sync Status'), this.syncStatusButtonPress_); + addSettingButton('log_button', _('Log'), this.logButtonPress_); + addSettingButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') }); } if (section.name === 'export') { - settingComps.push(); - settingComps.push(); - settingComps.push(); + addSettingComponent( + , + [exportButtonTitle(), exportButtonDescription()], + ); + addSettingComponent( + , + exportDebugReportTitle(), + ); + addSettingComponent( + , + exportProfileButtonTitle(), + ); } if (section.name === 'moreInfo') { @@ -372,7 +480,7 @@ class ConfigScreenComponent extends BaseScreenComponent @@ -389,95 +497,60 @@ class ConfigScreenComponent extends BaseScreenComponent , + '', ); } - settingComps.push( - - { - void Linking.openURL('https://joplinapp.org/donate/'); - }} - > - - {_('Make a donation')} - - - , - ); + addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/'); + addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/'); + addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/'); - settingComps.push( - - { - void Linking.openURL('https://joplinapp.org/'); - }} - > - - {_('Joplin website')} - - - , - ); - - settingComps.push( - - { - void Linking.openURL('https://joplinapp.org/privacy/'); - }} - > - - {_('Privacy Policy')} - - - , - ); - - settingComps.push( - - {`Joplin ${VersionInfo.appVersion}`} - , - ); - - settingComps.push( - - {_('Database v%s', reg.db().version())} - , - ); - - settingComps.push( - - {_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])} - , - ); - - settingComps.push( - - {_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)} - , - ); + addSettingText('version_info_app', `Joplin ${VersionInfo.appVersion}`); + addSettingText('version_info_db', _('Database v%s', reg.db().version())); + addSettingText('version_info_fts', _('FTS enabled: %d', this.props.settings['db.ftsEnabled'])); + addSettingText('version_info_hermes', _('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)); const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile); if (featureFlagKeys.length) { const headerKey = 'featureFlags'; - settingComps.push( this.onHeaderLayout(headerKey, event)} - />); + const featureFlagsTitle = _('Feature flags'); + addSettingComponent( + this.onHeaderLayout(headerKey, event)} + />, + _('Feature flags'), + ); - settingComps.push({this.renderFeatureFlags(settings, featureFlagKeys)}); + addSettingComponent( + {this.renderFeatureFlags(settings, featureFlagKeys)}, + featureFlagsTitle, + ); } } if (!settingComps.length) return null; - if (!isSelected) return null; + if (!isSelected && !this.state.searching) return null; + + const headerComponent = ( + { + this.onJumpToSection_(section.name); + }}> + + + ); return ( this.onSectionLayout(key, event)}> - {settingComps} + + {this.state.searching ? headerComponent : null} + {settingComps} + ); } @@ -563,18 +636,22 @@ class ConfigScreenComponent extends BaseScreenComponent ); let currentSection: ReactNode; - if (currentSectionName) { + if (currentSectionName || this.state.searching) { const settingComps = shared.settingsToComponents2( this, AppType.Mobile, settings, currentSectionName, @@ -582,11 +659,20 @@ class ConfigScreenComponent extends BaseScreenComponent; + currentSection = ( + {this.state.searching ? searchInput : null} {settingComps} ); @@ -620,10 +706,11 @@ class ConfigScreenComponent extends BaseScreenComponent {mainComponent} diff --git a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.tsx index 8d2e32df7..fce51c5a4 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.tsx @@ -11,6 +11,8 @@ interface Props { styles: ConfigScreenStyles; } +export const exportDebugReportTitle = () => _('Export Debug Report'); + const ExportDebugReportButton = (props: Props) => { const [creatingReport, setCreatingReport] = useState(false); @@ -24,7 +26,7 @@ const ExportDebugReportButton = (props: Props) => { const exportDebugReportButton = ( _('Export profile'); + const ExportProfileButton = (props: Props) => { const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle'); const [profileExportPath, setProfileExportPath] = useState(''); @@ -31,7 +33,7 @@ const ExportProfileButton = (props: Props) => { const exportProfileButton = ( _('Export all notes as JEX'); +export const exportButtonDescription = () => _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.'); + const NoteExportButton: FunctionComponent = props => { const [exportStatus, setExportStatus] = useState(ExportStatus.NotStarted); const [exportProgress, setExportProgress] = useState(0); @@ -80,13 +83,12 @@ const NoteExportButton: FunctionComponent = props => { indeterminate={exportProgress === undefined} progress={exportProgress}/> ); - const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.'); const startOrCancelExportButton = (