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/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..64cf3527 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,81 @@ 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, + customImageResizer: client.customImageResizer, + ); + if (shrinkImageMaxDimension != null) { + file = await MatrixImageFile.shrink( + bytes: file.bytes, + name: file.name, + maxDimension: shrinkImageMaxDimension, + customImageResizer: client.customImageResizer, + ); + } + } + 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 +776,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 +795,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...'); 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,