2018-03-09 22:59:12 +02:00
const React = require ( 'react' ) ; const Component = React . Component ;
2019-06-14 23:46:08 +02:00
const { Platform , Clipboard , Keyboard , BackHandler , View , Button , TextInput , Text , 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' ) ;
const RNFS = require ( 'react-native-fs' ) ;
const Note = require ( 'lib/models/Note.js' ) ;
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' ) ;
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' ) ;
const Icon = require ( 'react-native-vector-icons/Ionicons' ) . default ;
const { fileExtension , basename , safeFileExtension } = require ( 'lib/path-utils.js' ) ;
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' ) ;
const { globalStyle , themeStyle } = require ( 'lib/components/global-style.js' ) ;
2018-05-02 16:13:20 +02:00
const { dialogs } = require ( 'lib/dialogs.js' ) ;
2018-03-09 22:59:12 +02:00
const DialogBox = require ( 'react-native-dialogbox' ) . default ;
const { NoteBodyViewer } = require ( 'lib/components/note-body-viewer.js' ) ;
2018-10-07 19:55:49 +02:00
const RNFetchBlob = require ( 'rn-fetch-blob' ) . default ;
2018-03-09 22:59:12 +02:00
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 AlarmService = require ( 'lib/services/AlarmService.js' ) ;
const { SelectDateTimeDialog } = require ( 'lib/components/select-date-time-dialog.js' ) ;
2018-07-20 11:04:25 +02:00
const ShareExtension = require ( 'react-native-share-extension' ) . default ;
2018-10-13 11:32:44 +02:00
const CameraView = require ( 'lib/components/CameraView' ) ;
2018-12-16 19:32:42 +02:00
const SearchEngine = require ( 'lib/services/SearchEngine' ) ;
2017-05-12 22:23:54 +02:00
2018-06-30 20:45:21 +02:00
import FileViewer from 'react-native-file-viewer' ;
2017-07-14 20:49:14 +02:00
class NoteScreenComponent extends BaseScreenComponent {
2018-03-09 22:59:12 +02:00
2017-06-06 22:01:43 +02:00
static navigationOptions ( options ) {
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' ,
noteMetadata : '' ,
2017-07-13 20:47:31 +02:00
showNoteMetadata : false ,
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 ,
2018-03-18 01:00:01 +02:00
heightBumpView : 0 ,
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
// margin. This forces RN to update the text input and to display it. Maybe that hack can be removed once RN is upgraded.
// See https://github.com/laurent22/joplin/issues/1057
HACK _webviewLoadingState : 0 ,
2017-07-16 23:17:22 +02:00
} ;
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 ( ) ) {
2018-03-09 22:59:12 +02:00
let 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-08-24 20:10:03 +02:00
] ) ;
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 ;
2018-03-09 22:59:12 +02:00
}
2018-02-22 00:08:34 +02:00
this . navHandler = async ( ) => {
return await saveDialog ( ) ;
2018-03-09 22:59:12 +02:00
}
2018-02-22 00:08:34 +02:00
this . backHandler = async ( ) => {
const r = await saveDialog ( ) ;
if ( r ) return r ;
2017-08-24 20:10:03 +02:00
if ( ! this . state . note . id ) {
return false ;
}
2017-07-31 21:02:21 +02:00
2018-03-09 22:59:12 +02:00
if ( this . state . mode == 'edit' ) {
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
} ) ;
return true ;
}
return false ;
} ;
2018-03-18 01:00:01 +02:00
this . noteTagDialog _closeRequested = ( ) => {
this . setState ( { noteTagDialogShown : false } ) ;
}
2018-05-02 16:13:20 +02:00
this . onJoplinLinkClick _ = async ( msg ) => {
try {
if ( msg . indexOf ( 'joplin://' ) === 0 ) {
const itemId = msg . substr ( 'joplin://' . length ) ;
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
// the Note screen doesn't handle reloading a different note
this . props . dispatch ( {
type : 'NAV_BACK' ,
} ) ;
setTimeout ( ( ) => {
this . props . dispatch ( {
type : 'NAV_GO' ,
routeName : 'Note' ,
noteId : item . id ,
} ) ;
} , 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 ) {
dialogs . error ( this , error . message ) ;
}
}
2018-10-13 00:25:11 +02:00
2019-05-22 16:56:07 +02:00
this . refreshResource = async ( resource ) => {
2018-10-13 00:25:11 +02:00
if ( ! this . state . note || ! this . state . note . body ) return ;
const resourceIds = await Note . linkedResourceIds ( this . state . note . body ) ;
2018-12-15 02:45:35 +02:00
if ( resourceIds . indexOf ( resource . id ) >= 0 && this . refs . noteBodyViewer ) {
2019-05-22 16:56:07 +02:00
shared . clearResourceCache ( ) ;
2019-03-08 19:14:17 +02:00
const attachedResources = await shared . attachedResources ( this . state . note . body ) ;
this . setState ( { noteResources : attachedResources } , ( ) => {
this . refs . noteBodyViewer . rebuildMd ( ) ;
} ) ;
2018-10-13 00:25:11 +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-05-22 16:56:07 +02:00
this . onMarkForDownload = this . onMarkForDownload . bind ( this ) ;
2017-07-15 01:12:32 +02:00
}
2017-08-01 20:53:50 +02:00
styles ( ) {
const themeId = this . props . theme ;
const theme = themeStyle ( themeId ) ;
if ( this . styles _ [ themeId ] ) return this . styles _ [ themeId ] ;
this . styles _ = { } ;
let styles = {
bodyTextInput : {
flex : 1 ,
paddingLeft : theme . marginLeft ,
paddingRight : theme . marginRight ,
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 ,
} ,
noteBodyViewer : {
flex : 1 ,
paddingLeft : theme . marginLeft ,
paddingRight : theme . marginRight ,
paddingTop : theme . marginTop ,
paddingBottom : theme . marginBottom ,
} ,
2017-08-02 19:47:25 +02:00
metadata : {
paddingLeft : globalStyle . marginLeft ,
paddingRight : globalStyle . marginRight ,
color : theme . color ,
} ,
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
this . styles _ [ themeId ] = StyleSheet . create ( styles ) ;
return this . styles _ [ themeId ] ;
}
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
}
2018-04-30 18:38:19 +02:00
async UNSAFE _componentWillMount ( ) {
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
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-23 16:11:44 +02:00
this . refreshNoteMetadata ( ) ;
2017-07-13 23:50:21 +02:00
}
2019-05-22 16:56:07 +02:00
onMarkForDownload ( event ) {
ResourceFetcher . instance ( ) . markForDownload ( event . resourceId ) ;
}
2017-11-05 20:36:27 +02:00
refreshNoteMetadata ( force = null ) {
return shared . refreshNoteMetadata ( this , force ) ;
2017-05-12 22:23:54 +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
2018-10-03 09:17:37 +02:00
if ( Platform . OS !== 'ios' && this . state . fromShare ) {
2018-07-20 11:04:25 +02:00
ShareExtension . close ( ) ;
}
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 } ) ;
2017-05-12 22:23:54 +02:00
}
2017-06-06 22:01:43 +02:00
body _changeText ( text ) {
2018-03-09 22:59:12 +02:00
shared . noteComponent _change ( this , 'body' , text ) ;
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 ( ) {
let note = this . state . note ;
if ( ! note . id ) return ;
2018-03-09 22:59:12 +02:00
let ok = await dialogs . confirm ( this , _ ( 'Delete note?' ) ) ;
2017-07-15 01:12:32 +02:00
if ( ! ok ) return ;
let folderId = note . parent _id ;
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 ( ) {
return new Promise ( ( resolve , reject ) => {
2018-03-09 22:59:12 +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 ) => {
2018-03-09 22:59:12 +02:00
Image . getSize ( uri , ( width , height ) => {
resolve ( { width : width , height : height } ) ;
} , ( error ) => { reject ( error ) } ) ;
2017-08-02 19:47:25 +02:00
} ) ;
}
2017-11-19 17:18:07 +02:00
showImagePicker ( options ) {
return new Promise ( ( resolve , reject ) => {
2018-12-07 02:07:10 +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
2017-11-19 17:18:07 +02:00
let dimensions = await this . imageDimensions ( localFilePath ) ;
2018-03-09 22:59:12 +02:00
reg . logger ( ) . info ( 'Original dimensions ' , dimensions ) ;
2017-11-19 17:18:07 +02:00
if ( dimensions . width > maxSize || dimensions . height > maxSize ) {
dimensions . width = maxSize ;
dimensions . height = maxSize ;
}
2018-03-09 22:59:12 +02:00
reg . logger ( ) . info ( 'New dimensions ' , dimensions ) ;
2017-11-19 17:18:07 +02:00
2018-03-09 22:59:12 +02:00
const format = mimeType == 'image/png' ? 'PNG' : 'JPEG' ;
reg . logger ( ) . info ( 'Resizing image ' + localFilePath ) ;
2017-11-20 00:08:58 +02:00
const resizedImage = await ImageResizer . createResizedImage ( localFilePath , dimensions . width , dimensions . height , format , 85 ) ; //, 0, targetPath);
2017-12-19 22:14:40 +02:00
2017-11-19 17:18:07 +02:00
const resizedImagePath = resizedImage . uri ;
2018-03-09 22:59:12 +02:00
reg . logger ( ) . info ( 'Resized image ' , resizedImagePath ) ;
reg . logger ( ) . info ( 'Moving ' + resizedImagePath + ' => ' + targetPath ) ;
2017-12-19 22:14:40 +02:00
2017-11-20 00:08:58 +02:00
await RNFS . copyFile ( resizedImagePath , targetPath ) ;
2017-12-19 22:14:40 +02:00
2017-11-19 17:18:07 +02:00
try {
2017-11-20 00:08:58 +02:00
await RNFS . unlink ( resizedImagePath ) ;
2017-11-19 17:18:07 +02:00
} catch ( error ) {
2018-03-09 22:59:12 +02:00
reg . logger ( ) . warn ( 'Error when unlinking cached file: ' , error ) ;
2017-11-19 17:18:07 +02:00
}
}
2018-10-13 11:32:44 +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
2017-11-19 17:18:07 +02:00
const localFilePath = pickerResponse . uri ;
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
}
2018-03-09 22:59:12 +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
2017-08-02 19:47:25 +02:00
let targetPath = Resource . fullPath ( resource ) ;
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' ) {
2017-11-20 00:08:58 +02:00
await this . resizeImage ( localFilePath , targetPath , pickerResponse . mime ) ;
2017-11-19 17:18:07 +02:00
} else {
2018-03-09 22:59:12 +02:00
if ( fileType === 'image' ) {
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 ) ;
2018-05-03 12:31:07 +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 ) ;
if ( ! itDoes ) throw new Error ( 'Resource file was not created: ' + targetPath ) ;
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 ) ;
newNote . body += "\n" + resourceTag ;
this . setState ( { note : newNote } ) ;
2019-06-15 22:23:30 +02:00
this . refreshResource ( resource ) ;
2017-07-13 20:47:31 +02:00
}
2018-12-07 02:07:10 +02:00
async attachPhoto _onPress ( ) {
const response = await this . showImagePicker ( { mediaType : 'photo' } ) ;
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 } ) ;
}
cameraView _onPhoto ( data ) {
this . attachFile ( {
uri : data . uri ,
didCancel : false ,
error : null ,
type : 'image/jpg' ,
} , 'image' ) ;
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 ) ;
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 ( {
message : this . state . note . title + '\n\n' + this . state . note . body ,
title : this . state . note . title ,
} ) ;
}
2017-09-10 18:56:27 +02:00
setAlarm _onPress ( ) {
this . setState ( { alarmDialogShown : true } ) ;
}
async onAlarmDialogAccept ( date ) {
let newNote = Object . assign ( { } , this . state . note ) ;
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-13 20:47:31 +02:00
showMetadata _onPress ( ) {
2017-11-05 02:49:23 +02:00
shared . showMetadata _onPress ( this ) ;
2017-06-04 17:01:52 +02:00
}
2017-07-22 20:16:16 +02:00
async showOnMap _onPress ( ) {
if ( ! this . state . note . id ) return ;
let note = await Note . load ( this . state . note . id ) ;
try {
const url = Note . geolocationUrl ( note ) ;
Linking . openURL ( url ) ;
} catch ( error ) {
await dialogs . error ( this , error . message ) ;
}
}
2018-10-12 20:30:00 +02:00
async showSource _onPress ( ) {
if ( ! this . state . note . id ) return ;
let note = await Note . load ( this . state . note . id ) ;
try {
Linking . openURL ( note . source _url ) ;
} catch ( error ) {
await dialogs . error ( this , error . message ) ;
}
}
2018-05-02 16:13:20 +02:00
copyMarkdownLink _onPress ( ) {
const note = this . state . note ;
Clipboard . setString ( Note . markdownTag ( note ) ) ;
}
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 ;
2018-10-12 20:30:00 +02:00
const hasSource = note && note . source _url ;
2017-07-17 22:22:05 +02:00
2017-09-10 18:57:06 +02:00
let output = [ ] ;
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 ) {
2018-12-07 02:07:10 +02:00
output . push ( { title : _ ( 'Take photo' ) , onPress : ( ) => { this . takePhoto _onPress ( ) ; } } ) ;
2018-10-13 11:32:44 +02:00
output . push ( { title : _ ( 'Attach photo' ) , onPress : ( ) => { this . attachPhoto _onPress ( ) ; } } ) ;
2018-03-09 22:59:12 +02:00
output . push ( { title : _ ( 'Attach any file' ) , onPress : ( ) => { this . attachFile _onPress ( ) ; } } ) ;
2017-11-28 22:31:14 +02:00
output . push ( { isDivider : true } ) ;
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 ) {
2018-03-09 22:59:12 +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
2018-05-10 21:39:41 +02:00
output . push ( { title : _ ( 'Share' ) , onPress : ( ) => { this . share _onPress ( ) ; } } ) ;
2018-03-16 22:17:52 +02:00
if ( isSaved ) output . push ( { title : _ ( 'Tags' ) , onPress : ( ) => { this . tags _onPress ( ) ; } } ) ;
2018-03-09 22:59:12 +02:00
output . push ( { title : isTodo ? _ ( 'Convert to note' ) : _ ( 'Convert to todo' ) , onPress : ( ) => { this . toggleIsTodo _onPress ( ) ; } } ) ;
2018-05-02 16:13:20 +02:00
if ( isSaved ) output . push ( { title : _ ( 'Copy Markdown link' ) , onPress : ( ) => { this . copyMarkdownLink _onPress ( ) ; } } ) ;
2017-11-28 22:31:14 +02:00
output . push ( { isDivider : true } ) ;
2019-06-26 01:13:13 +02:00
output . push ( { title : this . state . showNoteMetadata ? _ ( 'Hide metadata' ) : _ ( 'Show metadata' ) , onPress : ( ) => { this . showMetadata _onPress ( ) ; } } ) ;
2018-03-09 22:59:12 +02:00
output . push ( { title : _ ( 'View on map' ) , onPress : ( ) => { this . showOnMap _onPress ( ) ; } } ) ;
2018-10-12 20:30:00 +02:00
if ( hasSource ) output . push ( { title : _ ( 'Go to source URL' ) , onPress : ( ) => { this . showSource _onPress ( ) ; } } ) ;
2017-11-28 22:31:14 +02:00
output . push ( { isDivider : true } ) ;
2018-03-09 22:59:12 +02:00
output . push ( { title : _ ( 'Delete' ) , onPress : ( ) => { this . deleteNote _onPress ( ) ; } } ) ;
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 ;
2017-07-31 19:56:14 +02:00
let height = event . nativeEvent . contentSize . height ;
this . setState ( { titleTextInputHeight : height } ) ;
}
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 } >
2018-03-09 22:59:12 +02:00
< ScreenHeader / >
2017-07-24 23:52:30 +02:00
< / V i e w >
) ;
}
2017-08-01 20:53:50 +02:00
const theme = themeStyle ( this . props . theme ) ;
2017-05-24 22:51:50 +02:00
const note = this . state . note ;
const isTodo = ! ! Number ( note . is _todo ) ;
2017-07-13 23:50:21 +02:00
const folder = this . state . folder ;
2017-07-24 23:52:30 +02:00
const isNew = ! note . id ;
2017-05-24 22:51:50 +02:00
2018-10-13 11:32:44 +02:00
if ( this . state . showCamera ) {
return < CameraView theme = { this . props . theme } style = { { flex : 1 } } onPhoto = { this . cameraView _onPhoto } onCancel = { this . cameraView _onCancel } / >
}
2017-07-10 23:34:26 +02:00
let bodyComponent = null ;
2018-03-09 22:59:12 +02:00
if ( this . state . mode == 'view' ) {
const onCheckboxChange = ( newBody ) => {
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 ) {
2018-12-16 19:32:42 +02:00
const parsedQuery = SearchEngine . instance ( ) . parseQuery ( this . props . searchQuery ) ;
keywords = SearchEngine . instance ( ) . allParsedQueryTerms ( parsedQuery ) ;
}
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.
bodyComponent = ! note || ! note . body . trim ( ) ? null : < NoteBodyViewer
2018-05-09 19:04:48 +02:00
onJoplinLinkClick = { this . onJoplinLinkClick _ }
2018-10-13 00:25:11 +02:00
ref = "noteBodyViewer"
2018-05-09 19:04:48 +02:00
style = { this . styles ( ) . noteBodyViewer }
webViewStyle = { theme }
note = { note }
2019-03-08 19:14:17 +02:00
noteResources = { this . state . noteResources }
2018-12-16 19:32:42 +02:00
highlightedKeywords = { keywords }
2019-03-08 19:14:17 +02:00
theme = { this . props . theme }
2018-05-09 19:04:48 +02:00
onCheckboxChange = { ( newBody ) => { onCheckboxChange ( newBody ) } }
2019-05-22 16:56:07 +02:00
onMarkForDownload = { this . onMarkForDownload }
2018-12-27 23:49:19 +02:00
onLoadEnd = { ( ) => {
setTimeout ( ( ) => {
2018-12-29 04:12:23 +02:00
this . setState ( { HACK _webviewLoadingState : 1 } ) ;
2018-12-27 23:49:19 +02:00
setTimeout ( ( ) => {
2018-12-29 04:12:23 +02:00
this . setState ( { HACK _webviewLoadingState : 0 } ) ;
2018-12-27 23:49:19 +02:00
} , 50 ) ;
2018-12-29 04:12:23 +02:00
} , 5 ) ;
2018-12-27 23:49:19 +02:00
} }
2018-05-09 19:04:48 +02:00
/ >
2017-07-10 23:34:26 +02:00
} else {
2017-07-24 23:52:30 +02:00
const focusBody = ! isNew && ! ! note . title ;
2017-10-30 23:29:36 +02:00
// Note: blurOnSubmit is necessary to get multiline to work.
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
2017-07-15 01:12:32 +02:00
bodyComponent = (
< TextInput
2017-07-16 18:31:42 +02:00
autoCapitalize = "sentences"
2017-07-24 23:52:30 +02:00
autoFocus = { focusBody }
2017-08-01 20:53:50 +02:00
style = { this . styles ( ) . bodyTextInput }
2017-07-15 01:12:32 +02:00
multiline = { true }
value = { note . body }
2018-03-09 22:59:12 +02:00
onChangeText = { ( text ) => this . body _changeText ( text ) }
2017-10-30 23:29:36 +02:00
blurOnSubmit = { false }
2018-04-11 19:25:07 +02:00
selectionColor = { theme . textSelectionColor }
2017-07-15 01:12:32 +02:00
/ >
) ;
2017-07-10 23:34:26 +02:00
}
2017-07-14 01:35:37 +02:00
const renderActionButton = ( ) => {
let buttons = [ ] ;
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' } ) ;
2017-07-14 01:35:37 +02:00
} ,
} ) ;
2018-05-01 11:09:36 +02:00
if ( this . state . mode == 'edit' ) return null ; //<ActionButton style={{display:'none'}}/>;
2017-07-15 01:12:32 +02:00
2018-03-09 22:59:12 +02:00
return < ActionButton multiStates = { true } buttons = { buttons } buttonIndex = { 0 } / >
}
2017-07-14 01:35:37 +02:00
const actionButtonComp = renderActionButton ( ) ;
2018-03-09 22:59:12 +02:00
let showSaveButton = this . state . mode == 'edit' || this . isModified ( ) || this . saveButtonHasBeenShown _ ;
2017-07-16 12:17:40 +02:00
let saveButtonDisabled = ! this . isModified ( ) ;
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 ;
let titleTextInputStyle = {
flex : 1 ,
2018-12-27 23:49:19 +02:00
marginTop : 0 ,
2017-08-01 20:53:50 +02:00
paddingLeft : 0 ,
color : theme . color ,
backgroundColor : theme . backgroundColor ,
2018-03-09 22:59:12 +02:00
fontWeight : 'bold' ,
2017-08-01 20:53:50 +02:00
fontSize : theme . fontSize ,
2017-11-19 17:19:36 +02:00
paddingTop : 10 , // Added for iOS (Not needed for Android??)
paddingBottom : 10 , // Added for iOS (Not needed for Android??)
2017-08-01 20:53:50 +02:00
} ;
2017-07-21 23:40:02 +02:00
2017-11-19 17:19:36 +02:00
if ( this . enableMultilineTitle _ ) titleTextInputStyle . height = this . state . titleTextInputHeight ;
2017-07-31 19:56:14 +02:00
2017-08-21 20:32:43 +02:00
let checkboxStyle = {
color : theme . color ,
paddingRight : 10 ,
paddingLeft : theme . marginLeft ,
2017-11-19 17:19:36 +02:00
paddingTop : 10 , // Added for iOS (Not needed for Android??)
paddingBottom : 10 , // Added for iOS (Not needed for Android??)
2018-03-09 22:59:12 +02:00
}
2017-08-21 20:32:43 +02:00
2018-12-27 23:49:19 +02:00
if ( this . state . HACK _webviewLoadingState === 1 ) {
titleTextInputStyle . marginTop = 1 ;
}
2017-09-10 18:56:27 +02:00
const dueDate = isTodo && note . todo _due ? new Date ( note . todo _due ) : null ;
2017-07-31 19:56:14 +02:00
const titleComp = (
< View style = { titleContainerStyle } >
2018-03-09 22:59:12 +02:00
{ isTodo && < Checkbox style = { checkboxStyle } checked = { ! ! Number ( note . todo _completed ) } onChange = { ( checked ) => { this . todoCheckbox _change ( checked ) } } / > }
2017-07-31 19:56:14 +02:00
< TextInput
2018-03-09 22:59:12 +02:00
onContentSizeChange = { ( event ) => this . titleTextInput _contentSizeChange ( event ) }
2017-07-31 19:56:14 +02:00
autoFocus = { isNew }
2017-11-19 17:19:36 +02:00
multiline = { this . enableMultilineTitle _ }
2017-07-31 19:56:14 +02:00
underlineColorAndroid = "#ffffff00"
autoCapitalize = "sentences"
style = { titleTextInputStyle }
value = { note . title }
2018-03-09 22:59:12 +02:00
onChangeText = { ( text ) => this . title _changeText ( text ) }
2018-04-11 19:25:07 +02:00
selectionColor = { theme . textSelectionColor }
2017-07-31 19:56:14 +02:00
/ >
< / V i e w >
) ;
2018-03-18 01:00:01 +02:00
const noteTagDialog = ! this . state . noteTagDialogShown ? null : < NoteTagsDialog onCloseRequested = { this . noteTagDialog _closeRequested } / > ;
2017-05-12 22:23:54 +02:00
return (
2018-02-05 20:32:59 +02:00
< View style = { this . rootStyle ( this . props . theme ) . root } >
2017-07-16 12:17:40 +02:00
< ScreenHeader
2017-11-23 20:47:51 +02:00
folderPickerOptions = { {
enabled : true ,
selectedFolderId : folder ? folder . id : null ,
2017-07-16 18:06:05 +02:00
onValueChange : async ( itemValue , itemIndex ) => {
2018-05-01 19:53:45 +02:00
if ( ! note . id ) {
await this . saveNoteButton _press ( itemValue ) ;
} else {
await Note . moveToFolder ( note . id , itemValue ) ;
}
2017-07-16 18:06:05 +02:00
note . parent _id = itemValue ;
const folder = await Folder . load ( note . parent _id ) ;
this . setState ( {
lastSavedNote : Object . assign ( { } , note ) ,
note : note ,
folder : folder ,
} ) ;
2017-11-23 20:47:51 +02:00
} ,
2017-07-16 18:06:05 +02:00
} }
2017-07-16 12:17:40 +02:00
menuOptions = { this . menuOptions ( ) }
showSaveButton = { showSaveButton }
saveButtonDisabled = { saveButtonDisabled }
onSaveButtonPress = { ( ) => this . saveNoteButton _press ( ) }
/ >
2018-03-09 22:59:12 +02:00
{ titleComp }
{ bodyComponent }
{ actionButtonComp }
{ this . state . showNoteMetadata && < Text style = { this . styles ( ) . metadata } > { this . state . noteMetadata } < / T e x t > }
< SelectDateTimeDialog
shown = { this . state . alarmDialogShown }
date = { dueDate }
onAccept = { ( date ) => this . onAlarmDialogAccept ( date ) }
onReject = { ( ) => this . onAlarmDialogReject ( ) }
2018-03-09 19:49:35 +02:00
/ >
2018-03-09 22:59:12 +02:00
< DialogBox ref = { dialogbox => { this . dialogbox = dialogbox } } / >
2018-03-18 01:00:01 +02:00
{ noteTagDialog }
2018-02-05 20:32:59 +02:00
< / V i e w >
2017-05-12 22:23:54 +02:00
) ;
}
2018-03-09 22:59:12 +02:00
2017-05-12 22:23:54 +02:00
}
2018-03-09 22:59:12 +02:00
const NoteScreen = connect (
( state ) => {
return {
noteId : state . selectedNoteIds . length ? state . selectedNoteIds [ 0 ] : null ,
folderId : state . selectedFolderId ,
itemType : state . selectedItemType ,
folders : state . folders ,
2018-12-16 19:32:42 +02:00
searchQuery : state . searchQuery ,
2018-03-09 22:59:12 +02:00
theme : state . settings . theme ,
2018-12-28 22:40:29 +02:00
ftsEnabled : state . settings [ 'db.ftsEnabled' ] ,
2018-07-20 11:04:25 +02:00
sharedData : state . sharedData ,
2018-03-09 22:59:12 +02:00
} ;
}
) ( NoteScreenComponent )
2018-07-20 11:04:25 +02:00
module . exports = { NoteScreen } ;