mirror of
https://github.com/standardnotes/mobile.git
synced 2026-04-19 05:39:20 -04:00
370 lines
10 KiB
JavaScript
370 lines
10 KiB
JavaScript
import React, { Component } from 'react';
|
|
import { StyleSheet, View, Platform, Text, StatusBar, Modal } from 'react-native';
|
|
import ModelManager from '../lib/modelManager'
|
|
import Storage from '../lib/storage'
|
|
import Sync from '../lib/sync'
|
|
import Auth from '../lib/auth'
|
|
import KeysManager from '../lib/keysManager'
|
|
import AlertManager from '../lib/alertManager'
|
|
import GlobalStyles from "../Styles"
|
|
import Keychain from "../lib/keychain"
|
|
import Icons from '../Icons';
|
|
import NoteList from "../containers/NoteList"
|
|
import Abstract from "./Abstract"
|
|
import OptionsState from "../OptionsState"
|
|
import App from "../app"
|
|
import AuthModal from "../containers/AuthModal"
|
|
import LockedView from "../containers/LockedView"
|
|
var _ = require('lodash')
|
|
import ApplicationState from "../ApplicationState";
|
|
|
|
export default class Notes extends Abstract {
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.rendersLockscreen = true;
|
|
|
|
this.stateObserver = ApplicationState.get().addStateObserver((state) => {
|
|
if(state == ApplicationState.Resuming) {
|
|
// we only want to perform sync here if the app is resuming, not if it's a fresh start
|
|
if(this.dataLoaded) {
|
|
Sync.getInstance().sync();
|
|
}
|
|
|
|
var authProps = ApplicationState.get().getAuthenticationPropsForAppState(state);
|
|
if((authProps.passcode || authProps.fingerprint)) {
|
|
// The auth modal is only presented if the Notes screen is visible.
|
|
this.props.navigator.popToRoot();
|
|
|
|
// Don't use the below as it will also for some reason dismiss the non RNN auth modal as well
|
|
// this.props.navigator.dismissAllModals({animationType: 'none'});
|
|
|
|
this.props.navigator.switchToTab({
|
|
tabIndex: 0
|
|
});
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
loadInitialState() {
|
|
this.options = App.get().globalOptions();
|
|
|
|
this.mergeState({
|
|
refreshing: false,
|
|
decrypting: false,
|
|
loading: true,
|
|
});
|
|
|
|
this.registerObservers();
|
|
this.loadTabbarIcons();
|
|
this.initializeNotes();
|
|
this.beginSyncTimer();
|
|
|
|
super.loadInitialState();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
super.componentWillUnmount();
|
|
ApplicationState.get().removeStateObserver(this.stateObserver);
|
|
Sync.getInstance().removeSyncObserver(this.syncObserver);
|
|
Auth.getInstance().removeEventObserver(this.signoutObserver);
|
|
this.options.removeChangeObserver(this.optionsObserver);
|
|
clearInterval(this.syncTimer);
|
|
}
|
|
|
|
beginSyncTimer() {
|
|
// Refresh every 30s
|
|
this.syncTimer = setInterval(function () {
|
|
Sync.getInstance().sync(null);
|
|
}, 30000);
|
|
}
|
|
|
|
registerObservers() {
|
|
this.optionsObserver = this.options.addChangeObserver((options) => {
|
|
this.reloadList(true);
|
|
// On iOS, configureNavBar would be handle by viewWillAppear. However, we're using a drawer in Android.
|
|
if(Platform.OS == "android" && !this.skipUpdatingNavBar) {
|
|
this.configureNavBar();
|
|
}
|
|
})
|
|
|
|
this.syncObserver = Sync.getInstance().registerSyncObserver(function(changesMade, retrieved, saved, unsaved){
|
|
if(_.find(retrieved, {content_type: "Note"}) || _.find(unsaved, {content_type: "Note"})) {
|
|
this.reloadList();
|
|
}
|
|
this.mergeState({refreshing: false, loading: false});
|
|
}.bind(this))
|
|
|
|
this.signoutObserver = Auth.getInstance().addEventObserver([Auth.DidSignOutEvent, Auth.WillSignInEvent], function(event){
|
|
if(event == Auth.WillSignInEvent) {
|
|
this.mergeState({loading: true})
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
loadTabbarIcons() {
|
|
if(!App.get().isIOS) {
|
|
return;
|
|
}
|
|
this.props.navigator.setTabButton({
|
|
tabIndex: 0,
|
|
icon: Icons.getIcon('ios-menu-outline'),
|
|
selectedIcon: Icons.getIcon('ios-menu-outline')
|
|
});
|
|
this.props.navigator.setTabButton({
|
|
tabIndex: 1,
|
|
icon: Icons.getIcon('ios-contact-outline'),
|
|
selectedIcon: Icons.getIcon('ios-contact-outline')
|
|
});
|
|
}
|
|
|
|
initializeNotes() {
|
|
var encryptionEnabled = KeysManager.get().encryptionEnabled();
|
|
this.mergeState({decrypting: encryptionEnabled, loading: !encryptionEnabled})
|
|
|
|
Sync.getInstance().loadLocalItems(function(items) {
|
|
setTimeout(function () {
|
|
this.dataLoaded = true;
|
|
this.reloadList();
|
|
this.configureNavBar(true);
|
|
this.mergeState({decrypting: false, loading: false});
|
|
// perform initial sync
|
|
Sync.getInstance().sync(null);
|
|
}.bind(this), 0);
|
|
}.bind(this));
|
|
}
|
|
|
|
configureNavBar(initial = false) {
|
|
if(!this.dataLoaded) {
|
|
return;
|
|
}
|
|
|
|
super.configureNavBar();
|
|
|
|
var options = this.options;
|
|
|
|
var notesTitle = "Notes";
|
|
var filterTitle = "Filter";
|
|
var numTags = options.selectedTags.length;
|
|
|
|
if(numTags > 0 || options.archivedOnly) {
|
|
if(numTags > 0) {
|
|
filterTitle += ` (${numTags})`
|
|
}
|
|
notesTitle = options.archivedOnly ? "Archived Notes" : "Filtered Notes";
|
|
}
|
|
|
|
// Android only allows 1 tag selection
|
|
if(App.isAndroid) {
|
|
if(numTags > 0) {
|
|
var tags = ModelManager.getInstance().getItemsWithIds(options.selectedTags);
|
|
if(tags.length > 0) {
|
|
var tag = tags[0];
|
|
notesTitle = tag.title + " notes";
|
|
} else {
|
|
notesTitle = "Notes";
|
|
}
|
|
}
|
|
|
|
if(options.archivedOnly) {
|
|
notesTitle = "Archived " + notesTitle;
|
|
}
|
|
}
|
|
|
|
if(notesTitle !== this.notesTitle) {
|
|
// no changes, return. We do this so when swiping back from compose to here,
|
|
// we don't change the title while a transition is taking place
|
|
this.notesTitle = notesTitle;
|
|
|
|
this.props.navigator.setTitle({title: notesTitle, animated: false});
|
|
}
|
|
|
|
if(!initial && App.isIOS && filterTitle === this.filterTitle) {
|
|
// On Android, we want to always run the bottom code in the case of the FAB that doesn't
|
|
// reappaer if on the next screen a keyboard is present and you hit back.
|
|
// on iOS, navigation button stack is saved so it only needs to be configured once
|
|
return;
|
|
}
|
|
|
|
this.filterTitle = filterTitle;
|
|
|
|
var rightButtons = [];
|
|
if(App.get().isIOS) {
|
|
rightButtons.push({
|
|
title: 'New',
|
|
id: 'new',
|
|
showAsAction: 'ifRoom',
|
|
})
|
|
} else {
|
|
rightButtons.push({
|
|
title: 'Settings',
|
|
id: 'settings',
|
|
showAsAction: 'ifRoom',
|
|
icon: Icons.getIcon('md-settings'),
|
|
})
|
|
}
|
|
|
|
this.props.navigator.setButtons({
|
|
rightButtons: rightButtons,
|
|
leftButtons: [
|
|
{
|
|
title: filterTitle,
|
|
id: 'sideMenu',
|
|
showAsAction: 'ifRoom',
|
|
},
|
|
],
|
|
fab: {
|
|
collapsedId: 'new',
|
|
collapsedIcon: Icons.getIcon('md-add'),
|
|
backgroundColor: GlobalStyles.constants().mainTintColor
|
|
},
|
|
animated: false
|
|
});
|
|
}
|
|
|
|
onNavigatorEvent(event) {
|
|
|
|
super.onNavigatorEvent(event);
|
|
|
|
if(event.id == "willAppear" || event.id == "didAppear") {
|
|
if(event.id == "willAppear") {
|
|
this.forceUpdate();
|
|
}
|
|
if(this.loadNotesOnVisible) {
|
|
this.loadNotesOnVisible = false;
|
|
this.reloadList();
|
|
}
|
|
}
|
|
|
|
if (event.type == 'NavBarButtonPress') {
|
|
if (event.id == 'new') {
|
|
this.presentNewComposer();
|
|
} else if (event.id == 'sideMenu') {
|
|
// Android is handled by the drawer attribute of rn-navigation
|
|
if(Platform.OS == "ios") {
|
|
this.presentFilterScreen();
|
|
}
|
|
} else if(event.id == "settings") {
|
|
this.presentSettingsScreen();
|
|
}
|
|
}
|
|
}
|
|
|
|
presentNewComposer() {
|
|
this.props.navigator.push({
|
|
screen: 'sn.Compose',
|
|
title: 'Compose',
|
|
passProps: {selectedTagId: this.selectedTags.length && this.selectedTags[0].uuid}, // For Android
|
|
});
|
|
}
|
|
|
|
presentFilterScreen() {
|
|
this.props.navigator.showModal({
|
|
screen: 'sn.Filter',
|
|
title: 'Options',
|
|
animationType: 'slide-up',
|
|
passProps: {
|
|
options: JSON.stringify(this.options),
|
|
onOptionsChange: (options) => {
|
|
this.options.mergeWith(options);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
presentSettingsScreen() {
|
|
this.props.navigator.showModal({
|
|
screen: 'sn.Account',
|
|
title: 'Account',
|
|
animationType: 'slide-up'
|
|
});
|
|
}
|
|
|
|
reloadList(force) {
|
|
if(!this.visible && !this.willBeVisible && !force) {
|
|
console.log("===Scheduling Notes Render Update===");
|
|
this.loadNotesOnVisible = true;
|
|
return;
|
|
}
|
|
|
|
console.log("===Reload Notes List===");
|
|
|
|
this.forceUpdate();
|
|
this.mergeState({refreshing: false})
|
|
}
|
|
|
|
_onRefresh() {
|
|
this.setState({refreshing: true});
|
|
Sync.getInstance().sync();
|
|
}
|
|
|
|
_onPressItem = (item: hash) => {
|
|
var run = () => {
|
|
if(item.conflictOf) {
|
|
item.conflictOf = null;
|
|
}
|
|
|
|
this.props.navigator.push({
|
|
screen: 'sn.Compose',
|
|
title: 'Compose',
|
|
passProps: {noteId: item.uuid},
|
|
animationType: "slide-horizontal"
|
|
});
|
|
}
|
|
|
|
if(item.errorDecrypting) {
|
|
AlertManager.showConfirmationAlert(
|
|
"Unable to Decrypt", "This note could not be decrypted. Perhaps it was encrypted with another key? Please visit standardnotes.org/help for more. Note: Editing this note may damage its original contents.", "Edit Anyway",
|
|
function(){
|
|
run();
|
|
}.bind(this)
|
|
)
|
|
} else {
|
|
run();
|
|
}
|
|
}
|
|
|
|
onSearchTextChange = (text) => {
|
|
this.skipUpdatingNavBar = true;
|
|
this.options.setSearchTerm(text);
|
|
this.skipUpdatingNavBar = false;
|
|
}
|
|
|
|
onSearchCancel = () => {
|
|
this.skipUpdatingNavBar = true;
|
|
this.options.setSearchTerm(null);
|
|
this.skipUpdatingNavBar = false;
|
|
}
|
|
|
|
render() {
|
|
if(this.state.lockContent) {
|
|
return <AuthModal />;
|
|
}
|
|
|
|
var result = ModelManager.getInstance().getNotes(this.options);
|
|
var notes = result.notes;
|
|
var tags = this.selectedTags = result.tags;
|
|
|
|
return (
|
|
<View style={GlobalStyles.styles().container}>
|
|
{notes &&
|
|
<NoteList
|
|
onRefresh={this._onRefresh.bind(this)}
|
|
onPressItem={this._onPressItem}
|
|
refreshing={this.state.refreshing}
|
|
onSearchChange={this.onSearchTextChange}
|
|
onSearchCancel={this.onSearchCancel}
|
|
notes={notes}
|
|
sortType={this.options.sortBy}
|
|
decrypting={this.state.decrypting}
|
|
loading={this.state.loading}
|
|
selectedTags={tags}
|
|
/>
|
|
}
|
|
</View>
|
|
);
|
|
}
|
|
}
|