From 6149f150dfd165cfc7d34faf0774b19c943ec013 Mon Sep 17 00:00:00 2001 From: Radek Czemerys Date: Thu, 7 May 2020 10:57:52 +0200 Subject: [PATCH] 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 --- .prettierrc.js | 2 +- README.md | 2 - ios/Podfile.lock | 8 +- package.json | 26 +-- src/App.tsx | 34 ++- src/components/ButtonCell.tsx | 14 +- src/components/Circle.tsx | 18 +- src/components/HeaderTitleView.tsx | 12 +- src/components/SectionHeader.tsx | 23 +- .../SectionedAccessoryTableCell.tsx | 49 +++-- src/components/SectionedOptionsTableCell.tsx | 23 +- src/components/SectionedTableCell.tsx | 17 +- src/components/TableSection.tsx | 8 +- src/components/ThemedComponent.tsx | 5 +- src/components/ThemedPureComponent.tsx | 8 +- src/containers/LockedView.tsx | 38 +++- src/global.ts | 1 + src/lib/ApplicationState.ts | 131 +++++++++--- src/lib/BackupsManager.ts | 31 +-- src/lib/OptionsState.ts | 78 ++++--- src/lib/componentManager.ts | 62 ++++-- src/lib/itemActionManager.ts | 55 +++-- src/lib/keychain.ts | 2 +- src/lib/keysManager.ts | 86 ++++++-- src/lib/moment.ts | 2 +- src/lib/reviewManager.ts | 2 +- src/lib/snjs/alertManager.ts | 51 +++-- src/lib/snjs/authManager.ts | 17 +- src/lib/snjs/httpManager.ts | 4 +- src/lib/snjs/migrationManager.ts | 14 +- src/lib/snjs/modelManager.ts | 145 ++++++++----- src/lib/snjs/privilegesManager.ts | 44 +++- src/lib/snjs/storageManager.ts | 48 +++-- src/lib/snjs/syncManager.ts | 6 +- src/lib/userPrefsManager.ts | 23 +- src/lib/utils.ts | 8 +- src/models/PlatformStyles.ts | 10 +- src/models/extend/item.ts | 4 +- src/screens/Abstract.tsx | 129 ++++++++--- src/screens/Authentication/Authenticate.tsx | 164 +++++++++----- .../Sources/AuthenticationSource.tsx | 18 +- .../AuthenticationSourceAccountPassword.tsx | 16 +- .../Sources/AuthenticationSourceBiometric.tsx | 9 +- .../AuthenticationSourceLocalPasscode.tsx | 8 +- src/screens/ComponentView.tsx | 45 +++- src/screens/Compose.tsx | 193 +++++++++++------ src/screens/InputModal.tsx | 30 ++- src/screens/KeyRecovery.tsx | 24 ++- src/screens/ManagePrivileges.tsx | 35 ++- src/screens/Notes/NoteCell.tsx | 66 ++++-- src/screens/Notes/NoteList.tsx | 39 +++- src/screens/Notes/Notes.tsx | 166 ++++++++++----- src/screens/Notes/OfflineBanner.tsx | 16 +- src/screens/Root.tsx | 192 +++++++++++------ src/screens/Settings/Sections/AuthSection.tsx | 45 +++- .../Settings/Sections/CompanySection.tsx | 8 +- .../Settings/Sections/EncryptionSection.tsx | 14 +- .../Settings/Sections/OptionsSection.tsx | 36 +++- .../Settings/Sections/PasscodeSection.tsx | 38 +++- src/screens/Settings/Settings.tsx | 32 ++- src/screens/SideMenu/AbstractSideMenu.tsx | 10 +- src/screens/SideMenu/MainSideMenu.tsx | 44 ++-- src/screens/SideMenu/NoteSideMenu.tsx | 65 ++++-- src/screens/SideMenu/SideMenuCell.tsx | 34 +-- src/screens/SideMenu/SideMenuHero.tsx | 17 +- src/screens/SideMenu/SideMenuManager.tsx | 39 +++- src/screens/SideMenu/SideMenuSection.tsx | 50 ++++- src/screens/SideMenu/TagSelectionList.tsx | 50 +++-- src/style/ActionSheetWrapper.tsx | 52 +++-- src/style/StyleKit.ts | 97 ++++++--- src/style/ThemeManager.ts | 32 +-- src/style/Themes/blue.json | 3 +- src/style/Themes/red.json | 3 +- src/style/Util/CSSParser.ts | 13 +- src/style/Util/ThemeDownloader.ts | 13 +- src/style/utils.ts | 27 +-- src/types/index.d.ts | 1 + src/types/react-native-search-box/index.d.ts | 1 + src/types/snjs/index.d.ts | 37 ++++ tsconfig.json | 4 +- yarn.lock | 200 ++++++++++++------ 81 files changed, 2278 insertions(+), 948 deletions(-) create mode 100644 src/types/index.d.ts create mode 100644 src/types/react-native-search-box/index.d.ts create mode 100644 src/types/snjs/index.d.ts diff --git a/.prettierrc.js b/.prettierrc.js index c9e8c723..595aa33d 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -9,4 +9,4 @@ module.exports = { singleQuote: true, tabWidth: 2, strailingComma: "none" -}; +}; \ No newline at end of file diff --git a/README.md b/README.md index 76df587a..23632992 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d369a1db..374b382c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/package.json b/package.json index 3df37f5d..ab333bfb 100644 --- a/package.json +++ b/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", diff --git a/src/App.tsx b/src/App.tsx index 90ef0352..b868b379 100644 --- a/src/App.tsx +++ b/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(); } diff --git a/src/components/ButtonCell.tsx b/src/components/ButtonCell.tsx index 78c8916e..e8ef497a 100644 --- a/src/components/ButtonCell.tsx +++ b/src/components/ButtonCell.tsx @@ -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 { 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); } diff --git a/src/components/Circle.tsx b/src/components/Circle.tsx index ecc35f2a..2c2a5fc5 100644 --- a/src/components/Circle.tsx +++ b/src/components/Circle.tsx @@ -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 { + styles!: Record; + size: number; + constructor(props: Readonly) { super(props); this.size = props.size || 12; this.loadStyles(); diff --git a/src/components/HeaderTitleView.tsx b/src/components/HeaderTitleView.tsx index 2dde0641..57064efb 100644 --- a/src/components/HeaderTitleView.tsx +++ b/src/components/HeaderTitleView.tsx @@ -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 { + constructor(props: Readonly) { super(props); } diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx index df667dd0..f829182b 100644 --- a/src/components/SectionHeader.tsx +++ b/src/components/SectionHeader.tsx @@ -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 { + styles!: Record; render() { let title = this.props.title; if (Platform.OS === 'ios') { diff --git a/src/components/SectionedAccessoryTableCell.tsx b/src/components/SectionedAccessoryTableCell.tsx index 79694999..a9fb5726 100644 --- a/src/components/SectionedAccessoryTableCell.tsx +++ b/src/components/SectionedAccessoryTableCell.tsx @@ -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 = ( - - - - ); - - if (!iconName) { - icon = null; + if (iconName) { + icon = ( + + + + ); } var textStyles = [StyleKit.styles.sectionedAccessoryTableCellLabel]; @@ -95,13 +110,6 @@ export default class SectionedAccessoryTableCell extends SectionedTableCell { ); - const containerStyles = { - flex: 1, - justifyContent: left ? 'flex-start' : 'space-between', - flexDirection: 'row', - alignItems: 'center', - }; - return ( - + {this.props.leftAlignIcon ? [icon, textWrapper] : [textWrapper, icon]} diff --git a/src/components/SectionedOptionsTableCell.tsx b/src/components/SectionedOptionsTableCell.tsx index 55da84ce..3601a7c3 100644 --- a/src/components/SectionedOptionsTableCell.tsx +++ b/src/components/SectionedOptionsTableCell.tsx @@ -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 { + styles!: Record; rules() { let rules = [StyleKit.styles.sectionedTableCell]; if (this.props.first) { diff --git a/src/components/SectionedTableCell.tsx b/src/components/SectionedTableCell.tsx index 25ec7262..02a19a6b 100644 --- a/src/components/SectionedTableCell.tsx +++ b/src/components/SectionedTableCell.tsx @@ -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 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); diff --git a/src/components/TableSection.tsx b/src/components/TableSection.tsx index 015f9a02..61d435ee 100644 --- a/src/components/TableSection.tsx +++ b/src/components/TableSection.tsx @@ -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 { rules() { let rules = [StyleKit.styles.tableSection]; if (this.props.extraStyles) { diff --git a/src/components/ThemedComponent.tsx b/src/components/ThemedComponent.tsx index 5405a4b8..ccc33196 100644 --- a/src/components/ThemedComponent.tsx +++ b/src/components/ThemedComponent.tsx @@ -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

