From ca1f6df1c65f1f0609874afbbd9fd83dfb4d9a51 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 5 Jan 2023 13:49:04 +0200 Subject: [PATCH] Generate video thumb from file url instead of public url (#6922) --- .../rnbeta/MattermostManagedModule.java | 30 +++++++++++ app/components/files/video_file.tsx | 14 +++-- ios/GekidouWrapper.swift | 8 +++ ios/Mattermost/MattermostManaged.m | 26 ++++++++- .../react-native-create-thumbnail+1.6.4.patch | 53 +++++++++++++++++++ 5 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 patches/react-native-create-thumbnail+1.6.4.patch diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java index 77df51b6e5..30c7fb2861 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.text.TextUtils; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; @@ -20,8 +21,12 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; + +import com.mattermost.helpers.Credentials; +import com.reactlibrary.createthumbnail.CreateThumbnailModule; import com.mattermost.helpers.RealPathUtil; import java.io.File; @@ -29,6 +34,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; +import java.net.URL; import java.nio.channels.FileChannel; public class MattermostManagedModule extends ReactContextBaseJavaModule { @@ -206,6 +212,30 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule { } } + @ReactMethod + public void createThumbnail(ReadableMap options, Promise promise) { + try { + WritableMap optionsMap = Arguments.createMap(); + optionsMap.merge(options); + String url = options.hasKey("url") ? options.getString("url") : ""; + URL videoUrl = new URL(url); + String serverUrl = videoUrl.getProtocol() + "://" + videoUrl.getHost() + ":" + videoUrl.getPort(); + String token = Credentials.getCredentialsForServerSync(this.reactContext, serverUrl); + if (!TextUtils.isEmpty(token)) { + WritableMap headers = Arguments.createMap(); + if (optionsMap.hasKey("headers")) { + headers.merge(optionsMap.getMap("headers")); + } + headers.putString("Authorization", "Bearer " + token); + optionsMap.putMap("headers", headers); + } + CreateThumbnailModule thumb = new CreateThumbnailModule(this.reactContext); + thumb.create(optionsMap.copy(), promise); + } catch (Exception e) { + promise.reject("CreateThumbnail_ERROR", e); + } + } + private static class SaveDataTask extends GuardedResultAsyncTask { private final WeakReference weakContext; private final String fromFile; diff --git a/app/components/files/video_file.tsx b/app/components/files/video_file.tsx index c5b7d919ec..580d915fed 100644 --- a/app/components/files/video_file.tsx +++ b/app/components/files/video_file.tsx @@ -2,11 +2,10 @@ // See LICENSE.txt for license information. import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {StyleSheet, useWindowDimensions, View} from 'react-native'; -import {createThumbnail} from 'react-native-create-thumbnail'; +import {StyleSheet, useWindowDimensions, View, NativeModules} from 'react-native'; import {updateLocalFile} from '@actions/local/file'; -import {buildFilePreviewUrl, fetchPublicLink} from '@actions/remote/file'; +import {buildFilePreviewUrl, buildFileUrl} from '@actions/remote/file'; import CompassIcon from '@components/compass_icon'; import ProgressiveImage from '@components/progressive_image'; import {useServerUrl} from '@context/server'; @@ -18,6 +17,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import FileIcon from './file_icon'; import type {ResizeMode} from 'react-native-fast-image'; +const {createThumbnail} = NativeModules.MattermostManaged; type Props = { index: number; @@ -84,11 +84,9 @@ const VideoFile = ({ try { const exists = data.mini_preview ? await fileExists(data.mini_preview) : false; if (!data.mini_preview || !exists) { - // We use the public link to avoid having to pass the token through a third party - // library - const publicUri = await fetchPublicLink(serverUrl, data.id!); - if (('link') in publicUri) { - const {path: uri, height, width} = await createThumbnail({url: data.localPath || publicUri.link, timeStamp: 2000}); + const videoUrl = buildFileUrl(serverUrl, data.id!); + if (videoUrl) { + const {path: uri, height, width} = await createThumbnail({url: data.localPath || videoUrl, timeStamp: 2000}); data.mini_preview = uri; data.height = height; data.width = width; diff --git a/ios/GekidouWrapper.swift b/ios/GekidouWrapper.swift index a58df7fb78..24bda2774f 100644 --- a/ios/GekidouWrapper.swift +++ b/ios/GekidouWrapper.swift @@ -27,4 +27,12 @@ import Gekidou @objc func setPreference(_ value: Any?, forKey name: String) { Preferences.default.set(value, forKey: name) } + + @objc func getToken(for url: String) -> String? { + if let token = try? Keychain.default.getToken(for: url) { + return token + } + + return nil + } } diff --git a/ios/Mattermost/MattermostManaged.m b/ios/Mattermost/MattermostManaged.m index ec68c3ca60..a56f97968b 100644 --- a/ios/Mattermost/MattermostManaged.m +++ b/ios/Mattermost/MattermostManaged.m @@ -8,6 +8,8 @@ #import "AppDelegate.h" #import "MattermostManaged.h" +#import "CreateThumbnail.h" +#import "Mattermost-Swift.h" @implementation MattermostManaged @@ -107,7 +109,6 @@ RCT_EXPORT_METHOD(renameDatabase: (NSString *)databaseName to: (NSString *) new NSDictionary *appGroupDir = [self appGroupSharedDirectory]; NSString *databaseDir; NSString *newDBDir; - if(databaseName){ databaseDir = [NSString stringWithFormat:@"%@/%@%@", appGroupDir[@"databasePath"], databaseName , @".db"]; @@ -200,4 +201,27 @@ RCT_EXPORT_METHOD(lockPortrait) } +RCT_EXPORT_METHOD(createThumbnail:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + NSMutableDictionary *newConfig = [config mutableCopy]; + NSMutableDictionary *headers = [config[@"headers"] ?: @{} mutableCopy]; + NSString *url = (NSString *)[config objectForKey:@"url"] ?: @""; + NSURL *vidURL = nil; + NSString *url_ = [url lowercaseString]; + + if ([url_ hasPrefix:@"http://"] || [url_ hasPrefix:@"https://"] || [url_ hasPrefix:@"file://"]) { + vidURL = [NSURL URLWithString:url]; + NSString *serverUrl = [NSString stringWithFormat:@"%@://%@:%@", vidURL.scheme, vidURL.host, vidURL.port]; + if (vidURL != nil) { + NSString *token = [[GekidouWrapper default] getTokenFor:serverUrl]; + if (token != nil) { + + headers[@"Authorization"] = [NSString stringWithFormat:@"Bearer %@", token]; + newConfig[@"headers"] = headers; + } + } + } + [CreateThumbnail create:newConfig findEventsWithResolver:resolve rejecter:reject]; +} + @end diff --git a/patches/react-native-create-thumbnail+1.6.4.patch b/patches/react-native-create-thumbnail+1.6.4.patch new file mode 100644 index 0000000000..7d1aa97d62 --- /dev/null +++ b/patches/react-native-create-thumbnail+1.6.4.patch @@ -0,0 +1,53 @@ +diff --git a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h +index 28b1d9b..cb63c52 100644 +--- a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h ++++ b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h +@@ -3,5 +3,5 @@ + #import + + @interface CreateThumbnail : NSObject +- +++(void)create:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; + @end +diff --git a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m +index 92cc49f..23cc83c 100644 +--- a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m ++++ b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m +@@ -6,6 +6,10 @@ RCT_EXPORT_MODULE() + + RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) + { ++ [CreateThumbnail create:config findEventsWithResolver:resolve rejecter:reject]; ++} ++ +++(void) create:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { + NSString *url = (NSString *)[config objectForKey:@"url"] ?: @""; + int timeStamp = [[config objectForKey:@"timeStamp"] intValue] ?: 0; + NSString *format = (NSString *)[config objectForKey:@"format"] ?: @"jpeg"; +@@ -82,7 +86,7 @@ RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromi + } + } + +-- (unsigned long long) sizeOfFolderAtPath:(NSString *)path { +++ (unsigned long long) sizeOfFolderAtPath:(NSString *)path { + NSArray *files = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:path error:nil]; + NSEnumerator *enumerator = [files objectEnumerator]; + NSString *fileName; +@@ -93,7 +97,7 @@ RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromi + return size; + } + +-- (void) cleanDir:(NSString *)path forSpace:(unsigned long long)size { +++ (void) cleanDir:(NSString *)path forSpace:(unsigned long long)size { + NSFileManager *fm = [NSFileManager defaultManager]; + NSError *error = nil; + unsigned long long deletedSize = 0; +@@ -110,7 +114,7 @@ RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromi + return; + } + +-- (void) generateThumbImage:(AVURLAsset *)asset atTime:(int)timeStamp completion:(void (^)(UIImage* thumbnail))completion failure:(void (^)(NSError* error))failure { +++ (void) generateThumbImage:(AVURLAsset *)asset atTime:(int)timeStamp completion:(void (^)(UIImage* thumbnail))completion failure:(void (^)(NSError* error))failure { + AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset]; + generator.appliesPreferredTrackTransform = YES; + generator.maximumSize = CGSizeMake(512, 512);