Merge branch 'krille/file-sending-status' into 'main'
feat: Display dummy event in timeline for sending files See merge request famedly/company/frontend/famedlysdk!993
This commit is contained in:
commit
ac24da4963
|
|
@ -104,6 +104,9 @@ class Client extends MatrixApi {
|
||||||
|
|
||||||
final Duration sendTimelineEventTimeout;
|
final Duration sendTimelineEventTimeout;
|
||||||
|
|
||||||
|
MatrixImageFileResizedResponse? Function(MatrixImageFileResizeArguments)?
|
||||||
|
customImageResizer;
|
||||||
|
|
||||||
/// Create a client
|
/// Create a client
|
||||||
/// [clientName] = unique identifier of this client
|
/// [clientName] = unique identifier of this client
|
||||||
/// [databaseBuilder]: A function that creates the database instance, that will be used.
|
/// [databaseBuilder]: A function that creates the database instance, that will be used.
|
||||||
|
|
@ -141,6 +144,8 @@ class Client extends MatrixApi {
|
||||||
/// code in background.
|
/// code in background.
|
||||||
/// Set [timelineEventTimeout] to the preferred time the Client should retry
|
/// Set [timelineEventTimeout] to the preferred time the Client should retry
|
||||||
/// sending events on connection problems or to `Duration.zero` to disable it.
|
/// 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(
|
Client(
|
||||||
this.clientName, {
|
this.clientName, {
|
||||||
this.databaseBuilder,
|
this.databaseBuilder,
|
||||||
|
|
@ -164,6 +169,7 @@ class Client extends MatrixApi {
|
||||||
Level? logLevel,
|
Level? logLevel,
|
||||||
Filter? syncFilter,
|
Filter? syncFilter,
|
||||||
this.sendTimelineEventTimeout = const Duration(minutes: 1),
|
this.sendTimelineEventTimeout = const Duration(minutes: 1),
|
||||||
|
this.customImageResizer,
|
||||||
@deprecated bool? debug,
|
@deprecated bool? debug,
|
||||||
}) : syncFilter = syncFilter ??
|
}) : syncFilter = syncFilter ??
|
||||||
Filter(
|
Filter(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
|
@ -796,4 +797,19 @@ class Event extends MatrixEvent {
|
||||||
return _countEmojiRegex.allMatches(plaintextBody).length;
|
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<String>(fileSendingStatusKey);
|
||||||
|
if (status == null) return null;
|
||||||
|
return FileSendingStatus.values.singleWhereOrNull(
|
||||||
|
(fileSendingStatus) => fileSendingStatus.name == status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FileSendingStatus {
|
||||||
|
generatingThumbnail,
|
||||||
|
encrypting,
|
||||||
|
uploading,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ const Map<HistoryVisibility, String> _historyVisibilityMap = {
|
||||||
const String messageSendingStatusKey =
|
const String messageSendingStatusKey =
|
||||||
'com.famedly.famedlysdk.message_sending_status';
|
'com.famedly.famedlysdk.message_sending_status';
|
||||||
|
|
||||||
|
const String fileSendingStatusKey =
|
||||||
|
'com.famedly.famedlysdk.file_sending_status';
|
||||||
|
|
||||||
const String sortOrderKey = 'com.famedly.famedlysdk.sort_order';
|
const String sortOrderKey = 'com.famedly.famedlysdk.sort_order';
|
||||||
|
|
||||||
/// Represents a Matrix room.
|
/// Represents a Matrix room.
|
||||||
|
|
@ -687,25 +690,81 @@ class Room {
|
||||||
///
|
///
|
||||||
/// In case [file] is a [MatrixImageFile], [thumbnail] is automatically
|
/// In case [file] is a [MatrixImageFile], [thumbnail] is automatically
|
||||||
/// computed unless it is explicitly provided.
|
/// 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<Uri> sendFileEvent(
|
Future<Uri> sendFileEvent(
|
||||||
MatrixFile file, {
|
MatrixFile file, {
|
||||||
String? txid,
|
String? txid,
|
||||||
Event? inReplyTo,
|
Event? inReplyTo,
|
||||||
String? editEventId,
|
String? editEventId,
|
||||||
bool waitUntilSent = false,
|
bool waitUntilSent = false,
|
||||||
|
int? shrinkImageMaxDimension,
|
||||||
MatrixImageFile? thumbnail,
|
MatrixImageFile? thumbnail,
|
||||||
Map<String, dynamic>? extraContent,
|
Map<String, dynamic>? extraContent,
|
||||||
}) async {
|
}) 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
|
MatrixFile uploadFile = file; // ignore: omit_local_variable_types
|
||||||
// computing the thumbnail in case we can
|
// computing the thumbnail in case we can
|
||||||
thumbnail ??= (file is MatrixImageFile && encrypted
|
if (file is MatrixImageFile &&
|
||||||
? await file.generateThumbnail(compute: client.runInBackground)
|
(thumbnail == null || shrinkImageMaxDimension != null)) {
|
||||||
: 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 =
|
MatrixFile? uploadThumbnail =
|
||||||
thumbnail; // ignore: omit_local_variable_types
|
thumbnail; // ignore: omit_local_variable_types
|
||||||
EncryptedFile? encryptedFile;
|
EncryptedFile? encryptedFile;
|
||||||
EncryptedFile? encryptedThumbnail;
|
EncryptedFile? encryptedThumbnail;
|
||||||
if (encrypted && client.fileEncryptionEnabled) {
|
if (encrypted && client.fileEncryptionEnabled) {
|
||||||
|
syncUpdate.rooms!.join!.values.first.timeline!.events!.first
|
||||||
|
.unsigned![fileSendingStatusKey] = FileSendingStatus.encrypting.name;
|
||||||
|
await _handleFakeSync(syncUpdate);
|
||||||
encryptedFile = await file.encrypt();
|
encryptedFile = await file.encrypt();
|
||||||
uploadFile = encryptedFile.toMatrixFile();
|
uploadFile = encryptedFile.toMatrixFile();
|
||||||
|
|
||||||
|
|
@ -717,6 +776,9 @@ class Room {
|
||||||
Uri? uploadResp, thumbnailUploadResp;
|
Uri? uploadResp, thumbnailUploadResp;
|
||||||
|
|
||||||
final timeoutDate = DateTime.now().add(client.sendTimelineEventTimeout);
|
final timeoutDate = DateTime.now().add(client.sendTimelineEventTimeout);
|
||||||
|
|
||||||
|
syncUpdate.rooms!.join!.values.first.timeline!.events!.first
|
||||||
|
.unsigned![fileSendingStatusKey] = FileSendingStatus.uploading.name;
|
||||||
while (uploadResp == null ||
|
while (uploadResp == null ||
|
||||||
(uploadThumbnail != null && thumbnailUploadResp == null)) {
|
(uploadThumbnail != null && thumbnailUploadResp == null)) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -733,9 +795,15 @@ class Room {
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
} on MatrixException catch (_) {
|
} on MatrixException catch (_) {
|
||||||
|
syncUpdate.rooms!.join!.values.first.timeline!.events!.first
|
||||||
|
.unsigned![messageSendingStatusKey] = EventStatus.error.intValue;
|
||||||
|
await _handleFakeSync(syncUpdate);
|
||||||
rethrow;
|
rethrow;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (DateTime.now().isAfter(timeoutDate)) {
|
if (DateTime.now().isAfter(timeoutDate)) {
|
||||||
|
syncUpdate.rooms!.join!.values.first.timeline!.events!.first
|
||||||
|
.unsigned![messageSendingStatusKey] = EventStatus.error.intValue;
|
||||||
|
await _handleFakeSync(syncUpdate);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
Logs().v('Send File into room failed. Try again...');
|
Logs().v('Send File into room failed. Try again...');
|
||||||
|
|
|
||||||
|
|
@ -105,14 +105,17 @@ class MatrixImageFile extends MatrixFile {
|
||||||
required String name,
|
required String name,
|
||||||
int maxDimension = 1600,
|
int maxDimension = 1600,
|
||||||
String? mimeType,
|
String? mimeType,
|
||||||
|
MatrixImageFileResizedResponse? Function(MatrixImageFileResizeArguments)?
|
||||||
|
customImageResizer,
|
||||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||||
compute}) async {
|
compute}) async {
|
||||||
final arguments = _ResizeArguments(
|
final arguments = MatrixImageFileResizeArguments(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
maxDimension: maxDimension,
|
maxDimension: maxDimension,
|
||||||
fileName: name,
|
fileName: name,
|
||||||
calcBlurhash: true,
|
calcBlurhash: true,
|
||||||
);
|
);
|
||||||
|
customImageResizer ??= _resize;
|
||||||
final resizedData = compute != null
|
final resizedData = compute != null
|
||||||
? await compute(_resize, arguments)
|
? await compute(_resize, arguments)
|
||||||
: _resize(arguments);
|
: _resize(arguments);
|
||||||
|
|
@ -154,6 +157,8 @@ class MatrixImageFile extends MatrixFile {
|
||||||
/// computes a thumbnail for the image
|
/// computes a thumbnail for the image
|
||||||
Future<MatrixImageFile?> generateThumbnail(
|
Future<MatrixImageFile?> generateThumbnail(
|
||||||
{int dimension = Client.defaultThumbnailSize,
|
{int dimension = Client.defaultThumbnailSize,
|
||||||
|
MatrixImageFileResizedResponse? Function(MatrixImageFileResizeArguments)?
|
||||||
|
customImageResizer,
|
||||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||||
compute}) async {
|
compute}) async {
|
||||||
final thumbnailFile = await shrink(
|
final thumbnailFile = await shrink(
|
||||||
|
|
@ -162,6 +167,7 @@ class MatrixImageFile extends MatrixFile {
|
||||||
mimeType: mimeType,
|
mimeType: mimeType,
|
||||||
compute: compute,
|
compute: compute,
|
||||||
maxDimension: dimension,
|
maxDimension: dimension,
|
||||||
|
customImageResizer: customImageResizer,
|
||||||
);
|
);
|
||||||
// the thumbnail should rather return null than the unshrinked image
|
// the thumbnail should rather return null than the unshrinked image
|
||||||
if ((thumbnailFile.width ?? 0) > dimension ||
|
if ((thumbnailFile.width ?? 0) > dimension ||
|
||||||
|
|
@ -171,11 +177,11 @@ class MatrixImageFile extends MatrixFile {
|
||||||
return thumbnailFile;
|
return thumbnailFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
static _ResizedResponse? _calcMetadata(Uint8List bytes) {
|
static MatrixImageFileResizedResponse? _calcMetadata(Uint8List bytes) {
|
||||||
final image = decodeImage(bytes);
|
final image = decodeImage(bytes);
|
||||||
if (image == null) return null;
|
if (image == null) return null;
|
||||||
|
|
||||||
return _ResizedResponse(
|
return MatrixImageFileResizedResponse(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
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 image = decodeImage(arguments.bytes);
|
||||||
|
|
||||||
final resized = copyResize(image!,
|
final resized = copyResize(image!,
|
||||||
|
|
@ -197,7 +204,7 @@ class MatrixImageFile extends MatrixFile {
|
||||||
final encoded = encodeNamedImage(resized, arguments.fileName);
|
final encoded = encodeNamedImage(resized, arguments.fileName);
|
||||||
if (encoded == null) return null;
|
if (encoded == null) return null;
|
||||||
final bytes = Uint8List.fromList(encoded);
|
final bytes = Uint8List.fromList(encoded);
|
||||||
return _ResizedResponse(
|
return MatrixImageFileResizedResponse(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
width: resized.width,
|
width: resized.width,
|
||||||
height: resized.height,
|
height: resized.height,
|
||||||
|
|
@ -212,13 +219,13 @@ class MatrixImageFile extends MatrixFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ResizedResponse {
|
class MatrixImageFileResizedResponse {
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
final int width;
|
final int width;
|
||||||
final int height;
|
final int height;
|
||||||
final String? blurhash;
|
final String? blurhash;
|
||||||
|
|
||||||
const _ResizedResponse({
|
const MatrixImageFileResizedResponse({
|
||||||
required this.bytes,
|
required this.bytes,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
|
|
@ -226,13 +233,13 @@ class _ResizedResponse {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ResizeArguments {
|
class MatrixImageFileResizeArguments {
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
final int maxDimension;
|
final int maxDimension;
|
||||||
final String fileName;
|
final String fileName;
|
||||||
final bool calcBlurhash;
|
final bool calcBlurhash;
|
||||||
|
|
||||||
const _ResizeArguments({
|
const MatrixImageFileResizeArguments({
|
||||||
required this.bytes,
|
required this.bytes,
|
||||||
required this.maxDimension,
|
required this.maxDimension,
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue