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": {},
"reopenChat": "Chat wieder eröffnen",
"@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": {},
"noOtherDevicesFound": "Keine anderen Geräte anwesend",

@ -2422,8 +2422,13 @@
"@noBackupWarning": {},
"noOtherDevicesFound": "No other devices found",
"@noOtherDevicesFound": {},
"fileIsTooBigForServer": "The server reports that the file is too large to be sent.",
"@fileIsTooBigForServer": {},
"fileIsTooBigForServer": "Unable to send! The server only supports attachments up to {max}.",
"@fileIsTooBigForServer": {
"type": "text",
"placeholders": {
"max": {}
}
},
"fileHasBeenSavedAt": "File has been saved at {path}",
"@fileHasBeenSavedAt": {
"type": "text",
@ -2762,5 +2767,24 @@
"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.",
"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(
files: details.files,
room: room,
outerContext: context,
),
);
}
@ -483,7 +484,7 @@ class ChatController extends State<ChatPageWithRoom>
final result = await AppLock.of(context).pauseWhile(
FilePicker.platform.pickFiles(
compressionQuality: 0,
allowMultiple: false,
allowMultiple: true,
),
);
if (result == null || result.files.isEmpty) return;
@ -492,6 +493,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog(
files: result.xFiles,
room: room,
outerContext: context,
),
);
}
@ -503,6 +505,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog(
files: [XFile.fromData(image)],
room: room,
outerContext: context,
),
);
}
@ -512,7 +515,7 @@ class ChatController extends State<ChatPageWithRoom>
FilePicker.platform.pickFiles(
compressionQuality: 0,
type: FileType.image,
allowMultiple: false,
allowMultiple: true,
),
);
if (result == null || result.files.isEmpty) return;
@ -522,6 +525,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog(
files: result.xFiles,
room: room,
outerContext: context,
),
);
}
@ -537,6 +541,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog(
files: [file],
room: room,
outerContext: context,
),
);
}
@ -555,6 +560,7 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => SendFileDialog(
files: [file],
room: room,
outerContext: context,
),
);
}

@ -6,12 +6,11 @@ import 'package:flutter/material.dart';
import 'package:cross_file/cross_file.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:mime/mime.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/platform_infos.dart';
import 'package:fluffychat/utils/size_string.dart';
@ -20,10 +19,12 @@ import '../../utils/resize_video.dart';
class SendFileDialog extends StatefulWidget {
final Room room;
final List<XFile> files;
final BuildContext outerContext;
const SendFileDialog({
required this.room,
required this.files,
required this.outerContext,
super.key,
});
@ -38,65 +39,98 @@ class SendFileDialogState extends State<SendFileDialog> {
static const int minSizeToCompress = 20 * 1024;
Future<void> _send() async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(widget.outerContext);
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(
context: context,
future: () async {
final clientConfig = await widget.room.client.getConfig();
final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024;
for (final xfile in widget.files) {
final MatrixFile file;
MatrixImageFile? thumbnail;
final length = await xfile.length();
final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path);
for (final xfile in widget.files) {
final MatrixFile file;
MatrixImageFile? thumbnail;
final length = await xfile.length();
final mimeType = xfile.mimeType ?? lookupMimeType(xfile.path);
// If file is a video, shrink it!
if (PlatformInfos.isMobile &&
mimeType != null &&
mimeType.startsWith('video') &&
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 (mimeType != null &&
mimeType.startsWith('video') &&
length > minSizeToCompress &&
!origImage) {
file = await xfile.resizeVideo();
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.bytes.length > maxUploadSize) {
throw FileTooBigMatrixException(length, maxUploadSize);
}
if (widget.files.length > 1) {
scaffoldMessenger.showLoadingSnackBar(
l10n.sendingAttachmentCountOfCount(
widget.files.indexOf(xfile) + 1,
widget.files.length,
),
);
} else {
scaffoldMessenger.showLoadingSnackBar(l10n.sendingAttachment);
}
if (file.bytes.length > maxUploadSize) {
throw FileTooBigMatrixException(length, maxUploadSize);
try {
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
.sendFileEvent(
await widget.room.sendFileEvent(
file,
thumbnail: thumbnail,
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;
}
@ -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,
outerContext: context,
),
);
Matrix.of(context).shareContent = null;

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
@ -10,10 +11,29 @@ import 'package:matrix/matrix.dart';
import 'uia_request_manager.dart';
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(
BuildContext context, [
ExceptionContext? exceptionContext,
]) {
if (this is FileTooBigMatrixException) {
final exception = this as FileTooBigMatrixException;
return L10n.of(context)!.fileIsTooBigForServer(
_formatFileSize(exception.maxFileSize),
);
}
if (this is MatrixException) {
switch ((this as MatrixException).error) {
case MatrixError.M_FORBIDDEN:
@ -30,9 +50,6 @@ extension LocalizedExceptionExtension on Object {
if (this is InvalidPassphraseException) {
return L10n.of(context)!.wrongRecoveryKey;
}
if (this is FileTooBigMatrixException) {
return L10n.of(context)!.fileIsTooBigForServer;
}
if (this is BadServerVersionsException) {
final serverVersions = (this as BadServerVersionsException)
.serverVersions

Loading…
Cancel
Save