extends Component { + themeChangeObserver: () => void; + constructor(props: Readonly

) { super(props); this.loadStyles(); diff --git a/src/components/ThemedPureComponent.tsx b/src/components/ThemedPureComponent.tsx index c4530e0f..d00b4422 100644 --- a/src/components/ThemedPureComponent.tsx +++ b/src/components/ThemedPureComponent.tsx @@ -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

extends PureComponent< + P, + S +> { + themeChangeObserver: () => void; + constructor(props: Readonly

) { super(props); this.loadStyles(); diff --git a/src/containers/LockedView.tsx b/src/containers/LockedView.tsx index 28d76888..aeb8bc8c 100644 --- a/src/containers/LockedView.tsx +++ b/src/containers/LockedView.tsx @@ -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 { + 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 ( - + Application Locked. {!this.props.onUnlockPress && ( - + Return to Notes to unlock. )} {this.props.onUnlockPress && ( - - Unlock + + Unlock )} diff --git a/src/global.ts b/src/global.ts index 525a430f..0c0c0a86 100644 --- a/src/global.ts +++ b/src/global.ts @@ -13,4 +13,5 @@ // require('core-js/fn/array/find'); // TODO: still crashes without this +// @ts-ignore global._ = require('lodash'); diff --git a/src/lib/ApplicationState.ts b/src/lib/ApplicationState.ts index 6dbcec4e..91592d88 100644 --- a/src/lib/ApplicationState.ts +++ b/src/lib/ApplicationState.ts @@ -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).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}.`); }; diff --git a/src/lib/BackupsManager.ts b/src/lib/BackupsManager.ts index 752a86e1..78b03950 100644 --- a/src/lib/BackupsManager.ts +++ b/src/lib/BackupsManager.ts @@ -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.'); } diff --git a/src/lib/OptionsState.ts b/src/lib/OptionsState.ts index 0c3aa77d..08c6fe7a 100644 --- a/src/lib/OptionsState.ts +++ b/src/lib/OptionsState.ts @@ -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') { diff --git a/src/lib/componentManager.ts b/src/lib/componentManager.ts index 63eb9fd6..6aa4e111 100644 --- a/src/lib/componentManager.ts +++ b/src/lib/componentManager.ts @@ -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); } } diff --git a/src/lib/itemActionManager.ts b/src/lib/itemActionManager.ts index e92be3e6..aacf2e31 100644 --- a/src/lib/itemActionManager.ts +++ b/src/lib/itemActionManager.ts @@ -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?`; diff --git a/src/lib/keychain.ts b/src/lib/keychain.ts index e61580b2..ab155ae7 100644 --- a/src/lib/keychain.ts +++ b/src/lib/keychain.ts @@ -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, diff --git a/src/lib/keysManager.ts b/src/lib/keysManager.ts index c96c9df1..b5b3df56 100644 --- a/src/lib/keysManager.ts +++ b/src/lib/keysManager.ts @@ -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 | 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) { 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); }); } diff --git a/src/lib/moment.ts b/src/lib/moment.ts index f7bf9000..794a4fb7 100644 --- a/src/lib/moment.ts +++ b/src/lib/moment.ts @@ -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 diff --git a/src/lib/reviewManager.ts b/src/lib/reviewManager.ts index afd67c45..3a7db77a 100644 --- a/src/lib/reviewManager.ts +++ b/src/lib/reviewManager.ts @@ -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)); } } diff --git a/src/lib/snjs/alertManager.ts b/src/lib/snjs/alertManager.ts index 5dce0a3f..9bed1b26 100644 --- a/src/lib/snjs/alertManager.ts +++ b/src/lib/snjs/alertManager.ts @@ -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; + }) { + 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; + onCancel?: () => void | Promise; + onDismiss?: () => void | Promise; + }) { 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(); }, }); diff --git a/src/lib/snjs/authManager.ts b/src/lib/snjs/authManager.ts index 88364433..318d35f9 100644 --- a/src/lib/snjs/authManager.ts +++ b/src/lib/snjs/authManager.ts @@ -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, diff --git a/src/lib/snjs/httpManager.ts b/src/lib/snjs/httpManager.ts index 046f3777..2735980c 100644 --- a/src/lib/snjs/httpManager.ts +++ b/src/lib/snjs/httpManager.ts @@ -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(); } diff --git a/src/lib/snjs/migrationManager.ts b/src/lib/snjs/migrationManager.ts index 9c64ac36..fc74b729 100644 --- a/src/lib/snjs/migrationManager.ts +++ b/src/lib/snjs/migrationManager.ts @@ -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); } } diff --git a/src/lib/snjs/modelManager.ts b/src/lib/snjs/modelManager.ts index c128a7e2..68eaa9af 100644 --- a/src/lib/snjs/modelManager.ts +++ b/src/lib/snjs/modelManager.ts @@ -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', diff --git a/src/lib/snjs/privilegesManager.ts b/src/lib/snjs/privilegesManager.ts index 924bb7f3..1e4ae3ed 100644 --- a/src/lib/snjs/privilegesManager.ts +++ b/src/lib/snjs/privilegesManager.ts @@ -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; diff --git a/src/lib/snjs/storageManager.ts b/src/lib/snjs/storageManager.ts index f25b4cc3..145eece1 100644 --- a/src/lib/snjs/storageManager.ts +++ b/src/lib/snjs/storageManager.ts @@ -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 = {}; + 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); } diff --git a/src/lib/snjs/syncManager.ts b/src/lib/snjs/syncManager.ts index ccd99c29..95499043 100644 --- a/src/lib/snjs/syncManager.ts +++ b/src/lib/snjs/syncManager.ts @@ -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 || diff --git a/src/lib/userPrefsManager.ts b/src/lib/userPrefsManager.ts index 624bb9df..bfb285f4 100644 --- a/src/lib/userPrefsManager.ts +++ b/src/lib/userPrefsManager.ts @@ -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; 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; } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9267a507..15c8d9d8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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)); } diff --git a/src/models/PlatformStyles.ts b/src/models/PlatformStyles.ts index bd9d4f75..70d6d2cb 100644 --- a/src/models/PlatformStyles.ts +++ b/src/models/PlatformStyles.ts @@ -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); diff --git a/src/models/extend/item.ts b/src/models/extend/item.ts index a0a1fba3..30308b0c 100644 --- a/src/models/extend/item.ts +++ b/src/models/extend/item.ts @@ -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'); }; diff --git a/src/screens/Abstract.tsx b/src/screens/Abstract.tsx index 97310e42..3505d605 100644 --- a/src/screens/Abstract.tsx +++ b/src/screens/Abstract.tsx @@ -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 as well as // 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 { 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 = ( - ); + // @ts-ignore setting a property on navigation object options.headerLeft = headerLeft; } @@ -74,7 +97,7 @@ export default class Abstract extends ThemedComponent { if (rightButton) { headerRight = ( - ); + // @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) { 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, + props: Readonly + ) => {} | 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); } diff --git a/src/screens/Authentication/Authenticate.tsx b/src/screens/Authentication/Authenticate.tsx index 688dd350..d8e0e333 100644 --- a/src/screens/Authentication/Authenticate.tsx +++ b/src/screens/Authentication/Authenticate.tsx @@ -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 { + 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; + stateObserver: { + key: () => number; + callback: (state: AppStateType) => void; + }; + pendingSources: Source[]; + _sessionLength: number; + successfulSources: Source[]; + activeSource: Source | null = null; + needsSuccessCallback: any; - constructor(props) { + constructor(props: Readonly) { 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 + ) => ( { - 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); }} /> ); - const biometricAuthenticationSource = source => ( + const biometricAuthenticationSource = ( + biometricSource: AuthenticationSourceBiometric + ) => ( { - this.onBiometricDirectPress(source); + this.onBiometricDirectPress(biometricSource); }} /> @@ -369,14 +406,40 @@ export default class Authenticate extends Abstract { - {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 + )} ); }; @@ -396,7 +459,8 @@ export default class Authenticate extends Abstract { })} 1 ? 'Next' : 'Submit'} @@ -407,20 +471,22 @@ export default class Authenticate extends Abstract { {this.sessionLengthOptions && this.sessionLengthOptions.length > 0 && ( - {this.sessionLengthOptions.map((option, index) => ( - { - return option.value === this._sessionLength; - }} - onPress={() => { - this.setSessionLength(option.value); - }} - /> - ))} + {this.sessionLengthOptions.map( + (option: { label: string; value: number }, index: number) => ( + { + return option.value === this._sessionLength; + }} + onPress={() => { + this.setSessionLength(option.value); + }} + /> + ) + )} )} diff --git a/src/screens/Authentication/Sources/AuthenticationSource.tsx b/src/screens/Authentication/Sources/AuthenticationSource.tsx index 4f0b33f0..c379f994 100644 --- a/src/screens/Authentication/Sources/AuthenticationSource.tsx +++ b/src/screens/Authentication/Sources/AuthenticationSource.tsx @@ -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 {} cancel() {} diff --git a/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.tsx b/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.tsx index 71da3805..c74881ca 100644 --- a/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.tsx +++ b/src/screens/Authentication/Sources/AuthenticationSourceAccountPassword.tsx @@ -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 } }; } diff --git a/src/screens/Authentication/Sources/AuthenticationSourceBiometric.tsx b/src/screens/Authentication/Sources/AuthenticationSourceBiometric.tsx index dd108e70..a78afdcc 100644 --- a/src/screens/Authentication/Sources/AuthenticationSourceBiometric.tsx +++ b/src/screens/Authentication/Sources/AuthenticationSourceBiometric.tsx @@ -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(); } diff --git a/src/screens/Authentication/Sources/AuthenticationSourceLocalPasscode.tsx b/src/screens/Authentication/Sources/AuthenticationSourceLocalPasscode.tsx index bc29d135..8de0c02f 100644 --- a/src/screens/Authentication/Sources/AuthenticationSourceLocalPasscode.tsx +++ b/src/screens/Authentication/Sources/AuthenticationSourceLocalPasscode.tsx @@ -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 } }; } diff --git a/src/screens/ComponentView.tsx b/src/screens/ComponentView.tsx index eedfc489..b4e1d77b 100644 --- a/src/screens/ComponentView.tsx +++ b/src/screens/ComponentView.tsx @@ -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 { + styles!: Record; + identifier: string; + editor: any; + note: any; + registrationTimeout: any; + webView: WebView | null = null; + alreadyTriggeredLoad: boolean = false; + constructor(props: Readonly) { + 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 */ } diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index 33323f02..95af89f9 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -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; + rawStyles!: Record; + note: any; + syncObserver: any; + componentHandler: any; + signoutObserver: any; + tabletModeChangeHandler?: AppStateEventHandler; + didShowErrorAlert?: boolean; + rightMenuHandler: SideMenuManager['rightSideMenuHandler'] = null; + input: TextView | null = null; + saveError?: boolean; + statusTimeout?: ReturnType; + syncTakingTooLong: boolean = false; + saveTimeout?: ReturnType; - constructor(props) { + constructor(props: Readonly) { 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; 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; diff --git a/src/screens/InputModal.tsx b/src/screens/InputModal.tsx index df2958d1..16d56a1a 100644 --- a/src/screens/InputModal.tsx +++ b/src/screens/InputModal.tsx @@ -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 { + 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) { 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(); diff --git a/src/screens/KeyRecovery.tsx b/src/screens/KeyRecovery.tsx index cc995644..09c2ed79 100644 --- a/src/screens/KeyRecovery.tsx +++ b/src/screens/KeyRecovery.tsx @@ -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 { + 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) { 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 }); }; diff --git a/src/screens/ManagePrivileges.tsx b/src/screens/ManagePrivileges.tsx index 7b02b0b4..f00a8161 100644 --- a/src/screens/ManagePrivileges.tsx +++ b/src/screens/ManagePrivileges.tsx @@ -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 { + 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 = {}; + styles: any; - constructor(props) { + constructor(props: Readonly) { 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() { diff --git a/src/screens/Notes/NoteCell.tsx b/src/screens/Notes/NoteCell.tsx index dc7c3640..1aa8252c 100644 --- a/src/screens/Notes/NoteCell.tsx +++ b/src/screens/Notes/NoteCell.tsx @@ -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 { + styles!: Record; + selectionTimeout?: number; + constructor(props: Readonly) { 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 ( {note.deleted && ( Deleting... @@ -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, diff --git a/src/screens/Notes/NoteList.tsx b/src/screens/Notes/NoteList.tsx index b5516407..331125ab 100644 --- a/src/screens/Notes/NoteList.tsx +++ b/src/screens/Notes/NoteList.tsx @@ -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 { + styles!: Record; 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 { diff --git a/src/screens/Notes/Notes.tsx b/src/screens/Notes/Notes.tsx index f7eb5367..d68c3da3 100644 --- a/src/screens/Notes/Notes.tsx +++ b/src/screens/Notes/Notes.tsx @@ -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) { 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} /> )} { + styles!: Record; _onPress = () => { this.props.navigation.navigate(SCREEN_SETTINGS); }; diff --git a/src/screens/Root.tsx b/src/screens/Root.tsx index 63677148..c24655ba 100644 --- a/src/screens/Root.tsx +++ b/src/screens/Root.tsx @@ -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 { + styles!: Record; + 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) { 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 */ } /> diff --git a/src/screens/Settings/Sections/AuthSection.tsx b/src/screens/Settings/Sections/AuthSection.tsx index 4a423967..0dd51d92 100644 --- a/src/screens/Settings/Sections/AuthSection.tsx +++ b/src/screens/Settings/Sections/AuthSection.tsx @@ -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 { + static emailInProgress: any; + static passwordInProgress: any; + constructor(props: Readonly) { 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 = {}; 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()} diff --git a/src/screens/Settings/Sections/CompanySection.tsx b/src/screens/Settings/Sections/CompanySection.tsx index 0c8ee9d5..6eddda21 100644 --- a/src/screens/Settings/Sections/CompanySection.tsx +++ b/src/screens/Settings/Sections/CompanySection.tsx @@ -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 { + onAction = (action: string) => { if (action === 'feedback') { const platformString = Platform.OS === 'android' ? 'Android' : 'iOS'; ApplicationState.openURL( diff --git a/src/screens/Settings/Sections/EncryptionSection.tsx b/src/screens/Settings/Sections/EncryptionSection.tsx index b7e15e20..aa7c5615 100644 --- a/src/screens/Settings/Sections/EncryptionSection.tsx +++ b/src/screens/Settings/Sections/EncryptionSection.tsx @@ -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 { + constructor(props: Readonly) { 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 = { diff --git a/src/screens/Settings/Sections/OptionsSection.tsx b/src/screens/Settings/Sections/OptionsSection.tsx index f7fe03f3..491b10a0 100644 --- a/src/screens/Settings/Sections/OptionsSection.tsx +++ b/src/screens/Settings/Sections/OptionsSection.tsx @@ -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; + onManagePrivileges: () => void; +} & AbstractProps; + +type State = { + loadingExport: boolean; + encryptionAvailable: boolean; + email?: string | null; + signedIn: boolean; + lastExportDate?: Date; +} & AbstractState; + +class OptionsSection extends Abstract { + constructor(props: Readonly) { 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 */} 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 { + constructor(props: Readonly) { 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} > { +type State = { + confirmRegistration: boolean; + hasPasscode: boolean; + storageEncryption: any; + storageEncryptionLoading: boolean; + hasBiometrics: boolean; +} & AbstractState; + +export default class Settings extends Abstract { + 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) { 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) diff --git a/src/screens/SideMenu/AbstractSideMenu.tsx b/src/screens/SideMenu/AbstractSideMenu.tsx index 15339350..a1637004 100644 --- a/src/screens/SideMenu/AbstractSideMenu.tsx +++ b/src/screens/SideMenu/AbstractSideMenu.tsx @@ -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 { + handler: any; + shouldComponentUpdate(nextProps: Readonly) { /* 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, diff --git a/src/screens/SideMenu/MainSideMenu.tsx b/src/screens/SideMenu/MainSideMenu.tsx index f115db01..d968074e 100644 --- a/src/screens/SideMenu/MainSideMenu.tsx +++ b/src/screens/SideMenu/MainSideMenu.tsx @@ -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; + 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 = diff --git a/src/screens/SideMenu/NoteSideMenu.tsx b/src/screens/SideMenu/NoteSideMenu.tsx index cf309656..579f499f 100644 --- a/src/screens/SideMenu/NoteSideMenu.tsx +++ b/src/screens/SideMenu/NoteSideMenu.tsx @@ -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; + constructor(props: Readonly) { 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: () => { diff --git a/src/screens/SideMenu/SideMenuCell.tsx b/src/screens/SideMenu/SideMenuCell.tsx index 9c0d1bc8..e733b4e9 100644 --- a/src/screens/SideMenu/SideMenuCell.tsx +++ b/src/screens/SideMenu/SideMenuCell.tsx @@ -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 { + styles!: Record; 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 ( ); @@ -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: { diff --git a/src/screens/SideMenu/SideMenuHero.tsx b/src/screens/SideMenu/SideMenuHero.tsx index 063edbb8..8b184be3 100644 --- a/src/screens/SideMenu/SideMenuHero.tsx +++ b/src/screens/SideMenu/SideMenuHero.tsx @@ -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 { + styles!: Record; getText() { const offline = Auth.get().offline(); const hasEncryption = diff --git a/src/screens/SideMenu/SideMenuManager.tsx b/src/screens/SideMenu/SideMenuManager.tsx index 872cd9fc..249ec007 100644 --- a/src/screens/SideMenu/SideMenuManager.tsx +++ b/src/screens/SideMenu/SideMenuManager.tsx @@ -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; } diff --git a/src/screens/SideMenu/SideMenuSection.tsx b/src/screens/SideMenu/SideMenuSection.tsx index 6d3f1dcb..635f63c7 100644 --- a/src/screens/SideMenu/SideMenuSection.tsx +++ b/src/screens/SideMenu/SideMenuSection.tsx @@ -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 { + styles!: Record; 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) { 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} > diff --git a/src/screens/SideMenu/TagSelectionList.tsx b/src/screens/SideMenu/TagSelectionList.tsx index 1bf392f6..c59c1e33 100644 --- a/src/screens/SideMenu/TagSelectionList.tsx +++ b/src/screens/SideMenu/TagSelectionList.tsx @@ -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 { + styles!: Record; + handledDataLoad: any; + signoutObserver: any; + syncObserverId: string | undefined; + syncEventHandler: any; /* @param props.selectedTags @param props.onTagSelect */ - constructor(props) { + constructor(props: Readonly) { 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)} /> diff --git a/src/style/ActionSheetWrapper.tsx b/src/style/ActionSheetWrapper.tsx index 82d4f5e2..2e189807 100644 --- a/src/style/ActionSheetWrapper.tsx +++ b/src/style/ActionSheetWrapper.tsx @@ -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(); + 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 ( (this.actionSheet = o)} + ref={this.actionSheet} title={this.title} options={this.options.map(option => { return option.text; diff --git a/src/style/StyleKit.ts b/src/style/StyleKit.ts index 03bd02a7..9a3c4e40 100644 --- a/src/style/StyleKit.ts +++ b/src/style/StyleKit.ts @@ -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; + constants: { mainTextFontSize: number; paddingLeft: number }; + styles: Record = {}; + 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; } } diff --git a/src/style/ThemeManager.ts b/src/style/ThemeManager.ts index 8cb16e0b..b3c56527 100644 --- a/src/style/ThemeManager.ts +++ b/src/style/ThemeManager.ts @@ -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(); } diff --git a/src/style/Themes/blue.json b/src/style/Themes/blue.json index c6195787..fd5fda9f 100644 --- a/src/style/Themes/blue.json +++ b/src/style/Themes/blue.json @@ -36,5 +36,6 @@ "stylekitInputPlaceholderColor": "rgb(168, 168, 168)", "stylekitInputBorderColor": "#e3e3e3", "stylekitScrollbarThumbColor": "#dfdfdf", - "stylekitScrollbarTrackBorderColor": "#E7E7E7" + "stylekitScrollbarTrackBorderColor": "#E7E7E7", + "statusBar": "" } diff --git a/src/style/Themes/red.json b/src/style/Themes/red.json index 268a749a..88c45add 100644 --- a/src/style/Themes/red.json +++ b/src/style/Themes/red.json @@ -36,5 +36,6 @@ "stylekitInputPlaceholderColor": "rgb(168, 168, 168)", "stylekitInputBorderColor": "#e3e3e3", "stylekitScrollbarThumbColor": "#dfdfdf", - "stylekitScrollbarTrackBorderColor": "#E7E7E7" + "stylekitScrollbarTrackBorderColor": "#E7E7E7", + "statusBar": "" } diff --git a/src/style/Util/CSSParser.ts b/src/style/Util/CSSParser.ts index 3b37c915..7071310a 100644 --- a/src/style/Util/CSSParser.ts +++ b/src/style/Util/CSSParser.ts @@ -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 = {}; 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, + 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); } } diff --git a/src/style/Util/ThemeDownloader.ts b/src/style/Util/ThemeDownloader.ts index 0716d569..b752e0d9 100644 --- a/src/style/Util/ThemeDownloader.ts +++ b/src/style/Util/ThemeDownloader.ts @@ -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); }, diff --git a/src/style/utils.ts b/src/style/utils.ts index 0072ce4b..2bd93c7c 100644 --- a/src/style/utils.ts +++ b/src/style/utils.ts @@ -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) { diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 00000000..d71934c8 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1 @@ +declare module 'react-native-search-box'; diff --git a/src/types/react-native-search-box/index.d.ts b/src/types/react-native-search-box/index.d.ts new file mode 100644 index 00000000..27b91c17 --- /dev/null +++ b/src/types/react-native-search-box/index.d.ts @@ -0,0 +1 @@ +declare module 'react-native-seach-box'; diff --git a/src/types/snjs/index.d.ts b/src/types/snjs/index.d.ts new file mode 100644 index 00000000..249fd224 --- /dev/null +++ b/src/types/snjs/index.d.ts @@ -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; +} diff --git a/tsconfig.json b/tsconfig.json index 7eaaecf8..eb53173c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ] } diff --git a/yarn.lock b/yarn.lock index db57a6e5..d5795dc0 100644 --- a/yarn.lock +++ b/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"