feat: Sending multiple files at once

This commit is contained in:
krille-chan 2024-09-22 19:18:08 +02:00
commit b83503585f
No known key found for this signature in database
6 changed files with 165 additions and 58 deletions

View file

@ -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,
),
);
}

View file

@ -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 (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);
}
widget.room
.sendFileEvent(
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);
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),
],
),
),
);
}
}

View file

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

View file

@ -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