mirror of
https://github.com/standardnotes/mobile.git
synced 2026-04-19 21:58:51 -04:00
feat: Android Face Biometric Auth support
This commit is contained in:
10
src/App.js
10
src/App.js
@@ -41,7 +41,7 @@ import StyleKit from '@Style/StyleKit';
|
||||
if (__DEV__ === false) {
|
||||
const bugsnag = new Client();
|
||||
|
||||
// Disable console.log for non-dev builds
|
||||
/** Disable console.log for non-dev builds */
|
||||
console.log = () => {};
|
||||
}
|
||||
|
||||
@@ -80,9 +80,9 @@ const AppDrawerStack = createDrawerNavigator(
|
||||
openRightDrawer: () => DrawerActions.openDrawer({ key: stateKey }),
|
||||
closeRightDrawer: () => DrawerActions.closeDrawer({ key: stateKey }),
|
||||
lockRightDrawer: lock => {
|
||||
// this is the key part
|
||||
/** This is the key part */
|
||||
SideMenuManager.get().setLockedForRightSideMenu(lock);
|
||||
// We have to return something
|
||||
/** We have to return something. */
|
||||
return NavigationActions.setParams({
|
||||
params: { dummy: true },
|
||||
key: route.key,
|
||||
@@ -159,9 +159,9 @@ const DrawerStack = createDrawerNavigator(
|
||||
openLeftDrawer: () => DrawerActions.openDrawer({ key: stateKey }),
|
||||
closeLeftDrawer: () => DrawerActions.closeDrawer({ key: stateKey }),
|
||||
lockLeftDrawer: lock => {
|
||||
// this is the key part
|
||||
/** This is the key part. */
|
||||
SideMenuManager.get().setLockedForLeftSideMenu(lock);
|
||||
// We have to return something
|
||||
/** We have to return something. */
|
||||
return NavigationActions.setParams({
|
||||
params: { dummy: true },
|
||||
key: route.key,
|
||||
|
||||
@@ -17,9 +17,9 @@ export function isMatchCaseInsensitive(a, b) {
|
||||
* Returns a Date object from a JSON stringifed date
|
||||
*/
|
||||
export function dateFromJsonString(str) {
|
||||
if(str) {
|
||||
if (str) {
|
||||
return new Date(JSON.parse(str));
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ export default class Authenticate extends Abstract {
|
||||
}
|
||||
|
||||
submitPressed() {
|
||||
if (this.activeSource && this.activeSource.isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we just pressed submit on the only pending source left, disable
|
||||
* submit button
|
||||
@@ -187,7 +191,7 @@ export default class Authenticate extends Abstract {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable submit while we're processing. Will be re-enabled below.
|
||||
/** Disable submit while we're processing. Will be re-enabled below. */
|
||||
this.setState({ submitDisabled: true });
|
||||
|
||||
const result = await source.authenticate();
|
||||
@@ -197,6 +201,7 @@ export default class Authenticate extends Abstract {
|
||||
this.forceUpdate();
|
||||
} else if (source.isLocked()) {
|
||||
this.onSourceLocked(source);
|
||||
return;
|
||||
} else {
|
||||
if (result.error && result.error.message) {
|
||||
Alert.alert('Unsuccessful', result.error.message);
|
||||
@@ -229,14 +234,14 @@ export default class Authenticate extends Abstract {
|
||||
/**
|
||||
* @private
|
||||
* When a source returns in a locked status we create a timeout for the lock
|
||||
* period.
|
||||
* period. This will auto reprompt the user for auth after the period is up.
|
||||
*/
|
||||
onSourceLocked(source) {
|
||||
this.setState({ sourceLocked: true, submitDisabled: true });
|
||||
|
||||
setTimeout(() => {
|
||||
source.setWaitingForInput();
|
||||
this.setState({ sourceLocked: false, submitDisabled: false });
|
||||
this.beginAuthenticationForSource(source);
|
||||
this.forceUpdate();
|
||||
}, source.lockTimeout);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export default class AuthenticationSourceBiometric extends AuthenticationSource
|
||||
super.initializeForInterface();
|
||||
|
||||
KeysManager.getDeviceBiometricsAvailability((available, type, noun) => {
|
||||
this.isReady = true;
|
||||
this.isAvailable = available;
|
||||
this.biometricsType = type;
|
||||
this.biometricsNoun = noun;
|
||||
@@ -32,7 +33,7 @@ export default class AuthenticationSourceBiometric extends AuthenticationSource
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.biometricsType;
|
||||
return this.biometricsType || 'Biometrics';
|
||||
}
|
||||
|
||||
get isFingerprint() {
|
||||
@@ -78,7 +79,7 @@ export default class AuthenticationSourceBiometric extends AuthenticationSource
|
||||
}
|
||||
|
||||
async authenticate() {
|
||||
if (!this.isAvailable) {
|
||||
if (this.isReady && !this.isAvailable) {
|
||||
this.didFail();
|
||||
return {
|
||||
success: false,
|
||||
@@ -92,6 +93,7 @@ export default class AuthenticationSourceBiometric extends AuthenticationSource
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return FingerprintScanner.authenticate({
|
||||
deviceCredentialAllowed: true,
|
||||
description: 'Biometrics are required to access your notes.',
|
||||
})
|
||||
.then(() => {
|
||||
@@ -102,6 +104,7 @@ export default class AuthenticationSourceBiometric extends AuthenticationSource
|
||||
|
||||
if (error.name === 'DeviceLocked') {
|
||||
this.setLocked();
|
||||
FingerprintScanner.release();
|
||||
return this._fail(
|
||||
'Authentication failed. Wait 30 seconds to try again.'
|
||||
);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
Alert
|
||||
} from 'react-native';
|
||||
import React from 'react';
|
||||
import { ScrollView, Alert } from 'react-native';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import SectionHeader from '@Components/SectionHeader';
|
||||
import SectionedAccessoryTableCell from '@Components/SectionedAccessoryTableCell';
|
||||
@@ -16,10 +13,7 @@ import SF from '@SFJS/sfjs';
|
||||
import Storage from '@SFJS/storageManager';
|
||||
import Sync from '@SFJS/syncManager';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import {
|
||||
SCREEN_INPUT_MODAL,
|
||||
SCREEN_MANAGE_PRIVILEGES
|
||||
} from '@Screens/screens';
|
||||
import { SCREEN_INPUT_MODAL, SCREEN_MANAGE_PRIVILEGES } from '@Screens/screens';
|
||||
import AuthSection from '@Screens/Settings/Sections/AuthSection';
|
||||
import CompanySection from '@Screens/Settings/Sections/CompanySection';
|
||||
import EncryptionSection from '@Screens/Settings/Sections/EncryptionSection';
|
||||
@@ -29,16 +23,21 @@ import { ICON_CHECKMARK } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class Settings extends Abstract {
|
||||
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
const templateOptions = {
|
||||
title: "Settings",
|
||||
title: 'Settings',
|
||||
leftButton: {
|
||||
title: ApplicationState.isIOS ? "Done" : null,
|
||||
iconName: ApplicationState.isIOS ? null : StyleKit.nameForIcon(ICON_CHECKMARK),
|
||||
}
|
||||
}
|
||||
return Abstract.getDefaultNavigationOptions({navigation, navigationOptions, templateOptions});
|
||||
title: ApplicationState.isIOS ? 'Done' : null,
|
||||
iconName: ApplicationState.isIOS
|
||||
? null
|
||||
: StyleKit.nameForIcon(ICON_CHECKMARK),
|
||||
},
|
||||
};
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
templateOptions,
|
||||
});
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -46,31 +45,33 @@ export default class Settings extends Abstract {
|
||||
|
||||
props.navigation.setParams({
|
||||
leftButton: {
|
||||
title: ApplicationState.isIOS ? "Done" : null,
|
||||
iconName: ApplicationState.isIOS ? null : StyleKit.nameForIcon(ICON_CHECKMARK),
|
||||
title: ApplicationState.isIOS ? 'Done' : null,
|
||||
iconName: ApplicationState.isIOS
|
||||
? null
|
||||
: StyleKit.nameForIcon(ICON_CHECKMARK),
|
||||
onPress: () => {
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if(__DEV__) {
|
||||
if (__DEV__) {
|
||||
props.navigation.setParams({
|
||||
rightButton: {
|
||||
title: "Destroy Data",
|
||||
title: 'Destroy Data',
|
||||
onPress: () => {
|
||||
Storage.get().clear();
|
||||
Auth.get().signout();
|
||||
KeysManager.get().clearOfflineKeysAndData(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.sortOptions = [
|
||||
{key: "created_at", label: "Date Added"},
|
||||
{key: "client_updated_at", label: "Date Modified"},
|
||||
{key: "title", label: "Title"},
|
||||
{ key: 'created_at', label: 'Date Added' },
|
||||
{ key: 'client_updated_at', label: 'Date Modified' },
|
||||
{ key: 'title', label: 'Title' },
|
||||
];
|
||||
|
||||
this.options = ApplicationState.getOptions();
|
||||
@@ -84,10 +85,14 @@ export default class Settings extends Abstract {
|
||||
}
|
||||
|
||||
loadSecurityStatus() {
|
||||
var hasPasscode = KeysManager.get().hasOfflinePasscode();
|
||||
var hasBiometrics = KeysManager.get().hasBiometrics();
|
||||
var encryptedStorage = KeysManager.get().isStorageEncryptionEnabled();
|
||||
this.mergeState({hasPasscode: hasPasscode, hasBiometrics: hasBiometrics, storageEncryption: encryptedStorage})
|
||||
const hasPasscode = KeysManager.get().hasOfflinePasscode();
|
||||
const hasBiometrics = KeysManager.get().hasBiometrics();
|
||||
const encryptedStorage = KeysManager.get().isStorageEncryptionEnabled();
|
||||
this.mergeState({
|
||||
hasPasscode: hasPasscode,
|
||||
hasBiometrics: hasBiometrics,
|
||||
storageEncryption: encryptedStorage,
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -102,215 +107,278 @@ export default class Settings extends Abstract {
|
||||
}
|
||||
|
||||
resaveOfflineData(callback, updateAfter = false) {
|
||||
Sync.get().resaveOfflineData().then(() => {
|
||||
if(updateAfter) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
callback && callback();
|
||||
});
|
||||
Sync.get()
|
||||
.resaveOfflineData()
|
||||
.then(() => {
|
||||
if (updateAfter) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
callback && callback();
|
||||
});
|
||||
}
|
||||
|
||||
onSignOutPress = () => {
|
||||
AlertManager.get().confirm({
|
||||
title: "Sign Out?",
|
||||
text: "Signing out will remove all data from this device, including notes and tags. Make sure your data is synced before proceeding.",
|
||||
confirmButtonText: "Sign Out",
|
||||
title: 'Sign Out?',
|
||||
text: 'Signing out will remove all data from this device, including notes and tags. Make sure your data is synced before proceeding.',
|
||||
confirmButtonText: 'Sign Out',
|
||||
onConfirm: () => {
|
||||
Auth.get().signout().then(() => {
|
||||
this.forceUpdate();
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Auth.get()
|
||||
.signout()
|
||||
.then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onStorageEncryptionEnable = () => {
|
||||
AlertManager.get().confirm({
|
||||
title: "Enable Storage Encryption?",
|
||||
text: "Storage encryption improves your security by encrypting your data on your device. It may increase app start-up time.",
|
||||
confirmButtonText: "Enable",
|
||||
title: 'Enable Storage Encryption?',
|
||||
text: 'Storage encryption improves your security by encrypting your data on your device. It may increase app start-up time.',
|
||||
confirmButtonText: 'Enable',
|
||||
onConfirm: () => {
|
||||
this.mergeState({storageEncryptionLoading: true});
|
||||
this.mergeState({ storageEncryptionLoading: true });
|
||||
KeysManager.get().enableStorageEncryption();
|
||||
this.resaveOfflineData(() => {
|
||||
this.mergeState({storageEncryption: true, storageEncryptionLoading: false});
|
||||
this.mergeState({
|
||||
storageEncryption: true,
|
||||
storageEncryptionLoading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onStorageEncryptionDisable = () => {
|
||||
AlertManager.get().confirm({
|
||||
title: "Disable Storage Encryption?",
|
||||
text: "Storage encryption improves your security by encrypting your data on your device. Disabling it can improve app start-up speed.",
|
||||
confirmButtonText: "Disable",
|
||||
title: 'Disable Storage Encryption?',
|
||||
text: 'Storage encryption improves your security by encrypting your data on your device. Disabling it can improve app start-up speed.',
|
||||
confirmButtonText: 'Disable',
|
||||
onConfirm: () => {
|
||||
this.mergeState({storageEncryptionLoading: true});
|
||||
this.mergeState({ storageEncryptionLoading: true });
|
||||
KeysManager.get().disableStorageEncryption();
|
||||
this.resaveOfflineData(() => {
|
||||
this.mergeState({storageEncryption: false, storageEncryptionLoading: false});
|
||||
this.mergeState({
|
||||
storageEncryption: false,
|
||||
storageEncryptionLoading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onPasscodeEnable = () => {
|
||||
this.props.navigation.navigate(SCREEN_INPUT_MODAL, {
|
||||
title: "Setup Passcode",
|
||||
placeholder: "Enter a passcode",
|
||||
confirmPlaceholder: "Confirm passcode",
|
||||
title: 'Setup Passcode',
|
||||
placeholder: 'Enter a passcode',
|
||||
confirmPlaceholder: 'Confirm passcode',
|
||||
secureTextEntry: true,
|
||||
requireConfirm: true,
|
||||
showKeyboardChooser: true,
|
||||
onSubmit: async (value, keyboardType) => {
|
||||
Storage.get().setItem("passcodeKeyboardType", keyboardType);
|
||||
Storage.get().setItem('passcodeKeyboardType', keyboardType);
|
||||
|
||||
let identifier = await SF.get().crypto.generateUUID();
|
||||
|
||||
SF.get().crypto.generateInitialKeysAndAuthParamsForUser(identifier, value).then((results) => {
|
||||
let keys = results.keys;
|
||||
let authParams = results.authParams;
|
||||
SF.get()
|
||||
.crypto.generateInitialKeysAndAuthParamsForUser(identifier, value)
|
||||
.then(results => {
|
||||
let keys = results.keys;
|
||||
let authParams = results.authParams;
|
||||
|
||||
// make sure it has valid items
|
||||
if(_.keys(keys).length > 0) {
|
||||
KeysManager.get().setOfflineAuthParams(authParams);
|
||||
KeysManager.get().persistOfflineKeys(keys);
|
||||
var encryptionSource = KeysManager.get().encryptionSource();
|
||||
if(encryptionSource == "offline") {
|
||||
this.resaveOfflineData(null, true);
|
||||
// make sure it has valid items
|
||||
if (_.keys(keys).length > 0) {
|
||||
KeysManager.get().setOfflineAuthParams(authParams);
|
||||
KeysManager.get().persistOfflineKeys(keys);
|
||||
const encryptionSource = KeysManager.get().encryptionSource();
|
||||
if (encryptionSource === 'offline') {
|
||||
this.resaveOfflineData(null, true);
|
||||
}
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Passcode Error',
|
||||
'There was an error setting up your passcode. Please try again.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.mergeState({setupButtonText: SAVE_BUTTON_DEFAULT_TEXT, setupButtonEnabled: true});
|
||||
Alert.alert("Passcode Error", "There was an error setting up your passcode. Please try again.");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onPasscodeDisable = () => {
|
||||
this.handlePrivilegedAction(true, SFPrivilegesManager.ActionManagePasscode, () => {
|
||||
var encryptionSource = KeysManager.get().encryptionSource();
|
||||
var message;
|
||||
if(encryptionSource == "account") {
|
||||
message = "Are you sure you want to disable your local passcode? This will not affect your encryption status, as your data is currently being encrypted through your sync account keys.";
|
||||
} else if(encryptionSource == "offline") {
|
||||
message = "Are you sure you want to disable your local passcode? This will disable encryption on your data.";
|
||||
}
|
||||
|
||||
AlertManager.get().confirm({
|
||||
title: "Disable Passcode",
|
||||
text: message,
|
||||
confirmButtonText: "Disable Passcode",
|
||||
onConfirm: async () => {
|
||||
var result = await KeysManager.get().clearOfflineKeysAndData();
|
||||
if(encryptionSource == "offline") {
|
||||
// remove encryption from all items
|
||||
this.resaveOfflineData(null, true);
|
||||
}
|
||||
|
||||
this.mergeState({hasPasscode: false});
|
||||
this.forceUpdate();
|
||||
this.handlePrivilegedAction(
|
||||
true,
|
||||
SFPrivilegesManager.ActionManagePasscode,
|
||||
() => {
|
||||
const encryptionSource = KeysManager.get().encryptionSource();
|
||||
let message;
|
||||
if (encryptionSource === 'account') {
|
||||
message = 'Are you sure you want to disable your local passcode? This will not affect your encryption status, as your data is currently being encrypted through your sync account keys.';
|
||||
} else if (encryptionSource === 'offline') {
|
||||
message = 'Are you sure you want to disable your local passcode? This will disable encryption on your data.';
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
AlertManager.get().confirm({
|
||||
title: 'Disable Passcode',
|
||||
text: message,
|
||||
confirmButtonText: 'Disable Passcode',
|
||||
onConfirm: async () => {
|
||||
await KeysManager.get().clearOfflineKeysAndData();
|
||||
|
||||
if (encryptionSource === 'offline') {
|
||||
// remove encryption from all items
|
||||
this.resaveOfflineData(null, true);
|
||||
}
|
||||
|
||||
this.mergeState({ hasPasscode: false });
|
||||
this.forceUpdate();
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onFingerprintEnable = () => {
|
||||
KeysManager.get().enableBiometrics();
|
||||
this.loadSecurityStatus();
|
||||
}
|
||||
};
|
||||
|
||||
onFingerprintDisable = () => {
|
||||
this.handlePrivilegedAction(true, SFPrivilegesManager.ActionManagePasscode, () => {
|
||||
KeysManager.get().disableBiometrics();
|
||||
this.loadSecurityStatus();
|
||||
});
|
||||
}
|
||||
this.handlePrivilegedAction(
|
||||
true,
|
||||
SFPrivilegesManager.ActionManagePasscode,
|
||||
() => {
|
||||
KeysManager.get().disableBiometrics();
|
||||
this.loadSecurityStatus();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onSortChange = (key) => {
|
||||
onSortChange = key => {
|
||||
this.options.setSortBy(key);
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onOptionSelect = (option) => {
|
||||
this.options.setDisplayOptionKeyValue(option, !this.options.getDisplayOptionValue(option));
|
||||
onOptionSelect = option => {
|
||||
this.options.setDisplayOptionKeyValue(
|
||||
option,
|
||||
!this.options.getDisplayOptionValue(option)
|
||||
);
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
toggleSortReverse = () => {
|
||||
this.options.setSortReverse(!this.options.sortReverse);
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
openManagePrivs = () => {
|
||||
this.handlePrivilegedAction(true, SFPrivilegesManager.ActionManagePrivileges, () => {
|
||||
this.props.navigation.navigate(SCREEN_MANAGE_PRIVILEGES)
|
||||
})
|
||||
}
|
||||
this.handlePrivilegedAction(
|
||||
true,
|
||||
SFPrivilegesManager.ActionManagePrivileges,
|
||||
() => {
|
||||
this.props.navigation.navigate(SCREEN_MANAGE_PRIVILEGES);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if(this.state.lockContent) {
|
||||
return (<LockedView />);
|
||||
if (this.state.lockContent) {
|
||||
return <LockedView />;
|
||||
}
|
||||
|
||||
let signedIn = !Auth.get().offline();
|
||||
const signedIn = !Auth.get().offline();
|
||||
|
||||
return (
|
||||
<SafeAreaView forceInset={{ top: 'never', bottom: 'never', left: 'always' }} style={[StyleKit.styles.container, StyleKit.styles.baseBackground]}>
|
||||
<ScrollView style={{backgroundColor: StyleKit.variable("stylekitBackgroundColor")}} keyboardShouldPersistTaps={'always'} keyboardDismissMode={'interactive'}>
|
||||
|
||||
{!signedIn && !this.state.confirmRegistration &&
|
||||
<SafeAreaView
|
||||
forceInset={{ top: 'never', bottom: 'never', left: 'always' }}
|
||||
style={[StyleKit.styles.container, StyleKit.styles.baseBackground]}
|
||||
>
|
||||
<ScrollView
|
||||
style={{
|
||||
backgroundColor: StyleKit.variables.stylekitBackgroundColor,
|
||||
}}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
keyboardDismissMode={'interactive'}
|
||||
>
|
||||
{!signedIn && !this.state.confirmRegistration && (
|
||||
<AuthSection
|
||||
title={"Account"}
|
||||
onAuthSuccess = {() => {this.dismiss()}}
|
||||
title={'Account'}
|
||||
onAuthSuccess={() => {
|
||||
this.dismiss();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
|
||||
<OptionsSection
|
||||
title={"Options"}
|
||||
title={'Options'}
|
||||
onSignOutPress={this.onSignOutPress}
|
||||
onManagePrivileges={this.openManagePrivs}
|
||||
/>
|
||||
|
||||
<TableSection>
|
||||
<SectionHeader title={"Sort Notes By"} buttonText={this.options.sortReverse ? "Disable Reverse Sort" : "Enable Reverse Sort"} buttonAction={this.toggleSortReverse} />
|
||||
<SectionHeader
|
||||
title={'Sort Notes By'}
|
||||
buttonText={
|
||||
this.options.sortReverse
|
||||
? 'Disable Reverse Sort'
|
||||
: 'Enable Reverse Sort'
|
||||
}
|
||||
buttonAction={this.toggleSortReverse}
|
||||
/>
|
||||
{this.sortOptions.map((option, i) => {
|
||||
return (
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={() => {this.onSortChange(option.key)}}
|
||||
onPress={() => {
|
||||
this.onSortChange(option.key);
|
||||
}}
|
||||
text={option.label}
|
||||
key={option.key}
|
||||
first={i == 0}
|
||||
last={i == this.sortOptions.length - 1}
|
||||
selected={() => {return option.key == this.options.sortBy}}
|
||||
selected={() => {
|
||||
return option.key === this.options.sortBy;
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</TableSection>
|
||||
|
||||
<TableSection>
|
||||
<SectionHeader title={"Note List Options"} />
|
||||
<SectionHeader title={'Note List Options'} />
|
||||
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={() => {this.onOptionSelect('hidePreviews')}}
|
||||
text={"Hide note previews"}
|
||||
onPress={() => {
|
||||
this.onOptionSelect('hidePreviews');
|
||||
}}
|
||||
text={'Hide note previews'}
|
||||
first={true}
|
||||
selected={() => {return this.options.hidePreviews}}
|
||||
selected={() => {
|
||||
return this.options.hidePreviews;
|
||||
}}
|
||||
/>
|
||||
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={() => {this.onOptionSelect('hideTags')}}
|
||||
text={"Hide note tags"}
|
||||
selected={() => {return this.options.hideTags}}
|
||||
onPress={() => {
|
||||
this.onOptionSelect('hideTags');
|
||||
}}
|
||||
text={'Hide note tags'}
|
||||
selected={() => {
|
||||
return this.options.hideTags;
|
||||
}}
|
||||
/>
|
||||
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={() => {this.onOptionSelect('hideDates')}}
|
||||
text={"Hide note dates"}
|
||||
onPress={() => {
|
||||
this.onOptionSelect('hideDates');
|
||||
}}
|
||||
text={'Hide note dates'}
|
||||
last={true}
|
||||
selected={() => {return this.options.hideDates}}
|
||||
selected={() => {
|
||||
return this.options.hideDates;
|
||||
}}
|
||||
/>
|
||||
|
||||
</TableSection>
|
||||
|
||||
<PasscodeSection
|
||||
@@ -324,13 +392,12 @@ export default class Settings extends Abstract {
|
||||
onDisable={this.onPasscodeDisable}
|
||||
onFingerprintEnable={this.onFingerprintEnable}
|
||||
onFingerprintDisable={this.onFingerprintDisable}
|
||||
title={"Security"}
|
||||
title={'Security'}
|
||||
/>
|
||||
|
||||
<EncryptionSection title={"Encryption Status"}/>
|
||||
|
||||
<CompanySection title={"Standard Notes"}/>
|
||||
<EncryptionSection title={'Encryption Status'} />
|
||||
|
||||
<CompanySection title={'Standard Notes'} />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { ScrollView, View, Text, FlatList } from 'react-native';
|
||||
|
||||
import ApplicationState from "@Lib/ApplicationState"
|
||||
|
||||
/*
|
||||
Because SideMenus (SideMenu and NoteSideMenu) are rendering by React Navigation as drawer components
|
||||
on app startup, we can't give them params at will. We need a way for components like the Compose
|
||||
screen to talk to the NoteSideMenu and give it the current note context. The only way seems to be
|
||||
some shared singleton object, which is this.
|
||||
|
||||
This object will handle state for both side menus.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Because SideMenus (SideMenu and NoteSideMenu) are rendering by React
|
||||
* Navigation as drawer components on app startup, we can't give them params at
|
||||
* will. We need a way for components like the Compose screen to talk to the
|
||||
* NoteSideMenu and give it the current note context. The only way seems to be
|
||||
* some shared singleton object, which is this.
|
||||
*
|
||||
* This object will handle state for both side menus.
|
||||
*/
|
||||
export default class SideMenuManager {
|
||||
|
||||
static instance = null;
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
@@ -24,35 +18,41 @@ export default class SideMenuManager {
|
||||
}
|
||||
|
||||
setLeftSideMenuReference(ref) {
|
||||
// The ref handler of the main component sometimes passes null, then passes the correct reference
|
||||
if(!this.leftSideMenu) {
|
||||
/**
|
||||
* The ref handler of the main component sometimes passes null, then passes
|
||||
* the correct reference
|
||||
*/
|
||||
if (!this.leftSideMenu) {
|
||||
this.leftSideMenu = ref;
|
||||
}
|
||||
}
|
||||
|
||||
setRightSideMenuReference(ref) {
|
||||
// The ref handler of the main component sometimes passes null, then passes the correct reference
|
||||
if(!this.rightSideMenu) {
|
||||
/**
|
||||
* The ref handler of the main component sometimes passes null, then passes
|
||||
* the correct reference
|
||||
*/
|
||||
if (!this.rightSideMenu) {
|
||||
this.rightSideMenu = ref;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@param handler.onEditorSelect
|
||||
@param handler.onTagSelect
|
||||
@param handler.getSelectedTags
|
||||
*/
|
||||
/**
|
||||
* @param handler.onEditorSelect
|
||||
* @param handler.onTagSelect
|
||||
* @param handler.getSelectedTags
|
||||
*/
|
||||
setHandlerForLeftSideMenu(handler) {
|
||||
this.leftSideMenuHandler = handler;
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/*
|
||||
@param handler.onTagSelect
|
||||
@param handler.getSelectedTags
|
||||
@param handler.getCurrentNote
|
||||
*/
|
||||
/**
|
||||
* @param handler.onTagSelect
|
||||
* @param handler.getSelectedTags
|
||||
* @param handler.getCurrentNote
|
||||
*/
|
||||
setHandlerForRightSideMenu(handler) {
|
||||
this.rightSideMenuHandler = handler;
|
||||
|
||||
@@ -72,7 +72,7 @@ export default class SideMenuManager {
|
||||
removeHandlerForRightSideMenu(handler) {
|
||||
// In tablet switching mode, a new Compose window may be created before the first one unmounts.
|
||||
// If an old instance asks us to remove handler, we want to make sure it's not the new one
|
||||
if(handler == this.rightSideMenuHandler) {
|
||||
if (handler === this.rightSideMenuHandler) {
|
||||
this.rightSideMenuHandler = null;
|
||||
}
|
||||
}
|
||||
@@ -92,5 +92,4 @@ export default class SideMenuManager {
|
||||
isRightSideMenuLocked() {
|
||||
return this.rightSideMenuLocked;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
const GenericVarPrefix = "--";
|
||||
const StylekitPrefix = "--sn-stylekit";
|
||||
const StylekitPrefixToBurn = "--sn-";
|
||||
const PREFIX_GENERIC = '--';
|
||||
const PREFIX_STANDARD_NOTES = '--sn-stylekit';
|
||||
const PREFIX_STANDARD_NOTES_BURN = '--sn-';
|
||||
|
||||
export default class CSSParser {
|
||||
/*
|
||||
@param css: CSS file contents in string format
|
||||
*/
|
||||
/**
|
||||
* @param css: CSS file contents in string format
|
||||
*/
|
||||
static cssToObject(css) {
|
||||
let object = {};
|
||||
let lines = css.split("\n");
|
||||
const object = {};
|
||||
const lines = css.split('\n');
|
||||
|
||||
for(var line of lines) {
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if(line.startsWith(GenericVarPrefix)) {
|
||||
// Remove initial "--"
|
||||
if(line.startsWith(StylekitPrefix)) {
|
||||
line = line.slice(StylekitPrefixToBurn.length, line.length);
|
||||
if (line.startsWith(PREFIX_GENERIC)) {
|
||||
// Remove initial '--'
|
||||
if (line.startsWith(PREFIX_STANDARD_NOTES)) {
|
||||
line = line.slice(PREFIX_STANDARD_NOTES_BURN.length, line.length);
|
||||
} else {
|
||||
// Not all vars start with --sn-stylekit. e.g --background-color
|
||||
line = line.slice(GenericVarPrefix.length, line.length);
|
||||
line = line.slice(PREFIX_GENERIC.length, line.length);
|
||||
}
|
||||
let parts = line.split(":");
|
||||
const parts = line.split(':');
|
||||
let key = parts[0].trim();
|
||||
let value = parts[1].trim();;
|
||||
let value = parts[1].trim();
|
||||
|
||||
key = this.hyphenatedStringToCamelCase(key);
|
||||
value = value.replace(";", "").trim();
|
||||
value = value.replace(';', '').trim();
|
||||
|
||||
object[key] = value;
|
||||
}
|
||||
@@ -37,18 +37,21 @@ export default class CSSParser {
|
||||
}
|
||||
|
||||
static resolveVariablesThatReferenceOtherVariables(object, round = 0) {
|
||||
for(const key of Object.keys(object)) {
|
||||
let value = object[key];
|
||||
let stripValue = "var(";
|
||||
if(typeof value == "string" && value.startsWith(stripValue)) {
|
||||
let from = stripValue.length;
|
||||
let to = value.indexOf(")");
|
||||
for (const key of Object.keys(object)) {
|
||||
const value = object[key];
|
||||
const stripValue = 'var(';
|
||||
if (typeof value === 'string' && value.startsWith(stripValue)) {
|
||||
const from = stripValue.length;
|
||||
const to = value.indexOf(')');
|
||||
let varName = value.slice(from, to);
|
||||
if(varName.startsWith(StylekitPrefix)) {
|
||||
varName = varName.slice(StylekitPrefixToBurn.length, varName.length);
|
||||
if (varName.startsWith(PREFIX_STANDARD_NOTES)) {
|
||||
varName = varName.slice(
|
||||
PREFIX_STANDARD_NOTES_BURN.length,
|
||||
varName.length
|
||||
);
|
||||
} else {
|
||||
// Not all vars start with --sn-stylekit. e.g --background-color
|
||||
varName = varName.slice(GenericVarPrefix.length, varName.length);
|
||||
varName = varName.slice(PREFIX_GENERIC.length, varName.length);
|
||||
}
|
||||
varName = this.hyphenatedStringToCamelCase(varName);
|
||||
object[key] = object[varName];
|
||||
@@ -59,17 +62,17 @@ export default class CSSParser {
|
||||
// the left hand counterparts. In the first round, variables on rhs mentioned before
|
||||
// its value has been gotten to in the loop will be missed. The second round takes care of this
|
||||
// and makes sure that all values will resolve.
|
||||
if(round == 0) {
|
||||
if (round === 0) {
|
||||
this.resolveVariablesThatReferenceOtherVariables(object, ++round);
|
||||
}
|
||||
}
|
||||
|
||||
static hyphenatedStringToCamelCase(string) {
|
||||
let comps = string.split("-");
|
||||
let result = "";
|
||||
for(var i = 0; i < comps.length; i++) {
|
||||
const comps = string.split('-');
|
||||
let result = '';
|
||||
for (let i = 0; i < comps.length; i++) {
|
||||
let part = comps[i];
|
||||
if(i == 0) {
|
||||
if (i === 0) {
|
||||
result += part;
|
||||
} else {
|
||||
result += this.capitalizeFirstLetter(part);
|
||||
|
||||
Reference in New Issue
Block a user