2020-11-07 17:59:37 +02:00
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue' ;
import uuid from '@joplin/lib/uuid' ;
import Setting from '@joplin/lib/models/Setting' ;
import shim from '@joplin/lib/shim' ;
2021-07-13 20:13:13 +02:00
import UndoRedoService from '@joplin/lib/services/UndoRedoService' ;
2020-11-05 18:58:23 +02:00
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer' ;
import checkPermissions from '../../utils/checkPermissions' ;
2022-08-08 17:00:14 +02:00
import NoteEditor from '../NoteEditor/NoteEditor' ;
import { ChangeEvent , UndoRedoDepthChangeEvent } from '../NoteEditor/types' ;
2020-10-16 17:26:19 +02:00
const FileViewer = require ( 'react-native-file-viewer' ) . default ;
2019-07-30 09:35:42 +02:00
const React = require ( 'react' ) ;
2022-09-30 12:46:26 +02:00
const { Platform , Keyboard , View , TextInput , StyleSheet , Linking , Image , Share , PermissionsAndroid } = require ( 'react-native' ) ;
2018-03-09 22:59:12 +02:00
const { connect } = require ( 'react-redux' ) ;
2020-11-07 17:59:37 +02:00
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
2021-01-22 19:41:11 +02:00
import Note from '@joplin/lib/models/Note' ;
import BaseItem from '@joplin/lib/models/BaseItem' ;
import Resource from '@joplin/lib/models/Resource' ;
import Folder from '@joplin/lib/models/Folder' ;
2020-10-16 17:26:19 +02:00
const Clipboard = require ( '@react-native-community/clipboard' ) . default ;
2019-07-12 19:32:08 +02:00
const md5 = require ( 'md5' ) ;
2020-11-05 18:58:23 +02:00
const { BackButtonService } = require ( '../../services/back-button.js' ) ;
2021-01-23 17:51:19 +02:00
import NavService from '@joplin/lib/services/NavService' ;
2021-01-22 19:41:11 +02:00
import BaseModel from '@joplin/lib/BaseModel' ;
2023-01-08 14:22:41 +02:00
import ActionButton from '../ActionButton' ;
2020-11-07 17:59:37 +02:00
const { fileExtension , safeFileExtension } = require ( '@joplin/lib/path-utils' ) ;
const mimeUtils = require ( '@joplin/lib/mime-utils.js' ) . mime ;
2022-08-27 14:36:59 +02:00
import ScreenHeader from '../ScreenHeader' ;
2020-11-05 18:58:23 +02:00
const NoteTagsDialog = require ( './NoteTagsDialog' ) ;
2021-01-22 19:41:11 +02:00
import time from '@joplin/lib/time' ;
2022-09-30 12:46:26 +02:00
const { Checkbox } = require ( '../checkbox.js' ) ;
2020-11-07 17:59:37 +02:00
const { _ } = require ( '@joplin/lib/locale' ) ;
2021-01-29 20:45:11 +02:00
import { reg } from '@joplin/lib/registry' ;
2021-01-23 17:51:19 +02:00
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher' ;
2020-11-05 18:58:23 +02:00
const { BaseScreenComponent } = require ( '../base-screen.js' ) ;
2022-09-30 12:46:26 +02:00
const { themeStyle , editorFont } = require ( '../global-style.js' ) ;
2020-11-05 18:58:23 +02:00
const { dialogs } = require ( '../../utils/dialogs.js' ) ;
2020-06-13 17:20:59 +02:00
const DialogBox = require ( 'react-native-dialogbox' ) . default ;
2018-03-09 22:59:12 +02:00
const ImageResizer = require ( 'react-native-image-resizer' ) . default ;
2020-11-07 17:59:37 +02:00
const shared = require ( '@joplin/lib/components/shared/note-screen-shared.js' ) ;
2022-12-22 14:38:11 +02:00
import { ImagePickerResponse , launchImageLibrary } from 'react-native-image-picker' ;
2021-01-22 19:41:11 +02:00
import SelectDateTimeDialog from '../SelectDateTimeDialog' ;
import ShareExtension from '../../utils/ShareExtension.js' ;
import CameraView from '../CameraView' ;
2021-07-13 20:13:13 +02:00
import { NoteEntity } from '@joplin/lib/services/database/types' ;
2021-11-29 12:37:06 +02:00
import Logger from '@joplin/lib/Logger' ;
2020-11-07 17:59:37 +02:00
const urlUtils = require ( '@joplin/lib/urlUtils' ) ;
2017-05-12 22:23:54 +02:00
2020-11-12 21:13:28 +02:00
const emptyArray : any [ ] = [ ] ;
2020-10-16 17:26:19 +02:00
2021-11-29 12:37:06 +02:00
const logger = Logger . create ( 'screens/Note' ) ;
2017-07-14 20:49:14 +02:00
class NoteScreenComponent extends BaseScreenComponent {
2022-09-30 12:46:26 +02:00
static navigationOptions ( ) : any {
2017-05-16 21:57:09 +02:00
return { header : null } ;
}
2017-05-12 22:23:54 +02:00
2022-09-30 12:46:26 +02:00
constructor ( ) {
2017-05-12 22:23:54 +02:00
super ( ) ;
2017-07-10 23:34:26 +02:00
this . state = {
note : Note.new ( ) ,
2018-03-09 22:59:12 +02:00
mode : 'view' ,
2017-07-13 23:50:21 +02:00
folder : null ,
2017-07-15 01:12:32 +02:00
lastSavedNote : null ,
2017-07-24 23:52:30 +02:00
isLoading : true ,
2017-07-31 19:56:14 +02:00
titleTextInputHeight : 20 ,
2017-09-10 18:56:27 +02:00
alarmDialogShown : false ,
2019-07-30 09:35:42 +02:00
heightBumpView : 0 ,
2018-03-18 01:00:01 +02:00
noteTagDialogShown : false ,
2018-07-20 11:04:25 +02:00
fromShare : false ,
2018-10-13 11:32:44 +02:00
showCamera : false ,
2019-03-08 19:14:17 +02:00
noteResources : { } ,
2018-12-27 23:49:19 +02:00
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
// no visible text). It will only appear when tapping it or doing certain action like selecting text on the webview. The bug started to
// appear one day and did not go away - reverting to an old RN version did not help, undoing all
// the commits till a working version did not help. The bug also does not happen in the simulator which makes it hard to fix.
// Eventually, a way that "worked" is to add a 1px margin on top of the text input just after the webview has loaded, then removing this
2019-07-30 09:35:42 +02:00
// margin. This forces RN to update the text input and to display it. Maybe that hack can be removed once RN is upgraded.
2018-12-27 23:49:19 +02:00
// See https://github.com/laurent22/joplin/issues/1057
HACK_webviewLoadingState : 0 ,
2017-07-16 23:17:22 +02:00
2020-06-13 17:20:18 +02:00
undoRedoButtonState : {
canUndo : false ,
canRedo : false ,
} ,
} ;
2020-05-20 18:46:01 +02:00
2020-06-10 01:30:32 +02:00
this . saveActionQueues_ = { } ;
2020-11-05 18:58:23 +02:00
// this.markdownEditorRef = React.createRef(); // For focusing the Markdown editor
2020-03-25 12:50:45 +02:00
2019-06-27 00:21:12 +02:00
this . doFocusUpdate_ = false ;
2017-07-16 23:17:22 +02:00
this . saveButtonHasBeenShown_ = false ;
2017-07-15 01:12:32 +02:00
2017-08-01 20:53:50 +02:00
this . styles_ = { } ;
2021-07-13 20:13:13 +02:00
this . editorRef = React . createRef ( ) ;
2018-02-22 00:08:34 +02:00
const saveDialog = async ( ) = > {
2017-08-24 20:10:03 +02:00
if ( this . isModified ( ) ) {
2020-06-13 17:20:59 +02:00
const buttonId = await dialogs . pop ( this , _ ( 'This note has been modified:' ) , [ { text : _ ( 'Save changes' ) , id : 'save' } , { text : _ ( 'Discard changes' ) , id : 'discard' } , { text : _ ( 'Cancel' ) , id : 'cancel' } ] ) ;
2017-07-31 21:02:21 +02:00
2022-07-23 09:31:32 +02:00
if ( buttonId === 'cancel' ) return true ;
if ( buttonId === 'save' ) await this . saveNoteButton_press ( ) ;
2017-08-24 20:10:03 +02:00
}
2017-07-31 21:02:21 +02:00
2018-02-22 00:08:34 +02:00
return false ;
2019-07-30 09:35:42 +02:00
} ;
2018-02-22 00:08:34 +02:00
this . navHandler = async ( ) = > {
return await saveDialog ( ) ;
2019-07-30 09:35:42 +02:00
} ;
2018-02-22 00:08:34 +02:00
this . backHandler = async ( ) = > {
2020-03-24 00:34:13 +02:00
if ( this . isModified ( ) ) {
await this . saveNoteButton_press ( ) ;
}
2018-02-22 00:08:34 +02:00
2020-03-06 20:49:30 +02:00
const isProvisionalNote = this . props . provisionalNoteIds . includes ( this . props . noteId ) ;
if ( isProvisionalNote ) {
2017-08-24 20:10:03 +02:00
return false ;
}
2017-07-31 21:02:21 +02:00
2022-07-23 09:31:32 +02:00
if ( this . state . mode === 'edit' ) {
2019-07-30 09:35:42 +02:00
Keyboard . dismiss ( ) ;
2017-08-24 20:10:03 +02:00
this . setState ( {
note : Object.assign ( { } , this . state . lastSavedNote ) ,
2018-03-09 22:59:12 +02:00
mode : 'view' ,
2017-08-24 20:10:03 +02:00
} ) ;
2020-06-13 17:20:18 +02:00
await this . undoRedoService_ . reset ( ) ;
2017-08-24 20:10:03 +02:00
return true ;
}
2020-06-04 19:40:44 +02:00
if ( this . state . fromShare ) {
// effectively the same as NAV_BACK but NAV_BACK causes undesired behaviour in this case:
// - share to Joplin from some other app
// - open Joplin and open any note
// - go back -- with NAV_BACK this causes the app to exit rather than just showing notes
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Notes' ,
folderId : this.state.note.parent_id ,
} ) ;
return true ;
}
2017-08-24 20:10:03 +02:00
return false ;
} ;
2018-03-18 01:00:01 +02:00
this . noteTagDialog_closeRequested = ( ) = > {
this . setState ( { noteTagDialogShown : false } ) ;
2019-07-30 09:35:42 +02:00
} ;
2018-05-02 16:13:20 +02:00
2020-11-12 21:13:28 +02:00
this . onJoplinLinkClick_ = async ( msg : string ) = > {
2018-05-02 16:13:20 +02:00
try {
2022-07-12 12:51:33 +02:00
const resourceUrlInfo = urlUtils . parseResourceUrl ( msg ) ;
if ( resourceUrlInfo ) {
2019-09-09 19:16:00 +02:00
const itemId = resourceUrlInfo . itemId ;
2018-05-02 16:13:20 +02:00
const item = await BaseItem . loadItemById ( itemId ) ;
if ( ! item ) throw new Error ( _ ( 'No item with ID %s' , itemId ) ) ;
if ( item . type_ === BaseModel . TYPE_NOTE ) {
// Easier to just go back, then go to the note since
2020-02-19 02:09:19 +02:00
// the Note screen doesn't handle reloading a different note
2018-05-02 16:13:20 +02:00
this . props . dispatch ( {
type : 'NAV_BACK' ,
} ) ;
2020-10-09 19:35:46 +02:00
shim . setTimeout ( ( ) = > {
2018-05-02 16:13:20 +02:00
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Note' ,
noteId : item.id ,
2019-09-09 19:16:00 +02:00
noteHash : resourceUrlInfo.hash ,
2018-05-02 16:13:20 +02:00
} ) ;
} , 5 ) ;
2018-06-30 20:45:21 +02:00
} else if ( item . type_ === BaseModel . TYPE_RESOURCE ) {
2018-11-13 02:45:08 +02:00
if ( ! ( await Resource . isReady ( item ) ) ) throw new Error ( _ ( 'This attachment is not downloaded or not decrypted yet.' ) ) ;
2018-06-30 20:45:21 +02:00
const resourcePath = Resource . fullPath ( item ) ;
2021-11-29 12:37:06 +02:00
logger . info ( ` Opening resource: ${ resourcePath } ` ) ;
2018-06-30 20:45:21 +02:00
await FileViewer . open ( resourcePath ) ;
2018-05-02 16:13:20 +02:00
} else {
throw new Error ( _ ( 'The Joplin mobile app does not currently support this type of link: %s' , BaseModel . modelTypeToName ( item . type_ ) ) ) ;
}
} else {
2019-06-14 09:11:15 +02:00
if ( msg . indexOf ( 'file://' ) === 0 ) {
throw new Error ( _ ( 'Links with protocol "%s" are not supported' , 'file://' ) ) ;
} else {
2022-09-30 12:46:26 +02:00
Linking . openURL ( msg ) ;
2019-06-14 09:11:15 +02:00
}
2018-05-02 16:13:20 +02:00
}
} catch ( error ) {
2020-06-13 17:20:59 +02:00
dialogs . error ( this , error . message ) ;
2018-05-02 16:13:20 +02:00
}
2019-07-30 09:35:42 +02:00
} ;
2018-10-13 00:25:11 +02:00
2020-11-12 21:13:28 +02:00
this . refreshResource = async ( resource : any , noteBody : string = null ) = > {
2019-10-12 20:49:10 +02:00
if ( noteBody === null && this . state . note && this . state . note . body ) noteBody = this . state . note . body ;
if ( noteBody === null ) return ;
const resourceIds = await Note . linkedResourceIds ( noteBody ) ;
if ( resourceIds . indexOf ( resource . id ) >= 0 ) {
2019-05-22 16:56:07 +02:00
shared . clearResourceCache ( ) ;
2019-10-12 20:49:10 +02:00
const attachedResources = await shared . attachedResources ( noteBody ) ;
2020-10-16 17:26:19 +02:00
this . setState ( { noteResources : attachedResources } ) ;
2018-10-13 00:25:11 +02:00
}
2019-07-30 09:35:42 +02:00
} ;
2018-10-13 11:32:44 +02:00
2018-12-07 02:07:10 +02:00
this . takePhoto_onPress = this . takePhoto_onPress . bind ( this ) ;
2018-10-13 11:32:44 +02:00
this . cameraView_onPhoto = this . cameraView_onPhoto . bind ( this ) ;
this . cameraView_onCancel = this . cameraView_onCancel . bind ( this ) ;
2019-07-11 19:23:29 +02:00
this . properties_onPress = this . properties_onPress . bind ( this ) ;
2020-03-13 21:58:17 +02:00
this . showOnMap_onPress = this . showOnMap_onPress . bind ( this ) ;
2019-05-22 16:56:07 +02:00
this . onMarkForDownload = this . onMarkForDownload . bind ( this ) ;
2019-07-11 19:23:29 +02:00
this . sideMenuOptions = this . sideMenuOptions . bind ( this ) ;
2019-07-12 19:32:08 +02:00
this . folderPickerOptions_valueChanged = this . folderPickerOptions_valueChanged . bind ( this ) ;
this . saveNoteButton_press = this . saveNoteButton_press . bind ( this ) ;
2019-07-12 20:36:12 +02:00
this . onAlarmDialogAccept = this . onAlarmDialogAccept . bind ( this ) ;
this . onAlarmDialogReject = this . onAlarmDialogReject . bind ( this ) ;
this . todoCheckbox_change = this . todoCheckbox_change . bind ( this ) ;
this . title_changeText = this . title_changeText . bind ( this ) ;
2020-06-13 17:20:18 +02:00
this . undoRedoService_stackChange = this . undoRedoService_stackChange . bind ( this ) ;
this . screenHeader_undoButtonPress = this . screenHeader_undoButtonPress . bind ( this ) ;
this . screenHeader_redoButtonPress = this . screenHeader_redoButtonPress . bind ( this ) ;
this . body_selectionChange = this . body_selectionChange . bind ( this ) ;
2020-10-16 17:26:19 +02:00
this . onBodyViewerLoadEnd = this . onBodyViewerLoadEnd . bind ( this ) ;
this . onBodyViewerCheckboxChange = this . onBodyViewerCheckboxChange . bind ( this ) ;
2021-07-13 20:13:13 +02:00
this . onBodyChange = this . onBodyChange . bind ( this ) ;
this . onUndoRedoDepthChange = this . onUndoRedoDepthChange . bind ( this ) ;
}
private useEditorBeta ( ) : boolean {
return this . props . useEditorBeta ;
}
private onBodyChange ( event : ChangeEvent ) {
shared . noteComponent_change ( this , 'body' , event . value ) ;
this . scheduleSave ( ) ;
}
private onUndoRedoDepthChange ( event : UndoRedoDepthChangeEvent ) {
if ( this . useEditorBeta ( ) ) {
this . setState ( { undoRedoButtonState : {
canUndo : ! ! event . undoDepth ,
canRedo : ! ! event . redoDepth ,
} } ) ;
}
2020-06-13 17:20:18 +02:00
}
2021-07-13 20:13:13 +02:00
private undoRedoService_stackChange() {
if ( ! this . useEditorBeta ( ) ) {
this . setState ( { undoRedoButtonState : {
canUndo : this.undoRedoService_.canUndo ,
canRedo : this.undoRedoService_.canRedo ,
} } ) ;
}
2020-06-13 17:20:18 +02:00
}
2021-07-13 20:13:13 +02:00
private async undoRedo ( type : string ) {
2020-06-13 17:20:18 +02:00
const undoState = await this . undoRedoService_ [ type ] ( this . undoState ( ) ) ;
if ( ! undoState ) return ;
2020-11-12 21:13:28 +02:00
this . setState ( ( state : any ) = > {
2020-06-13 17:20:18 +02:00
const newNote = Object . assign ( { } , state . note ) ;
newNote . body = undoState . body ;
return {
note : newNote ,
} ;
} ) ;
}
2022-09-30 12:46:26 +02:00
screenHeader_undoButtonPress() {
2021-07-13 20:13:13 +02:00
if ( this . useEditorBeta ( ) ) {
this . editorRef . current . undo ( ) ;
} else {
void this . undoRedo ( 'undo' ) ;
}
2020-06-13 17:20:18 +02:00
}
2022-09-30 12:46:26 +02:00
screenHeader_redoButtonPress() {
2021-07-13 20:13:13 +02:00
if ( this . useEditorBeta ( ) ) {
this . editorRef . current . redo ( ) ;
} else {
void this . undoRedo ( 'redo' ) ;
}
}
2022-09-30 12:46:26 +02:00
undoState ( noteBody : string = null ) {
2021-07-13 20:13:13 +02:00
return {
body : noteBody === null ? this . state.note.body : noteBody ,
} ;
2017-07-15 01:12:32 +02:00
}
2022-09-30 12:46:26 +02:00
styles() {
2020-09-15 15:01:07 +02:00
const themeId = this . props . themeId ;
2017-08-01 20:53:50 +02:00
const theme = themeStyle ( themeId ) ;
2019-07-12 20:36:12 +02:00
const cacheKey = [ themeId , this . state . titleTextInputHeight , this . state . HACK_webviewLoadingState ] . join ( '_' ) ;
if ( this . styles_ [ cacheKey ] ) return this . styles_ [ cacheKey ] ;
2017-08-01 20:53:50 +02:00
this . styles_ = { } ;
2020-03-25 12:50:45 +02:00
// TODO: Clean up these style names and nesting
2020-11-12 21:13:28 +02:00
const styles : any = {
2020-06-20 12:14:01 +02:00
screen : {
flex : 1 ,
backgroundColor : theme.backgroundColor ,
} ,
2017-08-01 20:53:50 +02:00
bodyTextInput : {
flex : 1 ,
paddingLeft : theme.marginLeft ,
paddingRight : theme.marginRight ,
2020-04-21 00:31:21 +02:00
2020-04-08 02:22:18 +02:00
// Add extra space to allow scrolling past end of document, and also to fix this:
// https://github.com/laurent22/joplin/issues/1437
2020-04-21 00:31:21 +02:00
// 2020-04-20: removed bottom padding because it doesn't work properly in Android
// Instead of being inside the scrollable area, the padding is outside thus
// restricting the view.
// See https://github.com/laurent22/joplin/issues/3041#issuecomment-616267739
// paddingBottom: Math.round(dimensions.height / 4),
2018-03-09 22:59:12 +02:00
textAlignVertical : 'top' ,
2017-08-01 20:53:50 +02:00
color : theme.color ,
backgroundColor : theme.backgroundColor ,
2023-01-07 19:47:52 +02:00
fontSize : this.props.editorFontSize ,
2019-09-17 22:32:00 +02:00
fontFamily : editorFont ( this . props . editorFont ) ,
2017-08-01 20:53:50 +02:00
} ,
noteBodyViewer : {
flex : 1 ,
paddingLeft : theme.marginLeft ,
paddingRight : theme.marginRight ,
2020-03-25 12:50:45 +02:00
} ,
2019-07-12 20:36:12 +02:00
checkbox : {
color : theme.color ,
paddingRight : 10 ,
paddingLeft : theme.marginLeft ,
paddingTop : 10 , // Added for iOS (Not needed for Android??)
paddingBottom : 10 , // Added for iOS (Not needed for Android??)
} ,
2020-03-25 12:50:45 +02:00
markdownButtons : {
borderColor : theme.dividerColor ,
2020-06-10 23:08:59 +02:00
color : theme.urlColor ,
2020-03-25 12:50:45 +02:00
} ,
2017-08-01 20:53:50 +02:00
} ;
2020-10-16 17:26:19 +02:00
styles . noteBodyViewerPreview = {
. . . styles . noteBodyViewer ,
borderTopColor : theme.dividerColor ,
borderTopWidth : 1 ,
borderBottomColor : theme.dividerColor ,
borderBottomWidth : 1 ,
} ;
2017-08-01 20:53:50 +02:00
styles . titleContainer = {
flex : 0 ,
2018-03-09 22:59:12 +02:00
flexDirection : 'row' ,
2017-08-01 20:53:50 +02:00
paddingLeft : theme.marginLeft ,
paddingRight : theme.marginRight ,
borderBottomColor : theme.dividerColor ,
borderBottomWidth : 1 ,
} ;
styles . titleContainerTodo = Object . assign ( { } , styles . titleContainer ) ;
2017-08-21 20:32:43 +02:00
styles . titleContainerTodo . paddingLeft = 0 ;
2017-08-01 20:53:50 +02:00
2019-07-12 20:36:12 +02:00
styles . titleTextInput = {
flex : 1 ,
marginTop : 0 ,
paddingLeft : 0 ,
color : theme.color ,
backgroundColor : theme.backgroundColor ,
fontWeight : 'bold' ,
fontSize : theme.fontSize ,
paddingTop : 10 , // Added for iOS (Not needed for Android??)
paddingBottom : 10 , // Added for iOS (Not needed for Android??)
} ;
if ( this . state . HACK_webviewLoadingState === 1 ) styles . titleTextInput . marginTop = 1 ;
this . styles_ [ cacheKey ] = StyleSheet . create ( styles ) ;
return this . styles_ [ cacheKey ] ;
2017-08-01 20:53:50 +02:00
}
2022-09-30 12:46:26 +02:00
isModified() {
2017-11-05 02:49:23 +02:00
return shared . isModified ( this ) ;
2017-05-12 22:23:54 +02:00
}
2022-09-30 12:46:26 +02:00
async requestGeoLocationPermissions() {
2020-10-16 17:26:19 +02:00
if ( ! Setting . value ( 'trackLocation' ) ) return ;
const response = await checkPermissions ( PermissionsAndroid . PERMISSIONS . ACCESS_FINE_LOCATION , {
message : _ ( 'In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.' ) ,
title : _ ( 'Permission needed' ) ,
} ) ;
// If the user simply pressed "Deny", we don't automatically switch it off because they might accept
// once we show the rationale again on second try. If they press "Never again" however we switch it off.
// https://github.com/zoontek/react-native-permissions/issues/385#issuecomment-563132396
if ( response === PermissionsAndroid . RESULTS . NEVER_ASK_AGAIN ) {
reg . logger ( ) . info ( 'Geo-location tracking has been automatically disabled' ) ;
Setting . setValue ( 'trackLocation' , false ) ;
}
}
2022-09-30 12:46:26 +02:00
async componentDidMount() {
2017-08-24 20:10:03 +02:00
BackButtonService . addHandler ( this . backHandler ) ;
2018-02-22 00:08:34 +02:00
NavService . addHandler ( this . navHandler ) ;
2017-07-15 01:12:32 +02:00
2019-05-22 16:56:07 +02:00
shared . clearResourceCache ( ) ;
shared . installResourceHandling ( this . refreshResource ) ;
2018-10-13 00:25:11 +02:00
2017-11-05 02:49:23 +02:00
await shared . initState ( this ) ;
2017-07-23 16:11:44 +02:00
2020-06-13 17:20:18 +02:00
this . undoRedoService_ = new UndoRedoService ( ) ;
this . undoRedoService_ . on ( 'stackChange' , this . undoRedoService_stackChange ) ;
2019-05-22 16:56:07 +02:00
if ( this . state . note && this . state . note . body && Setting . value ( 'sync.resourceDownloadMode' ) === 'auto' ) {
const resourceIds = await Note . linkedResourceIds ( this . state . note . body ) ;
await ResourceFetcher . instance ( ) . markForDownload ( resourceIds ) ;
}
2020-10-16 17:26:19 +02:00
// Although it is async, we don't wait for the answer so that if permission
// has already been granted, it doesn't slow down opening the note. If it hasn't
// been granted, the popup will open anyway.
2020-11-25 16:40:25 +02:00
void this . requestGeoLocationPermissions ( ) ;
2017-07-13 23:50:21 +02:00
}
2022-09-30 12:46:26 +02:00
onMarkForDownload ( event : any ) {
2021-01-22 19:41:11 +02:00
void ResourceFetcher . instance ( ) . markForDownload ( event . resourceId ) ;
2019-05-22 16:56:07 +02:00
}
2022-09-30 12:46:26 +02:00
componentDidUpdate ( prevProps : any ) {
2019-06-27 00:21:12 +02:00
if ( this . doFocusUpdate_ ) {
this . doFocusUpdate_ = false ;
this . focusUpdate ( ) ;
}
2019-07-21 15:11:30 +02:00
if ( prevProps . showSideMenu !== this . props . showSideMenu && this . props . showSideMenu ) {
this . props . dispatch ( {
type : 'NOTE_SIDE_MENU_OPTIONS_SET' ,
options : this.sideMenuOptions ( ) ,
} ) ;
}
2019-06-27 00:21:12 +02:00
}
2022-09-30 12:46:26 +02:00
componentWillUnmount() {
2017-08-24 20:10:03 +02:00
BackButtonService . removeHandler ( this . backHandler ) ;
2018-02-22 00:08:34 +02:00
NavService . removeHandler ( this . navHandler ) ;
2018-07-20 11:04:25 +02:00
2019-05-22 16:56:07 +02:00
shared . uninstallResourceHandling ( this . refreshResource ) ;
2018-10-13 00:25:11 +02:00
2020-06-04 19:40:44 +02:00
if ( this . state . fromShare ) {
ShareExtension . close ( ) ;
}
2020-06-10 01:30:32 +02:00
this . saveActionQueue ( this . state . note . id ) . processAllNow ( ) ;
2020-06-13 17:20:18 +02:00
2020-10-08 12:49:39 +02:00
// It cannot theoretically be undefined, since componentDidMount should always be called before
// componentWillUnmount, but with React Native the impossible often becomes possible.
if ( this . undoRedoService_ ) this . undoRedoService_ . off ( 'stackChange' , this . undoRedoService_stackChange ) ;
2017-07-13 20:47:31 +02:00
}
2022-09-30 12:46:26 +02:00
title_changeText ( text : string ) {
2018-03-09 22:59:12 +02:00
shared . noteComponent_change ( this , 'title' , text ) ;
2018-09-29 16:57:18 +02:00
this . setState ( { newAndNoTitleChangeNoteId : null } ) ;
2019-06-27 00:26:26 +02:00
this . scheduleSave ( ) ;
2017-05-12 22:23:54 +02:00
}
2022-09-30 12:46:26 +02:00
body_changeText ( text : string ) {
2020-06-13 17:20:18 +02:00
if ( ! this . undoRedoService_ . canUndo ) {
this . undoRedoService_ . push ( this . undoState ( ) ) ;
} else {
this . undoRedoService_ . schedulePush ( this . undoState ( ) ) ;
}
2021-07-13 20:13:13 +02:00
2018-03-09 22:59:12 +02:00
shared . noteComponent_change ( this , 'body' , text ) ;
2019-06-27 00:26:26 +02:00
this . scheduleSave ( ) ;
}
2022-09-30 12:46:26 +02:00
body_selectionChange ( event : any ) {
2022-04-11 12:56:45 +02:00
if ( this . useEditorBeta ( ) ) {
this . selection = event . selection ;
} else {
this . selection = event . nativeEvent . selection ;
}
2020-06-13 17:20:18 +02:00
}
2022-09-30 12:46:26 +02:00
makeSaveAction() {
2020-06-10 01:30:32 +02:00
return async ( ) = > {
return shared . saveNoteButton_press ( this ) ;
} ;
}
2022-09-30 12:46:26 +02:00
saveActionQueue ( noteId : string ) {
2020-06-10 01:30:32 +02:00
if ( ! this . saveActionQueues_ [ noteId ] ) {
this . saveActionQueues_ [ noteId ] = new AsyncActionQueue ( 500 ) ;
2019-06-27 00:26:26 +02:00
}
2020-06-10 01:30:32 +02:00
return this . saveActionQueues_ [ noteId ] ;
}
2019-06-27 00:26:26 +02:00
2022-09-30 12:46:26 +02:00
scheduleSave() {
2020-06-10 01:30:32 +02:00
this . saveActionQueue ( this . state . note . id ) . push ( this . makeSaveAction ( ) ) ;
2017-07-22 20:16:16 +02:00
}
2022-09-30 12:46:26 +02:00
async saveNoteButton_press ( folderId : string = null ) {
2018-05-01 19:53:45 +02:00
await shared . saveNoteButton_press ( this , folderId ) ;
2017-07-17 22:22:05 +02:00
2017-09-24 16:48:23 +02:00
Keyboard . dismiss ( ) ;
2017-05-12 22:23:54 +02:00
}
2022-09-30 12:46:26 +02:00
async saveOneProperty ( name : string , value : any ) {
2017-11-05 02:49:23 +02:00
await shared . saveOneProperty ( this , name , value ) ;
2017-07-22 20:16:16 +02:00
}
2022-09-30 12:46:26 +02:00
async deleteNote_onPress() {
2020-03-14 01:46:14 +02:00
const note = this . state . note ;
2017-07-15 01:12:32 +02:00
if ( ! note . id ) return ;
2020-06-13 17:20:59 +02:00
const ok = await dialogs . confirm ( this , _ ( 'Delete note?' ) ) ;
2017-07-15 01:12:32 +02:00
if ( ! ok ) return ;
2020-03-14 01:46:14 +02:00
const folderId = note . parent_id ;
2017-07-15 01:12:32 +02:00
await Note . delete ( note . id ) ;
2017-07-16 23:17:22 +02:00
2017-07-25 20:09:01 +02:00
this . props . dispatch ( {
2018-03-09 22:59:12 +02:00
type : 'NAV_GO' ,
routeName : 'Notes' ,
2017-07-25 20:09:01 +02:00
folderId : folderId ,
} ) ;
2017-06-04 17:01:52 +02:00
}
2022-09-11 17:58:36 +02:00
private async pickDocuments() {
2022-11-14 14:28:32 +02:00
const result = await shim . fsDriver ( ) . pickDocument ( { multiple : true } ) ;
2022-10-13 23:02:06 +02:00
if ( ! result ) {
2023-02-16 12:55:24 +02:00
// eslint-disable-next-line no-console
2022-10-13 23:02:06 +02:00
console . info ( 'pickDocuments: user has cancelled' ) ;
2020-10-16 17:26:19 +02:00
}
2022-10-13 23:02:06 +02:00
return result ;
2017-08-01 23:40:14 +02:00
}
2022-09-30 12:46:26 +02:00
async imageDimensions ( uri : string ) {
2017-08-02 19:47:25 +02:00
return new Promise ( ( resolve , reject ) = > {
2019-07-30 09:35:42 +02:00
Image . getSize (
uri ,
2020-11-12 21:13:28 +02:00
( width : number , height : number ) = > {
2019-07-30 09:35:42 +02:00
resolve ( { width : width , height : height } ) ;
} ,
2020-11-12 21:13:28 +02:00
( error : any ) = > {
2019-07-30 09:35:42 +02:00
reject ( error ) ;
}
) ;
2017-08-02 19:47:25 +02:00
} ) ;
}
2022-09-30 12:46:26 +02:00
async resizeImage ( localFilePath : string , targetPath : string , mimeType : string ) {
2017-11-19 17:18:07 +02:00
const maxSize = Resource . IMAGE_MAX_DIMENSION ;
2017-08-02 19:47:25 +02:00
2020-11-12 21:13:28 +02:00
const dimensions : any = await this . imageDimensions ( localFilePath ) ;
2017-11-19 17:18:07 +02:00
2018-03-09 22:59:12 +02:00
reg . logger ( ) . info ( 'Original dimensions ' , dimensions ) ;
2020-03-31 23:40:38 +02:00
let mustResize = dimensions . width > maxSize || dimensions . height > maxSize ;
if ( mustResize ) {
2020-06-13 17:20:59 +02:00
const buttonId = await dialogs . pop ( this , _ ( 'You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?' , dimensions . width , dimensions . height , maxSize ) , [
2020-03-31 23:40:38 +02:00
{ text : _ ( 'Yes' ) , id : 'yes' } ,
{ text : _ ( 'No' ) , id : 'no' } ,
{ text : _ ( 'Cancel' ) , id : 'cancel' } ,
] ) ;
if ( buttonId === 'cancel' ) return false ;
mustResize = buttonId === 'yes' ;
}
if ( mustResize ) {
2017-11-19 17:18:07 +02:00
dimensions . width = maxSize ;
dimensions . height = maxSize ;
2020-03-31 23:40:38 +02:00
reg . logger ( ) . info ( 'New dimensions ' , dimensions ) ;
2017-12-19 22:14:40 +02:00
2022-07-23 09:31:32 +02:00
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG' ;
2020-03-31 23:40:38 +02:00
reg . logger ( ) . info ( ` Resizing image ${ localFilePath } ` ) ;
const resizedImage = await ImageResizer . createResizedImage ( localFilePath , dimensions . width , dimensions . height , format , 85 ) ; // , 0, targetPath);
2017-12-19 22:14:40 +02:00
2020-03-31 23:40:38 +02:00
const resizedImagePath = resizedImage . uri ;
reg . logger ( ) . info ( 'Resized image ' , resizedImagePath ) ;
reg . logger ( ) . info ( ` Moving ${ resizedImagePath } => ${ targetPath } ` ) ;
2017-12-19 22:14:40 +02:00
2022-10-13 23:02:06 +02:00
await shim . fsDriver ( ) . copy ( resizedImagePath , targetPath ) ;
2020-03-31 23:40:38 +02:00
try {
2022-10-13 23:02:06 +02:00
await shim . fsDriver ( ) . unlink ( resizedImagePath ) ;
2020-03-31 23:40:38 +02:00
} catch ( error ) {
reg . logger ( ) . warn ( 'Error when unlinking cached file: ' , error ) ;
}
} else {
2022-10-13 23:02:06 +02:00
await shim . fsDriver ( ) . copy ( localFilePath , targetPath ) ;
2017-11-19 17:18:07 +02:00
}
2020-03-31 23:40:38 +02:00
return true ;
2017-11-19 17:18:07 +02:00
}
2022-09-30 12:46:26 +02:00
async attachFile ( pickerResponse : any , fileType : string ) {
2017-11-19 17:18:07 +02:00
if ( ! pickerResponse ) {
2020-10-16 17:26:19 +02:00
// User has cancelled
2017-11-01 19:39:56 +02:00
return ;
}
2017-08-01 23:40:14 +02:00
2019-07-21 15:39:52 +02:00
const localFilePath = Platform . select ( {
android : pickerResponse.uri ,
ios : decodeURI ( pickerResponse . uri ) ,
} ) ;
2019-07-30 09:35:42 +02:00
2017-11-20 00:08:58 +02:00
let mimeType = pickerResponse . type ;
if ( ! mimeType ) {
const ext = fileExtension ( localFilePath ) ;
mimeType = mimeUtils . fromFileExtension ( ext ) ;
}
2017-08-02 19:47:25 +02:00
2018-03-09 22:59:12 +02:00
if ( ! mimeType && fileType === 'image' ) {
2017-11-20 21:18:49 +02:00
// Assume JPEG if we couldn't determine the file type. It seems to happen with the image picker
// when the file path is something like content://media/external/images/media/123456
// If the image is not a JPEG, something will throw an error below, but there's a good chance
// it will work.
2018-03-09 22:59:12 +02:00
reg . logger ( ) . info ( 'Missing file type and could not detect it - assuming image/jpg' ) ;
mimeType = 'image/jpg' ;
2017-11-20 21:18:49 +02:00
}
2019-09-19 23:51:18 +02:00
reg . logger ( ) . info ( ` Got file: ${ localFilePath } ` ) ;
reg . logger ( ) . info ( ` Got type: ${ mimeType } ` ) ;
2017-08-01 23:40:14 +02:00
let resource = Resource . new ( ) ;
resource . id = uuid . create ( ) ;
2017-11-20 00:08:58 +02:00
resource . mime = mimeType ;
2021-01-17 13:40:24 +02:00
resource . title = pickerResponse . name ? pickerResponse . name : '' ;
resource . file_extension = safeFileExtension ( fileExtension ( pickerResponse . name ? pickerResponse.name : localFilePath ) ) ;
2017-12-02 01:15:49 +02:00
2018-03-09 22:59:12 +02:00
if ( ! resource . mime ) resource . mime = 'application/octet-stream' ;
2017-08-01 23:40:14 +02:00
2020-03-14 01:46:14 +02:00
const targetPath = Resource . fullPath ( resource ) ;
2017-08-02 19:47:25 +02:00
2017-11-20 00:08:58 +02:00
try {
2022-07-23 09:31:32 +02:00
if ( mimeType === 'image/jpeg' || mimeType === 'image/jpg' || mimeType === 'image/png' ) {
2020-06-04 19:40:44 +02:00
const done = await this . resizeImage ( localFilePath , targetPath , mimeType ) ;
2020-03-31 23:40:38 +02:00
if ( ! done ) return ;
2017-11-19 17:18:07 +02:00
} else {
2018-03-09 22:59:12 +02:00
if ( fileType === 'image' ) {
2020-06-13 17:20:59 +02:00
dialogs . error ( this , _ ( 'Unsupported image type: %s' , mimeType ) ) ;
2017-11-20 00:08:58 +02:00
return ;
} else {
2018-10-07 19:55:49 +02:00
await shim . fsDriver ( ) . copy ( localFilePath , targetPath ) ;
2018-05-03 12:31:07 +02:00
const stat = await shim . fsDriver ( ) . stat ( targetPath ) ;
2022-10-13 23:02:06 +02:00
2022-09-14 13:21:21 +02:00
if ( stat . size >= 200 * 1024 * 1024 ) {
2018-05-03 12:31:07 +02:00
await shim . fsDriver ( ) . remove ( targetPath ) ;
2022-09-14 13:21:21 +02:00
throw new Error ( 'Resources larger than 200 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.' ) ;
2018-05-03 12:31:07 +02:00
}
2017-11-20 00:08:58 +02:00
}
2017-08-02 19:47:25 +02:00
}
2017-11-20 00:08:58 +02:00
} catch ( error ) {
2018-03-09 22:59:12 +02:00
reg . logger ( ) . warn ( 'Could not attach file:' , error ) ;
2020-06-13 17:20:59 +02:00
await dialogs . error ( this , error . message ) ;
2017-11-20 00:08:58 +02:00
return ;
2017-08-02 19:47:25 +02:00
}
2017-08-01 23:40:14 +02:00
2019-05-12 02:15:52 +02:00
const itDoes = await shim . fsDriver ( ) . waitTillExists ( targetPath ) ;
2019-09-19 23:51:18 +02:00
if ( ! itDoes ) throw new Error ( ` Resource file was not created: ${ targetPath } ` ) ;
2019-05-12 02:15:52 +02:00
2019-05-11 18:55:40 +02:00
const fileStat = await shim . fsDriver ( ) . stat ( targetPath ) ;
resource . size = fileStat . size ;
2019-06-15 22:23:30 +02:00
resource = await Resource . save ( resource , { isNew : true } ) ;
2017-08-01 23:40:14 +02:00
const resourceTag = Resource . markdownTag ( resource ) ;
2017-07-13 20:47:31 +02:00
2017-08-02 19:47:25 +02:00
const newNote = Object . assign ( { } , this . state . note ) ;
2020-05-20 18:46:01 +02:00
2022-07-23 09:31:32 +02:00
if ( this . state . mode === 'edit' && ! ! this . selection ) {
2022-04-11 12:56:45 +02:00
const newText = ` \ n ${ resourceTag } \ n ` ;
2020-06-13 17:46:43 +02:00
const prefix = newNote . body . substring ( 0 , this . selection . start ) ;
const suffix = newNote . body . substring ( this . selection . end ) ;
2022-04-11 12:56:45 +02:00
newNote . body = ` ${ prefix } ${ newText } ${ suffix } ` ;
if ( this . useEditorBeta ( ) ) {
// The beta editor needs to be explicitly informed of changes
// to the note's body
this . editorRef . current . insertText ( newText ) ;
}
2020-05-20 18:46:01 +02:00
} else {
newNote . body += ` \ n ${ resourceTag } ` ;
}
2017-08-02 19:47:25 +02:00
this . setState ( { note : newNote } ) ;
2019-06-15 22:23:30 +02:00
2019-10-12 20:49:10 +02:00
this . refreshResource ( resource , newNote . body ) ;
2019-07-11 18:41:13 +02:00
this . scheduleSave ( ) ;
2017-07-13 20:47:31 +02:00
}
2022-09-09 16:06:03 +02:00
private async attachPhoto_onPress() {
2022-09-11 17:58:36 +02:00
// the selection Limit should be specfied. I think 200 is enough?
2022-12-22 14:38:11 +02:00
const response : ImagePickerResponse = await launchImageLibrary ( { mediaType : 'photo' , includeBase64 : false , selectionLimit : 200 } ) ;
2022-09-11 17:58:36 +02:00
if ( response . errorCode ) {
reg . logger ( ) . warn ( 'Got error from picker' , response . errorCode ) ;
return ;
}
if ( response . didCancel ) {
reg . logger ( ) . info ( 'User cancelled picker' ) ;
return ;
}
for ( const asset of response . assets ) {
await this . attachFile ( asset , 'image' ) ;
}
2017-11-19 17:18:07 +02:00
}
2022-09-30 12:46:26 +02:00
takePhoto_onPress() {
2018-10-13 11:32:44 +02:00
this . setState ( { showCamera : true } ) ;
2019-07-30 09:35:42 +02:00
}
2018-10-13 11:32:44 +02:00
2022-09-30 12:46:26 +02:00
cameraView_onPhoto ( data : any ) {
2020-11-25 16:40:25 +02:00
void this . attachFile (
2019-07-30 09:35:42 +02:00
{
uri : data.uri ,
type : 'image/jpg' ,
} ,
'image'
) ;
2018-10-13 11:32:44 +02:00
this . setState ( { showCamera : false } ) ;
}
2022-09-30 12:46:26 +02:00
cameraView_onCancel() {
2018-10-13 11:32:44 +02:00
this . setState ( { showCamera : false } ) ;
}
2022-09-09 16:06:03 +02:00
private async attachFile_onPress() {
2022-09-11 17:58:36 +02:00
const response = await this . pickDocuments ( ) ;
for ( const asset of response ) {
await this . attachFile ( asset , 'all' ) ;
}
2017-11-19 17:18:07 +02:00
}
2022-09-30 12:46:26 +02:00
toggleIsTodo_onPress() {
2017-11-05 02:49:23 +02:00
shared . toggleIsTodo_onPress ( this ) ;
2019-07-14 00:49:35 +02:00
this . scheduleSave ( ) ;
2017-07-17 22:22:05 +02:00
}
2022-09-30 12:46:26 +02:00
tags_onPress() {
2018-03-16 22:17:52 +02:00
if ( ! this . state . note || ! this . state . note . id ) return ;
2018-03-18 01:00:01 +02:00
this . setState ( { noteTagDialogShown : true } ) ;
2018-03-16 22:17:52 +02:00
}
2022-09-30 12:46:26 +02:00
async share_onPress() {
2018-05-10 21:39:41 +02:00
await Share . share ( {
2019-09-19 23:51:18 +02:00
message : ` ${ this . state . note . title } \ n \ n ${ this . state . note . body } ` ,
2018-05-10 21:39:41 +02:00
title : this.state.note.title ,
} ) ;
}
2022-09-30 12:46:26 +02:00
properties_onPress() {
2019-07-11 19:23:29 +02:00
this . props . dispatch ( { type : 'SIDE_MENU_OPEN' } ) ;
}
2022-09-30 12:46:26 +02:00
setAlarm_onPress() {
2017-09-10 18:56:27 +02:00
this . setState ( { alarmDialogShown : true } ) ;
}
2022-09-30 12:46:26 +02:00
async onAlarmDialogAccept ( date : Date ) {
2020-03-14 01:46:14 +02:00
const newNote = Object . assign ( { } , this . state . note ) ;
2017-09-10 18:56:27 +02:00
newNote . todo_due = date ? date . getTime ( ) : 0 ;
2018-03-09 22:59:12 +02:00
await this . saveOneProperty ( 'todo_due' , date ? date . getTime ( ) : 0 ) ;
2017-11-28 00:50:46 +02:00
this . setState ( { alarmDialogShown : false } ) ;
2017-09-10 18:56:27 +02:00
}
2022-09-30 12:46:26 +02:00
onAlarmDialogReject() {
2017-09-10 18:56:27 +02:00
this . setState ( { alarmDialogShown : false } ) ;
}
2022-09-30 12:46:26 +02:00
async showOnMap_onPress() {
2017-07-22 20:16:16 +02:00
if ( ! this . state . note . id ) return ;
2020-03-14 01:46:14 +02:00
const note = await Note . load ( this . state . note . id ) ;
2017-07-22 20:16:16 +02:00
try {
const url = Note . geolocationUrl ( note ) ;
2022-09-30 12:46:26 +02:00
Linking . openURL ( url ) ;
2017-07-22 20:16:16 +02:00
} catch ( error ) {
2020-03-13 21:58:17 +02:00
this . props . dispatch ( { type : 'SIDE_MENU_CLOSE' } ) ;
2020-06-13 17:20:59 +02:00
await dialogs . error ( this , error . message ) ;
2017-07-22 20:16:16 +02:00
}
}
2022-09-30 12:46:26 +02:00
async showSource_onPress() {
2018-10-12 20:30:00 +02:00
if ( ! this . state . note . id ) return ;
2020-03-14 01:46:14 +02:00
const note = await Note . load ( this . state . note . id ) ;
2018-10-12 20:30:00 +02:00
try {
2022-09-30 12:46:26 +02:00
Linking . openURL ( note . source_url ) ;
2018-10-12 20:30:00 +02:00
} catch ( error ) {
2020-06-13 17:20:59 +02:00
await dialogs . error ( this , error . message ) ;
2018-10-12 20:30:00 +02:00
}
}
2022-09-30 12:46:26 +02:00
copyMarkdownLink_onPress() {
2018-05-02 16:13:20 +02:00
const note = this . state . note ;
Clipboard . setString ( Note . markdownTag ( note ) ) ;
}
2022-09-30 12:46:26 +02:00
sideMenuOptions() {
2019-07-11 19:23:29 +02:00
const note = this . state . note ;
if ( ! note ) return [ ] ;
const output = [ ] ;
2019-07-16 16:11:58 +02:00
const createdDateString = time . formatMsToLocal ( note . user_created_time ) ;
const updatedDateString = time . formatMsToLocal ( note . user_updated_time ) ;
2019-07-11 19:23:29 +02:00
output . push ( { title : _ ( 'Created: %s' , createdDateString ) } ) ;
output . push ( { title : _ ( 'Updated: %s' , updatedDateString ) } ) ;
output . push ( { isDivider : true } ) ;
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'View on map' ) ,
onPress : ( ) = > {
2020-11-25 16:40:25 +02:00
void this . showOnMap_onPress ( ) ;
2019-07-30 09:35:42 +02:00
} ,
} ) ;
2020-03-14 01:57:34 +02:00
if ( note . source_url ) {
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'Go to source URL' ) ,
onPress : ( ) = > {
2020-11-25 16:40:25 +02:00
void this . showSource_onPress ( ) ;
2019-07-30 09:35:42 +02:00
} ,
} ) ;
2020-03-14 01:57:34 +02:00
}
2019-07-11 19:23:29 +02:00
return output ;
}
2022-08-29 15:19:04 +02:00
async showAttachMenu() {
const buttons = [ ] ;
// On iOS, it will show "local files", which means certain files saved from the browser
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
//
// On Android, it will depend on the phone, but usually it will allow browing all files and photos.
buttons . push ( { text : _ ( 'Attach file' ) , id : 'attachFile' } ) ;
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
// because that's only way to browse photos from the camera roll.
if ( Platform . OS === 'ios' ) buttons . push ( { text : _ ( 'Attach photo' ) , id : 'attachPhoto' } ) ;
buttons . push ( { text : _ ( 'Take photo' ) , id : 'takePhoto' } ) ;
const buttonId = await dialogs . pop ( this , _ ( 'Choose an option' ) , buttons ) ;
if ( buttonId === 'takePhoto' ) this . takePhoto_onPress ( ) ;
if ( buttonId === 'attachFile' ) void this . attachFile_onPress ( ) ;
if ( buttonId === 'attachPhoto' ) void this . attachPhoto_onPress ( ) ;
}
2022-09-30 12:46:26 +02:00
menuOptions() {
2017-07-17 22:22:05 +02:00
const note = this . state . note ;
2017-09-10 18:57:06 +02:00
const isTodo = note && ! ! note . is_todo ;
2018-03-16 22:17:52 +02:00
const isSaved = note && note . id ;
2017-07-17 22:22:05 +02:00
2019-07-12 19:32:08 +02:00
const cacheKey = md5 ( [ isTodo , isSaved ] . join ( '_' ) ) ;
if ( ! this . menuOptionsCache_ ) this . menuOptionsCache_ = { } ;
if ( this . menuOptionsCache_ [ cacheKey ] ) return this . menuOptionsCache_ [ cacheKey ] ;
2020-03-14 01:46:14 +02:00
const output = [ ] ;
2017-09-10 18:57:06 +02:00
2017-11-20 21:01:19 +02:00
// The file attachement modules only work in Android >= 5 (Version 21)
// https://github.com/react-community/react-native-image-picker/issues/606
2020-10-16 17:26:19 +02:00
// As of 2020-10-13, support for attaching images from the gallery is removed
// as the package react-native-image-picker has permission issues. It's still
// possible to attach files, which has often a similar UI, with thumbnails for
// images so normally it should be enough.
2017-11-20 20:29:39 +02:00
let canAttachPicture = true ;
2018-03-09 22:59:12 +02:00
if ( Platform . OS === 'android' && Platform . Version < 21 ) canAttachPicture = false ;
2017-11-20 20:29:39 +02:00
if ( canAttachPicture ) {
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'Attach...' ) ,
2022-08-29 15:19:04 +02:00
onPress : ( ) = > this . showAttachMenu ( ) ,
2019-07-30 09:35:42 +02:00
} ) ;
2017-11-20 21:01:19 +02:00
}
2017-07-17 22:22:05 +02:00
2017-11-28 00:50:46 +02:00
if ( isTodo ) {
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'Set alarm' ) ,
onPress : ( ) = > {
this . setState ( { alarmDialogShown : true } ) ;
} ,
} ) ;
2017-11-28 00:50:46 +02:00
}
2017-09-10 18:57:06 +02:00
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'Share' ) ,
onPress : ( ) = > {
2020-11-25 16:40:25 +02:00
void this . share_onPress ( ) ;
2019-07-30 09:35:42 +02:00
} ,
} ) ;
2020-03-14 01:57:34 +02:00
if ( isSaved ) {
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'Tags' ) ,
onPress : ( ) = > {
this . tags_onPress ( ) ;
} ,
} ) ;
2020-03-14 01:57:34 +02:00
}
2019-07-30 09:35:42 +02:00
output . push ( {
title : isTodo ? _ ( 'Convert to note' ) : _ ( 'Convert to todo' ) ,
onPress : ( ) = > {
this . toggleIsTodo_onPress ( ) ;
} ,
} ) ;
2020-03-14 01:57:34 +02:00
if ( isSaved ) {
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'Copy Markdown link' ) ,
onPress : ( ) = > {
this . copyMarkdownLink_onPress ( ) ;
} ,
} ) ;
2020-03-14 01:57:34 +02:00
}
2019-07-30 09:35:42 +02:00
output . push ( {
title : _ ( 'Properties' ) ,
onPress : ( ) = > {
this . properties_onPress ( ) ;
} ,
} ) ;
output . push ( {
title : _ ( 'Delete' ) ,
onPress : ( ) = > {
2020-11-25 16:40:25 +02:00
void this . deleteNote_onPress ( ) ;
2019-07-30 09:35:42 +02:00
} ,
} ) ;
2017-09-10 18:57:06 +02:00
2019-07-12 19:32:08 +02:00
this . menuOptionsCache_ = { } ;
this . menuOptionsCache_ [ cacheKey ] = output ;
2017-09-10 18:57:06 +02:00
return output ;
2017-06-04 17:01:52 +02:00
}
2022-09-30 12:46:26 +02:00
async todoCheckbox_change ( checked : boolean ) {
2018-03-09 22:59:12 +02:00
await this . saveOneProperty ( 'todo_completed' , checked ? time . unixMs ( ) : 0 ) ;
2017-07-15 01:12:32 +02:00
}
2022-09-30 12:46:26 +02:00
scheduleFocusUpdate() {
2020-10-09 19:35:46 +02:00
if ( this . focusUpdateIID_ ) shim . clearTimeout ( this . focusUpdateIID_ ) ;
2020-01-08 19:42:28 +02:00
2020-10-09 19:35:46 +02:00
this . focusUpdateIID_ = shim . setTimeout ( ( ) = > {
2020-01-08 19:42:28 +02:00
this . focusUpdateIID_ = null ;
this . focusUpdate ( ) ;
} , 100 ) ;
}
2022-09-30 12:46:26 +02:00
focusUpdate() {
2020-10-09 19:35:46 +02:00
if ( this . focusUpdateIID_ ) shim . clearTimeout ( this . focusUpdateIID_ ) ;
2020-01-08 19:42:28 +02:00
this . focusUpdateIID_ = null ;
2019-06-27 00:21:12 +02:00
if ( ! this . state . note ) return ;
2019-07-30 09:35:42 +02:00
let fieldToFocus = this . state . note . is_todo ? 'title' : 'body' ;
2019-06-27 00:21:12 +02:00
if ( this . state . mode === 'view' ) fieldToFocus = '' ;
2020-03-25 12:50:45 +02:00
if ( fieldToFocus === 'title' && this . refs . titleTextField ) {
this . refs . titleTextField . focus ( ) ;
}
2020-11-05 18:58:23 +02:00
// if (fieldToFocus === 'body' && this.markdownEditorRef.current) {
// if (this.markdownEditorRef.current) {
// this.markdownEditorRef.current.focus();
// }
// }
2019-06-27 00:21:12 +02:00
}
2022-09-30 12:46:26 +02:00
async folderPickerOptions_valueChanged ( itemValue : any ) {
2019-07-12 19:32:08 +02:00
const note = this . state . note ;
2020-06-08 09:40:52 +02:00
const isProvisionalNote = this . props . provisionalNoteIds . includes ( note . id ) ;
2019-07-12 19:32:08 +02:00
2020-06-08 09:40:52 +02:00
if ( isProvisionalNote ) {
2019-07-12 19:32:08 +02:00
await this . saveNoteButton_press ( itemValue ) ;
} else {
await Note . moveToFolder ( note . id , itemValue ) ;
}
note . parent_id = itemValue ;
const folder = await Folder . load ( note . parent_id ) ;
this . setState ( {
lastSavedNote : Object.assign ( { } , note ) ,
note : note ,
folder : folder ,
} ) ;
}
2022-09-30 12:46:26 +02:00
folderPickerOptions() {
2019-07-12 19:32:08 +02:00
const options = {
enabled : true ,
selectedFolderId : this.state.folder ? this . state.folder.id : null ,
onValueChange : this.folderPickerOptions_valueChanged ,
} ;
if ( this . folderPickerOptions_ && options . selectedFolderId === this . folderPickerOptions_ . selectedFolderId ) return this . folderPickerOptions_ ;
this . folderPickerOptions_ = options ;
return this . folderPickerOptions_ ;
}
2022-09-30 12:46:26 +02:00
onBodyViewerLoadEnd() {
2020-10-16 17:26:19 +02:00
shim . setTimeout ( ( ) = > {
this . setState ( { HACK_webviewLoadingState : 1 } ) ;
shim . setTimeout ( ( ) = > {
this . setState ( { HACK_webviewLoadingState : 0 } ) ;
} , 50 ) ;
} , 5 ) ;
}
2022-09-30 12:46:26 +02:00
onBodyViewerCheckboxChange ( newBody : string ) {
2020-11-25 16:40:25 +02:00
void this . saveOneProperty ( 'body' , newBody ) ;
2020-10-16 17:26:19 +02:00
}
2022-09-30 12:46:26 +02:00
render() {
2017-07-24 23:52:30 +02:00
if ( this . state . isLoading ) {
return (
< View style = { this . styles ( ) . screen } >
2019-07-30 09:35:42 +02:00
< ScreenHeader / >
2017-07-24 23:52:30 +02:00
< / View >
) ;
}
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2021-07-13 20:13:13 +02:00
const note : NoteEntity = this . state . note ;
2017-05-24 22:51:50 +02:00
const isTodo = ! ! Number ( note . is_todo ) ;
2018-10-13 11:32:44 +02:00
if ( this . state . showCamera ) {
2020-09-15 15:01:07 +02:00
return < CameraView themeId = { this . props . themeId } style = { { flex : 1 } } onPhoto = { this . cameraView_onPhoto } onCancel = { this . cameraView_onCancel } / > ;
2018-10-13 11:32:44 +02:00
}
2020-10-16 17:26:19 +02:00
// Currently keyword highlighting is supported only when FTS is available.
const keywords = this . props . searchQuery && ! ! this . props . ftsEnabled ? this . props.highlightedWords : emptyArray ;
2017-07-10 23:34:26 +02:00
let bodyComponent = null ;
2022-07-23 09:31:32 +02:00
if ( this . state . mode === 'view' ) {
2018-12-29 04:12:23 +02:00
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
// to avoid the HACK_webviewLoadingState related bug.
2019-07-30 09:35:42 +02:00
bodyComponent =
! note || ! note . body . trim ( ) ? null : (
< NoteBodyViewer
onJoplinLinkClick = { this . onJoplinLinkClick_ }
style = { this . styles ( ) . noteBodyViewer }
2020-03-25 12:50:45 +02:00
// Extra bottom padding to make it possible to scroll past the
// action button (so that it doesn't overlap the text)
2020-10-16 17:26:19 +02:00
paddingBottom = { 150 }
noteBody = { note . body }
noteMarkupLanguage = { note . markup_language }
2019-07-30 09:35:42 +02:00
noteResources = { this . state . noteResources }
highlightedKeywords = { keywords }
2020-09-15 15:01:07 +02:00
themeId = { this . props . themeId }
2019-09-09 19:16:00 +02:00
noteHash = { this . props . noteHash }
2020-10-16 17:26:19 +02:00
onCheckboxChange = { this . onBodyViewerCheckboxChange }
2019-07-30 09:35:42 +02:00
onMarkForDownload = { this . onMarkForDownload }
2020-10-16 17:26:19 +02:00
onLoadEnd = { this . onBodyViewerLoadEnd }
2019-07-30 09:35:42 +02:00
/ >
) ;
2017-07-10 23:34:26 +02:00
} else {
2021-07-12 15:00:51 +02:00
// Note: In theory ScrollView can be used to provide smoother scrolling of the TextInput.
// However it causes memory or rendering issues on older Android devices, probably because
// the whole text input has to be in memory for the scrollview to work. So we keep it as
// a plain TextInput for now.
// See https://github.com/laurent22/joplin/issues/3041
// IMPORTANT: The TextInput selection is unreliable and cannot be used in a controlled component
// context. In other words, the selection should be considered read-only. For example, if the seleciton
// is saved to the state in onSelectionChange and the current text in onChangeText, then set
// back in `selection` and `value` props, it will mostly work. But when typing fast, sooner or
// later the real selection will be different from what is stored in the state, thus making
// the cursor jump around. Eg, when typing "abcdef", it will do this:
// abcd|
// abcde|
// abcde|f
2021-07-13 20:13:13 +02:00
if ( ! this . useEditorBeta ( ) ) {
bodyComponent = (
< TextInput
autoCapitalize = "sentences"
style = { this . styles ( ) . bodyTextInput }
ref = "noteBodyTextField"
multiline = { true }
value = { note . body }
onChangeText = { ( text : string ) = > this . body_changeText ( text ) }
onSelectionChange = { this . body_selectionChange }
blurOnSubmit = { false }
selectionColor = { theme . textSelectionColor }
keyboardAppearance = { theme . keyboardAppearance }
placeholder = { _ ( 'Add body' ) }
placeholderTextColor = { theme . colorFaded }
// need some extra padding for iOS so that the keyboard won't cover last line of the note
// see https://github.com/laurent22/joplin/issues/3607
2022-09-30 12:46:26 +02:00
paddingBottom = { Platform . OS === 'ios' ? 40 : 0 }
2021-07-13 20:13:13 +02:00
/ >
) ;
} else {
2022-08-29 15:19:04 +02:00
const editorStyle = this . styles ( ) . bodyTextInput ;
2021-07-13 20:13:13 +02:00
bodyComponent = < NoteEditor
ref = { this . editorRef }
themeId = { this . props . themeId }
initialText = { note . body }
2022-04-11 12:56:45 +02:00
initialSelection = { this . selection }
2021-07-13 20:13:13 +02:00
onChange = { this . onBodyChange }
2022-04-11 12:56:45 +02:00
onSelectionChange = { this . body_selectionChange }
2021-07-13 20:13:13 +02:00
onUndoRedoDepthChange = { this . onUndoRedoDepthChange }
2022-08-29 15:19:04 +02:00
onAttach = { ( ) = > this . showAttachMenu ( ) }
style = { {
. . . editorStyle ,
paddingLeft : 0 ,
paddingRight : 0 ,
} }
contentStyle = { {
// Apply padding to the editor's content, but not the toolbar.
paddingLeft : editorStyle.paddingLeft ,
paddingRight : editorStyle.paddingRight ,
} }
2021-07-13 20:13:13 +02:00
/ > ;
}
2017-07-10 23:34:26 +02:00
}
2017-07-14 01:35:37 +02:00
const renderActionButton = ( ) = > {
2023-01-08 14:22:41 +02:00
const editButton = {
label : _ ( 'Edit' ) ,
2018-03-09 22:59:12 +02:00
icon : 'md-create' ,
2017-07-14 01:35:37 +02:00
onPress : ( ) = > {
2018-03-09 22:59:12 +02:00
this . setState ( { mode : 'edit' } ) ;
2019-06-27 00:21:12 +02:00
this . doFocusUpdate_ = true ;
2017-07-14 01:35:37 +02:00
} ,
2023-01-08 14:22:41 +02:00
} ;
2017-07-14 01:35:37 +02:00
2022-07-23 09:31:32 +02:00
if ( this . state . mode === 'edit' ) return null ;
2017-07-15 01:12:32 +02:00
2023-01-08 14:22:41 +02:00
return < ActionButton mainButton = { editButton } / > ;
2019-07-30 09:35:42 +02:00
} ;
2017-07-14 01:35:37 +02:00
const actionButtonComp = renderActionButton ( ) ;
2020-06-10 01:30:32 +02:00
// Save button is not really needed anymore with the improved save logic
2022-07-23 09:31:32 +02:00
const showSaveButton = false ; // this.state.mode === 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
2020-06-13 17:20:18 +02:00
const saveButtonDisabled = true ; // !this.isModified();
2017-07-16 12:17:40 +02:00
2017-07-16 23:17:22 +02:00
if ( showSaveButton ) this . saveButtonHasBeenShown_ = true ;
2017-08-01 20:53:50 +02:00
const titleContainerStyle = isTodo ? this . styles ( ) . titleContainerTodo : this.styles ( ) . titleContainer ;
2019-07-12 20:36:12 +02:00
const dueDate = Note . dueDateObject ( note ) ;
2017-09-10 18:56:27 +02:00
2017-07-31 19:56:14 +02:00
const titleComp = (
< View style = { titleContainerStyle } >
2019-07-30 09:35:42 +02:00
{ isTodo && < Checkbox style = { this . styles ( ) . checkbox } checked = { ! ! Number ( note . todo_completed ) } onChange = { this . todoCheckbox_change } / > }
2020-06-13 17:20:18 +02:00
< TextInput
ref = "titleTextField"
underlineColorAndroid = "#ffffff00"
autoCapitalize = "sentences"
style = { this . styles ( ) . titleTextInput }
value = { note . title }
onChangeText = { this . title_changeText }
selectionColor = { theme . textSelectionColor }
keyboardAppearance = { theme . keyboardAppearance }
placeholder = { _ ( 'Add title' ) }
placeholderTextColor = { theme . colorFaded }
/ >
2017-07-31 19:56:14 +02:00
< / View >
) ;
2019-07-30 09:35:42 +02:00
const noteTagDialog = ! this . state . noteTagDialogShown ? null : < NoteTagsDialog onCloseRequested = { this . noteTagDialog_closeRequested } / > ;
2018-03-18 01:00:01 +02:00
2017-05-12 22:23:54 +02:00
return (
2020-09-15 15:01:07 +02:00
< View style = { this . rootStyle ( this . props . themeId ) . root } >
2020-06-13 17:20:18 +02:00
< ScreenHeader
folderPickerOptions = { this . folderPickerOptions ( ) }
menuOptions = { this . menuOptions ( ) }
showSaveButton = { showSaveButton }
saveButtonDisabled = { saveButtonDisabled }
onSaveButtonPress = { this . saveNoteButton_press }
showSideMenuButton = { false }
showSearchButton = { false }
2022-05-10 11:23:36 +02:00
showUndoButton = { ( this . state . undoRedoButtonState . canUndo || this . state . undoRedoButtonState . canRedo ) && this . state . mode === 'edit' }
showRedoButton = { this . state . undoRedoButtonState . canRedo && this . state . mode === 'edit' }
2020-06-13 17:20:18 +02:00
undoButtonDisabled = { ! this . state . undoRedoButtonState . canUndo && this . state . undoRedoButtonState . canRedo }
onUndoButtonPress = { this . screenHeader_undoButtonPress }
onRedoButtonPress = { this . screenHeader_redoButtonPress }
/ >
2022-09-30 12:46:26 +02:00
{ titleComp }
{ bodyComponent }
2021-07-12 15:00:51 +02:00
{ actionButtonComp }
2019-07-30 09:35:42 +02:00
2020-10-16 17:26:19 +02:00
< SelectDateTimeDialog themeId = { this . props . themeId } shown = { this . state . alarmDialogShown } date = { dueDate } onAccept = { this . onAlarmDialogAccept } onReject = { this . onAlarmDialogReject } / >
2020-06-13 17:20:59 +02:00
< DialogBox
2020-11-12 21:13:28 +02:00
ref = { ( dialogbox : any ) = > {
2020-06-13 17:20:59 +02:00
this . dialogbox = dialogbox ;
} }
/ >
2019-07-30 09:35:42 +02:00
{ noteTagDialog }
2018-02-05 20:32:59 +02:00
< / View >
2017-05-12 22:23:54 +02:00
) ;
}
}
2022-09-30 12:46:26 +02:00
const NoteScreen = connect ( ( state : any ) = > {
2019-07-30 09:35:42 +02:00
return {
noteId : state.selectedNoteIds.length ? state . selectedNoteIds [ 0 ] : null ,
2019-09-09 19:16:00 +02:00
noteHash : state.selectedNoteHash ,
2019-07-30 09:35:42 +02:00
folderId : state.selectedFolderId ,
itemType : state.selectedItemType ,
folders : state.folders ,
searchQuery : state.searchQuery ,
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2019-09-17 22:32:00 +02:00
editorFont : [ state . settings [ 'style.editor.fontFamily' ] ] ,
2023-01-07 19:47:52 +02:00
editorFontSize : state.settings [ 'style.editor.fontSize' ] ,
2019-07-30 09:35:42 +02:00
ftsEnabled : state.settings [ 'db.ftsEnabled' ] ,
sharedData : state.sharedData ,
showSideMenu : state.showSideMenu ,
2020-03-04 03:13:10 +02:00
provisionalNoteIds : state.provisionalNoteIds ,
2020-09-06 14:07:00 +02:00
highlightedWords : state.highlightedWords ,
2023-01-03 21:48:33 +02:00
useEditorBeta : ! state . settings [ 'editor.usePlainText' ] ,
2019-07-30 09:35:42 +02:00
} ;
} ) ( NoteScreenComponent ) ;
2018-03-09 22:59:12 +02:00
2020-10-16 17:26:19 +02:00
export default NoteScreen ;