From 22311a3edfd3d844aed9c76d45e84aff5eb1d740 Mon Sep 17 00:00:00 2001
From: Martin Santangelo <msantang78@gmail.com>
Date: Fri, 1 Nov 2019 11:51:25 -0300
Subject: [PATCH] (fix) ios video preview and upload with ph:// paths

---
 package.json                         |   1 +
 src/capture/CapturePreview.js        |  34 +++---
 src/common/stores/AttachmentStore.js | 168 +++++++++++----------------
 src/media/MindsVideo.js              |  29 +++--
 yarn.lock                            |   5 +
 5 files changed, 112 insertions(+), 125 deletions(-)

diff --git a/package.json b/package.json
index 4acb4c0b97..05f4472f6b 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
     "react-native-actionsheet": "^2.4.2",
     "react-native-animatable": "^1.3.3",
     "react-native-collapsible-header-views": "^1.0.2",
+    "react-native-convert-ph-asset": "^1.0.3",
     "react-native-device-info": "^4.0.1",
     "react-native-device-log": "Minds/react-native-device-log#74f06b09c6656aa228a9a3a474c714d82abf509e",
     "react-native-elements": "^0.19.1",
diff --git a/src/capture/CapturePreview.js b/src/capture/CapturePreview.js
index 45cc4d9b03..33d3713c80 100644
--- a/src/capture/CapturePreview.js
+++ b/src/capture/CapturePreview.js
@@ -1,5 +1,5 @@
 import React, { PureComponent } from 'react';
-import { StyleSheet, View, Image } from 'react-native';
+import { StyleSheet, View, Image, Platform } from 'react-native';
 
 import MindsVideo from '../media/MindsVideo';
 
@@ -15,25 +15,29 @@ export default class CapturePreview extends PureComponent {
     switch (this.props.type) {
       case 'image/gif':
       case 'image/jpeg':
+      case 'image':
       default:
-        body = <Image
-          resizeMode='contain'
-          source={{ uri: this.props.uri }}
-          style={styles.preview}
-        />
+        body = (
+          <Image
+            resizeMode="contain"
+            source={{uri: this.props.uri}}
+            style={styles.preview}
+          />
+        );
         break;
       case 'video/mp4':
-        body = <View style={styles.preview}>
-          <MindsVideo video={{ 'uri': this.props.uri }} />
-        </View>
+      case 'video/quicktime':
+      case 'video/x-m4v':
+      case 'video':
+        body = (
+          <View style={styles.preview}>
+            <MindsVideo video={{uri: this.props.uri}} />
+          </View>
+        );
         break;
     }
 
-    return (
-      <View style={styles.wrapper}>
-        {body}
-      </View>
-    );
+    return <View style={styles.wrapper}>{body}</View>;
   }
 
 }
