const React = require('react'); import { AppState as RNAppState, View, StyleSheet, NativeEventSubscription } from 'react-native'; import { stateUtils } from '@joplin/lib/reducer'; import { connect } from 'react-redux'; import NoteList from '../NoteList'; import Folder from '@joplin/lib/models/Folder'; import Tag from '@joplin/lib/models/Tag'; import Note from '@joplin/lib/models/Note'; import Setting from '@joplin/lib/models/Setting'; import { themeStyle } from '../global-style'; import { ScreenHeader } from '../ScreenHeader'; import { _ } from '@joplin/lib/locale'; import ActionButton from '../ActionButton'; const { dialogs } = require('../../utils/dialogs.js'); const DialogBox = require('react-native-dialogbox').default; const { BaseScreenComponent } = require('../base-screen'); const { BackButtonService } = require('../../services/back-button.js'); import { AppState } from '../../utils/types'; import { NoteEntity } from '@joplin/lib/services/database/types'; import { itemIsInTrash } from '@joplin/lib/services/trash'; const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids.js'); class NotesScreenComponent extends BaseScreenComponent { private onAppStateChangeSub_: NativeEventSubscription = null; public constructor() { super(); this.onAppStateChange_ = async () => { // Force an update to the notes list when app state changes const newProps = { ...this.props }; newProps.notesSource = ''; await this.refreshNotes(newProps); }; this.sortButton_press = async () => { const buttons = []; const sortNoteOptions = Setting.enumOptions('notes.sortOrder.field'); const makeCheckboxText = function(selected: boolean, sign: string, label: string) { const s = sign === 'tick' ? '✓' : '⬤'; return (selected ? `${s} ` : '') + label; }; for (const field in sortNoteOptions) { if (!sortNoteOptions.hasOwnProperty(field)) continue; buttons.push({ text: makeCheckboxText(Setting.value('notes.sortOrder.field') === field, 'bullet', sortNoteOptions[field]), id: { name: 'notes.sortOrder.field', value: field }, }); } buttons.push({ text: makeCheckboxText(Setting.value('notes.sortOrder.reverse'), 'tick', `[ ${Setting.settingMetadata('notes.sortOrder.reverse').label()} ]`), id: { name: 'notes.sortOrder.reverse', value: !Setting.value('notes.sortOrder.reverse') }, }); buttons.push({ text: makeCheckboxText(Setting.value('uncompletedTodosOnTop'), 'tick', `[ ${Setting.settingMetadata('uncompletedTodosOnTop').label()} ]`), id: { name: 'uncompletedTodosOnTop', value: !Setting.value('uncompletedTodosOnTop') }, }); buttons.push({ text: makeCheckboxText(Setting.value('showCompletedTodos'), 'tick', `[ ${Setting.settingMetadata('showCompletedTodos').label()} ]`), id: { name: 'showCompletedTodos', value: !Setting.value('showCompletedTodos') }, }); const r = await dialogs.pop(this, Setting.settingMetadata('notes.sortOrder.field').label(), buttons); if (!r) return; Setting.setValue(r.name, r.value); }; this.backHandler = () => { if (this.dialogbox && this.dialogbox.state && this.dialogbox.state.isVisible) { this.dialogbox.close(); return true; } return false; }; } public styles() { if (!this.styles_) this.styles_ = {}; const themeId = this.props.themeId; const cacheKey = themeId; if (this.styles_[cacheKey]) return this.styles_[cacheKey]; this.styles_ = {}; const styles = { noteList: { flex: 1, }, }; this.styles_[cacheKey] = StyleSheet.create(styles); return this.styles_[cacheKey]; } public async componentDidMount() { BackButtonService.addHandler(this.backHandler); await this.refreshNotes(); this.onAppStateChangeSub_ = RNAppState.addEventListener('change', this.onAppStateChange_); } public async componentWillUnmount() { if (this.onAppStateChangeSub_) this.onAppStateChangeSub_.remove(); BackButtonService.removeHandler(this.backHandler); } public async componentDidUpdate(prevProps: any) { if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType || prevProps.uncompletedTodosOnTop !== this.props.uncompletedTodosOnTop || prevProps.showCompletedTodos !== this.props.showCompletedTodos) { await this.refreshNotes(this.props); } } public async refreshNotes(props: any = null) { if (props === null) props = this.props; const options = { order: props.notesOrder, uncompletedTodosOnTop: props.uncompletedTodosOnTop, showCompletedTodos: props.showCompletedTodos, caseInsensitive: true, }; const parent = this.parentItem(props); if (!parent) return; const source = JSON.stringify({ options: options, parentId: parent.id, }); if (source === props.notesSource) return; let notes: NoteEntity[] = []; if (props.notesParentType === 'Folder') { notes = await Note.previews(props.selectedFolderId, options); } else if (props.notesParentType === 'Tag') { notes = await Tag.notes(props.selectedTagId, options); } else if (props.notesParentType === 'SmartFilter') { notes = await Note.previews(null, options); } this.props.dispatch({ type: 'NOTE_UPDATE_ALL', notes: notes, notesSource: source, }); } public newNoteNavigate = async (folderId: string, isTodo: boolean) => { try { const newNote = await Note.save({ parent_id: folderId, is_todo: isTodo ? 1 : 0, }, { provisional: true }); this.props.dispatch({ type: 'NAV_GO', routeName: 'Note', noteId: newNote.id, }); } catch (error) { alert(_('Cannot create a new note: %s', error.message)); } }; public parentItem(props: any = null) { if (!props) props = this.props; let output = null; if (props.notesParentType === 'Folder') { output = Folder.byId(props.folders, props.selectedFolderId); } else if (props.notesParentType === 'Tag') { output = Tag.byId(props.tags, props.selectedTagId); } else if (props.notesParentType === 'SmartFilter') { output = { id: this.props.selectedSmartFilterId, title: _('All notes') }; } else { return null; // throw new Error('Invalid parent type: ' + props.notesParentType); } return output; } public folderPickerOptions() { const options = { enabled: this.props.noteSelectionEnabled, mustSelect: true, }; if (this.folderPickerOptions_ && options.enabled === this.folderPickerOptions_.enabled) return this.folderPickerOptions_; this.folderPickerOptions_ = options; return this.folderPickerOptions_; } public render() { const parent = this.parentItem(); const theme = themeStyle(this.props.themeId); const rootStyle = { flex: 1, backgroundColor: theme.backgroundColor, }; if (!this.props.visible) { rootStyle.flex = 0.001; // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it } const title = parent ? parent.title : null; if (!parent) { return ( ); } const icon = Folder.unserializeIcon(parent.icon); const iconString = icon ? `${icon.emoji} ` : ''; let buttonFolderId = this.props.selectedFolderId !== Folder.conflictFolderId() ? this.props.selectedFolderId : null; if (!buttonFolderId) buttonFolderId = this.props.activeFolderId; const isAllNotes = this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID; // Usually, when showing all notes, activeFolderId/selectedFolderId is set to the last // active folder. // If the app starts showing all notes, activeFolderId/selectedFolderId are // empty or null. As such, we need a special case to show the buttons: const addFolderNoteButtons = !!buttonFolderId || isAllNotes; const thisComp = this; const makeActionButtonComp = () => { if (this.props.notesParentType === 'Folder' && itemIsInTrash(parent)) return null; const getTargetFolderId = async () => { if (!buttonFolderId && isAllNotes) { return (await Folder.defaultFolder()).id; } return buttonFolderId; }; if (addFolderNoteButtons && this.props.folders.length > 0) { const buttons = []; buttons.push({ label: _('New to-do'), onPress: async () => { const folderId = await getTargetFolderId(); const isTodo = true; void this.newNoteNavigate(folderId, isTodo); }, color: '#9b59b6', icon: 'checkbox-outline', }); buttons.push({ label: _('New note'), onPress: async () => { const folderId = await getTargetFolderId(); const isTodo = false; void this.newNoteNavigate(folderId, isTodo); }, color: '#9b59b6', icon: 'document', }); return ; } return null; }; const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : makeActionButtonComp(); // Ensure that screen readers can't focus the notes list when it isn't visible. // accessibilityElementsHidden is used on iOS and importantForAccessibility is used // on Android. const accessibilityHidden = !this.props.visible; return ( {actionButtonComp} { this.dialogbox = dialogbox; }} /> ); } } const NotesScreen = connect((state: AppState) => { return { folders: state.folders, tags: state.tags, activeFolderId: state.settings.activeFolderId, selectedFolderId: state.selectedFolderId, selectedNoteIds: state.selectedNoteIds, selectedTagId: state.selectedTagId, selectedSmartFilterId: state.selectedSmartFilterId, notesParentType: state.notesParentType, notes: state.notes, notesSource: state.notesSource, uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop, showCompletedTodos: state.settings.showCompletedTodos, themeId: state.settings.theme, noteSelectionEnabled: state.noteSelectionEnabled, notesOrder: stateUtils.notesOrder(state.settings), }; })(NotesScreenComponent as any); export default NotesScreen as any;