From 19266206f4c1b6965afa24766a52e6658e002d47 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 1 Aug 2017 23:40:14 +0200 Subject: [PATCH] Attach resource from mobile --- CliClient/app/import-enex-md-gen.js | 2 + ReactNativeClient/android/app/build.gradle | 1 + .../com/awesomeproject/MainApplication.java | 4 +- ReactNativeClient/android/settings.gradle | 5 ++- .../lib/components/note-body-viewer.js | 7 ++-- .../lib/components/screens/note.js | 42 ++++++++++++++++++- .../lib/file-api-driver-onedrive.js | 14 ++++--- ReactNativeClient/lib/file-api.js | 4 +- ReactNativeClient/lib/models/resource.js | 22 ++++++++++ ReactNativeClient/lib/onedrive-api.js | 4 +- ReactNativeClient/lib/shim-init-react.js | 24 ++++++++++- ReactNativeClient/lib/shim.js | 3 +- ReactNativeClient/lib/synchronizer.js | 23 ++++++---- ReactNativeClient/package.json | 1 + 14 files changed, 131 insertions(+), 25 deletions(-) diff --git a/CliClient/app/import-enex-md-gen.js b/CliClient/app/import-enex-md-gen.js index 67d95d743..0f22fe33b 100644 --- a/CliClient/app/import-enex-md-gen.js +++ b/CliClient/app/import-enex-md-gen.js @@ -164,6 +164,8 @@ function isImageMimeType(m) { } function addResourceTag(lines, resource, alt = "") { + // TODO: refactor to use Resource.markdownTag + let tagAlt = alt == "" ? resource.alt : alt; if (!tagAlt) tagAlt = ''; if (isImageMimeType(resource.mime)) { diff --git a/ReactNativeClient/android/app/build.gradle b/ReactNativeClient/android/app/build.gradle index 6943a04a9..f10ba5f5d 100644 --- a/ReactNativeClient/android/app/build.gradle +++ b/ReactNativeClient/android/app/build.gradle @@ -143,6 +143,7 @@ dependencies { compile "com.facebook.react:react-native:+" // From node_modules compile project(':react-native-sqlite-storage') compile project(':react-native-fetch-blob') + compile project(':react-native-document-picker') } // Run this once to be able to run the application with BUCK diff --git a/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java b/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java index e0840bffc..f8b1c71b5 100644 --- a/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java +++ b/ReactNativeClient/android/app/src/main/java/com/awesomeproject/MainApplication.java @@ -10,6 +10,7 @@ import com.facebook.react.shell.MainReactPackage; import com.facebook.soloader.SoLoader; import org.pgsqlite.SQLitePluginPackage; import com.RNFetchBlob.RNFetchBlobPackage; +import com.reactnativedocumentpicker.ReactNativeDocumentPicker; import java.util.Arrays; import java.util.List; @@ -28,7 +29,8 @@ public class MainApplication extends Application implements ReactApplication { new SQLitePluginPackage(), new MainReactPackage(), new RNFSPackage(), - new RNFetchBlobPackage() + new RNFetchBlobPackage(), + new ReactNativeDocumentPicker() ); } }; diff --git a/ReactNativeClient/android/settings.gradle b/ReactNativeClient/android/settings.gradle index 5fe7bbd3f..99b4bd5cc 100644 --- a/ReactNativeClient/android/settings.gradle +++ b/ReactNativeClient/android/settings.gradle @@ -8,4 +8,7 @@ include ':react-native-sqlite-storage' project(':react-native-sqlite-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sqlite-storage/src/android') include ':react-native-fetch-blob' -project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android') \ No newline at end of file +project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android') + +include ':react-native-document-picker' +project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android') \ No newline at end of file diff --git a/ReactNativeClient/lib/components/note-body-viewer.js b/ReactNativeClient/lib/components/note-body-viewer.js index dca9989b8..8f58bc3ca 100644 --- a/ReactNativeClient/lib/components/note-body-viewer.js +++ b/ReactNativeClient/lib/components/note-body-viewer.js @@ -126,13 +126,14 @@ class NoteBodyViewer extends Component { } const r = this.state.resources[resourceId]; - if (r.mime == 'image/png' || r.mime == 'image/jpg' || r.mime == 'image/gif') { + const mime = r.mime.toLowerCase(); + if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') { const src = 'data:' + r.mime + ';base64,' + r.base64; - let output = ''; + let output = ''; return output; } - return '[Image: ' + htmlentities(r.title) + '(' + htmlentities(r.mime) + ')]'; + return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]'; } let styleHtml = ''; diff --git a/ReactNativeClient/lib/components/screens/note.js b/ReactNativeClient/lib/components/screens/note.js index bbf93c10a..5101c8511 100644 --- a/ReactNativeClient/lib/components/screens/note.js +++ b/ReactNativeClient/lib/components/screens/note.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking } from 'react-native'; import { connect } from 'react-redux' +import { uuid } from 'lib/uuid.js'; import { Log } from 'lib/log.js' import { Note } from 'lib/models/note.js' import { Resource } from 'lib/models/resource.js' @@ -19,6 +20,9 @@ import { dialogs } from 'lib/dialogs.js'; import { globalStyle, themeStyle } from 'lib/components/global-style.js'; import DialogBox from 'react-native-dialogbox'; import { NoteBodyViewer } from 'lib/components/note-body-viewer.js'; +import RNFetchBlob from 'react-native-fetch-blob'; +import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker'; + class NoteScreenComponent extends BaseScreenComponent { @@ -247,8 +251,42 @@ class NoteScreenComponent extends BaseScreenComponent { }); } - attachFile_onPress() { + async pickDocument() { + return new Promise((resolve, reject) => { + DocumentPicker.show({ filetype: [DocumentPickerUtil.images()] }, (error,res) => { + if (error) { + reject(error); + return; + } + resolve(res); + }); + }); + } + + async attachFile_onPress() { + const res = await this.pickDocument(); + + // res.uri, + // res.type, // mime type + // res.fileName, + // res.fileSize + + let resource = Resource.new(); + resource.id = uuid.create(); + resource.mime = res.type; + resource.title = res.fileName ? res.fileName : _('Untitled'); + + const targetPath = Resource.fullPath(resource); + RNFetchBlob.fs.cp(res.uri, targetPath); + + await Resource.save(resource, { isNew: true }); + + const resourceTag = Resource.markdownTag(resource); + + const newNote = Object.assign({}, this.state.note); + newNote.body += "\n" + resourceTag; + this.setState({ note: newNote }); } toggleIsTodo_onPress() { @@ -279,7 +317,7 @@ class NoteScreenComponent extends BaseScreenComponent { const note = this.state.note; return [ - // { title: _('Attach file'), onPress: () => { this.attachFile_onPress(); } }, + { title: _('Attach file'), onPress: () => { this.attachFile_onPress(); } }, { title: _('Delete note'), onPress: () => { this.deleteNote_onPress(); } }, { title: note && !!note.is_todo ? _('Convert to regular note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } }, { title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } }, diff --git a/ReactNativeClient/lib/file-api-driver-onedrive.js b/ReactNativeClient/lib/file-api-driver-onedrive.js index 0662f767b..9874e5d66 100644 --- a/ReactNativeClient/lib/file-api-driver-onedrive.js +++ b/ReactNativeClient/lib/file-api-driver-onedrive.js @@ -126,11 +126,15 @@ class FileApiDriverOneDrive { return this.makeItem_(item); } - put(path, content) { - let options = { - headers: { 'Content-Type': 'text/plain' }, - }; - return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options); + put(path, content, options = null) { + if (!options) options = {}; + + if (options.source == 'file') { + return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, null, options); + } else { + options.headers = { 'Content-Type': 'text/plain' }; + return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options); + } } delete(path) { diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index 9d11dcbef..855f57863 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -87,9 +87,9 @@ class FileApi { return this.driver_.get(this.fullPath_(path), options); } - put(path, content) { + put(path, content, options = null) { this.logger().debug('put ' + this.fullPath_(path)); - return this.driver_.put(this.fullPath_(path), content); + return this.driver_.put(this.fullPath_(path), content, options); } delete(path) { diff --git a/ReactNativeClient/lib/models/resource.js b/ReactNativeClient/lib/models/resource.js index 69810de77..64d20cfd5 100644 --- a/ReactNativeClient/lib/models/resource.js +++ b/ReactNativeClient/lib/models/resource.js @@ -3,6 +3,7 @@ import { BaseItem } from 'lib/models/base-item.js'; import { Setting } from 'lib/models/setting.js'; import { mime } from 'lib/mime-utils.js'; import { filename } from 'lib/path-utils.js'; +import { FsDriverDummy } from 'lib/fs-driver-dummy.js'; import lodash from 'lodash'; class Resource extends BaseItem { @@ -15,6 +16,11 @@ class Resource extends BaseItem { return BaseModel.TYPE_RESOURCE; } + static isSupportedImageMimeType(type) { + const imageMimeTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif"]; + return imageMimeTypes.indexOf(type.toLowerCase()) >= 0; + } + static fsDriver() { if (!Resource.fsDriver_) Resource.fsDriver_ = new FsDriverDummy(); return Resource.fsDriver_; @@ -32,6 +38,22 @@ class Resource extends BaseItem { return Setting.value('resourceDir') + '/' + resource.id + extension; } + static markdownTag(resource) { + let tagAlt = resource.alt ? resource.alt : resource.title; + if (!tagAlt) tagAlt = ''; + let lines = []; + if (Resource.isSupportedImageMimeType(resource.mime)) { + lines.push("!["); + lines.push(tagAlt); + lines.push("](:/" + resource.id + ")"); + } else { + lines.push("["); + lines.push(tagAlt); + lines.push("](:/" + resource.id + ")"); + } + return lines.join(''); + } + static pathToId(path) { return filename(path); } diff --git a/ReactNativeClient/lib/onedrive-api.js b/ReactNativeClient/lib/onedrive-api.js index 71c4c3b5d..9d87bf3cd 100644 --- a/ReactNativeClient/lib/onedrive-api.js +++ b/ReactNativeClient/lib/onedrive-api.js @@ -163,7 +163,9 @@ class OneDriveApi { let response = null; try { - if (options.target == 'string') { + if (options.source == 'file' && (method == 'POST' || method == 'PUT')) { + response = await shim.uploadBlob(url, options); + } else if (options.target == 'string') { response = await shim.fetch(url, options); } else { // file response = await shim.fetchBlob(url, options); diff --git a/ReactNativeClient/lib/shim-init-react.js b/ReactNativeClient/lib/shim-init-react.js index a1d520d5b..3d4277103 100644 --- a/ReactNativeClient/lib/shim-init-react.js +++ b/ReactNativeClient/lib/shim-init-react.js @@ -7,7 +7,6 @@ function shimInit() { shim.fetchBlob = async function(url, options) { if (!options || !options.path) throw new Error('fetchBlob: target file path is missing'); - if (!options.method) options.method = 'GET'; let headers = options.headers ? options.headers : {}; let method = options.method ? options.method : 'GET'; @@ -39,6 +38,29 @@ function shimInit() { } } + shim.uploadBlob = async function(url, options) { + if (!options || !options.path) throw new Error('uploadBlob: source file path is missing'); + + const headers = options.headers ? options.headers : {}; + const method = options.method ? options.method : 'POST'; + + try { + let response = await RNFetchBlob.fetch(method, url, headers, RNFetchBlob.wrap(options.path)); + + // Returns an object that's roughtly compatible with a standard Response object + return { + ok: response.respInfo.status < 400, + data: response.data, + text: response.text, + json: response.json, + status: response.respInfo.status, + headers: response.respInfo.headers, + }; + } catch (error) { + throw new Error('uploadBlob: ' + method + ' ' + url + ': ' + error.toString()); + } + } + shim.readLocalFileBase64 = async function(path) { return RNFetchBlob.fs.readFile(path, 'base64') } diff --git a/ReactNativeClient/lib/shim.js b/ReactNativeClient/lib/shim.js index ed7f383f9..cffc79057 100644 --- a/ReactNativeClient/lib/shim.js +++ b/ReactNativeClient/lib/shim.js @@ -2,7 +2,7 @@ let shim = {}; shim.isNode = () => { if (typeof process === 'undefined') return false; - return process.title = 'node'; + return process.title == 'node'; }; shim.isReactNative = () => { @@ -14,5 +14,6 @@ shim.FormData = typeof FormData !== 'undefined' ? FormData : null; shim.fs = null; shim.FileApiDriverLocal = null; shim.readLocalFileBase64 = () => { throw new Error('Not implemented'); } +shim.uploadBlob = () => { throw new Error('Not implemented'); } export { shim }; \ No newline at end of file diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index c25b67227..ea5df8c41 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -7,6 +7,7 @@ import { sprintf } from 'sprintf-js'; import { time } from 'lib/time-utils.js'; import { Logger } from 'lib/logger.js' import { _ } from 'lib/locale.js'; +import { shim } from 'lib/shim.js'; import moment from 'moment'; class Synchronizer { @@ -235,15 +236,21 @@ class Synchronizer { if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) { let remoteContentPath = this.resourceDirName_ + '/' + local.id; - let resourceContent = ''; - try { - resourceContent = await Resource.content(local); - } catch (error) { - error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message; - this.logger().error(error); - this.progressReport_.errors.push(error); + // TODO: handle node and mobile in the same way + if (shim.isNode()) { + let resourceContent = ''; + try { + resourceContent = await Resource.content(local); + } catch (error) { + error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message; + this.logger().error(error); + this.progressReport_.errors.push(error); + } + await this.api().put(remoteContentPath, resourceContent); + } else { + const localResourceContentPath = Resource.fullPath(local); + await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' }); } - await this.api().put(remoteContentPath, resourceContent); } if (action == 'createRemote' || action == 'updateRemote') { diff --git a/ReactNativeClient/package.json b/ReactNativeClient/package.json index 04d221b26..41f2e3787 100644 --- a/ReactNativeClient/package.json +++ b/ReactNativeClient/package.json @@ -17,6 +17,7 @@ "react-native": "0.46.0", "react-native-action-button": "^2.6.9", "react-native-dialogbox": "^0.6.6", + "react-native-document-picker": "^2.0.0", "react-native-fetch-blob": "^0.10.6", "react-native-fs": "^2.3.3", "react-native-popup-menu": "^0.7.4",