From a915cdacc8b67c25c32a60480e7d2e7f681eeba3 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 30 Mar 2022 09:17:21 +0200 Subject: [PATCH 1/2] feat: Display dummy event in timeline for sending files For thumbnail generation, encrypting and uploading it is not necessary to block the UI. The given file event should already be displayed in the timeline. This placed it in the UI and adds a additional fileSendingStatus property so the app can fetch the current status. --- lib/src/event.dart | 16 +++++++++++ lib/src/room.dart | 71 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/lib/src/event.dart b/lib/src/event.dart index 11c71319..305b1dd0 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -19,6 +19,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:html/parser.dart'; import 'package:http/http.dart' as http; @@ -796,4 +797,19 @@ class Event extends MatrixEvent { return _countEmojiRegex.allMatches(plaintextBody).length; } } + + /// If this event is in Status SENDING and it aims to send a file, then this + /// shows the status of the file sending. + FileSendingStatus? get fileSendingStatus { + final status = unsigned?.tryGet(fileSendingStatusKey); + if (status == null) return null; + return FileSendingStatus.values.singleWhereOrNull( + (fileSendingStatus) => fileSendingStatus.name == status); + } +} + +enum FileSendingStatus { + generatingThumbnail, + encrypting, + uploading, } diff --git a/lib/src/room.dart b/lib/src/room.dart index 3461b5cf..6f22754a 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -50,6 +50,9 @@ const Map _historyVisibilityMap = { const String messageSendingStatusKey = 'com.famedly.famedlysdk.message_sending_status'; +const String fileSendingStatusKey = + 'com.famedly.famedlysdk.file_sending_status'; + const String sortOrderKey = 'com.famedly.famedlysdk.sort_order'; /// Represents a Matrix room. @@ -687,25 +690,78 @@ class Room { /// /// In case [file] is a [MatrixImageFile], [thumbnail] is automatically /// computed unless it is explicitly provided. + /// Set [shrinkImageMaxDimension] to for example `1600` if you want to shrink + /// your image before sending. This is ignored if the File is not a + /// [MatrixImageFile]. Future sendFileEvent( MatrixFile file, { String? txid, Event? inReplyTo, String? editEventId, bool waitUntilSent = false, + int? shrinkImageMaxDimension, MatrixImageFile? thumbnail, Map? extraContent, }) async { + txid ??= client.generateUniqueTransactionId(); + + // Create a fake Event object as a placeholder for the uploading file: + final syncUpdate = SyncUpdate( + nextBatch: '', + rooms: RoomsUpdate( + join: { + id: JoinedRoomUpdate( + timeline: TimelineUpdate( + events: [ + MatrixEvent( + content: { + 'msgtype': file.msgType, + 'body': file.name, + 'filename': file.name, + }, + type: EventTypes.Message, + eventId: txid, + senderId: client.userID!, + originServerTs: DateTime.now(), + unsigned: { + messageSendingStatusKey: EventStatus.sending.intValue, + 'transaction_id': txid, + }, + ), + ], + ), + ), + }, + ), + ); + MatrixFile uploadFile = file; // ignore: omit_local_variable_types // computing the thumbnail in case we can - thumbnail ??= (file is MatrixImageFile && encrypted - ? await file.generateThumbnail(compute: client.runInBackground) - : null); + if (file is MatrixImageFile && + (thumbnail == null || shrinkImageMaxDimension != null)) { + syncUpdate.rooms!.join!.values.first.timeline!.events!.first + .unsigned![fileSendingStatusKey] = + FileSendingStatus.generatingThumbnail.name; + await _handleFakeSync(syncUpdate); + thumbnail ??= + await file.generateThumbnail(compute: client.runInBackground); + if (shrinkImageMaxDimension != null) { + file = await MatrixImageFile.shrink( + bytes: file.bytes, + name: file.name, + maxDimension: shrinkImageMaxDimension, + ); + } + } + MatrixFile? uploadThumbnail = thumbnail; // ignore: omit_local_variable_types EncryptedFile? encryptedFile; EncryptedFile? encryptedThumbnail; if (encrypted && client.fileEncryptionEnabled) { + syncUpdate.rooms!.join!.values.first.timeline!.events!.first + .unsigned![fileSendingStatusKey] = FileSendingStatus.encrypting.name; + await _handleFakeSync(syncUpdate); encryptedFile = await file.encrypt(); uploadFile = encryptedFile.toMatrixFile(); @@ -717,6 +773,9 @@ class Room { Uri? uploadResp, thumbnailUploadResp; final timeoutDate = DateTime.now().add(client.sendTimelineEventTimeout); + + syncUpdate.rooms!.join!.values.first.timeline!.events!.first + .unsigned![fileSendingStatusKey] = FileSendingStatus.uploading.name; while (uploadResp == null || (uploadThumbnail != null && thumbnailUploadResp == null)) { try { @@ -733,9 +792,15 @@ class Room { ) : null; } on MatrixException catch (_) { + syncUpdate.rooms!.join!.values.first.timeline!.events!.first + .unsigned![messageSendingStatusKey] = EventStatus.error.intValue; + await _handleFakeSync(syncUpdate); rethrow; } catch (_) { if (DateTime.now().isAfter(timeoutDate)) { + syncUpdate.rooms!.join!.values.first.timeline!.events!.first + .unsigned![messageSendingStatusKey] = EventStatus.error.intValue; + await _handleFakeSync(syncUpdate); rethrow; } Logs().v('Send File into room failed. Try again...'); From 445252b3a5f2dc1e6297b5a81070345638210635 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Wed, 30 Mar 2022 10:35:17 +0200 Subject: [PATCH 2/2] feat: Pass through a custom image resize function to the client This allows the use of the native imaging package in a more easy way. --- lib/src/client.dart | 6 ++++++ lib/src/room.dart | 7 +++++-- lib/src/utils/matrix_file.dart | 25 ++++++++++++++++--------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index 6e688c72..fe0828ee 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -104,6 +104,9 @@ class Client extends MatrixApi { final Duration sendTimelineEventTimeout; + MatrixImageFileResizedResponse? Function(MatrixImageFileResizeArguments)? + customImageResizer; + /// Create a client /// [clientName] = unique identifier of this client /// [databaseBuilder]: A function that creates the database instance, that will be used. @@ -141,6 +144,8 @@ class Client extends MatrixApi { /// code in background. /// Set [timelineEventTimeout] to the preferred time the Client should retry /// sending events on connection problems or to `Duration.zero` to disable it. + /// Set [customImageResizer] to your own implementation for a more advanced + /// and faster image resizing experience. Client( this.clientName, { this.databaseBuilder, @@ -164,6 +169,7 @@ class Client extends MatrixApi { Level? logLevel, Filter? syncFilter, this.sendTimelineEventTimeout = const Duration(minutes: 1), + this.customImageResizer, @deprecated bool? debug, }) : syncFilter = syncFilter ?? Filter( diff --git a/lib/src/room.dart b/lib/src/room.dart index 6f22754a..64cf3527 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -743,13 +743,16 @@ class Room { .unsigned![fileSendingStatusKey] = FileSendingStatus.generatingThumbnail.name; await _handleFakeSync(syncUpdate); - thumbnail ??= - await file.generateThumbnail(compute: client.runInBackground); + thumbnail ??= await file.generateThumbnail( + compute: client.runInBackground, + customImageResizer: client.customImageResizer, + ); if (shrinkImageMaxDimension != null) { file = await MatrixImageFile.shrink( bytes: file.bytes, name: file.name, maxDimension: shrinkImageMaxDimension, + customImageResizer: client.customImageResizer, ); } } diff --git a/lib/src/utils/matrix_file.dart b/lib/src/utils/matrix_file.dart index 7f360918..219a9dd9 100644 --- a/lib/src/utils/matrix_file.dart +++ b/lib/src/utils/matrix_file.dart @@ -105,14 +105,17 @@ class MatrixImageFile extends MatrixFile { required String name, int maxDimension = 1600, String? mimeType, + MatrixImageFileResizedResponse? Function(MatrixImageFileResizeArguments)? + customImageResizer, Future Function(FutureOr Function(U arg) function, U arg)? compute}) async { - final arguments = _ResizeArguments( + final arguments = MatrixImageFileResizeArguments( bytes: bytes, maxDimension: maxDimension, fileName: name, calcBlurhash: true, ); + customImageResizer ??= _resize; final resizedData = compute != null ? await compute(_resize, arguments) : _resize(arguments); @@ -154,6 +157,8 @@ class MatrixImageFile extends MatrixFile { /// computes a thumbnail for the image Future generateThumbnail( {int dimension = Client.defaultThumbnailSize, + MatrixImageFileResizedResponse? Function(MatrixImageFileResizeArguments)? + customImageResizer, Future Function(FutureOr Function(U arg) function, U arg)? compute}) async { final thumbnailFile = await shrink( @@ -162,6 +167,7 @@ class MatrixImageFile extends MatrixFile { mimeType: mimeType, compute: compute, maxDimension: dimension, + customImageResizer: customImageResizer, ); // the thumbnail should rather return null than the unshrinked image if ((thumbnailFile.width ?? 0) > dimension || @@ -171,11 +177,11 @@ class MatrixImageFile extends MatrixFile { return thumbnailFile; } - static _ResizedResponse? _calcMetadata(Uint8List bytes) { + static MatrixImageFileResizedResponse? _calcMetadata(Uint8List bytes) { final image = decodeImage(bytes); if (image == null) return null; - return _ResizedResponse( + return MatrixImageFileResizedResponse( bytes: bytes, width: image.width, height: image.height, @@ -187,7 +193,8 @@ class MatrixImageFile extends MatrixFile { ); } - static _ResizedResponse? _resize(_ResizeArguments arguments) { + static MatrixImageFileResizedResponse? _resize( + MatrixImageFileResizeArguments arguments) { final image = decodeImage(arguments.bytes); final resized = copyResize(image!, @@ -197,7 +204,7 @@ class MatrixImageFile extends MatrixFile { final encoded = encodeNamedImage(resized, arguments.fileName); if (encoded == null) return null; final bytes = Uint8List.fromList(encoded); - return _ResizedResponse( + return MatrixImageFileResizedResponse( bytes: bytes, width: resized.width, height: resized.height, @@ -212,13 +219,13 @@ class MatrixImageFile extends MatrixFile { } } -class _ResizedResponse { +class MatrixImageFileResizedResponse { final Uint8List bytes; final int width; final int height; final String? blurhash; - const _ResizedResponse({ + const MatrixImageFileResizedResponse({ required this.bytes, required this.width, required this.height, @@ -226,13 +233,13 @@ class _ResizedResponse { }); } -class _ResizeArguments { +class MatrixImageFileResizeArguments { final Uint8List bytes; final int maxDimension; final String fileName; final bool calcBlurhash; - const _ResizeArguments({ + const MatrixImageFileResizeArguments({ required this.bytes, required this.maxDimension, required this.fileName,