2023-11-09 21:19:08 +02:00
import * as React from 'react' ;
import { Platform , Linking , View , Switch , ScrollView , Text , TouchableOpacity , Alert , PermissionsAndroid , Dimensions , AccessibilityInfo } from 'react-native' ;
2024-01-18 13:26:51 +02:00
import Setting , { AppType , SettingItem , SettingMetadataSection } from '@joplin/lib/models/Setting' ;
2021-06-19 18:32:36 +02:00
import NavService from '@joplin/lib/services/NavService' ;
2024-01-05 16:15:47 +02:00
import SearchEngine from '@joplin/lib/services/search/SearchEngine' ;
2023-07-18 15:58:06 +02:00
import checkPermissions from '../../../utils/checkPermissions' ;
import setIgnoreTlsErrors from '../../../utils/TlsUtils' ;
2021-06-19 18:32:36 +02:00
import { reg } from '@joplin/lib/registry' ;
import { State } from '@joplin/lib/reducer' ;
2023-07-18 15:58:06 +02:00
const { BackButtonService } = require ( '../../../services/back-button.js' ) ;
2021-06-21 09:55:02 +02:00
const VersionInfo = require ( 'react-native-version-info' ) . default ;
2023-11-09 21:19:08 +02:00
import { connect } from 'react-redux' ;
2023-07-18 15:58:06 +02:00
import ScreenHeader from '../../ScreenHeader' ;
2023-11-09 21:19:08 +02:00
import { _ } from '@joplin/lib/locale' ;
import BaseScreenComponent from '../../base-screen' ;
2024-03-09 13:15:13 +02:00
import { themeStyle } from '../../global-style' ;
2023-11-09 21:19:08 +02:00
import * as shared from '@joplin/lib/components/shared/config/config-shared' ;
2021-08-16 16:20:14 +02:00
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry' ;
2023-07-18 15:58:06 +02:00
import biometricAuthenticate from '../../biometrics/biometricAuthenticate' ;
2023-11-09 21:19:08 +02:00
import configScreenStyles , { ConfigScreenStyles } from './configScreenStyles' ;
2023-11-16 14:17:03 +02:00
import NoteExportButton , { exportButtonDescription , exportButtonTitle } from './NoteExportSection/NoteExportButton' ;
2023-11-09 21:19:08 +02:00
import SettingsButton from './SettingsButton' ;
2023-07-18 21:15:45 +02:00
import Clipboard from '@react-native-community/clipboard' ;
2023-11-16 14:17:03 +02:00
import { ReactElement , ReactNode } from 'react' ;
2023-11-09 21:19:08 +02:00
import { Dispatch } from 'redux' ;
import SectionHeader from './SectionHeader' ;
2023-11-16 14:17:03 +02:00
import ExportProfileButton , { exportProfileButtonTitle } from './NoteExportSection/ExportProfileButton' ;
2023-11-09 21:19:08 +02:00
import SettingComponent from './SettingComponent' ;
2023-11-16 14:17:03 +02:00
import ExportDebugReportButton , { exportDebugReportTitle } from './NoteExportSection/ExportDebugReportButton' ;
2023-11-09 21:19:08 +02:00
import SectionSelector from './SectionSelector' ;
2024-01-18 13:26:51 +02:00
import { Button , TextInput } from 'react-native-paper' ;
2023-11-09 21:19:08 +02:00
interface ConfigScreenState {
settings : any ;
changedSettingKeys : string [ ] ;
2023-11-16 14:17:03 +02:00
searchQuery : string ;
searching : boolean ;
2023-11-09 21:19:08 +02:00
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 ;
}
2017-07-23 20:26:50 +02:00
2023-11-09 21:19:08 +02:00
class ConfigScreenComponent extends BaseScreenComponent < ConfigScreenProps , ConfigScreenState > {
2023-03-06 16:22:01 +02:00
public static navigationOptions ( ) : any {
2017-08-01 19:59:01 +02:00
return { header : null } ;
2017-07-30 23:04:26 +02:00
}
2017-07-23 20:26:50 +02:00
2021-08-16 17:18:32 +02:00
private componentsY_ : Record < string , number > = { } ;
2023-11-09 21:19:08 +02:00
private styles_ : Record < number , ConfigScreenStyles > = { } ;
private scrollViewRef_ : React.RefObject < ScrollView > ;
2021-08-16 17:18:32 +02:00
2023-11-09 21:19:08 +02:00
public constructor ( props : ConfigScreenProps ) {
super ( props ) ;
2018-01-25 21:01:14 +02:00
2019-06-26 01:13:13 +02:00
this . state = {
2023-11-09 21:19:08 +02:00
. . . shared . defaultScreenState ,
selectedSectionName : null ,
fixingSearchIndex : false ,
sidebarWidth : 100 ,
2023-11-16 14:17:03 +02:00
searchQuery : '' ,
searching : false ,
2019-06-26 01:13:13 +02:00
} ;
2023-11-09 21:19:08 +02:00
this . scrollViewRef_ = React . createRef < ScrollView > ( ) ;
2018-02-06 20:59:36 +02:00
2023-11-09 21:19:08 +02:00
shared . init ( reg ) ;
}
2022-07-10 16:26:24 +02:00
2023-11-09 21:19:08 +02:00
private checkSyncConfig_ = async ( ) = > {
2024-02-26 12:16:23 +02:00
// to ignore TLS errors we need to change the global state of the app, if the check fails we need to restore the original state
2023-11-09 21:19:08 +02:00
// 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 ) ;
}
} ;
2018-02-06 20:59:36 +02:00
2023-11-09 21:19:08 +02:00
private e2eeConfig_ = ( ) = > {
void NavService . go ( 'EncryptionConfig' ) ;
} ;
2019-06-26 01:13:13 +02:00
2023-11-09 21:19:08 +02:00
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.' ) ) ;
2022-07-10 16:26:24 +02:00
}
}
2021-04-25 10:50:52 +02:00
}
2023-05-29 14:14:09 +02:00
2023-11-09 21:19:08 +02:00
// Save settings anyway, even if permission has not been granted
}
2019-06-26 01:13:13 +02:00
2023-11-09 21:19:08 +02:00
// changedSettingKeys is cleared in shared.saveSettings so reading it now
const shouldSetIgnoreTlsErrors = this . state . changedSettingKeys . includes ( 'net.ignoreTlsErrors' ) ;
2023-01-10 14:08:13 +02:00
2024-01-27 20:20:51 +02:00
const done = await shared . saveSettings ( this ) ;
if ( ! done ) return ;
2019-06-26 01:13:13 +02:00
2023-11-09 21:19:08 +02:00
if ( shouldSetIgnoreTlsErrors ) {
await setIgnoreTlsErrors ( Setting . value ( 'net.ignoreTlsErrors' ) ) ;
}
} ;
2019-06-26 01:13:13 +02:00
2023-11-09 21:19:08 +02:00
private syncStatusButtonPress_ = ( ) = > {
void NavService . go ( 'Status' ) ;
} ;
2019-06-26 01:13:13 +02:00
2023-11-09 21:19:08 +02:00
private manageProfilesButtonPress_ = ( ) = > {
2023-11-15 15:31:36 +02:00
void NavService . go ( 'ProfileSwitcher' ) ;
2023-11-09 21:19:08 +02:00
} ;
2022-10-13 23:02:06 +02:00
2023-11-09 21:19:08 +02:00
private fixSearchEngineIndexButtonPress_ = async ( ) = > {
this . setState ( { fixingSearchIndex : true } ) ;
await SearchEngine . instance ( ) . rebuildIndex ( ) ;
this . setState ( { fixingSearchIndex : false } ) ;
} ;
2022-10-13 23:02:06 +02:00
2023-11-09 21:19:08 +02:00
private logButtonPress_ = ( ) = > {
void NavService . go ( 'Log' ) ;
} ;
2019-06-26 01:13:13 +02:00
2023-11-16 14:17:03 +02:00
private setShowSearch_ ( searching : boolean ) {
if ( searching !== this . state . searching ) {
this . setState ( { searching } ) ;
AccessibilityInfo . announceForAccessibility ( searching ? _ ( 'Search shown' ) : _ ( 'Search hidden' ) ) ;
}
}
private onSearchButtonPress_ = ( ) = > {
this . setShowSearch_ ( ! this . state . searching ) ;
} ;
private onSearchUpdate_ = ( newQuery : string ) = > {
this . setState ( { searchQuery : newQuery } ) ;
} ;
2023-11-09 21:19:08 +02:00
private updateSidebarWidth = ( ) = > {
const windowWidth = Dimensions . get ( 'window' ) . width ;
2019-06-26 01:13:13 +02:00
2023-11-09 21:19:08 +02:00
let sidebarNewWidth = windowWidth ;
2019-06-28 01:48:52 +02:00
2023-11-09 21:19:08 +02:00
const sidebarValidWidths = [ 280 , 230 ] ;
const maxFractionOfWindowSize = 1 / 3 ;
for ( const width of sidebarValidWidths ) {
if ( width < windowWidth * maxFractionOfWindowSize ) {
sidebarNewWidth = width ;
break ;
2022-10-13 23:02:06 +02:00
}
2023-11-09 21:19:08 +02:00
}
2019-12-28 19:47:37 +02:00
2023-11-09 21:19:08 +02:00
this . setState ( { sidebarWidth : sidebarNewWidth } ) ;
} ;
2019-12-28 19:47:37 +02:00
2023-11-09 21:19:08 +02:00
private navigationFillsScreen() {
const windowWidth = Dimensions . get ( 'window' ) . width ;
return this . state . sidebarWidth > windowWidth / 2 ;
}
2019-12-28 19:47:37 +02:00
2023-11-16 14:17:03 +02:00
private onJumpToSection_ = ( section : string ) = > {
2023-11-09 21:19:08 +02:00
const label = Setting . sectionNameToLabel ( section ) ;
AccessibilityInfo . announceForAccessibility ( _ ( 'Opening section %s' , label ) ) ;
2023-11-16 14:17:03 +02:00
this . setState ( {
selectedSectionName : section ,
searching : false ,
} ) ;
2023-11-09 21:19:08 +02:00
} ;
2023-05-29 14:14:09 +02:00
2023-11-09 21:19:08 +02:00
private showSectionNavigation_ = ( ) = > {
this . setState ( { selectedSectionName : null } ) ;
} ;
2018-01-25 21:01:14 +02:00
2023-03-06 16:22:01 +02:00
public async checkFilesystemPermission() {
2019-07-13 15:51:54 +02:00
if ( Platform . OS !== 'android' ) {
// Not implemented yet
return true ;
}
2020-06-08 10:01:11 +02:00
return await checkPermissions ( PermissionsAndroid . PERMISSIONS . WRITE_EXTERNAL_STORAGE , {
2019-07-30 09:35:42 +02:00
title : _ ( 'Information' ) ,
message : _ ( 'In order to use file system synchronisation your permission to write to external storage is required.' ) ,
buttonPositive : _ ( 'OK' ) ,
} ) ;
2019-07-13 15:51:54 +02:00
}
2023-03-06 16:22:01 +02:00
public UNSAFE_componentWillMount() {
2018-01-25 21:01:14 +02:00
this . setState ( { settings : this.props.settings } ) ;
2017-08-01 19:59:01 +02:00
}
2017-07-30 23:04:26 +02:00
2023-11-09 21:19:08 +02:00
public styles ( ) : ConfigScreenStyles {
2020-09-15 15:01:07 +02:00
const themeId = this . props . themeId ;
2017-08-01 19:59:01 +02:00
if ( this . styles_ [ themeId ] ) return this . styles_ [ themeId ] ;
this . styles_ = { } ;
2023-07-18 15:58:06 +02:00
this . styles_ [ themeId ] = configScreenStyles ( themeId ) ;
2017-08-01 19:59:01 +02:00
return this . styles_ [ themeId ] ;
2017-07-23 20:26:50 +02:00
}
2021-08-16 17:18:32 +02:00
private onHeaderLayout ( key : string , event : any ) {
const layout = event . nativeEvent . layout ;
this . componentsY_ [ ` header_ ${ key } ` ] = layout . y ;
}
private onSectionLayout ( key : string , event : any ) {
const layout = event . nativeEvent . layout ;
this . componentsY_ [ ` section_ ${ key } ` ] = layout . y ;
}
private componentY ( key : string ) : number {
if ( ( ` section_ ${ key } ` ) in this . componentsY_ ) return this . componentsY_ [ ` section_ ${ key } ` ] ;
if ( ( ` header_ ${ key } ` ) in this . componentsY_ ) return this . componentsY_ [ ` header_ ${ key } ` ] ;
console . error ( ` ConfigScreen: Could not find key to scroll to: ${ key } ` ) ;
return 0 ;
}
2023-11-15 15:31:36 +02:00
private hasUnsavedChanges() {
return this . state . changedSettingKeys . length > 0 ;
}
private promptSaveChanges ( ) : Promise < void > {
return new Promise ( resolve = > {
if ( this . hasUnsavedChanges ( ) ) {
const dialogTitle : string | null = null ;
Alert . alert (
dialogTitle ,
_ ( 'There are unsaved changes.' ) ,
[ {
text : _ ( 'Save changes' ) ,
onPress : async ( ) = > {
await this . saveButton_press ( ) ;
resolve ( ) ;
} ,
} ,
{
text : _ ( 'Discard changes' ) ,
onPress : ( ) = > resolve ( ) ,
} ] ,
) ;
} else {
resolve ( ) ;
}
} ) ;
}
2024-02-26 12:16:23 +02:00
private handleNavigateToNewScreen = async ( ) : Promise < boolean > = > {
2023-11-15 15:31:36 +02:00
await this . promptSaveChanges ( ) ;
// Continue navigation
return false ;
} ;
2023-01-11 20:45:00 +02:00
private handleBackButtonPress = ( ) : boolean = > {
const goBack = async ( ) = > {
BackButtonService . removeHandler ( this . handleBackButtonPress ) ;
await BackButtonService . back ( ) ;
} ;
2023-11-16 14:17:03 +02:00
// Cancel search on back
if ( this . state . searching ) {
this . setShowSearch_ ( false ) ;
return true ;
}
2023-11-09 21:19:08 +02:00
// Show navigation when pressing "back" (unless always visible).
if ( this . state . selectedSectionName && this . navigationFillsScreen ( ) ) {
this . showSectionNavigation_ ( ) ;
return true ;
}
2023-11-15 15:31:36 +02:00
if ( this . hasUnsavedChanges ( ) ) {
void ( async ( ) = > {
await this . promptSaveChanges ( ) ;
await goBack ( ) ;
} ) ( ) ;
2023-01-11 20:45:00 +02:00
return true ;
}
return false ;
} ;
2021-08-16 17:18:32 +02:00
public componentDidMount() {
if ( this . props . navigation . state . sectionName ) {
2023-11-09 21:19:08 +02:00
this . setState ( { selectedSectionName : this.props.navigation.state.sectionName } ) ;
2021-08-16 17:18:32 +02:00
setTimeout ( ( ) = > {
this . scrollViewRef_ . current . scrollTo ( {
x : 0 ,
y : this.componentY ( this . props . navigation . state . sectionName ) ,
animated : true ,
} ) ;
} , 200 ) ;
}
2023-01-11 20:45:00 +02:00
BackButtonService . addHandler ( this . handleBackButtonPress ) ;
2024-02-26 12:16:23 +02:00
NavService . addHandler ( this . handleNavigateToNewScreen ) ;
2023-11-09 21:19:08 +02:00
Dimensions . addEventListener ( 'change' , this . updateSidebarWidth ) ;
this . updateSidebarWidth ( ) ;
2023-01-11 20:45:00 +02:00
}
public componentWillUnmount() {
BackButtonService . removeHandler ( this . handleBackButtonPress ) ;
2024-02-26 12:16:23 +02:00
NavService . removeHandler ( this . handleNavigateToNewScreen ) ;
2021-08-16 17:18:32 +02:00
}
2023-07-18 15:58:06 +02:00
private renderButton ( key : string , title : string , clickHandler : ( ) = > void , options : any = null ) {
2019-06-26 01:13:13 +02:00
return (
2023-11-09 21:19:08 +02:00
< SettingsButton
2023-07-18 15:58:06 +02:00
key = { key }
title = { title }
clickHandler = { clickHandler }
description = { options ? . description }
statusComponent = { options ? . statusComp }
styles = { this . styles ( ) }
/ >
2019-06-26 01:13:13 +02:00
) ;
}
2023-11-16 14:17:03 +02:00
public sectionToComponent ( key : string , section : SettingMetadataSection , settings : any , isSelected : boolean ) {
const settingComps : ReactElement [ ] = [ ] ;
2024-01-18 13:26:51 +02:00
const advancedSettingComps : ReactElement [ ] = [ ] ;
2023-11-16 14:17:03 +02:00
const headerTitle = Setting . sectionNameToLabel ( section . name ) ;
const matchesSearchQuery = ( relatedText : string | string [ ] ) = > {
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 ;
} ;
2024-01-18 13:26:51 +02:00
const addSettingComponent = (
component : ReactElement ,
relatedText : string | string [ ] ,
settingMetadata? : SettingItem ,
) = > {
2023-11-16 14:17:03 +02:00
const hiddenBySearch = this . state . searching && ! matchesSearchQuery ( relatedText ) ;
if ( component && ! hiddenBySearch ) {
2024-01-18 13:26:51 +02:00
if ( settingMetadata ? . advanced ) {
advancedSettingComps . push ( component ) ;
} else {
settingComps . push ( component ) ;
}
2023-11-16 14:17:03 +02:00
}
} ;
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 ) ;
} ;
2019-06-08 01:23:17 +02:00
2023-11-09 21:19:08 +02:00
const styleSheet = this . styles ( ) . styleSheet ;
2023-11-16 14:17:03 +02:00
const addSettingLink = ( key : string , title : string , target : string ) = > {
const component = (
< View key = { key } style = { styleSheet . settingContainer } >
< TouchableOpacity
onPress = { ( ) = > {
void Linking . openURL ( target ) ;
} }
accessibilityRole = 'link'
>
< Text key = "label" style = { styleSheet . linkText } >
{ title }
< / Text >
< / TouchableOpacity >
< / View >
) ;
addSettingComponent ( component , title ) ;
} ;
const addSettingText = ( key : string , text : string ) = > {
addSettingComponent (
< View key = { key } style = { styleSheet . settingContainer } >
< Text style = { styleSheet . settingText } > { text } < / Text >
< / View > ,
text ,
) ;
} ;
2023-11-09 21:19:08 +02:00
2019-06-08 01:23:17 +02:00
for ( let i = 0 ; i < section . metadatas . length ; i ++ ) {
const md = section . metadatas [ i ] ;
if ( section . name === 'sync' && md . key === 'sync.resourceDownloadMode' ) {
const syncTargetMd = SyncTargetRegistry . idToMetadata ( settings [ 'sync.target' ] ) ;
if ( syncTargetMd . supportsConfigCheck ) {
const messages = shared . checkSyncConfigMessages ( this ) ;
const statusComp = ! messages . length ? null : (
2019-07-30 09:35:42 +02:00
< View style = { { flex : 1 , marginTop : 10 } } >
2023-11-09 21:19:08 +02:00
< Text style = { this . styles ( ) . styleSheet . descriptionText } > { messages [ 0 ] } < / Text >
2019-07-30 09:35:42 +02:00
{ messages . length >= 1 ? (
< View style = { { marginTop : 10 } } >
2023-11-09 21:19:08 +02:00
< Text style = { this . styles ( ) . styleSheet . descriptionText } > { messages [ 1 ] } < / Text >
2019-07-30 09:35:42 +02:00
< / View >
) : null }
< / View >
) ;
2019-06-08 01:23:17 +02:00
2023-11-16 14:17:03 +02:00
addSettingButton ( 'check_sync_config_button' , _ ( 'Check synchronisation configuration' ) , this . checkSyncConfig_ , { statusComp : statusComp } ) ;
2019-06-08 01:23:17 +02:00
}
}
const settingComp = this . settingToComponent ( md . key , settings [ md . key ] ) ;
2023-11-16 14:17:03 +02:00
const relatedText = [ md . label ? . ( ) ? ? '' , md . description ? . ( ) ? ? '' ] ;
addSettingComponent (
settingComp ,
relatedText ,
2024-01-18 13:26:51 +02:00
md ,
2023-11-16 14:17:03 +02:00
) ;
2019-06-08 01:23:17 +02:00
}
2019-06-26 01:13:13 +02:00
if ( section . name === 'sync' ) {
2023-11-16 14:17:03 +02:00
addSettingButton ( 'e2ee_config_button' , _ ( 'Encryption Config' ) , this . e2eeConfig_ ) ;
2019-06-26 01:13:13 +02:00
}
2023-07-18 21:15:45 +02:00
if ( section . name === 'joplinCloud' ) {
2023-11-16 14:17:03 +02:00
const label = _ ( 'Email to note' ) ;
2023-07-18 21:15:45 +02:00
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' ) ;
2023-11-16 14:17:03 +02:00
addSettingComponent (
2023-07-18 21:15:45 +02:00
< View key = "joplinCloud" >
2023-11-09 21:19:08 +02:00
< View style = { this . styles ( ) . styleSheet . settingContainerNoBottomBorder } >
2023-11-16 14:17:03 +02:00
< Text style = { this . styles ( ) . styleSheet . settingText } > { label } < / Text >
2023-12-13 21:47:26 +02:00
< Text style = { this . styles ( ) . styleSheet . settingTextEmphasis } > { this . props . settings [ 'sync.10.inboxEmail' ] } < / Text >
2023-07-18 21:15:45 +02:00
< / View >
{
this . renderButton (
2023-07-23 16:57:55 +02:00
'sync.10.inboxEmail' ,
2023-07-18 21:15:45 +02:00
_ ( 'Copy to clipboard' ) ,
2023-07-23 16:57:55 +02:00
( ) = > Clipboard . setString ( this . props . settings [ 'sync.10.inboxEmail' ] ) ,
2023-08-22 12:58:53 +02:00
{ description } ,
2023-07-18 21:15:45 +02:00
)
}
2023-08-22 12:58:53 +02:00
< / View > ,
2023-11-16 14:17:03 +02:00
[ label , description ] ,
2023-07-18 21:15:45 +02:00
) ;
}
2023-11-09 21:19:08 +02:00
if ( section . name === 'tools' ) {
2023-11-16 14:17:03 +02:00
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.' ) } ) ;
2023-11-09 21:19:08 +02:00
}
if ( section . name === 'export' ) {
2023-11-16 14:17:03 +02:00
addSettingComponent (
< NoteExportButton key = 'export_as_jex_button' styles = { this . styles ( ) } / > ,
[ exportButtonTitle ( ) , exportButtonDescription ( ) ] ,
) ;
addSettingComponent (
< ExportDebugReportButton key = 'export_report_button' styles = { this . styles ( ) } / > ,
exportDebugReportTitle ( ) ,
) ;
addSettingComponent (
< ExportProfileButton key = 'export_data' styles = { this . styles ( ) } / > ,
exportProfileButtonTitle ( ) ,
) ;
2023-11-09 21:19:08 +02:00
}
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
2023-11-16 14:17:03 +02:00
addSettingComponent (
2023-11-09 21:19:08 +02:00
< View key = "permission_info" style = { styleSheet . settingContainer } >
< View key = "permission_info_wrapper" >
< Text key = "perm1a" style = { styleSheet . settingText } >
{ _ ( 'To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions' ) }
< / Text >
< Text key = "perm2" style = { styleSheet . permissionText } >
{ _ ( '- Storage: to allow attaching files to notes and to enable filesystem synchronisation.' ) }
< / Text >
< Text key = "perm3" style = { styleSheet . permissionText } >
{ _ ( '- Camera: to allow taking a picture and attaching it to a note.' ) }
< / Text >
< Text key = "perm4" style = { styleSheet . permissionText } >
{ _ ( '- Location: to allow attaching geo-location information to a note.' ) }
< / Text >
< / View >
< / View > ,
2023-11-16 14:17:03 +02:00
'' ,
2023-11-09 21:19:08 +02:00
) ;
}
2023-11-16 14:17:03 +02:00
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/' ) ;
2023-11-09 21:19:08 +02:00
2023-11-16 14:17:03 +02:00
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 ) ) ;
2023-11-09 21:19:08 +02:00
const featureFlagKeys = Setting . featureFlagKeys ( AppType . Mobile ) ;
if ( featureFlagKeys . length ) {
const headerKey = 'featureFlags' ;
2023-11-16 14:17:03 +02:00
const featureFlagsTitle = _ ( 'Feature flags' ) ;
addSettingComponent (
< SectionHeader
key = { headerKey }
styles = { this . styles ( ) . styleSheet }
title = { featureFlagsTitle }
onLayout = { event = > this . onHeaderLayout ( headerKey , event ) }
/ > ,
_ ( 'Feature flags' ) ,
) ;
addSettingComponent (
< View key = "featureFlagsContainer" > { this . renderFeatureFlags ( settings , featureFlagKeys ) } < / View > ,
featureFlagsTitle ,
) ;
2023-11-09 21:19:08 +02:00
}
}
2024-01-18 13:26:51 +02:00
if ( ! settingComps . length && ! advancedSettingComps . length ) return null ;
2023-11-16 14:17:03 +02:00
if ( ! isSelected && ! this . state . searching ) return null ;
const headerComponent = (
< TouchableOpacity onPress = { ( ) = > {
this . onJumpToSection_ ( section . name ) ;
} } >
< SectionHeader
styles = { styleSheet }
title = { headerTitle }
/ >
< / TouchableOpacity >
) ;
2019-09-16 23:59:45 +02:00
2024-01-18 13:26:51 +02:00
const renderAdvancedSettings = ( ) = > {
if ( ! advancedSettingComps . length ) return null ;
const toggleAdvancedLabel = this . state . showAdvancedSettings ? _ ( 'Hide Advanced Settings' ) : _ ( 'Show Advanced Settings' ) ;
return (
< >
< Button
style = { { marginBottom : 20 } }
icon = { this . state . showAdvancedSettings ? 'menu-down' : 'menu-right' }
onPress = { ( ) = > this . setState ( { showAdvancedSettings : ! this . state . showAdvancedSettings } ) }
>
< Text > { toggleAdvancedLabel } < / Text >
< / Button >
{ this . state . showAdvancedSettings ? advancedSettingComps : null }
< / >
) ;
} ;
2019-06-08 01:23:17 +02:00
return (
2021-08-16 17:18:32 +02:00
< View key = { key } onLayout = { ( event : any ) = > this . onSectionLayout ( key , event ) } >
2023-11-16 14:17:03 +02:00
< View >
{ this . state . searching ? headerComponent : null }
{ settingComps }
2024-01-18 13:26:51 +02:00
{ renderAdvancedSettings ( ) }
2023-11-16 14:17:03 +02:00
< / View >
2019-06-08 01:23:17 +02:00
< / View >
) ;
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2021-06-19 18:32:36 +02:00
private renderToggle ( key : string , label : string , value : any , updateSettingValue : Function , descriptionComp : any = null ) {
const theme = themeStyle ( this . props . themeId ) ;
return (
< View key = { key } >
2023-11-09 21:19:08 +02:00
< View style = { this . styles ( ) . getContainerStyle ( false ) } >
< Text key = "label" style = { this . styles ( ) . styleSheet . switchSettingText } >
2021-06-19 18:32:36 +02:00
{ label }
< / Text >
2023-11-09 21:19:08 +02:00
< Switch key = "control" style = { this . styles ( ) . styleSheet . switchSettingControl } trackColor = { { false : theme . dividerColor } } value = { value } onValueChange = { ( value : any ) = > void updateSettingValue ( key , value ) } / >
2021-06-19 18:32:36 +02:00
< / View >
{ descriptionComp }
< / View >
) ;
}
2023-11-09 21:19:08 +02:00
private handleSetting = async ( key : string , value : any ) : Promise < boolean > = > {
2023-04-09 11:28:18 +02:00
// 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.
if ( key === 'security.biometricsEnabled' && ! ! value ) {
try {
await biometricAuthenticate ( ) ;
2023-05-29 14:14:09 +02:00
shared . updateSettingValue ( this , key , value , async ( ) = > await this . saveButton_press ( ) ) ;
2023-04-09 11:28:18 +02:00
} catch ( error ) {
shared . updateSettingValue ( this , key , false ) ;
Alert . alert ( error . message ) ;
}
return true ;
}
if ( key === 'security.biometricsEnabled' && ! value ) {
2023-05-29 14:14:09 +02:00
shared . updateSettingValue ( this , key , value , async ( ) = > await this . saveButton_press ( ) ) ;
2023-04-09 11:28:18 +02:00
return true ;
}
return false ;
2023-11-09 21:19:08 +02:00
} ;
2023-04-09 11:28:18 +02:00
2023-03-06 16:22:01 +02:00
public settingToComponent ( key : string , value : any ) {
2023-04-09 11:28:18 +02:00
const updateSettingValue = async ( key : string , value : any ) = > {
const handled = await this . handleSetting ( key , value ) ;
if ( ! handled ) shared . updateSettingValue ( this , key , value ) ;
2019-07-30 09:35:42 +02:00
} ;
2017-07-23 20:26:50 +02:00
2023-11-09 21:19:08 +02:00
return (
< SettingComponent
key = { key }
settingId = { key }
value = { value }
themeId = { this . props . themeId }
updateSettingValue = { updateSettingValue }
styles = { this . styles ( ) }
/ >
) ;
2017-07-23 20:26:50 +02:00
}
2021-06-26 11:19:48 +02:00
private renderFeatureFlags ( settings : any , featureFlagKeys : string [ ] ) : any [ ] {
2021-06-19 18:32:36 +02:00
const updateSettingValue = ( key : string , value : any ) = > {
return shared . updateSettingValue ( this , key , value ) ;
} ;
const output : any [ ] = [ ] ;
2021-06-26 11:19:48 +02:00
for ( const key of featureFlagKeys ) {
2021-06-19 18:32:36 +02:00
output . push ( this . renderToggle ( key , key , settings [ key ] , updateSettingValue ) ) ;
}
return output ;
}
2023-03-06 16:22:01 +02:00
public render() {
2018-01-25 21:01:14 +02:00
const settings = this . state . settings ;
2017-07-23 20:26:50 +02:00
2023-11-09 21:19:08 +02:00
const showAsSidebar = ! this . navigationFillsScreen ( ) ;
2019-06-26 01:13:13 +02:00
2023-11-09 21:19:08 +02:00
// If the navigation is a sidebar, always show a section.
let currentSectionName = this . state . selectedSectionName ;
if ( showAsSidebar && ! currentSectionName ) {
currentSectionName = 'general' ;
2019-12-28 19:47:37 +02:00
}
2023-11-16 14:17:03 +02:00
if ( this . state . searching ) {
currentSectionName = null ;
}
2023-11-09 21:19:08 +02:00
const sectionSelector = (
< SectionSelector
selectedSectionName = { currentSectionName }
styles = { this . styles ( ) }
settings = { settings }
2023-11-16 14:17:03 +02:00
openSection = { this . onJumpToSection_ }
2023-11-09 21:19:08 +02:00
width = { this . state . sidebarWidth }
/ >
) ;
2021-06-19 18:32:36 +02:00
2023-11-09 21:19:08 +02:00
let currentSection : ReactNode ;
2023-11-16 14:17:03 +02:00
if ( currentSectionName || this . state . searching ) {
2023-11-09 21:19:08 +02:00
const settingComps = shared . settingsToComponents2 (
this , AppType . Mobile , settings , currentSectionName ,
2018-03-09 11:09:13 +02:00
2023-11-09 21:19:08 +02:00
// TODO: Remove this cast. Currently necessary because of different versions
// of React in lib/ and app-mobile/
) as ReactNode [ ] ;
2019-06-08 01:23:17 +02:00
2023-11-16 14:17:03 +02:00
const searchInput = < TextInput
value = { this . state . searchQuery }
label = { _ ( 'Search' ) }
placeholder = { _ ( 'Search...' ) }
onChangeText = { this . onSearchUpdate_ }
autoFocus = { true }
/ > ;
2023-11-09 21:19:08 +02:00
currentSection = (
< ScrollView
ref = { this . scrollViewRef_ }
style = { { flexGrow : 1 } }
>
2023-11-16 14:17:03 +02:00
{ this . state . searching ? searchInput : null }
2023-11-09 21:19:08 +02:00
{ settingComps }
< / ScrollView >
2018-05-21 18:24:09 +02:00
) ;
2023-11-09 21:19:08 +02:00
} else {
currentSection = sectionSelector ;
2018-05-21 18:24:09 +02:00
}
2023-11-09 21:19:08 +02:00
let mainComponent ;
if ( showAsSidebar && currentSectionName ) {
mainComponent = (
< View style = { {
flex : 1 ,
flexDirection : 'row' ,
} } >
{ sectionSelector }
< View style = { { width : 10 } } / >
{ currentSection }
< / View >
) ;
} else {
mainComponent = currentSection ;
}
2018-11-02 02:43:42 +02:00
2023-11-09 21:19:08 +02:00
let screenHeadingText = _ ( 'Configuration' ) ;
if ( currentSectionName ) {
screenHeadingText = Setting . sectionNameToLabel ( currentSectionName ) ;
}
2023-05-08 18:50:19 +02:00
2017-07-23 20:26:50 +02:00
return (
2020-09-15 15:01:07 +02:00
< View style = { this . rootStyle ( this . props . themeId ) . root } >
2023-11-09 21:19:08 +02:00
< ScreenHeader
title = { screenHeadingText }
showSaveButton = { true }
2023-11-16 14:17:03 +02:00
showSearchButton = { true }
2023-11-09 21:19:08 +02:00
showSideMenuButton = { false }
2023-11-15 15:31:36 +02:00
saveButtonDisabled = { ! this . hasUnsavedChanges ( ) }
2023-11-09 21:19:08 +02:00
onSaveButtonPress = { this . saveButton_press }
2023-11-16 14:17:03 +02:00
onSearchButtonPress = { this . onSearchButtonPress_ }
2023-11-09 21:19:08 +02:00
/ >
{ mainComponent }
2017-07-23 20:26:50 +02:00
< / View >
) ;
}
}
2021-06-19 18:32:36 +02:00
const ConfigScreen = connect ( ( state : State ) = > {
2019-07-30 09:35:42 +02:00
return {
settings : state.settings ,
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2019-07-30 09:35:42 +02:00
} ;
} ) ( ConfigScreenComponent ) ;
2017-07-23 20:26:50 +02:00
2021-06-19 18:32:36 +02:00
export default ConfigScreen ;