feat: Sending multiple files at once

pull/1357/head
krille-chan 1 year ago
parent b328e95980
commit b83503585f
No known key found for this signature in database

@ -2271,8 +2271,6 @@
"@disableEncryptionWarning": {}, "@disableEncryptionWarning": {},
"reopenChat": "Chat wieder eröffnen", "reopenChat": "Chat wieder eröffnen",
"@reopenChat": {}, "@reopenChat": {},
"fileIsTooBigForServer": "Der Server meldet, dass die Datei zu groß ist für eine Übermittlung ist.",
"@fileIsTooBigForServer": {},
"noBackupWarning": "Achtung! Ohne Aktivierung des Chat-Backups verlierst du den Zugriff auf deine verschlüsselten Nachrichten. Vor dem Ausloggen wird dringend empfohlen, das Chat-Backup zu aktivieren.", "noBackupWarning": "Achtung! Ohne Aktivierung des Chat-Backups verlierst du den Zugriff auf deine verschlüsselten Nachrichten. Vor dem Ausloggen wird dringend empfohlen, das Chat-Backup zu aktivieren.",
"@noBackupWarning": {}, "@noBackupWarning": {},
"noOtherDevicesFound": "Keine anderen Geräte anwesend", "noOtherDevicesFound": "Keine anderen Geräte anwesend",

@ -2422,8 +2422,13 @@
"@noBackupWarning": {}, "@noBackupWarning": {},
"noOtherDevicesFound": "No other devices found", "noOtherDevicesFound": "No other devices found",
"@noOtherDevicesFound": {}, "@noOtherDevicesFound": {},
"fileIsTooBigForServer": "The server reports that the file is too large to be sent.", "fileIsTooBigForServer": "Unable to send! The server only supports attachments up to {max}.",
"@fileIsTooBigForServer": {}, "@fileIsTooBigForServer": {
"type": "text",
"placeholders": {
"max": {}
}
},
"fileHasBeenSavedAt": "File has been saved at {path}", "fileHasBeenSavedAt": "File has been saved at {path}",
"@fileHasBeenSavedAt": { "@fileHasBeenSavedAt": {
"type": "text", "type": "text",
@ -2762,5 +2767,24 @@
"whatIsAHomeserver": "What is a homeserver?", "whatIsAHomeserver": "What is a homeserver?",
"homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.", "homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org.",
"doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?", "doesNotSeemToBeAValidHomeserver": "Doesn't seem to be a compatible homeserver. Wrong URL?",
"calculatingFileSize": "Calculating file size..." "calculatingFileSize": "Calculating file size...",
"prepareSendingAttachment": "Prepare sending attachment...",
"sendingAttachment": "Sending attachment...",
"generatingVideoThumbnail": "Generating video thumbnail...",
"compressVideo": "Compressing video...",
"sendingAttachmentCountOfCount": "Sending attachment {index} of {length}...",
"@sendingAttachmentCountOfCount": {
"type": "integer",
"placeholders": {
"index": {},
"length": {}
}
},
"serverLimitReached": "Server limit reached! Waiting {seconds} seconds...",
"@serverLimitReached": {
"type": "integer",
"placeholders": {
"seconds": {}
}
}
} }

