2020-06-02 22:13:15 +02:00
import FileViewer from 'react-native-file-viewer' ;
2020-06-10 01:30:32 +02:00
import AsyncActionQueue from '../../AsyncActionQueue' ;
2020-06-02 22:13:15 +02:00
2019-07-30 09:35:42 +02:00
const React = require ( 'react' ) ;
2020-04-21 00:31:21 +02:00
const { Platform , Clipboard , Keyboard , View , TextInput , StyleSheet , Linking , Image , Share } = require ( 'react-native' ) ;
2018-03-09 22:59:12 +02:00
const { connect } = require ( 'react-redux' ) ;
const { uuid } = require ( 'lib/uuid.js' ) ;
2020-03-25 12:50:45 +02:00
const { MarkdownEditor } = require ( '../../../MarkdownEditor/index.js' ) ;
2018-03-09 22:59:12 +02:00
const RNFS = require ( 'react-native-fs' ) ;
const Note = require ( 'lib/models/Note.js' ) ;
2020-06-13 17:20:18 +02:00
const UndoRedoService = require ( 'lib/services/UndoRedoService.js' ) . default ;
2018-05-02 16:13:20 +02:00
const BaseItem = require ( 'lib/models/BaseItem.js' ) ;
2018-03-09 22:59:12 +02:00
const Setting = require ( 'lib/models/Setting.js' ) ;
const Resource = require ( 'lib/models/Resource.js' ) ;
const Folder = require ( 'lib/models/Folder.js' ) ;
2019-07-12 19:32:08 +02:00
const md5 = require ( 'md5' ) ;
2018-03-09 22:59:12 +02:00
const { BackButtonService } = require ( 'lib/services/back-button.js' ) ;
const NavService = require ( 'lib/services/NavService.js' ) ;
const BaseModel = require ( 'lib/BaseModel.js' ) ;
const { ActionButton } = require ( 'lib/components/action-button.js' ) ;
2019-07-30 09:35:42 +02:00
const { fileExtension , safeFileExtension } = require ( 'lib/path-utils.js' ) ;
2018-03-09 22:59:12 +02:00
const mimeUtils = require ( 'lib/mime-utils.js' ) . mime ;
const { ScreenHeader } = require ( 'lib/components/screen-header.js' ) ;
2018-03-18 01:00:01 +02:00
const NoteTagsDialog = require ( 'lib/components/screens/NoteTagsDialog' ) ;
2018-03-09 22:59:12 +02:00
const { time } = require ( 'lib/time-utils.js' ) ;
const { Checkbox } = require ( 'lib/components/checkbox.js' ) ;
const { _ } = require ( 'lib/locale.js' ) ;
const { reg } = require ( 'lib/registry.js' ) ;
const { shim } = require ( 'lib/shim.js' ) ;
2018-10-13 00:25:11 +02:00
const ResourceFetcher = require ( 'lib/services/ResourceFetcher' ) ;
2018-03-09 22:59:12 +02:00
const { BaseScreenComponent } = require ( 'lib/components/base-screen.js' ) ;
2019-09-17 22:32:00 +02:00
const { themeStyle , editorFont } = require ( 'lib/components/global-style.js' ) ;
2020-06-13 17:20:59 +02:00
const { dialogs } = require ( 'lib/dialogs.js' ) ;
const DialogBox = require ( 'react-native-dialogbox' ) . default ;
2018-03-09 22:59:12 +02:00
const { NoteBodyViewer } = require ( 'lib/components/note-body-viewer.js' ) ;
const { DocumentPicker , DocumentPickerUtil } = require ( 'react-native-document-picker' ) ;
const ImageResizer = require ( 'react-native-image-resizer' ) . default ;
const shared = require ( 'lib/components/shared/note-screen-shared.js' ) ;
const ImagePicker = require ( 'react-native-image-picker' ) ;
const { SelectDateTimeDialog } = require ( 'lib/components/select-date-time-dialog.js' ) ;
2020-06-04 19:40:44 +02:00
const ShareExtension = require ( 'lib/ShareExtension.js' ) . default ;
2018-10-13 11:32:44 +02:00
const CameraView = require ( 'lib/components/CameraView' ) ;
2019-09-09 19:16:00 +02:00
const urlUtils = require ( 'lib/urlUtils' ) ;
2017-05-12 22:23:54 +02:00
2017-07-14 20:49:14 +02:00
class NoteScreenComponent extends BaseScreenComponent {
2019-09-13 00:16:42 +02:00
static navigationOptions ( ) {
2017-05-16 21:57:09 +02:00
return { header : null } ;
}
2017-05-12 22:23:54 +02:00
constructor ( ) {
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-03-25 12:50:45 +02:00
this . markdownEditorRef = React . createRef ( ) ; // For focusing the Markdown editor
2019-06-27 00:21:12 +02:00
this . doFocusUpdate _ = false ;
2017-11-19 17:19:36 +02:00
// iOS doesn't support multiline text fields properly so disable it
2018-03-09 22:59:12 +02:00
this . enableMultilineTitle _ = Platform . OS !== 'ios' ;
2017-12-19 22:14:40 +02:00
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 _ = { } ;
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
2018-03-09 22:59:12 +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
2018-03-09 22:59:12 +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-05-21 10:14:33 +02:00
this . onJoplinLinkClick _ = async msg => {
2018-05-02 16:13:20 +02:00
try {
if ( msg . indexOf ( 'joplin://' ) === 0 ) {
2019-09-09 19:16:00 +02:00
const resourceUrlInfo = urlUtils . parseResourceUrl ( msg ) ;
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' ,
} ) ;
setTimeout ( ( ) => {
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 ) ;
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 {
Linking . openURL ( msg ) ;
}
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
2019-10-12 20:49:10 +02:00
this . refreshResource = async ( resource , noteBody = null ) => {
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 ) ;
2019-03-08 19:14:17 +02:00
this . setState ( { noteResources : attachedResources } , ( ) => {
2019-10-12 20:49:10 +02:00
if ( this . refs . noteBodyViewer ) this . refs . noteBodyViewer . rebuildMd ( ) ;
2019-03-08 19:14:17 +02:00
} ) ;
2018-10-13 00:25:11 +02:00
}
2019-07-30 09:35:42 +02:00
} ;
2018-10-13 11:32:44 +02:00
2020-09-22 17:01:00 +02:00
this . useBetaEditor = ( ) => {
return Setting . value ( 'editor.beta' ) && Platform . OS !== 'android' ;
} ;
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 . titleTextInput _contentSizeChange = this . titleTextInput _contentSizeChange . 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 ) ;
}
undoRedoService _stackChange ( ) {
this . setState ( { undoRedoButtonState : {
canUndo : this . undoRedoService _ . canUndo ,
canRedo : this . undoRedoService _ . canRedo ,
} } ) ;
}
async undoRedo ( type ) {
const undoState = await this . undoRedoService _ [ type ] ( this . undoState ( ) ) ;
if ( ! undoState ) return ;
this . setState ( ( state ) => {
const newNote = Object . assign ( { } , state . note ) ;
newNote . body = undoState . body ;
return {
note : newNote ,
} ;
} ) ;
}
screenHeader _undoButtonPress ( ) {
this . undoRedo ( 'undo' ) ;
}
screenHeader _redoButtonPress ( ) {
this . undoRedo ( 'redo' ) ;
2017-07-15 01:12:32 +02:00
}
2017-08-01 20:53:50 +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-03-14 01:46:14 +02:00
const styles = {
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 ,
fontSize : theme . fontSize ,
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
} ,
noteBodyViewerPreview : {
borderTopColor : theme . dividerColor ,
borderTopWidth : 1 ,
borderBottomColor : theme . dividerColor ,
borderBottomWidth : 1 ,
2017-08-01 20:53:50 +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
} ;
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 . enableMultilineTitle _ ) styles . titleTextInput . height = this . state . titleTextInputHeight ;
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
}
2017-07-15 01:12:32 +02:00
isModified ( ) {
2017-11-05 02:49:23 +02:00
return shared . isModified ( this ) ;
2017-05-12 22:23:54 +02:00
}
2020-06-13 17:20:18 +02:00
undoState ( noteBody = null ) {
return {
body : noteBody === null ? this . state . note . body : noteBody ,
} ;
}
2020-05-20 18:46:01 +02:00
2020-06-13 17:20:18 +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 ) ;
}
2017-07-13 23:50:21 +02:00
}
2019-05-22 16:56:07 +02:00
onMarkForDownload ( event ) {
ResourceFetcher . instance ( ) . markForDownload ( event . resourceId ) ;
}
2019-07-21 15:11:30 +02:00
componentDidUpdate ( prevProps ) {
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
}
2017-07-15 01:12:32 +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
}
2017-06-06 22:01:43 +02:00
title _changeText ( text ) {
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
}
2017-06-06 22:01:43 +02:00
body _changeText ( text ) {
2020-06-13 17:20:18 +02:00
if ( ! this . undoRedoService _ . canUndo ) {
this . undoRedoService _ . push ( this . undoState ( ) ) ;
} else {
this . undoRedoService _ . schedulePush ( this . undoState ( ) ) ;
}
2018-03-09 22:59:12 +02:00
shared . noteComponent _change ( this , 'body' , text ) ;
2019-06-27 00:26:26 +02:00
this . scheduleSave ( ) ;
}
2020-06-13 17:20:18 +02:00
body _selectionChange ( event ) {
2020-06-13 17:46:43 +02:00
this . selection = event . nativeEvent . selection ;
2020-06-13 17:20:18 +02:00
}
2020-06-10 01:30:32 +02:00
makeSaveAction ( ) {
return async ( ) => {
return shared . saveNoteButton _press ( this ) ;
} ;
}
saveActionQueue ( noteId ) {
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
2020-06-10 01:30:32 +02:00
scheduleSave ( ) {
this . saveActionQueue ( this . state . note . id ) . push ( this . makeSaveAction ( ) ) ;
2017-07-22 20:16:16 +02:00
}
2018-05-01 19:53:45 +02:00
async saveNoteButton _press ( folderId = null ) {
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
}
2017-07-22 20:16:16 +02:00
async saveOneProperty ( name , value ) {
2017-11-05 02:49:23 +02:00
await shared . saveOneProperty ( this , name , value ) ;
2017-07-22 20:16:16 +02:00
}
2017-07-15 01:12:32 +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
}
2017-08-01 23:40:14 +02:00
async pickDocument ( ) {
2019-09-13 00:16:42 +02:00
return new Promise ( ( resolve ) => {
2019-07-30 09:35:42 +02:00
DocumentPicker . show ( { filetype : [ DocumentPickerUtil . allFiles ( ) ] } , ( error , res ) => {
2017-08-01 23:40:14 +02:00
if ( error ) {
2017-09-10 18:57:06 +02:00
// Also returns an error if the user doesn't pick a file
// so just resolve with null.
2018-03-09 22:59:12 +02:00
console . info ( 'pickDocument error:' , error ) ;
2017-09-10 18:57:06 +02:00
resolve ( null ) ;
2017-08-01 23:40:14 +02:00
return ;
}
resolve ( res ) ;
} ) ;
} ) ;
}
2017-08-02 19:47:25 +02:00
async imageDimensions ( uri ) {
return new Promise ( ( resolve , reject ) => {
2019-07-30 09:35:42 +02:00
Image . getSize (
uri ,
( width , height ) => {
resolve ( { width : width , height : height } ) ;
} ,
2020-05-21 10:14:33 +02:00
error => {
2019-07-30 09:35:42 +02:00
reject ( error ) ;
}
) ;
2017-08-02 19:47:25 +02:00
} ) ;
}
2017-11-19 17:18:07 +02:00
showImagePicker ( options ) {
2019-09-13 00:16:42 +02:00
return new Promise ( ( resolve ) => {
2020-05-21 10:14:33 +02:00
ImagePicker . launchImageLibrary ( options , response => {
2017-11-19 17:18:07 +02:00
resolve ( response ) ;
} ) ;
} ) ;
}
2017-08-01 23:40:14 +02:00
2017-11-19 17:18:07 +02:00
async resizeImage ( localFilePath , targetPath , mimeType ) {
const maxSize = Resource . IMAGE _MAX _DIMENSION ;
2017-08-02 19:47:25 +02:00
2020-03-14 01:46:14 +02:00
const dimensions = 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
2020-03-31 23:40:38 +02:00
const format = mimeType == 'image/png' ? 'PNG' : 'JPEG' ;
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
2020-03-31 23:40:38 +02:00
await RNFS . copyFile ( resizedImagePath , targetPath ) ;
try {
await RNFS . unlink ( resizedImagePath ) ;
} catch ( error ) {
reg . logger ( ) . warn ( 'Error when unlinking cached file: ' , error ) ;
}
} else {
await RNFS . copyFile ( 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
}
2019-07-30 09:35:42 +02:00
async attachFile ( pickerResponse , fileType ) {
2017-11-19 17:18:07 +02:00
if ( ! pickerResponse ) {
2018-03-09 22:59:12 +02:00
reg . logger ( ) . warn ( 'Got no response from picker' ) ;
2017-11-01 19:39:56 +02:00
return ;
}
2017-08-01 23:40:14 +02:00
2017-11-19 17:18:07 +02:00
if ( pickerResponse . error ) {
2018-03-09 22:59:12 +02:00
reg . logger ( ) . warn ( 'Got error from picker' , pickerResponse . error ) ;
2017-11-01 19:39:56 +02:00
return ;
}
2017-08-02 19:47:25 +02:00
2017-11-19 17:18:07 +02:00
if ( pickerResponse . didCancel ) {
2018-03-09 22:59:12 +02:00
reg . logger ( ) . info ( 'User cancelled picker' ) ;
2017-11-19 17:18:07 +02:00
return ;
}
2017-08-02 19:47:25 +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 ;
2018-12-07 02:07:10 +02:00
resource . title = pickerResponse . fileName ? pickerResponse . fileName : '' ;
2018-10-13 11:32:44 +02:00
resource . file _extension = safeFileExtension ( fileExtension ( pickerResponse . fileName ? pickerResponse . fileName : 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 {
2018-03-09 22:59:12 +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 ) ;
if ( stat . size >= 10000000 ) {
await shim . fsDriver ( ) . remove ( targetPath ) ;
throw new Error ( 'Resources larger than 10 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.' ) ;
}
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
2020-09-22 17:01:00 +02:00
if ( this . state . mode == 'edit' && ! this . useBetaEditor ( ) && ! ! this . selection ) {
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 ) ;
2020-05-20 18:46:01 +02:00
newNote . body = ` ${ prefix } \n ${ resourceTag } \n ${ suffix } ` ;
} 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
}
2018-12-07 02:07:10 +02:00
async attachPhoto _onPress ( ) {
2020-05-09 12:26:03 +02:00
const response = await this . showImagePicker ( { mediaType : 'photo' , noData : true } ) ;
2018-03-09 22:59:12 +02:00
await this . attachFile ( response , 'image' ) ;
2017-11-19 17:18:07 +02:00
}
2018-12-07 02:07:10 +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
cameraView _onPhoto ( data ) {
2019-07-30 09:35:42 +02:00
this . attachFile (
{
uri : data . uri ,
didCancel : false ,
error : null ,
type : 'image/jpg' ,
} ,
'image'
) ;
2018-10-13 11:32:44 +02:00
this . setState ( { showCamera : false } ) ;
}
cameraView _onCancel ( ) {
this . setState ( { showCamera : false } ) ;
}
2017-11-19 17:18:07 +02:00
async attachFile _onPress ( ) {
const response = await this . pickDocument ( ) ;
2018-03-09 22:59:12 +02:00
await this . attachFile ( response , 'all' ) ;
2017-11-19 17:18:07 +02:00
}
2017-07-30 21:51:18 +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
}
2018-03-16 22:17:52 +02:00
tags _onPress ( ) {
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
}
2018-05-10 21:39:41 +02:00
async share _onPress ( ) {
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 ,
} ) ;
}
2019-07-11 19:23:29 +02:00
properties _onPress ( ) {
this . props . dispatch ( { type : 'SIDE_MENU_OPEN' } ) ;
}
2017-09-10 18:56:27 +02:00
setAlarm _onPress ( ) {
this . setState ( { alarmDialogShown : true } ) ;
}
async onAlarmDialogAccept ( 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
}
onAlarmDialogReject ( ) {
this . setState ( { alarmDialogShown : false } ) ;
}
2017-07-22 20:16:16 +02:00
async showOnMap _onPress ( ) {
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 ) ;
Linking . openURL ( url ) ;
} 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
}
}
2018-10-12 20:30:00 +02:00
async showSource _onPress ( ) {
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 {
Linking . openURL ( note . source _url ) ;
} catch ( error ) {
2020-06-13 17:20:59 +02:00
await dialogs . error ( this , error . message ) ;
2018-10-12 20:30:00 +02:00
}
}
2018-05-02 16:13:20 +02:00
copyMarkdownLink _onPress ( ) {
const note = this . state . note ;
Clipboard . setString ( Note . markdownTag ( note ) ) ;
}
2019-07-11 19:23:29 +02:00
sideMenuOptions ( ) {
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 : ( ) => {
this . showOnMap _onPress ( ) ;
} ,
} ) ;
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 : ( ) => {
this . showSource _onPress ( ) ;
} ,
} ) ;
2020-03-14 01:57:34 +02:00
}
2019-07-11 19:23:29 +02:00
return output ;
}
2017-06-06 22:01:43 +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
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...' ) ,
onPress : async ( ) => {
2020-06-13 17:20:59 +02:00
const buttonId = await dialogs . pop ( this , _ ( 'Choose an option' ) , [ { text : _ ( 'Take photo' ) , id : 'takePhoto' } , { text : _ ( 'Attach photo' ) , id : 'attachPhoto' } , { text : _ ( 'Attach any file' ) , id : 'attachFile' } ] ) ;
2019-07-30 09:35:42 +02:00
if ( buttonId === 'takePhoto' ) this . takePhoto _onPress ( ) ;
if ( buttonId === 'attachPhoto' ) this . attachPhoto _onPress ( ) ;
if ( buttonId === 'attachFile' ) this . attachFile _onPress ( ) ;
} ,
} ) ;
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 : ( ) => {
this . share _onPress ( ) ;
} ,
} ) ;
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 : ( ) => {
this . deleteNote _onPress ( ) ;
} ,
} ) ;
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
}
2017-07-16 18:06:05 +02:00
async todoCheckbox _change ( checked ) {
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
}
2017-07-31 19:56:14 +02:00
titleTextInput _contentSizeChange ( event ) {
2017-11-19 17:19:36 +02:00
if ( ! this . enableMultilineTitle _ ) return ;
2020-03-14 01:46:14 +02:00
const height = event . nativeEvent . contentSize . height ;
2017-07-31 19:56:14 +02:00
this . setState ( { titleTextInputHeight : height } ) ;
}
2020-01-08 19:42:28 +02:00
scheduleFocusUpdate ( ) {
if ( this . focusUpdateIID _ ) clearTimeout ( this . focusUpdateIID _ ) ;
this . focusUpdateIID _ = setTimeout ( ( ) => {
this . focusUpdateIID _ = null ;
this . focusUpdate ( ) ;
} , 100 ) ;
}
2019-06-27 00:21:12 +02:00
focusUpdate ( ) {
2020-01-08 19:42:28 +02:00
if ( this . focusUpdateIID _ ) clearTimeout ( this . focusUpdateIID _ ) ;
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 ( ) ;
}
if ( fieldToFocus === 'body' && this . markdownEditorRef . current ) {
if ( this . markdownEditorRef . current ) {
this . markdownEditorRef . current . focus ( ) ;
}
}
2019-06-27 00:21:12 +02:00
}
2019-09-13 00:16:42 +02:00
async folderPickerOptions _valueChanged ( itemValue ) {
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 ,
} ) ;
}
folderPickerOptions ( ) {
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 _ ;
}
2017-05-12 22:23:54 +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
< / V i e w >
) ;
}
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( this . props . themeId ) ;
2017-05-24 22:51:50 +02:00
const note = this . state . note ;
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
}
2017-07-10 23:34:26 +02:00
let bodyComponent = null ;
2020-09-22 17:01:00 +02:00
if ( this . state . mode == 'view' && ! this . useBetaEditor ( ) ) {
2020-05-21 10:14:33 +02:00
const onCheckboxChange = newBody => {
2018-03-09 22:59:12 +02:00
this . saveOneProperty ( 'body' , newBody ) ;
2017-07-30 21:51:18 +02:00
} ;
2017-07-17 23:34:08 +02:00
2018-12-28 22:40:29 +02:00
// Currently keyword highlighting is supported only when FTS is available.
2018-12-16 19:32:42 +02:00
let keywords = [ ] ;
2018-12-28 22:40:29 +02:00
if ( this . props . searchQuery && ! ! this . props . ftsEnabled ) {
2020-09-06 14:07:00 +02:00
keywords = this . props . highlightedWords ;
2018-12-16 19:32:42 +02:00
}
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 _ }
ref = "noteBodyViewer"
style = { this . styles ( ) . noteBodyViewer }
webViewStyle = { theme }
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-04-08 02:22:18 +02:00
paddingBottom = "150"
2019-07-30 09:35:42 +02:00
note = { note }
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-05-21 10:14:33 +02:00
onCheckboxChange = { newBody => {
2019-07-30 09:35:42 +02:00
onCheckboxChange ( newBody ) ;
} }
onMarkForDownload = { this . onMarkForDownload }
onLoadEnd = { ( ) => {
setTimeout ( ( ) => {
this . setState ( { HACK _webviewLoadingState : 1 } ) ;
setTimeout ( ( ) => {
this . setState ( { HACK _webviewLoadingState : 0 } ) ;
} , 50 ) ;
} , 5 ) ;
} }
/ >
) ;
2017-07-10 23:34:26 +02:00
} else {
2019-06-27 00:21:12 +02:00
// autoFocus={fieldToFocus === 'body'}
2017-10-30 23:29:36 +02:00
2020-03-25 12:50:45 +02:00
// Currently keyword highlighting is supported only when FTS is available.
let keywords = [ ] ;
if ( this . props . searchQuery && ! ! this . props . ftsEnabled ) {
2020-09-06 14:07:00 +02:00
keywords = this . props . highlightedWords ;
2020-03-25 12:50:45 +02:00
}
2020-05-21 10:14:33 +02:00
const onCheckboxChange = newBody => {
2020-03-25 12:50:45 +02:00
this . saveOneProperty ( 'body' , newBody ) ;
} ;
2020-09-22 17:01:00 +02:00
bodyComponent = this . useBetaEditor ( )
2020-03-25 12:50:45 +02:00
// Note: blurOnSubmit is necessary to get multiline to work.
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
? < MarkdownEditor
ref = { this . markdownEditorRef } // For focusing the Markdown editor
editorFont = { editorFont ( this . props . editorFont ) }
style = { this . styles ( ) . bodyTextInput }
previewStyles = { this . styles ( ) . noteBodyViewer }
value = { note . body }
borderColor = { this . styles ( ) . markdownButtons . borderColor }
markdownButtonsColor = { this . styles ( ) . markdownButtons . color }
2020-05-21 10:14:33 +02:00
saveText = { text => this . body _changeText ( text ) }
2020-03-25 12:50:45 +02:00
blurOnSubmit = { false }
selectionColor = { theme . textSelectionColor }
2020-04-17 00:35:31 +02:00
keyboardAppearance = { theme . keyboardAppearance }
2020-03-25 12:50:45 +02:00
placeholder = { _ ( 'Add body' ) }
placeholderTextColor = { theme . colorFaded }
noteBodyViewer = { {
onJoplinLinkClick : this . onJoplinLinkClick _ ,
ref : 'noteBodyViewer' ,
style : {
... this . styles ( ) . noteBodyViewer ,
... this . styles ( ) . noteBodyViewerPreview ,
} ,
webViewStyle : theme ,
note : note ,
noteResources : this . state . noteResources ,
highlightedKeywords : keywords ,
2020-09-15 15:01:07 +02:00
themeId : this . props . themeId ,
2020-03-25 12:50:45 +02:00
noteHash : this . props . noteHash ,
2020-05-21 10:14:33 +02:00
onCheckboxChange : newBody => {
2020-03-25 12:50:45 +02:00
onCheckboxChange ( newBody ) ;
} ,
onMarkForDownload : this . onMarkForDownload ,
onLoadEnd : ( ) => {
setTimeout ( ( ) => {
this . setState ( { HACK _webviewLoadingState : 1 } ) ;
setTimeout ( ( ) => {
this . setState ( { HACK _webviewLoadingState : 0 } ) ;
} , 50 ) ;
} , 5 ) ;
} ,
} }
/ >
2020-04-21 00:31:21 +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
2020-06-13 17:46:43 +02:00
// 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
2020-04-21 00:31:21 +02:00
(
2020-05-20 18:46:01 +02:00
< TextInput
autoCapitalize = "sentences"
style = { this . styles ( ) . bodyTextInput }
ref = "noteBodyTextField"
multiline = { true }
value = { note . body }
onChangeText = { ( text ) => this . body _changeText ( text ) }
2020-06-13 17:20:18 +02:00
onSelectionChange = { this . body _selectionChange }
2020-05-20 18:46:01 +02:00
blurOnSubmit = { false }
selectionColor = { theme . textSelectionColor }
keyboardAppearance = { theme . keyboardAppearance }
placeholder = { _ ( 'Add body' ) }
placeholderTextColor = { theme . colorFaded }
/ >
2020-03-25 12:50:45 +02:00
) ;
2017-07-10 23:34:26 +02:00
}
2017-07-14 01:35:37 +02:00
const renderActionButton = ( ) => {
2020-03-14 01:46:14 +02:00
const buttons = [ ] ;
2017-07-14 01:35:37 +02:00
buttons . push ( {
2018-03-09 22:59:12 +02:00
title : _ ( 'Edit' ) ,
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
} ,
} ) ;
2019-07-12 20:36:12 +02:00
if ( this . state . mode == 'edit' ) return null ;
2017-07-15 01:12:32 +02:00
2019-07-30 09:35:42 +02:00
return < ActionButton multiStates = { true } buttons = { buttons } buttonIndex = { 0 } / > ;
} ;
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
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
onContentSizeChange = { this . titleTextInput _contentSizeChange }
multiline = { this . enableMultilineTitle _ }
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
< / V i e w >
) ;
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 }
showUndoButton = { this . state . undoRedoButtonState . canUndo || this . state . undoRedoButtonState . canRedo }
showRedoButton = { this . state . undoRedoButtonState . canRedo }
undoButtonDisabled = { ! this . state . undoRedoButtonState . canUndo && this . state . undoRedoButtonState . canRedo }
onUndoButtonPress = { this . screenHeader _undoButtonPress }
onRedoButtonPress = { this . screenHeader _redoButtonPress }
/ >
2019-07-30 09:35:42 +02:00
{ titleComp }
{ bodyComponent }
2020-09-22 17:01:00 +02:00
{ ! this . useBetaEditor ( ) && actionButtonComp }
2019-07-30 09:35:42 +02:00
< SelectDateTimeDialog shown = { this . state . alarmDialogShown } date = { dueDate } onAccept = { this . onAlarmDialogAccept } onReject = { this . onAlarmDialogReject } / >
2020-06-13 17:20:59 +02:00
< DialogBox
ref = { dialogbox => {
this . dialogbox = dialogbox ;
} }
/ >
2019-07-30 09:35:42 +02:00
{ noteTagDialog }
2018-02-05 20:32:59 +02:00
< / V i e w >
2017-05-12 22:23:54 +02:00
) ;
}
}
2020-05-21 10:14:33 +02:00
const NoteScreen = connect ( state => {
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' ] ] ,
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 ,
2019-07-30 09:35:42 +02:00
} ;
} ) ( NoteScreenComponent ) ;
2018-03-09 22:59:12 +02:00
2018-07-20 11:04:25 +02:00
module . exports = { NoteScreen } ;