mirror of
https://github.com/standardnotes/mobile.git
synced 2026-05-19 03:54:30 -04:00
TS types (#253)
* Add TS to lib/components/styles/authentication * Rollback prettier changes to declutter diff * Authentication migration 526 errors * Notes 328 errors * Settings 172 errors * refactor: last screens * fix: revert setting wrong sideMenuHandler * feature: upgrade type deps
This commit is contained in:
@@ -9,4 +9,4 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
strailingComma: "none"
|
||||
};
|
||||
};
|
||||
@@ -21,8 +21,6 @@ Clone the project, then initialize the project with required files:
|
||||
1. `yarn init`
|
||||
3. `react-native run-ios` or `react-native run-android`
|
||||
|
||||
Note: You may need to set up an SSH key on GitHub to pull in submodules. Please follow [these instructions](https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/) to do so.
|
||||
|
||||
If upon building Android you see the error "Could not get unknown property 'repositoryUrl' for project ':ReactAndroid'", please edit the file in `node_modules/react-native/ReactAndroid/release.gradle` according to [these instructions](https://stackoverflow.com/questions/43967489/could-not-get-unknown-property-repositoryurl-for-project).
|
||||
|
||||
### Running on Device
|
||||
|
||||
@@ -313,7 +313,7 @@ PODS:
|
||||
- React
|
||||
- RNCAsyncStorage (1.6.3):
|
||||
- React
|
||||
- RNCMaskedView (0.1.9):
|
||||
- RNCMaskedView (0.1.10):
|
||||
- React
|
||||
- RNFileViewer (2.0.2):
|
||||
- React
|
||||
@@ -325,7 +325,7 @@ PODS:
|
||||
- React
|
||||
- RNReanimated (1.8.0):
|
||||
- React
|
||||
- RNScreens (2.5.0):
|
||||
- RNScreens (2.7.0):
|
||||
- React
|
||||
- RNStoreReview (0.1.5):
|
||||
- React
|
||||
@@ -549,13 +549,13 @@ SPEC CHECKSUMS:
|
||||
ReactNativeAlternateIcons: b2a8a729d9d9756ed0652c352694f190407f297f
|
||||
ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949
|
||||
RNCAsyncStorage: 3c304d1adfaea02ec732ac218801cb13897aa8c0
|
||||
RNCMaskedView: 71fc32d971f03b7f03d6ab6b86b730c4ee64f5b6
|
||||
RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f
|
||||
RNFileViewer: b815b353fdc08552766c6325e5b66ff52bb6b7af
|
||||
RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df
|
||||
RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38
|
||||
RNKeychain: 840f8e6f13be0576202aefcdffd26a4f54bfe7b5
|
||||
RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff
|
||||
RNScreens: ac02d0e4529f08ced69f5580d416f968a6ec3a1d
|
||||
RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706
|
||||
RNStoreReview: 62d6afd7c37db711a594bbffca6b0ea3a812b7a8
|
||||
RNVectorIcons: 0bb4def82230be1333ddaeee9fcba45f0b288ed4
|
||||
sn-textview: f478ee79531da2c7b129c4ea3b20c665e75e1f4b
|
||||
|
||||
26
package.json
26
package.json
@@ -10,7 +10,7 @@
|
||||
"apk-android": "cd android && ./gradlew assembleRelease",
|
||||
"bundle-android": "cd android && ./gradlew bundleRelease",
|
||||
"clear-cache": "watchman watch-del-all && rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*",
|
||||
"init": "yarn && cd ios && pod install",
|
||||
"init": "yarn && npx pod-install ios",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "npm-run-all --parallel lint:*",
|
||||
"lint:eslint": "yarn eslint . --ext .ts,.tsx --fix",
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-community/async-storage": "1.6.3",
|
||||
"@react-native-community/masked-view": "^0.1.9",
|
||||
"@react-native-community/masked-view": "^0.1.10",
|
||||
"base-64": "^0.1.0",
|
||||
"bugsnag-react-native": "^2.23.7",
|
||||
"immutable": "^3.8.2",
|
||||
@@ -29,21 +29,21 @@
|
||||
"moment": "^2.24.0",
|
||||
"react": "16.11.0",
|
||||
"react-native": "0.62.2",
|
||||
"react-native-actionsheet": "standardnotes/react-native-actionsheet#6846f21",
|
||||
"react-native-actionsheet": "standardnotes/react-native-actionsheet#9cb323f",
|
||||
"react-native-aes-crypto": "1.3.8",
|
||||
"react-native-alternate-icons": "standardnotes/react-native-alternate-icons#3154f8d",
|
||||
"react-native-alternate-icons": "standardnotes/react-native-alternate-icons#1d335d",
|
||||
"react-native-dark-mode": "^0.2.2",
|
||||
"react-native-fab": "standardnotes/react-native-fab#113661d",
|
||||
"react-native-fab": "standardnotes/react-native-fab#cb60e00",
|
||||
"react-native-file-viewer": "^2.0.0",
|
||||
"react-native-fingerprint-scanner": "standardnotes/react-native-fingerprint-scanner#5984941",
|
||||
"react-native-flag-secure-android": "standardnotes/react-native-flag-secure-android#d0cbae0",
|
||||
"react-native-flag-secure-android": "standardnotes/react-native-flag-secure-android#3d59055",
|
||||
"react-native-fs": "^2.16.6",
|
||||
"react-native-gesture-handler": "^1.6.1",
|
||||
"react-native-keychain": "^4.0.1",
|
||||
"react-native-mail": "standardnotes/react-native-mail#9862c76",
|
||||
"react-native-reanimated": "^1.8.0",
|
||||
"react-native-safe-area-context": "^0.7.3",
|
||||
"react-native-screens": "^2.5.0",
|
||||
"react-native-screens": "^2.7.0",
|
||||
"react-native-search-box": "standardnotes/react-native-search-box#210b036",
|
||||
"react-native-store-review": "^0.1.5",
|
||||
"react-native-vector-icons": "6.6.0",
|
||||
@@ -53,21 +53,21 @@
|
||||
"react-navigation-header-buttons": "^2.1.1",
|
||||
"react-navigation-stack": "^1.10.3",
|
||||
"regenerator": "^0.14.2",
|
||||
"sn-textview": "standardnotes/sn-textview#8b62cb2",
|
||||
"sn-textview": "standardnotes/sn-textview#f42f0bf",
|
||||
"snjs": "standardnotes/snjs#9382050",
|
||||
"stacktrace-parser": "0.1.8",
|
||||
"standard-notes-rn": "standardnotes/standard-notes-rn"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"@react-native-community/eslint-config": "^1.1.0",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/lodash": "^4.14.150",
|
||||
"@types/react-native": "^0.62.4",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"@types/react-native-vector-icons": "^6.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||
"@typescript-eslint/parser": "^2.30.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"detox": "^16.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
|
||||
34
src/App.tsx
34
src/App.tsx
@@ -4,6 +4,7 @@ import { Animated } from 'react-native';
|
||||
import {
|
||||
initialMode,
|
||||
eventEmitter as darkModeEventEmitter,
|
||||
Mode,
|
||||
} from 'react-native-dark-mode';
|
||||
import { createAppContainer, NavigationActions } from 'react-navigation';
|
||||
import { createDrawerNavigator, DrawerActions } from 'react-navigation-drawer';
|
||||
@@ -47,7 +48,9 @@ protocolManager.crypto.setNativeModules({
|
||||
});
|
||||
|
||||
if (__DEV__ === false) {
|
||||
const bugsnag = new Client();
|
||||
// bugsnag
|
||||
// eslint-disable-next-line no-new
|
||||
new Client();
|
||||
|
||||
/** Disable console.log for non-dev builds */
|
||||
console.log = () => {};
|
||||
@@ -60,7 +63,7 @@ const AppStack = createStackNavigator(
|
||||
},
|
||||
{
|
||||
initialRouteName: SCREEN_NOTES,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
navigationOptions: () => ({
|
||||
drawerLockMode: SideMenuManager.get().isRightSideMenuLocked()
|
||||
? 'locked-closed'
|
||||
: null,
|
||||
@@ -78,16 +81,18 @@ const AppDrawerStack = createDrawerNavigator(
|
||||
ref={ref => {
|
||||
SideMenuManager.get().setRightSideMenuReference(ref);
|
||||
}}
|
||||
// @ts-ignore navigation is ignored
|
||||
navigation={navigation}
|
||||
/>
|
||||
),
|
||||
drawerPosition: 'right',
|
||||
drawerType: 'slide',
|
||||
// @ts-ignore navigation is ignored
|
||||
getCustomActionCreators: (route, stateKey) => {
|
||||
return {
|
||||
openRightDrawer: () => DrawerActions.openDrawer({ key: stateKey }),
|
||||
closeRightDrawer: () => DrawerActions.closeDrawer({ key: stateKey }),
|
||||
lockRightDrawer: lock => {
|
||||
lockRightDrawer: (lock: any) => {
|
||||
/** This is the key part */
|
||||
SideMenuManager.get().setLockedForRightSideMenu(lock);
|
||||
/** We have to return something. */
|
||||
@@ -133,13 +138,14 @@ const AppDrawer = createStackNavigator(
|
||||
{
|
||||
mode: 'modal',
|
||||
headerMode: 'none',
|
||||
// @ts-ignore navigation is ignored
|
||||
transitionConfig: () => ({
|
||||
transitionSpec: {
|
||||
duration: 300,
|
||||
timing: Animated.timing,
|
||||
},
|
||||
}),
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
navigationOptions: () => ({
|
||||
drawerLockMode: SideMenuManager.get().isLeftSideMenuLocked()
|
||||
? 'locked-closed'
|
||||
: null,
|
||||
@@ -157,16 +163,18 @@ const DrawerStack = createDrawerNavigator(
|
||||
ref={ref => {
|
||||
SideMenuManager.get().setLeftSideMenuReference(ref);
|
||||
}}
|
||||
// @ts-ignore navigation is ignored
|
||||
navigation={navigation}
|
||||
/>
|
||||
),
|
||||
drawerPosition: 'left',
|
||||
drawerType: 'slide',
|
||||
// @ts-ignore navigation is ignored
|
||||
getCustomActionCreators: (route, stateKey) => {
|
||||
return {
|
||||
openLeftDrawer: () => DrawerActions.openDrawer({ key: stateKey }),
|
||||
closeLeftDrawer: () => DrawerActions.closeDrawer({ key: stateKey }),
|
||||
lockLeftDrawer: lock => {
|
||||
lockLeftDrawer: (lock: any) => {
|
||||
/** This is the key part. */
|
||||
SideMenuManager.get().setLockedForLeftSideMenu(lock);
|
||||
/** We have to return something. */
|
||||
@@ -182,10 +190,14 @@ const DrawerStack = createDrawerNavigator(
|
||||
|
||||
const AppContainer = createAppContainer(DrawerStack);
|
||||
|
||||
export default class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
type State = {
|
||||
ready: boolean;
|
||||
};
|
||||
|
||||
export default class App extends Component<{}, State> {
|
||||
authEventHandler: any;
|
||||
constructor(props: Readonly<{}>) {
|
||||
super(props);
|
||||
StyleKit.get().setModeTo(initialMode);
|
||||
darkModeEventEmitter.on('currentModeChanged', this.onChangeCurrentMode);
|
||||
|
||||
@@ -201,10 +213,10 @@ export default class App extends Component {
|
||||
MigrationManager.get().load();
|
||||
|
||||
/** Listen to sign out event */
|
||||
this.authEventHandler = Auth.get().addEventHandler(async event => {
|
||||
this.authEventHandler = Auth.get().addEventHandler(async (event: any) => {
|
||||
if (event === SFAuthManager.DidSignOutEvent) {
|
||||
ModelManager.get().handleSignout();
|
||||
await Sync.get().handleSignout();
|
||||
Sync.get().handleSignout();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -244,7 +256,7 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
/** @private */
|
||||
onChangeCurrentMode(mode) {
|
||||
onChangeCurrentMode(mode: Mode) {
|
||||
StyleKit.get().setModeTo(mode);
|
||||
StyleKit.get().activatePreferredTheme();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,17 @@ import { TouchableHighlight, Text, View } from 'react-native';
|
||||
import SectionedTableCell from '@Components/SectionedTableCell';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class ButtonCell extends SectionedTableCell {
|
||||
type Props = {
|
||||
maxHeight?: number;
|
||||
leftAligned?: boolean;
|
||||
bold?: boolean;
|
||||
disabled?: boolean;
|
||||
important?: boolean;
|
||||
onPress: () => void;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export default class ButtonCell extends SectionedTableCell<Props> {
|
||||
rules() {
|
||||
const rules = super.rules();
|
||||
if (this.props.maxHeight) {
|
||||
@@ -13,7 +23,7 @@ export default class ButtonCell extends SectionedTableCell {
|
||||
}
|
||||
|
||||
buttonRules() {
|
||||
const rules = [StyleKit.stylesForKey('buttonCellButton')];
|
||||
let rules = StyleKit.stylesForKey('buttonCellButton');
|
||||
if (this.props.leftAligned) {
|
||||
rules.push(StyleKit.styles.buttonCellButtonLeft);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import SectionedTableCell from '@Components/SectionedTableCell';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import SectionedTableCell, {
|
||||
Props as SectionTableCellProps,
|
||||
} from '@Components/SectionedTableCell';
|
||||
|
||||
export default class Circle extends SectionedTableCell {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
size?: number;
|
||||
backgroundColor: ViewStyle['backgroundColor'];
|
||||
borderColor: ViewStyle['borderColor'];
|
||||
};
|
||||
|
||||
export default class Circle extends SectionedTableCell<Props> {
|
||||
styles!: Record<string, ViewStyle>;
|
||||
size: number;
|
||||
constructor(props: Readonly<SectionTableCellProps & Props>) {
|
||||
super(props);
|
||||
this.size = props.size || 12;
|
||||
this.loadStyles();
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { View, Text, TextStyle } from 'react-native';
|
||||
import PlatformStyles from '@Models/PlatformStyles';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class HeaderTitleView extends Component {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
subtitleColor?: TextStyle['color'];
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
|
||||
export default class HeaderTitleView extends Component<Props> {
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Text, Platform, View, TouchableOpacity } from 'react-native';
|
||||
import {
|
||||
Text,
|
||||
Platform,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class SectionHeader extends ThemedComponent {
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
buttonText?: string;
|
||||
buttonAction?: () => void;
|
||||
buttonStyles?: ViewStyle | TextStyle;
|
||||
tinted?: boolean;
|
||||
backgroundColor?: ViewStyle['backgroundColor'];
|
||||
foregroundColor?: string;
|
||||
};
|
||||
|
||||
export default class SectionHeader extends ThemedComponent<Props> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
render() {
|
||||
let title = this.props.title;
|
||||
if (Platform.OS === 'ios') {
|
||||
|
||||
@@ -5,7 +5,23 @@ import StyleKit from '@Style/StyleKit';
|
||||
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
export default class SectionedAccessoryTableCell extends SectionedTableCell {
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
onPress: () => void;
|
||||
onLongPress?: () => void;
|
||||
iconName?: string;
|
||||
selected?: () => void;
|
||||
leftAlignIcon?: boolean;
|
||||
color?: string;
|
||||
bold?: boolean;
|
||||
tinted?: boolean;
|
||||
dimmed?: boolean;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export default class SectionedAccessoryTableCell extends SectionedTableCell<
|
||||
Props
|
||||
> {
|
||||
rules() {
|
||||
const rules = super
|
||||
.rules()
|
||||
@@ -63,15 +79,14 @@ export default class SectionedAccessoryTableCell extends SectionedTableCell {
|
||||
if (this.props.color) {
|
||||
color = this.props.color;
|
||||
}
|
||||
let icon = null;
|
||||
|
||||
var icon = (
|
||||
<View key={0} style={iconStyles}>
|
||||
<Icon name={iconName} size={iconSize} color={color} />
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!iconName) {
|
||||
icon = null;
|
||||
if (iconName) {
|
||||
icon = (
|
||||
<View key={0} style={iconStyles}>
|
||||
<Icon name={iconName} size={iconSize} color={color} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
var textStyles = [StyleKit.styles.sectionedAccessoryTableCellLabel];
|
||||
@@ -95,13 +110,6 @@ export default class SectionedAccessoryTableCell extends SectionedTableCell {
|
||||
</Text>
|
||||
);
|
||||
|
||||
const containerStyles = {
|
||||
flex: 1,
|
||||
justifyContent: left ? 'flex-start' : 'space-between',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
testID={this.props.testID}
|
||||
@@ -110,7 +118,14 @@ export default class SectionedAccessoryTableCell extends SectionedTableCell {
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onLongPress}
|
||||
>
|
||||
<View style={containerStyles}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: left ? 'flex-start' : 'space-between',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{this.props.leftAlignIcon ? [icon, textWrapper] : [textWrapper, icon]}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableHighlight } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class SectionedOptionsTableCell extends ThemedComponent {
|
||||
type Option = { selected: boolean; key: string; title: string };
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
first?: boolean;
|
||||
height?: number;
|
||||
extraStyles?: ViewStyle;
|
||||
testID?: string;
|
||||
onPress: (option: Option) => void;
|
||||
options: Option[];
|
||||
};
|
||||
|
||||
export default class SectionedOptionsTableCell extends ThemedComponent<Props> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
rules() {
|
||||
let rules = [StyleKit.styles.sectionedTableCell];
|
||||
if (this.props.first) {
|
||||
|
||||
@@ -3,14 +3,25 @@ import { View } from 'react-native';
|
||||
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class SectionedTableCell extends Component {
|
||||
export type Props = {
|
||||
first?: boolean;
|
||||
last?: boolean;
|
||||
textInputCell?: any;
|
||||
height?: number;
|
||||
extraStyles?: any;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
export default class SectionedTableCell<AdditionalProps = {}> extends Component<
|
||||
Props & AdditionalProps
|
||||
> {
|
||||
rules() {
|
||||
let rules = [StyleKit.styles.sectionedTableCell];
|
||||
if (this.props.first) {
|
||||
rules.push(StyleKit.stylesForKey('sectionedTableCellFirst'));
|
||||
rules.concat(StyleKit.stylesForKey('sectionedTableCellFirst'));
|
||||
}
|
||||
if (this.props.last) {
|
||||
rules.push(StyleKit.stylesForKey('sectionedTableCellLast'));
|
||||
rules.concat(StyleKit.stylesForKey('sectionedTableCellLast'));
|
||||
}
|
||||
if (this.props.textInputCell) {
|
||||
rules.push(StyleKit.styles.textInputCell);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class TableSection extends Component {
|
||||
type Props = {
|
||||
extraStyles?: ViewStyle | ViewStyle[];
|
||||
};
|
||||
|
||||
export default class TableSection extends Component<Props> {
|
||||
rules() {
|
||||
let rules = [StyleKit.styles.tableSection];
|
||||
if (this.props.extraStyles) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component } from 'react';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class ThemedComponent extends Component {
|
||||
constructor(props) {
|
||||
export default class ThemedComponent<P = {}, S = any> extends Component<P, S> {
|
||||
themeChangeObserver: () => void;
|
||||
constructor(props: Readonly<P>) {
|
||||
super(props);
|
||||
|
||||
this.loadStyles();
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { PureComponent } from 'react';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class ThemedPureComponent extends PureComponent {
|
||||
constructor(props) {
|
||||
export default class ThemedPureComponent<P = {}, S = {}> extends PureComponent<
|
||||
P,
|
||||
S
|
||||
> {
|
||||
themeChangeObserver: () => void;
|
||||
constructor(props: Readonly<P>) {
|
||||
super(props);
|
||||
|
||||
this.loadStyles();
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { isArray } from 'lodash';
|
||||
|
||||
export default class LockedView extends Component {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
style?: ViewStyle[];
|
||||
onUnlockPress?: () => void;
|
||||
};
|
||||
|
||||
export default class LockedView extends Component<Props> {
|
||||
styles?: {
|
||||
unlockButton: ViewStyle;
|
||||
unlockButtonText: TextStyle;
|
||||
};
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.loadStyles();
|
||||
}
|
||||
|
||||
render() {
|
||||
const color = StyleKit.variables.stylekitInfoColor;
|
||||
const styles = [
|
||||
let styles = [
|
||||
StyleKit.styles.centeredContainer,
|
||||
{ backgroundColor: StyleKit.variables.stylekitBackgroundColor },
|
||||
];
|
||||
if (this.props.style) {
|
||||
styles.push(this.props.style);
|
||||
if (isArray(this.props.style)) {
|
||||
styles = styles.concat(this.props.style);
|
||||
} else {
|
||||
styles.push(this.props.style);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View style={styles}>
|
||||
<Text style={{ color: color, marginTop: 5, fontWeight: 'bold' }}>
|
||||
<Text style={{ color, marginTop: 5, fontWeight: 'bold' }}>
|
||||
Application Locked.
|
||||
</Text>
|
||||
|
||||
{!this.props.onUnlockPress && (
|
||||
<Text style={{ color: color, marginTop: 5 }}>
|
||||
<Text style={{ color, marginTop: 5 }}>
|
||||
Return to Notes to unlock.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{this.props.onUnlockPress && (
|
||||
<TouchableOpacity onPress={this.props.onUnlockPress}>
|
||||
<View style={this.styles.unlockButton}>
|
||||
<Text style={this.styles.unlockButtonText}>Unlock</Text>
|
||||
<View style={this.styles?.unlockButton}>
|
||||
<Text style={this.styles?.unlockButtonText}>Unlock</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
// require('core-js/fn/array/find');
|
||||
|
||||
// TODO: still crashes without this
|
||||
// @ts-ignore
|
||||
global._ = require('lodash');
|
||||
|
||||
@@ -5,6 +5,9 @@ import {
|
||||
Linking,
|
||||
Alert,
|
||||
Keyboard,
|
||||
AppStateStatus,
|
||||
KeyboardEventListener,
|
||||
EmitterSubscription,
|
||||
} from 'react-native';
|
||||
import _ from 'lodash';
|
||||
import KeysManager from '@Lib/keysManager';
|
||||
@@ -16,36 +19,96 @@ import AuthenticationSourceBiometric from '@Screens/Authentication/Sources/Authe
|
||||
const pjson = require('../../package.json');
|
||||
const { PlatformConstants } = NativeModules;
|
||||
|
||||
export type AppStateType =
|
||||
| typeof ApplicationState.Launching
|
||||
| typeof ApplicationState.LosingFocus
|
||||
| typeof ApplicationState.Backgrounding
|
||||
| typeof ApplicationState.GainingFocus
|
||||
| typeof ApplicationState.ResumingFromBackground
|
||||
| typeof ApplicationState.Locking
|
||||
| typeof ApplicationState.Unlocking;
|
||||
|
||||
// AppStateEvents
|
||||
export type AppStateEventType =
|
||||
| typeof ApplicationState.KeyboardChangeEvent
|
||||
| typeof ApplicationState.AppStateEventTabletModeChange
|
||||
| typeof ApplicationState.AppStateEventNoteSideMenuToggle;
|
||||
export type TabletModeChangeData = {
|
||||
new_isInTabletMode: boolean;
|
||||
old_isInTabletMode: boolean;
|
||||
};
|
||||
export type NoteSideMenuToggleChange = {
|
||||
new_isNoteSideMenuCollapsed: boolean;
|
||||
old_isNoteSideMenuCollapsed: boolean;
|
||||
};
|
||||
|
||||
type KeyboardChangeEventHandler = (
|
||||
event: typeof ApplicationState.KeyboardChangeEvent,
|
||||
data: undefined
|
||||
) => void;
|
||||
type SideMenuToogleEvent = (
|
||||
event: typeof ApplicationState.AppStateEventNoteSideMenuToggle,
|
||||
data: NoteSideMenuToggleChange
|
||||
) => void;
|
||||
type TableModeChageEvent = (
|
||||
event: typeof ApplicationState.AppStateEventTabletModeChange,
|
||||
data: TabletModeChangeData
|
||||
) => void;
|
||||
export type AppStateEventHandler =
|
||||
| KeyboardChangeEventHandler
|
||||
| SideMenuToogleEvent
|
||||
| TableModeChageEvent;
|
||||
|
||||
type Observer = {
|
||||
key: () => number;
|
||||
callback: (state: AppStateType) => void;
|
||||
};
|
||||
|
||||
export default class ApplicationState {
|
||||
// When the app first launches
|
||||
static Launching = 'Launching';
|
||||
static Launching = 'Launching' as 'Launching';
|
||||
|
||||
// When the app enters into multitasking view, or control/notification center for iOS
|
||||
static LosingFocus = 'LosingFocus';
|
||||
static LosingFocus = 'LosingFocus' as 'LosingFocus';
|
||||
|
||||
// When the app enters the background completely
|
||||
static Backgrounding = 'Backgrounding';
|
||||
static Backgrounding = 'Backgrounding' as 'Backgrounding';
|
||||
|
||||
// When the app resumes from either the background or from multitasking switcher or notification center
|
||||
static GainingFocus = 'GainingFocus';
|
||||
static GainingFocus = 'GainingFocus' as 'GainingFocus';
|
||||
|
||||
// When the app resumes from the background
|
||||
static ResumingFromBackground = 'ResumingFromBackground';
|
||||
static ResumingFromBackground = 'ResumingFromBackground' as 'ResumingFromBackground';
|
||||
|
||||
// When the user enters their local passcode and/or fingerprint
|
||||
static Locking = 'Locking';
|
||||
static Locking = 'Locking' as 'Locking';
|
||||
|
||||
// When the user enters their local passcode and/or fingerprint
|
||||
static Unlocking = 'Unlocking';
|
||||
static Unlocking = 'Unlocking' as 'Unlocking';
|
||||
|
||||
/* Seperate events, unrelated to app state notifications */
|
||||
static AppStateEventTabletModeChange = 'AppStateEventTabletModeChange';
|
||||
static AppStateEventNoteSideMenuToggle = 'AppStateEventNoteSideMenuToggle';
|
||||
static KeyboardChangeEvent = 'KeyboardChangeEvent';
|
||||
static AppStateEventTabletModeChange = 'AppStateEventTabletModeChange' as 'AppStateEventTabletModeChange';
|
||||
static AppStateEventNoteSideMenuToggle = 'AppStateEventNoteSideMenuToggle' as 'AppStateEventNoteSideMenuToggle';
|
||||
static KeyboardChangeEvent = 'KeyboardChangeEvent' as 'KeyboardChangeEvent';
|
||||
|
||||
static instance = null;
|
||||
private static instance: ApplicationState;
|
||||
_isAndroid: boolean;
|
||||
observers: Observer[];
|
||||
eventSubscribers: AppStateEventHandler[];
|
||||
locked: boolean;
|
||||
keyboardDidShowListener: EmitterSubscription;
|
||||
keyboardDidHideListener: EmitterSubscription;
|
||||
keyboardHeight?: number;
|
||||
optionsState: OptionsState;
|
||||
loading: boolean = false;
|
||||
tabletMode: boolean = false;
|
||||
noteSideMenuCollapsed: boolean = false;
|
||||
ignoreStateChanges: boolean = false;
|
||||
mostRecentState?: AppStateType;
|
||||
didHandleApplicationStart: boolean = false;
|
||||
authenticationInProgress: boolean = false;
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new ApplicationState();
|
||||
}
|
||||
|
||||
@@ -54,6 +117,7 @@ export default class ApplicationState {
|
||||
|
||||
constructor() {
|
||||
this.observers = [];
|
||||
this.optionsState = new OptionsState();
|
||||
this.eventSubscribers = [];
|
||||
this.locked = true;
|
||||
this._isAndroid = Platform.OS === 'android';
|
||||
@@ -74,12 +138,12 @@ export default class ApplicationState {
|
||||
);
|
||||
}
|
||||
|
||||
keyboardDidShow = e => {
|
||||
keyboardDidShow: KeyboardEventListener = e => {
|
||||
this.keyboardHeight = e.endCoordinates.height;
|
||||
this.notifyEvent(ApplicationState.KeyboardChangeEvent);
|
||||
};
|
||||
|
||||
keyboardDidHide = e => {
|
||||
keyboardDidHide: KeyboardEventListener = () => {
|
||||
this.keyboardHeight = 0;
|
||||
this.notifyEvent(ApplicationState.KeyboardChangeEvent);
|
||||
};
|
||||
@@ -90,7 +154,6 @@ export default class ApplicationState {
|
||||
|
||||
initializeOptions() {
|
||||
// Initialize Options (sort by, filter, selected tags, etc)
|
||||
this.optionsState = new OptionsState();
|
||||
this.optionsState.addChangeObserver(options => {
|
||||
if (!this.loading) {
|
||||
options.persist();
|
||||
@@ -137,7 +200,7 @@ export default class ApplicationState {
|
||||
return this.tabletMode;
|
||||
}
|
||||
|
||||
setTabletModeEnabled(enabled) {
|
||||
setTabletModeEnabled(enabled: boolean) {
|
||||
if (enabled !== this.tabletMode) {
|
||||
this.tabletMode = enabled;
|
||||
this.notifyEvent(ApplicationState.AppStateEventTabletModeChange, {
|
||||
@@ -151,7 +214,7 @@ export default class ApplicationState {
|
||||
return this.noteSideMenuCollapsed;
|
||||
}
|
||||
|
||||
setNoteSideMenuCollapsed(collapsed) {
|
||||
setNoteSideMenuCollapsed(collapsed: boolean) {
|
||||
if (collapsed !== this.noteSideMenuCollapsed) {
|
||||
this.noteSideMenuCollapsed = collapsed;
|
||||
this.notifyEvent(ApplicationState.AppStateEventNoteSideMenuToggle, {
|
||||
@@ -161,22 +224,26 @@ export default class ApplicationState {
|
||||
}
|
||||
}
|
||||
|
||||
addEventHandler(handler) {
|
||||
addEventHandler(handler: AppStateEventHandler) {
|
||||
this.eventSubscribers.push(handler);
|
||||
return handler;
|
||||
}
|
||||
|
||||
removeEventHandler(handler) {
|
||||
removeEventHandler(handler: AppStateEventHandler) {
|
||||
_.pull(this.eventSubscribers, handler);
|
||||
}
|
||||
|
||||
notifyEvent(event, data) {
|
||||
notifyEvent(
|
||||
event: AppStateEventType,
|
||||
data?: TabletModeChangeData | NoteSideMenuToggleChange
|
||||
) {
|
||||
for (const handler of this.eventSubscribers) {
|
||||
// @ts-ignore not working type
|
||||
handler(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
handleAppStateChange = nextAppState => {
|
||||
handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||
if (this.ignoreStateChanges) {
|
||||
return;
|
||||
}
|
||||
@@ -235,14 +302,14 @@ export default class ApplicationState {
|
||||
// Visibility change events are like active, inactive, background,
|
||||
// while non-app cycle events are custom events like locking and unlocking
|
||||
|
||||
isAppVisibilityChange(state) {
|
||||
return [
|
||||
isAppVisibilityChange(state: AppStateType) {
|
||||
return ([
|
||||
ApplicationState.Launching,
|
||||
ApplicationState.LosingFocus,
|
||||
ApplicationState.Backgrounding,
|
||||
ApplicationState.GainingFocus,
|
||||
ApplicationState.ResumingFromBackground,
|
||||
].includes(state);
|
||||
] as Array<AppStateType>).includes(state);
|
||||
}
|
||||
|
||||
/* State Changes */
|
||||
@@ -265,7 +332,7 @@ export default class ApplicationState {
|
||||
this.notifyOfState(ApplicationState.Launching);
|
||||
}
|
||||
|
||||
notifyOfState(state) {
|
||||
notifyOfState(state: AppStateType) {
|
||||
if (this.ignoreStateChanges) {
|
||||
return;
|
||||
}
|
||||
@@ -284,7 +351,7 @@ export default class ApplicationState {
|
||||
Allows other parts of the code to perform external actions without triggering state change notifications.
|
||||
This is useful on Android when you present a share sheet and dont want immediate authentication to appear.
|
||||
*/
|
||||
performActionWithoutStateChangeImpact(block) {
|
||||
performActionWithoutStateChangeImpact(block: () => void) {
|
||||
this.ignoreStateChanges = true;
|
||||
block();
|
||||
setTimeout(() => {
|
||||
@@ -296,8 +363,8 @@ export default class ApplicationState {
|
||||
return this.mostRecentState;
|
||||
}
|
||||
|
||||
addStateObserver(callback) {
|
||||
const observer = { key: Math.random, callback: callback };
|
||||
addStateObserver(callback: Observer['callback']) {
|
||||
const observer = { key: Math.random, callback };
|
||||
this.observers.push(observer);
|
||||
|
||||
if (this.mostRecentState) {
|
||||
@@ -311,7 +378,7 @@ export default class ApplicationState {
|
||||
// this.previousEvents = [];
|
||||
// }
|
||||
|
||||
removeStateObserver(observer) {
|
||||
removeStateObserver(observer: Observer) {
|
||||
_.pull(this.observers, observer);
|
||||
}
|
||||
|
||||
@@ -347,7 +414,7 @@ export default class ApplicationState {
|
||||
this.locked = false;
|
||||
}
|
||||
|
||||
setAuthenticationInProgress(inProgress) {
|
||||
setAuthenticationInProgress(inProgress: boolean) {
|
||||
this.authenticationInProgress = inProgress;
|
||||
}
|
||||
|
||||
@@ -355,7 +422,7 @@ export default class ApplicationState {
|
||||
return this.authenticationInProgress;
|
||||
}
|
||||
|
||||
getAuthenticationPropsForAppState(state) {
|
||||
getAuthenticationPropsForAppState(state: AppStateType) {
|
||||
// We don't want to do anything on gaining focus, since that may be called extraenously,
|
||||
// when you come back from notification center, etc. Any immediate locking should be handled
|
||||
// LosingFocus anyway.
|
||||
@@ -413,7 +480,7 @@ export default class ApplicationState {
|
||||
};
|
||||
}
|
||||
|
||||
static openURL(url) {
|
||||
static openURL(url: string) {
|
||||
const showAlert = () => {
|
||||
Alert.alert('Unable to Open', `Unable to open URL ${url}.`);
|
||||
};
|
||||
|
||||
@@ -12,9 +12,9 @@ const Mailer = 'react-native-mail';
|
||||
const base64 = require('base-64');
|
||||
|
||||
export default class BackupsManager {
|
||||
static instance = null;
|
||||
private static instance: BackupsManager;
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new BackupsManager();
|
||||
}
|
||||
return this.instance;
|
||||
@@ -29,7 +29,7 @@ export default class BackupsManager {
|
||||
the path the file was saved to.
|
||||
*/
|
||||
|
||||
async export(encrypted) {
|
||||
async export(encrypted: boolean) {
|
||||
const auth_params = await Auth.get().getAuthParams();
|
||||
const keys = encrypted ? KeysManager.get().activeKeys() : null;
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class BackupsManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = { items: items };
|
||||
const data: { items: any; auth_params?: any } = { items };
|
||||
|
||||
if (keys) {
|
||||
const authParams = KeysManager.get().activeAuthParams();
|
||||
@@ -87,31 +87,31 @@ export default class BackupsManager {
|
||||
});
|
||||
}
|
||||
|
||||
async _exportIOS(filename, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
async _exportIOS(filename: string, data: string) {
|
||||
return new Promise(resolve => {
|
||||
ApplicationState.get().performActionWithoutStateChangeImpact(async () => {
|
||||
Share.share({
|
||||
title: filename,
|
||||
message: data,
|
||||
})
|
||||
.then(result => {
|
||||
resolve(result !== Share.dismissedAction);
|
||||
resolve(result.action !== Share.dismissedAction);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _exportAndroid(filename, data) {
|
||||
async _exportAndroid(filename: string, data: string) {
|
||||
const filepath = `${RNFS.ExternalDirectoryPath}/${filename}`;
|
||||
return RNFS.writeFile(filepath, data).then(() => {
|
||||
return filepath;
|
||||
});
|
||||
}
|
||||
|
||||
async _openFileAndroid(filepath) {
|
||||
async _openFileAndroid(filepath: string) {
|
||||
return FileViewer.open(filepath)
|
||||
.then(() => {
|
||||
// success
|
||||
@@ -123,7 +123,7 @@ export default class BackupsManager {
|
||||
});
|
||||
}
|
||||
|
||||
async _showFileSavePromptAndroid(filepath) {
|
||||
async _showFileSavePromptAndroid(filepath: string) {
|
||||
return AlertManager.get()
|
||||
.confirm({
|
||||
title: 'Backup Saved',
|
||||
@@ -143,8 +143,8 @@ export default class BackupsManager {
|
||||
});
|
||||
}
|
||||
|
||||
async _exportViaEmailAndroid(data, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
async _exportViaEmailAndroid(data: { items: any[] }, filename: string) {
|
||||
return new Promise(resolve => {
|
||||
const jsonString = JSON.stringify(data, null, 2 /* pretty print */);
|
||||
const stringData = base64.encode(
|
||||
unescape(encodeURIComponent(jsonString))
|
||||
@@ -152,7 +152,8 @@ export default class BackupsManager {
|
||||
const fileType = '.json'; // Android creates a tmp file and expects dot with extension
|
||||
|
||||
let resolved = false;
|
||||
|
||||
// TODO: fix mail types
|
||||
// @ts-ignore
|
||||
Mailer.mail(
|
||||
{
|
||||
subject: 'Standard Notes Backup',
|
||||
@@ -161,7 +162,7 @@ export default class BackupsManager {
|
||||
isHTML: true,
|
||||
attachment: { data: stringData, type: fileType, name: filename },
|
||||
},
|
||||
(error, event) => {
|
||||
(error: any) => {
|
||||
if (error) {
|
||||
Alert.alert('Error', 'Unable to send email.');
|
||||
}
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
import _ from 'lodash';
|
||||
import Storage from '@Lib/snjs/storageManager';
|
||||
|
||||
export default class OptionsState {
|
||||
static OptionsStateChangeEventSearch = 'OptionsStateChangeEventSearch';
|
||||
static OptionsStateChangeEventTags = 'OptionsStateChangeEventTags';
|
||||
static OptionsStateChangeEventViews = 'OptionsStateChangeEventViews';
|
||||
static OptionsStateChangeEventTags = 'OptionsStateChangeEventSort';
|
||||
type OptionsStateStateType =
|
||||
| typeof OptionsState.OptionsStateChangeEventSearch
|
||||
| typeof OptionsState.OptionsStateChangeEventTags
|
||||
| typeof OptionsState.OptionsStateChangeEventViews
|
||||
| typeof OptionsState.OptionsStateChangeEventSort;
|
||||
|
||||
constructor(json) {
|
||||
this.init();
|
||||
_.merge(this, _.omit(json, ['changeObservers']));
|
||||
export type Observer = {
|
||||
key: () => number;
|
||||
callback: (state: OptionsState, newState?: OptionsStateStateType) => void;
|
||||
};
|
||||
|
||||
export default class OptionsState {
|
||||
static OptionsStateChangeEventSearch = 'OptionsStateChangeEventSearch' as 'OptionsStateChangeEventSearch';
|
||||
static OptionsStateChangeEventTags = 'OptionsStateChangeEventTags' as 'OptionsStateChangeEventTags';
|
||||
static OptionsStateChangeEventViews = 'OptionsStateChangeEventViews' as 'OptionsStateChangeEventViews';
|
||||
static OptionsStateChangeEventSort = 'OptionsStateChangeEventSort' as 'OptionsStateChangeEventSort';
|
||||
changeObservers: Observer[];
|
||||
sortBy: string;
|
||||
selectedTagIds: string[];
|
||||
sortReverse: boolean;
|
||||
searchTerm: string | null = null;
|
||||
displayOptions?: {
|
||||
hidePreviews: boolean;
|
||||
hideTags: boolean;
|
||||
hideDates: boolean;
|
||||
};
|
||||
hidePreviews: boolean = false;
|
||||
hideDates: boolean = false;
|
||||
hideTags: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.searchTerm = '';
|
||||
this.selectedTagIds = [];
|
||||
this.sortBy = 'created_at';
|
||||
this.sortReverse = false;
|
||||
|
||||
// TODO: not used
|
||||
// _.merge(this, _.omit(json, ['changeObservers']));
|
||||
this.changeObservers = [];
|
||||
|
||||
if (this.sortBy === 'updated_at') {
|
||||
@@ -19,6 +48,7 @@ export default class OptionsState {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.searchTerm = '';
|
||||
this.selectedTagIds = [];
|
||||
this.sortBy = 'created_at';
|
||||
this.sortReverse = false;
|
||||
@@ -58,47 +88,42 @@ export default class OptionsState {
|
||||
);
|
||||
}
|
||||
|
||||
addChangeObserver(callback) {
|
||||
const observer = { key: Math.random, callback: callback };
|
||||
addChangeObserver(callback: Observer['callback']) {
|
||||
const observer = { key: Math.random, callback };
|
||||
this.changeObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
removeChangeObserver(observer) {
|
||||
removeChangeObserver(observer: Observer) {
|
||||
_.pull(this.changeObservers, observer);
|
||||
}
|
||||
|
||||
notifyObservers(event) {
|
||||
notifyObservers(newOption?: OptionsStateStateType) {
|
||||
this.changeObservers.forEach(
|
||||
function (observer) {
|
||||
observer.callback(this, event);
|
||||
function (observer: Observer) {
|
||||
// @ts-ignore
|
||||
observer.callback(this as OptionsState, newOption);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
// Interface
|
||||
|
||||
mergeWith(options) {
|
||||
_.extend(this, _.omit(options, ['changeObservers']));
|
||||
this.notifyObservers();
|
||||
}
|
||||
|
||||
setSearchTerm(term) {
|
||||
setSearchTerm(term: string | null) {
|
||||
this.searchTerm = term;
|
||||
this.notifyObservers(OptionsState.OptionsStateChangeEventSearch);
|
||||
}
|
||||
|
||||
setSortReverse(reverse) {
|
||||
setSortReverse(reverse: boolean) {
|
||||
this.sortReverse = reverse;
|
||||
this.notifyObservers(OptionsState.OptionsStateChangeEventSort);
|
||||
}
|
||||
|
||||
setSortBy(sortBy) {
|
||||
setSortBy(sortBy: string) {
|
||||
this.sortBy = sortBy;
|
||||
this.notifyObservers(OptionsState.OptionsStateChangeEventSort);
|
||||
}
|
||||
|
||||
setSelectedTagIds(selectedTagIds) {
|
||||
setSelectedTagIds(selectedTagIds: string[]) {
|
||||
this.selectedTagIds = selectedTagIds;
|
||||
this.notifyObservers(OptionsState.OptionsStateChangeEventTags);
|
||||
}
|
||||
@@ -122,7 +147,7 @@ export default class OptionsState {
|
||||
};
|
||||
}
|
||||
|
||||
getDisplayOptionValue(key) {
|
||||
getDisplayOptionValue(key: string) {
|
||||
if (key === 'hidePreviews') {
|
||||
return this.hidePreviews;
|
||||
} else if (key === 'hideDates') {
|
||||
@@ -130,9 +155,10 @@ export default class OptionsState {
|
||||
} else if (key === 'hideTags') {
|
||||
return this.hideTags;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setDisplayOptionKeyValue(key, value) {
|
||||
setDisplayOptionKeyValue(key: string, value: any) {
|
||||
if (key === 'hidePreviews') {
|
||||
this.hidePreviews = value;
|
||||
} else if (key === 'hideDates') {
|
||||
|
||||
@@ -5,11 +5,22 @@ import ModelManager from '@Lib/snjs/modelManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
type ComponentManagerInstance = {
|
||||
modelManager: ModelManager;
|
||||
syncManager: Sync;
|
||||
alertManager: AlertManager;
|
||||
environment: 'mobile';
|
||||
platform: typeof Platform.OS;
|
||||
desktopManager?: any;
|
||||
nativeExtManager?: any;
|
||||
$uiRunner?: any;
|
||||
};
|
||||
|
||||
export default class ComponentManager extends SNComponentManager {
|
||||
static instance = null;
|
||||
private static instance: ComponentManager;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new ComponentManager({
|
||||
modelManager: ModelManager.get(),
|
||||
syncManager: Sync.get(),
|
||||
@@ -31,7 +42,7 @@ export default class ComponentManager extends SNComponentManager {
|
||||
$uiRunner,
|
||||
platform,
|
||||
environment,
|
||||
}) {
|
||||
}: ComponentManagerInstance) {
|
||||
super({
|
||||
modelManager,
|
||||
syncManager,
|
||||
@@ -64,7 +75,11 @@ export default class ComponentManager extends SNComponentManager {
|
||||
@param {object} dialog: {permissions, String, component, callback}
|
||||
*/
|
||||
|
||||
presentPermissionsDialog(dialog) {
|
||||
presentPermissionsDialog(dialog: {
|
||||
component: { name: any };
|
||||
permissionsString: any;
|
||||
callback: (arg0: boolean) => void;
|
||||
}) {
|
||||
let text = `${dialog.component.name} would like to interact with your ${dialog.permissionsString}`;
|
||||
this.alertManager.confirm({
|
||||
title: 'Grant Permissions',
|
||||
@@ -89,12 +104,20 @@ export default class ComponentManager extends SNComponentManager {
|
||||
}
|
||||
|
||||
getDefaultEditor() {
|
||||
return this.getEditors().filter(e => {
|
||||
return e.content.isMobileDefault;
|
||||
})[0];
|
||||
return this.getEditors().filter(
|
||||
(e: { content: { isMobileDefault: any } }) => {
|
||||
return e.content.isMobileDefault;
|
||||
}
|
||||
)[0];
|
||||
}
|
||||
|
||||
setEditorAsMobileDefault(editor, isDefault) {
|
||||
setEditorAsMobileDefault(
|
||||
editor: {
|
||||
content: { isMobileDefault: any };
|
||||
setDirty?: (arg0: boolean) => void;
|
||||
},
|
||||
isDefault: boolean
|
||||
) {
|
||||
if (isDefault) {
|
||||
// Remove current default
|
||||
const currentDefault = this.getDefaultEditor();
|
||||
@@ -107,17 +130,28 @@ export default class ComponentManager extends SNComponentManager {
|
||||
// Could be null if plain editor
|
||||
if (editor) {
|
||||
editor.content.isMobileDefault = isDefault;
|
||||
editor.setDirty(true);
|
||||
editor.setDirty && editor.setDirty(true);
|
||||
}
|
||||
Sync.get().sync();
|
||||
}
|
||||
|
||||
associateEditorWithNote(editor, note) {
|
||||
associateEditorWithNote(
|
||||
editor: {
|
||||
disassociatedItemIds: any[];
|
||||
associatedItemIds: any[];
|
||||
setDirty: (arg0: boolean) => void;
|
||||
} | null,
|
||||
note: {
|
||||
uuid: any;
|
||||
content: { mobilePrefersPlainEditor: boolean };
|
||||
setDirty: (arg0: boolean) => void;
|
||||
}
|
||||
) {
|
||||
const currentEditor = this.editorForNote(note);
|
||||
if (currentEditor && currentEditor !== editor) {
|
||||
// Disassociate currentEditor with note
|
||||
currentEditor.associatedItemIds = currentEditor.associatedItemIds.filter(
|
||||
id => {
|
||||
(id: any) => {
|
||||
return id !== note.uuid;
|
||||
}
|
||||
);
|
||||
@@ -155,7 +189,11 @@ export default class ComponentManager extends SNComponentManager {
|
||||
Sync.get().sync();
|
||||
}
|
||||
|
||||
clearEditorForNote(note) {
|
||||
clearEditorForNote(note: {
|
||||
uuid: any;
|
||||
content: { mobilePrefersPlainEditor: boolean };
|
||||
setDirty: (arg0: boolean) => void;
|
||||
}) {
|
||||
this.associateEditorWithNote(null, note);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,56 @@ import AlertManager from '@Lib/snjs/alertManager';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
|
||||
export type EventType =
|
||||
| typeof ItemActionManager.DeleteEvent
|
||||
| typeof ItemActionManager.TrashEvent
|
||||
| typeof ItemActionManager.RestoreEvent
|
||||
| typeof ItemActionManager.EmptyTrashEvent
|
||||
| typeof ItemActionManager.PinEvent
|
||||
| typeof ItemActionManager.UnpinEvent
|
||||
| typeof ItemActionManager.ArchiveEvent
|
||||
| typeof ItemActionManager.UnarchiveEvent
|
||||
| typeof ItemActionManager.LockEvent
|
||||
| typeof ItemActionManager.UnlockEvent
|
||||
| typeof ItemActionManager.ProtectEvent
|
||||
| typeof ItemActionManager.UnprotectEvent
|
||||
| typeof ItemActionManager.ShareEvent;
|
||||
|
||||
export default class ItemActionManager {
|
||||
static DeleteEvent = 'DeleteEvent';
|
||||
static TrashEvent = 'TrashEvent';
|
||||
static RestoreEvent = 'RestoreEvent';
|
||||
static EmptyTrashEvent = 'EmptyTrashEvent';
|
||||
static DeleteEvent = 'DeleteEvent' as 'DeleteEvent';
|
||||
static TrashEvent = 'TrashEvent' as 'TrashEvent';
|
||||
static RestoreEvent = 'RestoreEvent' as 'RestoreEvent';
|
||||
static EmptyTrashEvent = 'EmptyTrashEvent' as 'EmptyTrashEvent';
|
||||
|
||||
static PinEvent = 'PinEvent';
|
||||
static UnpinEvent = 'UnpinEvent';
|
||||
static PinEvent = 'PinEvent' as 'PinEvent';
|
||||
static UnpinEvent = 'UnpinEvent' as 'UnpinEvent';
|
||||
|
||||
static ArchiveEvent = 'ArchiveEvent';
|
||||
static UnarchiveEvent = 'UnarchiveEvent';
|
||||
static ArchiveEvent = 'ArchiveEvent' as 'ArchiveEvent';
|
||||
static UnarchiveEvent = 'UnarchiveEvent' as 'UnarchiveEvent';
|
||||
|
||||
static LockEvent = 'LockEvent';
|
||||
static UnlockEvent = 'UnlockEvent';
|
||||
static LockEvent = 'LockEvent' as 'LockEvent';
|
||||
static UnlockEvent = 'UnlockEvent' as 'UnlockEvent';
|
||||
|
||||
static ProtectEvent = 'ProtectEvent';
|
||||
static UnprotectEvent = 'UnprotectEvent';
|
||||
static ProtectEvent = 'ProtectEvent' as 'ProtectEvent';
|
||||
static UnprotectEvent = 'UnprotectEvent' as 'UnprotectEvent';
|
||||
|
||||
static ShareEvent = 'ShareEvent';
|
||||
static ShareEvent = 'ShareEvent' as 'ShareEvent';
|
||||
|
||||
/* The afterConfirmCallback is called after user confirms deletion pop up */
|
||||
|
||||
static handleEvent(event, item, callback, afterConfirmCallback) {
|
||||
static handleEvent(
|
||||
event: EventType,
|
||||
item: {
|
||||
displayName: string;
|
||||
content: { trashed: boolean; protected: boolean };
|
||||
setDirty: (arg0: boolean) => void;
|
||||
setAppDataItem: (arg0: string, arg1: boolean) => void;
|
||||
title: any;
|
||||
text: any;
|
||||
},
|
||||
callback: { (): any },
|
||||
afterConfirmCallback?: () => void
|
||||
) {
|
||||
if (event === this.TrashEvent) {
|
||||
const title = 'Move to Trash';
|
||||
const message = `Are you sure you want to move this ${item.displayName.toLowerCase()} to the trash?`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as RCTKeychain from 'react-native-keychain';
|
||||
|
||||
export default class Keychain {
|
||||
static async setKeys(keys) {
|
||||
static async setKeys(keys: string) {
|
||||
const options = {
|
||||
/* iOS only */
|
||||
accessible: RCTKeychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform } from 'react-native';
|
||||
import { Platform, Alert } from 'react-native';
|
||||
import FlagSecure from 'react-native-flag-secure-android';
|
||||
import FingerprintScanner from 'react-native-fingerprint-scanner';
|
||||
import SNReactNative from 'standard-notes-rn';
|
||||
@@ -16,11 +16,42 @@ const BiometricsPrefs = 'biometrics_prefs';
|
||||
const FirstRunKey = 'first_run';
|
||||
const StorageEncryptionKey = 'storage_encryption';
|
||||
|
||||
type AccountKeys = {
|
||||
fingerprint: {
|
||||
enabled: boolean;
|
||||
timing: any;
|
||||
};
|
||||
encryptedAccountKeys: any;
|
||||
mk?: string;
|
||||
ak?: string;
|
||||
} | null;
|
||||
|
||||
type User = { server?: string; email?: string; jwt?: string | null } | null;
|
||||
|
||||
export type BiometricsType =
|
||||
| 'Fingerprint'
|
||||
| 'Face ID'
|
||||
| 'Biometrics'
|
||||
| 'Touch ID';
|
||||
|
||||
export default class KeysManager {
|
||||
static instance = null;
|
||||
private static instance: KeysManager;
|
||||
passcodeTiming: string | null = null;
|
||||
biometricPrefs: any;
|
||||
accountRelatedStorageKeys: string[];
|
||||
legacy_fingerprint: any;
|
||||
encryptedAccountKeys: any;
|
||||
accountKeys: AccountKeys = null;
|
||||
loadInitialDataPromise: Promise<void | unknown> | undefined;
|
||||
user: User = null;
|
||||
missingFirstRunKey: boolean = false;
|
||||
accountAuthParams: any;
|
||||
offlineAuthParams: any;
|
||||
_storageEncryptionEnabled: boolean = false;
|
||||
offlineKeys: any;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new KeysManager();
|
||||
}
|
||||
|
||||
@@ -73,6 +104,7 @@ export default class KeysManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.user = this.user ? this.user : {};
|
||||
this.user.jwt = null;
|
||||
this.activeKeys().jwt = jwt;
|
||||
await this.saveUser(this.user);
|
||||
@@ -233,13 +265,19 @@ export default class KeysManager {
|
||||
clear internal keys, like first_run. (If you accidentally delete the first_run key when you sign out,
|
||||
then the next time you sign in and refresh, it will treat it as a new run, and delete all data.)
|
||||
*/
|
||||
registerAccountRelatedStorageKeys(storageKeys) {
|
||||
registerAccountRelatedStorageKeys(storageKeys: ConcatArray<string>) {
|
||||
this.accountRelatedStorageKeys = _.uniq(
|
||||
this.accountRelatedStorageKeys.concat(storageKeys)
|
||||
);
|
||||
}
|
||||
|
||||
parseKeychainValue(keys) {
|
||||
parseKeychainValue(
|
||||
keys: {
|
||||
offline: boolean;
|
||||
fingerprint: { enabled: boolean; timing: any };
|
||||
encryptedAccountKeys: any;
|
||||
} | null
|
||||
) {
|
||||
if (keys) {
|
||||
this.offlineKeys = keys.offline;
|
||||
if (this.offlineKeys) {
|
||||
@@ -274,7 +312,9 @@ export default class KeysManager {
|
||||
}
|
||||
|
||||
// what we should write to keychain
|
||||
async generateKeychainStoreValue() {
|
||||
async generateKeychainStoreValue(): Promise<
|
||||
{} | AccountKeys | { offline: { pw: any; timing: string | null } }
|
||||
> {
|
||||
let value = {};
|
||||
|
||||
// If no offline keys, store account keys directly. Otherwise we'll encrypt account keys and store in storage.
|
||||
@@ -317,6 +357,8 @@ export default class KeysManager {
|
||||
}
|
||||
|
||||
let value = await this.generateKeychainStoreValue();
|
||||
// TODO: check keychain
|
||||
// @ts-ignore don't want to change that
|
||||
return Keychain.setKeys(value);
|
||||
}
|
||||
|
||||
@@ -338,12 +380,12 @@ export default class KeysManager {
|
||||
}
|
||||
}
|
||||
|
||||
async persistAccountKeys(keys) {
|
||||
async persistAccountKeys(keys: AccountKeys) {
|
||||
this.accountKeys = keys;
|
||||
return this.persistKeys();
|
||||
}
|
||||
|
||||
async saveUser(user) {
|
||||
async saveUser(user: User) {
|
||||
this.user = user;
|
||||
return Storage.get().setItem('user', JSON.stringify(user));
|
||||
}
|
||||
@@ -416,7 +458,7 @@ export default class KeysManager {
|
||||
|
||||
// Auth Params
|
||||
|
||||
async setAccountAuthParams(authParams) {
|
||||
async setAccountAuthParams(authParams: any) {
|
||||
this.accountAuthParams = authParams;
|
||||
await Storage.get().setItem('auth_params', JSON.stringify(authParams));
|
||||
|
||||
@@ -435,12 +477,12 @@ export default class KeysManager {
|
||||
}
|
||||
}
|
||||
|
||||
async setOfflineAuthParams(authParams) {
|
||||
async setOfflineAuthParams(authParams: any) {
|
||||
this.offlineAuthParams = authParams;
|
||||
return Storage.get().setItem(OfflineParamsKey, JSON.stringify(authParams));
|
||||
}
|
||||
|
||||
defaultProtocolVersionForKeys(keys) {
|
||||
defaultProtocolVersionForKeys(keys: AccountKeys) {
|
||||
if (keys && keys.ak) {
|
||||
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams.
|
||||
return '002';
|
||||
@@ -476,8 +518,7 @@ export default class KeysManager {
|
||||
async clearOfflineKeysAndData(force = false) {
|
||||
// make sure user is authenticated before performing this step
|
||||
if (this.offlineKeys && !this.offlineKeys.mk && !force) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(
|
||||
Alert.alert(
|
||||
'Unable to remove passcode. Make sure you are properly authenticated and try again.'
|
||||
);
|
||||
return false;
|
||||
@@ -488,7 +529,7 @@ export default class KeysManager {
|
||||
return this.persistKeys();
|
||||
}
|
||||
|
||||
async persistOfflineKeys(keys) {
|
||||
async persistOfflineKeys(keys: any) {
|
||||
this.setOfflineKeys(keys);
|
||||
if (!this.passcodeTiming) {
|
||||
this.passcodeTiming = 'on-quit';
|
||||
@@ -496,14 +537,14 @@ export default class KeysManager {
|
||||
return this.persistKeys();
|
||||
}
|
||||
|
||||
async setOfflineKeys(keys) {
|
||||
async setOfflineKeys(keys: any) {
|
||||
// offline keys are ephemeral and should not be stored anywhere
|
||||
this.offlineKeys = keys;
|
||||
|
||||
// Check to see if encryptedAccountKeys need decrypting
|
||||
if (this.encryptedAccountKeys) {
|
||||
// Decrypt and set
|
||||
await SF.get().itemTransformer.decryptItem(
|
||||
await protocolManager.itemTransformer.decryptItem(
|
||||
this.encryptedAccountKeys,
|
||||
this.offlineKeys
|
||||
);
|
||||
@@ -531,12 +572,12 @@ export default class KeysManager {
|
||||
return this.biometricPrefs.enabled;
|
||||
}
|
||||
|
||||
async setPasscodeTiming(timing) {
|
||||
async setPasscodeTiming(timing: string | null) {
|
||||
this.passcodeTiming = timing;
|
||||
return this.persistKeys();
|
||||
}
|
||||
|
||||
async setBiometricsTiming(timing) {
|
||||
async setBiometricsTiming(timing: { key: any }) {
|
||||
this.biometricPrefs.timing = timing;
|
||||
return this.saveBiometricPrefs();
|
||||
}
|
||||
@@ -591,7 +632,9 @@ export default class KeysManager {
|
||||
];
|
||||
}
|
||||
|
||||
static getDeviceBiometricsAvailability(callback) {
|
||||
static getDeviceBiometricsAvailability(
|
||||
callback: (available: boolean, type?: BiometricsType, noun?: string) => void
|
||||
) {
|
||||
if (__DEV__) {
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
if (isAndroid && Platform.Version < 23) {
|
||||
@@ -605,11 +648,10 @@ export default class KeysManager {
|
||||
}
|
||||
FingerprintScanner.isSensorAvailable()
|
||||
.then(type => {
|
||||
const noun =
|
||||
type === 'Touch ID' || type === 'Fingerprint' ? 'Fingerprint' : type;
|
||||
const noun = type === 'Touch ID' ? 'Fingerprint' : type;
|
||||
callback(true, type, noun);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Platform, NativeModules } from 'react-native';
|
||||
|
||||
// moment.js
|
||||
const moment = require('moment/min/moment-with-locales.min.js');
|
||||
import moment from 'moment';
|
||||
const locale =
|
||||
Platform.OS === 'android'
|
||||
? NativeModules.I18nManager.localeIdentifier
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class ReviewManager {
|
||||
});
|
||||
}
|
||||
|
||||
static async setRunCount(runCount) {
|
||||
static async setRunCount(runCount: number) {
|
||||
return Storage.get().setItem('runCount', JSON.stringify(runCount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,63 +3,70 @@ import { Alert } from 'react-native';
|
||||
import { SFAlertManager } from 'snjs';
|
||||
|
||||
export default class AlertManager extends SFAlertManager {
|
||||
static instance = null;
|
||||
private static instance: AlertManager;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new AlertManager();
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
async alert({ title, text, closeButtonText = 'OK', onClose } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
async alert(alertdata: {
|
||||
title: string;
|
||||
text: string;
|
||||
closeButtonText?: string;
|
||||
onClose?: () => void | Promise<any>;
|
||||
}) {
|
||||
return new Promise(resolve => {
|
||||
// On iOS, confirm should go first. On Android, cancel should go first.
|
||||
let buttons = [
|
||||
{
|
||||
text: closeButtonText,
|
||||
text: alertdata.closeButtonText,
|
||||
onPress: async () => {
|
||||
onClose && (await onClose());
|
||||
alertdata.onClose && (await alertdata.onClose());
|
||||
resolve();
|
||||
},
|
||||
},
|
||||
];
|
||||
Alert.alert(title, text, buttons, { cancelable: true });
|
||||
Alert.alert(alertdata.title, alertdata.text, buttons, {
|
||||
cancelable: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async confirm({
|
||||
title,
|
||||
text,
|
||||
confirmButtonText = 'OK',
|
||||
cancelButtonText = 'Cancel',
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onDismiss,
|
||||
} = {}) {
|
||||
async confirm(confirmData: {
|
||||
title: string;
|
||||
text?: string;
|
||||
confirmButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
onConfirm?: () => void | Promise<any>;
|
||||
onCancel?: () => void | Promise<any>;
|
||||
onDismiss?: () => void | Promise<any>;
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// On iOS, confirm should go first. On Android, cancel should go first.
|
||||
let buttons = [
|
||||
{
|
||||
text: cancelButtonText,
|
||||
text: confirmData.cancelButtonText,
|
||||
onPress: async () => {
|
||||
onCancel && (await onCancel());
|
||||
confirmData.onCancel && (await confirmData.onCancel());
|
||||
reject();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: confirmButtonText,
|
||||
text: confirmData.confirmButtonText,
|
||||
onPress: async () => {
|
||||
onConfirm && (await onConfirm());
|
||||
confirmData.onConfirm && (await confirmData.onConfirm());
|
||||
resolve();
|
||||
},
|
||||
},
|
||||
];
|
||||
Alert.alert(title, text, buttons, {
|
||||
Alert.alert(confirmData.title, confirmData.text, buttons, {
|
||||
cancelable: true,
|
||||
onDismiss: async () => {
|
||||
onDismiss && (await onDismiss());
|
||||
confirmData.onDismiss && (await confirmData.onDismiss());
|
||||
reject();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,9 +7,10 @@ import Server from '@Lib/snjs/httpManager';
|
||||
import Storage from '@Lib/snjs/storageManager';
|
||||
|
||||
export default class Auth extends SFAuthManager {
|
||||
static instance = null;
|
||||
private static instance: Auth;
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
// @ts-ignore underlying Authe manager handlers 3 agruments
|
||||
this.instance = new Auth(Storage.get(), Server.get(), AlertManager.get());
|
||||
}
|
||||
return this.instance;
|
||||
@@ -37,7 +38,7 @@ export default class Auth extends SFAuthManager {
|
||||
return !keys.jwt;
|
||||
}
|
||||
|
||||
async signout(clearAllData) {
|
||||
async signout() {
|
||||
await Storage.get().clearAllModels();
|
||||
await KeysManager.get().clearAccountKeysAndData();
|
||||
this._keys = null;
|
||||
@@ -60,7 +61,13 @@ export default class Auth extends SFAuthManager {
|
||||
return KeysManager.get().activeAuthParams();
|
||||
}
|
||||
|
||||
async handleAuthResponse(response, email, url, authParams, keys) {
|
||||
async handleAuthResponse(
|
||||
response: { token: any },
|
||||
email: string,
|
||||
url: string,
|
||||
authParams: any,
|
||||
keys: any
|
||||
) {
|
||||
// We don't want to call super, as the super implementation is meant for web credentials
|
||||
// super will save keys to storage, which we don't want.
|
||||
// await super.handleAuthResponse(response, email, url, authParams, keys);
|
||||
@@ -79,7 +86,7 @@ export default class Auth extends SFAuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
async verifyAccountPassword(password) {
|
||||
async verifyAccountPassword(password: string | undefined) {
|
||||
const authParams = await this.getAuthParams();
|
||||
const keys = await protocolManager.computeEncryptionKeysForUser(
|
||||
password,
|
||||
|
||||
@@ -3,10 +3,10 @@ import KeysManager from '@Lib/keysManager';
|
||||
import { SFHttpManager } from 'snjs';
|
||||
|
||||
export default class Server extends SFHttpManager {
|
||||
static instance = null;
|
||||
private static instance: Server;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new Server();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,17 @@ import { SFModelManager, SFMigrationManager } from 'snjs';
|
||||
const base64 = require('base-64');
|
||||
|
||||
export default class MigrationManager extends SFMigrationManager {
|
||||
static instance = null;
|
||||
private static instance: MigrationManager;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new MigrationManager();
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor(modelManager, syncManager, storageManager, authManager) {
|
||||
constructor() {
|
||||
super(ModelManager.get(), Sync.get(), Storage.get(), Auth.get());
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default class MigrationManager extends SFMigrationManager {
|
||||
// The user is signed in
|
||||
Sync.get()
|
||||
.stateless_downloadAllItems(options)
|
||||
.then(async items => {
|
||||
.then(async (items: any[]) => {
|
||||
const matchingPrivs = items.filter(candidate => {
|
||||
return candidate.content_type === contentType;
|
||||
});
|
||||
@@ -90,7 +90,7 @@ export default class MigrationManager extends SFMigrationManager {
|
||||
// The user is signed in
|
||||
Sync.get()
|
||||
.stateless_downloadAllItems(options)
|
||||
.then(async items => {
|
||||
.then(async (items: any[]) => {
|
||||
let matchingTags = items.filter(candidate => {
|
||||
return candidate.content_type === contentType;
|
||||
});
|
||||
@@ -111,11 +111,11 @@ export default class MigrationManager extends SFMigrationManager {
|
||||
|
||||
/* Overrides */
|
||||
|
||||
async encode(text) {
|
||||
async encode(text: string) {
|
||||
return base64.encode(text);
|
||||
}
|
||||
|
||||
async decode(base64String) {
|
||||
async decode(base64String: string) {
|
||||
return base64.decode(base64String);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@ import {
|
||||
SNTheme,
|
||||
SNComponent,
|
||||
SNSmartTag,
|
||||
SFItem as SNJSItem,
|
||||
} from 'snjs';
|
||||
import _ from 'lodash';
|
||||
import Storage from '@Lib/snjs/storageManager';
|
||||
import '../../models/extend/item';
|
||||
import OptionsState from '@Lib/OptionsState';
|
||||
|
||||
type SFItem = typeof SNJSItem;
|
||||
|
||||
SFModelManager.ContentTypeClassMapping = {
|
||||
Note: SNNote,
|
||||
@@ -30,10 +34,10 @@ SFModelManager.ContentTypeClassMapping = {
|
||||
};
|
||||
|
||||
export default class ModelManager extends SFModelManager {
|
||||
static instance = null;
|
||||
private static instance: ModelManager;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new ModelManager();
|
||||
}
|
||||
|
||||
@@ -57,19 +61,19 @@ export default class ModelManager extends SFModelManager {
|
||||
this.themes.length = 0;
|
||||
}
|
||||
|
||||
addItems(items, globalOnly = false) {
|
||||
addItems(items: SFItem, globalOnly = false) {
|
||||
super.addItems(items, globalOnly);
|
||||
|
||||
items.forEach(item => {
|
||||
items.forEach((item: SFItem) => {
|
||||
// In some cases, you just want to add the item to this.items, and not to the individual arrays
|
||||
// This applies when you want to keep an item syncable, but not display it via the individual arrays
|
||||
if (!globalOnly) {
|
||||
if (item.content_type === 'Tag') {
|
||||
if (!_.find(this.tags, { uuid: item.uuid })) {
|
||||
this.tags.splice(
|
||||
_.sortedIndexBy(this.tags, item, function (item) {
|
||||
if (item.title) {
|
||||
return item.title.toLowerCase();
|
||||
_.sortedIndexBy(this.tags, item, function (arrayItem) {
|
||||
if (arrayItem.title) {
|
||||
return arrayItem.title.toLowerCase();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@@ -91,7 +95,7 @@ export default class ModelManager extends SFModelManager {
|
||||
});
|
||||
}
|
||||
|
||||
async removeItemLocally(item) {
|
||||
async removeItemLocally(item: SFItem) {
|
||||
await super.removeItemLocally(item);
|
||||
|
||||
if (item.content_type === 'Tag') {
|
||||
@@ -106,17 +110,17 @@ export default class ModelManager extends SFModelManager {
|
||||
}
|
||||
|
||||
noteCount() {
|
||||
return this.notes.filter(n => !n.dummy).length;
|
||||
return this.notes.filter((n: { dummy: any }) => !n.dummy).length;
|
||||
}
|
||||
|
||||
/* Be sure not to use just findItems in your views, because those won't find system smart tags */
|
||||
getTagsWithIds(ids) {
|
||||
getTagsWithIds(ids: string[]) {
|
||||
let tagMatches = ModelManager.get().findItems(ids);
|
||||
let smartMatches = this.getSmartTagsWithIds(ids);
|
||||
return tagMatches.concat(smartMatches);
|
||||
}
|
||||
|
||||
getTagWithId(id) {
|
||||
getTagWithId(id: string) {
|
||||
let tags = this.getTagsWithIds([id]);
|
||||
if (tags.length > 0) {
|
||||
return tags[0];
|
||||
@@ -132,24 +136,29 @@ export default class ModelManager extends SFModelManager {
|
||||
}
|
||||
|
||||
systemSmartTagIds() {
|
||||
return this.systemSmartTags.map(tag => {
|
||||
return this.systemSmartTags.map((tag: { uuid: any }) => {
|
||||
return tag.uuid;
|
||||
});
|
||||
}
|
||||
|
||||
getSmartTagWithId(id) {
|
||||
return this.getSmartTags().find(candidate => candidate.uuid === id);
|
||||
getSmartTagWithId(id: string) {
|
||||
return this.getSmartTags().find(
|
||||
(candidate: { uuid: string }) => candidate.uuid === id
|
||||
);
|
||||
}
|
||||
|
||||
getSmartTagsWithIds(ids) {
|
||||
return this.getSmartTags().filter(candidate =>
|
||||
getSmartTagsWithIds(ids: string[]) {
|
||||
return this.getSmartTags().filter((candidate: { uuid: string }) =>
|
||||
ids.includes(candidate.uuid)
|
||||
);
|
||||
}
|
||||
|
||||
getSmartTags() {
|
||||
const userTags = this.validItemsForContentType('SN|SmartTag').sort(
|
||||
(a, b) => {
|
||||
(
|
||||
a: { content: { title: number } },
|
||||
b: { content: { title: number } }
|
||||
) => {
|
||||
return a.content.title < b.content.title ? -1 : 1;
|
||||
}
|
||||
);
|
||||
@@ -157,7 +166,9 @@ export default class ModelManager extends SFModelManager {
|
||||
}
|
||||
|
||||
trashSmartTag() {
|
||||
return this.systemSmartTags.find(tag => tag.content.isTrashTag);
|
||||
return this.systemSmartTags.find(
|
||||
(tag: { content: { isTrashTag: any } }) => tag.content.isTrashTag
|
||||
);
|
||||
}
|
||||
|
||||
trashedItems() {
|
||||
@@ -171,7 +182,7 @@ export default class ModelManager extends SFModelManager {
|
||||
}
|
||||
}
|
||||
|
||||
notesMatchingSmartTag(tag) {
|
||||
notesMatchingSmartTag(tag: { content: { predicate: any; isTrashTag: any } }) {
|
||||
const contentTypePredicate = new SFPredicate('content_type', '=', 'Note');
|
||||
const predicates = [contentTypePredicate, tag.content.predicate];
|
||||
if (!tag.content.isTrashTag) {
|
||||
@@ -185,10 +196,10 @@ export default class ModelManager extends SFModelManager {
|
||||
return this.itemsMatchingPredicates(predicates);
|
||||
}
|
||||
|
||||
getNotes(options = {}) {
|
||||
getNotes(options: OptionsState) {
|
||||
let notes,
|
||||
tags = [],
|
||||
selectedSmartTag;
|
||||
selectedSmartTag: { content: any };
|
||||
// if (options.selectedTagIds && options.selectedTagIds.length > 0 && options.selectedTagIds[0].key !== "all") {
|
||||
let selectedTagIds = options.selectedTagIds;
|
||||
if (selectedTagIds && selectedTagIds.length > 0) {
|
||||
@@ -216,10 +227,27 @@ export default class ModelManager extends SFModelManager {
|
||||
let searchTerm = options.searchTerm;
|
||||
if (searchTerm) {
|
||||
searchTerm = searchTerm.toLowerCase();
|
||||
notes = notes.filter(function (note) {
|
||||
notes = notes.filter(function (note: {
|
||||
safeTitle: () => {
|
||||
(): any;
|
||||
new (): any;
|
||||
toLowerCase: { (): (string | undefined)[]; new (): any };
|
||||
};
|
||||
safeText: () => {
|
||||
(): any;
|
||||
new (): any;
|
||||
toLowerCase: { (): (string | undefined)[]; new (): any };
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
note.safeTitle().toLowerCase().includes(searchTerm) ||
|
||||
note.safeText().toLowerCase().includes(searchTerm)
|
||||
note
|
||||
.safeTitle()
|
||||
.toLowerCase()
|
||||
.includes(searchTerm ?? '') ||
|
||||
note
|
||||
.safeText()
|
||||
.toLowerCase()
|
||||
.includes(searchTerm ?? '')
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -227,27 +255,40 @@ export default class ModelManager extends SFModelManager {
|
||||
const sortBy = options.sortBy;
|
||||
const sortReverse = options.sortReverse;
|
||||
|
||||
notes = notes.filter(note => {
|
||||
if (note.deleted || note.dummy) {
|
||||
return false;
|
||||
notes = notes.filter(
|
||||
(note: {
|
||||
deleted: any;
|
||||
dummy: any;
|
||||
content: { trashed: any };
|
||||
archived: any;
|
||||
}) => {
|
||||
if (note.deleted || note.dummy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTrash = selectedSmartTag && selectedSmartTag.content.isTrashTag;
|
||||
const canShowArchived =
|
||||
(selectedSmartTag && selectedSmartTag.content.isArchiveTag) ||
|
||||
isTrash;
|
||||
|
||||
if (!isTrash && note.content.trashed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.archived && !canShowArchived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const isTrash = selectedSmartTag && selectedSmartTag.content.isTrashTag;
|
||||
const canShowArchived =
|
||||
(selectedSmartTag && selectedSmartTag.content.isArchiveTag) || isTrash;
|
||||
|
||||
if (!isTrash && note.content.trashed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (note.archived && !canShowArchived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const sortValueFn = (a, b, pinCheck = false) => {
|
||||
// @ts-ignore function invokes itself
|
||||
const sortValueFn = (
|
||||
a: { [x: string]: string; pinned: any },
|
||||
b: { [x: string]: string; pinned: any },
|
||||
pinCheck = false
|
||||
) => {
|
||||
if (!pinCheck) {
|
||||
if (a.pinned && b.pinned) {
|
||||
return sortValueFn(a, b, true);
|
||||
@@ -259,9 +300,12 @@ export default class ModelManager extends SFModelManager {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
let aValue = a[sortBy] || '';
|
||||
let bValue = b[sortBy] || '';
|
||||
let aValue = '';
|
||||
let bValue = '';
|
||||
if (sortBy) {
|
||||
aValue = a[sortBy] || '';
|
||||
bValue = b[sortBy] || '';
|
||||
}
|
||||
|
||||
let vector = 1;
|
||||
|
||||
@@ -292,7 +336,10 @@ export default class ModelManager extends SFModelManager {
|
||||
return 0;
|
||||
};
|
||||
|
||||
notes = notes.sort(function (a, b) {
|
||||
notes = notes.sort(function (
|
||||
a: { [x: string]: string; pinned: any },
|
||||
b: { [x: string]: string; pinned: any }
|
||||
) {
|
||||
return sortValueFn(a, b);
|
||||
});
|
||||
|
||||
@@ -302,8 +349,8 @@ export default class ModelManager extends SFModelManager {
|
||||
/*
|
||||
Misc
|
||||
*/
|
||||
|
||||
humanReadableDisplayForContentType(contentType) {
|
||||
humanReadableDisplayForContentType(contentType: string) {
|
||||
// @ts-ignore cannot index this object trough uknown value
|
||||
return {
|
||||
Note: 'note',
|
||||
Tag: 'tag',
|
||||
|
||||
@@ -13,10 +13,10 @@ import { ICON_CLOSE } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class PrivilegesManager extends SFPrivilegesManager {
|
||||
static instance = null;
|
||||
private static instance: PrivilegesManager;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
let singletonManager = new SFSingletonManager(
|
||||
ModelManager.get(),
|
||||
Sync.get()
|
||||
@@ -31,7 +31,11 @@ export default class PrivilegesManager extends SFPrivilegesManager {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor(modelManager, syncManager, singletonManager) {
|
||||
constructor(
|
||||
modelManager: ModelManager,
|
||||
syncManager: Sync,
|
||||
singletonManager: any
|
||||
) {
|
||||
super(modelManager, syncManager, singletonManager);
|
||||
|
||||
this.setDelegate({
|
||||
@@ -43,16 +47,34 @@ export default class PrivilegesManager extends SFPrivilegesManager {
|
||||
const hasBiometrics = KeysManager.get().hasBiometrics();
|
||||
return hasPasscode || hasBiometrics;
|
||||
},
|
||||
saveToStorage: async (key, value) => {
|
||||
saveToStorage: async (key: string, value: string | null | undefined) => {
|
||||
return Storage.get().setItem(key, value);
|
||||
},
|
||||
getFromStorage: async key => {
|
||||
getFromStorage: async (key: string) => {
|
||||
return Storage.get().getItem(key);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async presentPrivilegesModal(action, navigation, onSuccess, onCancel) {
|
||||
async presentPrivilegesModal(
|
||||
action: any,
|
||||
navigation: {
|
||||
navigate: (
|
||||
arg0: string,
|
||||
arg1: {
|
||||
leftButton: { title: string | null; iconName: string | null };
|
||||
authenticationSources: any[];
|
||||
hasCancelOption: boolean;
|
||||
sessionLengthOptions: any;
|
||||
selectedSessionLength: any;
|
||||
onSuccess: (selectedSessionLength: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
) => void;
|
||||
},
|
||||
onSuccess: { (): void; (): any },
|
||||
onCancel?: () => any
|
||||
) {
|
||||
if (this.authenticationInProgress()) {
|
||||
onCancel && onCancel();
|
||||
return;
|
||||
@@ -83,7 +105,7 @@ export default class PrivilegesManager extends SFPrivilegesManager {
|
||||
hasCancelOption: true,
|
||||
sessionLengthOptions: sessionLengthOptions,
|
||||
selectedSessionLength: selectedSessionLength,
|
||||
onSuccess: selectedSessionLength => {
|
||||
onSuccess: (selectedSessionLength: any) => {
|
||||
this.setSessionLength(selectedSessionLength);
|
||||
customSuccess();
|
||||
},
|
||||
@@ -99,8 +121,8 @@ export default class PrivilegesManager extends SFPrivilegesManager {
|
||||
return this.authInProgress;
|
||||
}
|
||||
|
||||
async sourcesForAction(action) {
|
||||
const sourcesForCredential = credential => {
|
||||
async sourcesForAction(action: any) {
|
||||
const sourcesForCredential = (credential: any) => {
|
||||
if (credential === SFPrivilegesManager.CredentialAccountPassword) {
|
||||
return [new AuthenticationSourceAccountPassword()];
|
||||
} else if (credential === SFPrivilegesManager.CredentialLocalPasscode) {
|
||||
@@ -118,7 +140,7 @@ export default class PrivilegesManager extends SFPrivilegesManager {
|
||||
};
|
||||
|
||||
const credentials = await this.netCredentialsForAction(action);
|
||||
let sources = [];
|
||||
let sources: any[] = [];
|
||||
for (const credential of credentials) {
|
||||
sources = sources
|
||||
.concat(sourcesForCredential(credential))
|
||||
@@ -130,7 +152,7 @@ export default class PrivilegesManager extends SFPrivilegesManager {
|
||||
return sources;
|
||||
}
|
||||
|
||||
async grossCredentialsForAction(action) {
|
||||
async grossCredentialsForAction(action: any) {
|
||||
const privs = await this.getPrivileges();
|
||||
const creds = privs.getCredentialsForAction(action);
|
||||
return creds;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Platform } from 'react-native';
|
||||
import { SFStorageManager } from 'snjs';
|
||||
import { SFStorageManager, SFItem as SNJSItem } from 'snjs';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import AlertManager from '@Lib/snjs/alertManager';
|
||||
import { isNullOrUndefined } from '@Lib/utils';
|
||||
|
||||
type SFItem = typeof SNJSItem;
|
||||
|
||||
export default class Storage extends SFStorageManager {
|
||||
static instance = null;
|
||||
private static instance: Storage;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new Storage();
|
||||
}
|
||||
|
||||
@@ -21,7 +23,7 @@ export default class Storage extends SFStorageManager {
|
||||
this.platformString = this.isAndroid ? 'Android' : 'iOS';
|
||||
}
|
||||
|
||||
async getItem(key) {
|
||||
async getItem(key: string) {
|
||||
try {
|
||||
return AsyncStorage.getItem(key);
|
||||
} catch (error) {
|
||||
@@ -30,10 +32,10 @@ export default class Storage extends SFStorageManager {
|
||||
}
|
||||
}
|
||||
|
||||
async getMultiItems(keys) {
|
||||
async getMultiItems(keys: string[]) {
|
||||
return AsyncStorage.multiGet(keys).then(stores => {
|
||||
var items = {};
|
||||
stores.map((result, i, store) => {
|
||||
const items: Record<string, any> = {};
|
||||
stores.map((_result, i, store) => {
|
||||
let key = store[i][0];
|
||||
let value = store[i][1];
|
||||
items[key] = value;
|
||||
@@ -42,8 +44,8 @@ export default class Storage extends SFStorageManager {
|
||||
});
|
||||
}
|
||||
|
||||
async setItem(key, value) {
|
||||
if (isNullOrUndefined(value) || isNullOrUndefined(key)) {
|
||||
async setItem(key: string, value: string | undefined | null) {
|
||||
if (value === null || value === undefined || isNullOrUndefined(key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -54,11 +56,11 @@ export default class Storage extends SFStorageManager {
|
||||
}
|
||||
}
|
||||
|
||||
async removeItem(key) {
|
||||
async removeItem(key: string) {
|
||||
return AsyncStorage.removeItem(key);
|
||||
}
|
||||
|
||||
async clearKeys(keys) {
|
||||
async clearKeys(keys: string[]) {
|
||||
return AsyncStorage.multiRemove(keys);
|
||||
}
|
||||
|
||||
@@ -68,10 +70,10 @@ export default class Storage extends SFStorageManager {
|
||||
|
||||
// Models
|
||||
async getAllModels() {
|
||||
const itemsFromStores = stores => {
|
||||
const items = [];
|
||||
stores.map((result, i, store) => {
|
||||
const key = store[i][0];
|
||||
const itemsFromStores = (stores: any[]) => {
|
||||
const items: any[] = [];
|
||||
stores.map((_result, i, store) => {
|
||||
// const key = store[i][0];
|
||||
const value = store[i][1];
|
||||
if (value) {
|
||||
items.push(JSON.parse(value));
|
||||
@@ -124,7 +126,7 @@ export default class Storage extends SFStorageManager {
|
||||
*/
|
||||
|
||||
const keys = await this.getAllModelKeys();
|
||||
let items = [];
|
||||
let items: any[] = [];
|
||||
const failedItemIds = [];
|
||||
if (this.isAndroid) {
|
||||
for (const key of keys) {
|
||||
@@ -155,7 +157,7 @@ export default class Storage extends SFStorageManager {
|
||||
return items;
|
||||
}
|
||||
|
||||
showLoadFailForItemIds(failedItemIds) {
|
||||
showLoadFailForItemIds(failedItemIds: string[]) {
|
||||
let text = `The following items could not be loaded. This may happen if you are in low-memory conditions, or if the note is very large in size. For compatibility with ${this.platformString}, we recommend breaking up large notes into smaller chunks using the desktop or web app.\n\nItems:\n`;
|
||||
let index = 0;
|
||||
text += failedItemIds.map(id => {
|
||||
@@ -169,7 +171,7 @@ export default class Storage extends SFStorageManager {
|
||||
AlertManager.get().alert({ title: 'Unable to load item', text: text });
|
||||
}
|
||||
|
||||
keyForItem(item) {
|
||||
keyForItem(item: SFItem) {
|
||||
return 'Item-' + item.uuid;
|
||||
}
|
||||
|
||||
@@ -181,7 +183,9 @@ export default class Storage extends SFStorageManager {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
async saveModel(item) {
|
||||
// TODO: Not sure about this
|
||||
// @ts-ignore
|
||||
async saveModel(item: SFItem) {
|
||||
return this.saveModel([item]);
|
||||
}
|
||||
|
||||
@@ -190,7 +194,7 @@ export default class Storage extends SFStorageManager {
|
||||
AsyncStorage.multiSet(data, function(error){ callback(); })
|
||||
Each item is saved individually.
|
||||
*/
|
||||
async saveModels(items) {
|
||||
async saveModels(items?: SFItem[]) {
|
||||
if (!items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -205,11 +209,11 @@ export default class Storage extends SFStorageManager {
|
||||
);
|
||||
}
|
||||
|
||||
async deleteModel(item) {
|
||||
async deleteModel(item: SFItem) {
|
||||
return AsyncStorage.removeItem(this.keyForItem(item));
|
||||
}
|
||||
|
||||
async clearAllModels(callback) {
|
||||
async clearAllModels() {
|
||||
const itemKeys = await this.getAllModelKeys();
|
||||
return AsyncStorage.multiRemove(itemKeys);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import ModelManager from '@Lib/snjs/modelManager';
|
||||
import Storage from '@Lib/snjs/storageManager';
|
||||
|
||||
export default class Sync extends SFSyncManager {
|
||||
static instance = null;
|
||||
private static instance: Sync;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new Sync();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default class Sync extends SFSyncManager {
|
||||
'cursorToken',
|
||||
]);
|
||||
|
||||
this.setKeyRequestHandler(request => {
|
||||
this.setKeyRequestHandler((request: any) => {
|
||||
let keys;
|
||||
if (
|
||||
request === SFSyncManager.KeyRequestLoadSaveAccount ||
|
||||
|
||||
@@ -6,9 +6,10 @@ export const DONT_SHOW_AGAIN_UNSUPPORTED_EDITORS_KEY =
|
||||
'DoNotShowAgainUnsupportedEditorsKey';
|
||||
|
||||
export default class UserPrefsManager {
|
||||
static instance = null;
|
||||
private static instance: UserPrefsManager;
|
||||
data: Record<string, any>;
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new UserPrefsManager();
|
||||
}
|
||||
return this.instance;
|
||||
@@ -18,37 +19,39 @@ export default class UserPrefsManager {
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
async clearPref({ key }) {
|
||||
async clearPref({ key }: { key: string }) {
|
||||
this.data[key] = null;
|
||||
return Storage.get().clearKeys([key]);
|
||||
}
|
||||
|
||||
async setPref({ key, value }) {
|
||||
async setPref({ key, value }: { key: string; value: unknown }) {
|
||||
await Storage.get().setItem(key, JSON.stringify(value));
|
||||
this.data[key] = value;
|
||||
}
|
||||
|
||||
async getPref({ key }) {
|
||||
async getPref({ key }: { key: string }) {
|
||||
if (isNullOrUndefined(this.data[key])) {
|
||||
this.data[key] = JSON.parse(await Storage.get().getItem(key));
|
||||
const item = await Storage.get().getItem(key);
|
||||
this.data[key] = JSON.parse(item ?? '');
|
||||
}
|
||||
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
async getPrefAsDate({ key }) {
|
||||
async getPrefAsDate({ key }: { key: string }) {
|
||||
if (isNullOrUndefined(this.data[key])) {
|
||||
this.data[key] = dateFromJsonString(await Storage.get().getItem(key));
|
||||
const item = await Storage.get().getItem(key);
|
||||
this.data[key] = dateFromJsonString(item ?? '');
|
||||
}
|
||||
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
async isPrefSet({ key }) {
|
||||
async isPrefSet({ key }: { key: string }) {
|
||||
return (await this.getPref({ key: key })) !== null;
|
||||
}
|
||||
|
||||
async isPrefEqualTo({ key, value }) {
|
||||
async isPrefEqualTo({ key, value }: { key: string; value: unknown }) {
|
||||
return (await this.getPref({ key: key })) === value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
export function isNullOrUndefined(value) {
|
||||
export function isNullOrUndefined(value: unknown) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with non-alphanumeric characters stripped out
|
||||
*/
|
||||
export function stripNonAlphanumeric(str) {
|
||||
export function stripNonAlphanumeric(str: string) {
|
||||
return str.replace(/\W/g, '');
|
||||
}
|
||||
|
||||
export function isMatchCaseInsensitive(a, b) {
|
||||
export function isMatchCaseInsensitive(a: string, b: string) {
|
||||
return a.toLowerCase() === b.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Date object from a JSON stringifed date
|
||||
*/
|
||||
export function dateFromJsonString(str) {
|
||||
export function dateFromJsonString(str: string) {
|
||||
if (str) {
|
||||
return new Date(JSON.parse(str));
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Platform } from 'react-native';
|
||||
import { Platform, ViewStyle } from 'react-native';
|
||||
|
||||
export default class PlatformStyles {
|
||||
constructor(styles) {
|
||||
styles: ViewStyle | any;
|
||||
constructor(styles: ViewStyle | any) {
|
||||
this.styles = styles;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
get(key: any) {
|
||||
const rules = this.styles;
|
||||
const styles = [rules[key]];
|
||||
const platform = Platform.OS === 'android' ? 'Android' : 'IOS';
|
||||
|
||||
// TODO: should be removed
|
||||
// @ts-ignore
|
||||
const platformRules = rules[key + platform];
|
||||
if (platformRules) {
|
||||
styles.push(platformRules);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SFItem } from 'snjs';
|
||||
// to override all individual classes, like Note and Tag.
|
||||
|
||||
const original_updateFromJSON = SFItem.prototype.updateFromJSON;
|
||||
SFItem.prototype.updateFromJSON = function (json) {
|
||||
SFItem.prototype.updateFromJSON = function (json: unknown) {
|
||||
original_updateFromJSON.apply(this, [json]);
|
||||
|
||||
if (this.created_at) {
|
||||
@@ -18,7 +18,7 @@ SFItem.prototype.updateFromJSON = function (json) {
|
||||
}
|
||||
};
|
||||
|
||||
SFItem.prototype.dateToLocalizedString = function (date) {
|
||||
SFItem.prototype.dateToLocalizedString = function (date: string) {
|
||||
return moment(date).format('llll');
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import HeaderButtons, {
|
||||
HeaderButton,
|
||||
Item,
|
||||
HeaderButtonProps,
|
||||
} from 'react-navigation-header-buttons';
|
||||
import _ from 'lodash';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import HeaderTitleView from '@Components/HeaderTitleView';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import ApplicationState, { AppStateType } from '@Lib/ApplicationState';
|
||||
import PrivilegesManager from '@Lib/snjs/privilegesManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
const IoniconsHeaderButton = passMeFurther => (
|
||||
const IoniconsHeaderButton = (passMeFurther: HeaderButtonProps) => (
|
||||
// the `passMeFurther` variable here contains props from <Item .../> as well as <HeaderButtons ... />
|
||||
// and it is important to pass those props to `HeaderButton`
|
||||
// then you may add some information like icon size or color (if you use icons)
|
||||
@@ -23,11 +23,33 @@ const IoniconsHeaderButton = passMeFurther => (
|
||||
/>
|
||||
);
|
||||
|
||||
export default class Abstract extends ThemedComponent {
|
||||
export type AbstractProps = {
|
||||
navigation: any;
|
||||
};
|
||||
|
||||
export type AbstractState = {
|
||||
lockContent?: boolean;
|
||||
};
|
||||
|
||||
export default class Abstract<
|
||||
TProps extends AbstractProps = AbstractProps,
|
||||
TState extends AbstractState = AbstractState
|
||||
> extends ThemedComponent<TProps, TState> {
|
||||
static getDefaultNavigationOptions = ({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
_navigationOptions,
|
||||
templateOptions,
|
||||
}: {
|
||||
navigation: {
|
||||
getParam: (arg0: string) => string | undefined;
|
||||
};
|
||||
_navigationOptions: any;
|
||||
templateOptions?: {
|
||||
title?: any;
|
||||
subtitle?: any;
|
||||
leftButton?: any;
|
||||
rightButton?: any;
|
||||
};
|
||||
}) => {
|
||||
// templateOptions allow subclasses to specifiy things they want to display in nav bar before it actually loads.
|
||||
// this way, things like title and the Done button in the top left are visible during transition
|
||||
@@ -56,7 +78,7 @@ export default class Abstract extends ThemedComponent {
|
||||
if (leftButton) {
|
||||
headerLeft = (
|
||||
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
|
||||
<Item
|
||||
<HeaderButtons.Item
|
||||
testID="headerButton"
|
||||
disabled={leftButton.disabled}
|
||||
title={leftButton.title}
|
||||
@@ -66,6 +88,7 @@ export default class Abstract extends ThemedComponent {
|
||||
</HeaderButtons>
|
||||
);
|
||||
|
||||
// @ts-ignore setting a property on navigation object
|
||||
options.headerLeft = headerLeft;
|
||||
}
|
||||
|
||||
@@ -74,7 +97,7 @@ export default class Abstract extends ThemedComponent {
|
||||
if (rightButton) {
|
||||
headerRight = (
|
||||
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
|
||||
<Item
|
||||
<HeaderButtons.Item
|
||||
disabled={rightButton.disabled}
|
||||
title={rightButton.title}
|
||||
iconName={rightButton.iconName}
|
||||
@@ -83,35 +106,51 @@ export default class Abstract extends ThemedComponent {
|
||||
</HeaderButtons>
|
||||
);
|
||||
|
||||
// @ts-ignore setting a property on navigation object
|
||||
options.headerRight = headerRight;
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
static navigationOptions = (navigationProps: {
|
||||
navigation: any;
|
||||
navigationOptions: any;
|
||||
}) => {
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
navigation: navigationProps.navigation,
|
||||
_navigationOptions: navigationProps.navigationOptions,
|
||||
});
|
||||
};
|
||||
listeners: any[];
|
||||
_stateObserver: {
|
||||
key: () => number;
|
||||
callback: (state: AppStateType) => void;
|
||||
};
|
||||
willUnmount: boolean = false;
|
||||
mounted: boolean = false;
|
||||
loadedInitialState: boolean = false;
|
||||
_renderOnMount?: boolean;
|
||||
_renderOnMountCallback: (() => void) | null = null;
|
||||
willBeVisible: boolean = false;
|
||||
visible: boolean = false;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<TProps>) {
|
||||
super(props);
|
||||
|
||||
this.state = { lockContent: true };
|
||||
this.state = { lockContent: true } as TState;
|
||||
|
||||
this.listeners = [
|
||||
this.props.navigation.addListener('willFocus', payload => {
|
||||
this.props.navigation.addListener('willFocus', () => {
|
||||
this.componentWillFocus();
|
||||
}),
|
||||
this.props.navigation.addListener('didFocus', payload => {
|
||||
this.props.navigation.addListener('didFocus', () => {
|
||||
this.componentDidFocus();
|
||||
}),
|
||||
this.props.navigation.addListener('willBlur', payload => {
|
||||
this.props.navigation.addListener('willBlur', () => {
|
||||
this.componentWillBlur();
|
||||
}),
|
||||
this.props.navigation.addListener('didBlur', payload => {
|
||||
this.props.navigation.addListener('didBlur', () => {
|
||||
this.componentDidBlur();
|
||||
}),
|
||||
];
|
||||
@@ -131,9 +170,9 @@ export default class Abstract extends ThemedComponent {
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
shouldComponentUpdate(nextProps: TProps, nextState: TState) {
|
||||
let isSame =
|
||||
Abstract.IsDeepEqual(nextProps, this.props, null, ['navigation']) &&
|
||||
Abstract.IsDeepEqual(nextProps, this.props, [], ['navigation']) &&
|
||||
Abstract.IsDeepEqual(nextState, this.state);
|
||||
return !isSame;
|
||||
}
|
||||
@@ -203,21 +242,21 @@ export default class Abstract extends ThemedComponent {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
getProp = prop => {
|
||||
getProp = (prop: any) => {
|
||||
// this.props.navigation could be undefined if we're in the drawer
|
||||
return (
|
||||
this.props.navigation.getParam && this.props.navigation.getParam(prop)
|
||||
);
|
||||
};
|
||||
|
||||
setTitle(title) {
|
||||
let options = {};
|
||||
setTitle(title: string) {
|
||||
let options: { title?: string } = {};
|
||||
options.title = title;
|
||||
this.props.navigation.setParams(options);
|
||||
}
|
||||
|
||||
setSubTitle(subtitle, color) {
|
||||
let options = {};
|
||||
setSubTitle(subtitle: string | null, color?: string) {
|
||||
let options: { subtitle?: string | null; subtitleColor?: string } = {};
|
||||
options.subtitle = subtitle;
|
||||
options.subtitleColor = color;
|
||||
this.props.navigation.setParams(options);
|
||||
@@ -225,10 +264,10 @@ export default class Abstract extends ThemedComponent {
|
||||
|
||||
lockContent() {
|
||||
this.mergeState({ lockContent: true });
|
||||
this.configureNavBar();
|
||||
this.configureNavBar(false);
|
||||
}
|
||||
|
||||
unlockContent(callback) {
|
||||
unlockContent(callback?: { (): void }) {
|
||||
if (!this.loadedInitialState) {
|
||||
this.loadInitialState();
|
||||
}
|
||||
@@ -237,14 +276,22 @@ export default class Abstract extends ThemedComponent {
|
||||
});
|
||||
}
|
||||
|
||||
constructState(state) {
|
||||
constructState(state: { title?: any; noteLocked?: boolean; text?: any }) {
|
||||
this.state = _.merge(
|
||||
{ lockContent: ApplicationState.get().isLocked() },
|
||||
state
|
||||
);
|
||||
) as TState;
|
||||
}
|
||||
|
||||
mergeState(state) {
|
||||
mergeState(
|
||||
state:
|
||||
| {}
|
||||
| ((
|
||||
prevState: Readonly<TState>,
|
||||
props: Readonly<TProps>
|
||||
) => {} | Pick<{}, never> | null)
|
||||
| null
|
||||
) {
|
||||
/*
|
||||
We're getting rid of the original implementation of this, which was to pass a function into set state.
|
||||
The reason was, when compared new and previous values in componentShouldUpdate, if we used the function approach,
|
||||
@@ -257,7 +304,7 @@ export default class Abstract extends ThemedComponent {
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
renderOnMount(callback) {
|
||||
renderOnMount(callback: () => any) {
|
||||
if (this.isMounted()) {
|
||||
this.forceUpdate();
|
||||
callback && callback();
|
||||
@@ -271,7 +318,7 @@ export default class Abstract extends ThemedComponent {
|
||||
return this.mounted;
|
||||
}
|
||||
|
||||
configureNavBar(initial) {}
|
||||
configureNavBar(_initial: boolean) {}
|
||||
|
||||
popToRoot() {
|
||||
this.props.navigation.popToTop();
|
||||
@@ -284,7 +331,14 @@ export default class Abstract extends ThemedComponent {
|
||||
this.props.navigation.goBack(null);
|
||||
}
|
||||
|
||||
async handlePrivilegedAction(isProtected, action, run, onCancel) {
|
||||
async handlePrivilegedAction(
|
||||
isProtected: boolean,
|
||||
action: any,
|
||||
run: {
|
||||
(): void;
|
||||
},
|
||||
onCancel?: { (): void; (): any }
|
||||
) {
|
||||
if (isProtected) {
|
||||
const actionRequiresPrivs = await PrivilegesManager.get().actionRequiresPrivilege(
|
||||
action
|
||||
@@ -306,7 +360,11 @@ export default class Abstract extends ThemedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
static IsShallowEqual = (newObj, prevObj, keys) => {
|
||||
static IsShallowEqual = (
|
||||
newObj: { [x: string]: any },
|
||||
prevObj: { [x: string]: any },
|
||||
keys: string[]
|
||||
) => {
|
||||
if (!keys) {
|
||||
keys = Object.keys(newObj);
|
||||
}
|
||||
@@ -318,7 +376,12 @@ export default class Abstract extends ThemedComponent {
|
||||
return true;
|
||||
};
|
||||
|
||||
static IsDeepEqual = (newObj, prevObj, keys, omitKeys = []) => {
|
||||
static IsDeepEqual = (
|
||||
newObj: {},
|
||||
prevObj: any,
|
||||
keys?: string[],
|
||||
omitKeys: string[] = []
|
||||
) => {
|
||||
if (!keys) {
|
||||
keys = Object.keys(newObj);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import React from 'react';
|
||||
import { TextInput, View, Alert, ScrollView } from 'react-native';
|
||||
import {
|
||||
TextInput,
|
||||
View,
|
||||
Alert,
|
||||
ScrollView,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import _ from 'lodash';
|
||||
import ButtonCell from '@Components/ButtonCell';
|
||||
import SectionHeader from '@Components/SectionHeader';
|
||||
import SectionedAccessoryTableCell from '@Components/SectionedAccessoryTableCell';
|
||||
import SectionedTableCell from '@Components/SectionedTableCell';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import ApplicationState, { AppStateType } from '@Lib/ApplicationState';
|
||||
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
|
||||
import { ICON_CLOSE } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import AuthenticationSourceAccountPassword from './Sources/AuthenticationSourceAccountPassword';
|
||||
import AuthenticationSourceBiometric from './Sources/AuthenticationSourceBiometric';
|
||||
import AuthenticationSourceLocalPasscode from './Sources/AuthenticationSourceLocalPasscode';
|
||||
|
||||
export default class Authenticate extends Abstract {
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
type Source =
|
||||
| AuthenticationSourceAccountPassword
|
||||
| AuthenticationSourceBiometric
|
||||
| AuthenticationSourceLocalPasscode;
|
||||
|
||||
type State = {
|
||||
activeSource: any | null;
|
||||
submitDisabled: boolean;
|
||||
sourceLocked: boolean;
|
||||
} & AbstractState;
|
||||
|
||||
export default class Authenticate extends Abstract<AbstractProps, State> {
|
||||
static navigationOptions = ({ navigation, navigationOptions }: any) => {
|
||||
const templateOptions = {
|
||||
/**
|
||||
* On Android, not having a left button will make the title appear all
|
||||
@@ -21,12 +42,22 @@ export default class Authenticate extends Abstract {
|
||||
};
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
_navigationOptions: navigationOptions,
|
||||
templateOptions,
|
||||
});
|
||||
};
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
stateObserver: {
|
||||
key: () => number;
|
||||
callback: (state: AppStateType) => void;
|
||||
};
|
||||
pendingSources: Source[];
|
||||
_sessionLength: number;
|
||||
successfulSources: Source[];
|
||||
activeSource: Source | null = null;
|
||||
needsSuccessCallback: any;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<AbstractProps>) {
|
||||
super(props);
|
||||
|
||||
for (const source of this.sources) {
|
||||
@@ -67,7 +98,7 @@ export default class Authenticate extends Abstract {
|
||||
this.successfulSources = [];
|
||||
}
|
||||
|
||||
get sources() {
|
||||
get sources(): Source[] {
|
||||
return this.getProp('authenticationSources');
|
||||
}
|
||||
|
||||
@@ -134,7 +165,7 @@ export default class Authenticate extends Abstract {
|
||||
}
|
||||
}
|
||||
|
||||
async beginAuthenticationForSource(source) {
|
||||
async beginAuthenticationForSource(source: Source) {
|
||||
/**
|
||||
* Authentication modal may be displayed on lose focus just before the app
|
||||
* is closing. In this state however, we don't want to begin auth. We'll
|
||||
@@ -158,7 +189,7 @@ export default class Authenticate extends Abstract {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
successfulSourcesIncludesSource(source) {
|
||||
successfulSourcesIncludesSource(source: Source) {
|
||||
for (const candidate of this.successfulSources) {
|
||||
if (candidate.identifier === source.identifier) {
|
||||
return true;
|
||||
@@ -178,7 +209,7 @@ export default class Authenticate extends Abstract {
|
||||
return true;
|
||||
}
|
||||
|
||||
async validateAuthentication(source) {
|
||||
async validateAuthentication(source: Source) {
|
||||
if (this.state.sourceLocked) {
|
||||
return;
|
||||
}
|
||||
@@ -219,7 +250,7 @@ export default class Authenticate extends Abstract {
|
||||
}
|
||||
}
|
||||
|
||||
async onBiometricDirectPress(source) {
|
||||
async onBiometricDirectPress(source: Source) {
|
||||
if (source.isLocked()) {
|
||||
return;
|
||||
}
|
||||
@@ -237,7 +268,7 @@ export default class Authenticate extends Abstract {
|
||||
* When a source returns in a locked status we create a timeout for the lock
|
||||
* period. This will auto reprompt the user for auth after the period is up.
|
||||
*/
|
||||
onSourceLocked(source) {
|
||||
onSourceLocked(source: Source) {
|
||||
this.setState({ sourceLocked: true, submitDisabled: true });
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -283,7 +314,7 @@ export default class Authenticate extends Abstract {
|
||||
this.needsSuccessCallback = false;
|
||||
}
|
||||
|
||||
inputTextChanged(text, source) {
|
||||
inputTextChanged(text: string, source: Source) {
|
||||
source.setAuthenticationValue(text);
|
||||
this.forceUpdate();
|
||||
}
|
||||
@@ -292,15 +323,19 @@ export default class Authenticate extends Abstract {
|
||||
return this.getProp('sessionLengthOptions');
|
||||
}
|
||||
|
||||
setSessionLength(length) {
|
||||
setSessionLength(length: number) {
|
||||
this._sessionLength = length;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
_renderAuthenticationSoure = (source, index) => {
|
||||
_renderAuthenticationSoure = (source: Source, index: number) => {
|
||||
const isLast = index === this.sources.length - 1;
|
||||
|
||||
const inputAuthenticationSource = source => (
|
||||
const inputAuthenticationSource = (
|
||||
inputSource:
|
||||
| AuthenticationSourceAccountPassword
|
||||
| AuthenticationSourceLocalPasscode
|
||||
) => (
|
||||
<View
|
||||
style={[
|
||||
this.styles.authSourceSection,
|
||||
@@ -314,31 +349,33 @@ export default class Authenticate extends Abstract {
|
||||
>
|
||||
<TextInput
|
||||
ref={ref => {
|
||||
source.inputRef = ref;
|
||||
inputSource.inputRef = ref;
|
||||
}}
|
||||
style={StyleKit.styles.sectionedTableCellTextInput}
|
||||
placeholder={source.inputPlaceholder}
|
||||
placeholder={inputSource.inputPlaceholder}
|
||||
onChangeText={text => {
|
||||
this.inputTextChanged(text, source);
|
||||
this.inputTextChanged(text, inputSource);
|
||||
}}
|
||||
value={source.getAuthenticationValue()}
|
||||
value={inputSource.getAuthenticationValue()}
|
||||
autoCorrect={false}
|
||||
autoFocus={false}
|
||||
autoCapitalize={'none'}
|
||||
secureTextEntry={true}
|
||||
keyboardType={source.keyboardType || 'default'}
|
||||
keyboardType={inputSource.keyboardType || 'default'}
|
||||
keyboardAppearance={StyleKit.get().keyboardColorForActiveTheme()}
|
||||
underlineColorAndroid={'transparent'}
|
||||
placeholderTextColor={StyleKit.variables.stylekitNeutralColor}
|
||||
onSubmitEditing={() => {
|
||||
this.validateAuthentication(source);
|
||||
this.validateAuthentication(inputSource);
|
||||
}}
|
||||
/>
|
||||
</SectionedTableCell>
|
||||
</View>
|
||||
);
|
||||
|
||||
const biometricAuthenticationSource = source => (
|
||||
const biometricAuthenticationSource = (
|
||||
biometricSource: AuthenticationSourceBiometric
|
||||
) => (
|
||||
<View
|
||||
style={[
|
||||
this.styles.authSourceSection,
|
||||
@@ -347,11 +384,11 @@ export default class Authenticate extends Abstract {
|
||||
>
|
||||
<SectionedAccessoryTableCell
|
||||
first={true}
|
||||
dimmed={source !== this.state.activeSource}
|
||||
tinted={source === this.state.activeSource}
|
||||
text={source.label}
|
||||
dimmed={biometricSource !== this.state.activeSource}
|
||||
tinted={biometricSource === this.state.activeSource}
|
||||
text={biometricSource.label}
|
||||
onPress={() => {
|
||||
this.onBiometricDirectPress(source);
|
||||
this.onBiometricDirectPress(biometricSource);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -369,14 +406,40 @@ export default class Authenticate extends Abstract {
|
||||
<View key={source.identifier}>
|
||||
<SectionHeader
|
||||
title={sourceTitle}
|
||||
subtitle={hasHeaderSubtitle && source.label}
|
||||
subtitle={hasHeaderSubtitle ? source.label : undefined}
|
||||
tinted={source === this.state.activeSource}
|
||||
buttonText={source.headerButtonText}
|
||||
buttonAction={source.headerButtonAction}
|
||||
buttonStyles={source.headerButtonStyles}
|
||||
buttonText={
|
||||
source.type === 'input'
|
||||
? (source as
|
||||
| AuthenticationSourceAccountPassword
|
||||
| AuthenticationSourceLocalPasscode).headerButtonText
|
||||
: undefined
|
||||
}
|
||||
buttonAction={
|
||||
source.type === 'input'
|
||||
? (source as
|
||||
| AuthenticationSourceAccountPassword
|
||||
| AuthenticationSourceLocalPasscode).headerButtonAction
|
||||
: undefined
|
||||
}
|
||||
buttonStyles={
|
||||
source.type === 'input'
|
||||
? (source as
|
||||
| AuthenticationSourceAccountPassword
|
||||
| AuthenticationSourceLocalPasscode).headerButtonStyles
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{source.type === 'input' && inputAuthenticationSource(source)}
|
||||
{source.type === 'biometric' && biometricAuthenticationSource(source)}
|
||||
{source.type === 'input' &&
|
||||
inputAuthenticationSource(
|
||||
source as
|
||||
| AuthenticationSourceAccountPassword
|
||||
| AuthenticationSourceLocalPasscode
|
||||
)}
|
||||
{source.type === 'biometric' &&
|
||||
biometricAuthenticationSource(
|
||||
source as AuthenticationSourceBiometric
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -396,7 +459,8 @@ export default class Authenticate extends Abstract {
|
||||
})}
|
||||
|
||||
<ButtonCell
|
||||
style={this.styles.submitButtonCell}
|
||||
// TODO: there is no style prop
|
||||
// style={this.styles.submitButtonCell}
|
||||
maxHeight={45}
|
||||
disabled={this.state.submitDisabled}
|
||||
title={this.pendingSources.length > 1 ? 'Next' : 'Submit'}
|
||||
@@ -407,20 +471,22 @@ export default class Authenticate extends Abstract {
|
||||
{this.sessionLengthOptions && this.sessionLengthOptions.length > 0 && (
|
||||
<View style={this.styles.rememberForSection}>
|
||||
<SectionHeader title={'Remember For'} />
|
||||
{this.sessionLengthOptions.map((option, index) => (
|
||||
<SectionedAccessoryTableCell
|
||||
text={option.label}
|
||||
key={`${index}`}
|
||||
first={index === 0}
|
||||
last={index === this.sessionLengthOptions.length - 1}
|
||||
selected={() => {
|
||||
return option.value === this._sessionLength;
|
||||
}}
|
||||
onPress={() => {
|
||||
this.setSessionLength(option.value);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{this.sessionLengthOptions.map(
|
||||
(option: { label: string; value: number }, index: number) => (
|
||||
<SectionedAccessoryTableCell
|
||||
text={option.label}
|
||||
key={`${index}`}
|
||||
first={index === 0}
|
||||
last={index === this.sessionLengthOptions.length - 1}
|
||||
selected={() => {
|
||||
return option.value === this._sessionLength;
|
||||
}}
|
||||
onPress={() => {
|
||||
this.setSessionLength(option.value);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
const DEFAULT_LOCK_TIMEOUT = 30 * 1000;
|
||||
|
||||
export default class AuthenticationSource {
|
||||
status:
|
||||
| 'waiting-input'
|
||||
| 'locked'
|
||||
| 'processing'
|
||||
| 'waiting-turn'
|
||||
| 'did-fail'
|
||||
| 'did-succeed';
|
||||
authenticationValue?: string;
|
||||
onRequiresInterfaceReload?: () => void;
|
||||
inputRef: TextInput | null = null;
|
||||
constructor() {
|
||||
this.status = 'waiting-turn';
|
||||
}
|
||||
@@ -63,15 +75,15 @@ export default class AuthenticationSource {
|
||||
return this.status === 'processing';
|
||||
}
|
||||
|
||||
setAuthenticationValue(value) {
|
||||
setAuthenticationValue(value: string) {
|
||||
this.authenticationValue = value;
|
||||
}
|
||||
|
||||
getAuthenticationValue(value) {
|
||||
getAuthenticationValue() {
|
||||
return this.authenticationValue;
|
||||
}
|
||||
|
||||
async authenticate() {}
|
||||
async authenticate(): Promise<any> {}
|
||||
|
||||
cancel() {}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import AuthenticationSource from '@Screens/Authentication/Sources/AuthenticationSource';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import { KeyboardTypeOptions } from 'react-native';
|
||||
|
||||
export default class AuthenticationSourceAccountPassword extends AuthenticationSource {
|
||||
keyboardType: KeyboardTypeOptions = 'default';
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -18,6 +20,18 @@ export default class AuthenticationSourceAccountPassword extends AuthenticationS
|
||||
return 'Account Password';
|
||||
}
|
||||
|
||||
get headerButtonText() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
headerButtonAction = () => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
get headerButtonStyles() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get label() {
|
||||
switch (this.status) {
|
||||
case 'waiting-turn':
|
||||
@@ -60,7 +74,7 @@ export default class AuthenticationSourceAccountPassword extends AuthenticationS
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
_fail(message) {
|
||||
_fail(message: string) {
|
||||
this.didFail();
|
||||
return { success: false, error: { message: message } };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import KeysManager from '@Lib/keysManager';
|
||||
import AuthenticationSource from '@Screens/Authentication/Sources/AuthenticationSource';
|
||||
|
||||
export default class AuthenticationSourceBiometric extends AuthenticationSource {
|
||||
isReady: boolean = true;
|
||||
isAvailable: boolean = false;
|
||||
biometricsType: string | undefined;
|
||||
biometricsNoun: string | undefined;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -94,13 +98,14 @@ export default class AuthenticationSourceBiometric extends AuthenticationSource
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return FingerprintScanner.authenticate({
|
||||
// @ts-ignore TODO: check deviceCredentialAllowed
|
||||
deviceCredentialAllowed: true,
|
||||
description: 'Biometrics are required to access your notes.',
|
||||
})
|
||||
.then(() => {
|
||||
return this._success();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error: { name: string }) => {
|
||||
console.log('Biometrics error', error);
|
||||
|
||||
if (error.name === 'DeviceLocked') {
|
||||
@@ -143,7 +148,7 @@ export default class AuthenticationSourceBiometric extends AuthenticationSource
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
_fail(message) {
|
||||
_fail(message?: string) {
|
||||
if (!this.isLocked()) {
|
||||
this.didFail();
|
||||
}
|
||||
|
||||
@@ -3,21 +3,23 @@ import { protocolManager } from 'snjs';
|
||||
import Storage from '@Lib/snjs/storageManager';
|
||||
import AuthenticationSource from '@Screens/Authentication/Sources/AuthenticationSource';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { KeyboardTypeOptions } from 'react-native';
|
||||
|
||||
export default class AuthenticationSourceLocalPasscode extends AuthenticationSource {
|
||||
keyboardType: KeyboardTypeOptions | null = 'default';
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
Storage.get()
|
||||
.getItem('passcodeKeyboardType')
|
||||
.then(result => {
|
||||
this.keyboardType = result || 'default';
|
||||
this.keyboardType = (result as KeyboardTypeOptions) || 'default';
|
||||
this.requiresInterfaceReload();
|
||||
});
|
||||
}
|
||||
|
||||
get headerButtonText() {
|
||||
return this.isWaitingForInput() && 'Change Keyboard';
|
||||
return this.isWaitingForInput() ? 'Change Keyboard' : undefined;
|
||||
}
|
||||
|
||||
get headerButtonStyles() {
|
||||
@@ -93,7 +95,7 @@ export default class AuthenticationSourceLocalPasscode extends AuthenticationSou
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
_fail(message) {
|
||||
_fail(message: string) {
|
||||
this.didFail();
|
||||
return { success: false, error: { message: message } };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Alert, View, Platform, Text } from 'react-native';
|
||||
import {
|
||||
Alert,
|
||||
View,
|
||||
Platform,
|
||||
Text,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
@@ -11,11 +18,24 @@ import UserPrefsManager, {
|
||||
import { ICON_LOCK } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class ComponentView extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
type Props = {
|
||||
noteId: string;
|
||||
editorId: string;
|
||||
onLoadEnd: () => void;
|
||||
onLoadStart: () => void;
|
||||
onLoadError: () => void;
|
||||
};
|
||||
|
||||
this.state = {};
|
||||
export default class ComponentView extends Component<Props> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
identifier: string;
|
||||
editor: any;
|
||||
note: any;
|
||||
registrationTimeout: any;
|
||||
webView: WebView | null = null;
|
||||
alreadyTriggeredLoad: boolean = false;
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
|
||||
this.loadStyles();
|
||||
|
||||
@@ -83,7 +103,7 @@ export default class ComponentView extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (
|
||||
prevProps.noteId !== this.props.noteId ||
|
||||
prevProps.editorId !== this.props.editorId
|
||||
@@ -107,7 +127,7 @@ export default class ComponentView extends Component {
|
||||
ComponentManager.get().deactivateComponent(this.editor);
|
||||
}
|
||||
|
||||
onMessage = message => {
|
||||
onMessage = (message: { nativeEvent: { data: string } }) => {
|
||||
if (!this.note) {
|
||||
/** May be the case in tablet mode on app launch */
|
||||
return;
|
||||
@@ -124,7 +144,7 @@ export default class ComponentView extends Component {
|
||||
ComponentManager.get().handleMessage(this.editor, data);
|
||||
};
|
||||
|
||||
onFrameLoad = syntheticEvent => {
|
||||
onFrameLoad = () => {
|
||||
/**
|
||||
* We have no way of knowing if the webview load is successful or not. We
|
||||
* have to wait to see if the error event is fired. Looking at the code,
|
||||
@@ -165,12 +185,15 @@ export default class ComponentView extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onLoadError = syntheticEvent => {
|
||||
onLoadError = () => {
|
||||
clearTimeout(this.registrationTimeout);
|
||||
this.props.onLoadError();
|
||||
};
|
||||
|
||||
onShouldStartLoadWithRequest = request => {
|
||||
onShouldStartLoadWithRequest = (request: {
|
||||
navigationType: string;
|
||||
url: string;
|
||||
}) => {
|
||||
/**
|
||||
* We want to handle link clicks within an editor by opening the browser
|
||||
* instead of loading inline. On iOS, onShouldStartLoadWithRequest is
|
||||
@@ -246,13 +269,13 @@ export default class ComponentView extends Component {
|
||||
onLoadStart={this.onLoadStart}
|
||||
onError={this.onLoadError}
|
||||
onMessage={this.onMessage}
|
||||
useWebKit={true}
|
||||
hideKeyboardAccessoryView={true}
|
||||
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
|
||||
cacheEnabled={true}
|
||||
scalesPageToFit={
|
||||
true /* Android only, not available with WKWebView */
|
||||
}
|
||||
// @ts-ignore this is patched
|
||||
autoManageStatusBarEnabled={
|
||||
false /* To prevent StatusBar from changing colors when focusing */
|
||||
}
|
||||
|
||||
@@ -7,26 +7,54 @@ import {
|
||||
Platform,
|
||||
Text,
|
||||
Alert,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import TextView from 'sn-textview';
|
||||
import { SFAuthManager } from 'snjs';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import LockedView from '@Containers/LockedView';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import ApplicationState, {
|
||||
AppStateEventHandler,
|
||||
AppStateEventType,
|
||||
TabletModeChangeData,
|
||||
NoteSideMenuToggleChange,
|
||||
} from '@Lib/ApplicationState';
|
||||
import ComponentManager from '@Lib/componentManager';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
|
||||
import ComponentView from '@Screens/ComponentView';
|
||||
import SideMenuManager from '@Screens/SideMenu/SideMenuManager';
|
||||
import { ICON_ALERT, ICON_LOCK, ICON_MENU } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { lighten } from '@Style/utils';
|
||||
|
||||
export default class Compose extends Abstract {
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
type State = {
|
||||
webViewError: any;
|
||||
loadingWebView: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
noteLocked: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
selectedTagId?: string;
|
||||
};
|
||||
|
||||
export default class Compose extends Abstract<
|
||||
Props & AbstractProps,
|
||||
State & AbstractState
|
||||
> {
|
||||
static navigationOptions = ({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
}: {
|
||||
navigation: any;
|
||||
navigationOptions: any;
|
||||
}) => {
|
||||
const templateOptions = {
|
||||
rightButton: {
|
||||
title: null,
|
||||
@@ -35,12 +63,26 @@ export default class Compose extends Abstract {
|
||||
};
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
_navigationOptions: navigationOptions,
|
||||
templateOptions,
|
||||
});
|
||||
};
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
rawStyles!: Record<string, ViewStyle | TextStyle>;
|
||||
note: any;
|
||||
syncObserver: any;
|
||||
componentHandler: any;
|
||||
signoutObserver: any;
|
||||
tabletModeChangeHandler?: AppStateEventHandler;
|
||||
didShowErrorAlert?: boolean;
|
||||
rightMenuHandler: SideMenuManager['rightSideMenuHandler'] = null;
|
||||
input: TextView | null = null;
|
||||
saveError?: boolean;
|
||||
statusTimeout?: ReturnType<typeof setTimeout>;
|
||||
syncTakingTooLong: boolean = false;
|
||||
saveTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<Props & AbstractProps>) {
|
||||
super(props);
|
||||
|
||||
let note,
|
||||
@@ -73,35 +115,36 @@ export default class Compose extends Abstract {
|
||||
}
|
||||
|
||||
registerObservers() {
|
||||
this.syncObserver = Sync.get().addEventHandler((event, data) => {
|
||||
if (event === 'sync:error') {
|
||||
this.showSavedStatus(false);
|
||||
} else if (event === 'sync:completed') {
|
||||
let isInRetrieved =
|
||||
data.retrievedItems &&
|
||||
data.retrievedItems.map(i => i.uuid).includes(this.note.uuid);
|
||||
let isInSaved =
|
||||
data.savedItems &&
|
||||
data.savedItems.map(i => i.uuid).includes(this.note.uuid);
|
||||
if (this.note.deleted || this.note.content.trashed) {
|
||||
let clearNote = this.note.deleted === true;
|
||||
// if Trashed, and we're in the Trash view, don't clear note.
|
||||
if (!this.note.deleted) {
|
||||
let selectedTag = this.getSelectedTag();
|
||||
let isTrashTag =
|
||||
selectedTag != null && selectedTag.content.isTrashTag === true;
|
||||
if (this.note.content.trashed) {
|
||||
// clear the note if this is not the trash tag. Otherwise, keep it in view.
|
||||
clearNote = !isTrashTag;
|
||||
this.syncObserver = Sync.get().addEventHandler(
|
||||
(event: string, data: { retrievedItems: any[]; savedItems: any[] }) => {
|
||||
if (event === 'sync:error') {
|
||||
this.showSavedStatus(false);
|
||||
} else if (event === 'sync:completed') {
|
||||
let isInRetrieved =
|
||||
data.retrievedItems &&
|
||||
data.retrievedItems.map(i => i.uuid).includes(this.note.uuid);
|
||||
let isInSaved =
|
||||
data.savedItems &&
|
||||
data.savedItems.map(i => i.uuid).includes(this.note.uuid);
|
||||
if (this.note.deleted || this.note.content.trashed) {
|
||||
let clearNote = this.note.deleted === true;
|
||||
// if Trashed, and we're in the Trash view, don't clear note.
|
||||
if (!this.note.deleted) {
|
||||
let selectedTag = this.getSelectedTag();
|
||||
let isTrashTag =
|
||||
selectedTag != null && selectedTag.content.isTrashTag === true;
|
||||
if (this.note.content.trashed) {
|
||||
// clear the note if this is not the trash tag. Otherwise, keep it in view.
|
||||
clearNote = !isTrashTag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clearNote) {
|
||||
this.props.navigation.closeRightDrawer();
|
||||
this.setNote(null);
|
||||
}
|
||||
} else if (this.note.uuid && (isInRetrieved || isInSaved)) {
|
||||
/*
|
||||
if (clearNote) {
|
||||
this.props.navigation.closeRightDrawer();
|
||||
this.setNote(null);
|
||||
}
|
||||
} else if (this.note.uuid && (isInRetrieved || isInSaved)) {
|
||||
/*
|
||||
You have to be careful about when you render inside this component. Rendering with the native SNTextView component
|
||||
can cause the cursor to go to the end of the input, both on iOS and Android. We want to force an update only if retrievedItems includes this item
|
||||
|
||||
@@ -114,31 +157,36 @@ export default class Compose extends Abstract {
|
||||
Do not make text part of the state, otherwise that would cause a re-render on every keystroke.
|
||||
*/
|
||||
|
||||
let newState = {
|
||||
title: this.note.title,
|
||||
noteLocked: this.note.locked ? true : false,
|
||||
};
|
||||
let newState = {
|
||||
title: this.note.title,
|
||||
noteLocked: this.note.locked ? true : false,
|
||||
};
|
||||
|
||||
// only include `text` if this item is coming back from retrieved, as this will cause text view cursor to reset to top
|
||||
if (isInRetrieved) {
|
||||
newState.text = this.note.text;
|
||||
// only include `text` if this item is coming back from retrieved, as this will cause text view cursor to reset to top
|
||||
if (isInRetrieved) {
|
||||
newState = Object.assign(newState, { text: this.note.text });
|
||||
}
|
||||
|
||||
// Use true/false for note.locked as we don't want values of null or undefined, which may cause
|
||||
// unnecessary renders. (on constructor it was undefined, and here, it was null, causing a re-render to occur on android, causing textview to reset cursor)
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
// Use true/false for note.locked as we don't want values of null or undefined, which may cause
|
||||
// unnecessary renders. (on constructor it was undefined, and here, it was null, causing a re-render to occur on android, causing textview to reset cursor)
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
if ((isInSaved && !this.note.dirty) || this.saveError) {
|
||||
this.showSavedStatus(true);
|
||||
if ((isInSaved && !this.note.dirty) || this.saveError) {
|
||||
this.showSavedStatus(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
this.componentHandler = ComponentManager.get().registerHandler({
|
||||
identifier: 'composer',
|
||||
areas: ['editor-editor'],
|
||||
actionHandler: (component, action, data) => {
|
||||
actionHandler: (
|
||||
_component: any,
|
||||
action: string,
|
||||
data: { items: any[] }
|
||||
) => {
|
||||
if (action === 'save-items') {
|
||||
if (
|
||||
data.items
|
||||
@@ -153,17 +201,24 @@ export default class Compose extends Abstract {
|
||||
},
|
||||
});
|
||||
|
||||
this.signoutObserver = Auth.get().addEventHandler(event => {
|
||||
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
|
||||
if (event === SFAuthManager.DidSignOutEvent) {
|
||||
this.setNote(null);
|
||||
}
|
||||
});
|
||||
|
||||
this.tabletModeChangeHandler = ApplicationState.get().addEventHandler(
|
||||
(event, data) => {
|
||||
(
|
||||
event: AppStateEventType,
|
||||
data: TabletModeChangeData | NoteSideMenuToggleChange | undefined
|
||||
) => {
|
||||
if (event === ApplicationState.AppStateEventTabletModeChange) {
|
||||
// If we are now in tablet mode after not being in tablet mode
|
||||
if (data.new_isInTabletMode && !data.old_isInTabletMode) {
|
||||
if (
|
||||
data &&
|
||||
(data as TabletModeChangeData).new_isInTabletMode &&
|
||||
!(data as TabletModeChangeData).old_isInTabletMode
|
||||
) {
|
||||
this.setSideMenuHandler();
|
||||
}
|
||||
}
|
||||
@@ -185,7 +240,10 @@ export default class Compose extends Abstract {
|
||||
2. In setNote(null), if the current note is already a dummy, don't do anything.
|
||||
*/
|
||||
|
||||
setNote(note, isConstructor = false) {
|
||||
setNote(
|
||||
note: { dummy: boolean; initUUID: () => Promise<any>; title: any } | null,
|
||||
isConstructor = false
|
||||
) {
|
||||
if (!note) {
|
||||
if (this.note && this.note.dummy) {
|
||||
// This method can be called if the + button is pressed. On Tablet, it can be pressed even while we're
|
||||
@@ -198,12 +256,12 @@ export default class Compose extends Abstract {
|
||||
dummy: true,
|
||||
text: '',
|
||||
});
|
||||
note.dummy = true;
|
||||
note!.dummy = true;
|
||||
// Editors need a valid note with uuid and modelmanager mapped in order to interact with it
|
||||
// Note that this can create dummy notes that aren't deleted automatically.
|
||||
// Also useful to keep right menu enabled at all times. If the note has a uuid and is a dummy,
|
||||
// it will be removed locally on blur
|
||||
note.initUUID().then(() => {
|
||||
note!.initUUID().then(() => {
|
||||
ModelManager.get().addItem(note);
|
||||
this.forceUpdate();
|
||||
});
|
||||
@@ -212,7 +270,7 @@ export default class Compose extends Abstract {
|
||||
this.note = note;
|
||||
|
||||
if (!isConstructor) {
|
||||
this.setState({ title: note.title });
|
||||
this.setState({ title: note!.title });
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
@@ -237,7 +295,9 @@ export default class Compose extends Abstract {
|
||||
// we ignore render events. This will cause the screen to be white when you save the new tag.
|
||||
SideMenuManager.get().removeHandlerForRightSideMenu(this.rightMenuHandler);
|
||||
Auth.get().removeEventHandler(this.signoutObserver);
|
||||
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
|
||||
if (this.tabletModeChangeHandler) {
|
||||
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
|
||||
}
|
||||
Sync.get().removeEventHandler(this.syncObserver);
|
||||
ComponentManager.get().deregisterHandler(this.componentHandler);
|
||||
}
|
||||
@@ -267,6 +327,7 @@ export default class Compose extends Abstract {
|
||||
|
||||
if (this.note.dummy) {
|
||||
if (this.refs.input) {
|
||||
// @ts-ignore ignore wrong focus type
|
||||
this.refs.input.focus();
|
||||
}
|
||||
}
|
||||
@@ -335,7 +396,7 @@ export default class Compose extends Abstract {
|
||||
}
|
||||
}
|
||||
|
||||
replaceTagsForNote(newTags) {
|
||||
replaceTagsForNote(newTags: string | any[]) {
|
||||
let note = this.note;
|
||||
|
||||
var oldTags = note.tags.slice(); // original array will be modified in the for loop so we make a copy
|
||||
@@ -356,13 +417,13 @@ export default class Compose extends Abstract {
|
||||
}
|
||||
}
|
||||
|
||||
onTitleChange = text => {
|
||||
onTitleChange = (text: string) => {
|
||||
this.setState({ title: text });
|
||||
this.note.title = text;
|
||||
this.changesMade();
|
||||
};
|
||||
|
||||
onTextChange = text => {
|
||||
onTextChange = (text: string) => {
|
||||
if (this.note.locked) {
|
||||
// On Android, we don't disable the text view if the note is locked, as that also disables selection.
|
||||
Alert.alert(
|
||||
@@ -389,7 +450,7 @@ export default class Compose extends Abstract {
|
||||
this.setSubTitle('Saving...');
|
||||
}
|
||||
|
||||
showSavedStatus(success) {
|
||||
showSavedStatus(success: boolean) {
|
||||
const debouceMs = 300; // minimum time message is shown
|
||||
if (success) {
|
||||
if (this.statusTimeout) {
|
||||
@@ -457,12 +518,18 @@ export default class Compose extends Abstract {
|
||||
}, 325);
|
||||
}
|
||||
|
||||
sync(note, callback) {
|
||||
sync(
|
||||
note: {
|
||||
setDirty: (arg0: boolean, arg1: boolean) => void;
|
||||
hasChanges: boolean;
|
||||
},
|
||||
callback?: (arg0: boolean) => void
|
||||
) {
|
||||
note.setDirty(true, true);
|
||||
|
||||
Sync.get()
|
||||
.sync()
|
||||
.then(response => {
|
||||
.then((response: { error: any }) => {
|
||||
if (response && response.error) {
|
||||
if (!this.didShowErrorAlert) {
|
||||
this.didShowErrorAlert = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { TextInput, Keyboard, Alert } from 'react-native';
|
||||
import { TextInput, Keyboard, Alert, KeyboardTypeOptions } from 'react-native';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
import ButtonCell from '@Components/ButtonCell';
|
||||
import SectionedTableCell from '@Components/SectionedTableCell';
|
||||
@@ -7,12 +7,17 @@ import SectionedOptionsTableCell from '@Components/SectionedOptionsTableCell';
|
||||
import TableSection from '@Components/TableSection';
|
||||
import LockedView from '@Containers/LockedView';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
|
||||
import { ICON_CLOSE } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class InputModal extends Abstract {
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
type State = {
|
||||
text: string;
|
||||
confirmText: string;
|
||||
} & AbstractState;
|
||||
|
||||
export default class InputModal extends Abstract<AbstractProps, State> {
|
||||
static navigationOptions = ({ navigation, navigationOptions }: any) => {
|
||||
const templateOptions = {
|
||||
leftButton: {
|
||||
title: ApplicationState.isIOS ? 'Cancel' : null,
|
||||
@@ -23,12 +28,17 @@ export default class InputModal extends Abstract {
|
||||
};
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
_navigationOptions: navigationOptions,
|
||||
templateOptions,
|
||||
});
|
||||
};
|
||||
requireConfirm: boolean;
|
||||
showKeyboardChooser: boolean;
|
||||
keyboardType: KeyboardTypeOptions;
|
||||
confirmInputRef: TextInput | null = null;
|
||||
inputRef: TextInput | null = null;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<AbstractProps>) {
|
||||
super(props);
|
||||
|
||||
props.navigation.setParams({
|
||||
@@ -56,7 +66,7 @@ export default class InputModal extends Abstract {
|
||||
|
||||
onTextSubmit = () => {
|
||||
if (this.requireConfirm && !this.state.confirmText) {
|
||||
this.confirmInputRef.focus();
|
||||
this.confirmInputRef?.focus();
|
||||
} else {
|
||||
this.submit();
|
||||
}
|
||||
@@ -87,11 +97,11 @@ export default class InputModal extends Abstract {
|
||||
this.dismiss();
|
||||
};
|
||||
|
||||
onTextChange = text => {
|
||||
onTextChange = (text: string) => {
|
||||
this.setState({ text: text });
|
||||
};
|
||||
|
||||
onConfirmTextChange = text => {
|
||||
onConfirmTextChange = (text: string) => {
|
||||
this.setState({ confirmText: text });
|
||||
};
|
||||
|
||||
@@ -105,7 +115,7 @@ export default class InputModal extends Abstract {
|
||||
}
|
||||
}
|
||||
|
||||
onKeyboardOptionsSelect = option => {
|
||||
onKeyboardOptionsSelect = (option: { key: any }) => {
|
||||
this.keyboardType = option.key;
|
||||
this.forceUpdate();
|
||||
this.refreshKeyboard();
|
||||
|
||||
@@ -7,15 +7,19 @@ import SectionedTableCell from '@Components/SectionedTableCell';
|
||||
import TableSection from '@Components/TableSection';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import KeysManager from '@Lib/keysManager';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
import AlertManager from '@Lib/snjs/alertManager';
|
||||
import { ICON_CLOSE } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class KeyRecovery extends Abstract {
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
type State = {
|
||||
text: string;
|
||||
} & AbstractState;
|
||||
|
||||
export default class KeyRecovery extends Abstract<AbstractProps, State> {
|
||||
static navigationOptions = ({ navigation, navigationOptions }: any) => {
|
||||
const templateOptions = {
|
||||
title: 'Key Recovery',
|
||||
leftButton: {
|
||||
@@ -27,12 +31,15 @@ export default class KeyRecovery extends Abstract {
|
||||
};
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
_navigationOptions: navigationOptions,
|
||||
templateOptions,
|
||||
});
|
||||
};
|
||||
items: any;
|
||||
encryptedCount: number = 0;
|
||||
inputRef: TextInput | null = null;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<AbstractProps>) {
|
||||
super(props);
|
||||
|
||||
props.navigation.setParams({
|
||||
@@ -48,7 +55,6 @@ export default class KeyRecovery extends Abstract {
|
||||
});
|
||||
|
||||
this.state = { text: '' };
|
||||
|
||||
this.reloadData();
|
||||
}
|
||||
|
||||
@@ -72,7 +78,7 @@ export default class KeyRecovery extends Abstract {
|
||||
this.state.text,
|
||||
authParams
|
||||
);
|
||||
await SF.get().itemTransformer.decryptMultipleItems(this.items, keys);
|
||||
await protocolManager.decryptMultipleItems(this.items, keys);
|
||||
|
||||
this.encryptedCount = 0;
|
||||
for (const item of this.items) {
|
||||
@@ -81,7 +87,7 @@ export default class KeyRecovery extends Abstract {
|
||||
}
|
||||
}
|
||||
|
||||
let useKeys = async confirm => {
|
||||
let useKeys = async (confirm?: boolean) => {
|
||||
const run = async () => {
|
||||
await KeysManager.get().persistOfflineKeys(keys);
|
||||
await ModelManager.get().mapResponseItemsToLocalModelsOmittingFields(
|
||||
@@ -132,7 +138,7 @@ export default class KeyRecovery extends Abstract {
|
||||
}
|
||||
};
|
||||
|
||||
onTextChange = text => {
|
||||
onTextChange = (text: string) => {
|
||||
this.setState({ text: text });
|
||||
};
|
||||
|
||||
|
||||
@@ -10,12 +10,20 @@ import ApplicationState from '@Lib/ApplicationState';
|
||||
import KeysManager from '@Lib/keysManager';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import PrivilegesManager from '@Lib/snjs/privilegesManager';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
|
||||
import { ICON_CHECKMARK } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class ManagePrivileges extends Abstract {
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
type State = {
|
||||
availableActions: any[];
|
||||
availableCredentials: any[];
|
||||
notConfiguredCredentials?: any[];
|
||||
sessionExpirey?: string;
|
||||
sessionExpired?: boolean;
|
||||
} & AbstractState;
|
||||
|
||||
export default class ManagePrivileges extends Abstract<AbstractProps, State> {
|
||||
static navigationOptions = ({ navigation, navigationOptions }: any) => {
|
||||
const templateOptions = {
|
||||
title: 'Privileges',
|
||||
leftButton: {
|
||||
@@ -27,12 +35,17 @@ export default class ManagePrivileges extends Abstract {
|
||||
};
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
_navigationOptions: navigationOptions,
|
||||
templateOptions,
|
||||
});
|
||||
};
|
||||
hasPasscode: boolean;
|
||||
hasAccount: boolean;
|
||||
privileges: any;
|
||||
credentialDisplayInfo: Record<string, string> = {};
|
||||
styles: any;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<AbstractProps>) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -58,15 +71,15 @@ export default class ManagePrivileges extends Abstract {
|
||||
this.reloadPrivileges();
|
||||
}
|
||||
|
||||
displayInfoForCredential(credential) {
|
||||
displayInfoForCredential(credential: any) {
|
||||
return PrivilegesManager.get().displayInfoForCredential(credential).label;
|
||||
}
|
||||
|
||||
displayInfoForAction(action) {
|
||||
displayInfoForAction(action: any) {
|
||||
return PrivilegesManager.get().displayInfoForAction(action).label;
|
||||
}
|
||||
|
||||
isCredentialRequiredForAction(action, credential) {
|
||||
isCredentialRequiredForAction(action: any, credential: any) {
|
||||
if (!this.privileges) {
|
||||
return false;
|
||||
}
|
||||
@@ -120,14 +133,14 @@ export default class ManagePrivileges extends Abstract {
|
||||
});
|
||||
}
|
||||
|
||||
valueChanged(action, credential) {
|
||||
valueChanged(action: any, credential: any) {
|
||||
this.privileges.toggleCredentialForAction(action, credential);
|
||||
PrivilegesManager.get().savePrivileges();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
credentialUnavailable = credential => {
|
||||
return this.state.notConfiguredCredentials.includes(credential);
|
||||
credentialUnavailable = (credential: any) => {
|
||||
return this.state.notConfiguredCredentials?.includes(credential);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,18 +1,49 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, TouchableWithoutFeedback } from 'react-native';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
TouchableWithoutFeedback,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import ThemedPureComponent from '@Components/ThemedPureComponent';
|
||||
import ItemActionManager from '@Lib/itemActionManager';
|
||||
import ItemActionManager, { EventType } from '@Lib/itemActionManager';
|
||||
import ActionSheetWrapper from '@Style/ActionSheetWrapper';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { hexToRGBA } from '@Style/utils';
|
||||
import OptionsState from '@Lib/OptionsState';
|
||||
|
||||
export default class NoteCell extends ThemedPureComponent {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
item: any;
|
||||
options: OptionsState;
|
||||
highlighted?: boolean;
|
||||
onPressItem: (item: any) => void;
|
||||
handleAction: (item: any, key: EventType, callback: () => void) => void;
|
||||
renderTags: boolean;
|
||||
sortType: string;
|
||||
tagsString: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
selected: boolean;
|
||||
options: OptionsState;
|
||||
actionSheet: JSX.Element | null;
|
||||
};
|
||||
|
||||
export default class NoteCell extends ThemedPureComponent<Props, State> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
selectionTimeout?: number;
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
this.state = { selected: false, options: props.options || {} };
|
||||
this.state = {
|
||||
selected: false,
|
||||
options: props.options || {},
|
||||
actionSheet: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
if (props.options !== state.options) {
|
||||
return { options: props.options };
|
||||
}
|
||||
@@ -41,7 +72,7 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
this.setState({ selected: false });
|
||||
};
|
||||
|
||||
aggregateStyles(base, addition, condition) {
|
||||
aggregateStyles(base: ViewStyle, addition: ViewStyle, condition: boolean) {
|
||||
if (condition) {
|
||||
return [base, addition];
|
||||
} else {
|
||||
@@ -54,7 +85,7 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
let callbackForAction = action => {
|
||||
let callbackForAction = (action: any) => {
|
||||
this.props.handleAction(this.props.item, action.key, () => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
@@ -165,7 +196,14 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
sheet.show();
|
||||
};
|
||||
|
||||
getFlags(note) {
|
||||
getFlags(note: {
|
||||
pinned: boolean;
|
||||
archived: boolean;
|
||||
content: { protected: boolean; trashed: boolean; conflict_of: any };
|
||||
locked: boolean;
|
||||
errorDecrypting: any;
|
||||
deleted: boolean;
|
||||
}) {
|
||||
let flags = [];
|
||||
|
||||
if (note.pinned) {
|
||||
@@ -227,7 +265,7 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
return flags;
|
||||
}
|
||||
|
||||
flagElement = flag => {
|
||||
flagElement = (flag: { color: any; text: any }) => {
|
||||
let bgColor = flag.color;
|
||||
let textColor = StyleKit.variables.stylekitInfoContrastColor;
|
||||
if (this.state.selected || this.props.highlighted) {
|
||||
@@ -246,7 +284,7 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
text: {
|
||||
color: textColor,
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
fontWeight: 'bold' as 'bold',
|
||||
},
|
||||
};
|
||||
return (
|
||||
@@ -269,7 +307,7 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
note.tags.length > 0 &&
|
||||
!note.content.protected;
|
||||
|
||||
const highlight = this.state.selected || this.props.highlighted;
|
||||
const highlight = Boolean(this.state.selected || this.props.highlighted);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
@@ -284,7 +322,8 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
this.styles.noteCellSelected,
|
||||
highlight
|
||||
)}
|
||||
onPress={this._onPress}
|
||||
// TODO: onPreess does not exist on view
|
||||
// onPress={this._onPress}
|
||||
>
|
||||
{note.deleted && (
|
||||
<Text style={this.styles.deleting}>Deleting...</Text>
|
||||
@@ -389,6 +428,7 @@ export default class NoteCell extends ThemedPureComponent {
|
||||
|
||||
loadStyles() {
|
||||
let padding = 14;
|
||||
// @ts-ignore ignore null return from hexToRGBA
|
||||
this.styles = StyleSheet.create({
|
||||
noteCell: {
|
||||
padding: padding,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, FlatList, RefreshControl, Text } from 'react-native';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
Text,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import Search from 'react-native-search-box';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
@@ -7,8 +15,27 @@ import NoteCell from '@Screens/Notes/NoteCell';
|
||||
import OfflineBanner from '@Screens/Notes/OfflineBanner';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { EventType } from '@Lib/itemActionManager';
|
||||
|
||||
export default class NoteList extends ThemedComponent {
|
||||
type Props = {
|
||||
onSearchChange: (text: string) => void;
|
||||
onSearchCancel: () => void;
|
||||
onPressItem: (item: any) => void;
|
||||
selectedTags: any[];
|
||||
selectedNoteId: string | null;
|
||||
handleAction: (item: any, key: EventType, callback: () => void) => void;
|
||||
sortType: string;
|
||||
options: any;
|
||||
decrypting: boolean;
|
||||
loading: boolean;
|
||||
hasRefreshControl: boolean;
|
||||
notes: any[];
|
||||
refreshing: boolean;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
export default class NoteList extends ThemedComponent<Props> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
renderHeader = () => {
|
||||
const isOffline = Auth.get().offline();
|
||||
|
||||
@@ -48,7 +75,7 @@ export default class NoteList extends ThemedComponent {
|
||||
* Must pass title, text, and tags as props so that it re-renders when either
|
||||
* of those change.
|
||||
*/
|
||||
_renderItem = ({ item }) => {
|
||||
_renderItem = ({ item }: any) => {
|
||||
/**
|
||||
* On Android, only one tag is selected at a time. If it is selected, we
|
||||
* don't need to display the tags string above the note cell.
|
||||
@@ -63,6 +90,7 @@ export default class NoteList extends ThemedComponent {
|
||||
<NoteCell
|
||||
item={item}
|
||||
onPressItem={this.props.onPressItem}
|
||||
// @ts-ignore TODO: not used props for extra data
|
||||
title={item.title}
|
||||
text={item.text}
|
||||
tags={item.tags}
|
||||
@@ -72,6 +100,7 @@ export default class NoteList extends ThemedComponent {
|
||||
options={this.props.options}
|
||||
highlighted={item.uuid === this.props.selectedNoteId}
|
||||
handleAction={this.props.handleAction}
|
||||
// @ts-ignore TODO: not used props for extra data
|
||||
pinned={item.pinned /* extraData */}
|
||||
deleted={item.deleted /* extraData */}
|
||||
archived={item.archived /* extraData */}
|
||||
@@ -111,7 +140,7 @@ export default class NoteList extends ThemedComponent {
|
||||
keyboardDismissMode={'interactive'}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
refreshControl={
|
||||
!this.props.hasRefreshControl ? null : (
|
||||
!this.props.hasRefreshControl ? undefined : (
|
||||
<RefreshControl
|
||||
refreshing={this.props.refreshing}
|
||||
onRefresh={this.props.onRefresh}
|
||||
@@ -119,7 +148,7 @@ export default class NoteList extends ThemedComponent {
|
||||
)
|
||||
}
|
||||
data={this.props.notes}
|
||||
options={this.props.options}
|
||||
// options={this.props.options} no options prop
|
||||
renderItem={this._renderItem}
|
||||
ListHeaderComponent={this.renderHeader}
|
||||
/>
|
||||
|
||||
@@ -7,11 +7,16 @@ import LockedView from '@Containers/LockedView';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import ItemActionManager from '@Lib/itemActionManager';
|
||||
import ApplicationState, {
|
||||
AppStateEventHandler,
|
||||
AppStateEventType,
|
||||
TabletModeChangeData,
|
||||
NoteSideMenuToggleChange,
|
||||
} from '@Lib/ApplicationState';
|
||||
import ItemActionManager, { EventType } from '@Lib/itemActionManager';
|
||||
import KeysManager from '@Lib/keysManager';
|
||||
import OptionsState from '@Lib/OptionsState';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import OptionsState, { Observer } from '@Lib/OptionsState';
|
||||
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
|
||||
import NoteList from '@Screens/Notes/NoteList';
|
||||
import {
|
||||
SCREEN_SETTINGS,
|
||||
@@ -24,8 +29,35 @@ import StyleKit from '@Style/StyleKit';
|
||||
|
||||
import { SFAuthManager, SFPrivilegesManager } from 'snjs';
|
||||
|
||||
export default class Notes extends Abstract {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
onNoteSelect?: (note: any) => void;
|
||||
onUnlockPress: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
notes: any[];
|
||||
tags: any[];
|
||||
refreshing: boolean;
|
||||
selectedNoteId: string | null;
|
||||
decrypting: boolean;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default class Notes extends Abstract<
|
||||
Props & AbstractProps,
|
||||
State & AbstractState
|
||||
> {
|
||||
stateNotes: never[];
|
||||
options: OptionsState;
|
||||
loadNotesOnVisible: boolean = false;
|
||||
searching: any;
|
||||
tabletModeChangeHandler?: AppStateEventHandler;
|
||||
syncObserver: any;
|
||||
signoutObserver: any;
|
||||
optionsObserver?: Observer;
|
||||
styles: {} = {};
|
||||
mappingObserver: any;
|
||||
constructor(props: Readonly<Props & AbstractProps>) {
|
||||
super(props);
|
||||
this.stateNotes = [];
|
||||
|
||||
@@ -64,12 +96,12 @@ export default class Notes extends Abstract {
|
||||
this.reloadList();
|
||||
}
|
||||
|
||||
unlockContent(callback) {
|
||||
unlockContent(callback: { (): void; (): any } | undefined) {
|
||||
super.unlockContent(() => {
|
||||
// wait for the state.unlocked setState call to finish
|
||||
if (this.searching) {
|
||||
this.searching = false;
|
||||
this.options.setSearchTerm(null);
|
||||
this.options?.setSearchTerm(null);
|
||||
}
|
||||
|
||||
this.reloadHeaderBar();
|
||||
@@ -95,7 +127,13 @@ export default class Notes extends Abstract {
|
||||
}
|
||||
}
|
||||
|
||||
reloadTabletStateForEvent({ focus, blur }) {
|
||||
reloadTabletStateForEvent({
|
||||
focus,
|
||||
blur,
|
||||
}: {
|
||||
focus?: boolean;
|
||||
blur?: boolean;
|
||||
}) {
|
||||
if (focus) {
|
||||
if (!ApplicationState.get().isInTabletMode) {
|
||||
this.props.navigation.lockLeftDrawer(false);
|
||||
@@ -130,17 +168,18 @@ export default class Notes extends Abstract {
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
|
||||
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
|
||||
if (this.tabletModeChangeHandler) {
|
||||
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
|
||||
}
|
||||
Sync.get().removeEventHandler(this.syncObserver);
|
||||
Auth.get().removeEventHandler(this.signoutObserver);
|
||||
if (this.options) {
|
||||
if (this.options && this.optionsObserver) {
|
||||
this.options.removeChangeObserver(this.optionsObserver);
|
||||
}
|
||||
}
|
||||
|
||||
registerObservers() {
|
||||
this.optionsObserver = this.options.addChangeObserver(
|
||||
this.optionsObserver = this.options?.addChangeObserver(
|
||||
(options, eventType) => {
|
||||
this.reloadList(true);
|
||||
|
||||
@@ -163,11 +202,11 @@ export default class Notes extends Abstract {
|
||||
this.mappingObserver = ModelManager.get().addItemSyncObserver(
|
||||
'notes-screen',
|
||||
['Tag', 'Note'],
|
||||
(allRelevantItems, validItems, deletedItems) => {
|
||||
(allRelevantItems: any, validItems: any, deletedItems: any[]) => {
|
||||
if (deletedItems.find(item => item.content_type === 'Tag')) {
|
||||
// If a tag was deleted, let's check to see if we should reload our selected tags list
|
||||
var tags = ModelManager.get().getTagsWithIds(
|
||||
this.options.selectedTagIds
|
||||
this.options.selectedTagIds ?? []
|
||||
);
|
||||
if (tags.length === 0) {
|
||||
this.options.setSelectedTagIds(
|
||||
@@ -180,28 +219,30 @@ export default class Notes extends Abstract {
|
||||
}
|
||||
);
|
||||
|
||||
this.syncObserver = Sync.get().addEventHandler((event, data) => {
|
||||
if (event === 'sync:completed') {
|
||||
this.mergeState({ refreshing: false, loading: false });
|
||||
} else if (event === 'local-data-loaded') {
|
||||
this.displayNeedSignInAlertForLocalItemsIfApplicable(
|
||||
ModelManager.get().allItems
|
||||
);
|
||||
this.reloadList();
|
||||
this.reloadHeaderBar();
|
||||
this.mergeState({ decrypting: false, loading: false });
|
||||
if (ApplicationState.get().isInTabletMode) {
|
||||
this.selectFirstNote();
|
||||
this.syncObserver = Sync.get().addEventHandler(
|
||||
(event: string, data: any) => {
|
||||
if (event === 'sync:completed') {
|
||||
this.mergeState({ refreshing: false, loading: false });
|
||||
} else if (event === 'local-data-loaded') {
|
||||
this.displayNeedSignInAlertForLocalItemsIfApplicable(
|
||||
ModelManager.get().allItems
|
||||
);
|
||||
this.reloadList();
|
||||
this.reloadHeaderBar();
|
||||
this.mergeState({ decrypting: false, loading: false });
|
||||
if (ApplicationState.get().isInTabletMode) {
|
||||
this.selectFirstNote();
|
||||
}
|
||||
} else if (event === 'sync-exception') {
|
||||
Alert.alert(
|
||||
'Issue Syncing',
|
||||
`There was an error while trying to save your items. Please contact support and share this message: ${data}`
|
||||
);
|
||||
}
|
||||
} else if (event === 'sync-exception') {
|
||||
Alert.alert(
|
||||
'Issue Syncing',
|
||||
`There was an error while trying to save your items. Please contact support and share this message: ${data}`
|
||||
);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
this.signoutObserver = Auth.get().addEventHandler(event => {
|
||||
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
|
||||
if (event === SFAuthManager.DidSignOutEvent) {
|
||||
this.reloadList();
|
||||
} else if (event === SFAuthManager.WillSignInEvent) {
|
||||
@@ -217,14 +258,18 @@ export default class Notes extends Abstract {
|
||||
});
|
||||
|
||||
this.tabletModeChangeHandler = ApplicationState.get().addEventHandler(
|
||||
(event, data) => {
|
||||
(
|
||||
event: AppStateEventType,
|
||||
data: TabletModeChangeData | NoteSideMenuToggleChange | undefined
|
||||
) => {
|
||||
if (event === ApplicationState.KeyboardChangeEvent) {
|
||||
if (ApplicationState.get().isInTabletMode) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
} else if (event === ApplicationState.AppStateEventTabletModeChange) {
|
||||
const tableData = data as TabletModeChangeData;
|
||||
// If we are now in tablet mode after not being in tablet mode
|
||||
if (data.new_isInTabletMode && !data.old_isInTabletMode) {
|
||||
if (tableData.new_isInTabletMode && !tableData.old_isInTabletMode) {
|
||||
// Pop to root, if we are in Compose window.
|
||||
this.props.navigation.popToTop();
|
||||
setTimeout(() => {
|
||||
@@ -240,7 +285,7 @@ export default class Notes extends Abstract {
|
||||
}, 10);
|
||||
}
|
||||
|
||||
if (!data.new_isInTabletMode) {
|
||||
if (!tableData.new_isInTabletMode) {
|
||||
this.setState({ selectedNoteId: null });
|
||||
this.props.navigation.setParams({
|
||||
rightButton: null,
|
||||
@@ -260,7 +305,7 @@ export default class Notes extends Abstract {
|
||||
/* If there is at least one item that has an error decrypting, and there are no account keys saved,
|
||||
display an alert instructing the user to log in. This happens when restoring from iCloud and data is restored but keys are not.
|
||||
*/
|
||||
displayNeedSignInAlertForLocalItemsIfApplicable(items) {
|
||||
displayNeedSignInAlertForLocalItemsIfApplicable(items: any) {
|
||||
if (KeysManager.get().shouldPresentKeyRecoveryWizard()) {
|
||||
this.props.navigation.navigate(SCREEN_KEY_RECOVERY);
|
||||
return;
|
||||
@@ -317,7 +362,9 @@ export default class Notes extends Abstract {
|
||||
});
|
||||
}
|
||||
|
||||
async presentComposer(note) {
|
||||
async presentComposer(
|
||||
note?: { content?: { protected: any }; uuid: any } | null
|
||||
) {
|
||||
this.props.navigation.navigate(SCREEN_COMPOSE, {
|
||||
title: note ? 'Note' : 'Compose',
|
||||
noteId: note && note.uuid,
|
||||
@@ -326,7 +373,7 @@ export default class Notes extends Abstract {
|
||||
});
|
||||
}
|
||||
|
||||
reloadList(force) {
|
||||
reloadList(force?: boolean) {
|
||||
if (!this.visible && !this.willBeVisible && !force) {
|
||||
console.log('===Scheduling Notes Render Update===');
|
||||
this.loadNotesOnVisible = true;
|
||||
@@ -338,7 +385,7 @@ export default class Notes extends Abstract {
|
||||
const result = ModelManager.get().getNotes(this.options);
|
||||
const { notes, tags } = result;
|
||||
|
||||
this.setState({ notes: notes, tags: tags, refreshing: false });
|
||||
this.setState({ notes, tags: tags, refreshing: false });
|
||||
|
||||
// setState is async, but we need this value right away sometimes to select the first note of new set of notes
|
||||
this.stateNotes = notes;
|
||||
@@ -367,7 +414,7 @@ export default class Notes extends Abstract {
|
||||
});
|
||||
}
|
||||
|
||||
selectNote = note => {
|
||||
selectNote = (note?: { content: { protected: any }; uuid: any } | null) => {
|
||||
this.handlePrivilegedAction(
|
||||
note && note.content.protected,
|
||||
SFPrivilegesManager.ActionViewProtectedNotes,
|
||||
@@ -383,9 +430,16 @@ export default class Notes extends Abstract {
|
||||
);
|
||||
};
|
||||
|
||||
_onPressItem = (item: hash) => {
|
||||
_onPressItem = (
|
||||
item: {
|
||||
content: { protected: any; conflict_of: any };
|
||||
setDirty: (arg0: boolean) => void;
|
||||
uuid: any;
|
||||
errorDecrypting: any;
|
||||
} | null
|
||||
) => {
|
||||
const run = () => {
|
||||
if (item.content.conflict_of) {
|
||||
if (item && item.content.conflict_of) {
|
||||
item.content.conflict_of = null;
|
||||
item.setDirty(true);
|
||||
Sync.get().sync();
|
||||
@@ -394,7 +448,7 @@ export default class Notes extends Abstract {
|
||||
this.selectNote(item);
|
||||
};
|
||||
|
||||
if (item.errorDecrypting) {
|
||||
if (item && item.errorDecrypting) {
|
||||
this.props.navigation.navigate(SCREEN_SETTINGS);
|
||||
} else {
|
||||
run();
|
||||
@@ -408,7 +462,7 @@ export default class Notes extends Abstract {
|
||||
}
|
||||
};
|
||||
|
||||
onSearchTextChange = text => {
|
||||
onSearchTextChange = (text: string | null) => {
|
||||
this.searching = true;
|
||||
this.options.setSearchTerm(text);
|
||||
};
|
||||
@@ -418,7 +472,18 @@ export default class Notes extends Abstract {
|
||||
this.options.setSearchTerm(null);
|
||||
};
|
||||
|
||||
handleActionsheetAction = (item, action, callback) => {
|
||||
handleActionsheetAction = (
|
||||
item: {
|
||||
displayName: string;
|
||||
content: { trashed: boolean; protected: boolean };
|
||||
setDirty: (arg0: boolean) => void;
|
||||
setAppDataItem: (arg0: string, arg1: boolean) => void;
|
||||
title: any;
|
||||
text: any;
|
||||
},
|
||||
action: EventType,
|
||||
callback: () => void
|
||||
) => {
|
||||
const run = () => {
|
||||
ItemActionManager.handleEvent(
|
||||
action,
|
||||
@@ -479,17 +544,20 @@ export default class Notes extends Abstract {
|
||||
selectedTags={this.state.tags}
|
||||
options={this.options.displayOptions}
|
||||
selectedNoteId={
|
||||
ApplicationState.get().isInTabletMode && this.state.selectedNoteId
|
||||
ApplicationState.get().isInTabletMode
|
||||
? this.state.selectedNoteId
|
||||
: null
|
||||
}
|
||||
handleAction={this.handleActionsheetAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FAB
|
||||
// @ts-ignore style prop does not exist for types
|
||||
style={
|
||||
ApplicationState.get().isInTabletMode
|
||||
? { bottom: ApplicationState.get().getKeyboardHeight() }
|
||||
: null
|
||||
: undefined
|
||||
}
|
||||
buttonColor={StyleKit.variables.stylekitInfoColor}
|
||||
iconTextColor={StyleKit.variables.stylekitInfoContrastColor}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, TouchableWithoutFeedback } from 'react-native';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
TouchableWithoutFeedback,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import { withNavigation } from 'react-navigation';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import ThemedPureComponent from '@Components/ThemedPureComponent';
|
||||
@@ -10,7 +17,12 @@ import StyleKit from '@Style/StyleKit';
|
||||
const NOT_BACKED_UP_TEXT = 'Data not backed up';
|
||||
const SIGN_IN_TEXT = 'Sign in or register to backup your notes';
|
||||
|
||||
export class OfflineBanner extends ThemedPureComponent {
|
||||
type Props = {
|
||||
navigation: any;
|
||||
};
|
||||
|
||||
export class OfflineBanner extends ThemedPureComponent<Props> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
_onPress = () => {
|
||||
this.props.navigation.navigate(SCREEN_SETTINGS);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import React from 'react';
|
||||
import { TouchableHighlight, View } from 'react-native';
|
||||
import { TouchableHighlight, View, ViewStyle, TextStyle } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import { SFAuthManager } from 'snjs';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import ApplicationState, {
|
||||
AppStateEventHandler,
|
||||
AppStateEventType,
|
||||
TabletModeChangeData,
|
||||
NoteSideMenuToggleChange,
|
||||
AppStateType,
|
||||
} from '@Lib/ApplicationState';
|
||||
import KeysManager from '@Lib/keysManager';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
|
||||
import Compose from '@Screens/Compose';
|
||||
import Notes from '@Screens/Notes/Notes';
|
||||
import { SCREEN_AUTHENTICATE } from '@Screens/screens';
|
||||
@@ -14,9 +20,40 @@ import ModelManager from '@Lib/snjs/modelManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { hexToRGBA } from '@Style/utils';
|
||||
import AuthenticationSource from './Authentication/Sources/AuthenticationSource';
|
||||
|
||||
export default class Root extends Abstract {
|
||||
constructor(props) {
|
||||
type State = {
|
||||
shouldSplitLayout?: boolean;
|
||||
notesListCollapsed?: boolean;
|
||||
keyboardHeight?: number;
|
||||
selectedTagId?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
} & AbstractState;
|
||||
|
||||
export default class Root extends Abstract<AbstractProps, State> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
applicationStateEventHandler?: AppStateEventHandler;
|
||||
stateObserver?: {
|
||||
key: () => number;
|
||||
callback: (state: AppStateType) => void;
|
||||
};
|
||||
dataLoaded?: boolean;
|
||||
syncEventHandler?: any;
|
||||
didShowSessionInvalidAlert?: boolean;
|
||||
syncStatusObserver?: any;
|
||||
showingErrorStatus?: boolean;
|
||||
showingDownloadStatus?: boolean;
|
||||
signoutObserver?: any;
|
||||
authOnMount: any;
|
||||
authenticationInProgress: any;
|
||||
pendingAuthProps: any;
|
||||
syncTimer?: number;
|
||||
notesRef: any;
|
||||
composeRef: any;
|
||||
constructor(props: Readonly<AbstractProps>) {
|
||||
super(props);
|
||||
this.registerObservers();
|
||||
}
|
||||
@@ -37,11 +74,15 @@ export default class Root extends Abstract {
|
||||
});
|
||||
|
||||
this.applicationStateEventHandler = ApplicationState.get().addEventHandler(
|
||||
(event, data) => {
|
||||
(
|
||||
event: AppStateEventType,
|
||||
data: TabletModeChangeData | NoteSideMenuToggleChange | undefined
|
||||
) => {
|
||||
if (event === ApplicationState.AppStateEventNoteSideMenuToggle) {
|
||||
// update state to toggle Notes side menu if we triggered the collapse
|
||||
this.setState({
|
||||
notesListCollapsed: data.new_isNoteSideMenuCollapsed,
|
||||
notesListCollapsed: (data as NoteSideMenuToggleChange)
|
||||
.new_isNoteSideMenuCollapsed,
|
||||
});
|
||||
} else if (event === ApplicationState.KeyboardChangeEvent) {
|
||||
// need to refresh the height of the keyboard when it opens so that we can change the position
|
||||
@@ -55,52 +96,56 @@ export default class Root extends Abstract {
|
||||
}
|
||||
);
|
||||
|
||||
this.syncEventHandler = Sync.get().addEventHandler(async (event, data) => {
|
||||
if (event === 'sync-session-invalid') {
|
||||
if (!this.didShowSessionInvalidAlert) {
|
||||
this.didShowSessionInvalidAlert = true;
|
||||
AlertManager.get().confirm({
|
||||
title: 'Session Expired',
|
||||
text:
|
||||
'Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session.',
|
||||
confirmButtonText: 'Sign Out',
|
||||
onConfirm: () => {
|
||||
this.didShowSessionInvalidAlert = false;
|
||||
Auth.get().signout();
|
||||
},
|
||||
onCancel: () => {
|
||||
this.didShowSessionInvalidAlert = false;
|
||||
},
|
||||
});
|
||||
this.syncEventHandler = Sync.get().addEventHandler(
|
||||
async (event: string) => {
|
||||
if (event === 'sync-session-invalid') {
|
||||
if (!this.didShowSessionInvalidAlert) {
|
||||
this.didShowSessionInvalidAlert = true;
|
||||
AlertManager.get().confirm({
|
||||
title: 'Session Expired',
|
||||
text:
|
||||
'Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session.',
|
||||
confirmButtonText: 'Sign Out',
|
||||
onConfirm: () => {
|
||||
this.didShowSessionInvalidAlert = false;
|
||||
Auth.get().signout();
|
||||
},
|
||||
onCancel: () => {
|
||||
this.didShowSessionInvalidAlert = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
this.syncStatusObserver = Sync.get().registerSyncStatusObserver(status => {
|
||||
if (status.error) {
|
||||
const text = 'Unable to connect to sync server.';
|
||||
this.showingErrorStatus = true;
|
||||
setTimeout(() => {
|
||||
// need timeout for syncing on app launch
|
||||
this.setSubTitle(text, StyleKit.variables.stylekitWarningColor);
|
||||
}, 250);
|
||||
} else if (status.retrievedCount > 20) {
|
||||
const text = `Downloading ${status.retrievedCount} items. Keep app open.`;
|
||||
this.setSubTitle(text);
|
||||
this.showingDownloadStatus = true;
|
||||
} else if (this.showingDownloadStatus) {
|
||||
this.showingDownloadStatus = false;
|
||||
const text = 'Download Complete.';
|
||||
this.setSubTitle(text);
|
||||
setTimeout(() => {
|
||||
this.syncStatusObserver = Sync.get().registerSyncStatusObserver(
|
||||
(status: { error: any; retrievedCount: number }) => {
|
||||
if (status.error) {
|
||||
const text = 'Unable to connect to sync server.';
|
||||
this.showingErrorStatus = true;
|
||||
setTimeout(() => {
|
||||
// need timeout for syncing on app launch
|
||||
this.setSubTitle(text, StyleKit.variables.stylekitWarningColor);
|
||||
}, 250);
|
||||
} else if (status.retrievedCount > 20) {
|
||||
const text = `Downloading ${status.retrievedCount} items. Keep app open.`;
|
||||
this.setSubTitle(text);
|
||||
this.showingDownloadStatus = true;
|
||||
} else if (this.showingDownloadStatus) {
|
||||
this.showingDownloadStatus = false;
|
||||
const text = 'Download Complete.';
|
||||
this.setSubTitle(text);
|
||||
setTimeout(() => {
|
||||
this.setSubTitle(null);
|
||||
}, 2000);
|
||||
} else if (this.showingErrorStatus) {
|
||||
this.setSubTitle(null);
|
||||
}, 2000);
|
||||
} else if (this.showingErrorStatus) {
|
||||
this.setSubTitle(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
this.signoutObserver = Auth.get().addEventHandler(async event => {
|
||||
this.signoutObserver = Auth.get().addEventHandler(async (event: any) => {
|
||||
if (event === SFAuthManager.DidSignOutEvent) {
|
||||
this.setSubTitle(null);
|
||||
const notifyObservers = false;
|
||||
@@ -145,13 +190,20 @@ export default class Root extends Abstract {
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
ApplicationState.get().removeStateObserver(this.stateObserver);
|
||||
ApplicationState.get().removeEventHandler(
|
||||
this.applicationStateEventHandler
|
||||
);
|
||||
if (this.stateObserver) {
|
||||
ApplicationState.get().removeStateObserver(this.stateObserver);
|
||||
}
|
||||
if (this.applicationStateEventHandler) {
|
||||
ApplicationState.get().removeEventHandler(
|
||||
this.applicationStateEventHandler
|
||||
);
|
||||
}
|
||||
|
||||
Sync.get().removeEventHandler(this.syncEventHandler);
|
||||
Sync.get().removeSyncStatusObserver(this.syncStatusObserver);
|
||||
clearInterval(this.syncTimer);
|
||||
if (this.syncTimer !== undefined) {
|
||||
clearInterval(this.syncTimer);
|
||||
}
|
||||
}
|
||||
|
||||
/* Forward React Navigation lifecycle events to notes */
|
||||
@@ -198,7 +250,7 @@ export default class Root extends Abstract {
|
||||
this.setSubTitle(
|
||||
encryptionEnabled ? 'Decrypting items...' : 'Loading items...'
|
||||
);
|
||||
const incrementalCallback = (current, total) => {
|
||||
const incrementalCallback = (current: any, total: any) => {
|
||||
const notesString = `${current}/${total} items...`;
|
||||
this.setSubTitle(
|
||||
encryptionEnabled
|
||||
@@ -212,7 +264,7 @@ export default class Root extends Abstract {
|
||||
this.notesRef && this.notesRef.root_onIncrementalSync();
|
||||
};
|
||||
|
||||
const loadLocalCompletion = items => {
|
||||
const loadLocalCompletion = () => {
|
||||
this.setSubTitle('Syncing...');
|
||||
this.dataLoaded = true;
|
||||
|
||||
@@ -231,15 +283,27 @@ export default class Root extends Abstract {
|
||||
const batchSize = 100;
|
||||
Sync.get()
|
||||
.loadLocalItems({ incrementalCallback, batchSize })
|
||||
.then(items => {
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
loadLocalCompletion(items);
|
||||
loadLocalCompletion();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
presentAuthenticationModal(authProps) {
|
||||
presentAuthenticationModal(
|
||||
authProps:
|
||||
| {
|
||||
sources: AuthenticationSource[];
|
||||
title?: undefined;
|
||||
onAuthenticate?: undefined;
|
||||
}
|
||||
| {
|
||||
title: string;
|
||||
sources: AuthenticationSource[];
|
||||
onAuthenticate: () => void;
|
||||
}
|
||||
) {
|
||||
if (!this.isMounted()) {
|
||||
console.log('Not yet mounted, not authing.');
|
||||
this.authOnMount = authProps;
|
||||
@@ -270,7 +334,7 @@ export default class Root extends Abstract {
|
||||
this.props.navigation.navigate(SCREEN_AUTHENTICATE, {
|
||||
authenticationSources: authProps.sources,
|
||||
onSuccess: () => {
|
||||
authProps.onAuthenticate();
|
||||
authProps.onAuthenticate && authProps.onAuthenticate();
|
||||
this.pendingAuthProps = null;
|
||||
this.authenticationInProgress = false;
|
||||
if (this.dataLoaded) {
|
||||
@@ -283,7 +347,7 @@ export default class Root extends Abstract {
|
||||
});
|
||||
}
|
||||
|
||||
onNoteSelect = note => {
|
||||
onNoteSelect = (note: any) => {
|
||||
this.composeRef.setNote(note);
|
||||
this.setState({
|
||||
selectedTagId:
|
||||
@@ -292,7 +356,9 @@ export default class Root extends Abstract {
|
||||
});
|
||||
};
|
||||
|
||||
onLayout = e => {
|
||||
onLayout = (e: {
|
||||
nativeEvent: { layout: { width: any; height: any; x: any; y: any } };
|
||||
}) => {
|
||||
const width = e.nativeEvent.layout.width;
|
||||
/**
|
||||
If you're in tablet mode, but on an iPad where this app is running side by
|
||||
@@ -351,7 +417,7 @@ export default class Root extends Abstract {
|
||||
'-' +
|
||||
iconNames[collapseIconPrefix][notesListCollapsed ? 0 : 1];
|
||||
const collapseIconBottomPosition =
|
||||
this.state.keyboardHeight > this.state.height / 2
|
||||
(this.state.keyboardHeight ?? 0) > (this.state.height ?? 0) / 2
|
||||
? this.state.keyboardHeight
|
||||
: '50%';
|
||||
|
||||
@@ -369,7 +435,9 @@ export default class Root extends Abstract {
|
||||
onUnlockPress={this.onUnlockPress}
|
||||
navigation={this.props.navigation}
|
||||
onNoteSelect={
|
||||
shouldSplitLayout && this.onNoteSelect /* tablet only */
|
||||
shouldSplitLayout
|
||||
? this.onNoteSelect
|
||||
: undefined /* tablet only */
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -12,10 +12,33 @@ import StyleKit from '@Style/StyleKit';
|
||||
const DEFAULT_SIGN_IN_TEXT = 'Sign In';
|
||||
const DEFAULT_REGISTER_TEXT = 'Register';
|
||||
|
||||
export default class AuthSection extends Component {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
onAuthSuccess: () => void;
|
||||
title: string;
|
||||
};
|
||||
type State = {
|
||||
server?: string;
|
||||
showAdvanced: boolean;
|
||||
signingIn: boolean;
|
||||
signInButtonText: string;
|
||||
registerButtonText: string;
|
||||
password: string | null;
|
||||
email: string | null;
|
||||
registering: boolean;
|
||||
strictSignIn: boolean;
|
||||
mfa?: any;
|
||||
mfa_token?: string;
|
||||
confirmRegistration?: boolean;
|
||||
passwordConfirmation?: string;
|
||||
};
|
||||
|
||||
export default class AuthSection extends Component<Props, State> {
|
||||
static emailInProgress: any;
|
||||
static passwordInProgress: any;
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showAdvanced: false,
|
||||
email: AuthSection.emailInProgress,
|
||||
password: AuthSection.passwordInProgress,
|
||||
signingIn: false,
|
||||
@@ -60,9 +83,9 @@ export default class AuthSection extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const extraParams = {};
|
||||
const extraParams: Record<string, string | undefined> = {};
|
||||
if (this.state.mfa) {
|
||||
extraParams[this.state.mfa.payload.mfa_key] = this.state.mfa_token;
|
||||
extraParams[this.state.mfa?.payload.mfa_key] = this.state.mfa_token;
|
||||
}
|
||||
|
||||
const strict = this.state.strictSignIn;
|
||||
@@ -79,7 +102,7 @@ export default class AuthSection extends Component {
|
||||
|
||||
Auth.get()
|
||||
.login(this.state.server, email, password, strict, extraParams)
|
||||
.then(response => {
|
||||
.then((response: { error: any }) => {
|
||||
if (!response || response.error) {
|
||||
const error = response
|
||||
? response.error
|
||||
@@ -105,7 +128,7 @@ export default class AuthSection extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
validate(email, password) {
|
||||
validate(email: string | null, password: string | null) {
|
||||
if (!email) {
|
||||
Alert.alert('Missing Email', 'Please enter a valid email address.', [
|
||||
{ text: 'OK' },
|
||||
@@ -174,7 +197,7 @@ export default class AuthSection extends Component {
|
||||
|
||||
Auth.get()
|
||||
.register(this.state.server, this.state.email, this.state.password)
|
||||
.then(response => {
|
||||
.then((response: { error: any }) => {
|
||||
this.setState({ registering: false, confirmRegistration: false });
|
||||
|
||||
if (!response || response.error) {
|
||||
@@ -194,7 +217,7 @@ export default class AuthSection extends Component {
|
||||
this.setState({ mfa: null });
|
||||
};
|
||||
|
||||
emailInputChanged = text => {
|
||||
emailInputChanged = (text: string) => {
|
||||
this.setState({ email: text });
|
||||
/**
|
||||
* If you have a local passcode with immediate timing, and you're trying to
|
||||
@@ -205,7 +228,7 @@ export default class AuthSection extends Component {
|
||||
AuthSection.emailInProgress = text;
|
||||
};
|
||||
|
||||
passwordInputChanged = text => {
|
||||
passwordInputChanged = (text: string) => {
|
||||
this.setState({ password: text });
|
||||
AuthSection.passwordInProgress = text;
|
||||
};
|
||||
@@ -306,7 +329,7 @@ export default class AuthSection extends Component {
|
||||
style={StyleKit.styles.sectionedTableCellTextInput}
|
||||
placeholder={'Email'}
|
||||
onChangeText={text => this.emailInputChanged(text)}
|
||||
value={this.state.email}
|
||||
value={this.state.email ?? undefined}
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
keyboardType={'email-address'}
|
||||
@@ -323,7 +346,7 @@ export default class AuthSection extends Component {
|
||||
style={StyleKit.styles.sectionedTableCellTextInput}
|
||||
placeholder={'Password'}
|
||||
onChangeText={text => this.passwordInputChanged(text)}
|
||||
value={this.state.password}
|
||||
value={this.state.password ?? undefined}
|
||||
textContentType={'password'}
|
||||
secureTextEntry={true}
|
||||
keyboardAppearance={StyleKit.get().keyboardColorForActiveTheme()}
|
||||
|
||||
@@ -6,8 +6,12 @@ import TableSection from '@Components/TableSection';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class CompanySection extends Component {
|
||||
onAction = action => {
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export default class CompanySection extends Component<Props> {
|
||||
onAction = (action: string) => {
|
||||
if (action === 'feedback') {
|
||||
const platformString = Platform.OS === 'android' ? 'Android' : 'iOS';
|
||||
ApplicationState.openURL(
|
||||
|
||||
@@ -7,8 +7,16 @@ import KeysManager from '@Lib/keysManager';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class PasscodeSection extends Component {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
items: any[];
|
||||
};
|
||||
|
||||
export default class PasscodeSection extends Component<Props, State> {
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@@ -42,7 +50,7 @@ export default class PasscodeSection extends Component {
|
||||
const titleStyles = {
|
||||
color: StyleKit.variables.stylekitForegroundColor,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fontWeight: 'bold' as 'bold',
|
||||
};
|
||||
|
||||
const subtitleStyles = {
|
||||
|
||||
@@ -11,12 +11,26 @@ import KeysManager from '@Lib/keysManager';
|
||||
import moment from '@Lib/moment';
|
||||
import UserPrefsManager, { LAST_EXPORT_DATE_KEY } from '@Lib/userPrefsManager';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
|
||||
|
||||
import { SFPrivilegesManager } from 'snjs';
|
||||
|
||||
class OptionsSection extends Abstract {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
title: string;
|
||||
onSignOutPress: () => Promise<any>;
|
||||
onManagePrivileges: () => void;
|
||||
} & AbstractProps;
|
||||
|
||||
type State = {
|
||||
loadingExport: boolean;
|
||||
encryptionAvailable: boolean;
|
||||
email?: string | null;
|
||||
signedIn: boolean;
|
||||
lastExportDate?: Date;
|
||||
} & AbstractState;
|
||||
|
||||
class OptionsSection extends Abstract<Props, State> {
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
let encryptionAvailable = KeysManager.get().activeKeys() != null;
|
||||
let email = KeysManager.get().getUserEmail();
|
||||
@@ -38,7 +52,7 @@ class OptionsSection extends Abstract {
|
||||
});
|
||||
}
|
||||
|
||||
onExportPress = option => {
|
||||
onExportPress = (option: { key: string }) => {
|
||||
let encrypted = option.key === 'encrypted';
|
||||
if (encrypted && !this.state.encryptionAvailable) {
|
||||
Alert.alert(
|
||||
@@ -68,7 +82,7 @@ class OptionsSection extends Abstract {
|
||||
}
|
||||
this.setState({ loadingExport: false });
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
this.setState({ loadingExport: false });
|
||||
});
|
||||
},
|
||||
@@ -104,13 +118,15 @@ class OptionsSection extends Abstract {
|
||||
};
|
||||
|
||||
render() {
|
||||
let lastExportString, stale;
|
||||
let lastExportString;
|
||||
let stale: boolean = false;
|
||||
if (this.state.lastExportDate) {
|
||||
let formattedDate = moment(this.state.lastExportDate).format('lll');
|
||||
lastExportString = `Last exported on ${formattedDate}`;
|
||||
|
||||
// Date is stale if more than 7 days ago
|
||||
let staleThreshold = 7 * 86400;
|
||||
// @ts-ignore date type issue
|
||||
stale = (new Date() - this.state.lastExportDate) / 1000 > staleThreshold;
|
||||
} else {
|
||||
lastExportString = 'Your data has not yet been backed up.';
|
||||
@@ -138,13 +154,13 @@ class OptionsSection extends Abstract {
|
||||
onPress={this.onSignOutPress}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* some types don't exist on component */}
|
||||
<SectionedOptionsTableCell
|
||||
testID="exportData"
|
||||
last={!hasLastExportSection}
|
||||
// last={!hasLastExportSection}
|
||||
first={false}
|
||||
disabled={this.state.loadingExport}
|
||||
leftAligned={true}
|
||||
// disabled={this.state.loadingExport}
|
||||
// leftAligned={true}
|
||||
options={this.exportOptions()}
|
||||
title={this.state.loadingExport ? 'Processing...' : 'Export Data'}
|
||||
onPress={this.onExportPress}
|
||||
|
||||
@@ -5,13 +5,33 @@ import SectionHeader from '@Components/SectionHeader';
|
||||
import SectionedOptionsTableCell from '@Components/SectionedOptionsTableCell';
|
||||
import TableSection from '@Components/TableSection';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import KeysManager from '@Lib/keysManager';
|
||||
import KeysManager, { BiometricsType } from '@Lib/keysManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class PasscodeSection extends Component {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
hasBiometrics: boolean;
|
||||
hasPasscode: boolean;
|
||||
title: string;
|
||||
onDisable: () => void;
|
||||
onEnable: () => void;
|
||||
storageEncryption: boolean | null;
|
||||
onStorageEncryptionDisable: () => void;
|
||||
onStorageEncryptionEnable: () => void;
|
||||
storageEncryptionLoading: boolean;
|
||||
onFingerprintDisable: () => void;
|
||||
onFingerprintEnable: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
biometricsAvailable?: boolean;
|
||||
biometricsType?: BiometricsType;
|
||||
biometricsNoun?: string;
|
||||
};
|
||||
|
||||
export default class PasscodeSection extends Component<Props, State> {
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
let state = { biometricsAvailable: false || __DEV__ };
|
||||
let state: State = { biometricsAvailable: false || __DEV__ };
|
||||
if (__DEV__) {
|
||||
state.biometricsType = ApplicationState.isAndroid
|
||||
? 'Fingerprint'
|
||||
@@ -34,12 +54,12 @@ export default class PasscodeSection extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onPasscodeOptionPress = option => {
|
||||
onPasscodeOptionPress = (option: { key: string | null }) => {
|
||||
KeysManager.get().setPasscodeTiming(option.key);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onBiometricsOptionPress = option => {
|
||||
onBiometricsOptionPress = (option: { key: any }) => {
|
||||
KeysManager.get().setBiometricsTiming(option.key);
|
||||
this.forceUpdate();
|
||||
};
|
||||
@@ -53,7 +73,7 @@ export default class PasscodeSection extends Component {
|
||||
? 'Disable Storage Encryption'
|
||||
: 'Enable Storage Encryption'
|
||||
: 'Storage Encryption';
|
||||
let storageOnPress = this.props.storageEncryption
|
||||
let storageOnPress: (() => void) | null = this.props.storageEncryption
|
||||
? this.props.onStorageEncryptionDisable
|
||||
: this.props.onStorageEncryptionEnable;
|
||||
let storageSubText =
|
||||
@@ -109,7 +129,7 @@ export default class PasscodeSection extends Component {
|
||||
first={true}
|
||||
leftAligned={true}
|
||||
title={storageEncryptionTitle}
|
||||
onPress={storageOnPress}
|
||||
onPress={storageOnPress !== null ? storageOnPress : () => undefined}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
@@ -137,7 +157,6 @@ export default class PasscodeSection extends Component {
|
||||
|
||||
{this.props.hasPasscode && (
|
||||
<SectionedOptionsTableCell
|
||||
last={!this.props.hasBiometrics}
|
||||
title={'Require Passcode'}
|
||||
options={passcodeOptions}
|
||||
onPress={this.onPasscodeOptionPress}
|
||||
@@ -146,7 +165,6 @@ export default class PasscodeSection extends Component {
|
||||
|
||||
{this.props.hasBiometrics && (
|
||||
<SectionedOptionsTableCell
|
||||
last={true}
|
||||
title={`Require ${biometricsNoun}`}
|
||||
options={biometricOptions}
|
||||
onPress={this.onBiometricsOptionPress}
|
||||
|
||||
@@ -12,7 +12,7 @@ import AlertManager from '@Lib/snjs/alertManager';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import Storage from '@Lib/snjs/storageManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
|
||||
import { SCREEN_INPUT_MODAL, SCREEN_MANAGE_PRIVILEGES } from '@Screens/screens';
|
||||
import AuthSection from '@Screens/Settings/Sections/AuthSection';
|
||||
import CompanySection from '@Screens/Settings/Sections/CompanySection';
|
||||
@@ -23,9 +23,18 @@ import { ICON_CHECKMARK } from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
import { SFPrivilegesManager, protocolManager } from 'snjs';
|
||||
import OptionsState from '@Lib/OptionsState';
|
||||
|
||||
export default class Settings extends Abstract {
|
||||
static navigationOptions = ({ navigation, navigationOptions }) => {
|
||||
type State = {
|
||||
confirmRegistration: boolean;
|
||||
hasPasscode: boolean;
|
||||
storageEncryption: any;
|
||||
storageEncryptionLoading: boolean;
|
||||
hasBiometrics: boolean;
|
||||
} & AbstractState;
|
||||
|
||||
export default class Settings extends Abstract<AbstractProps, State> {
|
||||
static navigationOptions = ({ navigation, navigationOptions }: any) => {
|
||||
const templateOptions = {
|
||||
title: 'Settings',
|
||||
leftButton: {
|
||||
@@ -37,12 +46,15 @@ export default class Settings extends Abstract {
|
||||
};
|
||||
return Abstract.getDefaultNavigationOptions({
|
||||
navigation,
|
||||
navigationOptions,
|
||||
_navigationOptions: navigationOptions,
|
||||
templateOptions,
|
||||
});
|
||||
};
|
||||
sortOptions: { key: string; label: string }[];
|
||||
options: OptionsState;
|
||||
syncEventHandler: any;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<AbstractProps>) {
|
||||
super(props);
|
||||
|
||||
props.navigation.setParams({
|
||||
@@ -108,7 +120,7 @@ export default class Settings extends Abstract {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
resaveOfflineData(callback, updateAfter = false) {
|
||||
resaveOfflineData(callback: { (): void } | null, updateAfter = false) {
|
||||
Sync.get()
|
||||
.resaveOfflineData()
|
||||
.then(() => {
|
||||
@@ -181,14 +193,14 @@ export default class Settings extends Abstract {
|
||||
secureTextEntry: true,
|
||||
requireConfirm: true,
|
||||
showKeyboardChooser: true,
|
||||
onSubmit: async (value, keyboardType) => {
|
||||
onSubmit: async (value: any, keyboardType: string | null | undefined) => {
|
||||
Storage.get().setItem('passcodeKeyboardType', keyboardType);
|
||||
|
||||
let identifier = await protocolManager.crypto.generateUUID();
|
||||
|
||||
protocolManager
|
||||
.generateInitialKeysAndAuthParamsForUser(identifier, value)
|
||||
.then(results => {
|
||||
.then((results: { keys: any; authParams: any }) => {
|
||||
let keys = results.keys;
|
||||
let authParams = results.authParams;
|
||||
|
||||
@@ -262,12 +274,12 @@ export default class Settings extends Abstract {
|
||||
);
|
||||
};
|
||||
|
||||
onSortChange = key => {
|
||||
onSortChange = (key: string) => {
|
||||
this.options.setSortBy(key);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onOptionSelect = option => {
|
||||
onOptionSelect = (option: string) => {
|
||||
this.options.setDisplayOptionKeyValue(
|
||||
option,
|
||||
!this.options.getDisplayOptionValue(option)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Keyboard } from 'react-native';
|
||||
import Abstract from '@Screens/Abstract';
|
||||
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
|
||||
|
||||
export default class AbstractSideMenu extends Abstract {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
export default class AbstractSideMenu<
|
||||
AdditionalProps extends AbstractProps,
|
||||
AdditionalState extends AbstractState
|
||||
> extends Abstract<AdditionalProps, AdditionalState> {
|
||||
handler: any;
|
||||
shouldComponentUpdate(nextProps: Readonly<AbstractProps & AdditionalProps>) {
|
||||
/*
|
||||
We had some performance issues with this component rendering too many times when
|
||||
navigating to unrelated routes, like pushing Compose. It would render 6 times or so,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { View, FlatList } from 'react-native';
|
||||
import { View, FlatList, ViewStyle, TextStyle } from 'react-native';
|
||||
import FAB from 'react-native-fab';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import _ from 'lodash';
|
||||
@@ -21,21 +21,37 @@ import StyleKit from '@Style/StyleKit';
|
||||
import ThemeManager from '@Style/ThemeManager';
|
||||
import { LIGHT_MODE_KEY, DARK_MODE_KEY } from '@Style/utils';
|
||||
|
||||
import { SFAuthManager } from 'snjs';
|
||||
import { SFAuthManager, SNTheme as SNJSTheme } from 'snjs';
|
||||
import { Mode } from 'react-native-dark-mode';
|
||||
import { AbstractProps, AbstractState } from '@Screens/Abstract';
|
||||
|
||||
export default class MainSideMenu extends AbstractSideMenu {
|
||||
constructor(props) {
|
||||
type SNTheme = typeof SNJSTheme;
|
||||
|
||||
type State = {
|
||||
outOfSync: boolean;
|
||||
actionSheet: JSX.Element | null;
|
||||
} & AbstractState;
|
||||
|
||||
export default class MainSideMenu extends AbstractSideMenu<
|
||||
AbstractProps,
|
||||
State
|
||||
> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
signoutObserver: any;
|
||||
syncEventHandler: any;
|
||||
|
||||
constructor(props: Readonly<{ navigation: any }>) {
|
||||
super(props);
|
||||
this.constructState({});
|
||||
|
||||
this.signoutObserver = Auth.get().addEventHandler(event => {
|
||||
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
|
||||
if (event === SFAuthManager.DidSignOutEvent) {
|
||||
this.setState({ outOfSync: false });
|
||||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
this.syncEventHandler = Sync.get().addEventHandler((event, data) => {
|
||||
this.syncEventHandler = Sync.get().addEventHandler((event: string) => {
|
||||
if (event === 'enter-out-of-sync') {
|
||||
this.setState({ outOfSync: true });
|
||||
} else if (event === 'exit-out-of-sync') {
|
||||
@@ -70,12 +86,12 @@ export default class MainSideMenu extends AbstractSideMenu {
|
||||
return SideMenuManager.get().getHandlerForLeftSideMenu();
|
||||
}
|
||||
|
||||
onTagSelect = tag => {
|
||||
this.handler.onTagSelect(tag);
|
||||
onTagSelect = (tag: any) => {
|
||||
this.handler?.onTagSelect(tag);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onThemeSelect = theme => {
|
||||
onThemeSelect = (theme: SNTheme) => {
|
||||
/** Prevent themes that aren't meant for mobile from being activated. */
|
||||
if (theme.content.package_info && theme.content.package_info.no_mobile) {
|
||||
AlertManager.get().alert({
|
||||
@@ -94,7 +110,7 @@ export default class MainSideMenu extends AbstractSideMenu {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onThemeLongPress = theme => {
|
||||
onThemeLongPress = (theme: SNTheme) => {
|
||||
const actionSheetOptions = [];
|
||||
|
||||
/**
|
||||
@@ -169,7 +185,7 @@ export default class MainSideMenu extends AbstractSideMenu {
|
||||
sheet.show();
|
||||
};
|
||||
|
||||
onThemeRedownload(theme) {
|
||||
onThemeRedownload(theme: SNTheme) {
|
||||
AlertManager.get().confirm({
|
||||
title: 'Redownload Theme',
|
||||
text:
|
||||
@@ -181,7 +197,7 @@ export default class MainSideMenu extends AbstractSideMenu {
|
||||
});
|
||||
}
|
||||
|
||||
getModeActionForTheme({ theme, mode }) {
|
||||
getModeActionForTheme({ theme, mode }: { theme: SNTheme; mode: Mode }) {
|
||||
return ThemeManager.get().isThemeEnabledForMode({
|
||||
mode: mode,
|
||||
theme: theme,
|
||||
@@ -190,10 +206,10 @@ export default class MainSideMenu extends AbstractSideMenu {
|
||||
: 'Set as';
|
||||
}
|
||||
|
||||
iconDescriptorForTheme = theme => {
|
||||
iconDescriptorForTheme = (theme: SNTheme) => {
|
||||
const desc = {
|
||||
type: 'circle',
|
||||
side: 'right',
|
||||
side: 'right' as 'right',
|
||||
};
|
||||
|
||||
const dockIcon =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { View, FlatList } from 'react-native';
|
||||
import { View, FlatList, ViewStyle, TextStyle } from 'react-native';
|
||||
import FAB from 'react-native-fab';
|
||||
import { SFPrivilegesManager, SNTag } from 'snjs';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
@@ -7,16 +7,19 @@ import { SafeAreaView } from 'react-navigation';
|
||||
import LockedView from '@Containers/LockedView';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import ComponentManager from '@Lib/componentManager';
|
||||
import ItemActionManager from '@Lib/itemActionManager';
|
||||
import ItemActionManager, { EventType } from '@Lib/itemActionManager';
|
||||
import { SCREEN_INPUT_MODAL, SCREEN_MANAGE_PRIVILEGES } from '@Screens/screens';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
import PrivilegesManager from '@Lib/snjs/privilegesManager';
|
||||
import Sync from '@Lib/snjs/syncManager';
|
||||
import AbstractSideMenu from '@Screens/SideMenu/AbstractSideMenu';
|
||||
import SideMenuManager from '@Screens/SideMenu/SideMenuManager';
|
||||
import SideMenuSection from '@Screens/SideMenu/SideMenuSection';
|
||||
import SideMenuSection, {
|
||||
SideMenuOption,
|
||||
} from '@Screens/SideMenu/SideMenuSection';
|
||||
import TagSelectionList from '@Screens/SideMenu/TagSelectionList';
|
||||
import ActionSheetWrapper from '@Style/ActionSheetWrapper';
|
||||
import type { AbstractProps } from '@Screens/Abstract';
|
||||
import {
|
||||
ICON_BOOKMARK,
|
||||
ICON_ARCHIVE,
|
||||
@@ -29,8 +32,18 @@ import {
|
||||
} from '@Style/icons';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class NoteSideMenu extends AbstractSideMenu {
|
||||
constructor(props) {
|
||||
type State = {
|
||||
lockContent: boolean;
|
||||
outOfSync: boolean;
|
||||
actionSheet: JSX.Element | null;
|
||||
};
|
||||
|
||||
export default class NoteSideMenu extends AbstractSideMenu<
|
||||
AbstractProps,
|
||||
State
|
||||
> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
constructor(props: Readonly<AbstractProps>) {
|
||||
super(props);
|
||||
this.constructState({});
|
||||
}
|
||||
@@ -39,21 +52,27 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
return SideMenuManager.get().getHandlerForRightSideMenu();
|
||||
}
|
||||
|
||||
onEditorSelect = editor => {
|
||||
this.handler.onEditorSelect(editor);
|
||||
onEditorSelect = (editor: any) => {
|
||||
this.handler?.onEditorSelect(editor);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onTagSelect = tag => {
|
||||
this.handler.onTagSelect(tag);
|
||||
onTagSelect = (tag: any) => {
|
||||
this.handler?.onTagSelect(tag);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
get note() {
|
||||
return this.handler.getCurrentNote();
|
||||
return this.handler?.getCurrentNote();
|
||||
}
|
||||
|
||||
onEditorLongPress = editor => {
|
||||
onEditorLongPress = (
|
||||
editor: {
|
||||
content: any;
|
||||
name?: any;
|
||||
setDirty?: (arg0: boolean) => void;
|
||||
} | null
|
||||
) => {
|
||||
const currentDefaultEditor = ComponentManager.get().getDefaultEditor();
|
||||
|
||||
let isDefault = false;
|
||||
@@ -109,8 +128,8 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
this.props.navigation.navigate(SCREEN_INPUT_MODAL, {
|
||||
title: 'New Tag',
|
||||
placeholder: 'New tag name',
|
||||
onSubmit: text => {
|
||||
this.createTag(text, tag => {
|
||||
onSubmit: (text: string) => {
|
||||
this.createTag(text, (tag: any) => {
|
||||
if (this.note) {
|
||||
// select this tag
|
||||
this.onTagSelect(tag);
|
||||
@@ -120,7 +139,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
});
|
||||
};
|
||||
|
||||
createTag(text, callback) {
|
||||
createTag(text: string, callback: (tag: any) => void) {
|
||||
const tag = new SNTag({ content: { title: text } });
|
||||
tag.initUUID().then(() => {
|
||||
tag.setDirty(true);
|
||||
@@ -135,7 +154,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
Render
|
||||
*/
|
||||
|
||||
runAction(action) {
|
||||
runAction(action: EventType) {
|
||||
let run = () => {
|
||||
ItemActionManager.handleEvent(action, this.note, async () => {
|
||||
if (
|
||||
@@ -147,7 +166,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
this.popToRoot();
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
this.handler.onPropertyChange();
|
||||
this.handler?.onPropertyChange();
|
||||
|
||||
if (action === ItemActionManager.ProtectEvent) {
|
||||
// Show Privileges management screen if protected notes privs are not set up yet
|
||||
@@ -208,7 +227,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
{ text: lockOption, key: lockEvent, icon: ICON_LOCK },
|
||||
{ text: protectOption, key: protectEvent, icon: ICON_FINGER_PRINT },
|
||||
{ text: 'Share', key: ItemActionManager.ShareEvent, icon: ICON_SHARE },
|
||||
];
|
||||
] as { text: string; key: EventType; icon: string }[];
|
||||
|
||||
if (!this.note.content.trashed) {
|
||||
rawOptions.push({
|
||||
@@ -218,7 +237,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
});
|
||||
}
|
||||
|
||||
let options = [];
|
||||
let options: SideMenuOption[] = [];
|
||||
for (const rawOption of rawOptions) {
|
||||
let option = SideMenuSection.BuildOption({
|
||||
text: rawOption.text,
|
||||
@@ -246,7 +265,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
},
|
||||
{
|
||||
text: 'Delete Permanently',
|
||||
textClass: 'danger',
|
||||
textClass: 'danger' as 'danger',
|
||||
key: 'delete-forever',
|
||||
onSelect: () => {
|
||||
this.runAction(ItemActionManager.DeleteEvent);
|
||||
@@ -254,7 +273,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
},
|
||||
{
|
||||
text: 'Empty Trash',
|
||||
textClass: 'danger',
|
||||
textClass: 'danger' as 'danger',
|
||||
key: 'empty trash',
|
||||
onSelect: () => {
|
||||
this.runAction(ItemActionManager.EmptyTrashEvent);
|
||||
@@ -269,7 +288,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
buildOptionsForEditors() {
|
||||
const editors = ComponentManager.get()
|
||||
.getEditors()
|
||||
.sort((a, b) => {
|
||||
.sort((a: { name: string }, b: { name: string }) => {
|
||||
if (!a.name || !b.name) {
|
||||
return -1;
|
||||
}
|
||||
@@ -277,7 +296,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
const selectedEditor = ComponentManager.get().editorForNote(this.note);
|
||||
const options = [
|
||||
const options: SideMenuOption[] = [
|
||||
{
|
||||
text: 'Plain Editor',
|
||||
key: 'plain-editor',
|
||||
@@ -294,7 +313,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
|
||||
for (const editor of editors) {
|
||||
const option = SideMenuSection.BuildOption({
|
||||
text: editor.name,
|
||||
subtext: editor.content.isMobileDefault ? 'Mobile Default' : null,
|
||||
subtext: editor.content.isMobileDefault ? 'Mobile Default' : undefined,
|
||||
key: editor.uuid || editor.name,
|
||||
selected: editor === selectedEditor,
|
||||
onSelect: () => {
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import _ from 'lodash';
|
||||
import Circle from '@Components/Circle';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { hexToRGBA } from '@Style/utils';
|
||||
import { SideMenuOption } from './SideMenuSection';
|
||||
|
||||
export default class SideMenuCell extends ThemedComponent {
|
||||
type Props = SideMenuOption;
|
||||
|
||||
export default class SideMenuCell extends ThemedComponent<Props> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
componentDidUpdate() {
|
||||
this.updateStyles();
|
||||
}
|
||||
@@ -24,7 +33,7 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
}
|
||||
|
||||
onPress = () => {
|
||||
this.props.onSelect();
|
||||
this.props.onSelect && this.props.onSelect();
|
||||
};
|
||||
|
||||
onLongPress = () => {
|
||||
@@ -37,13 +46,13 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (desc.type === 'icon') {
|
||||
if (desc.type === 'icon' && desc.name) {
|
||||
return (
|
||||
<View style={this.styles.iconGraphic}>
|
||||
<Icon
|
||||
name={desc.name}
|
||||
size={desc.size || 20}
|
||||
color={this.styles.iconColor}
|
||||
color={StyleKit.variables.stylekitInfoColor}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -63,7 +72,7 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
aggregateStyles(base, addition, condition) {
|
||||
aggregateStyles(base: ViewStyle, addition: ViewStyle, condition?: boolean) {
|
||||
if (condition) {
|
||||
return [base, addition];
|
||||
} else {
|
||||
@@ -71,7 +80,7 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
colorForTextClass = textClass => {
|
||||
colorForTextClass = (textClass: Props['textClass']) => {
|
||||
if (!textClass) {
|
||||
return null;
|
||||
}
|
||||
@@ -86,7 +95,7 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
render() {
|
||||
const hasIcon = this.props.iconDesc;
|
||||
const iconSide =
|
||||
hasIcon && this.props.iconDesc.side
|
||||
hasIcon && this.props.iconDesc?.side
|
||||
? this.props.iconDesc.side
|
||||
: hasIcon
|
||||
? 'left'
|
||||
@@ -172,9 +181,6 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
|
||||
loadStyles() {
|
||||
this.styles = {
|
||||
iconColor: StyleKit.variables.stylekitInfoColor,
|
||||
selectionBgColor: hexToRGBA(StyleKit.variables.stylekitInfoColor, 0.1),
|
||||
|
||||
cell: {
|
||||
minHeight: this.props.subtext ? 52 : 42,
|
||||
},
|
||||
@@ -217,7 +223,7 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 15,
|
||||
paddingBottom: 0,
|
||||
fontFamily: ApplicationState.isAndroid ? 'Roboto' : null, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
|
||||
fontFamily: ApplicationState.isAndroid ? 'Roboto' : undefined, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
|
||||
},
|
||||
|
||||
subtext: {
|
||||
@@ -226,7 +232,7 @@ export default class SideMenuCell extends ThemedComponent {
|
||||
fontSize: 12,
|
||||
marginTop: -5,
|
||||
marginBottom: 3,
|
||||
fontFamily: ApplicationState.isAndroid ? 'Roboto' : null, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
|
||||
fontFamily: ApplicationState.isAndroid ? 'Roboto' : undefined, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
|
||||
},
|
||||
|
||||
iconGraphic: {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from 'react-native';
|
||||
import Circle from '@Components/Circle';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import KeysManager from '@Lib/keysManager';
|
||||
@@ -7,7 +13,14 @@ import Auth from '@Lib/snjs/authManager';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class SideMenuHero extends ThemedComponent {
|
||||
type Props = {
|
||||
onPress: () => void;
|
||||
outOfSync: boolean;
|
||||
onOutOfSyncPress: () => void;
|
||||
};
|
||||
|
||||
export default class SideMenuHero extends ThemedComponent<Props> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
getText() {
|
||||
const offline = Auth.get().offline();
|
||||
const hasEncryption =
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import MainSideMenu from './MainSideMenu';
|
||||
import NoteSideMenu from './NoteSideMenu';
|
||||
|
||||
/**
|
||||
* Because SideMenus (SideMenu and NoteSideMenu) are rendering by React
|
||||
* Navigation as drawer components on app startup, we can't give them params at
|
||||
@@ -8,16 +11,32 @@
|
||||
* This object will handle state for both side menus.
|
||||
*/
|
||||
export default class SideMenuManager {
|
||||
static instance = null;
|
||||
private static instance: SideMenuManager;
|
||||
leftSideMenu: MainSideMenu | null = null;
|
||||
rightSideMenu: NoteSideMenu | null = null;
|
||||
leftSideMenuHandler: {
|
||||
onTagSelect: (tag: any) => void;
|
||||
getSelectedTags: () => any;
|
||||
} | null = null;
|
||||
rightSideMenuHandler: {
|
||||
getCurrentNote: () => any;
|
||||
onEditorSelect: (editor: any) => void;
|
||||
onPropertyChange: () => void;
|
||||
onTagSelect: (tag: any) => void;
|
||||
getSelectedTags: () => any;
|
||||
onKeyboardDismiss: () => void;
|
||||
} | null = null;
|
||||
leftSideMenuLocked?: boolean;
|
||||
rightSideMenuLocked?: boolean;
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new SideMenuManager();
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
setLeftSideMenuReference(ref) {
|
||||
setLeftSideMenuReference(ref: MainSideMenu | null) {
|
||||
/**
|
||||
* The ref handler of the main component sometimes passes null, then passes
|
||||
* the correct reference
|
||||
@@ -27,7 +46,7 @@ export default class SideMenuManager {
|
||||
}
|
||||
}
|
||||
|
||||
setRightSideMenuReference(ref) {
|
||||
setRightSideMenuReference(ref: NoteSideMenu | null) {
|
||||
/**
|
||||
* The ref handler of the main component sometimes passes null, then passes
|
||||
* the correct reference
|
||||
@@ -42,7 +61,7 @@ export default class SideMenuManager {
|
||||
* @param handler.onTagSelect
|
||||
* @param handler.getSelectedTags
|
||||
*/
|
||||
setHandlerForLeftSideMenu(handler) {
|
||||
setHandlerForLeftSideMenu(handler: SideMenuManager['leftSideMenuHandler']) {
|
||||
this.leftSideMenuHandler = handler;
|
||||
|
||||
return handler;
|
||||
@@ -53,7 +72,7 @@ export default class SideMenuManager {
|
||||
* @param handler.getSelectedTags
|
||||
* @param handler.getCurrentNote
|
||||
*/
|
||||
setHandlerForRightSideMenu(handler) {
|
||||
setHandlerForRightSideMenu(handler: SideMenuManager['rightSideMenuHandler']) {
|
||||
this.rightSideMenuHandler = handler;
|
||||
|
||||
this.rightSideMenu && this.rightSideMenu.forceUpdate();
|
||||
@@ -69,7 +88,9 @@ export default class SideMenuManager {
|
||||
return this.rightSideMenuHandler;
|
||||
}
|
||||
|
||||
removeHandlerForRightSideMenu(handler) {
|
||||
removeHandlerForRightSideMenu(
|
||||
handler: SideMenuManager['rightSideMenuHandler']
|
||||
) {
|
||||
// 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) {
|
||||
@@ -77,11 +98,11 @@ export default class SideMenuManager {
|
||||
}
|
||||
}
|
||||
|
||||
setLockedForLeftSideMenu(locked) {
|
||||
setLockedForLeftSideMenu(locked: boolean) {
|
||||
this.leftSideMenuLocked = locked;
|
||||
}
|
||||
|
||||
setLockedForRightSideMenu(locked) {
|
||||
setLockedForRightSideMenu(locked: boolean) {
|
||||
this.rightSideMenuLocked = locked;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,47 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import SideMenuCell from '@Screens/SideMenu/SideMenuCell';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
export default class SideMenuSection extends ThemedComponent {
|
||||
type Props = {
|
||||
title: string;
|
||||
collapsed?: boolean;
|
||||
options?: any[];
|
||||
};
|
||||
|
||||
type State = {
|
||||
collapsed: boolean;
|
||||
};
|
||||
|
||||
export type SideMenuOption = {
|
||||
text: string;
|
||||
subtext?: string;
|
||||
textClass?: 'info' | 'danger' | 'warning';
|
||||
key?: string;
|
||||
iconDesc?: {
|
||||
type: string;
|
||||
side?: 'left' | 'right';
|
||||
name?: string;
|
||||
value?: string;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
size?: number;
|
||||
};
|
||||
dimmed?: boolean;
|
||||
selected?: boolean;
|
||||
onSelect?: () => void;
|
||||
onLongPress?: () => void;
|
||||
};
|
||||
|
||||
export default class SideMenuSection extends ThemedComponent<Props, State> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
static BuildOption({
|
||||
text,
|
||||
subtext,
|
||||
@@ -15,7 +52,7 @@ export default class SideMenuSection extends ThemedComponent {
|
||||
selected,
|
||||
onSelect,
|
||||
onLongPress,
|
||||
}) {
|
||||
}: SideMenuOption) {
|
||||
return {
|
||||
text,
|
||||
subtext,
|
||||
@@ -29,9 +66,9 @@ export default class SideMenuSection extends ThemedComponent {
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
this.state = { collapsed: props.collapsed };
|
||||
this.state = { collapsed: props.collapsed ?? false };
|
||||
}
|
||||
|
||||
toggleCollapse = () => {
|
||||
@@ -51,7 +88,8 @@ export default class SideMenuSection extends ThemedComponent {
|
||||
this.styles.header,
|
||||
this.state.collapsed ? this.styles.collapsedHeader : null,
|
||||
]}
|
||||
underlayColor={StyleKit.variables.stylekitBorderColor}
|
||||
// TODO: removed prop
|
||||
// underlayColor={StyleKit.variables.stylekitBorderColor}
|
||||
onPress={this.toggleCollapse}
|
||||
>
|
||||
<Fragment>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { View, Text, FlatList } from 'react-native';
|
||||
import { View, Text, FlatList, ViewStyle, TextStyle } from 'react-native';
|
||||
import { withNavigation } from 'react-navigation';
|
||||
import ThemedComponent from '@Components/ThemedComponent';
|
||||
import ItemActionManager from '@Lib/itemActionManager';
|
||||
@@ -13,15 +13,34 @@ import StyleKit from '@Style/StyleKit';
|
||||
|
||||
import { SFAuthManager } from 'snjs';
|
||||
|
||||
class TagSelectionList extends ThemedComponent {
|
||||
type Props = {
|
||||
contentType: string;
|
||||
onTagSelect: (tag: string) => void;
|
||||
navigation: any;
|
||||
selectedTags: any[];
|
||||
emptyPlaceholder?: string;
|
||||
hasBottomPadding?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
actionSheet: JSX.Element | null;
|
||||
tags: any[];
|
||||
};
|
||||
|
||||
class TagSelectionList extends ThemedComponent<Props, State> {
|
||||
styles!: Record<string, ViewStyle | TextStyle>;
|
||||
handledDataLoad: any;
|
||||
signoutObserver: any;
|
||||
syncObserverId: string | undefined;
|
||||
syncEventHandler: any;
|
||||
/*
|
||||
@param props.selectedTags
|
||||
@param props.onTagSelect
|
||||
*/
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Readonly<Props>) {
|
||||
super(props);
|
||||
this.state = { tags: [] };
|
||||
this.state = { tags: [], actionSheet: null };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -35,7 +54,7 @@ class TagSelectionList extends ThemedComponent {
|
||||
|
||||
this.reload();
|
||||
|
||||
this.signoutObserver = Auth.get().addEventHandler(event => {
|
||||
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
|
||||
if (event === SFAuthManager.DidSignOutEvent) {
|
||||
this.reload();
|
||||
}
|
||||
@@ -51,7 +70,7 @@ class TagSelectionList extends ThemedComponent {
|
||||
}
|
||||
);
|
||||
|
||||
this.syncEventHandler = Sync.get().addEventHandler((event, data) => {
|
||||
this.syncEventHandler = Sync.get().addEventHandler((event: string) => {
|
||||
if (event === 'local-data-loaded') {
|
||||
handleInitialDataLoad();
|
||||
}
|
||||
@@ -79,11 +98,18 @@ class TagSelectionList extends ThemedComponent {
|
||||
Tag Options
|
||||
*/
|
||||
|
||||
onTagSelect = tag => {
|
||||
onTagSelect = (tag: string) => {
|
||||
this.props.onTagSelect(tag);
|
||||
};
|
||||
|
||||
showActionSheet = tag => {
|
||||
showActionSheet = (tag: {
|
||||
content: any;
|
||||
title: any;
|
||||
setDirty: any;
|
||||
displayName: string;
|
||||
setAppDataItem: (arg0: string, arg1: boolean) => void;
|
||||
text: string;
|
||||
}) => {
|
||||
if (tag.content.isSystemTag) {
|
||||
return;
|
||||
}
|
||||
@@ -98,7 +124,7 @@ class TagSelectionList extends ThemedComponent {
|
||||
title: 'Rename Tag',
|
||||
placeholder: 'Tag name',
|
||||
initialValue: tag.title,
|
||||
onSubmit: text => {
|
||||
onSubmit: (text: string) => {
|
||||
if (tag) {
|
||||
tag.title = text; // Update the text on the tag to the input text
|
||||
tag.setDirty(true);
|
||||
@@ -133,7 +159,7 @@ class TagSelectionList extends ThemedComponent {
|
||||
sheet.show();
|
||||
};
|
||||
|
||||
iconDescriptorForTag = tag => {
|
||||
iconDescriptorForTag = () => {
|
||||
return {
|
||||
type: 'ascii',
|
||||
value: '#',
|
||||
@@ -141,7 +167,7 @@ class TagSelectionList extends ThemedComponent {
|
||||
};
|
||||
|
||||
// must pass title, text, and tags as props so that it re-renders when either of those change
|
||||
renderTagCell = ({ item }) => {
|
||||
renderTagCell = ({ item }: any) => {
|
||||
let title = item.deleted ? 'Deleting...' : item.title;
|
||||
if (item.errorDecrypting) {
|
||||
title = 'Unable to Decrypt';
|
||||
@@ -154,7 +180,7 @@ class TagSelectionList extends ThemedComponent {
|
||||
}}
|
||||
onLongPress={() => this.showActionSheet(item)}
|
||||
text={title}
|
||||
iconDesc={this.iconDescriptorForTag(item)}
|
||||
iconDesc={this.iconDescriptorForTag()}
|
||||
key={item.uuid}
|
||||
selected={this.props.selectedTags.includes(item)}
|
||||
/>
|
||||
|
||||
@@ -4,8 +4,27 @@ import ActionSheet from 'react-native-actionsheet';
|
||||
import ApplicationState from '@Lib/ApplicationState';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
|
||||
type Option =
|
||||
| {
|
||||
text: string;
|
||||
key?: string;
|
||||
callback: () => void;
|
||||
destructive?: boolean;
|
||||
}
|
||||
| {
|
||||
text: string;
|
||||
key?: string;
|
||||
callback: (option: Option) => void;
|
||||
destructive?: boolean;
|
||||
};
|
||||
|
||||
export default class ActionSheetWrapper {
|
||||
static BuildOption({ text, key, callback, destructive }) {
|
||||
options: Option[];
|
||||
destructiveIndex?: number;
|
||||
cancelIndex: number;
|
||||
title: string;
|
||||
actionSheet = React.createRef<ActionSheet>();
|
||||
static BuildOption({ text, key, callback, destructive }: Option) {
|
||||
return {
|
||||
text,
|
||||
key,
|
||||
@@ -14,24 +33,31 @@ export default class ActionSheetWrapper {
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ title, options, onCancel }) {
|
||||
options.push({ text: 'Cancel', callback: onCancel });
|
||||
this.options = options;
|
||||
constructor(props: {
|
||||
title: string;
|
||||
options: Option[];
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const cancelOption: Option[] = [
|
||||
{
|
||||
text: 'Cancel',
|
||||
callback: props.onCancel,
|
||||
key: 'CancelItem',
|
||||
destructive: false,
|
||||
},
|
||||
];
|
||||
this.options = props.options.concat(cancelOption);
|
||||
|
||||
this.destructiveIndex = this.options.indexOf(
|
||||
this.options.find(candidate => {
|
||||
return candidate.destructive;
|
||||
})
|
||||
);
|
||||
this.destructiveIndex = this.options.findIndex(item => item.destructive);
|
||||
this.cancelIndex = this.options.length - 1;
|
||||
this.title = title;
|
||||
this.title = props.title;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.actionSheet.show();
|
||||
this.actionSheet.current?.show();
|
||||
}
|
||||
|
||||
handleActionSheetPress = index => {
|
||||
handleActionSheetPress = (index: number) => {
|
||||
let option = this.options[index];
|
||||
option.callback && option.callback(option);
|
||||
};
|
||||
@@ -39,7 +65,7 @@ export default class ActionSheetWrapper {
|
||||
actionSheetElement() {
|
||||
return (
|
||||
<ActionSheet
|
||||
ref={o => (this.actionSheet = o)}
|
||||
ref={this.actionSheet}
|
||||
title={this.title}
|
||||
options={this.options.map(option => {
|
||||
return option.text;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { StatusBar, Alert, Platform } from 'react-native';
|
||||
import { StatusBar, Alert, Platform, ViewStyle, TextStyle } from 'react-native';
|
||||
import IconChanger from 'react-native-alternate-icons';
|
||||
import { supportsDarkMode } from 'react-native-dark-mode';
|
||||
import { supportsDarkMode, Mode } from 'react-native-dark-mode';
|
||||
import _ from 'lodash';
|
||||
import Auth from '@Lib/snjs/authManager';
|
||||
import ModelManager from '@Lib/snjs/modelManager';
|
||||
@@ -14,16 +14,26 @@ import {
|
||||
LIGHT_MODE_KEY,
|
||||
} from '@Style/utils';
|
||||
import ThemeDownloader from '@Style/Util/ThemeDownloader';
|
||||
import { SFAuthManager, SNTheme } from 'snjs';
|
||||
import { SFAuthManager, SNTheme as SNJSTheme } from 'snjs';
|
||||
|
||||
import THEME_RED_JSON from './Themes/red.json';
|
||||
import THEME_BLUE_JSON from './Themes/blue.json';
|
||||
|
||||
type SNTheme = typeof SNJSTheme;
|
||||
|
||||
export default class StyleKit {
|
||||
static instance = null;
|
||||
private static instance: StyleKit;
|
||||
themeChangeObservers: Array<() => void>;
|
||||
activeTheme: SNTheme;
|
||||
mode?: Mode;
|
||||
currentDarkMode!: Mode;
|
||||
systemThemes: Array<SNTheme>;
|
||||
constants: { mainTextFontSize: number; paddingLeft: number };
|
||||
styles: Record<string, ViewStyle | TextStyle> = {};
|
||||
signoutObserver: any;
|
||||
|
||||
static get() {
|
||||
if (this.instance === null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new StyleKit();
|
||||
}
|
||||
|
||||
@@ -32,14 +42,19 @@ export default class StyleKit {
|
||||
|
||||
constructor() {
|
||||
this.themeChangeObservers = [];
|
||||
this.systemThemes = [];
|
||||
|
||||
this.constants = {
|
||||
mainTextFontSize: 16,
|
||||
paddingLeft: 14,
|
||||
};
|
||||
|
||||
this.buildConstants();
|
||||
this.buildDefaultThemes();
|
||||
|
||||
ModelManager.get().addItemSyncObserver(
|
||||
'themes',
|
||||
'SN|Theme',
|
||||
(allItems, validItems, deletedItems, source) => {
|
||||
(_allItems: any, _validItems: any, deletedItems: any, _source: any) => {
|
||||
if (
|
||||
this.activeTheme &&
|
||||
!this.activeTheme.isSystemTheme &&
|
||||
@@ -62,7 +77,7 @@ export default class StyleKit {
|
||||
}
|
||||
);
|
||||
|
||||
this.signoutObserver = Auth.get().addEventHandler(event => {
|
||||
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
|
||||
if (event === SFAuthManager.DidSignOutEvent) {
|
||||
this.resetToSystemTheme();
|
||||
}
|
||||
@@ -74,16 +89,16 @@ export default class StyleKit {
|
||||
await this.resolveInitialTheme();
|
||||
}
|
||||
|
||||
setModeTo(mode) {
|
||||
setModeTo(mode: Mode) {
|
||||
this.currentDarkMode = mode;
|
||||
}
|
||||
|
||||
addThemeChangeObserver(observer) {
|
||||
addThemeChangeObserver(observer: () => void) {
|
||||
this.themeChangeObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
removeThemeChangeObserver(observer) {
|
||||
removeThemeChangeObserver(observer: () => void) {
|
||||
_.pull(this.themeChangeObservers, observer);
|
||||
}
|
||||
|
||||
@@ -93,7 +108,7 @@ export default class StyleKit {
|
||||
}
|
||||
}
|
||||
|
||||
assignThemeForMode({ theme, mode }) {
|
||||
assignThemeForMode({ theme, mode }: { theme: SNTheme; mode: Mode }) {
|
||||
if (!StyleKit.doesDeviceSupportDarkMode()) {
|
||||
mode = LIGHT_MODE_KEY;
|
||||
}
|
||||
@@ -138,7 +153,7 @@ export default class StyleKit {
|
||||
variables.statusBar =
|
||||
Platform.OS === 'android' ? LIGHT_CONTENT : DARK_CONTENT;
|
||||
|
||||
const theme = new SNTheme({
|
||||
const theme = new SNJSTheme({
|
||||
uuid: option.name,
|
||||
content: {
|
||||
isSystemTheme: true,
|
||||
@@ -194,7 +209,7 @@ export default class StyleKit {
|
||||
if (matchingTheme) {
|
||||
newTheme = matchingTheme;
|
||||
} else {
|
||||
newTheme = new SNTheme(themeData);
|
||||
newTheme = new SNJSTheme(themeData);
|
||||
newTheme.isSwapIn = true;
|
||||
}
|
||||
|
||||
@@ -210,20 +225,25 @@ export default class StyleKit {
|
||||
}
|
||||
|
||||
themes() {
|
||||
const themes = ModelManager.get().themes.sort((a, b) => {
|
||||
if (!a.name || !b.name) {
|
||||
return -1;
|
||||
const themes = ModelManager.get().themes.sort(
|
||||
(
|
||||
a: { name: { toLowerCase: () => number } },
|
||||
b: { name: { toLowerCase: () => number } }
|
||||
) => {
|
||||
if (!a.name || !b.name) {
|
||||
return -1;
|
||||
}
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
);
|
||||
return this.systemThemes.concat(themes);
|
||||
}
|
||||
|
||||
isThemeActive(theme) {
|
||||
isThemeActive(theme: SNTheme) {
|
||||
return this.activeTheme && theme.uuid === this.activeTheme.uuid;
|
||||
}
|
||||
|
||||
setActiveTheme(theme) {
|
||||
setActiveTheme(theme: SNTheme) {
|
||||
/** Merge default variables to ensure this theme has all the variables. */
|
||||
const variables = theme.content.variables;
|
||||
theme.content.variables = _.merge(this.templateVariables(), variables);
|
||||
@@ -244,7 +264,7 @@ export default class StyleKit {
|
||||
* - Status Bar color
|
||||
* - Local App Icon color
|
||||
*/
|
||||
updateDeviceForTheme(theme) {
|
||||
updateDeviceForTheme(theme: SNTheme) {
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
|
||||
/** On Android, a time out is required, especially during app startup. */
|
||||
@@ -290,7 +310,12 @@ export default class StyleKit {
|
||||
}
|
||||
}
|
||||
|
||||
activateTheme(theme) {
|
||||
activateTheme(theme: {
|
||||
content: { variables: { stylekitInfoColor: any } };
|
||||
setDirty: (arg0: boolean) => void;
|
||||
getNotAvailOnMobile: () => any;
|
||||
setNotAvailOnMobile: (arg0: boolean) => void;
|
||||
}) {
|
||||
const performActivation = () => {
|
||||
this.setActiveTheme(theme);
|
||||
this.assignThemeForMode({ theme: theme, mode: this.currentDarkMode });
|
||||
@@ -305,7 +330,7 @@ export default class StyleKit {
|
||||
if (!hasValidInfoColor) {
|
||||
ThemeDownloader.get()
|
||||
.downloadTheme(theme)
|
||||
.then(variables => {
|
||||
.then((variables: any) => {
|
||||
if (!variables) {
|
||||
Alert.alert(
|
||||
'Not Available',
|
||||
@@ -353,7 +378,16 @@ export default class StyleKit {
|
||||
}
|
||||
}
|
||||
|
||||
async downloadThemeAndReload(theme) {
|
||||
async downloadThemeAndReload(theme: {
|
||||
content: {
|
||||
package_info: { no_mobile: any };
|
||||
isSystemTheme: any;
|
||||
variables: any;
|
||||
};
|
||||
name?: any;
|
||||
setDirty: (arg0: boolean) => void;
|
||||
uuid?: any;
|
||||
}) {
|
||||
const updatedVariables = await ThemeDownloader.get().downloadTheme(theme);
|
||||
|
||||
/** Merge default variables to ensure this theme has all the variables. */
|
||||
@@ -551,13 +585,6 @@ export default class StyleKit {
|
||||
};
|
||||
}
|
||||
|
||||
buildConstants() {
|
||||
this.constants = {
|
||||
mainTextFontSize: 16,
|
||||
paddingLeft: 14,
|
||||
};
|
||||
}
|
||||
|
||||
static doesDeviceSupportDarkMode() {
|
||||
const isAndroid = Platform.OS === 'android';
|
||||
|
||||
@@ -573,7 +600,7 @@ export default class StyleKit {
|
||||
return supportsDarkMode;
|
||||
}
|
||||
|
||||
static variable(name) {
|
||||
static variable(name: string) {
|
||||
return this.get().activeTheme.content.variables[name];
|
||||
}
|
||||
|
||||
@@ -589,7 +616,7 @@ export default class StyleKit {
|
||||
return this.get().styles;
|
||||
}
|
||||
|
||||
static stylesForKey(key) {
|
||||
static stylesForKey(key: string) {
|
||||
const allStyles = this.styles;
|
||||
const styles = [allStyles[key]];
|
||||
const platform = Platform.OS === 'android' ? 'Android' : 'IOS';
|
||||
@@ -604,7 +631,7 @@ export default class StyleKit {
|
||||
return Platform.OS === 'android' ? 'md' : 'ios';
|
||||
}
|
||||
|
||||
static nameForIcon(iconName) {
|
||||
static nameForIcon(iconName: string) {
|
||||
return StyleKit.platformIconPrefix() + '-' + iconName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { SFItemParams } from 'snjs';
|
||||
import { SFItemParams, SNTheme as SNJSTheme } from 'snjs';
|
||||
import _ from 'lodash';
|
||||
import UserPrefsManager from '@Lib/userPrefsManager';
|
||||
import { isNullOrUndefined } from '@Lib/utils';
|
||||
import Storage from '@Lib/snjs/storageManager';
|
||||
import StyleKit from '@Style/StyleKit';
|
||||
import { LIGHT_MODE_KEY } from '@Style/utils';
|
||||
import { Mode } from 'react-native-dark-mode';
|
||||
|
||||
const THEME_PREFERENCES_KEY = 'ThemePreferencesKey';
|
||||
const LIGHT_THEME_KEY = 'lightTheme';
|
||||
const DARK_THEME_KEY = 'darkTheme';
|
||||
|
||||
function getThemeKeyForMode(mode) {
|
||||
function getThemeKeyForMode(mode: Mode) {
|
||||
return mode === LIGHT_MODE_KEY ? LIGHT_THEME_KEY : DARK_THEME_KEY;
|
||||
}
|
||||
|
||||
type SNTheme = typeof SNJSTheme;
|
||||
|
||||
type ThemeManagerData = {
|
||||
[LIGHT_THEME_KEY]: any;
|
||||
[DARK_THEME_KEY]: any;
|
||||
};
|
||||
|
||||
export default class ThemeManager {
|
||||
static instance = null;
|
||||
private static instance: ThemeManager;
|
||||
data: ThemeManagerData | null = null;
|
||||
saveAction: any;
|
||||
|
||||
static get() {
|
||||
if (isNullOrUndefined(this.instance)) {
|
||||
@@ -25,8 +35,6 @@ export default class ThemeManager {
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.data = null;
|
||||
|
||||
await this.runPendingMigrations();
|
||||
}
|
||||
|
||||
@@ -75,23 +83,23 @@ export default class ThemeManager {
|
||||
await UserPrefsManager.get().clearPref({ key: savedThemeKey });
|
||||
}
|
||||
|
||||
getThemeUuidForMode(mode) {
|
||||
getThemeUuidForMode(mode: Mode) {
|
||||
const pref = this.getThemeForMode(mode);
|
||||
return pref && pref.uuid;
|
||||
}
|
||||
|
||||
isThemeEnabledForMode({ mode, theme }) {
|
||||
isThemeEnabledForMode({ mode, theme }: { mode: Mode; theme: SNTheme }) {
|
||||
const pref = this.getThemeForMode(mode);
|
||||
return pref.uuid === theme.uuid;
|
||||
}
|
||||
|
||||
getThemeForMode(mode) {
|
||||
return this.data[getThemeKeyForMode(mode)];
|
||||
getThemeForMode(mode: Mode) {
|
||||
return this.data![getThemeKeyForMode(mode)];
|
||||
}
|
||||
|
||||
async setThemeForMode({ mode, theme }) {
|
||||
async setThemeForMode({ mode, theme }: { mode: Mode; theme: SNTheme }) {
|
||||
const themeData = await this.buildThemeDataForTheme(theme);
|
||||
this.data[getThemeKeyForMode(mode)] = themeData;
|
||||
this.data![getThemeKeyForMode(mode)] = themeData;
|
||||
this.saveToStorage();
|
||||
}
|
||||
|
||||
@@ -104,7 +112,7 @@ export default class ThemeManager {
|
||||
};
|
||||
}
|
||||
|
||||
async buildThemeDataForTheme(theme) {
|
||||
async buildThemeDataForTheme(theme: SNTheme) {
|
||||
const transformer = new SFItemParams(theme);
|
||||
return transformer.paramsForLocalStorage();
|
||||
}
|
||||
|
||||
@@ -36,5 +36,6 @@
|
||||
"stylekitInputPlaceholderColor": "rgb(168, 168, 168)",
|
||||
"stylekitInputBorderColor": "#e3e3e3",
|
||||
"stylekitScrollbarThumbColor": "#dfdfdf",
|
||||
"stylekitScrollbarTrackBorderColor": "#E7E7E7"
|
||||
"stylekitScrollbarTrackBorderColor": "#E7E7E7",
|
||||
"statusBar": ""
|
||||
}
|
||||
|
||||
@@ -36,5 +36,6 @@
|
||||
"stylekitInputPlaceholderColor": "rgb(168, 168, 168)",
|
||||
"stylekitInputBorderColor": "#e3e3e3",
|
||||
"stylekitScrollbarThumbColor": "#dfdfdf",
|
||||
"stylekitScrollbarTrackBorderColor": "#E7E7E7"
|
||||
"stylekitScrollbarTrackBorderColor": "#E7E7E7",
|
||||
"statusBar": ""
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export default class CSSParser {
|
||||
/**
|
||||
* @param css: CSS file contents in string format
|
||||
*/
|
||||
static cssToObject(css) {
|
||||
const object = {};
|
||||
static cssToObject(css: string) {
|
||||
const object: Record<string, string> = {};
|
||||
const lines = css.split('\n');
|
||||
|
||||
for (let line of lines) {
|
||||
@@ -36,7 +36,10 @@ export default class CSSParser {
|
||||
return object;
|
||||
}
|
||||
|
||||
static resolveVariablesThatReferenceOtherVariables(object, round = 0) {
|
||||
static resolveVariablesThatReferenceOtherVariables(
|
||||
object: Record<string, any>,
|
||||
round = 0
|
||||
) {
|
||||
for (const key of Object.keys(object)) {
|
||||
const value = object[key];
|
||||
const stripValue = 'var(';
|
||||
@@ -67,7 +70,7 @@ export default class CSSParser {
|
||||
}
|
||||
}
|
||||
|
||||
static hyphenatedStringToCamelCase(string) {
|
||||
static hyphenatedStringToCamelCase(string: string) {
|
||||
const comps = string.split('-');
|
||||
let result = '';
|
||||
for (let i = 0; i < comps.length; i++) {
|
||||
@@ -82,7 +85,7 @@ export default class CSSParser {
|
||||
return result;
|
||||
}
|
||||
|
||||
static capitalizeFirstLetter(string) {
|
||||
static capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { Platform } from 'react-native';
|
||||
import Server from '@Lib/snjs/httpManager';
|
||||
import CSSParser from '@Style/Util/CSSParser';
|
||||
import { SNTheme as SNJSTheme } from 'snjs';
|
||||
|
||||
type SNTheme = typeof SNJSTheme;
|
||||
|
||||
export default class ThemeDownloader {
|
||||
static instance = null;
|
||||
private static instance: ThemeDownloader;
|
||||
|
||||
static get() {
|
||||
if (this.instance == null) {
|
||||
if (!this.instance) {
|
||||
this.instance = new ThemeDownloader();
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
async downloadTheme(theme) {
|
||||
let errorBlock = error => {
|
||||
async downloadTheme(theme: SNTheme) {
|
||||
let errorBlock = (error: null) => {
|
||||
if (!theme.getNotAvailOnMobile()) {
|
||||
theme.setNotAvailOnMobile(true);
|
||||
theme.setDirty(true);
|
||||
@@ -38,7 +41,7 @@ export default class ThemeDownloader {
|
||||
Server.get().getAbsolute(
|
||||
url,
|
||||
{},
|
||||
response => {
|
||||
(response: any) => {
|
||||
let variables = CSSParser.cssToObject(response);
|
||||
resolve(variables);
|
||||
},
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { SNTheme as SNJSTheme } from 'snjs';
|
||||
/* eslint-disable no-bitwise */
|
||||
export const LIGHT_MODE_KEY = 'light';
|
||||
export const DARK_MODE_KEY = 'dark';
|
||||
export const LIGHT_CONTENT = 'light-content';
|
||||
export const DARK_CONTENT = 'dark-content';
|
||||
|
||||
export function statusBarColorForTheme(theme) {
|
||||
type SNTheme = typeof SNJSTheme;
|
||||
|
||||
export function statusBarColorForTheme(theme: SNTheme) {
|
||||
// The main nav bar uses contrast background color
|
||||
if (!theme.luminosity) {
|
||||
theme.luminosity = getColorLuminosity(
|
||||
@@ -20,7 +23,7 @@ export function statusBarColorForTheme(theme) {
|
||||
}
|
||||
}
|
||||
|
||||
export function keyboardColorForTheme(theme) {
|
||||
export function keyboardColorForTheme(theme: SNTheme) {
|
||||
if (!theme.luminosity) {
|
||||
theme.luminosity = getColorLuminosity(
|
||||
theme.content.variables.stylekitContrastBackgroundColor
|
||||
@@ -35,7 +38,7 @@ export function keyboardColorForTheme(theme) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getColorLuminosity(hexCode) {
|
||||
export function getColorLuminosity(hexCode: string) {
|
||||
let c = hexCode;
|
||||
c = c.substring(1); // strip #
|
||||
const rgb = parseInt(c, 16); // convert rrggbb to decimal
|
||||
@@ -46,12 +49,12 @@ export function getColorLuminosity(hexCode) {
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
|
||||
}
|
||||
|
||||
export function shadeBlend(p, c0, c1) {
|
||||
var n = p < 0 ? p * -1 : p,
|
||||
export function shadeBlend(p: number, c0: string, c1?: string) {
|
||||
let n = p < 0 ? p * -1 : p,
|
||||
u = Math.round,
|
||||
w = parseInt;
|
||||
if (c0.length > 7) {
|
||||
var f = c0.split(','),
|
||||
const f = c0.split(','),
|
||||
t = (c1 ? c1 : p < 0 ? 'rgb(0,0,0)' : 'rgb(255,255,255)').split(','),
|
||||
R = w(f[0].slice(4)),
|
||||
G = w(f[1]),
|
||||
@@ -66,7 +69,7 @@ export function shadeBlend(p, c0, c1) {
|
||||
')'
|
||||
);
|
||||
} else {
|
||||
var f = w(c0.slice(1), 16),
|
||||
const f = w(c0.slice(1), 16),
|
||||
t = w((c1 ? c1 : p < 0 ? '#000000' : '#FFFFFF').slice(1), 16),
|
||||
R1 = f >> 16,
|
||||
G1 = (f >> 8) & 0x00ff,
|
||||
@@ -85,19 +88,19 @@ export function shadeBlend(p, c0, c1) {
|
||||
}
|
||||
}
|
||||
|
||||
export function darken(color, value = -0.15) {
|
||||
export function darken(color: string, value = -0.15) {
|
||||
return shadeBlend(value, color);
|
||||
}
|
||||
|
||||
export function lighten(color, value = 0.25) {
|
||||
export function lighten(color: string, value = 0.25) {
|
||||
return shadeBlend(value, color);
|
||||
}
|
||||
|
||||
export function hexToRGBA(hex, alpha) {
|
||||
export function hexToRGBA(hex: string, alpha: number) {
|
||||
if (!hex || !hex.startsWith('#')) {
|
||||
return null;
|
||||
return '';
|
||||
}
|
||||
var c;
|
||||
let c: any;
|
||||
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
|
||||
c = hex.substring(1).split('');
|
||||
if (c.length === 3) {
|
||||
|
||||
1
src/types/index.d.ts
vendored
Normal file
1
src/types/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'react-native-search-box';
|
||||
1
src/types/react-native-search-box/index.d.ts
vendored
Normal file
1
src/types/react-native-search-box/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'react-native-seach-box';
|
||||
37
src/types/snjs/index.d.ts
vendored
Normal file
37
src/types/snjs/index.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
declare module 'snjs' {
|
||||
export const SNTheme: any;
|
||||
export const SNProtocolManager: any;
|
||||
export const protocolManager: any;
|
||||
export const SFItem: any;
|
||||
export const SFItemParams: any;
|
||||
export const SFPredicate: any;
|
||||
export const SNNote: any;
|
||||
export const SNTag: any;
|
||||
export const SNSmartTag: any;
|
||||
export const SNMfa: any;
|
||||
export const SNServerExtension: any;
|
||||
export const SNComponent: any;
|
||||
export const SNEditor: any;
|
||||
export const Action: any;
|
||||
export const SNExtension: any;
|
||||
export const SNEncryptedStorage: any;
|
||||
export const SFHistorySession: any;
|
||||
export const SFItemHistory: any;
|
||||
export const SFPrivileges: any;
|
||||
export const SNWebCrypto: any;
|
||||
export const SNCryptoJS: any;
|
||||
export const SNReactNativeCrypto: any;
|
||||
export const findInArray: any;
|
||||
export const SFModelManager: any;
|
||||
export const SFHttpManager: any;
|
||||
export const SFStorageManager: any;
|
||||
export const SFSyncManager: any;
|
||||
export const SFAuthManager: any;
|
||||
export const SFAlertManager: any;
|
||||
export const SFSessionHistoryManager: any;
|
||||
export const SFPrivilegesManager: any;
|
||||
export const SFSingletonManager: any;
|
||||
export const SNEncryptedStorage: any;
|
||||
export const SNComponentManager: any;
|
||||
export const SFMigrationManager: any;
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"isolatedModules": false, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"skipLibCheck": true,
|
||||
// "noImplicitAny": false,
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": "src", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
@@ -28,9 +29,10 @@
|
||||
"@Style/*": ["./style/*"],
|
||||
|
||||
},
|
||||
"typeRoots": ["./src/types/*"],
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
|
||||
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js", "types"
|
||||
]
|
||||
}
|
||||
|
||||
200
yarn.lock
200
yarn.lock
@@ -9,7 +9,7 @@
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.8.3"
|
||||
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.4.5", "@babel/core@^7.8.4", "@babel/core@^7.9.0":
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.4.5", "@babel/core@^7.8.4":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
|
||||
integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==
|
||||
@@ -31,6 +31,28 @@
|
||||
semver "^5.4.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/core@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.6.tgz#d9aa1f580abf3b2286ef40b6904d390904c63376"
|
||||
integrity sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.8.3"
|
||||
"@babel/generator" "^7.9.6"
|
||||
"@babel/helper-module-transforms" "^7.9.0"
|
||||
"@babel/helpers" "^7.9.6"
|
||||
"@babel/parser" "^7.9.6"
|
||||
"@babel/template" "^7.8.6"
|
||||
"@babel/traverse" "^7.9.6"
|
||||
"@babel/types" "^7.9.6"
|
||||
convert-source-map "^1.7.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.1"
|
||||
json5 "^2.1.2"
|
||||
lodash "^4.17.13"
|
||||
resolve "^1.3.2"
|
||||
semver "^5.4.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.4.0", "@babel/generator@^7.5.0", "@babel/generator@^7.9.0", "@babel/generator@^7.9.5":
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9"
|
||||
@@ -41,6 +63,16 @@
|
||||
lodash "^4.17.13"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.6.tgz#5408c82ac5de98cda0d77d8124e99fa1f2170a43"
|
||||
integrity sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==
|
||||
dependencies:
|
||||
"@babel/types" "^7.9.6"
|
||||
jsesc "^2.5.1"
|
||||
lodash "^4.17.13"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee"
|
||||
@@ -233,6 +265,15 @@
|
||||
"@babel/traverse" "^7.9.0"
|
||||
"@babel/types" "^7.9.0"
|
||||
|
||||
"@babel/helpers@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.6.tgz#092c774743471d0bb6c7de3ad465ab3d3486d580"
|
||||
integrity sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw==
|
||||
dependencies:
|
||||
"@babel/template" "^7.8.3"
|
||||
"@babel/traverse" "^7.9.6"
|
||||
"@babel/types" "^7.9.6"
|
||||
|
||||
"@babel/highlight@^7.8.3":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
|
||||
@@ -247,6 +288,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
|
||||
integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
|
||||
|
||||
"@babel/parser@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.6.tgz#3b1bbb30dabe600cd72db58720998376ff653bc7"
|
||||
integrity sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==
|
||||
|
||||
"@babel/plugin-external-helpers@^7.0.0":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-external-helpers/-/plugin-external-helpers-7.8.3.tgz#5a94164d9af393b2820a3cdc407e28ebf237de4b"
|
||||
@@ -648,13 +694,20 @@
|
||||
core-js-pure "^3.0.0"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.8.4":
|
||||
version "7.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
|
||||
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
|
||||
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.0.0", "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||
version "7.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
|
||||
@@ -679,6 +732,21 @@
|
||||
globals "^11.1.0"
|
||||
lodash "^4.17.13"
|
||||
|
||||
"@babel/traverse@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.6.tgz#5540d7577697bf619cc57b92aa0f1c231a94f442"
|
||||
integrity sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.8.3"
|
||||
"@babel/generator" "^7.9.6"
|
||||
"@babel/helper-function-name" "^7.9.5"
|
||||
"@babel/helper-split-export-declaration" "^7.8.3"
|
||||
"@babel/parser" "^7.9.6"
|
||||
"@babel/types" "^7.9.6"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
lodash "^4.17.13"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.7.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5":
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444"
|
||||
@@ -688,6 +756,15 @@
|
||||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7"
|
||||
integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.9.5"
|
||||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@cnakazawa/watch@^1.0.3":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
||||
@@ -1017,10 +1094,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.1.0.tgz#e42b1bef12d2415411519fd528e64b593b1363dc"
|
||||
integrity sha512-W/J0fNYVO01tioHjvYWQ9m6RgndVtbElzYozBq1ZPrHO/iCzlqoySHl4gO/fpCl9QEFjvJfjPgtPMTMlsoq5DQ==
|
||||
|
||||
"@react-native-community/masked-view@^0.1.9":
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.9.tgz#383aca2fb053e3e14405c99cce2d5805df730821"
|
||||
integrity sha512-nUtzbiLeXU0K9oVed6rc/WVrjGJwDSL4q1RTDkpZYU4j0FeovfuzcNUIDesD2r728LYfIop+uAgQdm+6qBOCug==
|
||||
"@react-native-community/masked-view@^0.1.10":
|
||||
version "0.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.10.tgz#5dda643e19e587793bc2034dd9bf7398ad43d401"
|
||||
integrity sha512-rk4sWFsmtOw8oyx8SD3KSvawwaK7gRBSEIy2TAwURyGt+3TizssXP1r8nx3zY+R7v2vYYHXZ+k2/GULAT/bcaQ==
|
||||
|
||||
"@react-navigation/core@^3.7.5":
|
||||
version "3.7.5"
|
||||
@@ -1032,13 +1109,13 @@
|
||||
query-string "^6.11.1"
|
||||
react-is "^16.13.0"
|
||||
|
||||
"@react-navigation/native@^3.7.11":
|
||||
version "3.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-3.7.11.tgz#00aa5d8187b346b16a5d2e9d1e32413c78bc4ad4"
|
||||
integrity sha512-FlnGsLsAVPB9CZ+A0H6Xdv1qTi1Gg39tdXA85Oz0eijb/ilWwvk3x+IKTt/Rex3CU3oEl0ZKwtYC5aSy4CIO7Q==
|
||||
"@react-navigation/native@^3.7.12":
|
||||
version "3.7.12"
|
||||
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-3.7.12.tgz#f1c2a1b8b2db647c239acaf966360cb72d7b71e6"
|
||||
integrity sha512-StECfhxtEJvFehh16Wc9jnepy5gYKovzynVW+jr/+jKa7xlKskSCvASDnIwLHoFcWom084afKbqpVoVLEsE3lg==
|
||||
dependencies:
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
react-native-safe-area-view "^0.14.8"
|
||||
react-native-safe-area-view "^0.14.9"
|
||||
|
||||
"@types/babel__core@^7.1.0":
|
||||
version "7.1.7"
|
||||
@@ -1151,10 +1228,10 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-native@^0.62.4":
|
||||
version "0.62.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.62.4.tgz#0aea6619a19de1c6994ce8e4175fc4e44409bcdb"
|
||||
integrity sha512-AKImyybUzqqPItKNuURkMe7y1X5cxuSJh5td8qbFSEXO58S5qjw01eZweWkKyTVCvrWGXkfm43u1zoYcZSGL6w==
|
||||
"@types/react-native@^0.62.7":
|
||||
version "0.62.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.62.7.tgz#bfc5ed03ba576f288603daa3f67f0f67d9a8bf57"
|
||||
integrity sha512-FGFEt9GcFVl//XxWmxkeBxAx0YnzyEhJpR8hOJrjfaFKZm0KjHzzyCmCksBAP2qHSTrcJCiBkIvYCX/kGiOgww==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
@@ -1200,12 +1277,12 @@
|
||||
regexpp "^3.0.0"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^2.30.0":
|
||||
version "2.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.30.0.tgz#312a37e80542a764d96e8ad88a105316cdcd7b05"
|
||||
integrity sha512-PGejii0qIZ9Q40RB2jIHyUpRWs1GJuHP1pkoCiaeicfwO9z7Fx03NQzupuyzAmv+q9/gFNHu7lo1ByMXe8PNyg==
|
||||
"@typescript-eslint/eslint-plugin@^2.31.0":
|
||||
version "2.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.31.0.tgz#942c921fec5e200b79593c71fafb1e3f57aa2e36"
|
||||
integrity sha512-iIC0Pb8qDaoit+m80Ln/aaeu9zKQdOLF4SHcGLarSeY1gurW6aU4JsOPMjKQwXlw70MvWKZQc6S2NamA8SJ/gg==
|
||||
dependencies:
|
||||
"@typescript-eslint/experimental-utils" "2.30.0"
|
||||
"@typescript-eslint/experimental-utils" "2.31.0"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
regexpp "^3.0.0"
|
||||
tsutils "^3.17.1"
|
||||
@@ -1220,13 +1297,13 @@
|
||||
eslint-scope "^5.0.0"
|
||||
eslint-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/experimental-utils@2.30.0":
|
||||
version "2.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.30.0.tgz#9845e868c01f3aed66472c561d4b6bac44809dd0"
|
||||
integrity sha512-L3/tS9t+hAHksy8xuorhOzhdefN0ERPDWmR9CclsIGOUqGKy6tqc/P+SoXeJRye5gazkuPO0cK9MQRnolykzkA==
|
||||
"@typescript-eslint/experimental-utils@2.31.0":
|
||||
version "2.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.31.0.tgz#a9ec514bf7fd5e5e82bc10dcb6a86d58baae9508"
|
||||
integrity sha512-MI6IWkutLYQYTQgZ48IVnRXmLR/0Q6oAyJgiOror74arUMh7EWjJkADfirZhRsUMHeLJ85U2iySDwHTSnNi9vA==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.3"
|
||||
"@typescript-eslint/typescript-estree" "2.30.0"
|
||||
"@typescript-eslint/typescript-estree" "2.31.0"
|
||||
eslint-scope "^5.0.0"
|
||||
eslint-utils "^2.0.0"
|
||||
|
||||
@@ -1240,14 +1317,14 @@
|
||||
"@typescript-eslint/typescript-estree" "2.29.0"
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
|
||||
"@typescript-eslint/parser@^2.30.0":
|
||||
version "2.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.30.0.tgz#7681c305a6f4341ae2579f5e3a75846c29eee9ce"
|
||||
integrity sha512-9kDOxzp0K85UnpmPJqUzdWaCNorYYgk1yZmf4IKzpeTlSAclnFsrLjfwD9mQExctLoLoGAUXq1co+fbr+3HeFw==
|
||||
"@typescript-eslint/parser@^2.31.0":
|
||||
version "2.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.31.0.tgz#beddd4e8efe64995108b229b2862cd5752d40d6f"
|
||||
integrity sha512-uph+w6xUOlyV2DLSC6o+fBDzZ5i7+3/TxAsH4h3eC64tlga57oMb96vVlXoMwjR/nN+xyWlsnxtbDkB46M2EPQ==
|
||||
dependencies:
|
||||
"@types/eslint-visitor-keys" "^1.0.0"
|
||||
"@typescript-eslint/experimental-utils" "2.30.0"
|
||||
"@typescript-eslint/typescript-estree" "2.30.0"
|
||||
"@typescript-eslint/experimental-utils" "2.31.0"
|
||||
"@typescript-eslint/typescript-estree" "2.31.0"
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@2.29.0":
|
||||
@@ -1263,10 +1340,10 @@
|
||||
semver "^6.3.0"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/typescript-estree@2.30.0":
|
||||
version "2.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.30.0.tgz#1b8e848b55144270255ffbfe4c63291f8f766615"
|
||||
integrity sha512-nI5WOechrA0qAhnr+DzqwmqHsx7Ulr/+0H7bWCcClDhhWkSyZR5BmTvnBEyONwJCTWHfc5PAQExX24VD26IAVw==
|
||||
"@typescript-eslint/typescript-estree@2.31.0":
|
||||
version "2.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.31.0.tgz#ac536c2d46672aa1f27ba0ec2140d53670635cfd"
|
||||
integrity sha512-vxW149bXFXXuBrAak0eKHOzbcu9cvi6iNcJDzEtOkRwGHxJG15chiAQAwhLOsk+86p9GTr/TziYvw+H9kMaIgA==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
@@ -2305,11 +2382,6 @@ dayjs@^1.8.15:
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.25.tgz#d09a8696cee7191bc1289e739f96626391b9c73c"
|
||||
integrity sha512-Pk36juDfQQGDCgr0Lqd1kw15w3OS6xt21JaLPE3lCfsEf8KrERGwDNwvK1tRjrjqFC0uZBJncT4smZQ4F+uV5g==
|
||||
|
||||
debounce@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"
|
||||
integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==
|
||||
|
||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
@@ -5759,18 +5831,18 @@ react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1, react-
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-native-actionsheet@standardnotes/react-native-actionsheet#6846f21:
|
||||
version "2.3.0"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-actionsheet/tar.gz/6846f2165a3b178b04a1489ba49f3bbca47522cc"
|
||||
react-native-actionsheet@standardnotes/react-native-actionsheet#9cb323f:
|
||||
version "2.4.2"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-actionsheet/tar.gz/9cb323f2fd5ca9687395354b88c5ae3c1a92c43e"
|
||||
|
||||
react-native-aes-crypto@1.3.8:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/react-native-aes-crypto/-/react-native-aes-crypto-1.3.8.tgz#883017f03ac1c23c7ab8bc3879dbd7ee89737cab"
|
||||
integrity sha512-uL6aEk7qor6MQ1m+wctgGznjL/khGgt3jfXD6TLNfgqm+glyIcQIqRAK6RpHqXJXpNGoCSC3fdliy/dmBUNs9g==
|
||||
|
||||
react-native-alternate-icons@standardnotes/react-native-alternate-icons#3154f8d:
|
||||
react-native-alternate-icons@standardnotes/react-native-alternate-icons#1d335d:
|
||||
version "0.3.0"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-alternate-icons/tar.gz/3154f8de477e8dd6358a88fc5795e73730e4b744"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-alternate-icons/tar.gz/1d335d13bb518ed4d26cb00bcd1f6b1c4d60a052"
|
||||
|
||||
react-native-dark-mode@^0.2.2:
|
||||
version "0.2.2"
|
||||
@@ -5783,9 +5855,9 @@ react-native-dark-mode@^0.2.2:
|
||||
events "^3.0.0"
|
||||
toolkit.ts "^0.0.2"
|
||||
|
||||
react-native-fab@standardnotes/react-native-fab#113661d:
|
||||
react-native-fab@standardnotes/react-native-fab#cb60e00:
|
||||
version "1.0.8"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-fab/tar.gz/113661db797296295f5ab92b9c55e4b1ce82f8f4"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-fab/tar.gz/cb60e0067bbd938df5e85838760d8ff87f0cddda"
|
||||
dependencies:
|
||||
prop-types "^15.5.10"
|
||||
|
||||
@@ -5798,9 +5870,9 @@ react-native-fingerprint-scanner@standardnotes/react-native-fingerprint-scanner#
|
||||
version "4.0.0"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-fingerprint-scanner/tar.gz/5984941f452e978da9d18f8ad324a16b0b459580"
|
||||
|
||||
react-native-flag-secure-android@standardnotes/react-native-flag-secure-android#d0cbae0:
|
||||
react-native-flag-secure-android@standardnotes/react-native-flag-secure-android#3d59055:
|
||||
version "1.0.0"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-flag-secure-android/tar.gz/d0cbae0c10ad2ed990a66ee9a4dc5c433ee75a9e"
|
||||
resolved "https://codeload.github.com/standardnotes/react-native-flag-secure-android/tar.gz/3d5905538fedd356e2fb59917a667279b1ce4313"
|
||||
|
||||
react-native-fs@^2.16.6:
|
||||
version "2.16.6"
|
||||
@@ -5846,19 +5918,17 @@ react-native-safe-area-context@^0.7.3:
|
||||
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-0.7.3.tgz#ad6bd4abbabe195332c53810e4ce5851eb21aa2a"
|
||||
integrity sha512-9Uqu1vlXPi+2cKW/CW6OnHxA76mWC4kF3wvlqzq4DY8hn37AeiXtLFs2WkxH4yXQRrnJdP6ivc65Lz+MqwRZAA==
|
||||
|
||||
react-native-safe-area-view@^0.14.8:
|
||||
react-native-safe-area-view@^0.14.9:
|
||||
version "0.14.9"
|
||||
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.14.9.tgz#90ee8383037010d9a5055a97cf97e4c1da1f0c3d"
|
||||
integrity sha512-WII/ulhpVyL/qbYb7vydq7dJAfZRBcEhg4/UWt6F6nAKpLa3gAceMOxBxI914ppwSP/TdUsandFy6lkJQE0z4A==
|
||||
dependencies:
|
||||
hoist-non-react-statics "^2.3.1"
|
||||
|
||||
react-native-screens@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.5.0.tgz#2c5b5a56d3b8be64d459d53da154bde926021a50"
|
||||
integrity sha512-5Ak/J41eZzvU2zvik8YH825ppFdKR4cootbAq9ETwdQoN0fu4GBdOpX7paKW2jFCTsh3OOoyRRW2xYvLczZqVw==
|
||||
dependencies:
|
||||
debounce "^1.2.0"
|
||||
react-native-screens@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.7.0.tgz#2d3cf3c39a665e9ca1c774264fccdb90e7944047"
|
||||
integrity sha512-n/23IBOkrTKCfuUd6tFeRkn3lB2QZ3cmvoubRscR0JU/Zl4/ZyKmwnFmUv1/Fr+2GH/H8UTX59kEKDYYg3dMgA==
|
||||
|
||||
react-native-search-box@standardnotes/react-native-search-box#210b036:
|
||||
version "0.0.19"
|
||||
@@ -5926,9 +5996,9 @@ react-native@0.62.2:
|
||||
whatwg-fetch "^3.0.0"
|
||||
|
||||
react-navigation-drawer@^2.3.3:
|
||||
version "2.4.11"
|
||||
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-2.4.11.tgz#3202d1f90e01da838b092c53ac043a567cef5ba7"
|
||||
integrity sha512-DBA5rKYyjxPzzrB0m5ZApZ+pJpl6aueEcz4TSDzoh/10wjifSaaDwDhZxM0Tz9mwd/wn5rgzMq8EYt2eZhjPYA==
|
||||
version "2.4.12"
|
||||
resolved "https://registry.yarnpkg.com/react-navigation-drawer/-/react-navigation-drawer-2.4.12.tgz#35eb1c34749119f5089e50fa1c374028413cda47"
|
||||
integrity sha512-3fv5+YbhWsu4RRDIUetXEniZtQ02frZuQp42zbkvZWk+uLApbEYpW5Aw/MNjNWA1BKoOjtmLCz2qyriZD6uQxQ==
|
||||
|
||||
react-navigation-header-buttons@^2.1.1:
|
||||
version "2.3.1"
|
||||
@@ -5945,12 +6015,12 @@ react-navigation-stack@^1.10.3:
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-navigation@^4.0.10:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/react-navigation/-/react-navigation-4.3.7.tgz#33809fdfd7922c503d454c6367fae45dd67cb08a"
|
||||
integrity sha512-mFElgGxjAoCPGjOedEOYyORN0IdubyCSTWf5/mcWWHiigakXSs0XRuC8pe5d+d0sqES10Gy5BYRb5KrdVJg2Lw==
|
||||
version "4.3.8"
|
||||
resolved "https://registry.yarnpkg.com/react-navigation/-/react-navigation-4.3.8.tgz#7eacd186fbaa849355341046d9c5c95dec97d3bf"
|
||||
integrity sha512-Hxb6VkGu38x4r8nysAJutFkZ1yax29H6BrcdsqxlfGO2pCd821JkRL9h1Zqxn7qLm5JM6+7h0Yx3AS+YKDU5nw==
|
||||
dependencies:
|
||||
"@react-navigation/core" "^3.7.5"
|
||||
"@react-navigation/native" "^3.7.11"
|
||||
"@react-navigation/native" "^3.7.12"
|
||||
|
||||
react-refresh@^0.4.0:
|
||||
version "0.4.2"
|
||||
@@ -6561,9 +6631,9 @@ slide@^1.1.5:
|
||||
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
|
||||
integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=
|
||||
|
||||
sn-textview@standardnotes/sn-textview#8b62cb2:
|
||||
sn-textview@standardnotes/sn-textview#f42f0bf:
|
||||
version "1.0.0"
|
||||
resolved "https://codeload.github.com/standardnotes/sn-textview/tar.gz/8b62cb2d61facbd7b4799c3d9f5efe19542d13b4"
|
||||
resolved "https://codeload.github.com/standardnotes/sn-textview/tar.gz/f42f0bffa8942080c829ea9f699e400ab95ac90a"
|
||||
|
||||
snapdragon-node@^2.0.1:
|
||||
version "2.1.1"
|
||||
|
||||
Reference in New Issue
Block a user