@ -128,6 +128,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: details.files, files: details.files,
room: room, room: room,
outerContext: context,
), ),
); );
} }
@ -483,7 +484,7 @@ class ChatController extends State<ChatPageWithRoom>
final result = await AppLock.of(context).pauseWhile( final result = await AppLock.of(context).pauseWhile(
FilePicker.platform.pickFiles( FilePicker.platform.pickFiles(
compressionQuality: 0, compressionQuality: 0,
allowMultiple: false, allowMultiple: true,
), ),
); );
if (result == null || result.files.isEmpty) return; if (result == null || result.files.isEmpty) return;
@ -492,6 +493,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: result.xFiles, files: result.xFiles,
room: room, room: room,
outerContext: context,
), ),
); );
} }
@ -503,6 +505,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: [XFile.fromData(image)], files: [XFile.fromData(image)],
room: room, room: room,
outerContext: context,
), ),
); );
} }
@ -512,7 +515,7 @@ class ChatController extends State<ChatPageWithRoom>
FilePicker.platform.pickFiles( FilePicker.platform.pickFiles(
compressionQuality: 0, compressionQuality: 0,
type: FileType.image, type: FileType.image,
allowMultiple: false, allowMultiple: true,
), ),
); );
if (result == null || result.files.isEmpty) return; if (result == null || result.files.isEmpty) return;
@ -522,6 +525,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: result.xFiles, files: result.xFiles,
room: room, room: room,
outerContext: context,
), ),
); );
} }
@ -537,6 +541,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: [file], files: [file],
room: room, room: room,
outerContext: context,
), ),
); );
} }
@ -555,6 +560,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: [file], files: [file],
room: room, room: room,
outerContext: context,
), ),
); );
} }

@ -6,12 +6,11 @@ import 'package:flutter/material.dart';
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/size_string.dart'; import 'package:fluffychat/utils/size_string.dart';
@ -20,10 +19,12 @@ import '../../utils/resize_video.dart';
class SendFileDialog extends StatefulWidget { class SendFileDialog extends StatefulWidget {
final Room room; final Room room;
final List<XFile> files; final List<XFile> files;
final BuildContext outerContext;
const SendFileDialog({ const SendFileDialog({
required this.room, required this.room,
required this.files, required this.files,
required this.outerContext,
super.key, super.key,
}); });
@ -38,65 +39,98 @@ class SendFileDialogState extends State<SendFileDialog> {
static const int minSizeToCompress = 20 * 1024; static const int minSizeToCompress = 20 * 1024;
Future<void> _send() async { Future<void> _send() async {
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(widget.outerContext);
final l10n = L10n.of(context)!; final l10n = L10n.of(context)!;
Navigator.of(context, rootNavigator: false).pop(); try {
scaffoldMessenger.showLoadingSnackBar(l10n.prepareSendingAttachment);
Navigator.of(context, rootNavigator: false).pop();
final clientConfig = await widget.room.client.getConfig();
final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024;
showFutureLoadingDialog( for (final xfile in widget.files) {
context: context, final MatrixFile file;
future: () async { MatrixImageFile? thumbnail;
final clientConfig = await widget.room.client.getConfig(); final length = await xfile.length();
final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024; final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path);
for (final xfile in widget.files) { // If file is a video, shrink it!
final MatrixFile file; if (PlatformInfos.isMobile &&
MatrixImageFile? thumbnail; mimeType != null &&
final length = await xfile.length(); mimeType.startsWith('video') &&
final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path); length > minSizeToCompress &&
!origImage) {
scaffoldMessenger.showLoadingSnackBar(l10n.compressVideo);
file = await xfile.resizeVideo();
scaffoldMessenger.showLoadingSnackBar(l10n.generatingVideoThumbnail);
thumbnail = await xfile.getVideoThumbnail();
} else {
// Else we just create a MatrixFile
file = MatrixFile(
bytes: await xfile.readAsBytes(),
name: xfile.name,
mimeType: xfile.mimeType,
).detectFileType;
}
// If file is a video, shrink it! if (file.bytes.length > maxUploadSize) {
if (mimeType != null && throw FileTooBigMatrixException(length, maxUploadSize);
mimeType.startsWith('video') && }
length > minSizeToCompress &&
!origImage) { if (widget.files.length > 1) {
file = await xfile.resizeVideo(); scaffoldMessenger.showLoadingSnackBar(
thumbnail = await xfile.getVideoThumbnail(); l10n.sendingAttachmentCountOfCount(
} else { widget.files.indexOf(xfile) + 1,
// Else we just create a MatrixFile widget.files.length,
file = MatrixFile( ),
bytes: await xfile.readAsBytes(), );
name: xfile.name, } else {
mimeType: xfile.mimeType, scaffoldMessenger.showLoadingSnackBar(l10n.sendingAttachment);
).detectFileType; }
}
if (file.bytes.length > maxUploadSize) { try {
throw FileTooBigMatrixException(length, maxUploadSize); await widget.room.sendFileEvent(
file,
thumbnail: thumbnail,
shrinkImageMaxDimension: origImage ? null : 1600,
);
} on MatrixException catch (e) {
final retryAfterMs = e.retryAfterMs;
if (e.error != MatrixError.M_LIMIT_EXCEEDED || retryAfterMs == null) {
rethrow;
} }
final retryAfterDuration =
Duration(milliseconds: retryAfterMs + 1000);
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(
l10n.serverLimitReached(retryAfterDuration.inSeconds),
),
),
);
await Future.delayed(retryAfterDuration);
scaffoldMessenger.showLoadingSnackBar(l10n.sendingAttachment);
widget.room await widget.room.sendFileEvent(
.sendFileEvent(
file, file,
thumbnail: thumbnail, thumbnail: thumbnail,
shrinkImageMaxDimension: origImage ? null : 1600, shrinkImageMaxDimension: origImage ? null : 1600,
)
.catchError(
(e, s) {
if (e is FileTooBigMatrixException) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.fileIsTooBigForServer)),
);
return null;
}
ErrorReporter(context, 'Unable to send file')
.onErrorCallback(e, s);
return null;
},
); );
} }
}, }
); } catch (e) {
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(e.toLocalizedString(widget.outerContext)),
duration: const Duration(seconds: 30),
showCloseIcon: true,
),
);
rethrow;
}
return; return;
} }
@ -270,3 +304,30 @@ class SendFileDialogState extends State<SendFileDialog> {
); );
} }
} }
extension on ScaffoldMessengerState {
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showLoadingSnackBar(
String title,
) {
clearSnackBars();
return showSnackBar(
SnackBar(
duration: const Duration(minutes: 5),
dismissDirection: DismissDirection.none,
content: Row(
children: [
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
const SizedBox(width: 16),
Text(title),
],
),
),
);
}
}

