* Add TS to lib/components/styles/authentication

* Rollback prettier changes to declutter diff

* Authentication migration

526 errors

* Notes

328 errors

* Settings

172 errors

* refactor: last screens

* fix: revert setting wrong sideMenuHandler

* feature: upgrade type deps
This commit is contained in:
Radek Czemerys
2020-05-07 10:57:52 +02:00
committed by GitHub
parent d86b4b98f0
commit 6149f150df
81 changed files with 2278 additions and 948 deletions

View File

@@ -9,4 +9,4 @@ module.exports = {
singleQuote: true,
tabWidth: 2,
strailingComma: "none"
};
};

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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();
}

View File

@@ -3,7 +3,17 @@ import { TouchableHighlight, Text, View } from 'react-native';
import SectionedTableCell from '@Components/SectionedTableCell';
import StyleKit from '@Style/StyleKit';
export default class ButtonCell extends SectionedTableCell {
type Props = {
maxHeight?: number;
leftAligned?: boolean;
bold?: boolean;
disabled?: boolean;
important?: boolean;
onPress: () => void;
title?: string;
};
export default class ButtonCell extends SectionedTableCell<Props> {
rules() {
const rules = super.rules();
if (this.props.maxHeight) {
@@ -13,7 +23,7 @@ export default class ButtonCell extends SectionedTableCell {
}
buttonRules() {
const rules = [StyleKit.stylesForKey('buttonCellButton')];
let rules = StyleKit.stylesForKey('buttonCellButton');
if (this.props.leftAligned) {
rules.push(StyleKit.styles.buttonCellButtonLeft);
}

View File

@@ -1,9 +1,19 @@
import React from 'react';
import { View } from 'react-native';
import SectionedTableCell from '@Components/SectionedTableCell';
import { View, ViewStyle } from 'react-native';
import SectionedTableCell, {
Props as SectionTableCellProps,
} from '@Components/SectionedTableCell';
export default class Circle extends SectionedTableCell {
constructor(props) {
type Props = {
size?: number;
backgroundColor: ViewStyle['backgroundColor'];
borderColor: ViewStyle['borderColor'];
};
export default class Circle extends SectionedTableCell<Props> {
styles!: Record<string, ViewStyle>;
size: number;
constructor(props: Readonly<SectionTableCellProps & Props>) {
super(props);
this.size = props.size || 12;
this.loadStyles();

View File

@@ -1,10 +1,16 @@
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { View, Text, TextStyle } from 'react-native';
import PlatformStyles from '@Models/PlatformStyles';
import StyleKit from '@Style/StyleKit';
export default class HeaderTitleView extends Component {
constructor(props) {
type Props = {
subtitleColor?: TextStyle['color'];
title: string;
subtitle?: string;
};
export default class HeaderTitleView extends Component<Props> {
constructor(props: Readonly<Props>) {
super(props);
}

View File

@@ -1,9 +1,28 @@
import React from 'react';
import { Text, Platform, View, TouchableOpacity } from 'react-native';
import {
Text,
Platform,
View,
TouchableOpacity,
ViewStyle,
TextStyle,
} from 'react-native';
import ThemedComponent from '@Components/ThemedComponent';
import StyleKit from '@Style/StyleKit';
export default class SectionHeader extends ThemedComponent {
type Props = {
title: string;
subtitle?: string;
buttonText?: string;
buttonAction?: () => void;
buttonStyles?: ViewStyle | TextStyle;
tinted?: boolean;
backgroundColor?: ViewStyle['backgroundColor'];
foregroundColor?: string;
};
export default class SectionHeader extends ThemedComponent<Props> {
styles!: Record<string, ViewStyle | TextStyle>;
render() {
let title = this.props.title;
if (Platform.OS === 'ios') {

View File

@@ -5,7 +5,23 @@ import StyleKit from '@Style/StyleKit';
import Icon from 'react-native-vector-icons/Ionicons';
export default class SectionedAccessoryTableCell extends SectionedTableCell {
type Props = {
disabled?: boolean;
onPress: () => void;
onLongPress?: () => void;
iconName?: string;
selected?: () => void;
leftAlignIcon?: boolean;
color?: string;
bold?: boolean;
tinted?: boolean;
dimmed?: boolean;
text: string;
};
export default class SectionedAccessoryTableCell extends SectionedTableCell<
Props
> {
rules() {
const rules = super
.rules()
@@ -63,15 +79,14 @@ export default class SectionedAccessoryTableCell extends SectionedTableCell {
if (this.props.color) {
color = this.props.color;
}
let icon = null;
var icon = (
<View key={0} style={iconStyles}>
<Icon name={iconName} size={iconSize} color={color} />
</View>
);
if (!iconName) {
icon = null;
if (iconName) {
icon = (
<View key={0} style={iconStyles}>
<Icon name={iconName} size={iconSize} color={color} />
</View>
);
}
var textStyles = [StyleKit.styles.sectionedAccessoryTableCellLabel];
@@ -95,13 +110,6 @@ export default class SectionedAccessoryTableCell extends SectionedTableCell {
</Text>
);
const containerStyles = {
flex: 1,
justifyContent: left ? 'flex-start' : 'space-between',
flexDirection: 'row',
alignItems: 'center',
};
return (
<TouchableHighlight
testID={this.props.testID}
@@ -110,7 +118,14 @@ export default class SectionedAccessoryTableCell extends SectionedTableCell {
onPress={this.onPress}
onLongPress={this.onLongPress}
>
<View style={containerStyles}>
<View
style={{
flex: 1,
justifyContent: left ? 'flex-start' : 'space-between',
flexDirection: 'row',
alignItems: 'center',
}}
>
{this.props.leftAlignIcon ? [icon, textWrapper] : [textWrapper, icon]}
</View>
</TouchableHighlight>

View File

@@ -1,9 +1,28 @@
import React from 'react';
import { View, Text, TouchableHighlight } from 'react-native';
import {
View,
Text,
TouchableHighlight,
ViewStyle,
TextStyle,
} from 'react-native';
import ThemedComponent from '@Components/ThemedComponent';
import StyleKit from '@Style/StyleKit';
export default class SectionedOptionsTableCell extends ThemedComponent {
type Option = { selected: boolean; key: string; title: string };
type Props = {
title: string;
first?: boolean;
height?: number;
extraStyles?: ViewStyle;
testID?: string;
onPress: (option: Option) => void;
options: Option[];
};
export default class SectionedOptionsTableCell extends ThemedComponent<Props> {
styles!: Record<string, ViewStyle | TextStyle>;
rules() {
let rules = [StyleKit.styles.sectionedTableCell];
if (this.props.first) {

View File

@@ -3,14 +3,25 @@ import { View } from 'react-native';
import StyleKit from '@Style/StyleKit';
export default class SectionedTableCell extends Component {
export type Props = {
first?: boolean;
last?: boolean;
textInputCell?: any;
height?: number;
extraStyles?: any;
testID?: string;
};
export default class SectionedTableCell<AdditionalProps = {}> extends Component<
Props & AdditionalProps
> {
rules() {
let rules = [StyleKit.styles.sectionedTableCell];
if (this.props.first) {
rules.push(StyleKit.stylesForKey('sectionedTableCellFirst'));
rules.concat(StyleKit.stylesForKey('sectionedTableCellFirst'));
}
if (this.props.last) {
rules.push(StyleKit.stylesForKey('sectionedTableCellLast'));
rules.concat(StyleKit.stylesForKey('sectionedTableCellLast'));
}
if (this.props.textInputCell) {
rules.push(StyleKit.styles.textInputCell);

View File

@@ -1,8 +1,12 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import { View, ViewStyle } from 'react-native';
import StyleKit from '@Style/StyleKit';
export default class TableSection extends Component {
type Props = {
extraStyles?: ViewStyle | ViewStyle[];
};
export default class TableSection extends Component<Props> {
rules() {
let rules = [StyleKit.styles.tableSection];
if (this.props.extraStyles) {

View File

@@ -1,8 +1,9 @@
import { Component } from 'react';
import StyleKit from '@Style/StyleKit';
export default class ThemedComponent extends Component {
constructor(props) {
export default class ThemedComponent<P = {}, S = any> extends Component<P, S> {
themeChangeObserver: () => void;
constructor(props: Readonly<P>) {
super(props);
this.loadStyles();

View File

@@ -1,8 +1,12 @@
import { PureComponent } from 'react';
import StyleKit from '@Style/StyleKit';
export default class ThemedPureComponent extends PureComponent {
constructor(props) {
export default class ThemedPureComponent<P = {}, S = {}> extends PureComponent<
P,
S
> {
themeChangeObserver: () => void;
constructor(props: Readonly<P>) {
super(props);
this.loadStyles();

View File

@@ -1,38 +1,58 @@
import React, { Component } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import {
View,
Text,
TouchableOpacity,
ViewStyle,
TextStyle,
} from 'react-native';
import StyleKit from '@Style/StyleKit';
import { isArray } from 'lodash';
export default class LockedView extends Component {
constructor(props) {
type Props = {
style?: ViewStyle[];
onUnlockPress?: () => void;
};
export default class LockedView extends Component<Props> {
styles?: {
unlockButton: ViewStyle;
unlockButtonText: TextStyle;
};
constructor(props: Props) {
super(props);
this.loadStyles();
}
render() {
const color = StyleKit.variables.stylekitInfoColor;
const styles = [
let styles = [
StyleKit.styles.centeredContainer,
{ backgroundColor: StyleKit.variables.stylekitBackgroundColor },
];
if (this.props.style) {
styles.push(this.props.style);
if (isArray(this.props.style)) {
styles = styles.concat(this.props.style);
} else {
styles.push(this.props.style);
}
}
return (
<View style={styles}>
<Text style={{ color: color, marginTop: 5, fontWeight: 'bold' }}>
<Text style={{ color, marginTop: 5, fontWeight: 'bold' }}>
Application Locked.
</Text>
{!this.props.onUnlockPress && (
<Text style={{ color: color, marginTop: 5 }}>
<Text style={{ color, marginTop: 5 }}>
Return to Notes to unlock.
</Text>
)}
{this.props.onUnlockPress && (
<TouchableOpacity onPress={this.props.onUnlockPress}>
<View style={this.styles.unlockButton}>
<Text style={this.styles.unlockButtonText}>Unlock</Text>
<View style={this.styles?.unlockButton}>
<Text style={this.styles?.unlockButtonText}>Unlock</Text>
</View>
</TouchableOpacity>
)}

View File

@@ -13,4 +13,5 @@
// require('core-js/fn/array/find');
// TODO: still crashes without this
// @ts-ignore
global._ = require('lodash');

View File

@@ -5,6 +5,9 @@ import {
Linking,
Alert,
Keyboard,
AppStateStatus,
KeyboardEventListener,
EmitterSubscription,
} from 'react-native';
import _ from 'lodash';
import KeysManager from '@Lib/keysManager';
@@ -16,36 +19,96 @@ import AuthenticationSourceBiometric from '@Screens/Authentication/Sources/Authe
const pjson = require('../../package.json');
const { PlatformConstants } = NativeModules;
export type AppStateType =
| typeof ApplicationState.Launching
| typeof ApplicationState.LosingFocus
| typeof ApplicationState.Backgrounding
| typeof ApplicationState.GainingFocus
| typeof ApplicationState.ResumingFromBackground
| typeof ApplicationState.Locking
| typeof ApplicationState.Unlocking;
// AppStateEvents
export type AppStateEventType =
| typeof ApplicationState.KeyboardChangeEvent
| typeof ApplicationState.AppStateEventTabletModeChange
| typeof ApplicationState.AppStateEventNoteSideMenuToggle;
export type TabletModeChangeData = {
new_isInTabletMode: boolean;
old_isInTabletMode: boolean;
};
export type NoteSideMenuToggleChange = {
new_isNoteSideMenuCollapsed: boolean;
old_isNoteSideMenuCollapsed: boolean;
};
type KeyboardChangeEventHandler = (
event: typeof ApplicationState.KeyboardChangeEvent,
data: undefined
) => void;
type SideMenuToogleEvent = (
event: typeof ApplicationState.AppStateEventNoteSideMenuToggle,
data: NoteSideMenuToggleChange
) => void;
type TableModeChageEvent = (
event: typeof ApplicationState.AppStateEventTabletModeChange,
data: TabletModeChangeData
) => void;
export type AppStateEventHandler =
| KeyboardChangeEventHandler
| SideMenuToogleEvent
| TableModeChageEvent;
type Observer = {
key: () => number;
callback: (state: AppStateType) => void;
};
export default class ApplicationState {
// When the app first launches
static Launching = 'Launching';
static Launching = 'Launching' as 'Launching';
// When the app enters into multitasking view, or control/notification center for iOS
static LosingFocus = 'LosingFocus';
static LosingFocus = 'LosingFocus' as 'LosingFocus';
// When the app enters the background completely
static Backgrounding = 'Backgrounding';
static Backgrounding = 'Backgrounding' as 'Backgrounding';
// When the app resumes from either the background or from multitasking switcher or notification center
static GainingFocus = 'GainingFocus';
static GainingFocus = 'GainingFocus' as 'GainingFocus';
// When the app resumes from the background
static ResumingFromBackground = 'ResumingFromBackground';
static ResumingFromBackground = 'ResumingFromBackground' as 'ResumingFromBackground';
// When the user enters their local passcode and/or fingerprint
static Locking = 'Locking';
static Locking = 'Locking' as 'Locking';
// When the user enters their local passcode and/or fingerprint
static Unlocking = 'Unlocking';
static Unlocking = 'Unlocking' as 'Unlocking';
/* Seperate events, unrelated to app state notifications */
static AppStateEventTabletModeChange = 'AppStateEventTabletModeChange';
static AppStateEventNoteSideMenuToggle = 'AppStateEventNoteSideMenuToggle';
static KeyboardChangeEvent = 'KeyboardChangeEvent';
static AppStateEventTabletModeChange = 'AppStateEventTabletModeChange' as 'AppStateEventTabletModeChange';
static AppStateEventNoteSideMenuToggle = 'AppStateEventNoteSideMenuToggle' as 'AppStateEventNoteSideMenuToggle';
static KeyboardChangeEvent = 'KeyboardChangeEvent' as 'KeyboardChangeEvent';
static instance = null;
private static instance: ApplicationState;
_isAndroid: boolean;
observers: Observer[];
eventSubscribers: AppStateEventHandler[];
locked: boolean;
keyboardDidShowListener: EmitterSubscription;
keyboardDidHideListener: EmitterSubscription;
keyboardHeight?: number;
optionsState: OptionsState;
loading: boolean = false;
tabletMode: boolean = false;
noteSideMenuCollapsed: boolean = false;
ignoreStateChanges: boolean = false;
mostRecentState?: AppStateType;
didHandleApplicationStart: boolean = false;
authenticationInProgress: boolean = false;
static get() {
if (this.instance == null) {
if (!this.instance) {
this.instance = new ApplicationState();
}
@@ -54,6 +117,7 @@ export default class ApplicationState {
constructor() {
this.observers = [];
this.optionsState = new OptionsState();
this.eventSubscribers = [];
this.locked = true;
this._isAndroid = Platform.OS === 'android';
@@ -74,12 +138,12 @@ export default class ApplicationState {
);
}
keyboardDidShow = e => {
keyboardDidShow: KeyboardEventListener = e => {
this.keyboardHeight = e.endCoordinates.height;
this.notifyEvent(ApplicationState.KeyboardChangeEvent);
};
keyboardDidHide = e => {
keyboardDidHide: KeyboardEventListener = () => {
this.keyboardHeight = 0;
this.notifyEvent(ApplicationState.KeyboardChangeEvent);
};
@@ -90,7 +154,6 @@ export default class ApplicationState {
initializeOptions() {
// Initialize Options (sort by, filter, selected tags, etc)
this.optionsState = new OptionsState();
this.optionsState.addChangeObserver(options => {
if (!this.loading) {
options.persist();
@@ -137,7 +200,7 @@ export default class ApplicationState {
return this.tabletMode;
}
setTabletModeEnabled(enabled) {
setTabletModeEnabled(enabled: boolean) {
if (enabled !== this.tabletMode) {
this.tabletMode = enabled;
this.notifyEvent(ApplicationState.AppStateEventTabletModeChange, {
@@ -151,7 +214,7 @@ export default class ApplicationState {
return this.noteSideMenuCollapsed;
}
setNoteSideMenuCollapsed(collapsed) {
setNoteSideMenuCollapsed(collapsed: boolean) {
if (collapsed !== this.noteSideMenuCollapsed) {
this.noteSideMenuCollapsed = collapsed;
this.notifyEvent(ApplicationState.AppStateEventNoteSideMenuToggle, {
@@ -161,22 +224,26 @@ export default class ApplicationState {
}
}
addEventHandler(handler) {
addEventHandler(handler: AppStateEventHandler) {
this.eventSubscribers.push(handler);
return handler;
}
removeEventHandler(handler) {
removeEventHandler(handler: AppStateEventHandler) {
_.pull(this.eventSubscribers, handler);
}
notifyEvent(event, data) {
notifyEvent(
event: AppStateEventType,
data?: TabletModeChangeData | NoteSideMenuToggleChange
) {
for (const handler of this.eventSubscribers) {
// @ts-ignore not working type
handler(event, data);
}
}
handleAppStateChange = nextAppState => {
handleAppStateChange = (nextAppState: AppStateStatus) => {
if (this.ignoreStateChanges) {
return;
}
@@ -235,14 +302,14 @@ export default class ApplicationState {
// Visibility change events are like active, inactive, background,
// while non-app cycle events are custom events like locking and unlocking
isAppVisibilityChange(state) {
return [
isAppVisibilityChange(state: AppStateType) {
return ([
ApplicationState.Launching,
ApplicationState.LosingFocus,
ApplicationState.Backgrounding,
ApplicationState.GainingFocus,
ApplicationState.ResumingFromBackground,
].includes(state);
] as Array<AppStateType>).includes(state);
}
/* State Changes */
@@ -265,7 +332,7 @@ export default class ApplicationState {
this.notifyOfState(ApplicationState.Launching);
}
notifyOfState(state) {
notifyOfState(state: AppStateType) {
if (this.ignoreStateChanges) {
return;
}
@@ -284,7 +351,7 @@ export default class ApplicationState {
Allows other parts of the code to perform external actions without triggering state change notifications.
This is useful on Android when you present a share sheet and dont want immediate authentication to appear.
*/
performActionWithoutStateChangeImpact(block) {
performActionWithoutStateChangeImpact(block: () => void) {
this.ignoreStateChanges = true;
block();
setTimeout(() => {
@@ -296,8 +363,8 @@ export default class ApplicationState {
return this.mostRecentState;
}
addStateObserver(callback) {
const observer = { key: Math.random, callback: callback };
addStateObserver(callback: Observer['callback']) {
const observer = { key: Math.random, callback };
this.observers.push(observer);
if (this.mostRecentState) {
@@ -311,7 +378,7 @@ export default class ApplicationState {
// this.previousEvents = [];
// }
removeStateObserver(observer) {
removeStateObserver(observer: Observer) {
_.pull(this.observers, observer);
}
@@ -347,7 +414,7 @@ export default class ApplicationState {
this.locked = false;
}
setAuthenticationInProgress(inProgress) {
setAuthenticationInProgress(inProgress: boolean) {
this.authenticationInProgress = inProgress;
}
@@ -355,7 +422,7 @@ export default class ApplicationState {
return this.authenticationInProgress;
}
getAuthenticationPropsForAppState(state) {
getAuthenticationPropsForAppState(state: AppStateType) {
// We don't want to do anything on gaining focus, since that may be called extraenously,
// when you come back from notification center, etc. Any immediate locking should be handled
// LosingFocus anyway.
@@ -413,7 +480,7 @@ export default class ApplicationState {
};
}
static openURL(url) {
static openURL(url: string) {
const showAlert = () => {
Alert.alert('Unable to Open', `Unable to open URL ${url}.`);
};

View File

@@ -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.');
}

View File

@@ -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') {

View File

@@ -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);
}
}

View File

@@ -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?`;

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
import { Platform } from 'react-native';
import { Platform, Alert } from 'react-native';
import FlagSecure from 'react-native-flag-secure-android';
import FingerprintScanner from 'react-native-fingerprint-scanner';
import SNReactNative from 'standard-notes-rn';
@@ -16,11 +16,42 @@ const BiometricsPrefs = 'biometrics_prefs';
const FirstRunKey = 'first_run';
const StorageEncryptionKey = 'storage_encryption';
type AccountKeys = {
fingerprint: {
enabled: boolean;
timing: any;
};
encryptedAccountKeys: any;
mk?: string;
ak?: string;
} | null;
type User = { server?: string; email?: string; jwt?: string | null } | null;
export type BiometricsType =
| 'Fingerprint'
| 'Face ID'
| 'Biometrics'
| 'Touch ID';
export default class KeysManager {
static instance = null;
private static instance: KeysManager;
passcodeTiming: string | null = null;
biometricPrefs: any;
accountRelatedStorageKeys: string[];
legacy_fingerprint: any;
encryptedAccountKeys: any;
accountKeys: AccountKeys = null;
loadInitialDataPromise: Promise<void | unknown> | undefined;
user: User = null;
missingFirstRunKey: boolean = false;
accountAuthParams: any;
offlineAuthParams: any;
_storageEncryptionEnabled: boolean = false;
offlineKeys: any;
static get() {
if (this.instance == null) {
if (!this.instance) {
this.instance = new KeysManager();
}
@@ -73,6 +104,7 @@ export default class KeysManager {
return;
}
this.user = this.user ? this.user : {};
this.user.jwt = null;
this.activeKeys().jwt = jwt;
await this.saveUser(this.user);
@@ -233,13 +265,19 @@ export default class KeysManager {
clear internal keys, like first_run. (If you accidentally delete the first_run key when you sign out,
then the next time you sign in and refresh, it will treat it as a new run, and delete all data.)
*/
registerAccountRelatedStorageKeys(storageKeys) {
registerAccountRelatedStorageKeys(storageKeys: ConcatArray<string>) {
this.accountRelatedStorageKeys = _.uniq(
this.accountRelatedStorageKeys.concat(storageKeys)
);
}
parseKeychainValue(keys) {
parseKeychainValue(
keys: {
offline: boolean;
fingerprint: { enabled: boolean; timing: any };
encryptedAccountKeys: any;
} | null
) {
if (keys) {
this.offlineKeys = keys.offline;
if (this.offlineKeys) {
@@ -274,7 +312,9 @@ export default class KeysManager {
}
// what we should write to keychain
async generateKeychainStoreValue() {
async generateKeychainStoreValue(): Promise<
{} | AccountKeys | { offline: { pw: any; timing: string | null } }
> {
let value = {};
// If no offline keys, store account keys directly. Otherwise we'll encrypt account keys and store in storage.
@@ -317,6 +357,8 @@ export default class KeysManager {
}
let value = await this.generateKeychainStoreValue();
// TODO: check keychain
// @ts-ignore don't want to change that
return Keychain.setKeys(value);
}
@@ -338,12 +380,12 @@ export default class KeysManager {
}
}
async persistAccountKeys(keys) {
async persistAccountKeys(keys: AccountKeys) {
this.accountKeys = keys;
return this.persistKeys();
}
async saveUser(user) {
async saveUser(user: User) {
this.user = user;
return Storage.get().setItem('user', JSON.stringify(user));
}
@@ -416,7 +458,7 @@ export default class KeysManager {
// Auth Params
async setAccountAuthParams(authParams) {
async setAccountAuthParams(authParams: any) {
this.accountAuthParams = authParams;
await Storage.get().setItem('auth_params', JSON.stringify(authParams));
@@ -435,12 +477,12 @@ export default class KeysManager {
}
}
async setOfflineAuthParams(authParams) {
async setOfflineAuthParams(authParams: any) {
this.offlineAuthParams = authParams;
return Storage.get().setItem(OfflineParamsKey, JSON.stringify(authParams));
}
defaultProtocolVersionForKeys(keys) {
defaultProtocolVersionForKeys(keys: AccountKeys) {
if (keys && keys.ak) {
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams.
return '002';
@@ -476,8 +518,7 @@ export default class KeysManager {
async clearOfflineKeysAndData(force = false) {
// make sure user is authenticated before performing this step
if (this.offlineKeys && !this.offlineKeys.mk && !force) {
// eslint-disable-next-line no-alert
alert(
Alert.alert(
'Unable to remove passcode. Make sure you are properly authenticated and try again.'
);
return false;
@@ -488,7 +529,7 @@ export default class KeysManager {
return this.persistKeys();
}
async persistOfflineKeys(keys) {
async persistOfflineKeys(keys: any) {
this.setOfflineKeys(keys);
if (!this.passcodeTiming) {
this.passcodeTiming = 'on-quit';
@@ -496,14 +537,14 @@ export default class KeysManager {
return this.persistKeys();
}
async setOfflineKeys(keys) {
async setOfflineKeys(keys: any) {
// offline keys are ephemeral and should not be stored anywhere
this.offlineKeys = keys;
// Check to see if encryptedAccountKeys need decrypting
if (this.encryptedAccountKeys) {
// Decrypt and set
await SF.get().itemTransformer.decryptItem(
await protocolManager.itemTransformer.decryptItem(
this.encryptedAccountKeys,
this.offlineKeys
);
@@ -531,12 +572,12 @@ export default class KeysManager {
return this.biometricPrefs.enabled;
}
async setPasscodeTiming(timing) {
async setPasscodeTiming(timing: string | null) {
this.passcodeTiming = timing;
return this.persistKeys();
}
async setBiometricsTiming(timing) {
async setBiometricsTiming(timing: { key: any }) {
this.biometricPrefs.timing = timing;
return this.saveBiometricPrefs();
}
@@ -591,7 +632,9 @@ export default class KeysManager {
];
}
static getDeviceBiometricsAvailability(callback) {
static getDeviceBiometricsAvailability(
callback: (available: boolean, type?: BiometricsType, noun?: string) => void
) {
if (__DEV__) {
const isAndroid = Platform.OS === 'android';
if (isAndroid && Platform.Version < 23) {
@@ -605,11 +648,10 @@ export default class KeysManager {
}
FingerprintScanner.isSensorAvailable()
.then(type => {
const noun =
type === 'Touch ID' || type === 'Fingerprint' ? 'Fingerprint' : type;
const noun = type === 'Touch ID' ? 'Fingerprint' : type;
callback(true, type, noun);
})
.catch(error => {
.catch(() => {
callback(false);
});
}

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -3,63 +3,70 @@ import { Alert } from 'react-native';
import { SFAlertManager } from 'snjs';
export default class AlertManager extends SFAlertManager {
static instance = null;
private static instance: AlertManager;
static get() {
if (this.instance == null) {
if (!this.instance) {
this.instance = new AlertManager();
}
return this.instance;
}
async alert({ title, text, closeButtonText = 'OK', onClose } = {}) {
return new Promise((resolve, reject) => {
async alert(alertdata: {
title: string;
text: string;
closeButtonText?: string;
onClose?: () => void | Promise<any>;
}) {
return new Promise(resolve => {
// On iOS, confirm should go first. On Android, cancel should go first.
let buttons = [
{
text: closeButtonText,
text: alertdata.closeButtonText,
onPress: async () => {
onClose && (await onClose());
alertdata.onClose && (await alertdata.onClose());
resolve();
},
},
];
Alert.alert(title, text, buttons, { cancelable: true });
Alert.alert(alertdata.title, alertdata.text, buttons, {
cancelable: true,
});
});
}
async confirm({
title,
text,
confirmButtonText = 'OK',
cancelButtonText = 'Cancel',
onConfirm,
onCancel,
onDismiss,
} = {}) {
async confirm(confirmData: {
title: string;
text?: string;
confirmButtonText?: string;
cancelButtonText?: string;
onConfirm?: () => void | Promise<any>;
onCancel?: () => void | Promise<any>;
onDismiss?: () => void | Promise<any>;
}) {
return new Promise((resolve, reject) => {
// On iOS, confirm should go first. On Android, cancel should go first.
let buttons = [
{
text: cancelButtonText,
text: confirmData.cancelButtonText,
onPress: async () => {
onCancel && (await onCancel());
confirmData.onCancel && (await confirmData.onCancel());
reject();
},
},
{
text: confirmButtonText,
text: confirmData.confirmButtonText,
onPress: async () => {
onConfirm && (await onConfirm());
confirmData.onConfirm && (await confirmData.onConfirm());
resolve();
},
},
];
Alert.alert(title, text, buttons, {
Alert.alert(confirmData.title, confirmData.text, buttons, {
cancelable: true,
onDismiss: async () => {
onDismiss && (await onDismiss());
confirmData.onDismiss && (await confirmData.onDismiss());
reject();
},
});

View File

@@ -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,

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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',

View File

@@ -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;

View File

@@ -1,14 +1,16 @@
import { Platform } from 'react-native';
import { SFStorageManager } from 'snjs';
import { SFStorageManager, SFItem as SNJSItem } from 'snjs';
import AsyncStorage from '@react-native-community/async-storage';
import AlertManager from '@Lib/snjs/alertManager';
import { isNullOrUndefined } from '@Lib/utils';
type SFItem = typeof SNJSItem;
export default class Storage extends SFStorageManager {
static instance = null;
private static instance: Storage;
static get() {
if (this.instance == null) {
if (!this.instance) {
this.instance = new Storage();
}
@@ -21,7 +23,7 @@ export default class Storage extends SFStorageManager {
this.platformString = this.isAndroid ? 'Android' : 'iOS';
}
async getItem(key) {
async getItem(key: string) {
try {
return AsyncStorage.getItem(key);
} catch (error) {
@@ -30,10 +32,10 @@ export default class Storage extends SFStorageManager {
}
}
async getMultiItems(keys) {
async getMultiItems(keys: string[]) {
return AsyncStorage.multiGet(keys).then(stores => {
var items = {};
stores.map((result, i, store) => {
const items: Record<string, any> = {};
stores.map((_result, i, store) => {
let key = store[i][0];
let value = store[i][1];
items[key] = value;
@@ -42,8 +44,8 @@ export default class Storage extends SFStorageManager {
});
}
async setItem(key, value) {
if (isNullOrUndefined(value) || isNullOrUndefined(key)) {
async setItem(key: string, value: string | undefined | null) {
if (value === null || value === undefined || isNullOrUndefined(key)) {
return;
}
try {
@@ -54,11 +56,11 @@ export default class Storage extends SFStorageManager {
}
}
async removeItem(key) {
async removeItem(key: string) {
return AsyncStorage.removeItem(key);
}
async clearKeys(keys) {
async clearKeys(keys: string[]) {
return AsyncStorage.multiRemove(keys);
}
@@ -68,10 +70,10 @@ export default class Storage extends SFStorageManager {
// Models
async getAllModels() {
const itemsFromStores = stores => {
const items = [];
stores.map((result, i, store) => {
const key = store[i][0];
const itemsFromStores = (stores: any[]) => {
const items: any[] = [];
stores.map((_result, i, store) => {
// const key = store[i][0];
const value = store[i][1];
if (value) {
items.push(JSON.parse(value));
@@ -124,7 +126,7 @@ export default class Storage extends SFStorageManager {
*/
const keys = await this.getAllModelKeys();
let items = [];
let items: any[] = [];
const failedItemIds = [];
if (this.isAndroid) {
for (const key of keys) {
@@ -155,7 +157,7 @@ export default class Storage extends SFStorageManager {
return items;
}
showLoadFailForItemIds(failedItemIds) {
showLoadFailForItemIds(failedItemIds: string[]) {
let text = `The following items could not be loaded. This may happen if you are in low-memory conditions, or if the note is very large in size. For compatibility with ${this.platformString}, we recommend breaking up large notes into smaller chunks using the desktop or web app.\n\nItems:\n`;
let index = 0;
text += failedItemIds.map(id => {
@@ -169,7 +171,7 @@ export default class Storage extends SFStorageManager {
AlertManager.get().alert({ title: 'Unable to load item', text: text });
}
keyForItem(item) {
keyForItem(item: SFItem) {
return 'Item-' + item.uuid;
}
@@ -181,7 +183,9 @@ export default class Storage extends SFStorageManager {
return filtered;
}
async saveModel(item) {
// TODO: Not sure about this
// @ts-ignore
async saveModel(item: SFItem) {
return this.saveModel([item]);
}
@@ -190,7 +194,7 @@ export default class Storage extends SFStorageManager {
AsyncStorage.multiSet(data, function(error){ callback(); })
Each item is saved individually.
*/
async saveModels(items) {
async saveModels(items?: SFItem[]) {
if (!items || items.length === 0) {
return;
}
@@ -205,11 +209,11 @@ export default class Storage extends SFStorageManager {
);
}
async deleteModel(item) {
async deleteModel(item: SFItem) {
return AsyncStorage.removeItem(this.keyForItem(item));
}
async clearAllModels(callback) {
async clearAllModels() {
const itemKeys = await this.getAllModelKeys();
return AsyncStorage.multiRemove(itemKeys);
}

View File

@@ -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 ||

View File

@@ -6,9 +6,10 @@ export const DONT_SHOW_AGAIN_UNSUPPORTED_EDITORS_KEY =
'DoNotShowAgainUnsupportedEditorsKey';
export default class UserPrefsManager {
static instance = null;
private static instance: UserPrefsManager;
data: Record<string, any>;
static get() {
if (this.instance == null) {
if (!this.instance) {
this.instance = new UserPrefsManager();
}
return this.instance;
@@ -18,37 +19,39 @@ export default class UserPrefsManager {
this.data = {};
}
async clearPref({ key }) {
async clearPref({ key }: { key: string }) {
this.data[key] = null;
return Storage.get().clearKeys([key]);
}
async setPref({ key, value }) {
async setPref({ key, value }: { key: string; value: unknown }) {
await Storage.get().setItem(key, JSON.stringify(value));
this.data[key] = value;
}
async getPref({ key }) {
async getPref({ key }: { key: string }) {
if (isNullOrUndefined(this.data[key])) {
this.data[key] = JSON.parse(await Storage.get().getItem(key));
const item = await Storage.get().getItem(key);
this.data[key] = JSON.parse(item ?? '');
}
return this.data[key];
}
async getPrefAsDate({ key }) {
async getPrefAsDate({ key }: { key: string }) {
if (isNullOrUndefined(this.data[key])) {
this.data[key] = dateFromJsonString(await Storage.get().getItem(key));
const item = await Storage.get().getItem(key);
this.data[key] = dateFromJsonString(item ?? '');
}
return this.data[key];
}
async isPrefSet({ key }) {
async isPrefSet({ key }: { key: string }) {
return (await this.getPref({ key: key })) !== null;
}
async isPrefEqualTo({ key, value }) {
async isPrefEqualTo({ key, value }: { key: string; value: unknown }) {
return (await this.getPref({ key: key })) === value;
}
}

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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');
};

View File

@@ -1,17 +1,17 @@
import React from 'react';
import HeaderButtons, {
HeaderButton,
Item,
HeaderButtonProps,
} from 'react-navigation-header-buttons';
import _ from 'lodash';
import Icon from 'react-native-vector-icons/Ionicons';
import HeaderTitleView from '@Components/HeaderTitleView';
import ThemedComponent from '@Components/ThemedComponent';
import ApplicationState from '@Lib/ApplicationState';
import ApplicationState, { AppStateType } from '@Lib/ApplicationState';
import PrivilegesManager from '@Lib/snjs/privilegesManager';
import StyleKit from '@Style/StyleKit';
const IoniconsHeaderButton = passMeFurther => (
const IoniconsHeaderButton = (passMeFurther: HeaderButtonProps) => (
// the `passMeFurther` variable here contains props from <Item .../> as well as <HeaderButtons ... />
// and it is important to pass those props to `HeaderButton`
// then you may add some information like icon size or color (if you use icons)
@@ -23,11 +23,33 @@ const IoniconsHeaderButton = passMeFurther => (
/>
);
export default class Abstract extends ThemedComponent {
export type AbstractProps = {
navigation: any;
};
export type AbstractState = {
lockContent?: boolean;
};
export default class Abstract<
TProps extends AbstractProps = AbstractProps,
TState extends AbstractState = AbstractState
> extends ThemedComponent<TProps, TState> {
static getDefaultNavigationOptions = ({
navigation,
navigationOptions,
_navigationOptions,
templateOptions,
}: {
navigation: {
getParam: (arg0: string) => string | undefined;
};
_navigationOptions: any;
templateOptions?: {
title?: any;
subtitle?: any;
leftButton?: any;
rightButton?: any;
};
}) => {
// templateOptions allow subclasses to specifiy things they want to display in nav bar before it actually loads.
// this way, things like title and the Done button in the top left are visible during transition
@@ -56,7 +78,7 @@ export default class Abstract extends ThemedComponent {
if (leftButton) {
headerLeft = (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
<HeaderButtons.Item
testID="headerButton"
disabled={leftButton.disabled}
title={leftButton.title}
@@ -66,6 +88,7 @@ export default class Abstract extends ThemedComponent {
</HeaderButtons>
);
// @ts-ignore setting a property on navigation object
options.headerLeft = headerLeft;
}
@@ -74,7 +97,7 @@ export default class Abstract extends ThemedComponent {
if (rightButton) {
headerRight = (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
<HeaderButtons.Item
disabled={rightButton.disabled}
title={rightButton.title}
iconName={rightButton.iconName}
@@ -83,35 +106,51 @@ export default class Abstract extends ThemedComponent {
</HeaderButtons>
);
// @ts-ignore setting a property on navigation object
options.headerRight = headerRight;
}
return options;
};
static navigationOptions = ({ navigation, navigationOptions }) => {
static navigationOptions = (navigationProps: {
navigation: any;
navigationOptions: any;
}) => {
return Abstract.getDefaultNavigationOptions({
navigation,
navigationOptions,
navigation: navigationProps.navigation,
_navigationOptions: navigationProps.navigationOptions,
});
};
listeners: any[];
_stateObserver: {
key: () => number;
callback: (state: AppStateType) => void;
};
willUnmount: boolean = false;
mounted: boolean = false;
loadedInitialState: boolean = false;
_renderOnMount?: boolean;
_renderOnMountCallback: (() => void) | null = null;
willBeVisible: boolean = false;
visible: boolean = false;
constructor(props) {
constructor(props: Readonly<TProps>) {
super(props);
this.state = { lockContent: true };
this.state = { lockContent: true } as TState;
this.listeners = [
this.props.navigation.addListener('willFocus', payload => {
this.props.navigation.addListener('willFocus', () => {
this.componentWillFocus();
}),
this.props.navigation.addListener('didFocus', payload => {
this.props.navigation.addListener('didFocus', () => {
this.componentDidFocus();
}),
this.props.navigation.addListener('willBlur', payload => {
this.props.navigation.addListener('willBlur', () => {
this.componentWillBlur();
}),
this.props.navigation.addListener('didBlur', payload => {
this.props.navigation.addListener('didBlur', () => {
this.componentDidBlur();
}),
];
@@ -131,9 +170,9 @@ export default class Abstract extends ThemedComponent {
});
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(nextProps: TProps, nextState: TState) {
let isSame =
Abstract.IsDeepEqual(nextProps, this.props, null, ['navigation']) &&
Abstract.IsDeepEqual(nextProps, this.props, [], ['navigation']) &&
Abstract.IsDeepEqual(nextState, this.state);
return !isSame;
}
@@ -203,21 +242,21 @@ export default class Abstract extends ThemedComponent {
this.visible = false;
}
getProp = prop => {
getProp = (prop: any) => {
// this.props.navigation could be undefined if we're in the drawer
return (
this.props.navigation.getParam && this.props.navigation.getParam(prop)
);
};
setTitle(title) {
let options = {};
setTitle(title: string) {
let options: { title?: string } = {};
options.title = title;
this.props.navigation.setParams(options);
}
setSubTitle(subtitle, color) {
let options = {};
setSubTitle(subtitle: string | null, color?: string) {
let options: { subtitle?: string | null; subtitleColor?: string } = {};
options.subtitle = subtitle;
options.subtitleColor = color;
this.props.navigation.setParams(options);
@@ -225,10 +264,10 @@ export default class Abstract extends ThemedComponent {
lockContent() {
this.mergeState({ lockContent: true });
this.configureNavBar();
this.configureNavBar(false);
}
unlockContent(callback) {
unlockContent(callback?: { (): void }) {
if (!this.loadedInitialState) {
this.loadInitialState();
}
@@ -237,14 +276,22 @@ export default class Abstract extends ThemedComponent {
});
}
constructState(state) {
constructState(state: { title?: any; noteLocked?: boolean; text?: any }) {
this.state = _.merge(
{ lockContent: ApplicationState.get().isLocked() },
state
);
) as TState;
}
mergeState(state) {
mergeState(
state:
| {}
| ((
prevState: Readonly<TState>,
props: Readonly<TProps>
) => {} | Pick<{}, never> | null)
| null
) {
/*
We're getting rid of the original implementation of this, which was to pass a function into set state.
The reason was, when compared new and previous values in componentShouldUpdate, if we used the function approach,
@@ -257,7 +304,7 @@ export default class Abstract extends ThemedComponent {
this.setState(state);
}
renderOnMount(callback) {
renderOnMount(callback: () => any) {
if (this.isMounted()) {
this.forceUpdate();
callback && callback();
@@ -271,7 +318,7 @@ export default class Abstract extends ThemedComponent {
return this.mounted;
}
configureNavBar(initial) {}
configureNavBar(_initial: boolean) {}
popToRoot() {
this.props.navigation.popToTop();
@@ -284,7 +331,14 @@ export default class Abstract extends ThemedComponent {
this.props.navigation.goBack(null);
}
async handlePrivilegedAction(isProtected, action, run, onCancel) {
async handlePrivilegedAction(
isProtected: boolean,
action: any,
run: {
(): void;
},
onCancel?: { (): void; (): any }
) {
if (isProtected) {
const actionRequiresPrivs = await PrivilegesManager.get().actionRequiresPrivilege(
action
@@ -306,7 +360,11 @@ export default class Abstract extends ThemedComponent {
}
}
static IsShallowEqual = (newObj, prevObj, keys) => {
static IsShallowEqual = (
newObj: { [x: string]: any },
prevObj: { [x: string]: any },
keys: string[]
) => {
if (!keys) {
keys = Object.keys(newObj);
}
@@ -318,7 +376,12 @@ export default class Abstract extends ThemedComponent {
return true;
};
static IsDeepEqual = (newObj, prevObj, keys, omitKeys = []) => {
static IsDeepEqual = (
newObj: {},
prevObj: any,
keys?: string[],
omitKeys: string[] = []
) => {
if (!keys) {
keys = Object.keys(newObj);
}

View File

@@ -1,17 +1,38 @@
import React from 'react';
import { TextInput, View, Alert, ScrollView } from 'react-native';
import {
TextInput,
View,
Alert,
ScrollView,
ViewStyle,
TextStyle,
} from 'react-native';
import _ from 'lodash';
import ButtonCell from '@Components/ButtonCell';
import SectionHeader from '@Components/SectionHeader';
import SectionedAccessoryTableCell from '@Components/SectionedAccessoryTableCell';
import SectionedTableCell from '@Components/SectionedTableCell';
import ApplicationState from '@Lib/ApplicationState';
import Abstract from '@Screens/Abstract';
import ApplicationState, { AppStateType } from '@Lib/ApplicationState';
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
import { ICON_CLOSE } from '@Style/icons';
import StyleKit from '@Style/StyleKit';
import AuthenticationSourceAccountPassword from './Sources/AuthenticationSourceAccountPassword';
import AuthenticationSourceBiometric from './Sources/AuthenticationSourceBiometric';
import AuthenticationSourceLocalPasscode from './Sources/AuthenticationSourceLocalPasscode';
export default class Authenticate extends Abstract {
static navigationOptions = ({ navigation, navigationOptions }) => {
type Source =
| AuthenticationSourceAccountPassword
| AuthenticationSourceBiometric
| AuthenticationSourceLocalPasscode;
type State = {
activeSource: any | null;
submitDisabled: boolean;
sourceLocked: boolean;
} & AbstractState;
export default class Authenticate extends Abstract<AbstractProps, State> {
static navigationOptions = ({ navigation, navigationOptions }: any) => {
const templateOptions = {
/**
* On Android, not having a left button will make the title appear all
@@ -21,12 +42,22 @@ export default class Authenticate extends Abstract {
};
return Abstract.getDefaultNavigationOptions({
navigation,
navigationOptions,
_navigationOptions: navigationOptions,
templateOptions,
});
};
styles!: Record<string, ViewStyle | TextStyle>;
stateObserver: {
key: () => number;
callback: (state: AppStateType) => void;
};
pendingSources: Source[];
_sessionLength: number;
successfulSources: Source[];
activeSource: Source | null = null;
needsSuccessCallback: any;
constructor(props) {
constructor(props: Readonly<AbstractProps>) {
super(props);
for (const source of this.sources) {
@@ -67,7 +98,7 @@ export default class Authenticate extends Abstract {
this.successfulSources = [];
}
get sources() {
get sources(): Source[] {
return this.getProp('authenticationSources');
}
@@ -134,7 +165,7 @@ export default class Authenticate extends Abstract {
}
}
async beginAuthenticationForSource(source) {
async beginAuthenticationForSource(source: Source) {
/**
* Authentication modal may be displayed on lose focus just before the app
* is closing. In this state however, we don't want to begin auth. We'll
@@ -158,7 +189,7 @@ export default class Authenticate extends Abstract {
this.forceUpdate();
}
successfulSourcesIncludesSource(source) {
successfulSourcesIncludesSource(source: Source) {
for (const candidate of this.successfulSources) {
if (candidate.identifier === source.identifier) {
return true;
@@ -178,7 +209,7 @@ export default class Authenticate extends Abstract {
return true;
}
async validateAuthentication(source) {
async validateAuthentication(source: Source) {
if (this.state.sourceLocked) {
return;
}
@@ -219,7 +250,7 @@ export default class Authenticate extends Abstract {
}
}
async onBiometricDirectPress(source) {
async onBiometricDirectPress(source: Source) {
if (source.isLocked()) {
return;
}
@@ -237,7 +268,7 @@ export default class Authenticate extends Abstract {
* When a source returns in a locked status we create a timeout for the lock
* period. This will auto reprompt the user for auth after the period is up.
*/
onSourceLocked(source) {
onSourceLocked(source: Source) {
this.setState({ sourceLocked: true, submitDisabled: true });
setTimeout(() => {
@@ -283,7 +314,7 @@ export default class Authenticate extends Abstract {
this.needsSuccessCallback = false;
}
inputTextChanged(text, source) {
inputTextChanged(text: string, source: Source) {
source.setAuthenticationValue(text);
this.forceUpdate();
}
@@ -292,15 +323,19 @@ export default class Authenticate extends Abstract {
return this.getProp('sessionLengthOptions');
}
setSessionLength(length) {
setSessionLength(length: number) {
this._sessionLength = length;
this.forceUpdate();
}
_renderAuthenticationSoure = (source, index) => {
_renderAuthenticationSoure = (source: Source, index: number) => {
const isLast = index === this.sources.length - 1;
const inputAuthenticationSource = source => (
const inputAuthenticationSource = (
inputSource:
| AuthenticationSourceAccountPassword
| AuthenticationSourceLocalPasscode
) => (
<View
style={[
this.styles.authSourceSection,
@@ -314,31 +349,33 @@ export default class Authenticate extends Abstract {
>
<TextInput
ref={ref => {
source.inputRef = ref;
inputSource.inputRef = ref;
}}
style={StyleKit.styles.sectionedTableCellTextInput}
placeholder={source.inputPlaceholder}
placeholder={inputSource.inputPlaceholder}
onChangeText={text => {
this.inputTextChanged(text, source);
this.inputTextChanged(text, inputSource);
}}
value={source.getAuthenticationValue()}
value={inputSource.getAuthenticationValue()}
autoCorrect={false}
autoFocus={false}
autoCapitalize={'none'}
secureTextEntry={true}
keyboardType={source.keyboardType || 'default'}
keyboardType={inputSource.keyboardType || 'default'}
keyboardAppearance={StyleKit.get().keyboardColorForActiveTheme()}
underlineColorAndroid={'transparent'}
placeholderTextColor={StyleKit.variables.stylekitNeutralColor}
onSubmitEditing={() => {
this.validateAuthentication(source);
this.validateAuthentication(inputSource);
}}
/>
</SectionedTableCell>
</View>
);
const biometricAuthenticationSource = source => (
const biometricAuthenticationSource = (
biometricSource: AuthenticationSourceBiometric
) => (
<View
style={[
this.styles.authSourceSection,
@@ -347,11 +384,11 @@ export default class Authenticate extends Abstract {
>
<SectionedAccessoryTableCell
first={true}
dimmed={source !== this.state.activeSource}
tinted={source === this.state.activeSource}
text={source.label}
dimmed={biometricSource !== this.state.activeSource}
tinted={biometricSource === this.state.activeSource}
text={biometricSource.label}
onPress={() => {
this.onBiometricDirectPress(source);
this.onBiometricDirectPress(biometricSource);
}}
/>
</View>
@@ -369,14 +406,40 @@ export default class Authenticate extends Abstract {
<View key={source.identifier}>
<SectionHeader
title={sourceTitle}
subtitle={hasHeaderSubtitle && source.label}
subtitle={hasHeaderSubtitle ? source.label : undefined}
tinted={source === this.state.activeSource}
buttonText={source.headerButtonText}
buttonAction={source.headerButtonAction}
buttonStyles={source.headerButtonStyles}
buttonText={
source.type === 'input'
? (source as
| AuthenticationSourceAccountPassword
| AuthenticationSourceLocalPasscode).headerButtonText
: undefined
}
buttonAction={
source.type === 'input'
? (source as
| AuthenticationSourceAccountPassword
| AuthenticationSourceLocalPasscode).headerButtonAction
: undefined
}
buttonStyles={
source.type === 'input'
? (source as
| AuthenticationSourceAccountPassword
| AuthenticationSourceLocalPasscode).headerButtonStyles
: undefined
}
/>
{source.type === 'input' && inputAuthenticationSource(source)}
{source.type === 'biometric' && biometricAuthenticationSource(source)}
{source.type === 'input' &&
inputAuthenticationSource(
source as
| AuthenticationSourceAccountPassword
| AuthenticationSourceLocalPasscode
)}
{source.type === 'biometric' &&
biometricAuthenticationSource(
source as AuthenticationSourceBiometric
)}
</View>
);
};
@@ -396,7 +459,8 @@ export default class Authenticate extends Abstract {
})}
<ButtonCell
style={this.styles.submitButtonCell}
// TODO: there is no style prop
// style={this.styles.submitButtonCell}
maxHeight={45}
disabled={this.state.submitDisabled}
title={this.pendingSources.length > 1 ? 'Next' : 'Submit'}
@@ -407,20 +471,22 @@ export default class Authenticate extends Abstract {
{this.sessionLengthOptions && this.sessionLengthOptions.length > 0 && (
<View style={this.styles.rememberForSection}>
<SectionHeader title={'Remember For'} />
{this.sessionLengthOptions.map((option, index) => (
<SectionedAccessoryTableCell
text={option.label}
key={`${index}`}
first={index === 0}
last={index === this.sessionLengthOptions.length - 1}
selected={() => {
return option.value === this._sessionLength;
}}
onPress={() => {
this.setSessionLength(option.value);
}}
/>
))}
{this.sessionLengthOptions.map(
(option: { label: string; value: number }, index: number) => (
<SectionedAccessoryTableCell
text={option.label}
key={`${index}`}
first={index === 0}
last={index === this.sessionLengthOptions.length - 1}
selected={() => {
return option.value === this._sessionLength;
}}
onPress={() => {
this.setSessionLength(option.value);
}}
/>
)
)}
</View>
)}
</ScrollView>

View File

@@ -1,6 +1,18 @@
import { TextInput } from 'react-native';
const DEFAULT_LOCK_TIMEOUT = 30 * 1000;
export default class AuthenticationSource {
status:
| 'waiting-input'
| 'locked'
| 'processing'
| 'waiting-turn'
| 'did-fail'
| 'did-succeed';
authenticationValue?: string;
onRequiresInterfaceReload?: () => void;
inputRef: TextInput | null = null;
constructor() {
this.status = 'waiting-turn';
}
@@ -63,15 +75,15 @@ export default class AuthenticationSource {
return this.status === 'processing';
}
setAuthenticationValue(value) {
setAuthenticationValue(value: string) {
this.authenticationValue = value;
}
getAuthenticationValue(value) {
getAuthenticationValue() {
return this.authenticationValue;
}
async authenticate() {}
async authenticate(): Promise<any> {}
cancel() {}

View File

@@ -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 } };
}

View File

@@ -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();
}

View File

@@ -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 } };
}

View File

@@ -1,5 +1,12 @@
import React, { Component } from 'react';
import { Alert, View, Platform, Text } from 'react-native';
import {
Alert,
View,
Platform,
Text,
ViewStyle,
TextStyle,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { WebView } from 'react-native-webview';
import ApplicationState from '@Lib/ApplicationState';
@@ -11,11 +18,24 @@ import UserPrefsManager, {
import { ICON_LOCK } from '@Style/icons';
import StyleKit from '@Style/StyleKit';
export default class ComponentView extends Component {
constructor(props) {
super(props);
type Props = {
noteId: string;
editorId: string;
onLoadEnd: () => void;
onLoadStart: () => void;
onLoadError: () => void;
};
this.state = {};
export default class ComponentView extends Component<Props> {
styles!: Record<string, ViewStyle | TextStyle>;
identifier: string;
editor: any;
note: any;
registrationTimeout: any;
webView: WebView | null = null;
alreadyTriggeredLoad: boolean = false;
constructor(props: Readonly<Props>) {
super(props);
this.loadStyles();
@@ -83,7 +103,7 @@ export default class ComponentView extends Component {
}
}
componentDidUpdate(prevProps, prevState) {
componentDidUpdate(prevProps: Props) {
if (
prevProps.noteId !== this.props.noteId ||
prevProps.editorId !== this.props.editorId
@@ -107,7 +127,7 @@ export default class ComponentView extends Component {
ComponentManager.get().deactivateComponent(this.editor);
}
onMessage = message => {
onMessage = (message: { nativeEvent: { data: string } }) => {
if (!this.note) {
/** May be the case in tablet mode on app launch */
return;
@@ -124,7 +144,7 @@ export default class ComponentView extends Component {
ComponentManager.get().handleMessage(this.editor, data);
};
onFrameLoad = syntheticEvent => {
onFrameLoad = () => {
/**
* We have no way of knowing if the webview load is successful or not. We
* have to wait to see if the error event is fired. Looking at the code,
@@ -165,12 +185,15 @@ export default class ComponentView extends Component {
}
};
onLoadError = syntheticEvent => {
onLoadError = () => {
clearTimeout(this.registrationTimeout);
this.props.onLoadError();
};
onShouldStartLoadWithRequest = request => {
onShouldStartLoadWithRequest = (request: {
navigationType: string;
url: string;
}) => {
/**
* We want to handle link clicks within an editor by opening the browser
* instead of loading inline. On iOS, onShouldStartLoadWithRequest is
@@ -246,13 +269,13 @@ export default class ComponentView extends Component {
onLoadStart={this.onLoadStart}
onError={this.onLoadError}
onMessage={this.onMessage}
useWebKit={true}
hideKeyboardAccessoryView={true}
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
cacheEnabled={true}
scalesPageToFit={
true /* Android only, not available with WKWebView */
}
// @ts-ignore this is patched
autoManageStatusBarEnabled={
false /* To prevent StatusBar from changing colors when focusing */
}

View File

@@ -7,26 +7,54 @@ import {
Platform,
Text,
Alert,
ViewStyle,
TextStyle,
} from 'react-native';
import TextView from 'sn-textview';
import { SFAuthManager } from 'snjs';
import Icon from 'react-native-vector-icons/Ionicons';
import { SafeAreaView } from 'react-navigation';
import LockedView from '@Containers/LockedView';
import ApplicationState from '@Lib/ApplicationState';
import ApplicationState, {
AppStateEventHandler,
AppStateEventType,
TabletModeChangeData,
NoteSideMenuToggleChange,
} from '@Lib/ApplicationState';
import ComponentManager from '@Lib/componentManager';
import Auth from '@Lib/snjs/authManager';
import ModelManager from '@Lib/snjs/modelManager';
import Sync from '@Lib/snjs/syncManager';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
import ComponentView from '@Screens/ComponentView';
import SideMenuManager from '@Screens/SideMenu/SideMenuManager';
import { ICON_ALERT, ICON_LOCK, ICON_MENU } from '@Style/icons';
import StyleKit from '@Style/StyleKit';
import { lighten } from '@Style/utils';
export default class Compose extends Abstract {
static navigationOptions = ({ navigation, navigationOptions }) => {
type State = {
webViewError: any;
loadingWebView: boolean;
title: string;
text: string;
noteLocked: boolean;
};
type Props = {
selectedTagId?: string;
};
export default class Compose extends Abstract<
Props & AbstractProps,
State & AbstractState
> {
static navigationOptions = ({
navigation,
navigationOptions,
}: {
navigation: any;
navigationOptions: any;
}) => {
const templateOptions = {
rightButton: {
title: null,
@@ -35,12 +63,26 @@ export default class Compose extends Abstract {
};
return Abstract.getDefaultNavigationOptions({
navigation,
navigationOptions,
_navigationOptions: navigationOptions,
templateOptions,
});
};
styles!: Record<string, ViewStyle | TextStyle>;
rawStyles!: Record<string, ViewStyle | TextStyle>;
note: any;
syncObserver: any;
componentHandler: any;
signoutObserver: any;
tabletModeChangeHandler?: AppStateEventHandler;
didShowErrorAlert?: boolean;
rightMenuHandler: SideMenuManager['rightSideMenuHandler'] = null;
input: TextView | null = null;
saveError?: boolean;
statusTimeout?: ReturnType<typeof setTimeout>;
syncTakingTooLong: boolean = false;
saveTimeout?: ReturnType<typeof setTimeout>;
constructor(props) {
constructor(props: Readonly<Props & AbstractProps>) {
super(props);
let note,
@@ -73,35 +115,36 @@ export default class Compose extends Abstract {
}
registerObservers() {
this.syncObserver = Sync.get().addEventHandler((event, data) => {
if (event === 'sync:error') {
this.showSavedStatus(false);
} else if (event === 'sync:completed') {
let isInRetrieved =
data.retrievedItems &&
data.retrievedItems.map(i => i.uuid).includes(this.note.uuid);
let isInSaved =
data.savedItems &&
data.savedItems.map(i => i.uuid).includes(this.note.uuid);
if (this.note.deleted || this.note.content.trashed) {
let clearNote = this.note.deleted === true;
// if Trashed, and we're in the Trash view, don't clear note.
if (!this.note.deleted) {
let selectedTag = this.getSelectedTag();
let isTrashTag =
selectedTag != null && selectedTag.content.isTrashTag === true;
if (this.note.content.trashed) {
// clear the note if this is not the trash tag. Otherwise, keep it in view.
clearNote = !isTrashTag;
this.syncObserver = Sync.get().addEventHandler(
(event: string, data: { retrievedItems: any[]; savedItems: any[] }) => {
if (event === 'sync:error') {
this.showSavedStatus(false);
} else if (event === 'sync:completed') {
let isInRetrieved =
data.retrievedItems &&
data.retrievedItems.map(i => i.uuid).includes(this.note.uuid);
let isInSaved =
data.savedItems &&
data.savedItems.map(i => i.uuid).includes(this.note.uuid);
if (this.note.deleted || this.note.content.trashed) {
let clearNote = this.note.deleted === true;
// if Trashed, and we're in the Trash view, don't clear note.
if (!this.note.deleted) {
let selectedTag = this.getSelectedTag();
let isTrashTag =
selectedTag != null && selectedTag.content.isTrashTag === true;
if (this.note.content.trashed) {
// clear the note if this is not the trash tag. Otherwise, keep it in view.
clearNote = !isTrashTag;
}
}
}
if (clearNote) {
this.props.navigation.closeRightDrawer();
this.setNote(null);
}
} else if (this.note.uuid && (isInRetrieved || isInSaved)) {
/*
if (clearNote) {
this.props.navigation.closeRightDrawer();
this.setNote(null);
}
} else if (this.note.uuid && (isInRetrieved || isInSaved)) {
/*
You have to be careful about when you render inside this component. Rendering with the native SNTextView component
can cause the cursor to go to the end of the input, both on iOS and Android. We want to force an update only if retrievedItems includes this item
@@ -114,31 +157,36 @@ export default class Compose extends Abstract {
Do not make text part of the state, otherwise that would cause a re-render on every keystroke.
*/
let newState = {
title: this.note.title,
noteLocked: this.note.locked ? true : false,
};
let newState = {
title: this.note.title,
noteLocked: this.note.locked ? true : false,
};
// only include `text` if this item is coming back from retrieved, as this will cause text view cursor to reset to top
if (isInRetrieved) {
newState.text = this.note.text;
// only include `text` if this item is coming back from retrieved, as this will cause text view cursor to reset to top
if (isInRetrieved) {
newState = Object.assign(newState, { text: this.note.text });
}
// Use true/false for note.locked as we don't want values of null or undefined, which may cause
// unnecessary renders. (on constructor it was undefined, and here, it was null, causing a re-render to occur on android, causing textview to reset cursor)
this.setState(newState);
}
// Use true/false for note.locked as we don't want values of null or undefined, which may cause
// unnecessary renders. (on constructor it was undefined, and here, it was null, causing a re-render to occur on android, causing textview to reset cursor)
this.setState(newState);
}
if ((isInSaved && !this.note.dirty) || this.saveError) {
this.showSavedStatus(true);
if ((isInSaved && !this.note.dirty) || this.saveError) {
this.showSavedStatus(true);
}
}
}
});
);
this.componentHandler = ComponentManager.get().registerHandler({
identifier: 'composer',
areas: ['editor-editor'],
actionHandler: (component, action, data) => {
actionHandler: (
_component: any,
action: string,
data: { items: any[] }
) => {
if (action === 'save-items') {
if (
data.items
@@ -153,17 +201,24 @@ export default class Compose extends Abstract {
},
});
this.signoutObserver = Auth.get().addEventHandler(event => {
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
if (event === SFAuthManager.DidSignOutEvent) {
this.setNote(null);
}
});
this.tabletModeChangeHandler = ApplicationState.get().addEventHandler(
(event, data) => {
(
event: AppStateEventType,
data: TabletModeChangeData | NoteSideMenuToggleChange | undefined
) => {
if (event === ApplicationState.AppStateEventTabletModeChange) {
// If we are now in tablet mode after not being in tablet mode
if (data.new_isInTabletMode && !data.old_isInTabletMode) {
if (
data &&
(data as TabletModeChangeData).new_isInTabletMode &&
!(data as TabletModeChangeData).old_isInTabletMode
) {
this.setSideMenuHandler();
}
}
@@ -185,7 +240,10 @@ export default class Compose extends Abstract {
2. In setNote(null), if the current note is already a dummy, don't do anything.
*/
setNote(note, isConstructor = false) {
setNote(
note: { dummy: boolean; initUUID: () => Promise<any>; title: any } | null,
isConstructor = false
) {
if (!note) {
if (this.note && this.note.dummy) {
// This method can be called if the + button is pressed. On Tablet, it can be pressed even while we're
@@ -198,12 +256,12 @@ export default class Compose extends Abstract {
dummy: true,
text: '',
});
note.dummy = true;
note!.dummy = true;
// Editors need a valid note with uuid and modelmanager mapped in order to interact with it
// Note that this can create dummy notes that aren't deleted automatically.
// Also useful to keep right menu enabled at all times. If the note has a uuid and is a dummy,
// it will be removed locally on blur
note.initUUID().then(() => {
note!.initUUID().then(() => {
ModelManager.get().addItem(note);
this.forceUpdate();
});
@@ -212,7 +270,7 @@ export default class Compose extends Abstract {
this.note = note;
if (!isConstructor) {
this.setState({ title: note.title });
this.setState({ title: note!.title });
this.forceUpdate();
}
}
@@ -237,7 +295,9 @@ export default class Compose extends Abstract {
// we ignore render events. This will cause the screen to be white when you save the new tag.
SideMenuManager.get().removeHandlerForRightSideMenu(this.rightMenuHandler);
Auth.get().removeEventHandler(this.signoutObserver);
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
if (this.tabletModeChangeHandler) {
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
}
Sync.get().removeEventHandler(this.syncObserver);
ComponentManager.get().deregisterHandler(this.componentHandler);
}
@@ -267,6 +327,7 @@ export default class Compose extends Abstract {
if (this.note.dummy) {
if (this.refs.input) {
// @ts-ignore ignore wrong focus type
this.refs.input.focus();
}
}
@@ -335,7 +396,7 @@ export default class Compose extends Abstract {
}
}
replaceTagsForNote(newTags) {
replaceTagsForNote(newTags: string | any[]) {
let note = this.note;
var oldTags = note.tags.slice(); // original array will be modified in the for loop so we make a copy
@@ -356,13 +417,13 @@ export default class Compose extends Abstract {
}
}
onTitleChange = text => {
onTitleChange = (text: string) => {
this.setState({ title: text });
this.note.title = text;
this.changesMade();
};
onTextChange = text => {
onTextChange = (text: string) => {
if (this.note.locked) {
// On Android, we don't disable the text view if the note is locked, as that also disables selection.
Alert.alert(
@@ -389,7 +450,7 @@ export default class Compose extends Abstract {
this.setSubTitle('Saving...');
}
showSavedStatus(success) {
showSavedStatus(success: boolean) {
const debouceMs = 300; // minimum time message is shown
if (success) {
if (this.statusTimeout) {
@@ -457,12 +518,18 @@ export default class Compose extends Abstract {
}, 325);
}
sync(note, callback) {
sync(
note: {
setDirty: (arg0: boolean, arg1: boolean) => void;
hasChanges: boolean;
},
callback?: (arg0: boolean) => void
) {
note.setDirty(true, true);
Sync.get()
.sync()
.then(response => {
.then((response: { error: any }) => {
if (response && response.error) {
if (!this.didShowErrorAlert) {
this.didShowErrorAlert = true;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { TextInput, Keyboard, Alert } from 'react-native';
import { TextInput, Keyboard, Alert, KeyboardTypeOptions } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import ButtonCell from '@Components/ButtonCell';
import SectionedTableCell from '@Components/SectionedTableCell';
@@ -7,12 +7,17 @@ import SectionedOptionsTableCell from '@Components/SectionedOptionsTableCell';
import TableSection from '@Components/TableSection';
import LockedView from '@Containers/LockedView';
import ApplicationState from '@Lib/ApplicationState';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
import { ICON_CLOSE } from '@Style/icons';
import StyleKit from '@Style/StyleKit';
export default class InputModal extends Abstract {
static navigationOptions = ({ navigation, navigationOptions }) => {
type State = {
text: string;
confirmText: string;
} & AbstractState;
export default class InputModal extends Abstract<AbstractProps, State> {
static navigationOptions = ({ navigation, navigationOptions }: any) => {
const templateOptions = {
leftButton: {
title: ApplicationState.isIOS ? 'Cancel' : null,
@@ -23,12 +28,17 @@ export default class InputModal extends Abstract {
};
return Abstract.getDefaultNavigationOptions({
navigation,
navigationOptions,
_navigationOptions: navigationOptions,
templateOptions,
});
};
requireConfirm: boolean;
showKeyboardChooser: boolean;
keyboardType: KeyboardTypeOptions;
confirmInputRef: TextInput | null = null;
inputRef: TextInput | null = null;
constructor(props) {
constructor(props: Readonly<AbstractProps>) {
super(props);
props.navigation.setParams({
@@ -56,7 +66,7 @@ export default class InputModal extends Abstract {
onTextSubmit = () => {
if (this.requireConfirm && !this.state.confirmText) {
this.confirmInputRef.focus();
this.confirmInputRef?.focus();
} else {
this.submit();
}
@@ -87,11 +97,11 @@ export default class InputModal extends Abstract {
this.dismiss();
};
onTextChange = text => {
onTextChange = (text: string) => {
this.setState({ text: text });
};
onConfirmTextChange = text => {
onConfirmTextChange = (text: string) => {
this.setState({ confirmText: text });
};
@@ -105,7 +115,7 @@ export default class InputModal extends Abstract {
}
}
onKeyboardOptionsSelect = option => {
onKeyboardOptionsSelect = (option: { key: any }) => {
this.keyboardType = option.key;
this.forceUpdate();
this.refreshKeyboard();

View File

@@ -7,15 +7,19 @@ import SectionedTableCell from '@Components/SectionedTableCell';
import TableSection from '@Components/TableSection';
import ApplicationState from '@Lib/ApplicationState';
import KeysManager from '@Lib/keysManager';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
import ModelManager from '@Lib/snjs/modelManager';
import Sync from '@Lib/snjs/syncManager';
import AlertManager from '@Lib/snjs/alertManager';
import { ICON_CLOSE } from '@Style/icons';
import StyleKit from '@Style/StyleKit';
export default class KeyRecovery extends Abstract {
static navigationOptions = ({ navigation, navigationOptions }) => {
type State = {
text: string;
} & AbstractState;
export default class KeyRecovery extends Abstract<AbstractProps, State> {
static navigationOptions = ({ navigation, navigationOptions }: any) => {
const templateOptions = {
title: 'Key Recovery',
leftButton: {
@@ -27,12 +31,15 @@ export default class KeyRecovery extends Abstract {
};
return Abstract.getDefaultNavigationOptions({
navigation,
navigationOptions,
_navigationOptions: navigationOptions,
templateOptions,
});
};
items: any;
encryptedCount: number = 0;
inputRef: TextInput | null = null;
constructor(props) {
constructor(props: Readonly<AbstractProps>) {
super(props);
props.navigation.setParams({
@@ -48,7 +55,6 @@ export default class KeyRecovery extends Abstract {
});
this.state = { text: '' };
this.reloadData();
}
@@ -72,7 +78,7 @@ export default class KeyRecovery extends Abstract {
this.state.text,
authParams
);
await SF.get().itemTransformer.decryptMultipleItems(this.items, keys);
await protocolManager.decryptMultipleItems(this.items, keys);
this.encryptedCount = 0;
for (const item of this.items) {
@@ -81,7 +87,7 @@ export default class KeyRecovery extends Abstract {
}
}
let useKeys = async confirm => {
let useKeys = async (confirm?: boolean) => {
const run = async () => {
await KeysManager.get().persistOfflineKeys(keys);
await ModelManager.get().mapResponseItemsToLocalModelsOmittingFields(
@@ -132,7 +138,7 @@ export default class KeyRecovery extends Abstract {
}
};
onTextChange = text => {
onTextChange = (text: string) => {
this.setState({ text: text });
};

View File

@@ -10,12 +10,20 @@ import ApplicationState from '@Lib/ApplicationState';
import KeysManager from '@Lib/keysManager';
import Auth from '@Lib/snjs/authManager';
import PrivilegesManager from '@Lib/snjs/privilegesManager';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
import { ICON_CHECKMARK } from '@Style/icons';
import StyleKit from '@Style/StyleKit';
export default class ManagePrivileges extends Abstract {
static navigationOptions = ({ navigation, navigationOptions }) => {
type State = {
availableActions: any[];
availableCredentials: any[];
notConfiguredCredentials?: any[];
sessionExpirey?: string;
sessionExpired?: boolean;
} & AbstractState;
export default class ManagePrivileges extends Abstract<AbstractProps, State> {
static navigationOptions = ({ navigation, navigationOptions }: any) => {
const templateOptions = {
title: 'Privileges',
leftButton: {
@@ -27,12 +35,17 @@ export default class ManagePrivileges extends Abstract {
};
return Abstract.getDefaultNavigationOptions({
navigation,
navigationOptions,
_navigationOptions: navigationOptions,
templateOptions,
});
};
hasPasscode: boolean;
hasAccount: boolean;
privileges: any;
credentialDisplayInfo: Record<string, string> = {};
styles: any;
constructor(props) {
constructor(props: Readonly<AbstractProps>) {
super(props);
this.state = {
@@ -58,15 +71,15 @@ export default class ManagePrivileges extends Abstract {
this.reloadPrivileges();
}
displayInfoForCredential(credential) {
displayInfoForCredential(credential: any) {
return PrivilegesManager.get().displayInfoForCredential(credential).label;
}
displayInfoForAction(action) {
displayInfoForAction(action: any) {
return PrivilegesManager.get().displayInfoForAction(action).label;
}
isCredentialRequiredForAction(action, credential) {
isCredentialRequiredForAction(action: any, credential: any) {
if (!this.privileges) {
return false;
}
@@ -120,14 +133,14 @@ export default class ManagePrivileges extends Abstract {
});
}
valueChanged(action, credential) {
valueChanged(action: any, credential: any) {
this.privileges.toggleCredentialForAction(action, credential);
PrivilegesManager.get().savePrivileges();
this.forceUpdate();
}
credentialUnavailable = credential => {
return this.state.notConfiguredCredentials.includes(credential);
credentialUnavailable = (credential: any) => {
return this.state.notConfiguredCredentials?.includes(credential);
};
render() {

View File

@@ -1,18 +1,49 @@
import React from 'react';
import { StyleSheet, View, Text, TouchableWithoutFeedback } from 'react-native';
import {
StyleSheet,
View,
Text,
TouchableWithoutFeedback,
TextStyle,
ViewStyle,
} from 'react-native';
import ThemedPureComponent from '@Components/ThemedPureComponent';
import ItemActionManager from '@Lib/itemActionManager';
import ItemActionManager, { EventType } from '@Lib/itemActionManager';
import ActionSheetWrapper from '@Style/ActionSheetWrapper';
import StyleKit from '@Style/StyleKit';
import { hexToRGBA } from '@Style/utils';
import OptionsState from '@Lib/OptionsState';
export default class NoteCell extends ThemedPureComponent {
constructor(props) {
type Props = {
item: any;
options: OptionsState;
highlighted?: boolean;
onPressItem: (item: any) => void;
handleAction: (item: any, key: EventType, callback: () => void) => void;
renderTags: boolean;
sortType: string;
tagsString: string;
};
type State = {
selected: boolean;
options: OptionsState;
actionSheet: JSX.Element | null;
};
export default class NoteCell extends ThemedPureComponent<Props, State> {
styles!: Record<string, ViewStyle | TextStyle>;
selectionTimeout?: number;
constructor(props: Readonly<Props>) {
super(props);
this.state = { selected: false, options: props.options || {} };
this.state = {
selected: false,
options: props.options || {},
actionSheet: null,
};
}
static getDerivedStateFromProps(props, state) {
static getDerivedStateFromProps(props: Props, state: State) {
if (props.options !== state.options) {
return { options: props.options };
}
@@ -41,7 +72,7 @@ export default class NoteCell extends ThemedPureComponent {
this.setState({ selected: false });
};
aggregateStyles(base, addition, condition) {
aggregateStyles(base: ViewStyle, addition: ViewStyle, condition: boolean) {
if (condition) {
return [base, addition];
} else {
@@ -54,7 +85,7 @@ export default class NoteCell extends ThemedPureComponent {
return;
}
let callbackForAction = action => {
let callbackForAction = (action: any) => {
this.props.handleAction(this.props.item, action.key, () => {
this.forceUpdate();
});
@@ -165,7 +196,14 @@ export default class NoteCell extends ThemedPureComponent {
sheet.show();
};
getFlags(note) {
getFlags(note: {
pinned: boolean;
archived: boolean;
content: { protected: boolean; trashed: boolean; conflict_of: any };
locked: boolean;
errorDecrypting: any;
deleted: boolean;
}) {
let flags = [];
if (note.pinned) {
@@ -227,7 +265,7 @@ export default class NoteCell extends ThemedPureComponent {
return flags;
}
flagElement = flag => {
flagElement = (flag: { color: any; text: any }) => {
let bgColor = flag.color;
let textColor = StyleKit.variables.stylekitInfoContrastColor;
if (this.state.selected || this.props.highlighted) {
@@ -246,7 +284,7 @@ export default class NoteCell extends ThemedPureComponent {
text: {
color: textColor,
fontSize: 10,
fontWeight: 'bold',
fontWeight: 'bold' as 'bold',
},
};
return (
@@ -269,7 +307,7 @@ export default class NoteCell extends ThemedPureComponent {
note.tags.length > 0 &&
!note.content.protected;
const highlight = this.state.selected || this.props.highlighted;
const highlight = Boolean(this.state.selected || this.props.highlighted);
return (
<TouchableWithoutFeedback
@@ -284,7 +322,8 @@ export default class NoteCell extends ThemedPureComponent {
this.styles.noteCellSelected,
highlight
)}
onPress={this._onPress}
// TODO: onPreess does not exist on view
// onPress={this._onPress}
>
{note.deleted && (
<Text style={this.styles.deleting}>Deleting...</Text>
@@ -389,6 +428,7 @@ export default class NoteCell extends ThemedPureComponent {
loadStyles() {
let padding = 14;
// @ts-ignore ignore null return from hexToRGBA
this.styles = StyleSheet.create({
noteCell: {
padding: padding,

View File

@@ -1,5 +1,13 @@
import React from 'react';
import { StyleSheet, View, FlatList, RefreshControl, Text } from 'react-native';
import {
StyleSheet,
View,
FlatList,
RefreshControl,
Text,
ViewStyle,
TextStyle,
} from 'react-native';
import Search from 'react-native-search-box';
import ThemedComponent from '@Components/ThemedComponent';
import ApplicationState from '@Lib/ApplicationState';
@@ -7,8 +15,27 @@ import NoteCell from '@Screens/Notes/NoteCell';
import OfflineBanner from '@Screens/Notes/OfflineBanner';
import Auth from '@Lib/snjs/authManager';
import StyleKit from '@Style/StyleKit';
import { EventType } from '@Lib/itemActionManager';
export default class NoteList extends ThemedComponent {
type Props = {
onSearchChange: (text: string) => void;
onSearchCancel: () => void;
onPressItem: (item: any) => void;
selectedTags: any[];
selectedNoteId: string | null;
handleAction: (item: any, key: EventType, callback: () => void) => void;
sortType: string;
options: any;
decrypting: boolean;
loading: boolean;
hasRefreshControl: boolean;
notes: any[];
refreshing: boolean;
onRefresh: () => void;
};
export default class NoteList extends ThemedComponent<Props> {
styles!: Record<string, ViewStyle | TextStyle>;
renderHeader = () => {
const isOffline = Auth.get().offline();
@@ -48,7 +75,7 @@ export default class NoteList extends ThemedComponent {
* Must pass title, text, and tags as props so that it re-renders when either
* of those change.
*/
_renderItem = ({ item }) => {
_renderItem = ({ item }: any) => {
/**
* On Android, only one tag is selected at a time. If it is selected, we
* don't need to display the tags string above the note cell.
@@ -63,6 +90,7 @@ export default class NoteList extends ThemedComponent {
<NoteCell
item={item}
onPressItem={this.props.onPressItem}
// @ts-ignore TODO: not used props for extra data
title={item.title}
text={item.text}
tags={item.tags}
@@ -72,6 +100,7 @@ export default class NoteList extends ThemedComponent {
options={this.props.options}
highlighted={item.uuid === this.props.selectedNoteId}
handleAction={this.props.handleAction}
// @ts-ignore TODO: not used props for extra data
pinned={item.pinned /* extraData */}
deleted={item.deleted /* extraData */}
archived={item.archived /* extraData */}
@@ -111,7 +140,7 @@ export default class NoteList extends ThemedComponent {
keyboardDismissMode={'interactive'}
keyboardShouldPersistTaps={'always'}
refreshControl={
!this.props.hasRefreshControl ? null : (
!this.props.hasRefreshControl ? undefined : (
<RefreshControl
refreshing={this.props.refreshing}
onRefresh={this.props.onRefresh}
@@ -119,7 +148,7 @@ export default class NoteList extends ThemedComponent {
)
}
data={this.props.notes}
options={this.props.options}
// options={this.props.options} no options prop
renderItem={this._renderItem}
ListHeaderComponent={this.renderHeader}
/>

View File

@@ -7,11 +7,16 @@ import LockedView from '@Containers/LockedView';
import Auth from '@Lib/snjs/authManager';
import ModelManager from '@Lib/snjs/modelManager';
import Sync from '@Lib/snjs/syncManager';
import ApplicationState from '@Lib/ApplicationState';
import ItemActionManager from '@Lib/itemActionManager';
import ApplicationState, {
AppStateEventHandler,
AppStateEventType,
TabletModeChangeData,
NoteSideMenuToggleChange,
} from '@Lib/ApplicationState';
import ItemActionManager, { EventType } from '@Lib/itemActionManager';
import KeysManager from '@Lib/keysManager';
import OptionsState from '@Lib/OptionsState';
import Abstract from '@Screens/Abstract';
import OptionsState, { Observer } from '@Lib/OptionsState';
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
import NoteList from '@Screens/Notes/NoteList';
import {
SCREEN_SETTINGS,
@@ -24,8 +29,35 @@ import StyleKit from '@Style/StyleKit';
import { SFAuthManager, SFPrivilegesManager } from 'snjs';
export default class Notes extends Abstract {
constructor(props) {
type Props = {
onNoteSelect?: (note: any) => void;
onUnlockPress: () => void;
};
type State = {
notes: any[];
tags: any[];
refreshing: boolean;
selectedNoteId: string | null;
decrypting: boolean;
loading: boolean;
};
export default class Notes extends Abstract<
Props & AbstractProps,
State & AbstractState
> {
stateNotes: never[];
options: OptionsState;
loadNotesOnVisible: boolean = false;
searching: any;
tabletModeChangeHandler?: AppStateEventHandler;
syncObserver: any;
signoutObserver: any;
optionsObserver?: Observer;
styles: {} = {};
mappingObserver: any;
constructor(props: Readonly<Props & AbstractProps>) {
super(props);
this.stateNotes = [];
@@ -64,12 +96,12 @@ export default class Notes extends Abstract {
this.reloadList();
}
unlockContent(callback) {
unlockContent(callback: { (): void; (): any } | undefined) {
super.unlockContent(() => {
// wait for the state.unlocked setState call to finish
if (this.searching) {
this.searching = false;
this.options.setSearchTerm(null);
this.options?.setSearchTerm(null);
}
this.reloadHeaderBar();
@@ -95,7 +127,13 @@ export default class Notes extends Abstract {
}
}
reloadTabletStateForEvent({ focus, blur }) {
reloadTabletStateForEvent({
focus,
blur,
}: {
focus?: boolean;
blur?: boolean;
}) {
if (focus) {
if (!ApplicationState.get().isInTabletMode) {
this.props.navigation.lockLeftDrawer(false);
@@ -130,17 +168,18 @@ export default class Notes extends Abstract {
componentWillUnmount() {
super.componentWillUnmount();
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
if (this.tabletModeChangeHandler) {
ApplicationState.get().removeEventHandler(this.tabletModeChangeHandler);
}
Sync.get().removeEventHandler(this.syncObserver);
Auth.get().removeEventHandler(this.signoutObserver);
if (this.options) {
if (this.options && this.optionsObserver) {
this.options.removeChangeObserver(this.optionsObserver);
}
}
registerObservers() {
this.optionsObserver = this.options.addChangeObserver(
this.optionsObserver = this.options?.addChangeObserver(
(options, eventType) => {
this.reloadList(true);
@@ -163,11 +202,11 @@ export default class Notes extends Abstract {
this.mappingObserver = ModelManager.get().addItemSyncObserver(
'notes-screen',
['Tag', 'Note'],
(allRelevantItems, validItems, deletedItems) => {
(allRelevantItems: any, validItems: any, deletedItems: any[]) => {
if (deletedItems.find(item => item.content_type === 'Tag')) {
// If a tag was deleted, let's check to see if we should reload our selected tags list
var tags = ModelManager.get().getTagsWithIds(
this.options.selectedTagIds
this.options.selectedTagIds ?? []
);
if (tags.length === 0) {
this.options.setSelectedTagIds(
@@ -180,28 +219,30 @@ export default class Notes extends Abstract {
}
);
this.syncObserver = Sync.get().addEventHandler((event, data) => {
if (event === 'sync:completed') {
this.mergeState({ refreshing: false, loading: false });
} else if (event === 'local-data-loaded') {
this.displayNeedSignInAlertForLocalItemsIfApplicable(
ModelManager.get().allItems
);
this.reloadList();
this.reloadHeaderBar();
this.mergeState({ decrypting: false, loading: false });
if (ApplicationState.get().isInTabletMode) {
this.selectFirstNote();
this.syncObserver = Sync.get().addEventHandler(
(event: string, data: any) => {
if (event === 'sync:completed') {
this.mergeState({ refreshing: false, loading: false });
} else if (event === 'local-data-loaded') {
this.displayNeedSignInAlertForLocalItemsIfApplicable(
ModelManager.get().allItems
);
this.reloadList();
this.reloadHeaderBar();
this.mergeState({ decrypting: false, loading: false });
if (ApplicationState.get().isInTabletMode) {
this.selectFirstNote();
}
} else if (event === 'sync-exception') {
Alert.alert(
'Issue Syncing',
`There was an error while trying to save your items. Please contact support and share this message: ${data}`
);
}
} else if (event === 'sync-exception') {
Alert.alert(
'Issue Syncing',
`There was an error while trying to save your items. Please contact support and share this message: ${data}`
);
}
});
);
this.signoutObserver = Auth.get().addEventHandler(event => {
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
if (event === SFAuthManager.DidSignOutEvent) {
this.reloadList();
} else if (event === SFAuthManager.WillSignInEvent) {
@@ -217,14 +258,18 @@ export default class Notes extends Abstract {
});
this.tabletModeChangeHandler = ApplicationState.get().addEventHandler(
(event, data) => {
(
event: AppStateEventType,
data: TabletModeChangeData | NoteSideMenuToggleChange | undefined
) => {
if (event === ApplicationState.KeyboardChangeEvent) {
if (ApplicationState.get().isInTabletMode) {
this.forceUpdate();
}
} else if (event === ApplicationState.AppStateEventTabletModeChange) {
const tableData = data as TabletModeChangeData;
// If we are now in tablet mode after not being in tablet mode
if (data.new_isInTabletMode && !data.old_isInTabletMode) {
if (tableData.new_isInTabletMode && !tableData.old_isInTabletMode) {
// Pop to root, if we are in Compose window.
this.props.navigation.popToTop();
setTimeout(() => {
@@ -240,7 +285,7 @@ export default class Notes extends Abstract {
}, 10);
}
if (!data.new_isInTabletMode) {
if (!tableData.new_isInTabletMode) {
this.setState({ selectedNoteId: null });
this.props.navigation.setParams({
rightButton: null,
@@ -260,7 +305,7 @@ export default class Notes extends Abstract {
/* If there is at least one item that has an error decrypting, and there are no account keys saved,
display an alert instructing the user to log in. This happens when restoring from iCloud and data is restored but keys are not.
*/
displayNeedSignInAlertForLocalItemsIfApplicable(items) {
displayNeedSignInAlertForLocalItemsIfApplicable(items: any) {
if (KeysManager.get().shouldPresentKeyRecoveryWizard()) {
this.props.navigation.navigate(SCREEN_KEY_RECOVERY);
return;
@@ -317,7 +362,9 @@ export default class Notes extends Abstract {
});
}
async presentComposer(note) {
async presentComposer(
note?: { content?: { protected: any }; uuid: any } | null
) {
this.props.navigation.navigate(SCREEN_COMPOSE, {
title: note ? 'Note' : 'Compose',
noteId: note && note.uuid,
@@ -326,7 +373,7 @@ export default class Notes extends Abstract {
});
}
reloadList(force) {
reloadList(force?: boolean) {
if (!this.visible && !this.willBeVisible && !force) {
console.log('===Scheduling Notes Render Update===');
this.loadNotesOnVisible = true;
@@ -338,7 +385,7 @@ export default class Notes extends Abstract {
const result = ModelManager.get().getNotes(this.options);
const { notes, tags } = result;
this.setState({ notes: notes, tags: tags, refreshing: false });
this.setState({ notes, tags: tags, refreshing: false });
// setState is async, but we need this value right away sometimes to select the first note of new set of notes
this.stateNotes = notes;
@@ -367,7 +414,7 @@ export default class Notes extends Abstract {
});
}
selectNote = note => {
selectNote = (note?: { content: { protected: any }; uuid: any } | null) => {
this.handlePrivilegedAction(
note && note.content.protected,
SFPrivilegesManager.ActionViewProtectedNotes,
@@ -383,9 +430,16 @@ export default class Notes extends Abstract {
);
};
_onPressItem = (item: hash) => {
_onPressItem = (
item: {
content: { protected: any; conflict_of: any };
setDirty: (arg0: boolean) => void;
uuid: any;
errorDecrypting: any;
} | null
) => {
const run = () => {
if (item.content.conflict_of) {
if (item && item.content.conflict_of) {
item.content.conflict_of = null;
item.setDirty(true);
Sync.get().sync();
@@ -394,7 +448,7 @@ export default class Notes extends Abstract {
this.selectNote(item);
};
if (item.errorDecrypting) {
if (item && item.errorDecrypting) {
this.props.navigation.navigate(SCREEN_SETTINGS);
} else {
run();
@@ -408,7 +462,7 @@ export default class Notes extends Abstract {
}
};
onSearchTextChange = text => {
onSearchTextChange = (text: string | null) => {
this.searching = true;
this.options.setSearchTerm(text);
};
@@ -418,7 +472,18 @@ export default class Notes extends Abstract {
this.options.setSearchTerm(null);
};
handleActionsheetAction = (item, action, callback) => {
handleActionsheetAction = (
item: {
displayName: string;
content: { trashed: boolean; protected: boolean };
setDirty: (arg0: boolean) => void;
setAppDataItem: (arg0: string, arg1: boolean) => void;
title: any;
text: any;
},
action: EventType,
callback: () => void
) => {
const run = () => {
ItemActionManager.handleEvent(
action,
@@ -479,17 +544,20 @@ export default class Notes extends Abstract {
selectedTags={this.state.tags}
options={this.options.displayOptions}
selectedNoteId={
ApplicationState.get().isInTabletMode && this.state.selectedNoteId
ApplicationState.get().isInTabletMode
? this.state.selectedNoteId
: null
}
handleAction={this.handleActionsheetAction}
/>
)}
<FAB
// @ts-ignore style prop does not exist for types
style={
ApplicationState.get().isInTabletMode
? { bottom: ApplicationState.get().getKeyboardHeight() }
: null
: undefined
}
buttonColor={StyleKit.variables.stylekitInfoColor}
iconTextColor={StyleKit.variables.stylekitInfoContrastColor}

View File

@@ -1,5 +1,12 @@
import React from 'react';
import { StyleSheet, View, Text, TouchableWithoutFeedback } from 'react-native';
import {
StyleSheet,
View,
Text,
TouchableWithoutFeedback,
ViewStyle,
TextStyle,
} from 'react-native';
import { withNavigation } from 'react-navigation';
import Icon from 'react-native-vector-icons/Ionicons';
import ThemedPureComponent from '@Components/ThemedPureComponent';
@@ -10,7 +17,12 @@ import StyleKit from '@Style/StyleKit';
const NOT_BACKED_UP_TEXT = 'Data not backed up';
const SIGN_IN_TEXT = 'Sign in or register to backup your notes';
export class OfflineBanner extends ThemedPureComponent {
type Props = {
navigation: any;
};
export class OfflineBanner extends ThemedPureComponent<Props> {
styles!: Record<string, ViewStyle | TextStyle>;
_onPress = () => {
this.props.navigation.navigate(SCREEN_SETTINGS);
};

View File

@@ -1,10 +1,16 @@
import React from 'react';
import { TouchableHighlight, View } from 'react-native';
import { TouchableHighlight, View, ViewStyle, TextStyle } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { SFAuthManager } from 'snjs';
import ApplicationState from '@Lib/ApplicationState';
import ApplicationState, {
AppStateEventHandler,
AppStateEventType,
TabletModeChangeData,
NoteSideMenuToggleChange,
AppStateType,
} from '@Lib/ApplicationState';
import KeysManager from '@Lib/keysManager';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
import Compose from '@Screens/Compose';
import Notes from '@Screens/Notes/Notes';
import { SCREEN_AUTHENTICATE } from '@Screens/screens';
@@ -14,9 +20,40 @@ import ModelManager from '@Lib/snjs/modelManager';
import Sync from '@Lib/snjs/syncManager';
import StyleKit from '@Style/StyleKit';
import { hexToRGBA } from '@Style/utils';
import AuthenticationSource from './Authentication/Sources/AuthenticationSource';
export default class Root extends Abstract {
constructor(props) {
type State = {
shouldSplitLayout?: boolean;
notesListCollapsed?: boolean;
keyboardHeight?: number;
selectedTagId?: string;
width?: number;
height?: number;
x?: number;
y?: number;
} & AbstractState;
export default class Root extends Abstract<AbstractProps, State> {
styles!: Record<string, ViewStyle | TextStyle>;
applicationStateEventHandler?: AppStateEventHandler;
stateObserver?: {
key: () => number;
callback: (state: AppStateType) => void;
};
dataLoaded?: boolean;
syncEventHandler?: any;
didShowSessionInvalidAlert?: boolean;
syncStatusObserver?: any;
showingErrorStatus?: boolean;
showingDownloadStatus?: boolean;
signoutObserver?: any;
authOnMount: any;
authenticationInProgress: any;
pendingAuthProps: any;
syncTimer?: number;
notesRef: any;
composeRef: any;
constructor(props: Readonly<AbstractProps>) {
super(props);
this.registerObservers();
}
@@ -37,11 +74,15 @@ export default class Root extends Abstract {
});
this.applicationStateEventHandler = ApplicationState.get().addEventHandler(
(event, data) => {
(
event: AppStateEventType,
data: TabletModeChangeData | NoteSideMenuToggleChange | undefined
) => {
if (event === ApplicationState.AppStateEventNoteSideMenuToggle) {
// update state to toggle Notes side menu if we triggered the collapse
this.setState({
notesListCollapsed: data.new_isNoteSideMenuCollapsed,
notesListCollapsed: (data as NoteSideMenuToggleChange)
.new_isNoteSideMenuCollapsed,
});
} else if (event === ApplicationState.KeyboardChangeEvent) {
// need to refresh the height of the keyboard when it opens so that we can change the position
@@ -55,52 +96,56 @@ export default class Root extends Abstract {
}
);
this.syncEventHandler = Sync.get().addEventHandler(async (event, data) => {
if (event === 'sync-session-invalid') {
if (!this.didShowSessionInvalidAlert) {
this.didShowSessionInvalidAlert = true;
AlertManager.get().confirm({
title: 'Session Expired',
text:
'Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session.',
confirmButtonText: 'Sign Out',
onConfirm: () => {
this.didShowSessionInvalidAlert = false;
Auth.get().signout();
},
onCancel: () => {
this.didShowSessionInvalidAlert = false;
},
});
this.syncEventHandler = Sync.get().addEventHandler(
async (event: string) => {
if (event === 'sync-session-invalid') {
if (!this.didShowSessionInvalidAlert) {
this.didShowSessionInvalidAlert = true;
AlertManager.get().confirm({
title: 'Session Expired',
text:
'Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session.',
confirmButtonText: 'Sign Out',
onConfirm: () => {
this.didShowSessionInvalidAlert = false;
Auth.get().signout();
},
onCancel: () => {
this.didShowSessionInvalidAlert = false;
},
});
}
}
}
});
);
this.syncStatusObserver = Sync.get().registerSyncStatusObserver(status => {
if (status.error) {
const text = 'Unable to connect to sync server.';
this.showingErrorStatus = true;
setTimeout(() => {
// need timeout for syncing on app launch
this.setSubTitle(text, StyleKit.variables.stylekitWarningColor);
}, 250);
} else if (status.retrievedCount > 20) {
const text = `Downloading ${status.retrievedCount} items. Keep app open.`;
this.setSubTitle(text);
this.showingDownloadStatus = true;
} else if (this.showingDownloadStatus) {
this.showingDownloadStatus = false;
const text = 'Download Complete.';
this.setSubTitle(text);
setTimeout(() => {
this.syncStatusObserver = Sync.get().registerSyncStatusObserver(
(status: { error: any; retrievedCount: number }) => {
if (status.error) {
const text = 'Unable to connect to sync server.';
this.showingErrorStatus = true;
setTimeout(() => {
// need timeout for syncing on app launch
this.setSubTitle(text, StyleKit.variables.stylekitWarningColor);
}, 250);
} else if (status.retrievedCount > 20) {
const text = `Downloading ${status.retrievedCount} items. Keep app open.`;
this.setSubTitle(text);
this.showingDownloadStatus = true;
} else if (this.showingDownloadStatus) {
this.showingDownloadStatus = false;
const text = 'Download Complete.';
this.setSubTitle(text);
setTimeout(() => {
this.setSubTitle(null);
}, 2000);
} else if (this.showingErrorStatus) {
this.setSubTitle(null);
}, 2000);
} else if (this.showingErrorStatus) {
this.setSubTitle(null);
}
}
});
);
this.signoutObserver = Auth.get().addEventHandler(async event => {
this.signoutObserver = Auth.get().addEventHandler(async (event: any) => {
if (event === SFAuthManager.DidSignOutEvent) {
this.setSubTitle(null);
const notifyObservers = false;
@@ -145,13 +190,20 @@ export default class Root extends Abstract {
componentWillUnmount() {
super.componentWillUnmount();
ApplicationState.get().removeStateObserver(this.stateObserver);
ApplicationState.get().removeEventHandler(
this.applicationStateEventHandler
);
if (this.stateObserver) {
ApplicationState.get().removeStateObserver(this.stateObserver);
}
if (this.applicationStateEventHandler) {
ApplicationState.get().removeEventHandler(
this.applicationStateEventHandler
);
}
Sync.get().removeEventHandler(this.syncEventHandler);
Sync.get().removeSyncStatusObserver(this.syncStatusObserver);
clearInterval(this.syncTimer);
if (this.syncTimer !== undefined) {
clearInterval(this.syncTimer);
}
}
/* Forward React Navigation lifecycle events to notes */
@@ -198,7 +250,7 @@ export default class Root extends Abstract {
this.setSubTitle(
encryptionEnabled ? 'Decrypting items...' : 'Loading items...'
);
const incrementalCallback = (current, total) => {
const incrementalCallback = (current: any, total: any) => {
const notesString = `${current}/${total} items...`;
this.setSubTitle(
encryptionEnabled
@@ -212,7 +264,7 @@ export default class Root extends Abstract {
this.notesRef && this.notesRef.root_onIncrementalSync();
};
const loadLocalCompletion = items => {
const loadLocalCompletion = () => {
this.setSubTitle('Syncing...');
this.dataLoaded = true;
@@ -231,15 +283,27 @@ export default class Root extends Abstract {
const batchSize = 100;
Sync.get()
.loadLocalItems({ incrementalCallback, batchSize })
.then(items => {
.then(() => {
setTimeout(() => {
loadLocalCompletion(items);
loadLocalCompletion();
});
});
}
}
presentAuthenticationModal(authProps) {
presentAuthenticationModal(
authProps:
| {
sources: AuthenticationSource[];
title?: undefined;
onAuthenticate?: undefined;
}
| {
title: string;
sources: AuthenticationSource[];
onAuthenticate: () => void;
}
) {
if (!this.isMounted()) {
console.log('Not yet mounted, not authing.');
this.authOnMount = authProps;
@@ -270,7 +334,7 @@ export default class Root extends Abstract {
this.props.navigation.navigate(SCREEN_AUTHENTICATE, {
authenticationSources: authProps.sources,
onSuccess: () => {
authProps.onAuthenticate();
authProps.onAuthenticate && authProps.onAuthenticate();
this.pendingAuthProps = null;
this.authenticationInProgress = false;
if (this.dataLoaded) {
@@ -283,7 +347,7 @@ export default class Root extends Abstract {
});
}
onNoteSelect = note => {
onNoteSelect = (note: any) => {
this.composeRef.setNote(note);
this.setState({
selectedTagId:
@@ -292,7 +356,9 @@ export default class Root extends Abstract {
});
};
onLayout = e => {
onLayout = (e: {
nativeEvent: { layout: { width: any; height: any; x: any; y: any } };
}) => {
const width = e.nativeEvent.layout.width;
/**
If you're in tablet mode, but on an iPad where this app is running side by
@@ -351,7 +417,7 @@ export default class Root extends Abstract {
'-' +
iconNames[collapseIconPrefix][notesListCollapsed ? 0 : 1];
const collapseIconBottomPosition =
this.state.keyboardHeight > this.state.height / 2
(this.state.keyboardHeight ?? 0) > (this.state.height ?? 0) / 2
? this.state.keyboardHeight
: '50%';
@@ -369,7 +435,9 @@ export default class Root extends Abstract {
onUnlockPress={this.onUnlockPress}
navigation={this.props.navigation}
onNoteSelect={
shouldSplitLayout && this.onNoteSelect /* tablet only */
shouldSplitLayout
? this.onNoteSelect
: undefined /* tablet only */
}
/>
</View>

View File

@@ -12,10 +12,33 @@ import StyleKit from '@Style/StyleKit';
const DEFAULT_SIGN_IN_TEXT = 'Sign In';
const DEFAULT_REGISTER_TEXT = 'Register';
export default class AuthSection extends Component {
constructor(props) {
type Props = {
onAuthSuccess: () => void;
title: string;
};
type State = {
server?: string;
showAdvanced: boolean;
signingIn: boolean;
signInButtonText: string;
registerButtonText: string;
password: string | null;
email: string | null;
registering: boolean;
strictSignIn: boolean;
mfa?: any;
mfa_token?: string;
confirmRegistration?: boolean;
passwordConfirmation?: string;
};
export default class AuthSection extends Component<Props, State> {
static emailInProgress: any;
static passwordInProgress: any;
constructor(props: Readonly<Props>) {
super(props);
this.state = {
showAdvanced: false,
email: AuthSection.emailInProgress,
password: AuthSection.passwordInProgress,
signingIn: false,
@@ -60,9 +83,9 @@ export default class AuthSection extends Component {
return;
}
const extraParams = {};
const extraParams: Record<string, string | undefined> = {};
if (this.state.mfa) {
extraParams[this.state.mfa.payload.mfa_key] = this.state.mfa_token;
extraParams[this.state.mfa?.payload.mfa_key] = this.state.mfa_token;
}
const strict = this.state.strictSignIn;
@@ -79,7 +102,7 @@ export default class AuthSection extends Component {
Auth.get()
.login(this.state.server, email, password, strict, extraParams)
.then(response => {
.then((response: { error: any }) => {
if (!response || response.error) {
const error = response
? response.error
@@ -105,7 +128,7 @@ export default class AuthSection extends Component {
});
};
validate(email, password) {
validate(email: string | null, password: string | null) {
if (!email) {
Alert.alert('Missing Email', 'Please enter a valid email address.', [
{ text: 'OK' },
@@ -174,7 +197,7 @@ export default class AuthSection extends Component {
Auth.get()
.register(this.state.server, this.state.email, this.state.password)
.then(response => {
.then((response: { error: any }) => {
this.setState({ registering: false, confirmRegistration: false });
if (!response || response.error) {
@@ -194,7 +217,7 @@ export default class AuthSection extends Component {
this.setState({ mfa: null });
};
emailInputChanged = text => {
emailInputChanged = (text: string) => {
this.setState({ email: text });
/**
* If you have a local passcode with immediate timing, and you're trying to
@@ -205,7 +228,7 @@ export default class AuthSection extends Component {
AuthSection.emailInProgress = text;
};
passwordInputChanged = text => {
passwordInputChanged = (text: string) => {
this.setState({ password: text });
AuthSection.passwordInProgress = text;
};
@@ -306,7 +329,7 @@ export default class AuthSection extends Component {
style={StyleKit.styles.sectionedTableCellTextInput}
placeholder={'Email'}
onChangeText={text => this.emailInputChanged(text)}
value={this.state.email}
value={this.state.email ?? undefined}
autoCorrect={false}
autoCapitalize={'none'}
keyboardType={'email-address'}
@@ -323,7 +346,7 @@ export default class AuthSection extends Component {
style={StyleKit.styles.sectionedTableCellTextInput}
placeholder={'Password'}
onChangeText={text => this.passwordInputChanged(text)}
value={this.state.password}
value={this.state.password ?? undefined}
textContentType={'password'}
secureTextEntry={true}
keyboardAppearance={StyleKit.get().keyboardColorForActiveTheme()}

View File

@@ -6,8 +6,12 @@ import TableSection from '@Components/TableSection';
import ApplicationState from '@Lib/ApplicationState';
import StyleKit from '@Style/StyleKit';
export default class CompanySection extends Component {
onAction = action => {
type Props = {
title: string;
};
export default class CompanySection extends Component<Props> {
onAction = (action: string) => {
if (action === 'feedback') {
const platformString = Platform.OS === 'android' ? 'Android' : 'iOS';
ApplicationState.openURL(

View File

@@ -7,8 +7,16 @@ import KeysManager from '@Lib/keysManager';
import ModelManager from '@Lib/snjs/modelManager';
import StyleKit from '@Style/StyleKit';
export default class PasscodeSection extends Component {
constructor(props) {
type Props = {
title: string;
};
type State = {
items: any[];
};
export default class PasscodeSection extends Component<Props, State> {
constructor(props: Readonly<Props>) {
super(props);
this.state = {
@@ -42,7 +50,7 @@ export default class PasscodeSection extends Component {
const titleStyles = {
color: StyleKit.variables.stylekitForegroundColor,
fontSize: 16,
fontWeight: 'bold',
fontWeight: 'bold' as 'bold',
};
const subtitleStyles = {

View File

@@ -11,12 +11,26 @@ import KeysManager from '@Lib/keysManager';
import moment from '@Lib/moment';
import UserPrefsManager, { LAST_EXPORT_DATE_KEY } from '@Lib/userPrefsManager';
import Auth from '@Lib/snjs/authManager';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
import { SFPrivilegesManager } from 'snjs';
class OptionsSection extends Abstract {
constructor(props) {
type Props = {
title: string;
onSignOutPress: () => Promise<any>;
onManagePrivileges: () => void;
} & AbstractProps;
type State = {
loadingExport: boolean;
encryptionAvailable: boolean;
email?: string | null;
signedIn: boolean;
lastExportDate?: Date;
} & AbstractState;
class OptionsSection extends Abstract<Props, State> {
constructor(props: Readonly<Props>) {
super(props);
let encryptionAvailable = KeysManager.get().activeKeys() != null;
let email = KeysManager.get().getUserEmail();
@@ -38,7 +52,7 @@ class OptionsSection extends Abstract {
});
}
onExportPress = option => {
onExportPress = (option: { key: string }) => {
let encrypted = option.key === 'encrypted';
if (encrypted && !this.state.encryptionAvailable) {
Alert.alert(
@@ -68,7 +82,7 @@ class OptionsSection extends Abstract {
}
this.setState({ loadingExport: false });
})
.catch(error => {
.catch(() => {
this.setState({ loadingExport: false });
});
},
@@ -104,13 +118,15 @@ class OptionsSection extends Abstract {
};
render() {
let lastExportString, stale;
let lastExportString;
let stale: boolean = false;
if (this.state.lastExportDate) {
let formattedDate = moment(this.state.lastExportDate).format('lll');
lastExportString = `Last exported on ${formattedDate}`;
// Date is stale if more than 7 days ago
let staleThreshold = 7 * 86400;
// @ts-ignore date type issue
stale = (new Date() - this.state.lastExportDate) / 1000 > staleThreshold;
} else {
lastExportString = 'Your data has not yet been backed up.';
@@ -138,13 +154,13 @@ class OptionsSection extends Abstract {
onPress={this.onSignOutPress}
/>
)}
{/* some types don't exist on component */}
<SectionedOptionsTableCell
testID="exportData"
last={!hasLastExportSection}
// last={!hasLastExportSection}
first={false}
disabled={this.state.loadingExport}
leftAligned={true}
// disabled={this.state.loadingExport}
// leftAligned={true}
options={this.exportOptions()}
title={this.state.loadingExport ? 'Processing...' : 'Export Data'}
onPress={this.onExportPress}

View File

@@ -5,13 +5,33 @@ import SectionHeader from '@Components/SectionHeader';
import SectionedOptionsTableCell from '@Components/SectionedOptionsTableCell';
import TableSection from '@Components/TableSection';
import ApplicationState from '@Lib/ApplicationState';
import KeysManager from '@Lib/keysManager';
import KeysManager, { BiometricsType } from '@Lib/keysManager';
import StyleKit from '@Style/StyleKit';
export default class PasscodeSection extends Component {
constructor(props) {
type Props = {
hasBiometrics: boolean;
hasPasscode: boolean;
title: string;
onDisable: () => void;
onEnable: () => void;
storageEncryption: boolean | null;
onStorageEncryptionDisable: () => void;
onStorageEncryptionEnable: () => void;
storageEncryptionLoading: boolean;
onFingerprintDisable: () => void;
onFingerprintEnable: () => void;
};
type State = {
biometricsAvailable?: boolean;
biometricsType?: BiometricsType;
biometricsNoun?: string;
};
export default class PasscodeSection extends Component<Props, State> {
constructor(props: Readonly<Props>) {
super(props);
let state = { biometricsAvailable: false || __DEV__ };
let state: State = { biometricsAvailable: false || __DEV__ };
if (__DEV__) {
state.biometricsType = ApplicationState.isAndroid
? 'Fingerprint'
@@ -34,12 +54,12 @@ export default class PasscodeSection extends Component {
});
}
onPasscodeOptionPress = option => {
onPasscodeOptionPress = (option: { key: string | null }) => {
KeysManager.get().setPasscodeTiming(option.key);
this.forceUpdate();
};
onBiometricsOptionPress = option => {
onBiometricsOptionPress = (option: { key: any }) => {
KeysManager.get().setBiometricsTiming(option.key);
this.forceUpdate();
};
@@ -53,7 +73,7 @@ export default class PasscodeSection extends Component {
? 'Disable Storage Encryption'
: 'Enable Storage Encryption'
: 'Storage Encryption';
let storageOnPress = this.props.storageEncryption
let storageOnPress: (() => void) | null = this.props.storageEncryption
? this.props.onStorageEncryptionDisable
: this.props.onStorageEncryptionEnable;
let storageSubText =
@@ -109,7 +129,7 @@ export default class PasscodeSection extends Component {
first={true}
leftAligned={true}
title={storageEncryptionTitle}
onPress={storageOnPress}
onPress={storageOnPress !== null ? storageOnPress : () => undefined}
>
<Text
style={{
@@ -137,7 +157,6 @@ export default class PasscodeSection extends Component {
{this.props.hasPasscode && (
<SectionedOptionsTableCell
last={!this.props.hasBiometrics}
title={'Require Passcode'}
options={passcodeOptions}
onPress={this.onPasscodeOptionPress}
@@ -146,7 +165,6 @@ export default class PasscodeSection extends Component {
{this.props.hasBiometrics && (
<SectionedOptionsTableCell
last={true}
title={`Require ${biometricsNoun}`}
options={biometricOptions}
onPress={this.onBiometricsOptionPress}

View File

@@ -12,7 +12,7 @@ import AlertManager from '@Lib/snjs/alertManager';
import Auth from '@Lib/snjs/authManager';
import Storage from '@Lib/snjs/storageManager';
import Sync from '@Lib/snjs/syncManager';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract';
import { SCREEN_INPUT_MODAL, SCREEN_MANAGE_PRIVILEGES } from '@Screens/screens';
import AuthSection from '@Screens/Settings/Sections/AuthSection';
import CompanySection from '@Screens/Settings/Sections/CompanySection';
@@ -23,9 +23,18 @@ import { ICON_CHECKMARK } from '@Style/icons';
import StyleKit from '@Style/StyleKit';
import { SFPrivilegesManager, protocolManager } from 'snjs';
import OptionsState from '@Lib/OptionsState';
export default class Settings extends Abstract {
static navigationOptions = ({ navigation, navigationOptions }) => {
type State = {
confirmRegistration: boolean;
hasPasscode: boolean;
storageEncryption: any;
storageEncryptionLoading: boolean;
hasBiometrics: boolean;
} & AbstractState;
export default class Settings extends Abstract<AbstractProps, State> {
static navigationOptions = ({ navigation, navigationOptions }: any) => {
const templateOptions = {
title: 'Settings',
leftButton: {
@@ -37,12 +46,15 @@ export default class Settings extends Abstract {
};
return Abstract.getDefaultNavigationOptions({
navigation,
navigationOptions,
_navigationOptions: navigationOptions,
templateOptions,
});
};
sortOptions: { key: string; label: string }[];
options: OptionsState;
syncEventHandler: any;
constructor(props) {
constructor(props: Readonly<AbstractProps>) {
super(props);
props.navigation.setParams({
@@ -108,7 +120,7 @@ export default class Settings extends Abstract {
this.forceUpdate();
}
resaveOfflineData(callback, updateAfter = false) {
resaveOfflineData(callback: { (): void } | null, updateAfter = false) {
Sync.get()
.resaveOfflineData()
.then(() => {
@@ -181,14 +193,14 @@ export default class Settings extends Abstract {
secureTextEntry: true,
requireConfirm: true,
showKeyboardChooser: true,
onSubmit: async (value, keyboardType) => {
onSubmit: async (value: any, keyboardType: string | null | undefined) => {
Storage.get().setItem('passcodeKeyboardType', keyboardType);
let identifier = await protocolManager.crypto.generateUUID();
protocolManager
.generateInitialKeysAndAuthParamsForUser(identifier, value)
.then(results => {
.then((results: { keys: any; authParams: any }) => {
let keys = results.keys;
let authParams = results.authParams;
@@ -262,12 +274,12 @@ export default class Settings extends Abstract {
);
};
onSortChange = key => {
onSortChange = (key: string) => {
this.options.setSortBy(key);
this.forceUpdate();
};
onOptionSelect = option => {
onOptionSelect = (option: string) => {
this.options.setDisplayOptionKeyValue(
option,
!this.options.getDisplayOptionValue(option)

View File

@@ -1,8 +1,12 @@
import { Keyboard } from 'react-native';
import Abstract from '@Screens/Abstract';
import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract';
export default class AbstractSideMenu extends Abstract {
shouldComponentUpdate(nextProps, nextState) {
export default class AbstractSideMenu<
AdditionalProps extends AbstractProps,
AdditionalState extends AbstractState
> extends Abstract<AdditionalProps, AdditionalState> {
handler: any;
shouldComponentUpdate(nextProps: Readonly<AbstractProps & AdditionalProps>) {
/*
We had some performance issues with this component rendering too many times when
navigating to unrelated routes, like pushing Compose. It would render 6 times or so,

View File

@@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import { View, FlatList } from 'react-native';
import { View, FlatList, ViewStyle, TextStyle } from 'react-native';
import FAB from 'react-native-fab';
import Icon from 'react-native-vector-icons/Ionicons';
import _ from 'lodash';
@@ -21,21 +21,37 @@ import StyleKit from '@Style/StyleKit';
import ThemeManager from '@Style/ThemeManager';
import { LIGHT_MODE_KEY, DARK_MODE_KEY } from '@Style/utils';
import { SFAuthManager } from 'snjs';
import { SFAuthManager, SNTheme as SNJSTheme } from 'snjs';
import { Mode } from 'react-native-dark-mode';
import { AbstractProps, AbstractState } from '@Screens/Abstract';
export default class MainSideMenu extends AbstractSideMenu {
constructor(props) {
type SNTheme = typeof SNJSTheme;
type State = {
outOfSync: boolean;
actionSheet: JSX.Element | null;
} & AbstractState;
export default class MainSideMenu extends AbstractSideMenu<
AbstractProps,
State
> {
styles!: Record<string, ViewStyle | TextStyle>;
signoutObserver: any;
syncEventHandler: any;
constructor(props: Readonly<{ navigation: any }>) {
super(props);
this.constructState({});
this.signoutObserver = Auth.get().addEventHandler(event => {
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
if (event === SFAuthManager.DidSignOutEvent) {
this.setState({ outOfSync: false });
this.forceUpdate();
}
});
this.syncEventHandler = Sync.get().addEventHandler((event, data) => {
this.syncEventHandler = Sync.get().addEventHandler((event: string) => {
if (event === 'enter-out-of-sync') {
this.setState({ outOfSync: true });
} else if (event === 'exit-out-of-sync') {
@@ -70,12 +86,12 @@ export default class MainSideMenu extends AbstractSideMenu {
return SideMenuManager.get().getHandlerForLeftSideMenu();
}
onTagSelect = tag => {
this.handler.onTagSelect(tag);
onTagSelect = (tag: any) => {
this.handler?.onTagSelect(tag);
this.forceUpdate();
};
onThemeSelect = theme => {
onThemeSelect = (theme: SNTheme) => {
/** Prevent themes that aren't meant for mobile from being activated. */
if (theme.content.package_info && theme.content.package_info.no_mobile) {
AlertManager.get().alert({
@@ -94,7 +110,7 @@ export default class MainSideMenu extends AbstractSideMenu {
this.forceUpdate();
};
onThemeLongPress = theme => {
onThemeLongPress = (theme: SNTheme) => {
const actionSheetOptions = [];
/**
@@ -169,7 +185,7 @@ export default class MainSideMenu extends AbstractSideMenu {
sheet.show();
};
onThemeRedownload(theme) {
onThemeRedownload(theme: SNTheme) {
AlertManager.get().confirm({
title: 'Redownload Theme',
text:
@@ -181,7 +197,7 @@ export default class MainSideMenu extends AbstractSideMenu {
});
}
getModeActionForTheme({ theme, mode }) {
getModeActionForTheme({ theme, mode }: { theme: SNTheme; mode: Mode }) {
return ThemeManager.get().isThemeEnabledForMode({
mode: mode,
theme: theme,
@@ -190,10 +206,10 @@ export default class MainSideMenu extends AbstractSideMenu {
: 'Set as';
}
iconDescriptorForTheme = theme => {
iconDescriptorForTheme = (theme: SNTheme) => {
const desc = {
type: 'circle',
side: 'right',
side: 'right' as 'right',
};
const dockIcon =

View File

@@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import { View, FlatList } from 'react-native';
import { View, FlatList, ViewStyle, TextStyle } from 'react-native';
import FAB from 'react-native-fab';
import { SFPrivilegesManager, SNTag } from 'snjs';
import Icon from 'react-native-vector-icons/Ionicons';
@@ -7,16 +7,19 @@ import { SafeAreaView } from 'react-navigation';
import LockedView from '@Containers/LockedView';
import ApplicationState from '@Lib/ApplicationState';
import ComponentManager from '@Lib/componentManager';
import ItemActionManager from '@Lib/itemActionManager';
import ItemActionManager, { EventType } from '@Lib/itemActionManager';
import { SCREEN_INPUT_MODAL, SCREEN_MANAGE_PRIVILEGES } from '@Screens/screens';
import ModelManager from '@Lib/snjs/modelManager';
import PrivilegesManager from '@Lib/snjs/privilegesManager';
import Sync from '@Lib/snjs/syncManager';
import AbstractSideMenu from '@Screens/SideMenu/AbstractSideMenu';
import SideMenuManager from '@Screens/SideMenu/SideMenuManager';
import SideMenuSection from '@Screens/SideMenu/SideMenuSection';
import SideMenuSection, {
SideMenuOption,
} from '@Screens/SideMenu/SideMenuSection';
import TagSelectionList from '@Screens/SideMenu/TagSelectionList';
import ActionSheetWrapper from '@Style/ActionSheetWrapper';
import type { AbstractProps } from '@Screens/Abstract';
import {
ICON_BOOKMARK,
ICON_ARCHIVE,
@@ -29,8 +32,18 @@ import {
} from '@Style/icons';
import StyleKit from '@Style/StyleKit';
export default class NoteSideMenu extends AbstractSideMenu {
constructor(props) {
type State = {
lockContent: boolean;
outOfSync: boolean;
actionSheet: JSX.Element | null;
};
export default class NoteSideMenu extends AbstractSideMenu<
AbstractProps,
State
> {
styles!: Record<string, ViewStyle | TextStyle>;
constructor(props: Readonly<AbstractProps>) {
super(props);
this.constructState({});
}
@@ -39,21 +52,27 @@ export default class NoteSideMenu extends AbstractSideMenu {
return SideMenuManager.get().getHandlerForRightSideMenu();
}
onEditorSelect = editor => {
this.handler.onEditorSelect(editor);
onEditorSelect = (editor: any) => {
this.handler?.onEditorSelect(editor);
this.forceUpdate();
};
onTagSelect = tag => {
this.handler.onTagSelect(tag);
onTagSelect = (tag: any) => {
this.handler?.onTagSelect(tag);
this.forceUpdate();
};
get note() {
return this.handler.getCurrentNote();
return this.handler?.getCurrentNote();
}
onEditorLongPress = editor => {
onEditorLongPress = (
editor: {
content: any;
name?: any;
setDirty?: (arg0: boolean) => void;
} | null
) => {
const currentDefaultEditor = ComponentManager.get().getDefaultEditor();
let isDefault = false;
@@ -109,8 +128,8 @@ export default class NoteSideMenu extends AbstractSideMenu {
this.props.navigation.navigate(SCREEN_INPUT_MODAL, {
title: 'New Tag',
placeholder: 'New tag name',
onSubmit: text => {
this.createTag(text, tag => {
onSubmit: (text: string) => {
this.createTag(text, (tag: any) => {
if (this.note) {
// select this tag
this.onTagSelect(tag);
@@ -120,7 +139,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
});
};
createTag(text, callback) {
createTag(text: string, callback: (tag: any) => void) {
const tag = new SNTag({ content: { title: text } });
tag.initUUID().then(() => {
tag.setDirty(true);
@@ -135,7 +154,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
Render
*/
runAction(action) {
runAction(action: EventType) {
let run = () => {
ItemActionManager.handleEvent(action, this.note, async () => {
if (
@@ -147,7 +166,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
this.popToRoot();
} else {
this.forceUpdate();
this.handler.onPropertyChange();
this.handler?.onPropertyChange();
if (action === ItemActionManager.ProtectEvent) {
// Show Privileges management screen if protected notes privs are not set up yet
@@ -208,7 +227,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
{ text: lockOption, key: lockEvent, icon: ICON_LOCK },
{ text: protectOption, key: protectEvent, icon: ICON_FINGER_PRINT },
{ text: 'Share', key: ItemActionManager.ShareEvent, icon: ICON_SHARE },
];
] as { text: string; key: EventType; icon: string }[];
if (!this.note.content.trashed) {
rawOptions.push({
@@ -218,7 +237,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
});
}
let options = [];
let options: SideMenuOption[] = [];
for (const rawOption of rawOptions) {
let option = SideMenuSection.BuildOption({
text: rawOption.text,
@@ -246,7 +265,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
},
{
text: 'Delete Permanently',
textClass: 'danger',
textClass: 'danger' as 'danger',
key: 'delete-forever',
onSelect: () => {
this.runAction(ItemActionManager.DeleteEvent);
@@ -254,7 +273,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
},
{
text: 'Empty Trash',
textClass: 'danger',
textClass: 'danger' as 'danger',
key: 'empty trash',
onSelect: () => {
this.runAction(ItemActionManager.EmptyTrashEvent);
@@ -269,7 +288,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
buildOptionsForEditors() {
const editors = ComponentManager.get()
.getEditors()
.sort((a, b) => {
.sort((a: { name: string }, b: { name: string }) => {
if (!a.name || !b.name) {
return -1;
}
@@ -277,7 +296,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
const selectedEditor = ComponentManager.get().editorForNote(this.note);
const options = [
const options: SideMenuOption[] = [
{
text: 'Plain Editor',
key: 'plain-editor',
@@ -294,7 +313,7 @@ export default class NoteSideMenu extends AbstractSideMenu {
for (const editor of editors) {
const option = SideMenuSection.BuildOption({
text: editor.name,
subtext: editor.content.isMobileDefault ? 'Mobile Default' : null,
subtext: editor.content.isMobileDefault ? 'Mobile Default' : undefined,
key: editor.uuid || editor.name,
selected: editor === selectedEditor,
onSelect: () => {

View File

@@ -1,14 +1,23 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import {
View,
Text,
TouchableOpacity,
ViewStyle,
TextStyle,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import _ from 'lodash';
import Circle from '@Components/Circle';
import ThemedComponent from '@Components/ThemedComponent';
import ApplicationState from '@Lib/ApplicationState';
import StyleKit from '@Style/StyleKit';
import { hexToRGBA } from '@Style/utils';
import { SideMenuOption } from './SideMenuSection';
export default class SideMenuCell extends ThemedComponent {
type Props = SideMenuOption;
export default class SideMenuCell extends ThemedComponent<Props> {
styles!: Record<string, ViewStyle | TextStyle>;
componentDidUpdate() {
this.updateStyles();
}
@@ -24,7 +33,7 @@ export default class SideMenuCell extends ThemedComponent {
}
onPress = () => {
this.props.onSelect();
this.props.onSelect && this.props.onSelect();
};
onLongPress = () => {
@@ -37,13 +46,13 @@ export default class SideMenuCell extends ThemedComponent {
return null;
}
if (desc.type === 'icon') {
if (desc.type === 'icon' && desc.name) {
return (
<View style={this.styles.iconGraphic}>
<Icon
name={desc.name}
size={desc.size || 20}
color={this.styles.iconColor}
color={StyleKit.variables.stylekitInfoColor}
/>
</View>
);
@@ -63,7 +72,7 @@ export default class SideMenuCell extends ThemedComponent {
}
}
aggregateStyles(base, addition, condition) {
aggregateStyles(base: ViewStyle, addition: ViewStyle, condition?: boolean) {
if (condition) {
return [base, addition];
} else {
@@ -71,7 +80,7 @@ export default class SideMenuCell extends ThemedComponent {
}
}
colorForTextClass = textClass => {
colorForTextClass = (textClass: Props['textClass']) => {
if (!textClass) {
return null;
}
@@ -86,7 +95,7 @@ export default class SideMenuCell extends ThemedComponent {
render() {
const hasIcon = this.props.iconDesc;
const iconSide =
hasIcon && this.props.iconDesc.side
hasIcon && this.props.iconDesc?.side
? this.props.iconDesc.side
: hasIcon
? 'left'
@@ -172,9 +181,6 @@ export default class SideMenuCell extends ThemedComponent {
loadStyles() {
this.styles = {
iconColor: StyleKit.variables.stylekitInfoColor,
selectionBgColor: hexToRGBA(StyleKit.variables.stylekitInfoColor, 0.1),
cell: {
minHeight: this.props.subtext ? 52 : 42,
},
@@ -217,7 +223,7 @@ export default class SideMenuCell extends ThemedComponent {
fontWeight: 'bold',
fontSize: 15,
paddingBottom: 0,
fontFamily: ApplicationState.isAndroid ? 'Roboto' : null, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
fontFamily: ApplicationState.isAndroid ? 'Roboto' : undefined, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
},
subtext: {
@@ -226,7 +232,7 @@ export default class SideMenuCell extends ThemedComponent {
fontSize: 12,
marginTop: -5,
marginBottom: 3,
fontFamily: ApplicationState.isAndroid ? 'Roboto' : null, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
fontFamily: ApplicationState.isAndroid ? 'Roboto' : undefined, // https://github.com/facebook/react-native/issues/15114#issuecomment-364458149
},
iconGraphic: {

View File

@@ -1,5 +1,11 @@
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import {
View,
Text,
TouchableOpacity,
ViewStyle,
TextStyle,
} from 'react-native';
import Circle from '@Components/Circle';
import ThemedComponent from '@Components/ThemedComponent';
import KeysManager from '@Lib/keysManager';
@@ -7,7 +13,14 @@ import Auth from '@Lib/snjs/authManager';
import ModelManager from '@Lib/snjs/modelManager';
import StyleKit from '@Style/StyleKit';
export default class SideMenuHero extends ThemedComponent {
type Props = {
onPress: () => void;
outOfSync: boolean;
onOutOfSyncPress: () => void;
};
export default class SideMenuHero extends ThemedComponent<Props> {
styles!: Record<string, ViewStyle | TextStyle>;
getText() {
const offline = Auth.get().offline();
const hasEncryption =

View File

@@ -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;
}

View File

@@ -1,10 +1,47 @@
import React, { Fragment } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import {
View,
Text,
TouchableOpacity,
TextStyle,
ViewStyle,
} from 'react-native';
import ThemedComponent from '@Components/ThemedComponent';
import SideMenuCell from '@Screens/SideMenu/SideMenuCell';
import StyleKit from '@Style/StyleKit';
export default class SideMenuSection extends ThemedComponent {
type Props = {
title: string;
collapsed?: boolean;
options?: any[];
};
type State = {
collapsed: boolean;
};
export type SideMenuOption = {
text: string;
subtext?: string;
textClass?: 'info' | 'danger' | 'warning';
key?: string;
iconDesc?: {
type: string;
side?: 'left' | 'right';
name?: string;
value?: string;
backgroundColor?: string;
borderColor?: string;
size?: number;
};
dimmed?: boolean;
selected?: boolean;
onSelect?: () => void;
onLongPress?: () => void;
};
export default class SideMenuSection extends ThemedComponent<Props, State> {
styles!: Record<string, ViewStyle | TextStyle>;
static BuildOption({
text,
subtext,
@@ -15,7 +52,7 @@ export default class SideMenuSection extends ThemedComponent {
selected,
onSelect,
onLongPress,
}) {
}: SideMenuOption) {
return {
text,
subtext,
@@ -29,9 +66,9 @@ export default class SideMenuSection extends ThemedComponent {
};
}
constructor(props) {
constructor(props: Readonly<Props>) {
super(props);
this.state = { collapsed: props.collapsed };
this.state = { collapsed: props.collapsed ?? false };
}
toggleCollapse = () => {
@@ -51,7 +88,8 @@ export default class SideMenuSection extends ThemedComponent {
this.styles.header,
this.state.collapsed ? this.styles.collapsedHeader : null,
]}
underlayColor={StyleKit.variables.stylekitBorderColor}
// TODO: removed prop
// underlayColor={StyleKit.variables.stylekitBorderColor}
onPress={this.toggleCollapse}
>
<Fragment>

View File

@@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import { View, Text, FlatList } from 'react-native';
import { View, Text, FlatList, ViewStyle, TextStyle } from 'react-native';
import { withNavigation } from 'react-navigation';
import ThemedComponent from '@Components/ThemedComponent';
import ItemActionManager from '@Lib/itemActionManager';
@@ -13,15 +13,34 @@ import StyleKit from '@Style/StyleKit';
import { SFAuthManager } from 'snjs';
class TagSelectionList extends ThemedComponent {
type Props = {
contentType: string;
onTagSelect: (tag: string) => void;
navigation: any;
selectedTags: any[];
emptyPlaceholder?: string;
hasBottomPadding?: boolean;
};
type State = {
actionSheet: JSX.Element | null;
tags: any[];
};
class TagSelectionList extends ThemedComponent<Props, State> {
styles!: Record<string, ViewStyle | TextStyle>;
handledDataLoad: any;
signoutObserver: any;
syncObserverId: string | undefined;
syncEventHandler: any;
/*
@param props.selectedTags
@param props.onTagSelect
*/
constructor(props) {
constructor(props: Readonly<Props>) {
super(props);
this.state = { tags: [] };
this.state = { tags: [], actionSheet: null };
}
componentDidMount() {
@@ -35,7 +54,7 @@ class TagSelectionList extends ThemedComponent {
this.reload();
this.signoutObserver = Auth.get().addEventHandler(event => {
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
if (event === SFAuthManager.DidSignOutEvent) {
this.reload();
}
@@ -51,7 +70,7 @@ class TagSelectionList extends ThemedComponent {
}
);
this.syncEventHandler = Sync.get().addEventHandler((event, data) => {
this.syncEventHandler = Sync.get().addEventHandler((event: string) => {
if (event === 'local-data-loaded') {
handleInitialDataLoad();
}
@@ -79,11 +98,18 @@ class TagSelectionList extends ThemedComponent {
Tag Options
*/
onTagSelect = tag => {
onTagSelect = (tag: string) => {
this.props.onTagSelect(tag);
};
showActionSheet = tag => {
showActionSheet = (tag: {
content: any;
title: any;
setDirty: any;
displayName: string;
setAppDataItem: (arg0: string, arg1: boolean) => void;
text: string;
}) => {
if (tag.content.isSystemTag) {
return;
}
@@ -98,7 +124,7 @@ class TagSelectionList extends ThemedComponent {
title: 'Rename Tag',
placeholder: 'Tag name',
initialValue: tag.title,
onSubmit: text => {
onSubmit: (text: string) => {
if (tag) {
tag.title = text; // Update the text on the tag to the input text
tag.setDirty(true);
@@ -133,7 +159,7 @@ class TagSelectionList extends ThemedComponent {
sheet.show();
};
iconDescriptorForTag = tag => {
iconDescriptorForTag = () => {
return {
type: 'ascii',
value: '#',
@@ -141,7 +167,7 @@ class TagSelectionList extends ThemedComponent {
};
// must pass title, text, and tags as props so that it re-renders when either of those change
renderTagCell = ({ item }) => {
renderTagCell = ({ item }: any) => {
let title = item.deleted ? 'Deleting...' : item.title;
if (item.errorDecrypting) {
title = 'Unable to Decrypt';
@@ -154,7 +180,7 @@ class TagSelectionList extends ThemedComponent {
}}
onLongPress={() => this.showActionSheet(item)}
text={title}
iconDesc={this.iconDescriptorForTag(item)}
iconDesc={this.iconDescriptorForTag()}
key={item.uuid}
selected={this.props.selectedTags.includes(item)}
/>

View File

@@ -4,8 +4,27 @@ import ActionSheet from 'react-native-actionsheet';
import ApplicationState from '@Lib/ApplicationState';
import StyleKit from '@Style/StyleKit';
type Option =
| {
text: string;
key?: string;
callback: () => void;
destructive?: boolean;
}
| {
text: string;
key?: string;
callback: (option: Option) => void;
destructive?: boolean;
};
export default class ActionSheetWrapper {
static BuildOption({ text, key, callback, destructive }) {
options: Option[];
destructiveIndex?: number;
cancelIndex: number;
title: string;
actionSheet = React.createRef<ActionSheet>();
static BuildOption({ text, key, callback, destructive }: Option) {
return {
text,
key,
@@ -14,24 +33,31 @@ export default class ActionSheetWrapper {
};
}
constructor({ title, options, onCancel }) {
options.push({ text: 'Cancel', callback: onCancel });
this.options = options;
constructor(props: {
title: string;
options: Option[];
onCancel: () => void;
}) {
const cancelOption: Option[] = [
{
text: 'Cancel',
callback: props.onCancel,
key: 'CancelItem',
destructive: false,
},
];
this.options = props.options.concat(cancelOption);
this.destructiveIndex = this.options.indexOf(
this.options.find(candidate => {
return candidate.destructive;
})
);
this.destructiveIndex = this.options.findIndex(item => item.destructive);
this.cancelIndex = this.options.length - 1;
this.title = title;
this.title = props.title;
}
show() {
this.actionSheet.show();
this.actionSheet.current?.show();
}
handleActionSheetPress = index => {
handleActionSheetPress = (index: number) => {
let option = this.options[index];
option.callback && option.callback(option);
};
@@ -39,7 +65,7 @@ export default class ActionSheetWrapper {
actionSheetElement() {
return (
<ActionSheet
ref={o => (this.actionSheet = o)}
ref={this.actionSheet}
title={this.title}
options={this.options.map(option => {
return option.text;

View File

@@ -1,6 +1,6 @@
import { StatusBar, Alert, Platform } from 'react-native';
import { StatusBar, Alert, Platform, ViewStyle, TextStyle } from 'react-native';
import IconChanger from 'react-native-alternate-icons';
import { supportsDarkMode } from 'react-native-dark-mode';
import { supportsDarkMode, Mode } from 'react-native-dark-mode';
import _ from 'lodash';
import Auth from '@Lib/snjs/authManager';
import ModelManager from '@Lib/snjs/modelManager';
@@ -14,16 +14,26 @@ import {
LIGHT_MODE_KEY,
} from '@Style/utils';
import ThemeDownloader from '@Style/Util/ThemeDownloader';
import { SFAuthManager, SNTheme } from 'snjs';
import { SFAuthManager, SNTheme as SNJSTheme } from 'snjs';
import THEME_RED_JSON from './Themes/red.json';
import THEME_BLUE_JSON from './Themes/blue.json';
type SNTheme = typeof SNJSTheme;
export default class StyleKit {
static instance = null;
private static instance: StyleKit;
themeChangeObservers: Array<() => void>;
activeTheme: SNTheme;
mode?: Mode;
currentDarkMode!: Mode;
systemThemes: Array<SNTheme>;
constants: { mainTextFontSize: number; paddingLeft: number };
styles: Record<string, ViewStyle | TextStyle> = {};
signoutObserver: any;
static get() {
if (this.instance === null) {
if (!this.instance) {
this.instance = new StyleKit();
}
@@ -32,14 +42,19 @@ export default class StyleKit {
constructor() {
this.themeChangeObservers = [];
this.systemThemes = [];
this.constants = {
mainTextFontSize: 16,
paddingLeft: 14,
};
this.buildConstants();
this.buildDefaultThemes();
ModelManager.get().addItemSyncObserver(
'themes',
'SN|Theme',
(allItems, validItems, deletedItems, source) => {
(_allItems: any, _validItems: any, deletedItems: any, _source: any) => {
if (
this.activeTheme &&
!this.activeTheme.isSystemTheme &&
@@ -62,7 +77,7 @@ export default class StyleKit {
}
);
this.signoutObserver = Auth.get().addEventHandler(event => {
this.signoutObserver = Auth.get().addEventHandler((event: any) => {
if (event === SFAuthManager.DidSignOutEvent) {
this.resetToSystemTheme();
}
@@ -74,16 +89,16 @@ export default class StyleKit {
await this.resolveInitialTheme();
}
setModeTo(mode) {
setModeTo(mode: Mode) {
this.currentDarkMode = mode;
}
addThemeChangeObserver(observer) {
addThemeChangeObserver(observer: () => void) {
this.themeChangeObservers.push(observer);
return observer;
}
removeThemeChangeObserver(observer) {
removeThemeChangeObserver(observer: () => void) {
_.pull(this.themeChangeObservers, observer);
}
@@ -93,7 +108,7 @@ export default class StyleKit {
}
}
assignThemeForMode({ theme, mode }) {
assignThemeForMode({ theme, mode }: { theme: SNTheme; mode: Mode }) {
if (!StyleKit.doesDeviceSupportDarkMode()) {
mode = LIGHT_MODE_KEY;
}
@@ -138,7 +153,7 @@ export default class StyleKit {
variables.statusBar =
Platform.OS === 'android' ? LIGHT_CONTENT : DARK_CONTENT;
const theme = new SNTheme({
const theme = new SNJSTheme({
uuid: option.name,
content: {
isSystemTheme: true,
@@ -194,7 +209,7 @@ export default class StyleKit {
if (matchingTheme) {
newTheme = matchingTheme;
} else {
newTheme = new SNTheme(themeData);
newTheme = new SNJSTheme(themeData);
newTheme.isSwapIn = true;
}
@@ -210,20 +225,25 @@ export default class StyleKit {
}
themes() {
const themes = ModelManager.get().themes.sort((a, b) => {
if (!a.name || !b.name) {
return -1;
const themes = ModelManager.get().themes.sort(
(
a: { name: { toLowerCase: () => number } },
b: { name: { toLowerCase: () => number } }
) => {
if (!a.name || !b.name) {
return -1;
}
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
}
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
});
);
return this.systemThemes.concat(themes);
}
isThemeActive(theme) {
isThemeActive(theme: SNTheme) {
return this.activeTheme && theme.uuid === this.activeTheme.uuid;
}
setActiveTheme(theme) {
setActiveTheme(theme: SNTheme) {
/** Merge default variables to ensure this theme has all the variables. */
const variables = theme.content.variables;
theme.content.variables = _.merge(this.templateVariables(), variables);
@@ -244,7 +264,7 @@ export default class StyleKit {
* - Status Bar color
* - Local App Icon color
*/
updateDeviceForTheme(theme) {
updateDeviceForTheme(theme: SNTheme) {
const isAndroid = Platform.OS === 'android';
/** On Android, a time out is required, especially during app startup. */
@@ -290,7 +310,12 @@ export default class StyleKit {
}
}
activateTheme(theme) {
activateTheme(theme: {
content: { variables: { stylekitInfoColor: any } };
setDirty: (arg0: boolean) => void;
getNotAvailOnMobile: () => any;
setNotAvailOnMobile: (arg0: boolean) => void;
}) {
const performActivation = () => {
this.setActiveTheme(theme);
this.assignThemeForMode({ theme: theme, mode: this.currentDarkMode });
@@ -305,7 +330,7 @@ export default class StyleKit {
if (!hasValidInfoColor) {
ThemeDownloader.get()
.downloadTheme(theme)
.then(variables => {
.then((variables: any) => {
if (!variables) {
Alert.alert(
'Not Available',
@@ -353,7 +378,16 @@ export default class StyleKit {
}
}
async downloadThemeAndReload(theme) {
async downloadThemeAndReload(theme: {
content: {
package_info: { no_mobile: any };
isSystemTheme: any;
variables: any;
};
name?: any;
setDirty: (arg0: boolean) => void;
uuid?: any;
}) {
const updatedVariables = await ThemeDownloader.get().downloadTheme(theme);
/** Merge default variables to ensure this theme has all the variables. */
@@ -551,13 +585,6 @@ export default class StyleKit {
};
}
buildConstants() {
this.constants = {
mainTextFontSize: 16,
paddingLeft: 14,
};
}
static doesDeviceSupportDarkMode() {
const isAndroid = Platform.OS === 'android';
@@ -573,7 +600,7 @@ export default class StyleKit {
return supportsDarkMode;
}
static variable(name) {
static variable(name: string) {
return this.get().activeTheme.content.variables[name];
}
@@ -589,7 +616,7 @@ export default class StyleKit {
return this.get().styles;
}
static stylesForKey(key) {
static stylesForKey(key: string) {
const allStyles = this.styles;
const styles = [allStyles[key]];
const platform = Platform.OS === 'android' ? 'Android' : 'IOS';
@@ -604,7 +631,7 @@ export default class StyleKit {
return Platform.OS === 'android' ? 'md' : 'ios';
}
static nameForIcon(iconName) {
static nameForIcon(iconName: string) {
return StyleKit.platformIconPrefix() + '-' + iconName;
}
}

View File

@@ -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();
}

View File

@@ -36,5 +36,6 @@
"stylekitInputPlaceholderColor": "rgb(168, 168, 168)",
"stylekitInputBorderColor": "#e3e3e3",
"stylekitScrollbarThumbColor": "#dfdfdf",
"stylekitScrollbarTrackBorderColor": "#E7E7E7"
"stylekitScrollbarTrackBorderColor": "#E7E7E7",
"statusBar": ""
}

View File

@@ -36,5 +36,6 @@
"stylekitInputPlaceholderColor": "rgb(168, 168, 168)",
"stylekitInputBorderColor": "#e3e3e3",
"stylekitScrollbarThumbColor": "#dfdfdf",
"stylekitScrollbarTrackBorderColor": "#E7E7E7"
"stylekitScrollbarTrackBorderColor": "#E7E7E7",
"statusBar": ""
}

View File

@@ -6,8 +6,8 @@ export default class CSSParser {
/**
* @param css: CSS file contents in string format
*/
static cssToObject(css) {
const object = {};
static cssToObject(css: string) {
const object: Record<string, string> = {};
const lines = css.split('\n');
for (let line of lines) {
@@ -36,7 +36,10 @@ export default class CSSParser {
return object;
}
static resolveVariablesThatReferenceOtherVariables(object, round = 0) {
static resolveVariablesThatReferenceOtherVariables(
object: Record<string, any>,
round = 0
) {
for (const key of Object.keys(object)) {
const value = object[key];
const stripValue = 'var(';
@@ -67,7 +70,7 @@ export default class CSSParser {
}
}
static hyphenatedStringToCamelCase(string) {
static hyphenatedStringToCamelCase(string: string) {
const comps = string.split('-');
let result = '';
for (let i = 0; i < comps.length; i++) {
@@ -82,7 +85,7 @@ export default class CSSParser {
return result;
}
static capitalizeFirstLetter(string) {
static capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
}

View File

@@ -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);
},

View File

@@ -1,10 +1,13 @@
import { SNTheme as SNJSTheme } from 'snjs';
/* eslint-disable no-bitwise */
export const LIGHT_MODE_KEY = 'light';
export const DARK_MODE_KEY = 'dark';
export const LIGHT_CONTENT = 'light-content';
export const DARK_CONTENT = 'dark-content';
export function statusBarColorForTheme(theme) {
type SNTheme = typeof SNJSTheme;
export function statusBarColorForTheme(theme: SNTheme) {
// The main nav bar uses contrast background color
if (!theme.luminosity) {
theme.luminosity = getColorLuminosity(
@@ -20,7 +23,7 @@ export function statusBarColorForTheme(theme) {
}
}
export function keyboardColorForTheme(theme) {
export function keyboardColorForTheme(theme: SNTheme) {
if (!theme.luminosity) {
theme.luminosity = getColorLuminosity(
theme.content.variables.stylekitContrastBackgroundColor
@@ -35,7 +38,7 @@ export function keyboardColorForTheme(theme) {
}
}
export function getColorLuminosity(hexCode) {
export function getColorLuminosity(hexCode: string) {
let c = hexCode;
c = c.substring(1); // strip #
const rgb = parseInt(c, 16); // convert rrggbb to decimal
@@ -46,12 +49,12 @@ export function getColorLuminosity(hexCode) {
return 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
}
export function shadeBlend(p, c0, c1) {
var n = p < 0 ? p * -1 : p,
export function shadeBlend(p: number, c0: string, c1?: string) {
let n = p < 0 ? p * -1 : p,
u = Math.round,
w = parseInt;
if (c0.length > 7) {
var f = c0.split(','),
const f = c0.split(','),
t = (c1 ? c1 : p < 0 ? 'rgb(0,0,0)' : 'rgb(255,255,255)').split(','),
R = w(f[0].slice(4)),
G = w(f[1]),
@@ -66,7 +69,7 @@ export function shadeBlend(p, c0, c1) {
')'
);
} else {
var f = w(c0.slice(1), 16),
const f = w(c0.slice(1), 16),
t = w((c1 ? c1 : p < 0 ? '#000000' : '#FFFFFF').slice(1), 16),
R1 = f >> 16,
G1 = (f >> 8) & 0x00ff,
@@ -85,19 +88,19 @@ export function shadeBlend(p, c0, c1) {
}
}
export function darken(color, value = -0.15) {
export function darken(color: string, value = -0.15) {
return shadeBlend(value, color);
}
export function lighten(color, value = 0.25) {
export function lighten(color: string, value = 0.25) {
return shadeBlend(value, color);
}
export function hexToRGBA(hex, alpha) {
export function hexToRGBA(hex: string, alpha: number) {
if (!hex || !hex.startsWith('#')) {
return null;
return '';
}
var c;
let c: any;
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
c = hex.substring(1).split('');
if (c.length === 3) {

1
src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'react-native-search-box';

View File

@@ -0,0 +1 @@
declare module 'react-native-seach-box';

37
src/types/snjs/index.d.ts vendored Normal file
View File

@@ -0,0 +1,37 @@
declare module 'snjs' {
export const SNTheme: any;
export const SNProtocolManager: any;
export const protocolManager: any;
export const SFItem: any;
export const SFItemParams: any;
export const SFPredicate: any;
export const SNNote: any;
export const SNTag: any;
export const SNSmartTag: any;
export const SNMfa: any;
export const SNServerExtension: any;
export const SNComponent: any;
export const SNEditor: any;
export const Action: any;
export const SNExtension: any;
export const SNEncryptedStorage: any;
export const SFHistorySession: any;
export const SFItemHistory: any;
export const SFPrivileges: any;
export const SNWebCrypto: any;
export const SNCryptoJS: any;
export const SNReactNativeCrypto: any;
export const findInArray: any;
export const SFModelManager: any;
export const SFHttpManager: any;
export const SFStorageManager: any;
export const SFSyncManager: any;
export const SFAuthManager: any;
export const SFAlertManager: any;
export const SFSessionHistoryManager: any;
export const SFPrivilegesManager: any;
export const SFSingletonManager: any;
export const SNEncryptedStorage: any;
export const SNComponentManager: any;
export const SFMigrationManager: any;
}

View File

@@ -11,6 +11,7 @@
"isolatedModules": false, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true,
// "noImplicitAny": false,
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "src", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
@@ -28,9 +29,10 @@
"@Style/*": ["./style/*"],
},
"typeRoots": ["./src/types/*"],
"resolveJsonModule": true,
},
"exclude": [
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js", "types"
]
}

200
yarn.lock
View File

@@ -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"