@@ -43,7 +47,7 @@ const styles = StyleSheet.create({
     flex: 1,
     flexDirection: 'row',
     alignItems: 'stretch',
-    backgroundColor: 'black'
+    backgroundColor: 'black',
   },
   preview: {
     flex: 1,
diff --git a/src/common/stores/AttachmentStore.js b/src/common/stores/AttachmentStore.js
index e41f47605f..ff01601cb3 100644
--- a/src/common/stores/AttachmentStore.js
+++ b/src/common/stores/AttachmentStore.js
@@ -1,14 +1,11 @@
-import { observable, action, extendObservable } from 'mobx'
-import { Platform, Alert } from 'react-native';
-import rnFS from 'react-native-fs';
-import MediaMeta from 'react-native-media-meta';
-import fileType from 'react-native-file-type';
+import {observable, action} from 'mobx';
+import {Alert, Platform} from 'react-native';
+import RNConvertPhAsset from 'react-native-convert-ph-asset';
 
 import attachmentService from '../services/attachment.service';
-import {MINDS_MAX_VIDEO_LENGTH} from '../../config/Config';
-import mindsService from '../services/minds.service';
 import logService from '../services/log.service';
 import i18n from '../services/i18n.service';
+import mindsService from '../services/minds.service';
 
 /**
  * Attachment Store
@@ -16,15 +13,14 @@ import i18n from '../services/i18n.service';
 export default class AttachmentStore {
   @observable hasAttachment = false;
   @observable uploading = false;
-  @observable checkingVideoLength = false;
   @observable progress = 0;
-
-  guid = '';
-
-  @observable uri  = '';
+  @observable uri = '';
   @observable type = '';
   @observable license = '';
-  tempIosVideo = '';
+
+  guid = '';
+  fileName = null;
+  transcoding = false;
 
   /**
    * Attach media
@@ -33,14 +29,10 @@ export default class AttachmentStore {
    */
   @action
   async attachMedia(media, extra = null) {
-
-    // no new media acepted if we are checking for video length
-    if (this.checkingVideoLength) return;
-
-    // validate media
-    const valid = await this.validate(media);
-    if (!valid) return;
-
+    if (this.transcoding) {
+      return;
+    }
+    console.log('ATTACHING', media, extra);
     if (this.uploading) {
       // abort current upload
       this.cancelCurrentUpload();
@@ -50,16 +42,45 @@ export default class AttachmentStore {
         await attachmentService.deleteMedia(this.guid);
       } catch (error) {
         // we ignore delete error for now
-        logService.info('Error deleting the uploaded media '+this.guid);
+        logService.info('Error deleting the uploaded media ' + this.guid);
       }
     }
 
-    this.uri  = media.uri;
-    this.type = media.type;
+    if (!await this.validate(media)) {
+      return;
+    }
+
     this.setHasAttachment(true);
 
+    // correctly handle videos from ph:// paths on ios
+    if (
+      Platform.OS === 'ios' &&
+      media.type === 'video' &&
+      media.uri.startsWith('ph://')
+    ) {
+      try {
+        this.transcoding = true;
+        const converted = await RNConvertPhAsset.convertVideoFromUrl({
+          url: media.uri,
+          convertTo: 'm4v',
+          quality: 'high',
+        });
+        media.type = converted.mimeType;
+        media.uri = converted.path;
+        media.filename = converted.filename;
+      } catch (error) {
+        Alert.alert('Error reading the video', 'Please try again');
+      } finally {
+        this.transcoding = false;
+      }
+    }
+
+    this.uri = media.uri;
+    this.type = media.type;
+    this.fileName = media.fileName;
+
     try {
-      const uploadPromise = attachmentService.attachMedia(media, extra, (pct) => {
+      const uploadPromise = attachmentService.attachMedia(media, extra, pct => {
         this.setProgress(pct);
       });
 
@@ -70,7 +91,9 @@ export default class AttachmentStore {
 
       const result = await uploadPromise;
       // ignore canceled
-      if ((uploadPromise.isCanceled && uploadPromise.isCanceled()) || !result) return;
+      if ((uploadPromise.isCanceled && uploadPromise.isCanceled()) || !result) {
+        return;
+      }
       this.guid = result.guid;
     } catch (err) {
       this.clear();
@@ -79,82 +102,31 @@ export default class AttachmentStore {
       this.setUploading(false);
     }
 
-    // delete temp ios video if necessary
-    if (this.tempIosVideo) {
-      rnFS.unlink(this.tempIosVideo);
-      this.tempIosVideo = '';
-    }
-
     return this.guid;
   }
 
-  /**
-   * Cancel current upload promise and request
-   */
-  cancelCurrentUpload(clear=true)
-  {
-    this.uploadPromise && this.uploadPromise.cancel(() => {
-      if (clear) this.clear();
-    });
-  }
-
-  /**
-   * Validate media
-   * @param {object} media
-   */
-  @action
   async validate(media) {
-
-    if(!media.type){
-      const type = await fileType(media.path);
-      media.type = type.mime;
-    }
-
-    if (media.fileName && media.fileName.includes(' ')) media.fileName = media.fileName.replace(/\s/g, "_");
-
     const settings = await mindsService.getSettings();
-
-    let videoPath = null;
-    switch (media.type) {
-      case 'video/mp4':
-        videoPath = media.path || media.uri.replace(/^.*:\/\//, '');
-        break;
-      case 'ALAssetTypeVideo':
-        // if video is selected from cameraroll we need to copy
-        await this.copyVideoIos(media);
-        videoPath = this.tempIosVideo;
-        media.type = 'video/mp4';
-        media.path = videoPath;
-        media.uri  = 'file:\/\/'+videoPath;
-        break;
+    if (media.duration && media.duration > settings.max_video_length * 1000) {
+      Alert.alert(
+        i18n.t('sorry'),
+        i18n.t('attachment.tooLong', {minutes: settings.max_video_length / 60}),
+      );
+      return false;
     }
-
-    if (videoPath) {
-      this.checkingVideoLength = true;
-      const meta = await MediaMeta.get(videoPath);
-
-      this.checkingVideoLength = false;
-
-      // check video length
-      if (meta.duration && meta.duration > (settings.max_video_length * 1000) ) {
-        Alert.alert(
-          i18n.t('sorry'),
-          i18n.t('attachment.tooLong', {minutes: (settings.max_video_length / 60)})
-        );
-        return false;
-      }
-    }
-
     return true;
   }
 
   /**
-   * copy a video from ios library assets to temporal app folder
-   * @param {object} media
+   * Cancel current upload promise and request
    */
-  copyVideoIos(media) {
-    this.tempIosVideo = rnFS.TemporaryDirectoryPath+'MINDS-'+Date.now()+'.MP4'
-    return rnFS.copyAssetsVideoIOS(media.uri, this.tempIosVideo);
+  cancelCurrentUpload(clear = true) {
+    this.uploadPromise &&
+      this.uploadPromise.cancel(() => {
+        if (clear) {
+          this.clear();
+        }
+      });
   }
 
   /**
@@ -165,7 +137,7 @@ export default class AttachmentStore {
       try {
         attachmentService.deleteMedia(this.guid);
         this.clear();
-        return true
+        return true;
       } catch (err) {
         return false;
       }
@@ -177,12 +149,12 @@ export default class AttachmentStore {
 
   @action
   setProgress(value) {
-    this.progress = value
+    this.progress = value;
   }
 
   @action
   setUploading(value) {
-    this.uploading = value
+    this.uploading = value;
   }
 
   @action
@@ -205,11 +177,5 @@ export default class AttachmentStore {
     this.checkingVideoLength = false;
     this.uploading = false;
     this.progress = 0;
-
-    if (this.tempIosVideo) {
-      rnFS.unlink(this.tempIosVideo);
-      this.tempIosVideo = '';
-    }
   }
-
-}
\ No newline at end of file
+}
diff --git a/src/media/MindsVideo.js b/src/media/MindsVideo.js
index 2850dbc2fc..926dcedc20 100644
--- a/src/media/MindsVideo.js
+++ b/src/media/MindsVideo.js
@@ -40,28 +40,39 @@ class MindsVideo extends Component {
       paused: true,
       volume: 1,
       loaded: true,
-      active: false,
+      active: !props.entity,
       showOverlay: true,
       fullScreen: false,
       error: false,
       inProgress: false,
+      video: {},
     };
   }
 
+  /**
+   * Derive state from props
+   * @param {object} nextProps
+   * @param {object} prevState
+   */
+  static getDerivedStateFromProps(nextProps, prevState) {
+    if (nextProps.video && nextProps.video.uri !== prevState.video.uri) {
+      return {
+        video: {uri: nextProps.video.uri},
+      };
+    }
+    return null;
+  }
+
   /**
    * On component will mount
    */
-  componentWillMount () {
+  componentDidMount() {
     this._panResponder = PanResponder.create({
       onStartShouldSetPanResponder: (evt, gestureState) => true,
       onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
       onMoveShouldSetPanResponder: (evt, gestureState) => true,
       onMoveShouldSetPanResponderCapture: (evt, gestureState) => true
-    })
-
-    if (!this.props.entity) {
-      this.setState({active: true})
-    }
+    });
 
     this.onScreenBlur = this.props.navigation.addListener(
       'didBlur',
@@ -103,7 +114,7 @@ class MindsVideo extends Component {
   };
 
   onError = (err) => {
-    logService.exception('[MindsVideo]', err)
+    logService.exception('[MindsVideo]', new Error(err));
     this.setState({ error: true, inProgress: false, });
   };
 
@@ -244,7 +255,7 @@ class MindsVideo extends Component {
           onProgress = {this.onProgress}
           onError={this.onError}
           ignoreSilentSwitch={'obey'}
-          source={{ uri: video.uri.replace('file://',''), type: 'mp4' }}
+          source={this.state.video}
           paused={paused}
           fullscreen={this.state.fullScreen}
           resizeMode={"contain"}
diff --git a/yarn.lock b/yarn.lock
index 4694bfa65c..e5c96c7c71 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7376,6 +7376,11 @@ react-native-collapsible-header-views@^1.0.2:
   dependencies:
     fast-memoize "^2.5.1"
 
+react-native-convert-ph-asset@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/react-native-convert-ph-asset/-/react-native-convert-ph-asset-1.0.3.tgz#af299a6a3f7270b55c3bdfe472a40cea02c93c80"
+  integrity sha512-qdTrUsDlxOvC7KnamjjRnxaijr3myfOwzbYR0vyTl0BjPT25jkCQ9a4Q26egRdy8uqiw7mqB1ZhG5OohwnpsOg==
+
 react-native-crypto@^2.0.1:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/react-native-crypto/-/react-native-crypto-2.2.0.tgz#c999ed7c96064f830e1f958687f53d0c44025770"
-- 
GitLab