@ -214,6 +214,7 @@ class ChatListController extends State<ChatList>
), ),
], ],
room: room, room: room,
outerContext: context,
), ),
); );
Matrix.of(context).shareContent = null; Matrix.of(context).shareContent = null;

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -10,10 +11,29 @@ import 'package:matrix/matrix.dart';
import 'uia_request_manager.dart'; import 'uia_request_manager.dart';
extension LocalizedExceptionExtension on Object { extension LocalizedExceptionExtension on Object {
static String _formatFileSize(int size) {
if (size < 1024) return '$size B';
final i = (log(size) / log(1024)).floor();
final num = (size / pow(1024, i));
final round = num.round();
final numString = round < 10
? num.toStringAsFixed(2)
: round < 100
? num.toStringAsFixed(1)
: round.toString();
return '$numString ${'kMGTPEZY'[i - 1]}B';
}
String toLocalizedString( String toLocalizedString(
BuildContext context, [ BuildContext context, [
ExceptionContext? exceptionContext, ExceptionContext? exceptionContext,
]) { ]) {
if (this is FileTooBigMatrixException) {
final exception = this as FileTooBigMatrixException;
return L10n.of(context)!.fileIsTooBigForServer(
_formatFileSize(exception.maxFileSize),
);
}
if (this is MatrixException) { if (this is MatrixException) {
switch ((this as MatrixException).error) { switch ((this as MatrixException).error) {
case MatrixError.M_FORBIDDEN: case MatrixError.M_FORBIDDEN:
@ -30,9 +50,6 @@ extension LocalizedExceptionExtension on Object {
if (this is InvalidPassphraseException) { if (this is InvalidPassphraseException) {
return L10n.of(context)!.wrongRecoveryKey; return L10n.of(context)!.wrongRecoveryKey;
} }
if (this is FileTooBigMatrixException) {
return L10n.of(context)!.fileIsTooBigForServer;
}
if (this is BadServerVersionsException) { if (this is BadServerVersionsException) {
final serverVersions = (this as BadServerVersionsException) final serverVersions = (this as BadServerVersionsException)
.serverVersions .serverVersions

Loading…
Cancel
Save