From 672d028d293e04a76b8293034593f5cb26869b25 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:19:08 -0800 Subject: [PATCH] Mobile: Settings screen: Create separate pages for each screen (#8567) --- .eslintignore | 18 +- .gitignore | 18 +- .../gui/ConfigScreen/ConfigScreen.tsx | 11 +- .../app-desktop/gui/ConfigScreen/Sidebar.tsx | 6 +- packages/app-mobile/components/Icon.tsx | 44 + .../{base-screen.js => base-screen.ts} | 13 +- .../screens/ConfigScreen/ConfigScreen.tsx | 783 ++++++++---------- .../ConfigScreen/FileSystemPathSelector.tsx | 65 ++ .../ExportDebugReportButton.tsx | 42 + .../NoteExportSection/ExportProfileButton.tsx | 79 ++ .../NoteExportSection/NoteExportButton.tsx | 12 +- .../{ => utils}/exportAllFolders.ts | 0 .../utils/exportDebugReport.ts | 32 + .../NoteExportSection/utils/exportProfile.ts | 35 + .../screens/ConfigScreen/SectionHeader.tsx | 25 + .../screens/ConfigScreen/SectionSelector.tsx | 101 +++ .../screens/ConfigScreen/SettingComponent.tsx | 156 ++++ .../screens/ConfigScreen/SettingItem.tsx | 0 ...figScreenButton.tsx => SettingsButton.tsx} | 12 +- .../screens/ConfigScreen/SettingsToggle.tsx | 45 + .../ConfigScreen/configScreenStyles.ts | 78 +- .../components/screens/ConfigScreen/types.ts | 10 + .../app-mobile/components/screens/Note.tsx | 2 +- .../app-mobile/components/screens/Notes.tsx | 2 +- .../components/screens/dropbox-login.js | 2 +- .../app-mobile/components/screens/folder.js | 2 +- .../components/screens/onedrive-login.js | 2 +- .../app-mobile/components/screens/search.tsx | 2 +- .../app-mobile/components/screens/status.js | 2 +- .../app-mobile/components/screens/tags.js | 2 +- .../{config-shared.js => config-shared.ts} | 139 ++-- packages/lib/models/Setting.ts | 93 ++- packages/lib/reducer.ts | 2 +- packages/lib/registry.ts | 2 + 34 files changed, 1293 insertions(+), 544 deletions(-) create mode 100644 packages/app-mobile/components/Icon.tsx rename packages/app-mobile/components/{base-screen.js => base-screen.ts} (50%) create mode 100644 packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.tsx rename packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/{ => utils}/exportAllFolders.ts (100%) create mode 100644 packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.ts create mode 100644 packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.ts create mode 100644 packages/app-mobile/components/screens/ConfigScreen/SectionHeader.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/SectionSelector.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/SettingItem.tsx rename packages/app-mobile/components/screens/ConfigScreen/{ConfigScreenButton.tsx => SettingsButton.tsx} (74%) create mode 100644 packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/types.ts rename packages/lib/components/shared/config/{config-shared.js => config-shared.ts} (60%) diff --git a/.eslintignore b/.eslintignore index 045e78cc3..d0114686c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -427,6 +427,7 @@ packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.js packages/app-mobile/components/ExtendedWebView.js packages/app-mobile/components/FolderPicker.js +packages/app-mobile/components/Icon.js packages/app-mobile/components/Modal.js packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js @@ -467,17 +468,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js packages/app-mobile/components/SideMenu.js packages/app-mobile/components/TextInput.js packages/app-mobile/components/app-nav.js +packages/app-mobile/components/base-screen.js packages/app-mobile/components/biometrics/BiometricPopup.js packages/app-mobile/components/biometrics/biometricAuthenticate.js packages/app-mobile/components/biometrics/sensorInfo.js packages/app-mobile/components/getResponsiveValue.test.js packages/app-mobile/components/getResponsiveValue.js packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js -packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js +packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js -packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js +packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js +packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js +packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js +packages/app-mobile/components/screens/ConfigScreen/SettingItem.js +packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js +packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js +packages/app-mobile/components/screens/ConfigScreen/types.js packages/app-mobile/components/screens/LogScreen.js packages/app-mobile/components/screens/Note.js packages/app-mobile/components/screens/Notes.js @@ -600,6 +613,7 @@ packages/lib/commands/index.js packages/lib/commands/openMasterPasswordDialog.js packages/lib/commands/synchronize.js packages/lib/components/EncryptionConfigScreen/utils.js +packages/lib/components/shared/config/config-shared.js packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js packages/lib/components/shared/note-screen-shared.js diff --git a/.gitignore b/.gitignore index b6569cb05..2e5be75bf 100644 --- a/.gitignore +++ b/.gitignore @@ -409,6 +409,7 @@ packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.js packages/app-mobile/components/ExtendedWebView.js packages/app-mobile/components/FolderPicker.js +packages/app-mobile/components/Icon.js packages/app-mobile/components/Modal.js packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js @@ -449,17 +450,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js packages/app-mobile/components/SideMenu.js packages/app-mobile/components/TextInput.js packages/app-mobile/components/app-nav.js +packages/app-mobile/components/base-screen.js packages/app-mobile/components/biometrics/BiometricPopup.js packages/app-mobile/components/biometrics/biometricAuthenticate.js packages/app-mobile/components/biometrics/sensorInfo.js packages/app-mobile/components/getResponsiveValue.test.js packages/app-mobile/components/getResponsiveValue.js packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js -packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js +packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js -packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js +packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js +packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js +packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js +packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js +packages/app-mobile/components/screens/ConfigScreen/SettingItem.js +packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js +packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js +packages/app-mobile/components/screens/ConfigScreen/types.js packages/app-mobile/components/screens/LogScreen.js packages/app-mobile/components/screens/Note.js packages/app-mobile/components/screens/Notes.js @@ -582,6 +595,7 @@ packages/lib/commands/index.js packages/lib/commands/openMasterPasswordDialog.js packages/lib/commands/synchronize.js packages/lib/components/EncryptionConfigScreen/utils.js +packages/lib/components/shared/config/config-shared.js packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js packages/lib/components/shared/note-screen-shared.js diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 4c9d5155e..1ba1350ce 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -12,7 +12,7 @@ const { connect } = require('react-redux'); const { themeStyle } = require('@joplin/lib/theme'); const pathUtils = require('@joplin/lib/path-utils'); import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; -const shared = require('@joplin/lib/components/shared/config/config-shared.js'); +import * as shared from '@joplin/lib/components/shared/config/config-shared.js'; import ClipperConfigScreen from '../ClipperConfigScreen'; import restart from '../../services/restart'; import PluginService from '@joplin/lib/services/plugins/PluginService'; @@ -35,9 +35,10 @@ class ConfigScreenComponent extends React.Component { public constructor(props: any) { super(props); - shared.init(this, reg); + shared.init(reg); this.state = { + ...shared.defaultScreenState, selectedSectionName: 'general', screenName: '', changedSettingKeys: [], @@ -98,7 +99,7 @@ class ConfigScreenComponent extends React.Component { } public sectionByName(name: string) { - const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings }); + const sections = shared.settingsSections({ device: AppType.Desktop, settings: this.state.settings }); for (const section of sections) { if (section.name === name) return section; } @@ -699,7 +700,7 @@ class ConfigScreenComponent extends React.Component { const hasChanges = this.hasChanges(); - const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName); + const settingComps = shared.settingsToComponents2(this, AppType.Desktop, settings, this.state.selectedSectionName); // screenComp is a custom config screen, such as the encryption config screen or keymap config screen. // These screens handle their own loading/saving of settings and have bespoke rendering. @@ -708,7 +709,7 @@ class ConfigScreenComponent extends React.Component { if (screenComp) containerStyle.display = 'none'; - const sections = shared.settingsSections({ device: 'desktop', settings }); + const sections = shared.settingsSections({ device: AppType.Desktop, settings }); const needRestartComp: any = this.state.needRestart ? (
diff --git a/packages/app-desktop/gui/ConfigScreen/Sidebar.tsx b/packages/app-desktop/gui/ConfigScreen/Sidebar.tsx index 308e57fb7..ab89b9aa1 100644 --- a/packages/app-desktop/gui/ConfigScreen/Sidebar.tsx +++ b/packages/app-desktop/gui/ConfigScreen/Sidebar.tsx @@ -1,4 +1,4 @@ -import { SettingSectionSource } from '@joplin/lib/models/Setting'; +import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting'; import * as React from 'react'; import { useMemo } from 'react'; import Setting from '@joplin/lib/models/Setting'; @@ -93,7 +93,9 @@ export default function Sidebar(props: Props) { const selected = props.selection === section.name; return ( { props.onSelectionChange({ section: section }); }}> - + {Setting.sectionNameToLabel(section.name)} diff --git a/packages/app-mobile/components/Icon.tsx b/packages/app-mobile/components/Icon.tsx new file mode 100644 index 000000000..741f0ab6d --- /dev/null +++ b/packages/app-mobile/components/Icon.tsx @@ -0,0 +1,44 @@ + +import * as React from 'react'; +import { TextStyle } from 'react-native'; +const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default; + +interface Props { + name: string; + style: TextStyle; + + // If `null` is given, the content must be labeled elsewhere. + accessibilityLabel: string|null; +} + +const Icon: React.FC = props => { + // Matches: + // 1. A prefix of word characters (\w+) + // 2. A suffix of non-spaces (\S+) + // An "fa-" at the beginning of the suffix is ignored. + const nameMatch = props.name.match(/^(\w+)\s+(?:fa-)?(\S+)$/); + + const namePrefix = nameMatch ? nameMatch[1] : ''; + const nameSuffix = nameMatch ? nameMatch[2] : props.name; + + // If there's no label, make sure that the screen reader doesn't try + // to read the characters from the icon font (they don't make sense + // without the icon font applied). + const accessibilityHidden = props.accessibilityLabel === null; + + return ( + + ); +}; + +export default Icon; diff --git a/packages/app-mobile/components/base-screen.js b/packages/app-mobile/components/base-screen.ts similarity index 50% rename from packages/app-mobile/components/base-screen.js rename to packages/app-mobile/components/base-screen.ts index b94c6f701..c2b2e6e1e 100644 --- a/packages/app-mobile/components/base-screen.js +++ b/packages/app-mobile/components/base-screen.ts @@ -1,12 +1,12 @@ -const React = require('react'); -const { StyleSheet } = require('react-native'); +import * as React from 'react'; +import { StyleSheet } from 'react-native'; const { themeStyle } = require('./global-style.js'); -const rootStyles_ = {}; +const rootStyles_: Record = {}; -class BaseScreenComponent extends React.Component { +class BaseScreenComponent extends React.Component { - rootStyle(themeId) { + protected rootStyle(themeId: number) { const theme = themeStyle(themeId); if (rootStyles_[themeId]) return rootStyles_[themeId]; rootStyles_[themeId] = StyleSheet.create({ @@ -19,4 +19,5 @@ class BaseScreenComponent extends React.Component { } } -module.exports = { BaseScreenComponent }; +export { BaseScreenComponent }; +export default BaseScreenComponent; diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx index 63e1e7f61..659fe9b56 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx @@ -1,214 +1,168 @@ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ -import Slider from '@react-native-community/slider'; -const React = require('react'); -import { Platform, Linking, View, Switch, ScrollView, Text, Button, TouchableOpacity, TextInput, Alert, PermissionsAndroid, TouchableNativeFeedback } from 'react-native'; +import * as React from 'react'; +import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native'; import Setting, { AppType } from '@joplin/lib/models/Setting'; import NavService from '@joplin/lib/services/NavService'; -import ReportService from '@joplin/lib/services/ReportService'; import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine'; import checkPermissions from '../../../utils/checkPermissions'; -import time from '@joplin/lib/time'; -import shim from '@joplin/lib/shim'; import setIgnoreTlsErrors from '../../../utils/TlsUtils'; import { reg } from '@joplin/lib/registry'; import { State } from '@joplin/lib/reducer'; const { BackButtonService } = require('../../../services/back-button.js'); const VersionInfo = require('react-native-version-info').default; -const { connect } = require('react-redux'); +import { connect } from 'react-redux'; import ScreenHeader from '../../ScreenHeader'; -const { _ } = require('@joplin/lib/locale'); -const { BaseScreenComponent } = require('../../base-screen.js'); -const { Dropdown } = require('../../Dropdown'); +import { _ } from '@joplin/lib/locale'; +import BaseScreenComponent from '../../base-screen'; const { themeStyle } = require('../../global-style.js'); -const shared = require('@joplin/lib/components/shared/config/config-shared.js'); +import * as shared from '@joplin/lib/components/shared/config/config-shared'; import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; -import { openDocumentTree } from '@joplin/react-native-saf-x'; import biometricAuthenticate from '../../biometrics/biometricAuthenticate'; -import configScreenStyles from './configScreenStyles'; +import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles'; import NoteExportButton from './NoteExportSection/NoteExportButton'; -import ConfigScreenButton from './ConfigScreenButton'; +import SettingsButton from './SettingsButton'; import Clipboard from '@react-native-community/clipboard'; +import { ReactNode } from 'react'; +import { Dispatch } from 'redux'; +import SectionHeader from './SectionHeader'; +import ExportProfileButton from './NoteExportSection/ExportProfileButton'; +import SettingComponent from './SettingComponent'; +import ExportDebugReportButton from './NoteExportSection/ExportDebugReportButton'; +import SectionSelector from './SectionSelector'; -class ConfigScreenComponent extends BaseScreenComponent { +interface ConfigScreenState { + settings: any; + changedSettingKeys: string[]; + + fixingSearchIndex: boolean; + checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null; + showAdvancedSettings: boolean; + + selectedSectionName: string|null; + sidebarWidth: number; +} + +interface ConfigScreenProps { + settings: any; + themeId: number; + navigation: any; + + dispatch: Dispatch; +} + +class ConfigScreenComponent extends BaseScreenComponent { public static navigationOptions(): any { return { header: null }; } private componentsY_: Record = {}; + private styles_: Record = {}; + private scrollViewRef_: React.RefObject; - public constructor() { - super(); - this.styles_ = {}; + public constructor(props: ConfigScreenProps) { + super(props); this.state = { - creatingReport: false, - profileExportStatus: 'idle', - profileExportPath: '', - fileSystemSyncPath: Setting.value('sync.2.path'), + ...shared.defaultScreenState, + selectedSectionName: null, + fixingSearchIndex: false, + sidebarWidth: 100, }; - this.scrollViewRef_ = React.createRef(); + this.scrollViewRef_ = React.createRef(); - shared.init(this, reg); - - this.selectDirectoryButtonPress = async () => { - try { - const doc = await openDocumentTree(true); - if (doc?.uri) { - this.setState({ fileSystemSyncPath: doc.uri }); - shared.updateSettingValue(this, 'sync.2.path', doc.uri); - } else { - throw new Error('User cancelled operation'); - } - } catch (e) { - reg.logger().info('Didn\'t pick sync dir: ', e); - } - }; - - this.checkSyncConfig_ = async () => { - // to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state - // this call sets the new value and returns the previous one which we can use later to revert the change - const prevIgnoreTlsErrors = await setIgnoreTlsErrors(this.state.settings['net.ignoreTlsErrors']); - const result = await shared.checkSyncConfig(this, this.state.settings); - if (!result || !result.ok) { - await setIgnoreTlsErrors(prevIgnoreTlsErrors); - } - }; - - this.e2eeConfig_ = () => { - void NavService.go('EncryptionConfig'); - }; - - this.saveButton_press = async () => { - if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) { - if (Platform.OS === 'android') { - if (Platform.Version < 29) { - if (!(await this.checkFilesystemPermission())) { - Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.')); - } - } - } - - // Save settings anyway, even if permission has not been granted - } - - // changedSettingKeys is cleared in shared.saveSettings so reading it now - const setIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors'); - - await shared.saveSettings(this); - - if (setIgnoreTlsErrors) { - await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors')); - } - }; - - this.saveButton_press = this.saveButton_press.bind(this); - - this.syncStatusButtonPress_ = () => { - void NavService.go('Status'); - }; - - this.manageProfilesButtonPress_ = () => { - this.props.dispatch({ - type: 'NAV_GO', - routeName: 'ProfileSwitcher', - }); - }; - - this.exportDebugButtonPress_ = async () => { - this.setState({ creatingReport: true }); - const service = new ReportService(); - - const logItems = await reg.logger().lastEntries(null); - const logItemRows = [['Date', 'Level', 'Message']]; - for (let i = 0; i < logItems.length; i++) { - const item = logItems[i]; - logItemRows.push([time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'), item.level, item.message]); - } - const logItemCsv = service.csvCreate(logItemRows); - - const itemListCsv = await service.basicItemList({ format: 'csv' }); - - const externalDir = await shim.fsDriver().getExternalDirectoryPath(); - - if (!externalDir) { - this.setState({ creatingReport: false }); - return; - } - - const filePath = `${externalDir}/syncReport-${new Date().getTime()}.txt`; - - const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n'); - await shim.fsDriver().writeFile(filePath, finalText, 'utf8'); - alert(`Debug report exported to ${filePath}`); - this.setState({ creatingReport: false }); - }; - - this.fixSearchEngineIndexButtonPress_ = async () => { - this.setState({ fixingSearchIndex: true }); - await SearchEngine.instance().rebuildIndex(); - this.setState({ fixingSearchIndex: false }); - }; - - this.exportProfileButtonPress_ = async () => { - const externalDir = await shim.fsDriver().getExternalDirectoryPath(); - if (!externalDir) { - return; - } - const p = this.state.profileExportPath ? this.state.profileExportPath : `${externalDir}/JoplinProfileExport`; - - this.setState({ - profileExportStatus: 'prompt', - profileExportPath: p, - }); - }; - - this.exportProfileButtonPress2_ = async () => { - this.setState({ profileExportStatus: 'exporting' }); - - const dbPath = '/data/data/net.cozic.joplin/databases'; - const exportPath = this.state.profileExportPath; - const resourcePath = `${exportPath}/resources`; - try { - const response = await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE); - if (response !== PermissionsAndroid.RESULTS.GRANTED) { - throw new Error('Permission denied'); - } - - const copyFiles = async (source: string, dest: string) => { - await shim.fsDriver().mkdir(dest); - - const files = await shim.fsDriver().readDirStats(source); - - for (const file of files) { - const source_ = `${source}/${file.path}`; - const dest_ = `${dest}/${file.path}`; - if (!file.isDirectory()) { - reg.logger().info(`Copying profile: ${source_} => ${dest_}`); - await shim.fsDriver().copy(source_, dest_); - } else { - await copyFiles(source_, dest_); - } - } - }; - await copyFiles(dbPath, exportPath); - await copyFiles(Setting.value('resourceDir'), resourcePath); - - alert('Profile has been exported!'); - } catch (error) { - alert(`Could not export files: ${error.message}`); - } finally { - this.setState({ profileExportStatus: 'idle' }); - } - }; - - this.logButtonPress_ = () => { - void NavService.go('Log'); - }; - - this.handleSetting = this.handleSetting.bind(this); + shared.init(reg); } + private checkSyncConfig_ = async () => { + // to ignore TLS erros we need to chage the global state of the app, if the check fails we need to restore the original state + // this call sets the new value and returns the previous one which we can use later to revert the change + const prevIgnoreTlsErrors = await setIgnoreTlsErrors(this.state.settings['net.ignoreTlsErrors']); + const result = await shared.checkSyncConfig(this, this.state.settings); + if (!result || !result.ok) { + await setIgnoreTlsErrors(prevIgnoreTlsErrors); + } + }; + + private e2eeConfig_ = () => { + void NavService.go('EncryptionConfig'); + }; + + private saveButton_press = async () => { + if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) { + if (Platform.OS === 'android') { + if (Platform.Version < 29) { + if (!(await this.checkFilesystemPermission())) { + Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.')); + } + } + } + + // Save settings anyway, even if permission has not been granted + } + + // changedSettingKeys is cleared in shared.saveSettings so reading it now + const shouldSetIgnoreTlsErrors = this.state.changedSettingKeys.includes('net.ignoreTlsErrors'); + + await shared.saveSettings(this); + + if (shouldSetIgnoreTlsErrors) { + await setIgnoreTlsErrors(Setting.value('net.ignoreTlsErrors')); + } + }; + + private syncStatusButtonPress_ = () => { + void NavService.go('Status'); + }; + + private manageProfilesButtonPress_ = () => { + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'ProfileSwitcher', + }); + }; + + private fixSearchEngineIndexButtonPress_ = async () => { + this.setState({ fixingSearchIndex: true }); + await SearchEngine.instance().rebuildIndex(); + this.setState({ fixingSearchIndex: false }); + }; + + private logButtonPress_ = () => { + void NavService.go('Log'); + }; + + private updateSidebarWidth = () => { + const windowWidth = Dimensions.get('window').width; + + let sidebarNewWidth = windowWidth; + + const sidebarValidWidths = [280, 230]; + const maxFractionOfWindowSize = 1 / 3; + for (const width of sidebarValidWidths) { + if (width < windowWidth * maxFractionOfWindowSize) { + sidebarNewWidth = width; + break; + } + } + + this.setState({ sidebarWidth: sidebarNewWidth }); + }; + + private navigationFillsScreen() { + const windowWidth = Dimensions.get('window').width; + return this.state.sidebarWidth > windowWidth / 2; + } + + private switchSectionPress_ = (section: string) => { + const label = Setting.sectionNameToLabel(section); + AccessibilityInfo.announceForAccessibility(_('Opening section %s', label)); + this.setState({ selectedSectionName: section }); + }; + + private showSectionNavigation_ = () => { + this.setState({ selectedSectionName: null }); + }; + public async checkFilesystemPermission() { if (Platform.OS !== 'android') { // Not implemented yet @@ -225,7 +179,7 @@ class ConfigScreenComponent extends BaseScreenComponent { this.setState({ settings: this.props.settings }); } - public styles() { + public styles(): ConfigScreenStyles { const themeId = this.props.themeId; if (this.styles_[themeId]) return this.styles_[themeId]; @@ -258,6 +212,12 @@ class ConfigScreenComponent extends BaseScreenComponent { await BackButtonService.back(); }; + // Show navigation when pressing "back" (unless always visible). + if (this.state.selectedSectionName && this.navigationFillsScreen()) { + this.showSectionNavigation_(); + return true; + } + if (this.state.changedSettingKeys.length > 0) { const dialogTitle: string|null = null; Alert.alert( @@ -284,6 +244,7 @@ class ConfigScreenComponent extends BaseScreenComponent { public componentDidMount() { if (this.props.navigation.state.sectionName) { + this.setState({ selectedSectionName: this.props.navigation.state.sectionName }); setTimeout(() => { this.scrollViewRef_.current.scrollTo({ x: 0, @@ -294,24 +255,17 @@ class ConfigScreenComponent extends BaseScreenComponent { } BackButtonService.addHandler(this.handleBackButtonPress); + Dimensions.addEventListener('change', this.updateSidebarWidth); + this.updateSidebarWidth(); } public componentWillUnmount() { BackButtonService.removeHandler(this.handleBackButtonPress); } - public renderHeader(key: string, title: string) { - const theme = themeStyle(this.props.themeId); - return ( - this.onHeaderLayout(key, event)}> - {title} - - ); - } - private renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) { return ( - - {messages[0]} + {messages[0]} {messages.length >= 1 ? ( - {messages[1]} + {messages[1]} ) : null} @@ -360,8 +316,8 @@ class ConfigScreenComponent extends BaseScreenComponent { 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( - - {_('Email to note')} + + {_('Email to note')} {this.props.settings['sync.10.inboxEmail']} { @@ -376,11 +332,129 @@ class ConfigScreenComponent extends BaseScreenComponent { ); } + 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.') })); + } + + if (section.name === 'export') { + settingComps.push(); + settingComps.push(); + settingComps.push(); + } + + if (section.name === 'moreInfo') { + if (Platform.OS === 'android' && Platform.Version >= 23) { + // Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually + // set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again + + settingComps.push( + + + + {_('To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions')} + + + {_('- Storage: to allow attaching files to notes and to enable filesystem synchronisation.')} + + + {_('- Camera: to allow taking a picture and attaching it to a note.')} + + + {_('- Location: to allow attaching geo-location information to a note.')} + + + , + ); + } + + settingComps.push( + + { + void Linking.openURL('https://joplinapp.org/donate/'); + }} + > + + {_('Make a donation')} + + + , + ); + + 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)} + , + ); + + const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile); + if (featureFlagKeys.length) { + const headerKey = 'featureFlags'; + settingComps.push( this.onHeaderLayout(headerKey, event)} + />); + + settingComps.push({this.renderFeatureFlags(settings, featureFlagKeys)}); + } + } + if (!settingComps.length) return null; + if (!isSelected) return null; return ( this.onSectionLayout(key, event)}> - {this.renderHeader(section.name, Setting.sectionNameToLabel(section.name))} {settingComps} ); @@ -392,22 +466,18 @@ class ConfigScreenComponent extends BaseScreenComponent { return ( - - + + {label} - void updateSettingValue(key, value)} /> + void updateSettingValue(key, value)} /> {descriptionComp} ); } - private containerStyle(hasDescription: boolean): any { - return !hasDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder; - } - - private async handleSetting(key: string, value: any): Promise { + private handleSetting = async (key: string, value: any): Promise => { // When the user tries to enable biometrics unlock, we ask for the // fingerprint or Face ID, and if it's correct we save immediately. If // it's not, we don't turn on the setting. @@ -428,123 +498,24 @@ class ConfigScreenComponent extends BaseScreenComponent { } return false; - } + }; public settingToComponent(key: string, value: any) { - const themeId = this.props.themeId; - const theme = themeStyle(themeId); - const output: any = null; - const updateSettingValue = async (key: string, value: any) => { const handled = await this.handleSetting(key, value); if (!handled) shared.updateSettingValue(this, key, value); }; - const md = Setting.settingMetadata(key); - const settingDescription = md.description ? md.description() : ''; - - const descriptionComp = !settingDescription ? null : {settingDescription}; - const containerStyle = this.containerStyle(!!settingDescription); - - if (md.isEnum) { - value = value.toString(); - - const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []); - - return ( - - - - {md.label()} - - { - void updateSettingValue(key, itemValue); - }} - /> - - {descriptionComp} - - ); - } else if (md.type === Setting.TYPE_BOOL) { - return this.renderToggle(key, md.label(), value, updateSettingValue, descriptionComp); - // return ( - // - // - // - // {md.label()} - // - // updateSettingValue(key, value)} /> - // - // {descriptionComp} - // - // ); - } else if (md.type === Setting.TYPE_INT) { - const unitLabel = md.unitLabel ? md.unitLabel(value) : value; - const minimum = 'minimum' in md ? md.minimum : 0; - const maximum = 'maximum' in md ? md.maximum : 10; - - // Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props - // on the Slider as they are buggy and can crash the app on certain devices. - // https://github.com/laurent22/joplin/issues/2733 - // https://github.com/react-native-community/react-native-slider/issues/161 - return ( - - - {md.label()} - - - {unitLabel} - void updateSettingValue(key, value)} /> - - - ); - } else if (md.type === Setting.TYPE_STRING) { - if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) { - return ( - - - - {md.label()} - - - {this.state.fileSystemSyncPath} - - - - ); - } - return ( - - - - {md.label()} - - void updateSettingValue(key, value)} secureTextEntry={!!md.secure} /> - - {descriptionComp} - - ); - } else { - // throw new Error('Unsupported setting type: ' + md.type); - } - - return output; + return ( + + ); } private renderFeatureFlags(settings: any, featureFlagKeys: string[]): any[] { @@ -562,139 +533,77 @@ class ConfigScreenComponent extends BaseScreenComponent { public render() { const settings = this.state.settings; - const theme = themeStyle(this.props.themeId); + const showAsSidebar = !this.navigationFillsScreen(); - const settingComps = shared.settingsToComponents2(this, 'mobile', settings); - - settingComps.push(this.renderHeader('tools', _('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.') })); - - settingComps.push(this.renderHeader('export', _('Export'))); - settingComps.push(); - - if (shim.mobilePlatform() === 'android') { - settingComps.push(this.renderButton('export_report_button', this.state.creatingReport ? _('Creating report...') : _('Export Debug Report'), this.exportDebugButtonPress_, { disabled: this.state.creatingReport })); - settingComps.push(this.renderButton('export_data', this.state.profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile'), this.exportProfileButtonPress_, { disabled: this.state.profileExportStatus === 'exporting', description: _('For debugging purpose only: export your profile to an external SD card.') })); - - if (this.state.profileExportStatus === 'prompt') { - const profileExportPrompt = ( - - Path: - this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard" keyboardAppearance={theme.keyboardAppearance} /> -