diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index 1f33fc3e1d..c59cd235fc 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; typedef TransactionId = String; @@ -27,6 +28,7 @@ extension SendFileExtension on Room { Event? inReplyTo, String? editEventId, int? shrinkImageMaxDimension, + FileInfo? thumbnail, Map? extraContent, }) async { FileInfo tempfileInfo = fileInfo; @@ -65,7 +67,32 @@ extension SendFileExtension on Room { final tempEncryptedFile = await File('${tempDir.path}/$formattedDateTime${fileInfo.fileName}') .create(); + final tempThumbnailFile = await File( + '${tempDir.path}/$formattedDateTime${fileInfo.fileName}_thumbnail.jpg', + ).create(); + final tempEncryptedThumbnailFile = await File( + '${tempDir.path}/$formattedDateTime${fileInfo.fileName}_encrypted_thumbnail', + ).create(); + + // computing the thumbnail in case we can + if (fileInfo is ImageFileInfo && + (thumbnail == null || shrinkImageMaxDimension != null)) { + fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first + .unsigned![fileSendingStatusKey] = + FileSendingStatus.generatingThumbnail.name; + await handleImageFakeSync(fakeImageEvent); + thumbnail ??= await _generateThumbnail( + fileInfo, + targetPath: tempThumbnailFile.path, + ); + + if (thumbnail != null && fileInfo.fileSize < thumbnail.fileSize) { + thumbnail = null; // in this case, the thumbnail is not usefull + } + } + EncryptedFileInfo? encryptedFileInfo; + EncryptedFileInfo? encryptedThumbnail; if (encrypted && client.fileEncryptionEnabled) { fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first .unsigned![fileSendingStatusKey] = FileSendingStatus.encrypting.name; @@ -80,18 +107,38 @@ extension SendFileExtension on Room { tempEncryptedFile.path, fileInfo.fileSize, ); + if (thumbnail != null) { + encryptedThumbnail = await encryptedService.encryptFile( + fileInfo: thumbnail, + outputFile: tempEncryptedThumbnailFile, + ); + } } - Uri? uploadResp; + Uri? uploadResp, thumbnailUploadResp; fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first .unsigned![fileSendingStatusKey] = FileSendingStatus.uploading.name; - while (uploadResp == null) { + while (uploadResp == null || + (encryptedThumbnail != null && thumbnailUploadResp == null)) { try { final uploadFileApi = getIt.get(); final response = await uploadFileApi.uploadFile(fileInfo: tempfileInfo); if (response.contentUri != null) { uploadResp = Uri.parse(response.contentUri!); } + if (uploadResp != null && + encryptedThumbnail != null && + thumbnail != null) { + final thumbnailResponse = await uploadFileApi.uploadFile( + fileInfo: FileInfo( + thumbnail.fileName, + tempEncryptedThumbnailFile.path, + thumbnail.fileSize, + ), + ); + thumbnailUploadResp = + Uri.tryParse(thumbnailResponse.contentUri ?? ""); + } } on MatrixException catch (e) { fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first .unsigned![messageSendingStatusKey] = EventStatus.error.intValue; @@ -119,6 +166,17 @@ extension SendFileExtension on Room { Logs().d('RoomExtension::EncryptedFileInfo: $encryptedFileInfo'); } + if (encryptedThumbnail != null) { + encryptedThumbnail = EncryptedFileInfo( + key: encryptedThumbnail.key, + version: encryptedThumbnail.version, + initialVector: encryptedThumbnail.initialVector, + hashes: encryptedThumbnail.hashes, + url: thumbnailUploadResp.toString(), + ); + Logs().d('RoomExtension::EncryptedThumbnail: $encryptedThumbnail'); + } + // Send event final content = { 'msgtype': msgType, @@ -128,6 +186,11 @@ extension SendFileExtension on Room { if (encryptedFileInfo != null) 'file': encryptedFileInfo.toJson(), 'info': { ...fileInfo.metadata, + if (thumbnail != null && encryptedThumbnail == null) + 'thumbnail_url': thumbnailUploadResp.toString(), + if (thumbnail != null && encryptedThumbnail != null) + 'thumbnail_file': encryptedThumbnail.toJson(), + if (thumbnail != null) 'thumbnail_info': thumbnail.metadata, }, if (extraContent != null) ...extraContent, }; @@ -247,4 +310,23 @@ extension SendFileExtension on Room { User? getUser(mxId) { return getParticipants().firstWhereOrNull((user) => user.id == mxId); } + + Future _generateThumbnail( + ImageFileInfo originalFile, { + required String targetPath, + }) async { + try { + final result = await FlutterImageCompress.compressAndGetFile( + originalFile.filePath, + targetPath, + quality: 70, + ); + if (result == null) return null; + final size = await result.length(); + return FileInfo(result.name, result.path, size); + } catch (e) { + Logs().e('Error while generating thumbnail', e); + return null; + } + } } diff --git a/lib/presentation/extensions/send_file_web_extension.dart b/lib/presentation/extensions/send_file_web_extension.dart index 642d0f3a4b..43f2829135 100644 --- a/lib/presentation/extensions/send_file_web_extension.dart +++ b/lib/presentation/extensions/send_file_web_extension.dart @@ -1,3 +1,4 @@ +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:matrix/matrix.dart'; extension SendFileWebExtension on Room { @@ -10,6 +11,7 @@ extension SendFileWebExtension on Room { Event? inReplyTo, String? editEventId, int? shrinkImageMaxDimension, + MatrixFile? thumbnail, Map? extraContent, }) async { txid ??= client.generateUniqueTransactionId(); @@ -39,9 +41,23 @@ extension SendFileWebExtension on Room { await handleImageFakeSync(fakeImageEvent); rethrow; } + // computing the thumbnail in case we can + if (file.msgType == MessageTypes.Image && + (thumbnail == null || shrinkImageMaxDimension != null)) { + fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first + .unsigned![fileSendingStatusKey] = + FileSendingStatus.generatingThumbnail.name; + await handleImageFakeSync(fakeImageEvent); + thumbnail ??= await _generateThumbnail(file); + if (thumbnail != null && file.size < thumbnail.size) { + thumbnail = null; // in this case, the thumbnail is not usefull + } + } EncryptedFile? encryptedFile; MatrixFile? uploadFile; + MatrixFile? uploadThumbnail = thumbnail; + EncryptedFile? encryptedThumbnail; if (encrypted && client.fileEncryptionEnabled) { fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first .unsigned![fileSendingStatusKey] = FileSendingStatus.encrypting.name; @@ -49,8 +65,15 @@ extension SendFileWebExtension on Room { encryptedFile = await file.encrypt(); uploadFile = MatrixFile.fromMimeType(bytes: encryptedFile!.data, name: 'crypt'); + if (thumbnail != null) { + encryptedThumbnail = await thumbnail.encrypt(); + uploadThumbnail = MatrixFile.fromMimeType( + bytes: encryptedThumbnail?.data, + name: 'crypt', + ); + } } - Uri? uploadResp; + Uri? uploadResp, thumbnailUploadResp; fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first .unsigned![fileSendingStatusKey] = FileSendingStatus.uploading.name; @@ -62,6 +85,14 @@ extension SendFileWebExtension on Room { filename: uploadFile.name, contentType: uploadFile.mimeType, ); + thumbnailUploadResp = + uploadThumbnail != null && uploadThumbnail.bytes != null + ? await client.uploadContent( + uploadThumbnail.bytes!, + filename: uploadThumbnail.name, + contentType: uploadThumbnail.mimeType, + ) + : null; } on MatrixException catch (e) { fakeImageEvent.rooms!.join!.values.first.timeline!.events!.first .unsigned![messageSendingStatusKey] = EventStatus.error.intValue; @@ -101,6 +132,24 @@ extension SendFileWebExtension on Room { }, 'info': { ...file.info, + if (thumbnail != null && encryptedThumbnail == null) + 'thumbnail_url': thumbnailUploadResp.toString(), + if (thumbnail != null && encryptedThumbnail != null) + 'thumbnail_file': { + 'url': thumbnailUploadResp.toString(), + 'mimetype': thumbnail.mimeType, + 'v': 'v2', + 'key': { + 'alg': 'A256CTR', + 'ext': true, + 'k': encryptedThumbnail.k, + 'key_ops': ['encrypt', 'decrypt'], + 'kty': 'oct' + }, + 'iv': encryptedThumbnail.iv, + 'hashes': {'sha256': encryptedThumbnail.sha256} + }, + if (thumbnail != null) 'thumbnail_info': thumbnail.info, }, if (extraContent != null) ...extraContent, }; @@ -175,4 +224,22 @@ extension SendFileWebExtension on Room { await client.handleSync(fakeImageEvent, direction: direction); } } + + Future _generateThumbnail(MatrixFile originalFile) async { + if (originalFile.bytes == null) return null; + try { + final result = await FlutterImageCompress.compressWithList( + originalFile.bytes!, + quality: 70, + ); + return MatrixFile( + bytes: result, + name: originalFile.name, + mimeType: originalFile.mimeType, + ); + } catch (e) { + Logs().e('Error while generating thumbnail', e); + return null; + } + } } diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 283462c724..db2209ab23 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -175,7 +175,7 @@ class _MxcImageState extends State MaterialLocalizations.of(context).modalBarrierDismissLabel, transitionDuration: const Duration(milliseconds: 200), pageBuilder: (_, animationOne, animationTwo) => - ImageViewer(widget.event!, imageData: _imageData), + ImageViewer(widget.event!), ); } else if (widget.onTapSelectMode != null) { widget.onTapSelectMode!(); @@ -231,6 +231,8 @@ class _MxcImageState extends State data, width: widget.width, height: widget.height, + cacheWidth: widget.width?.toInt(), + cacheHeight: widget.height?.toInt(), fit: widget.fit, filterQuality: FilterQuality.medium, errorBuilder: (context, __, ___) { diff --git a/pubspec.lock b/pubspec.lock index ab6ef6668e..d9a093dbb3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -772,6 +772,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "2725cce5c58fdeaf1db8f4203688228bb67e3523a66305ccaa6f99071beb6dc2" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: "8e7299afe109dc4b97fda34bf0f4967cc1fc10bc8050c374d449cab262d095b3" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "3c7e86da7540b1adfa919b461885a41a018d4a26544d0fcbeaa769f6542e603d" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: e879189dc7f246dcf8f06c07ee849231341508bf51e8ed7d5dcbe778ddde0e81 + url: "https://pub.dev" + source: hosted + version: "0.1.3+1" flutter_inappwebview: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a53a3bf87f..beff3853e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -132,6 +132,7 @@ dependencies: async: ^2.11.0 cached_network_image: ^3.2.3 pull_to_refresh: ^2.0.0 + flutter_image_compress: ^2.0.4 dev_dependencies: build_runner: ^2.3.3 dart_code_metrics: ^5.7.3 diff --git a/web/index.html b/web/index.html index b3d3ffd3a2..6a4447b648 100644 --- a/web/index.html +++ b/web/index.html @@ -136,5 +136,6 @@ } +