From c8062618edb7618c8ae21ccc9243f1283e841291 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 27 Dec 2018 14:15:42 -0600 Subject: [PATCH] Refactor of Filter -> SideMenu, Account -> Settings, and NoteOptions screen --- src/app.js | 11 +- src/containers/ManageNote.js | 73 ---- src/containers/TagList.js | 101 +++++ src/screens/Abstract.js | 110 +++-- src/screens/Authenticate.js | 2 +- src/screens/Compose.js | 30 +- src/screens/Filter.js | 535 ------------------------ src/screens/NoteOptions.js | 320 ++++++++++++++ src/screens/Notes.js | 1 + src/screens/{Account.js => Settings.js} | 80 +++- src/screens/SideMenu.js | 174 ++++++++ src/screens/index.js | 18 - 12 files changed, 740 insertions(+), 715 deletions(-) delete mode 100644 src/containers/ManageNote.js create mode 100644 src/containers/TagList.js delete mode 100644 src/screens/Filter.js create mode 100644 src/screens/NoteOptions.js rename src/screens/{Account.js => Settings.js} (88%) create mode 100644 src/screens/SideMenu.js delete mode 100644 src/screens/index.js diff --git a/src/app.js b/src/app.js index d6d32cb1..97235914 100644 --- a/src/app.js +++ b/src/app.js @@ -14,20 +14,21 @@ import ReviewManager from './lib/reviewManager'; import Compose from "./screens/Compose" import Notes from "./screens/Notes" -import Filter from "./screens/Filter" -import Account from "./screens/Account" +import SideMenu from "./screens/SideMenu" +import Settings from "./screens/Settings" +import NoteOptions from "./screens/NoteOptions" import InputModal from "./screens/InputModal" const AppStack = createStackNavigator({ Notes: {screen: Notes}, Compose: {screen: Compose}, - NoteOptions: {screen : Filter}, + NoteOptions: {screen : NoteOptions}, }, { initialRouteName: 'Notes' }) const SettingsStack = createStackNavigator({ - Screen1: Account + Screen1: Settings }) const InputModalStack = createStackNavigator({ @@ -46,7 +47,7 @@ const ModalStack = createStackNavigator({ const DrawerStack = createDrawerNavigator({ Main: ModalStack }, { - contentComponent: Filter, + contentComponent: SideMenu, }); const AppContainer = createAppContainer(DrawerStack); diff --git a/src/containers/ManageNote.js b/src/containers/ManageNote.js deleted file mode 100644 index 4bdf5da5..00000000 --- a/src/containers/ManageNote.js +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react'; -import { TextInput, SectionList, ScrollView, View, Text, Platform } from 'react-native'; -import SectionHeader from "../components/SectionHeader"; -import TableSection from "../components/TableSection"; -import SectionedAccessoryTableCell from "../components/SectionedAccessoryTableCell"; -import ItemActionManager from '../lib/itemActionManager' -import Icons from "../Icons"; - -import GlobalStyles from "../Styles" - -export default class SortSection extends Component { - constructor(props) { - super(props); - } - - onPress = (key) => { - this.props.onEvent(key); - this.forceUpdate(); - } - - render() { - let root = this; - var pinAction = this.props.note.pinned ? "Unpin" : "Pin"; - let pinEvent = pinAction == "Pin" ? ItemActionManager.PinEvent : ItemActionManager.UnpinEvent; - - var archiveOption = this.props.note.archived ? "Unarchive" : "Archive"; - let archiveEvent = archiveOption == "Archive" ? ItemActionManager.ArchiveEvent : ItemActionManager.UnarchiveEvent; - - var lockOption = this.props.note.locked ? "Unlock" : "Lock"; - let lockEvent = lockOption == "Lock" ? ItemActionManager.LockEvent : ItemActionManager.UnlockEvent; - - return ( - - - {this.onPress(pinEvent)}} - first={true} text={pinAction} - leftAlignIcon={true} - /> - - {this.onPress(archiveEvent)}} - text={archiveOption} - leftAlignIcon={true} - /> - - {this.onPress(lockEvent)}} - text={lockOption} - leftAlignIcon={true} - /> - - {this.onPress(ItemActionManager.ShareEvent)}} - text={"Share"} - leftAlignIcon={true} - /> - - {this.onPress(ItemActionManager.DeleteEvent)}} - text={"Delete"} - last={true} - leftAlignIcon={true} - /> - - ); - } -} diff --git a/src/containers/TagList.js b/src/containers/TagList.js new file mode 100644 index 00000000..89be814b --- /dev/null +++ b/src/containers/TagList.js @@ -0,0 +1,101 @@ +import React, { Component } from 'react'; +import { StyleSheet, View, FlatList, RefreshControl, ScrollView, Text } from 'react-native'; +import GlobalStyles from "../Styles" +import TableSection from "../components/TableSection"; +import SectionHeader from "../components/SectionHeader"; +import SectionedAccessoryTableCell from "../components/SectionedAccessoryTableCell"; +import ItemActionManager from '../lib/itemActionManager' +import ActionSheet from 'react-native-actionsheet' +import ApplicationState from "../ApplicationState" + +export default class TagList extends Component { + constructor(props) { + super(props); + this.state = {}; + } + + onPress = (tag) => { + this.props.onTagSelect(tag); + } + + onLongPress = (tag) => { + this.props.onTagLongPress(tag); + } + + static ActionSheetCancelIndex = 0; + static ActionSheetDestructiveIndex = 1; + + actionSheetActions() { + return [ + ['Cancel', ""], + ['Delete', ItemActionManager.DeleteEvent] + ]; + } + + showActionSheet = (item) => { + // Dont show actionsheet for "All notes" tag + if(item.key !== "all") { + this.actionSheetItem = item; + this.setState((prevState) => { + return _.merge(prevState, {actionSheetTitle: item.title}) + }) + this.actionSheet.show(); + } + } + + handleActionSheetPress = (index) => { + if(index == 0) { + return; + } + + this.props.onManageTagEvent(this.actionSheetActions()[index][1], this.actionSheetItem, () => { + this.forceUpdate(); + }); + this.actionSheetItem = null; + } + + // must pass title, text, and tags as props so that it re-renders when either of those change + _renderItem = ({item}) => { + return ( + + {this.onPress(item)}} + onLongPress={() => this.showActionSheet(item)} + text={item.deleted ? "Deleting..." : item.title} + color={item.deleted ? GlobalStyles.constants().mainTintColor : undefined} + key={item.uuid} + first={this.props.tags.indexOf(item) == 0} + last={this.props.tags.indexOf(item) == this.props.tags.length - 1} + selected={() => {return this.props.selected.includes(item.uuid)}} + /> + + this.actionSheet = o} + options={this.actionSheetActions().map((action) => {return action[0]})} + cancelButtonIndex={TagList.ActionSheetCancelIndex} + destructiveButtonIndex={TagList.ActionSheetDestructiveIndex} + onPress={this.handleActionSheetPress} + {...GlobalStyles.actionSheetStyles()} + /> + + ) + } + + render() { + return ( + + {this.props.clearSelection(true)}}/> + + + + + ); + } +} diff --git a/src/screens/Abstract.js b/src/screens/Abstract.js index 002e2c3d..b0ba02c3 100644 --- a/src/screens/Abstract.js +++ b/src/screens/Abstract.js @@ -86,17 +86,53 @@ export default class Abstract extends Component { } componentWillUnmount() { - for(var listener of this.listeners) { - listener.remove(); + this.willUnmount = true; + this.mounted = false; + ApplicationState.get().removeStateObserver(this._stateObserver); + } + + componentDidMount() { + this.mounted = true; + this.configureNavBar(true); + console.log("componentDidMount"); + + if(ApplicationState.get().isUnlocked() && !this.loadedInitialState) { + console.log("Loading initial state"); + this.loadInitialState(); + } + + if(this._renderOnMount) { + this._renderOnMount = false; + this.forceUpdate(); + + this._renderOnMountCallback && this._renderOnMountCallback(); + this._renderOnMountCallback = null; } } - componentWillFocus(){ + loadInitialState() { + this.loadedInitialState = true; + this.configureNavBar(true); + } + componentWillUnmount() { + for(var listener of this.listeners) { + listener.remove(); + } } - componentDidFocus(){ + componentWillFocus() { + this.willUnmount = false; + this.mounted = false; + if(ApplicationState.get().isUnlocked() && this.state.lockContent) { + this.unlockContent(); + } + } + componentDidFocus() { + this.visible = true; + this.willBeVisible = true; // Just in case willAppear isn't called for whatever reason + this.configureNavBar(false); } componentWillBlur(){ @@ -104,10 +140,11 @@ export default class Abstract extends Component { } componentDidBlur(){ - + this.willBeVisible = false; + this.visible = false; } - getProp(prop) { + getProp = (prop) => { // this.props.navigation could be undefined if we're in the drawer return this.props.navigation.getParam && this.props.navigation.getParam(prop); } @@ -119,20 +156,6 @@ export default class Abstract extends Component { this.props.navigation.setParams(options); } - // Called by RNN - componentDidAppear() { - this.visible = true; - this.willBeVisible = true; // Just in case willAppear isn't called for whatever reason - this.configureNavBar(false); - } - - // Called by RNN - componentDidDisappear() { - console.log("Component did disappear", this); - this.willBeVisible = false; - this.visible = false; - } - lockContent() { this.mergeState({lockContent: true}); this.configureNavBar(); @@ -145,42 +168,6 @@ export default class Abstract extends Component { this.mergeState({lockContent: false}); } - componentWillUnmount() { - this.willUnmount = true; - this.mounted = false; - ApplicationState.get().removeStateObserver(this._stateObserver); - } - - componentWillMount() { - this.willUnmount = false; - this.mounted = false; - if(ApplicationState.get().isUnlocked() && this.state.lockContent) { - this.unlockContent(); - } - } - - componentDidMount() { - this.mounted = true; - this.configureNavBar(true); - - if(ApplicationState.get().isUnlocked() && !this.loadedInitialState) { - this.loadInitialState(); - } - - if(this._renderOnMount) { - this._renderOnMount = false; - this.forceUpdate(); - - this._renderOnMountCallback && this._renderOnMountCallback(); - this._renderOnMountCallback = null; - } - } - - loadInitialState() { - this.loadedInitialState = true; - this.configureNavBar(true); - } - constructState(state) { this.state = _.merge({lockContent: ApplicationState.get().isLocked()}, state); } @@ -209,7 +196,14 @@ export default class Abstract extends Component { } - dismissModal() { - Navigation.dismissModal(); + popToRoot() { + this.props.navigation.popToTop(); + } + + dismiss() { + /* + the `null` parameter is actually very important: https://reactnavigation.org/docs/en/navigation-prop.html#goback-close-the-active-screen-and-move-back + */ + this.props.navigation.goBack(null); } } diff --git a/src/screens/Authenticate.js b/src/screens/Authenticate.js index 0f66eab0..1d867620 100644 --- a/src/screens/Authenticate.js +++ b/src/screens/Authenticate.js @@ -73,7 +73,7 @@ export default class Authenticate extends Abstract { dismiss() { if(!this.props.pseudoModal) { - this.dismissModal(); + this.dismiss(); } } diff --git a/src/screens/Compose.js b/src/screens/Compose.js index 7806022b..f7a4b449 100644 --- a/src/screens/Compose.js +++ b/src/screens/Compose.js @@ -56,15 +56,7 @@ export default class Compose extends Abstract { this.note = note; this.constructState({title: note.title, text: note.text}); - props.navigation.setParams({ - title: 'Compose', - drawerLockMode: "locked-closed", - rightButton: { - title: "Options", - onPress: () => {this.presentOptions();}, - disabled: !this.note.uuid - } - }) + this.configureHeaderBar(); this.loadStyles(); @@ -91,15 +83,23 @@ export default class Compose extends Abstract { } }); - this.configureNavBar(true); + this.configureHeaderBar(); } - refreshContent() { - this.mergeState({title: this.note.title, text: this.note.text}); + configureHeaderBar() { + this.props.navigation.setParams({ + title: 'Compose', + drawerLockMode: "locked-closed", + rightButton: { + title: "Options", + onPress: () => {this.presentOptions();}, + disabled: !this.note.uuid + } + }) } - componentDidMount() { - super.componentDidMount(); + refreshContent() { + this.mergeState({title: this.note.title, text: this.note.text}); } componentWillUnmount() { @@ -241,7 +241,7 @@ export default class Compose extends Abstract { tag.setDirty(true); } this.save(); - this.configureNavBar(true); + this.configureHeaderBar(); }); } else { this.save(); diff --git a/src/screens/Filter.js b/src/screens/Filter.js deleted file mode 100644 index f7937a40..00000000 --- a/src/screens/Filter.js +++ /dev/null @@ -1,535 +0,0 @@ -import React, { Component } from 'react'; -import { TextInput, SectionList, ScrollView, View, Text, Linking, Share, Platform, StatusBar, FlatList, Dimensions } from 'react-native'; -import Sync from '../lib/sfjs/syncManager' -import ModelManager from '../lib/sfjs/modelManager' -import ComponentManager from '../lib/componentManager' -import AlertManager from '../lib/sfjs/alertManager' -import ItemActionManager from '../lib/itemActionManager' -import SectionHeader from "../components/SectionHeader"; -import ButtonCell from "../components/ButtonCell"; -import TableSection from "../components/TableSection"; -import ManageNote from "../containers/ManageNote"; -import LockedView from "../containers/LockedView"; -import SectionedAccessoryTableCell from "../components/SectionedAccessoryTableCell"; -import Abstract from "./Abstract" -import Icons from '../Icons'; -import OptionsState from "../OptionsState" -import GlobalStyles from "../Styles" -import ApplicationState from "../ApplicationState"; -import ActionSheet from 'react-native-actionsheet' - -import { SafeAreaView } from 'react-navigation'; -import Icon from 'react-native-vector-icons/Ionicons'; -import FAB from 'react-native-fab'; - -export default class Filter extends Abstract { - - static navigationOptions = ({ navigation, navigationOptions }) => { - let templateOptions = { - title: "Options" - } - return Abstract.getDefaultNavigationOptions({navigation, navigationOptions, templateOptions}); - }; - - constructor(props) { - super(props); - this.tags = []; - } - - loadInitialState() { - super.loadInitialState(); - if(this.getProp("options")) { - this.options = new OptionsState(JSON.parse(this.getProp("options"))); - } else { - this.options = ApplicationState.getOptions(); - } - - var selectedTags; - if(this.options.selectedTags) { - selectedTags = this.options.selectedTags.slice(); // copy the array - } else { - selectedTags = []; - } - - this.mergeState({tags: [], selectedTags: selectedTags, options: this.options}); - - if(this.getProp("noteId")) { - this.note = ModelManager.get().findItem(this.getProp("noteId")); - } - - let handleInitialDataLoad = () => { - if(this.handledDataLoad) { return; } - this.handledDataLoad = true; - - this.loadTags = true; - this.forceUpdate(); - } - - if(Sync.get().initialDataLoaded()) { - handleInitialDataLoad(); - } - - this.syncEventHandler = Sync.get().addEventHandler((event, data) => { - if(event == "local-data-loaded") { - handleInitialDataLoad(); - } - - else if(event == "sync:completed") { - if(data.retrievedItems && _.find(data.retrievedItems, {content_type: "Tag"})) { - this.forceUpdate(); - } - } - }) - } - - componentWillUnmount() { - super.componentWillUnmount(); - ApplicationState.get().removeStateObserver(this.stateObserver); - Sync.get().removeEventHandler(this.syncEventHandler); - } - - componentDidFocus() { - super.componentDidFocus(); - this.forceUpdate(); - } - - componentDidBlur() { - super.componentDidBlur(); - - // will disappear - if(!this.isSingleSelectMode) { - // we prefer to notify the parent via NavBarButtonPress.accept, but when this view is presented via nav push, - // the user can swipe back and miss that. So we do it here as a backup. - if(!this.didNotifyParent) { - this.notifyParentOfOptionsChange(); - } - } - } - - dismiss = () => { - this.props.navigation.goBack(); - } - - notifyParentOfOptionsChange() { - this.getProp("onOptionsChange")(this.options); - } - - presentNewTag() { - this.props.navigation.navigate("NewTag", { - title: 'New Tag', - placeholder: "New tag name", - onSave: (text) => { - this.createTag(text, (tag) => { - if(this.note) { - // select this tag - this.onTagSelect(tag) - } - }); - } - }) - } - - createTag(text, callback) { - var tag = new SNTag({content: {title: text}}); - tag.initUUID().then(() => { - tag.setDirty(true); - ModelManager.get().addItem(tag); - Sync.get().sync(); - callback(tag); - this.forceUpdate(); - }) - } - - onSortChange = (key) => { - this.options.setSortBy(key); - if(this.isSingleSelectMode) { - this.notifyParentOfOptionsChange(); - } - } - - onTagSelect = (tag) => { - var selectedTags; - - if(this.isSingleSelectMode) { - selectedTags = [tag.uuid]; - } else { - selectedTags = this.state.selectedTags; - var selected = selectedTags.includes(tag.uuid); - if(selected) { - // deselect - selectedTags.splice(selectedTags.indexOf(tag.uuid), 1); - } else { - // select - selectedTags.push(tag.uuid); - } - } - - this.setSelectedTags(selectedTags); - } - - setSelectedTags = (selectedTags) => { - this.selectedTags = selectedTags.slice(); - this.options.setSelectedTags(selectedTags); - this.setState({selectedTags: selectedTags}); - - if(this.isSingleSelectMode) { - this.notifyParentOfOptionsChange(); - } - } - - isTagSelected(tag) { - return this.tags.indexOf(tag.uuid) !== -1; - } - - onManageTagEvent = (event, tag, renderBlock) => { - ItemActionManager.handleEvent(event, tag, () => { - if(event == ItemActionManager.DeleteEvent) { - this.loadTags = true; - this.forceUpdate(); - } - }, () => { - // afterConfirmCallback - // We want to show "Deleting.." on top of note cell after the user confirms the dialogue - renderBlock(); - }) - } - - onManageNoteEvent(event) { - ItemActionManager.handleEvent(event, this.note, () => { - this.getProp("onManageNoteEvent")(); - if(event == ItemActionManager.DeleteEvent) { - Navigation.popToRoot({ - animated: true, - }); - } - }) - } - - onOptionSelect = (option) => { - this.options.setDisplayOptionKeyValue(option, !this.options.getDisplayOptionValue(option)); - this.forceUpdate(); - // this.mergeState({archivedOnly: this.options.archivedOnly}); - - if(this.isSingleSelectMode) { - this.notifyParentOfOptionsChange(); - } - } - - onEditorSelect = (editor) => { - - if(editor) { - ComponentManager.get().associateEditorWithNote(editor, this.note); - } else { - ComponentManager.get().clearEditorForNote(this.note); - } - - this.getProp("onEditorSelect") && this.getProp("onEditorSelect")(editor); - - this.dismiss(); - } - - getEditors() { - return ModelManager.get().validItemsForContentType("SN|Component").filter(function(component){ - return component.area == "editor-editor"; - }) - } - - clearTags = (close) => { - this.setSelectedTags([]); - if(close) { this.dismiss(); } - } - - presentSettings() { - this.props.navigation.navigate("Settings"); - } - - get isSingleSelectMode() { - return this.getProp("singleSelectMode"); - } - - render() { - var viewStyles = [GlobalStyles.styles().container]; - - if(this.state.lockContent) { - return (); - } - - if(this.loadTags) { - var tags = ModelManager.get().tags.slice(); - if(this.isSingleSelectMode) { - tags.unshift({title: "All notes", key: "all", uuid: 100}) - } - this.tags = tags; - } - - return ( - - - - {!this.note && - - } - - {!this.note && - - } - - { this.note && - - } - - { this.note && - - } - - 0} - clearSelection={this.clearTags} - onManageTagEvent={this.onManageTagEvent} - title={"Tags"} - /> - - - {this.note ? this.presentNewTag() : this.presentSettings()}} - visible={true} - iconTextComponent={} - /> - - - ); - } -} - - -class TagsSection extends Component { - constructor(props) { - super(props); - this.state = {}; - } - - onPress = (tag) => { - this.props.onTagSelect(tag); - } - - onLongPress = (tag) => { - this.props.onTagLongPress(tag); - } - - static ActionSheetCancelIndex = 0; - static ActionSheetDestructiveIndex = 1; - - actionSheetActions() { - return [ - ['Cancel', ""], - ['Delete', ItemActionManager.DeleteEvent] - ]; - } - - showActionSheet = (item) => { - // Dont show actionsheet for "All notes" tag - if(item.key !== "all") { - this.actionSheetItem = item; - this.setState((prevState) => { - return _.merge(prevState, {actionSheetTitle: item.title}) - }) - this.actionSheet.show(); - } - } - - handleActionSheetPress = (index) => { - if(index == 0) { - return; - } - - this.props.onManageTagEvent(this.actionSheetActions()[index][1], this.actionSheetItem, () => { - this.forceUpdate(); - }); - this.actionSheetItem = null; - } - - // must pass title, text, and tags as props so that it re-renders when either of those change - _renderItem = ({item}) => { - return ( - - {this.onPress(item)}} - onLongPress={() => this.showActionSheet(item)} - text={item.deleted ? "Deleting..." : item.title} - color={item.deleted ? GlobalStyles.constants().mainTintColor : undefined} - key={item.uuid} - first={this.props.tags.indexOf(item) == 0} - last={this.props.tags.indexOf(item) == this.props.tags.length - 1} - selected={() => {return this.props.selected.includes(item.uuid)}} - /> - - this.actionSheet = o} - options={this.actionSheetActions().map((action) => {return action[0]})} - cancelButtonIndex={TagsSection.ActionSheetCancelIndex} - destructiveButtonIndex={TagsSection.ActionSheetDestructiveIndex} - onPress={this.handleActionSheetPress} - {...GlobalStyles.actionSheetStyles()} - /> - - ) - } - - render() { - return ( - - {this.props.clearSelection(true)}}/> - - - - - ); - } -} - -class OptionsSection extends Component { - constructor(props) { - super(props); - // this.state = {archivedOnly: props.archivedOnly} - } - - onOptionSelect = (option) => { - this.props.onOptionSelect(option); - } - - render() { - return ( - - - - {this.onOptionSelect('archivedOnly')}} - text={"Show only archived notes"} - first={true} - selected={() => {return this.props.options.archivedOnly}} - /> - - {this.onOptionSelect('hidePreviews')}} - text={"Hide note previews"} - selected={() => {return this.props.options.hidePreviews}} - /> - - {this.onOptionSelect('hideTags')}} - text={"Hide note tags"} - selected={() => {return this.props.options.hideTags}} - /> - - {this.onOptionSelect('hideDates')}} - text={"Hide note dates"} - last={true} - selected={() => {return this.props.options.hideDates}} - /> - - - ); - } -} - -class SortSection extends Component { - constructor(props) { - super(props); - this.state = {sortBy: props.sortBy} - this.options = [ - {key: "created_at", label: "Date Added"}, - {key: "client_updated_at", label: "Date Modified"}, - {key: "title", label: "Title"}, - ]; - } - - onPress = (key) => { - this.setState({sortBy: key}); - this.props.onSortChange(key); - } - - render() { - let root = this; - return ( - - - {this.options.map(function(option, i){ - return ( - {root.onPress(option.key)}} - text={option.label} - key={option.key} - first={i == 0} - last={i == root.options.length - 1} - selected={() => {return option.key == root.state.sortBy}} - /> - ) - })} - - - ); - } -} - -class EditorsSection extends Component { - constructor(props) { - super(props); - } - - onPress = (editor) => { - this.props.onEditorSelect(editor); - } - - getEditors = () => { - Linking.openURL("https://standardnotes.org/extensions"); - } - - clearEditorSelection = () => { - this.props.onEditorSelect(null); - } - - render() { - let root = this; - return ( - - {this.clearEditorSelection()}}/> - {this.props.editors.map(function(editor, i){ - return ( - {root.onPress(editor)}} - text={editor.name} - key={editor.uuid} - first={i == 0} - selected={() => {return editor == root.props.selectedEditor}} - buttonCell={true} - /> - ) - })} - - {this.props.editors.length == 0 && - {root.getEditors()}} - text={"Get Editors →"} - first={true} - buttonCell={true} - /> - } - - - - ); - } -} diff --git a/src/screens/NoteOptions.js b/src/screens/NoteOptions.js new file mode 100644 index 00000000..e853c5a6 --- /dev/null +++ b/src/screens/NoteOptions.js @@ -0,0 +1,320 @@ +import React, { Component } from 'react'; +import { ScrollView, View, Text, Linking, Share, StatusBar } from 'react-native'; +import Abstract from "./Abstract" + +import Sync from '../lib/sfjs/syncManager' +import ModelManager from '../lib/sfjs/modelManager' +import ComponentManager from '../lib/componentManager' +import ItemActionManager from '../lib/itemActionManager' + +import SectionHeader from "../components/SectionHeader"; +import ButtonCell from "../components/ButtonCell"; +import TableSection from "../components/TableSection"; +import LockedView from "../containers/LockedView"; +import SectionedAccessoryTableCell from "../components/SectionedAccessoryTableCell"; +import TagList from "../containers/TagList"; + +import Icons from '../Icons'; +import GlobalStyles from "../Styles" +import ApplicationState from "../ApplicationState"; +import OptionsState from "../OptionsState"; +import Icon from 'react-native-vector-icons/Ionicons'; +import FAB from 'react-native-fab'; + +export default class NoteOptions extends Abstract { + + static navigationOptions = ({ navigation, navigationOptions }) => { + let templateOptions = { + title: "Manage Note" + } + return Abstract.getDefaultNavigationOptions({navigation, navigationOptions, templateOptions}); + }; + + constructor(props) { + super(props); + this.tags = []; + } + + loadInitialState() { + super.loadInitialState(); + + if(this.getProp("options")) { + this.options = new OptionsState(JSON.parse(this.getProp("options"))); + } + + var selectedTags; + if(this.options.selectedTags) { + selectedTags = this.options.selectedTags.slice(); // copy the array + } else { + selectedTags = []; + } + + this.mergeState({tags: [], selectedTags: selectedTags, options: this.options}); + + this.note = ModelManager.get().findItem(this.getProp("noteId")); + + let handleInitialDataLoad = () => { + if(this.handledDataLoad) { return; } + this.handledDataLoad = true; + + this.tagsNeedReload = true; + this.forceUpdate(); + } + + if(Sync.get().initialDataLoaded()) { + handleInitialDataLoad(); + } + + this.syncEventHandler = Sync.get().addEventHandler((event, data) => { + if(event == "local-data-loaded") { + handleInitialDataLoad(); + } + + else if(event == "sync:completed") { + if(data.retrievedItems && _.find(data.retrievedItems, {content_type: "Tag"})) { + this.forceUpdate(); + } + } + }) + } + + componentWillUnmount() { + super.componentWillUnmount(); + ApplicationState.get().removeStateObserver(this.stateObserver); + Sync.get().removeEventHandler(this.syncEventHandler); + } + + componentDidFocus() { + super.componentDidFocus(); + this.forceUpdate(); + } + + componentDidBlur() { + super.componentDidBlur(); + this.notifyParentOfOptionsChange(); + } + + notifyParentOfOptionsChange() { + this.getProp("onOptionsChange")(this.options); + } + + presentNewTag() { + this.props.navigation.navigate("NewTag", { + title: 'New Tag', + placeholder: "New tag name", + onSave: (text) => { + this.createTag(text, (tag) => { + if(this.note) { + // select this tag + this.onTagSelect(tag) + } + }); + } + }) + } + + createTag(text, callback) { + var tag = new SNTag({content: {title: text}}); + tag.initUUID().then(() => { + tag.setDirty(true); + ModelManager.get().addItem(tag); + Sync.get().sync(); + callback(tag); + this.forceUpdate(); + }) + } + + onTagSelect = (tag) => { + var selectedTags = this.state.selectedTags; + var selected = selectedTags.includes(tag.uuid); + if(selected) { + // deselect + selectedTags.splice(selectedTags.indexOf(tag.uuid), 1); + } else { + // select + selectedTags.push(tag.uuid); + } + + this.setSelectedTags(selectedTags); + } + + setSelectedTags = (selectedTags) => { + this.selectedTags = selectedTags.slice(); + this.options.setSelectedTags(selectedTags); + this.setState({selectedTags: selectedTags}); + } + + isTagSelected(tag) { + return this.tags.indexOf(tag.uuid) !== -1; + } + + onManageTagEvent = (event, tag, renderBlock) => { + ItemActionManager.handleEvent(event, tag, () => { + if(event == ItemActionManager.DeleteEvent) { + this.tagsNeedReload = true; + this.forceUpdate(); + } + }, () => { + // afterConfirmCallback + // We want to show "Deleting.." on top of note cell after the user confirms the dialogue + renderBlock(); + }) + } + + onManageNoteEvent(event) { + ItemActionManager.handleEvent(event, this.note, () => { + this.getProp("onManageNoteEvent")(); + if(event == ItemActionManager.DeleteEvent) { + this.popToRoot(); + } else { + this.forceUpdate(); + } + }) + } + + onEditorSelect = (editor) => { + if(editor) { + ComponentManager.get().associateEditorWithNote(editor, this.note); + } else { + ComponentManager.get().clearEditorForNote(this.note); + } + + this.getProp("onEditorSelect") && this.getProp("onEditorSelect")(editor); + this.dismiss(); + } + + getEditors() { + return ModelManager.get().validItemsForContentType("SN|Component").filter((component) => { + return component.area == "editor-editor"; + }) + } + + clearTags = (close) => { + this.setSelectedTags([]); + if(close) { this.dismiss(); } + } + + openExternalEditorsLink() { + Linking.openURL("https://standardnotes.org/extensions"); + } + + render() { + var viewStyles = [GlobalStyles.styles().container]; + + if(this.state.lockContent) { + return (); + } + + if(this.tagsNeedReload) { + var tags = ModelManager.get().tags.slice(); + this.tags = tags; + this.tagsNeedReload = false; + } + + var pinAction = this.note.pinned ? "Unpin" : "Pin"; + let pinEvent = pinAction == "Pin" ? ItemActionManager.PinEvent : ItemActionManager.UnpinEvent; + + var archiveOption = this.note.archived ? "Unarchive" : "Archive"; + let archiveEvent = archiveOption == "Archive" ? ItemActionManager.ArchiveEvent : ItemActionManager.UnarchiveEvent; + + var lockOption = this.note.locked ? "Unlock" : "Lock"; + let lockEvent = lockOption == "Lock" ? ItemActionManager.LockEvent : ItemActionManager.UnlockEvent; + + let editors = this.getEditors(); + let selectedEditor = ComponentManager.get().editorForNote(this.note); + + return ( + + + + + + {this.onManageNoteEvent(pinEvent)}} + first={true} text={pinAction} + leftAlignIcon={true} + /> + + {this.onManageNoteEvent(archiveEvent)}} + text={archiveOption} + leftAlignIcon={true} + /> + + {this.onManageNoteEvent(lockEvent)}} + text={lockOption} + leftAlignIcon={true} + /> + + {this.onManageNoteEvent(ItemActionManager.ShareEvent)}} + text={"Share"} + leftAlignIcon={true} + /> + + {this.onManageNoteEvent(ItemActionManager.DeleteEvent)}} + text={"Delete"} + last={true} + leftAlignIcon={true} + /> + + + + {this.onEditorSelect(null)}} + /> + {editors.map((editor, i) => { + return ( + {this.onEditorSelect(editor)}} + text={editor.name} + key={editor.uuid} + first={i == 0} + selected={() => {return editor == selectedEditor}} + buttonCell={true} + /> + ) + })} + + {editors.length == 0 && + {this.openExternalEditorsLink()}} + text={"Get Editors →"} + first={true} + buttonCell={true} + /> + } + + + + + + + {this.presentNewTag()}} + visible={true} + iconTextComponent={} + /> + + + ); + } +} diff --git a/src/screens/Notes.js b/src/screens/Notes.js index fa0c1cb6..7df3d8fc 100644 --- a/src/screens/Notes.js +++ b/src/screens/Notes.js @@ -89,6 +89,7 @@ export default class Notes extends Abstract { } componentDidMount() { + super.componentDidMount(); this.props.navigation.setParams({ toggleDrawer: this.toggleDrawer }); } diff --git a/src/screens/Account.js b/src/screens/Settings.js similarity index 88% rename from src/screens/Account.js rename to src/screens/Settings.js index 3b0156d3..4684c358 100644 --- a/src/screens/Account.js +++ b/src/screens/Settings.js @@ -8,6 +8,7 @@ import AlertManager from '../lib/sfjs/alertManager' import Auth from '../lib/sfjs/authManager' import KeysManager from '../lib/keysManager' import UserPrefsManager from '../lib/userPrefsManager' +import OptionsState from "../OptionsState"; import SectionHeader from "../components/SectionHeader"; import ButtonCell from "../components/ButtonCell"; @@ -31,7 +32,7 @@ import GlobalStyles from "../Styles" var base64 = require('base-64'); var Mailer = require('NativeModules').RNMail; -export default class Account extends Abstract { +export default class Settings extends Abstract { static navigationOptions = ({ navigation, navigationOptions }) => { let templateOptions = { @@ -55,6 +56,13 @@ export default class Account extends Abstract { } }) + this.sortOptions = [ + {key: "created_at", label: "Date Added"}, + {key: "client_updated_at", label: "Date Modified"}, + {key: "title", label: "Title"}, + ]; + + this.options = ApplicationState.getOptions(); this.constructState({params: {}}); } @@ -88,13 +96,6 @@ export default class Account extends Abstract { this.loadSecurityStatus(); } - dismiss() { - /* - the `null` parameter is actually very important: https://reactnavigation.org/docs/en/navigation-prop.html#goback-close-the-active-screen-and-move-back - */ - this.props.navigation.goBack(null); - } - loadSecurityStatus() { var hasPasscode = KeysManager.get().hasOfflinePasscode(); var hasFingerprint = KeysManager.get().hasFingerprint(); @@ -107,8 +108,8 @@ export default class Account extends Abstract { Sync.get().removeEventHandler(this.syncEventHandler); } - componentWillMount() { - super.componentWillMount(); + componentWillFocus() { + super.componentWillFocus(); this.loadLastExportDate(); } @@ -471,6 +472,16 @@ export default class Account extends Abstract { } } + onSortChange = (key) => { + this.options.setSortBy(key); + this.forceUpdate(); + } + + onOptionSelect = (option) => { + this.options.setDisplayOptionKeyValue(option, !this.options.getDisplayOptionValue(option)); + this.forceUpdate(); + } + render() { if(this.state.lockContent) { return (); @@ -514,6 +525,55 @@ export default class Account extends Abstract { email={KeysManager.get().getUserEmail()} /> + + + + {this.sortOptions.map((option, i) => { + return ( + {this.onSortChange(option.key)}} + text={option.label} + key={option.key} + first={i == 0} + last={i == this.sortOptions.length - 1} + selected={() => {return option.key == this.options.sortBy}} + /> + ) + })} + + + + + + {this.onOptionSelect('archivedOnly')}} + text={"Show only archived notes"} + first={true} + selected={() => {return this.options.archivedOnly}} + /> + + {this.onOptionSelect('hidePreviews')}} + text={"Hide note previews"} + selected={() => {return this.options.hidePreviews}} + /> + + {this.onOptionSelect('hideTags')}} + text={"Hide note tags"} + selected={() => {return this.options.hideTags}} + /> + + {this.onOptionSelect('hideDates')}} + text={"Hide note dates"} + last={true} + selected={() => {return this.options.hideDates}} + /> + + + + { + if(this.handledDataLoad) { return; } + this.handledDataLoad = true; + this.tagsNeedReload = true; + this.forceUpdate(); + } + + if(Sync.get().initialDataLoaded()) { + handleInitialDataLoad(); + } + + this.syncEventHandler = Sync.get().addEventHandler((event, data) => { + if(event == "local-data-loaded") { + handleInitialDataLoad(); + } + + else if(event == "sync:completed") { + if(data.retrievedItems && _.find(data.retrievedItems, {content_type: "Tag"})) { + this.forceUpdate(); + } + } + }) + + this.setState({initialDataLoaded: true}); + } + + componentWillUnmount() { + super.componentWillUnmount(); + ApplicationState.get().removeStateObserver(this.stateObserver); + Sync.get().removeEventHandler(this.syncEventHandler); + } + + componentDidFocus() { + super.componentDidFocus(); + this.forceUpdate(); + } + + presentNewTag() { + this.props.navigation.navigate("NewTag", { + title: 'New Tag', + placeholder: "New tag name", + onSave: (text) => { + this.createTag(text, (tag) => { + if(this.note) { + // select this tag + this.onTagSelect(tag) + } + }); + } + }) + } + + createTag(text, callback) { + var tag = new SNTag({content: {title: text}}); + tag.initUUID().then(() => { + tag.setDirty(true); + ModelManager.get().addItem(tag); + Sync.get().sync(); + callback(tag); + this.forceUpdate(); + }) + } + + onTagSelect = (tag) => { + var selectedTags = [tag.uuid]; + this.setSelectedTags(selectedTags); + } + + setSelectedTags = (selectedTags) => { + this.selectedTags = selectedTags.slice(); + this.options.setSelectedTags(selectedTags); + this.setState({selectedTags: selectedTags}); + } + + isTagSelected(tag) { + return this.tags.indexOf(tag.uuid) !== -1; + } + + onManageTagEvent = (event, tag, renderBlock) => { + ItemActionManager.handleEvent(event, tag, () => { + if(event == ItemActionManager.DeleteEvent) { + this.tagsNeedReload = true; + this.forceUpdate(); + } + }, () => { + // afterConfirmCallback + // We want to show "Deleting.." on top of note cell after the user confirms the dialogue + renderBlock(); + }) + } + + presentSettings() { + this.props.navigation.navigate("Settings"); + } + + render() { + var viewStyles = [GlobalStyles.styles().container]; + + if(this.state.lockContent || !this.state.initialDataLoaded) { + return (); + } + + if(this.tagsNeedReload) { + var tags = ModelManager.get().tags.slice(); + tags.unshift({title: "All notes", key: "all", uuid: 100}) + this.tags = tags; + this.tagsNeedReload = false; + } + + return ( + + + + + + {this.note ? this.presentNewTag() : this.presentSettings()}} + visible={true} + iconTextComponent={} + /> + + + ); + } +} diff --git a/src/screens/index.js b/src/screens/index.js deleted file mode 100644 index 5bb0c22d..00000000 --- a/src/screens/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import Notes from './Notes' -import Compose from './Compose' -import Account from './Account' -import Authenticate from './Authenticate' -import Filter from './Filter' -import InputModal from './InputModal' -import Sync from '../lib/sfjs/syncManager' -import Webview from './Webview' - -export function registerScreens() { - Navigation.registerComponent('sn.Notes', () => Notes); - Navigation.registerComponent('sn.Compose', () => Compose); - Navigation.registerComponent('sn.Account', () => Account); - Navigation.registerComponent('sn.Filter', () => Filter); - Navigation.registerComponent('sn.InputModal', () => InputModal); - Navigation.registerComponent('sn.Authenticate', () => Authenticate); - Navigation.registerComponent('sn.Webview', () => Webview); -}