From 7333dceb009e2f74f08a55d7a046973b1fddc6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=B9i=20Trung=20Hi=E1=BA=BFu?= Date: Tue, 19 Mar 2024 14:08:23 +0700 Subject: [PATCH 001/183] TW-1435: Big dot in message notification and chat list preview (#1600) (cherry picked from commit e4612a887079d0e29d90f086fb5da20731932f57) --- lib/presentation/mixins/chat_list_item_mixin.dart | 2 ++ pubspec.lock | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/presentation/mixins/chat_list_item_mixin.dart b/lib/presentation/mixins/chat_list_item_mixin.dart index 1530c4c39f..ca1bc8def5 100644 --- a/lib/presentation/mixins/chat_list_item_mixin.dart +++ b/lib/presentation/mixins/chat_list_item_mixin.dart @@ -19,6 +19,7 @@ mixin ChatListItemMixin { hideEdit: true, plaintextBody: true, removeMarkdown: true, + removeBreakLine: true, ) ?? Future.value(L10n.of(context)!.emptyChat), builder: (context, snapshot) { @@ -108,6 +109,7 @@ mixin ChatListItemMixin { hideEdit: true, plaintextBody: true, removeMarkdown: true, + removeBreakLine: true, ) ?? L10n.of(context)!.emptyChat; diff --git a/pubspec.lock b/pubspec.lock index 7bc5d12c23..b93823499a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1559,7 +1559,7 @@ packages: description: path: "." ref: "twake-supported-0.22.6" - resolved-ref: "40bab63ea8895015aed935bd3fb6c279c6fe294c" + resolved-ref: "22311972c4d781133b893ae032dcb509b1351075" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" From d53545193c5e3a11fafa65bf49e1ed87798cd9a0 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 19 Mar 2024 15:18:43 +0700 Subject: [PATCH 002/183] TW-1453: Error handling when update profile fail (cherry picked from commit d7cad625e45dafe5318797e3caa90e959761fea1) --- .../settings_profile/settings_profile.dart | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 24294b30ac..ada07b9f99 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/room/upload_content_state.dart'; +import 'package:fluffychat/domain/app_state/settings/update_profile_failure.dart'; import 'package:fluffychat/domain/app_state/settings/update_profile_success.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_for_web_interactor.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; @@ -375,6 +376,19 @@ class SettingsProfileController extends State Logs().e( 'SettingsProfile::_handleUploadAvatarOnData() - failure: $failure', ); + if (failure is UploadContentFailed) { + TwakeDialog.hideLoadingDialog(context); + TwakeSnackBar.show( + context, + failure.exception.toString(), + ); + } else if (failure is FileTooBigMatrix) { + TwakeDialog.hideLoadingDialog(context); + TwakeSnackBar.show( + context, + failure.fileTooBigMatrixException.toString(), + ); + } }, (success) { Logs().d( @@ -437,6 +451,9 @@ class SettingsProfileController extends State Logs().e( 'SettingsProfile::_handleUploadProfileOnData() - failure: $failure', ); + if (failure is UpdateProfileFailure) { + _handleUpdateProfileFailure(failure.exception.toString()); + } }, (success) { Logs().d( @@ -447,7 +464,8 @@ class SettingsProfileController extends State final newProfile = Profile( userId: client.userID!, displayName: success.displayName ?? displayName, - avatarUrl: success.avatar, + avatarUrl: + success.avatar == null ? currentProfile?.avatarUrl : null, ); _sendAccountDataEvent(profile: newProfile); if (!success.isDeleteAvatar) { @@ -549,6 +567,18 @@ class SettingsProfileController extends State }); } + void _handleUpdateProfileFailure(String errorMessage) { + TwakeDialog.hideLoadingDialog(context); + TwakeSnackBar.show( + context, + errorMessage, + ); + _clearImageInLocal(); + if (currentProfile != null) { + _sendAccountDataEvent(profile: currentProfile!); + } + } + @override void initState() { _handleViewState(); From 2bf7c1b9b81f839cefc578cd4dff3ab99e12ec49 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 18 Mar 2024 17:55:15 +0700 Subject: [PATCH 003/183] TW-1565: Verify the message has video and mime type is avi, wmv (cherry picked from commit 2cae7563666b55f22f20908b6589724c36e6b259) --- .../preview_file/supported_preview_file_types.dart | 12 ++++++++++++ lib/utils/extension/event_info_extension.dart | 12 ++++++++++++ lib/utils/matrix_sdk_extensions/event_extension.dart | 7 +++++-- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 lib/utils/extension/event_info_extension.dart diff --git a/lib/domain/model/preview_file/supported_preview_file_types.dart b/lib/domain/model/preview_file/supported_preview_file_types.dart index b7a0c65aff..0b3cd1371c 100644 --- a/lib/domain/model/preview_file/supported_preview_file_types.dart +++ b/lib/domain/model/preview_file/supported_preview_file_types.dart @@ -201,4 +201,16 @@ class SupportedPreviewFileTypes { static const pptFileTypes = ['ppt', 'pptx', 'pps', 'ppsx', 'ppsm', 'pptm']; static const zipFileTypes = ['zip']; + + static const aviMineType = [ + 'application/x-troff-msvideo', + 'video/avi', + 'video/msvideo', + 'video/x-msvideo', + ]; + + static const wmvMineType = [ + 'video/x-ms-wmv', + 'video/x-ms-asf', + ]; } diff --git a/lib/utils/extension/event_info_extension.dart b/lib/utils/extension/event_info_extension.dart new file mode 100644 index 0000000000..9ff5427501 --- /dev/null +++ b/lib/utils/extension/event_info_extension.dart @@ -0,0 +1,12 @@ +import 'package:fluffychat/domain/model/preview_file/supported_preview_file_types.dart'; +import 'package:matrix/matrix.dart'; + +extension EventInfoExtension on Event { + bool get isVideoAvailable => !isWmvVideo && !isAviVideo; + + bool get isWmvVideo => + SupportedPreviewFileTypes.wmvMineType.contains(attachmentMimetype); + + bool get isAviVideo => + SupportedPreviewFileTypes.aviMineType.contains(attachmentMimetype); +} diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 4a95631900..5fe62f452a 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/domain/model/extensions/string_extension.dart'; import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/extension/event_info_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; @@ -152,10 +153,12 @@ extension LocalizedBody on Event { bool get isOwnMessage => senderId == room.client.userID; - bool get timelineOverlayMessage => { + bool get timelineOverlayMessage => + { MessageTypes.Video, MessageTypes.Image, - }.contains(messageType); + }.contains(messageType) && + isVideoAvailable; bool get hideDisplayNameInBubbleChat => { MessageTypes.Video, From 6d52c697fa98e5f3def1de238016eec86fd134af Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 18 Mar 2024 17:56:44 +0700 Subject: [PATCH 004/183] TW-1565: Update UI if video has mime type is avi, wmv (cherry picked from commit a522c3fdd498b471cbde03ae3c55707db9e3a215) --- lib/pages/chat/events/message_content.dart | 33 ++++++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index aeb332ffba..651dd7c59d 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pages/chat/events/sending_image_info_widget.dart'; import 'package:fluffychat/pages/chat/events/sending_video_widget.dart'; import 'package:fluffychat/pages/chat/events/unknown_content.dart'; import 'package:fluffychat/presentation/model/file/display_image_info.dart'; +import 'package:fluffychat/utils/extension/event_info_extension.dart'; import 'package:fluffychat/utils/extension/image_size_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -90,13 +91,33 @@ class MessageContent extends StatelessWidget ), ); case MessageTypes.Video: - return _MessageVideoBuilder( - event: event, - onFileTapped: (event) => onFileTapped( - context: context, + if (event.isVideoAvailable) { + return _MessageVideoBuilder( event: event, - ), - ); + onFileTapped: (event) => onFileTapped( + context: context, + event: event, + ), + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + MessageDownloadContent( + event, + onFileTapped: (event) => onFileTapped( + context: context, + event: event, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: endOfBubbleWidget, + ), + ], + ); + } + case MessageTypes.File: return Column( crossAxisAlignment: CrossAxisAlignment.end, From 2ff9e6ec1dcd69df5396a6f37eba9be09b71d7dd Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 19 Mar 2024 01:01:48 +0700 Subject: [PATCH 005/183] TW-1565: Update preview icon when reply a message is video has mime type avi or wmv (cherry picked from commit 2b202d6db723f12a4b2e63b1c7d8947f2fcd803c) --- lib/pages/chat/events/reply_content.dart | 62 ++++++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index 2c859bee9f..f78cf44f53 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -1,11 +1,14 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/reply_content_style.dart'; +import 'package:fluffychat/utils/extension/event_info_extension.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -85,22 +88,8 @@ class ReplyContent extends StatelessWidget { const SizedBox(width: ReplyContentStyle.contentSpacing), if (displayEvent.hasAttachment) Center( - child: ClipRRect( - borderRadius: ReplyContentStyle.previewedImageBorderRadius, - child: MxcImage( - key: ValueKey(displayEvent.eventId), - noResize: true, - event: displayEvent, - width: ReplyContentStyle.replyContentSize, - height: ReplyContentStyle.replyContentSize, - isThumbnail: true, - fit: BoxFit.cover, - placeholder: (context) { - return BlurHashPlaceHolder( - event: displayEvent, - ); - }, - ), + child: ReplyPreviewIconBuilder( + event: displayEvent, ), ), const SizedBox( @@ -142,6 +131,47 @@ class ReplyContent extends StatelessWidget { } } +class ReplyPreviewIconBuilder extends StatelessWidget { + final Event event; + + const ReplyPreviewIconBuilder({ + Key? key, + required this.event, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (event.messageType == MessageTypes.Video && + !event.isVideoAvailable && + event.mimeType != null) { + return SvgPicture.asset( + event.mimeType!.getIcon( + fileType: event.fileType, + ), + width: ReplyContentStyle.replyContentSize, + height: ReplyContentStyle.replyContentSize, + ); + } + return ClipRRect( + borderRadius: ReplyContentStyle.previewedImageBorderRadius, + child: MxcImage( + key: ValueKey(event.eventId), + noResize: true, + event: event, + width: ReplyContentStyle.replyContentSize, + height: ReplyContentStyle.replyContentSize, + isThumbnail: true, + fit: BoxFit.cover, + placeholder: (context) { + return BlurHashPlaceHolder( + event: event, + ); + }, + ), + ); + } +} + class BlurHashPlaceHolder extends StatelessWidget { final Event event; From f3bdec0ec4ff78e70ea80dfe96ea460190035e80 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 19 Mar 2024 01:02:17 +0700 Subject: [PATCH 006/183] TW-1565: Update padding for `MessageContent` widget (cherry picked from commit a66a00f8fca9b7fdecfa3bc24dfdb454af61196c) --- lib/pages/chat/events/message_content.dart | 6 +++--- lib/pages/chat/events/message_content_style.dart | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 651dd7c59d..dac8af6376 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -111,7 +111,7 @@ class MessageContent extends StatelessWidget ), ), Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: MessageContentStyle.endOfBubbleWidgetPadding, child: endOfBubbleWidget, ), ], @@ -130,7 +130,7 @@ class MessageContent extends StatelessWidget ), ), Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: MessageContentStyle.endOfBubbleWidgetPadding, child: endOfBubbleWidget, ), ], @@ -153,7 +153,7 @@ class MessageContent extends StatelessWidget event.numberEmotes > 0 && event.numberEmotes <= 10; return Padding( - padding: const EdgeInsets.only(left: 8.0, right: 8.0), + padding: MessageContentStyle.emojiPadding, child: HtmlMessage( html: html, defaultTextStyle: Theme.of(context).textTheme.bodyLarge, diff --git a/lib/pages/chat/events/message_content_style.dart b/lib/pages/chat/events/message_content_style.dart index 15826e994c..9fe099a541 100644 --- a/lib/pages/chat/events/message_content_style.dart +++ b/lib/pages/chat/events/message_content_style.dart @@ -67,4 +67,12 @@ class MessageContentStyle { BorderRadius.all(Radius.circular(12.0)); static const backIconColor = Colors.white; + + static const EdgeInsets endOfBubbleWidgetPadding = + EdgeInsets.symmetric(vertical: 4); + + static const EdgeInsets emojiPadding = EdgeInsets.only( + left: 8.0, + right: 8.0, + ); } From f313a66081cab63b0a2c3aa16d437ccdf0a4ad98 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 20 Mar 2024 16:11:12 +0700 Subject: [PATCH 007/183] TW-1586: add task model and task state (cherry picked from commit ee5819cf1f53f37b33bc8b2a31e13e83f8f6e102) --- lib/utils/task_queue/task.dart | 30 ++++++++++++++++++++++++++++ lib/utils/task_queue/task_state.dart | 20 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 lib/utils/task_queue/task.dart create mode 100644 lib/utils/task_queue/task_state.dart diff --git a/lib/utils/task_queue/task.dart b/lib/utils/task_queue/task.dart new file mode 100644 index 0000000000..23e5f72514 --- /dev/null +++ b/lib/utils/task_queue/task.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:fluffychat/utils/task_queue/task_state.dart'; + +class Task with EquatableMixin { + final String? id; + final Future Function() runnable; + final void Function()? onTaskCompleted; + + Task({ + this.id, + required this.runnable, + this.onTaskCompleted, + }); + + Future execute() async { + final resultCompleter = Completer(); + try { + final result = await runnable.call(); + resultCompleter.complete(TaskSuccess(result: result)); + } catch (exception) { + resultCompleter.completeError(TaskFailure(exception: exception)); + } + return resultCompleter.future; + } + + @override + List get props => [id, runnable]; +} diff --git a/lib/utils/task_queue/task_state.dart b/lib/utils/task_queue/task_state.dart new file mode 100644 index 0000000000..dbc2edbd45 --- /dev/null +++ b/lib/utils/task_queue/task_state.dart @@ -0,0 +1,20 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; + +class TaskSuccess extends Success { + final dynamic result; + + const TaskSuccess({this.result}); + + @override + List get props => [result]; +} + +class TaskFailure extends Failure { + final dynamic exception; + + const TaskFailure({this.exception}); + + @override + List get props => [exception]; +} From 27f5d78a3b06406dec29f440d32ea779a432e676 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 20 Mar 2024 16:11:35 +0700 Subject: [PATCH 008/183] TW-1586: add worker queue class (cherry picked from commit 9925c059d246a8f6e5160574f48280aded11fd0b) --- lib/utils/task_queue/worker_queue.dart | 78 ++++++ test/worker_queue_test.dart | 319 +++++++++++++++++++++++++ 2 files changed, 397 insertions(+) create mode 100644 lib/utils/task_queue/worker_queue.dart create mode 100644 test/worker_queue_test.dart diff --git a/lib/utils/task_queue/worker_queue.dart b/lib/utils/task_queue/worker_queue.dart new file mode 100644 index 0000000000..f58766370d --- /dev/null +++ b/lib/utils/task_queue/worker_queue.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:fluffychat/utils/task_queue/task.dart'; +import 'package:matrix/matrix.dart'; + +typedef OnTaskCompleted = void Function(String? taskId); + +abstract class WorkerQueue { + final Queue _queue = Queue(); + Completer? _completer; + + String get workerName; + + Queue get queue => _queue; + + Future addTask(Task task) { + _queue.add(task); + Logs().i( + 'WorkerQueue<$workerName>::addTask(): QUEUE_LENGTH: ${_queue.length}', + ); + return _processTask(); + } + + Future _processTask() async { + try { + if (_completer != null) { + return _completer!.future; + } + _completer = Completer(); + if (_queue.isNotEmpty) { + final firstTask = _queue.removeFirst(); + Logs().i('WorkerQueue<$workerName>::_processTask(): ${firstTask.id}'); + firstTask + .execute() + .then(_handleTaskExecuteCompleted) + .catchError(_handleTaskExecuteError) + .whenComplete(() { + firstTask.onTaskCompleted?.call(); + }); + } else { + _completer?.complete(); + } + return _completer!.future; + } catch (e) { + Logs().e('WorkerQueue<$workerName>::_processTask(): $e'); + _completer?.complete(e); + } + } + + void _handleTaskExecuteCompleted(dynamic value) { + Logs().i('WorkerQueue<$workerName>::_handleTaskExecuteCompleted(): $value'); + _completer?.complete(value); + _releaseCompleter(); + if (_queue.isNotEmpty) { + _processTask(); + } + } + + void _handleTaskExecuteError(error) { + Logs().i('WorkerQueue<$workerName>::_handleTaskExecuteError(): $error'); + _completer?.complete(error); + _releaseCompleter(); + if (_queue.isNotEmpty) { + _processTask(); + } + } + + void _releaseCompleter() { + _completer = null; + } + + Future release() async { + Logs().i('WorkerQueue<$workerName>::release():'); + _queue.clear(); + _releaseCompleter(); + } +} diff --git a/test/worker_queue_test.dart b/test/worker_queue_test.dart new file mode 100644 index 0000000000..e80b2564af --- /dev/null +++ b/test/worker_queue_test.dart @@ -0,0 +1,319 @@ +import 'package:fluffychat/utils/manager/downloading_worker_queue.dart'; +import 'package:fluffychat/utils/task_queue/task.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:matrix/matrix.dart'; + +void main() { + // Helper function to generate tasks + Task generateTask( + String taskId, + Future Function() runnable, { + void Function()? onTaskCompleted, + }) { + return Task( + id: taskId, + runnable: runnable, + onTaskCompleted: onTaskCompleted, + ); + } + + // Testing group for worker queue + group("worker queue test", () { + // Test case for adding tasks without cancelation or error + test(""" + WHEN add 4 tasks without any cancel or error in worker queue, + SHOULD completed tasks should be order and in time + """, () async { + final completedTasks = []; + final alreadyRunTasks = []; + final tasks = List.generate(4, (index) { + return generateTask( + '${index + 1}', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(index + 1); + return index + 1; + }), + onTaskCompleted: () { + Logs().i('task${index + 1} completed'); + completedTasks.add(index + 1); + }, + ); + }); + + final workerQueue = DownloadWorkerQueue(); + tasks.forEach(workerQueue.addTask); + + await Future.delayed(const Duration(seconds: 1)); + expect(workerQueue.queue.length, 3); + await Future.delayed(const Duration(seconds: 1)); + expect(workerQueue.queue.length, 2); + await Future.delayed(const Duration(seconds: 7)); + // Verify that all tasks completed in the expected order + expect(completedTasks, [1, 2, 3, 4]); + expect(alreadyRunTasks, [1, 2, 3, 4]); + }); + + test(""" + WHEN add 4 tasks consecutively, + THEN there is an error task while the other task is running + SHOULD completed tasks should be order + """, () async { + final completedTasks = []; + final alreadyRunTasks = []; + final errorTasks = []; + + final tasks = [ + generateTask( + '1', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(1); + return 1; + }), + onTaskCompleted: () { + Logs().i('task1 completed'); + completedTasks.add(1); + }, + ), + generateTask( + '2', + () async { + await Future.delayed(const Duration(seconds: 1)); + errorTasks.add('task2 error'); + throw Exception('task2 error'); + }, + onTaskCompleted: () { + Logs().i('task2 completed'); + completedTasks.add(2); + }, + ), + generateTask( + '3', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(3); + return 3; + }), + onTaskCompleted: () { + Logs().i('task3 completed'); + completedTasks.add(3); + }, + ), + generateTask( + '4', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(4); + return 4; + }), + onTaskCompleted: () { + Logs().i('task4 completed'); + completedTasks.add(4); + }, + ), + ]; + + final workerQueue = DownloadWorkerQueue(); + try { + for (final task in tasks) { + workerQueue.addTask(task); + } + } catch (e) { + Logs().e('workerQueue.addTask(): $e'); + } + + await Future.delayed(const Duration(seconds: 10)); + // Verify that completedTasks includes tasks 1, 3, and 4 but not the errored task 2 + expect(completedTasks, containsAllInOrder([1, 3, 4])); + expect(alreadyRunTasks, containsAllInOrder([1, 3, 4])); + expect( + errorTasks, + ['task2 error'], + ); // Verify that task 2's error is recorded + }); + + test(""" + WHEN a task is processing in queue with 4 tasks + THEN add a new task to the queue + SHOULD the new task should be added to the end of the queue + """, () async { + final completedTasks = []; + final alreadyRunTasks = []; + final tasks = List.generate(4, (index) { + return generateTask( + '${index + 1}', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(index + 1); + return index + 1; + }), + onTaskCompleted: () { + Logs().i('task${index + 1} completed'); + completedTasks.add(index + 1); + }, + ); + }); + + final workerQueue = DownloadWorkerQueue(); + tasks.forEach(workerQueue.addTask); + + await Future.delayed(const Duration(seconds: 1)); + expect(workerQueue.queue.length, 3); + + workerQueue.addTask( + Task( + id: '5', + runnable: () async => + await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(5); + return 5; + }), + onTaskCompleted: () { + Logs().i('task5 completed'); + completedTasks.add(5); + }, + ), + ); + + expect(workerQueue.queue.length, 4); + await Future.delayed(const Duration(seconds: 10)); + // Verify that all tasks completed in the expected order + expect(completedTasks, containsAllInOrder([1, 2, 3, 4])); + expect(alreadyRunTasks, containsAllInOrder([1, 2, 3, 4])); + }); + + test(""" + WHEN add 4 tasks with the same task id to the worker queue, + SHOULD the queue executes task in order and consider the duplicate task id as normal task + """, () async { + final completedTasks = []; + final alreadyRunTasks = []; + final tasks = List.generate(4, (index) { + return generateTask( + '1', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(index + 1); + return index + 1; + }), + onTaskCompleted: () { + Logs().i('task${index + 1} completed'); + completedTasks.add(index + 1); + }, + ); + }); + + final workerQueue = DownloadWorkerQueue(); + tasks.forEach(workerQueue.addTask); + + await Future.delayed(const Duration(seconds: 1)); + expect(workerQueue.queue.length, 3); + expect(workerQueue.queue.first.id, '1'); + + await Future.delayed(const Duration(seconds: 1)); + expect(workerQueue.queue.length, 2); + await Future.delayed(const Duration(seconds: 7)); + // Verify that all tasks completed in the expected order + expect(completedTasks, containsAllInOrder([1, 2, 3, 4])); + expect(alreadyRunTasks, containsAllInOrder([1, 2, 3, 4])); + }); + + test(""" + WHEN add 4 tasks with the same task id to the worker queue, + THEN in task complete, there is async task + SHOULD async task do not block any task in the queue + """, () async { + final completedTasks = []; + final alreadyRunTasks = []; + final tasks = List.generate(4, (index) { + return generateTask( + '1', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(index + 1); + return index + 1; + }), + onTaskCompleted: () async { + Logs().i('task${index + 1} completed'); + await Future.delayed(const Duration(seconds: 1)); + completedTasks.add(index + 1); + }, + ); + }); + + final workerQueue = DownloadWorkerQueue(); + tasks.forEach(workerQueue.addTask); + + await Future.delayed(const Duration(seconds: 9)); + // Verify that all tasks completed in the expected order + expect(alreadyRunTasks, containsAllInOrder([1, 2, 3, 4])); + await Future.delayed(const Duration(seconds: 4)); + expect(completedTasks, containsAllInOrder([1, 2, 3, 4])); + }); + + test(""" + WHEN add 4 tasks at once to the queue using Future.wait + SHOULD the tasks are executed in order and the queue is empty after all tasks are completed + """, () async { + final completedTasks = []; + final alreadyRunTasks = []; + + final tasks = List.generate(4, (index) { + return generateTask( + '1', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(index + 1); + return index + 1; + }), + onTaskCompleted: () async { + Logs().i('task${index + 1} completed'); + completedTasks.add(index + 1); + }, + ); + }); + + final workerQueue = DownloadWorkerQueue(); + Future.wait(tasks.map((task) => workerQueue.addTask(task))); + await Future.delayed(const Duration(seconds: 1)); + expect(workerQueue.queue.length, 3); + expect(workerQueue.queue.first.id, '1'); + + await Future.delayed(const Duration(seconds: 9)); + // Verify that all tasks completed in the expected order + expect(alreadyRunTasks, containsAllInOrder([1, 2, 3, 4])); + expect(completedTasks, containsAllInOrder([1, 2, 3, 4])); + }); + + test(""" + WHEN add 4 tasks to the worker queue + THEN task 1 is processing + THEN remove the last task from the queue + SHOULD the queue executes task except the removed task + """, () async { + final completedTasks = []; + final alreadyRunTasks = []; + + final tasks = List.generate(4, (index) { + return generateTask( + '1', + () async => await Future.delayed(const Duration(seconds: 2), () { + alreadyRunTasks.add(index + 1); + return index + 1; + }), + onTaskCompleted: () async { + Logs().i('task${index + 1} completed'); + completedTasks.add(index + 1); + }, + ); + }); + + final workerQueue = DownloadWorkerQueue(); + Future.wait(tasks.map((task) => workerQueue.addTask(task))); + await Future.delayed(const Duration(seconds: 1)); + + workerQueue.queue.removeLast(); + // Verify that the worker queue has 2 remaining tasks and the first task is being processed + expect(workerQueue.queue.length, 2); + expect(workerQueue.queue.first.id, '1'); + + await Future.delayed(const Duration(seconds: 7)); + // Verify that all tasks completed in the expected order + expect(alreadyRunTasks, containsAllInOrder([1, 2, 3])); + expect(completedTasks, containsAllInOrder([1, 2, 3])); + }); + }); +} From 286158f5cea5a8cfaef2d3aa0a01192379be35ae Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 20 Mar 2024 16:12:37 +0700 Subject: [PATCH 009/183] TW-1586: add download worker queue implements worker queue (cherry picked from commit f62472492dce01b88899e785bff2e58f78e34647) --- lib/utils/manager/downloading_worker_queue.dart | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 lib/utils/manager/downloading_worker_queue.dart diff --git a/lib/utils/manager/downloading_worker_queue.dart b/lib/utils/manager/downloading_worker_queue.dart new file mode 100644 index 0000000000..9ddd7a5a34 --- /dev/null +++ b/lib/utils/manager/downloading_worker_queue.dart @@ -0,0 +1,6 @@ +import 'package:fluffychat/utils/task_queue/worker_queue.dart'; + +class DownloadWorkerQueue extends WorkerQueue { + @override + String get workerName => 'downloading_queue'; +} From 62ccc5a4157e027d8fad2a3603f73c48035a67c4 Mon Sep 17 00:00:00 2001 From: hieubt Date: Thu, 21 Mar 2024 10:33:27 +0700 Subject: [PATCH 010/183] hot-fix: add unpin condition (cherry picked from commit cb8927179073e790d05dd662fe4f11a9546b7c77) --- lib/pages/chat/chat.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 5f6d3ebdc6..1770181e92 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1401,7 +1401,9 @@ class ChatController extends State iconAction: action.getIconData( unpin: isUnpinEvent(event), ), - imagePath: action.getImagePath(), + imagePath: action.getImagePath( + unpin: isUnpinEvent(event), + ), onCallbackAction: () => _handleClickOnContextMenuItem( action, event, From 9016ec4e9dfb4fb9cac60489cff1faff7e354871 Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 20 Mar 2024 15:12:31 +0700 Subject: [PATCH 011/183] TW-1124: Update profile info view for direct chat (cherry picked from commit 2ebeda0655c749105f9d764525e1992b210ad167) --- .../chat_profile_info_style.dart | 1 + .../chat_profile_info_view.dart | 83 ++++++++++--------- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/lib/pages/chat_profile_info/chat_profile_info_style.dart b/lib/pages/chat_profile_info/chat_profile_info_style.dart index cc59821150..cc250dbb88 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_style.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_style.dart @@ -9,6 +9,7 @@ class ChatProfileInfoStyle { static const double textSpacing = 4; static const double avatarFontSize = 36; + static const double avatarSize = 96; static BorderRadius copiableContainerBorderRadius = BorderRadius.circular(16); diff --git a/lib/pages/chat_profile_info/chat_profile_info_view.dart b/lib/pages/chat_profile_info/chat_profile_info_view.dart index cd1963c82b..01f0a501e9 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_view.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_view.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -17,7 +18,6 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_style.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; class ProfileInfoView extends StatelessWidget { final ProfileInfoController controller; @@ -106,7 +106,6 @@ class ProfileInfoView extends StatelessWidget { } class _Information extends StatelessWidget { - static const double avatarRatio = 1; const _Information({ Key? key, @@ -128,54 +127,56 @@ class _Information extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ - LayoutBuilder( - builder: (context, constraints) => Builder( - builder: (context) { - final text = displayName?.getShortcutNameForAvatar() ?? '@'; - final placeholder = Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: text.avatarColors, - stops: RoundAvatarStyle.defaultGradientStops, + Padding( + padding: ChatProfileInfoStyle.mainPadding, + child: LayoutBuilder( + builder: (context, constraints) => Builder( + builder: (context) { + final text = displayName?.getShortcutNameForAvatar() ?? '@'; + final placeholder = Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: text.avatarColors, + stops: RoundAvatarStyle.defaultGradientStops, + ), ), - ), - width: constraints.maxWidth, - height: constraints.maxWidth * avatarRatio, - child: Center( - child: Text( - text, - style: TextStyle( - fontSize: ChatProfileInfoStyle.avatarFontSize, - color: AvatarStyle.defaultTextColor(true), - fontFamily: AvatarStyle.fontFamily, - fontWeight: AvatarStyle.fontWeight, + width: ChatProfileInfoStyle.avatarSize, + height: ChatProfileInfoStyle.avatarSize, + child: Center( + child: Text( + text, + style: TextStyle( + fontSize: ChatProfileInfoStyle.avatarFontSize, + color: AvatarStyle.defaultTextColor(true), + fontFamily: AvatarStyle.fontFamily, + fontWeight: AvatarStyle.fontWeight, + ), ), ), - ), - ); - if (avatarUri == null) { - return placeholder; - } - return MxcImage( - uri: avatarUri, - width: constraints.maxWidth, - height: constraints.maxWidth * avatarRatio, - fit: BoxFit.cover, - placeholder: (_) => placeholder, - cacheKey: avatarUri.toString(), - noResize: true, - ); - }, + ); + if (avatarUri == null) { + return placeholder; + } + return Avatar( + mxContent: avatarUri, + name: displayName, + size: ChatProfileInfoStyle.avatarSize, + fontSize: ChatProfileInfoStyle.avatarFontSize, + ); + }, + ), ), ), Padding( padding: ChatProfileInfoStyle.mainPadding, child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( displayName ?? '', From 112ec78ad6b3d63d17565e32912dab003c1c17be Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 20 Mar 2024 15:31:04 +0700 Subject: [PATCH 012/183] fixup! TW-1124: Update profile info view for direct chat (cherry picked from commit f1f26448c34d3a91ea23e96ceff43a9242f49152) --- lib/pages/chat_profile_info/chat_profile_info_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/chat_profile_info/chat_profile_info_view.dart b/lib/pages/chat_profile_info/chat_profile_info_view.dart index 01f0a501e9..1cd4d67027 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_view.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_view.dart @@ -106,7 +106,6 @@ class ProfileInfoView extends StatelessWidget { } class _Information extends StatelessWidget { - const _Information({ Key? key, this.avatarUri, From 0616d02b011ddd59c6be8e437665ab023dcd8fdf Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 20 Mar 2024 16:37:08 +0700 Subject: [PATCH 013/183] TW-1528: Close selection mode when click button and selection item is empty (cherry picked from commit b55603a3e7665e8452891bfed63058bd39d1bbed) --- lib/pages/chat_list/chat_list.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index a48c44775b..10fa9d7132 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -210,6 +210,9 @@ class ChatListController extends State if (conversation != null && conversation.isSelected) { tempConversationSelectionPresentation.remove(conversation); + if (tempConversationSelectionPresentation.isEmpty) { + toggleSelectMode(); + } } else { tempConversationSelectionPresentation.add( ConversationSelectionPresentation( @@ -239,11 +242,7 @@ class ChatListController extends State } void onClickClearSelection() { - if (conversationSelectionNotifier.value.isNotEmpty) { - _clearSelectionItem(); - } else { - toggleSelectMode(); - } + toggleSelectMode(); } void resetActiveSpaceId() { From 51fbb57bc62ef6874a11544c358b18d27db1672d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 21 Mar 2024 00:07:16 +0700 Subject: [PATCH 014/183] TW-1564: Improvement for video player (cherry picked from commit 6ae07aa2f9896d2dbf331d47bc80cb452d7a6a41) --- .../image_viewer/media_viewer_app_bar.dart | 2 ++ .../media_viewer_app_bar_view.dart | 6 ++++-- lib/widgets/video_player.dart | 20 +++++-------------- lib/widgets/video_viewer_desktop_theme.dart | 6 ++++++ lib/widgets/video_viewer_mobile_theme.dart | 10 ++++++++++ lib/widgets/video_viewer_style.dart | 2 ++ 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/pages/image_viewer/media_viewer_app_bar.dart b/lib/pages/image_viewer/media_viewer_app_bar.dart index 11da155cbf..9285827f35 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar.dart @@ -16,10 +16,12 @@ class MediaViewerAppBar extends StatefulWidget { Key? key, this.showAppbarPreviewNotifier, this.event, + this.enablePaddingAppbar = true, }) : super(key: key); final ValueNotifier? showAppbarPreviewNotifier; final Event? event; + final bool? enablePaddingAppbar; static final responsiveUtils = getIt.get(); diff --git a/lib/pages/image_viewer/media_viewer_app_bar_view.dart b/lib/pages/image_viewer/media_viewer_app_bar_view.dart index 59988889ce..5e39a8fdbe 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_view.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_view.dart @@ -24,8 +24,10 @@ class MediaViewerAppbarView extends StatelessWidget { duration: MediaViewewAppbarStyle.opacityAnimationDuration, curve: Curves.easeIn, child: Container( - padding: showAppbarPreview - ? ImageViewerStyle.paddingTopAppBar + padding: controller.widget.enablePaddingAppbar == true + ? showAppbarPreview + ? ImageViewerStyle.paddingTopAppBar + : EdgeInsets.zero : EdgeInsets.zero, height: ImageViewerStyle.appBarHeight, width: MediaQuery.sizeOf(context).width, diff --git a/lib/widgets/video_player.dart b/lib/widgets/video_player.dart index 0d7c9e6dcc..2233a7cfbd 100644 --- a/lib/widgets/video_player.dart +++ b/lib/widgets/video_player.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:media_kit/media_kit.dart'; @@ -36,20 +35,11 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - Video( - fill: Colors.black, - pauseUponEnteringBackgroundMode: true, - resumeUponEnteringForegroundMode: true, - controller: videoController, - ), - MediaViewerAppBar( - event: widget.event, - ), - ], - ), + return Video( + fill: Colors.black, + pauseUponEnteringBackgroundMode: true, + resumeUponEnteringForegroundMode: true, + controller: videoController, ); } } diff --git a/lib/widgets/video_viewer_desktop_theme.dart b/lib/widgets/video_viewer_desktop_theme.dart index b9ca88ec81..fad9432011 100644 --- a/lib/widgets/video_viewer_desktop_theme.dart +++ b/lib/widgets/video_viewer_desktop_theme.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; import 'package:fluffychat/widgets/video_player.dart'; import 'package:fluffychat/widgets/video_viewer_style.dart'; import 'package:flutter/material.dart'; @@ -24,6 +25,11 @@ class VideoViewerDesktopTheme extends StatelessWidget { seekBarHeight: VideoViewerStyle.seekBarHeight, seekBarThumbColor: Theme.of(context).colorScheme.primary, topButtonBarMargin: const EdgeInsets.all(0), + topButtonBar: [ + MediaViewerAppBar( + event: event, + ), + ], ), fullscreen: MaterialDesktopVideoControlsThemeData( seekBarColor: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/lib/widgets/video_viewer_mobile_theme.dart b/lib/widgets/video_viewer_mobile_theme.dart index 82584967ea..ec717f6325 100644 --- a/lib/widgets/video_viewer_mobile_theme.dart +++ b/lib/widgets/video_viewer_mobile_theme.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/video_player.dart'; import 'package:fluffychat/widgets/video_viewer_style.dart'; @@ -25,6 +26,15 @@ class VideoViewerMobileTheme extends StatelessWidget { MaterialPositionIndicator(), Spacer(), ], + topButtonBar: [ + Expanded( + child: MediaViewerAppBar( + event: event, + enablePaddingAppbar: false, + ), + ), + ], + controlsHoverDuration: VideoViewerStyle.controlsHoverDuration, seekBarColor: Theme.of(context).colorScheme.onSurfaceVariant, seekBarPositionColor: Theme.of(context).colorScheme.primary, bottomButtonBarMargin: VideoViewerStyle.bottomBarMargin(context), diff --git a/lib/widgets/video_viewer_style.dart b/lib/widgets/video_viewer_style.dart index ec1976bfa5..07d9425fa3 100644 --- a/lib/widgets/video_viewer_style.dart +++ b/lib/widgets/video_viewer_style.dart @@ -17,4 +17,6 @@ class VideoViewerStyle { static EdgeInsets backButtonMargin(context) => PlatformInfos.isWeb ? const EdgeInsets.only(top: 8.0, left: 16.0) : EdgeInsets.only(top: MediaQuery.of(context).viewPadding.top); + + static const Duration controlsHoverDuration = Duration(seconds: 5); } From 384e5d5af89f8e27081ddb81dc8d395a0e07d46a Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 21 Mar 2024 01:23:36 +0700 Subject: [PATCH 015/183] TW-1453: Improvement for video player for web (cherry picked from commit a71a0a5b769a1d0b32bc1c96842663693212ac52) --- .../chat/events/download_video_widget.dart | 15 ++- .../image_viewer/media_viewer_app_bar.dart | 87 +------------ .../media_viewer_app_bar_view.dart | 30 +++-- .../media_viewer_app_bar_web.dart | 94 +++++++++++++++ .../mixins/media_viewer_app_bar_mixin.dart | 114 ++++++++++++++++++ lib/widgets/video_viewer_desktop_theme.dart | 6 +- 6 files changed, 247 insertions(+), 99 deletions(-) create mode 100644 lib/pages/image_viewer/media_viewer_app_bar_web.dart create mode 100644 lib/presentation/mixins/media_viewer_app_bar_mixin.dart diff --git a/lib/pages/chat/events/download_video_widget.dart b/lib/pages/chat/events/download_video_widget.dart index 10da0dbe94..2b8dfc0195 100644 --- a/lib/pages/chat/events/download_video_widget.dart +++ b/lib/pages/chat/events/download_video_widget.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat/events/download_video_state.dart'; import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; +import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_web.dart'; import 'package:fluffychat/presentation/mixins/handle_video_download_mixin.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; @@ -178,10 +179,16 @@ class _DownloadVideoWidgetState extends State ), ], ), - MediaViewerAppBar( - showAppbarPreviewNotifier: showAppbarPreview, - event: widget.event, - ), + if (PlatformInfos.isMobile) ...[ + MediaViewerAppBar( + showAppbarPreviewNotifier: showAppbarPreview, + event: widget.event, + ), + ] else ...[ + MediaViewerAppBarWeb( + event: widget.event, + ), + ], ], ), ), diff --git a/lib/pages/image_viewer/media_viewer_app_bar.dart b/lib/pages/image_viewer/media_viewer_app_bar.dart index 9285827f35..1a58e95f9c 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar.dart @@ -1,15 +1,9 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/pages/forward/forward.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_view.dart'; -import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; -import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; -import 'package:fluffychat/utils/extension/build_context_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; class MediaViewerAppBar extends StatefulWidget { const MediaViewerAppBar({ @@ -29,13 +23,10 @@ class MediaViewerAppBar extends StatefulWidget { State createState() => MediaViewerAppBarController(); } -class MediaViewerAppBarController extends State { - final MenuController menuController = MenuController(); - +class MediaViewerAppBarController extends State + with MediaViewerAppBarMixin { ValueNotifier? showAppbarPreview; - final responsiveUtils = getIt.get(); - @override void initState() { super.initState(); @@ -48,78 +39,6 @@ class MediaViewerAppBarController extends State { showAppbarPreview?.dispose(); } - void toggleShowMoreActions() { - if (menuController.isOpen) { - menuController.close(); - } else { - menuController.open(); - } - } - - /// Forward this image to another room. - void forwardAction() async { - Matrix.of(context).shareContent = widget.event?.content; - final result = await showDialog( - context: context, - useSafeArea: false, - useRootNavigator: false, - builder: (c) => const Forward(), - ); - if (result is PopResultFromForward) { - Navigator.of(context).pop(); - } - } - - void showInChat() { - if (!PlatformInfos.isMobile) { - handleShowInChatInWeb(); - } else { - handleShowInChatInMobile(); - } - } - - void handleShowInChatInWeb() { - backToChatScreenInWeb(); - scrollToEventInChat(); - return; - } - - void handleShowInChatInMobile() { - backToChatScreenInMobile(); - scrollToEventInChat(); - } - - void backToChatScreenInWeb() { - if (responsiveUtils.isTablet(context) || - responsiveUtils.isMobile(context)) { - Navigator.of(context) - .pop(MediaViewerPopupResultEnum.closeRightColumnFlag); - } else { - Navigator.of(context).pop(); - } - } - - void scrollToEventInChat() { - if (widget.event != null) { - context.goToRoomWithEvent(widget.event!.room.id, widget.event!.eventId); - } - } - - void backToChatScreenInMobile() { - Navigator.of(context).popUntil( - (Route route) => route.settings.name == '/rooms/room', - ); - } - - void onClose() { - Navigator.of(context).pop(); - } - - void saveFileAction() => widget.event?.saveFile(context); - - void shareFileAction(BuildContext context) => - widget.event?.shareFile(context); - @override Widget build(BuildContext context) { return MediaViewerAppbarView(this); diff --git a/lib/pages/image_viewer/media_viewer_app_bar_view.dart b/lib/pages/image_viewer/media_viewer_app_bar_view.dart index 5e39a8fdbe..a9ca319bc0 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_view.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_view.dart @@ -43,7 +43,9 @@ class MediaViewerAppbarView extends StatelessWidget { : Icons.close, color: LinagoraSysColors.material().onPrimary, ), - onPressed: controller.onClose, + onPressed: () => controller.onClose( + context, + ), color: LinagoraSysColors.material().onPrimary, tooltip: L10n.of(context)!.back, ), @@ -52,8 +54,10 @@ class MediaViewerAppbarView extends StatelessWidget { if (PlatformInfos.isMobile) Builder( builder: (context) => IconButton( - onPressed: () => - controller.shareFileAction(context), + onPressed: () => controller.shareFileAction( + context, + controller.widget.event, + ), tooltip: L10n.of(context)!.share, color: LinagoraSysColors.material().onPrimary, icon: Icon( @@ -68,7 +72,10 @@ class MediaViewerAppbarView extends StatelessWidget { Icons.shortcut, color: LinagoraSysColors.material().onPrimary, ), - onPressed: controller.forwardAction, + onPressed: () => controller.forwardAction( + context, + controller.widget.event, + ), color: LinagoraSysColors.material().onPrimary, tooltip: L10n.of(context)!.share, ), @@ -86,19 +93,28 @@ class MediaViewerAppbarView extends StatelessWidget { ContextMenuItemImageViewer( icon: Icons.file_download_outlined, title: L10n.of(context)!.saveFile, - onTap: controller.saveFileAction, + onTap: () => controller.saveFileAction( + context, + controller.widget.event, + ), ), ContextMenuItemImageViewer( title: L10n.of(context)!.showInChat, imagePath: ImagePaths.icShowInChat, - onTap: controller.showInChat, + onTap: () => controller.showInChat( + context, + controller.widget.event, + ), haveDivider: false, ), ], child: InkWell( borderRadius: MediaViewewAppbarStyle .showMoreIconSplashRadius, - onTap: controller.toggleShowMoreActions, + onTap: () => + controller.toggleShowMoreActions( + controller.menuController, + ), child: Padding( padding: MediaViewewAppbarStyle .marginAllShowMoreIcon, diff --git a/lib/pages/image_viewer/media_viewer_app_bar_web.dart b/lib/pages/image_viewer/media_viewer_app_bar_web.dart new file mode 100644 index 0000000000..f8db6c5be6 --- /dev/null +++ b/lib/pages/image_viewer/media_viewer_app_bar_web.dart @@ -0,0 +1,94 @@ +import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/pages/image_viewer/image_viewer_style.dart'; + +import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; +import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_style.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:matrix/matrix.dart'; + +class MediaViewerAppBarWeb extends StatelessWidget with MediaViewerAppBarMixin { + final Event? event; + + MediaViewerAppBarWeb({super.key, this.event}); + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + opacity: 1, + duration: MediaViewewAppbarStyle.opacityAnimationDuration, + curve: Curves.easeIn, + child: Container( + padding: ImageViewerStyle.paddingTopAppBar, + height: ImageViewerStyle.appBarHeight, + width: MediaQuery.sizeOf(context).width, + color: MediaViewewAppbarStyle.appBarBackgroundColor, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + MediaViewerAppBar.responsiveUtils.isMobile(context) + ? Icons.arrow_back_rounded + : Icons.close, + color: LinagoraSysColors.material().onPrimary, + ), + onPressed: () => onClose( + context, + ), + color: LinagoraSysColors.material().onPrimary, + tooltip: L10n.of(context)!.back, + ), + Row( + children: [ + if (event != null) + IconButton( + icon: Icon( + Icons.shortcut, + color: LinagoraSysColors.material().onPrimary, + ), + onPressed: () => forwardAction( + context, + event, + ), + color: LinagoraSysColors.material().onPrimary, + tooltip: L10n.of(context)!.share, + ), + if (event != null) ...[ + IconButton( + icon: Icon( + Icons.file_download_outlined, + color: LinagoraSysColors.material().onPrimary, + ), + tooltip: L10n.of(context)!.saveFile, + onPressed: () => saveFileAction( + context, + event, + ), + ), + IconButton( + tooltip: L10n.of(context)!.showInChat, + icon: SvgPicture.asset( + ImagePaths.icShowInChat, + colorFilter: ColorFilter.mode( + LinagoraSysColors.material().onPrimary, + BlendMode.srcIn, + ), + ), + onPressed: () => showInChat( + context, + event, + ), + ), + ], + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart new file mode 100644 index 0000000000..0d734a4e77 --- /dev/null +++ b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart @@ -0,0 +1,114 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/pages/forward/forward.dart'; +import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; +import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; +import 'package:fluffychat/utils/extension/build_context_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; + +mixin MediaViewerAppBarMixin { + final MenuController menuController = MenuController(); + + final responsiveUtils = getIt.get(); + + void toggleShowMoreActions(MenuController menuController) { + if (menuController.isOpen) { + menuController.close(); + } else { + menuController.open(); + } + } + + /// Forward this image to another room. + void forwardAction( + BuildContext context, + Event? event, + ) async { + Matrix.of(context).shareContent = event?.content; + final result = await showDialog( + context: context, + useSafeArea: false, + useRootNavigator: false, + builder: (c) => const Forward(), + ); + if (result is PopResultFromForward) { + Navigator.of(context).pop(); + } + } + + void showInChat( + BuildContext context, + Event? event, + ) { + if (!PlatformInfos.isMobile) { + handleShowInChatInWeb(context, event); + } else { + handleShowInChatInMobile(context, event); + } + } + + void handleShowInChatInWeb( + BuildContext context, + Event? event, + ) { + backToChatScreenInWeb(context, event); + scrollToEventInChat(context, event); + return; + } + + void handleShowInChatInMobile( + BuildContext context, + Event? event, + ) { + backToChatScreenInMobile(context); + scrollToEventInChat(context, event); + } + + void backToChatScreenInWeb( + BuildContext context, + Event? event, + ) { + if (responsiveUtils.isTablet(context) || + responsiveUtils.isMobile(context)) { + Navigator.of(context) + .pop(MediaViewerPopupResultEnum.closeRightColumnFlag); + } else { + Navigator.of(context).pop(); + } + } + + void scrollToEventInChat( + BuildContext context, + Event? event, + ) { + if (event != null) { + context.goToRoomWithEvent(event.room.id, event.eventId); + } + } + + void backToChatScreenInMobile(BuildContext context) { + Navigator.of(context).popUntil( + (Route route) => route.settings.name == '/rooms/room', + ); + } + + void onClose(BuildContext context) { + Navigator.of(context).pop(); + } + + void saveFileAction( + BuildContext context, + Event? event, + ) => + event?.saveFile(context); + + void shareFileAction( + BuildContext context, + Event? event, + ) => + event?.shareFile(context); +} diff --git a/lib/widgets/video_viewer_desktop_theme.dart b/lib/widgets/video_viewer_desktop_theme.dart index fad9432011..1f3d49230a 100644 --- a/lib/widgets/video_viewer_desktop_theme.dart +++ b/lib/widgets/video_viewer_desktop_theme.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; +import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_web.dart'; import 'package:fluffychat/widgets/video_player.dart'; import 'package:fluffychat/widgets/video_viewer_style.dart'; import 'package:flutter/material.dart'; @@ -26,9 +26,7 @@ class VideoViewerDesktopTheme extends StatelessWidget { seekBarThumbColor: Theme.of(context).colorScheme.primary, topButtonBarMargin: const EdgeInsets.all(0), topButtonBar: [ - MediaViewerAppBar( - event: event, - ), + MediaViewerAppBarWeb(event: event), ], ), fullscreen: MaterialDesktopVideoControlsThemeData( From fc95d6d815470c49df7537ca2d429825025ce323 Mon Sep 17 00:00:00 2001 From: hieubt Date: Fri, 22 Mar 2024 11:34:27 +0700 Subject: [PATCH 016/183] hot-fix: add unpin icon for chat app bar (cherry picked from commit 0dae81a406c88da3064861ee59c5f61ef0ec8917) --- lib/pages/chat/chat_view.dart | 14 ++++++++++++-- lib/pages/chat/chat_view_style.dart | 2 ++ .../twake_components/twake_icon_button.dart | 7 +++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 9b99e955a6..5da6aa216b 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pages/chat/chat_invitation_body.dart'; import 'package:fluffychat/pages/chat/chat_view_body.dart'; import 'package:fluffychat/pages/chat/chat_view_style.dart'; import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; +import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; @@ -42,13 +43,22 @@ class ChatView extends StatelessWidget with MessageContentMixin { // ), if (controller.selectedEvents.length == 1) TwakeIconButton( - icon: Icons.push_pin_outlined, - tooltip: L10n.of(context)!.pinChat, + icon: !controller.isUnpinEvent(controller.selectedEvents.first) + ? Icons.push_pin_outlined + : null, + imagePath: + controller.isUnpinEvent(controller.selectedEvents.first) + ? ImagePaths.icUnpin + : null, + tooltip: !controller.isUnpinEvent(controller.selectedEvents.first) + ? L10n.of(context)!.pinChat + : L10n.of(context)!.unpin, onTap: () => controller.actionWithClearSelections( () => controller.pinEventAction( controller.selectedEvents.single, ), ), + imageSize: ChatViewStyle.appBarIconSize, ), if (controller.selectedEvents.length == 1) PopupMenuButton<_EventContextAction>( diff --git a/lib/pages/chat/chat_view_style.dart b/lib/pages/chat/chat_view_style.dart index d3285d695f..c706eb832b 100644 --- a/lib/pages/chat/chat_view_style.dart +++ b/lib/pages/chat/chat_view_style.dart @@ -9,6 +9,8 @@ class ChatViewStyle { static const double pinnedMessageHintHeight = 48; + static const double appBarIconSize = 24.0; + static EdgeInsetsDirectional paddingLeading(BuildContext context) => EdgeInsetsDirectional.only( start: responsive.isMobile(context) ? 0 : 16, diff --git a/lib/widgets/twake_components/twake_icon_button.dart b/lib/widgets/twake_components/twake_icon_button.dart index 11db3edc55..cc2b4dacd2 100644 --- a/lib/widgets/twake_components/twake_icon_button.dart +++ b/lib/widgets/twake_components/twake_icon_button.dart @@ -103,6 +103,13 @@ class TwakeIconButton extends StatelessWidget { imagePath!, height: imageSize, width: imageSize, + colorFilter: ColorFilter.mode( + iconColor ?? + Theme.of(context) + .colorScheme + .onSurfaceVariant, + BlendMode.srcIn, + ), ) : null, ), From 515ef02609f255669bc130bb222c427d29ba3217 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 21 Mar 2024 15:08:09 +0700 Subject: [PATCH 017/183] TW-1544: Prevent create multiple direct chats when creating direct chat (cherry picked from commit 47faf4ee7792c2568ae8b75b9b3a44239187edba) --- lib/pages/chat/chat_input_row_send_btn.dart | 40 +++++++++--- lib/pages/chat_draft/draft_chat.dart | 5 ++ .../chat_draft/draft_chat_input_row.dart | 63 +++++++++++++------ lib/pages/chat_draft/draft_chat_view.dart | 16 ++++- 4 files changed, 95 insertions(+), 29 deletions(-) diff --git a/lib/pages/chat/chat_input_row_send_btn.dart b/lib/pages/chat/chat_input_row_send_btn.dart index 12f03a7cce..f27011aee8 100644 --- a/lib/pages/chat/chat_input_row_send_btn.dart +++ b/lib/pages/chat/chat_input_row_send_btn.dart @@ -8,12 +8,14 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class ChatInputRowSendBtn extends StatelessWidget { final ValueListenable inputText; + final ValueNotifier? sendingNotifier; final void Function() onTap; const ChatInputRowSendBtn({ Key? key, required this.inputText, required this.onTap, + this.sendingNotifier, }) : super(key: key); @override @@ -37,16 +39,34 @@ class ChatInputRowSendBtn extends StatelessWidget { return const SizedBox(); }, - child: Padding( - padding: ChatInputRowStyle.sendIconPadding, - child: TwakeIconButton( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - size: ChatInputRowStyle.sendIconBtnSize, - onTap: onTap, - tooltip: L10n.of(context)!.send, - imagePath: ImagePaths.icSend, - paddingAll: 0, + child: ValueListenableBuilder( + valueListenable: sendingNotifier ?? ValueNotifier(false), + builder: (context, isSending, child) { + if (isSending) { + return child!; + } + return Padding( + padding: ChatInputRowStyle.sendIconPadding, + child: TwakeIconButton( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + size: ChatInputRowStyle.sendIconBtnSize, + onTap: onTap, + tooltip: L10n.of(context)!.send, + imagePath: ImagePaths.icSend, + paddingAll: 0, + ), + ); + }, + child: const Padding( + padding: ChatInputRowStyle.sendIconPadding, + child: Center( + child: SizedBox( + width: ChatInputRowStyle.sendIconBtnSize, + height: ChatInputRowStyle.sendIconBtnSize, + child: CircularProgressIndicator.adaptive(), + ), + ), ), ), ); diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index cddb3208ad..ecf5bcbe8e 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -36,6 +36,11 @@ import 'package:scroll_to_index/scroll_to_index.dart'; typedef OnRoomCreatedSuccess = FutureOr Function(Room room)?; typedef OnRoomCreatedFailed = FutureOr Function()?; +typedef OnSendFileClick = void Function(BuildContext context); +typedef OnInputBarSubmitted = void Function(); +typedef OnEmojiAction = void Function(); +typedef OnKeyboardAction = void Function(); +typedef OnInputBarChanged = void Function(String text); class DraftChat extends StatefulWidget { final PresentationContact contact; diff --git a/lib/pages/chat_draft/draft_chat_input_row.dart b/lib/pages/chat_draft/draft_chat_input_row.dart index d4ada4ddff..b3175270ac 100644 --- a/lib/pages/chat_draft/draft_chat_input_row.dart +++ b/lib/pages/chat_draft/draft_chat_input_row.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat/chat_input_row_send_btn.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; import 'package:fluffychat/pages/chat/chat_input_row_web.dart'; import 'package:fluffychat/pages/chat/chat_view_body_style.dart'; +import 'package:fluffychat/pages/chat/input_bar/focus_suggestion_controller.dart'; import 'package:fluffychat/pages/chat/input_bar/input_bar.dart'; import 'package:fluffychat/pages/chat_draft/draft_chat.dart'; import 'package:fluffychat/pages/chat_draft/draft_chat_view_style.dart'; @@ -12,9 +13,34 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class DraftChatInputRow extends StatelessWidget { - final DraftChatController controller; + final OnSendFileClick onSendFileClick; + final ValueNotifier inputText; + final OnInputBarSubmitted onInputBarSubmitted; + final ValueNotifier isSendingNotifier; + final ValueNotifier emojiPickerNotifier; + final OnEmojiAction onEmojiAction; + final OnKeyboardAction onKeyboardAction; + final ValueKey typeAheadKey; + final OnInputBarChanged onInputBarChanged; + final FocusNode? typeAheadFocusNode; + final TextEditingController? textEditingController; + final FocusSuggestionController focusSuggestionController; - const DraftChatInputRow(this.controller, {Key? key}) : super(key: key); + const DraftChatInputRow({ + Key? key, + required this.onSendFileClick, + required this.inputText, + required this.onInputBarSubmitted, + required this.isSendingNotifier, + required this.emojiPickerNotifier, + required this.onEmojiAction, + required this.onKeyboardAction, + required this.typeAheadKey, + required this.onInputBarChanged, + this.typeAheadFocusNode, + this.textEditingController, + required this.focusSuggestionController, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -31,7 +57,7 @@ class DraftChatInputRow extends StatelessWidget { size: ChatInputRowStyle.chatInputRowMoreBtnSize, tooltip: L10n.of(context)!.more, icon: Icons.add_circle_outline, - onTap: () => controller.onSendFileClick(context), + onTap: () => onSendFileClick(context), ), ), Expanded( @@ -40,8 +66,9 @@ class DraftChatInputRow extends StatelessWidget { : _buildWebInputRow(context), ), ChatInputRowSendBtn( - inputText: controller.inputText, - onTap: controller.onInputBarSubmitted, + inputText: inputText, + onTap: onInputBarSubmitted, + sendingNotifier: isSendingNotifier, ), ], ), @@ -51,38 +78,38 @@ class DraftChatInputRow extends StatelessWidget { ChatInputRowMobile _buildMobileInputRow(BuildContext context) { return ChatInputRowMobile( inputBar: _buildInputBar(context), - emojiPickerNotifier: controller.showEmojiPickerNotifier, - onEmojiAction: controller.onEmojiAction, - onKeyboardAction: controller.onKeyboardAction, + emojiPickerNotifier: emojiPickerNotifier, + onEmojiAction: onEmojiAction, + onKeyboardAction: onKeyboardAction, ); } ChatInputRowWeb _buildWebInputRow(BuildContext context) { return ChatInputRowWeb( inputBar: _buildInputBar(context), - emojiPickerNotifier: controller.showEmojiPickerNotifier, - onTapMoreBtn: () => controller.onSendFileClick(context), - onEmojiAction: controller.onEmojiAction, - onKeyboardAction: controller.onKeyboardAction, + emojiPickerNotifier: emojiPickerNotifier, + onTapMoreBtn: () => onSendFileClick(context), + onEmojiAction: onEmojiAction, + onKeyboardAction: onKeyboardAction, ); } Widget _buildInputBar(BuildContext context) { return InputBar( - typeAheadKey: controller.draftChatComposerTypeAheadKey, + typeAheadKey: typeAheadKey, minLines: DraftChatViewStyle.minLinesInputBar, maxLines: DraftChatViewStyle.maxLinesInputBar, autofocus: !PlatformInfos.isMobile, keyboardType: TextInputType.multiline, textInputAction: null, - onSubmitted: (_) => controller.onInputBarSubmitted(), - typeAheadFocusNode: controller.inputFocus, - controller: controller.sendController, + onSubmitted: (_) => onInputBarSubmitted(), + typeAheadFocusNode: typeAheadFocusNode, + controller: textEditingController, decoration: DraftChatViewStyle.bottomBarInputDecoration( context, ), - onChanged: controller.onInputBarChanged, - focusSuggestionController: controller.focusSuggestionController, + onChanged: onInputBarChanged, + focusSuggestionController: focusSuggestionController, ); } } diff --git a/lib/pages/chat_draft/draft_chat_view.dart b/lib/pages/chat_draft/draft_chat_view.dart index e19c5dea9e..b16ab75f2e 100644 --- a/lib/pages/chat_draft/draft_chat_view.dart +++ b/lib/pages/chat_draft/draft_chat_view.dart @@ -93,7 +93,21 @@ class DraftChatView extends StatelessWidget { ), Column( children: [ - DraftChatInputRow(controller), + DraftChatInputRow( + onEmojiAction: controller.onEmojiAction, + onInputBarChanged: controller.onInputBarChanged, + onInputBarSubmitted: controller.onInputBarSubmitted, + onKeyboardAction: controller.onKeyboardAction, + onSendFileClick: controller.onSendFileClick, + textEditingController: controller.sendController, + typeAheadFocusNode: controller.inputFocus, + typeAheadKey: controller.draftChatComposerTypeAheadKey, + focusSuggestionController: + controller.focusSuggestionController, + inputText: controller.inputText, + isSendingNotifier: controller.isSendingNotifier, + emojiPickerNotifier: controller.showEmojiPickerNotifier, + ), const SizedBox( height: DraftChatViewStyle.bottomBarInputPadding, ), From 9c2485fbfadafd4753c31790f611c2d71d133361 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 12:13:54 +0700 Subject: [PATCH 018/183] TW-1586: create download_worker_queue extends worker queue abstract method (cherry picked from commit 27dcbe0c4c722d46ecea2143b23a272ff0b96d7e) --- .../{ => download_manager}/downloading_worker_queue.dart | 2 +- test/worker_queue_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/utils/manager/{ => download_manager}/downloading_worker_queue.dart (70%) diff --git a/lib/utils/manager/downloading_worker_queue.dart b/lib/utils/manager/download_manager/downloading_worker_queue.dart similarity index 70% rename from lib/utils/manager/downloading_worker_queue.dart rename to lib/utils/manager/download_manager/downloading_worker_queue.dart index 9ddd7a5a34..99922f6d97 100644 --- a/lib/utils/manager/downloading_worker_queue.dart +++ b/lib/utils/manager/download_manager/downloading_worker_queue.dart @@ -2,5 +2,5 @@ import 'package:fluffychat/utils/task_queue/worker_queue.dart'; class DownloadWorkerQueue extends WorkerQueue { @override - String get workerName => 'downloading_queue'; + String get workerName => 'download_worker_queue'; } diff --git a/test/worker_queue_test.dart b/test/worker_queue_test.dart index e80b2564af..a652a32a5f 100644 --- a/test/worker_queue_test.dart +++ b/test/worker_queue_test.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/utils/manager/downloading_worker_queue.dart'; +import 'package:fluffychat/utils/manager/download_manager/downloading_worker_queue.dart'; import 'package:fluffychat/utils/task_queue/task.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:matrix/matrix.dart'; From 739ab346a64339299f02c463c69dba816e650650 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:24:20 +0700 Subject: [PATCH 019/183] TW-1586: create extension method for downloading file and media separately (cherry picked from commit da4177657f3330b470fe7215f561486f82dbfa39) --- .../download_file_extension.dart | 178 +++++++++++++++++- 1 file changed, 177 insertions(+), 1 deletion(-) diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 7771c33a43..62352a65e4 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -1,9 +1,14 @@ +import 'dart:async'; import 'dart:io'; +import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/data/network/media/cancel_exception.dart'; import 'package:fluffychat/data/network/media/media_api.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/storage_directory_utils.dart'; import 'package:matrix/matrix.dart'; @@ -30,6 +35,138 @@ extension DownloadFileExtension on Event { } Future downloadOrRetrieveAttachment( + Uri mxcUrl, + String savePath, { + required StreamController> + downloadStreamController, + bool getThumbnail = false, + CancelToken? cancelToken, + }) async { + final database = room.client.database; + final attachment = await database?.getFileEntity(mxcUrl); + final downloadLink = mxcUrl.getDownloadLink(room.client); + + if (attachment != null) { + if (await attachment.length() == + getFileSize(getThumbnail: getThumbnail)) { + return FileInfo( + filename, + attachment.path, + getFileSize(getThumbnail: getThumbnail), + ); + } else { + await attachment.delete(); + } + } + try { + final mediaAPI = getIt(); + final downloadResponse = await mediaAPI.downloadFileInfo( + uriPath: downloadLink, + savePath: savePath, + onReceiveProgress: (receive, total) { + downloadStreamController.add( + Right( + DownloadingFileState( + receive: receive, + total: total, + ), + ), + ); + }, + cancelToken: cancelToken, + ); + if (downloadResponse.statusCode == 200) { + final fileInfo = FileInfo( + filename, + savePath, + content.tryGet('size') ?? await File(savePath).length(), + ); + await _handleDownloadFileDone( + this, + fileInfo, + downloadStreamController, + savePath, + ); + return fileInfo; + } + throw ('getFileInfo: Download file $filename failed'); + } catch (e) { + if (e is CancelRequestException) { + Logs().i("downloadOrRetrieveAttachment: user cancel the download"); + } + Logs().e("downloadOrRetrieveAttachment: $e"); + } + return null; + } + + Future _handleDownloadFileDone( + Event event, + FileInfo fileInfo, + StreamController> streamController, + String savePath, + ) async { + if (event.isAttachmentEncrypted) { + await _handleEncryptedFileEvent( + streamController, + event, + fileInfo, + savePath, + ); + } else { + streamController.add( + Right( + DownloadNativeFileSuccessState( + filePath: fileInfo.filePath, + ), + ), + ); + } + return; + } + + Future _handleEncryptedFileEvent( + StreamController> streamController, + Event event, + FileInfo fileInfo, + String savePath, + ) async { + streamController.add( + const Right( + DecryptingFileState(), + ), + ); + try { + final decryptedFile = await event.decryptFile( + fileInfo, + event.getAttachmentOrThumbnailMxcUrl()!, + '${savePath}decrypted', + ); + if (decryptedFile == null) { + throw Exception( + 'DownloadManager::download(): decryptedFile is null', + ); + } + final saveFile = File('${savePath}decrypted').copySync(savePath); + streamController.add( + Right( + DownloadNativeFileSuccessState( + filePath: saveFile.path, + ), + ), + ); + } catch (e) { + Logs().e( + 'DownloadManager::_handleEncryptedFileEvent(): $e', + ); + streamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); + } + } + + Future downloadOrRetrieveAttachmentForMedia( Uri mxcUrl, String savePath, { ProgressCallback? progressCallback, @@ -112,6 +249,41 @@ extension DownloadFileExtension on Event { } Future getFileInfo({ + getThumbnail = false, + required StreamController> + downloadStreamController, + CancelToken? cancelToken, + }) async { + if (!canContainAttachment()) { + throw ("getFileInfo: This event has the type '$type' and so it can't contain an attachment."); + } + + if (isSending()) { + final localFile = room.sendingFilePlaceholders[eventId]; + if (localFile != null) return FileInfo.fromMatrixFile(localFile); + } + + final mxcUrl = getAttachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail); + if (mxcUrl == null) { + throw "getFileInfo: This event hasn't any attachment or thumbnail."; + } + + final isFileEncrypted = + getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted; + if (isEncryptionDisabled(isFileEncrypted)) { + throw ('getFileInfo: Encryption is not enabled in your Client.'); + } + + return downloadOrRetrieveAttachment( + mxcUrl, + await getFileNameInAppDownload(), + downloadStreamController: downloadStreamController, + getThumbnail: getThumbnail, + cancelToken: cancelToken, + ); + } + + Future getMediaFileInfo({ getThumbnail = false, ProgressCallback? progressCallback, CancelToken? cancelToken, @@ -158,7 +330,7 @@ extension DownloadFileExtension on Event { } } - final fileInfo = await downloadOrRetrieveAttachment( + final fileInfo = await downloadOrRetrieveAttachmentForMedia( mxcUrl, '$tempDirectory/${Uri.encodeComponent(mxcUrl.toString())}', progressCallback: progressCallback, @@ -177,4 +349,8 @@ extension DownloadFileExtension on Event { return fileInfo; } + + Future getFileNameInAppDownload() async { + return '${await StorageDirectoryUtils.instance.getDownloadFolderInApp()}/$eventId/$filename'; + } } From 64ebe41eeb2d1da8e66305fc3a61064350951aec Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:24:59 +0700 Subject: [PATCH 020/183] TW-1586: create download manager class to handle multiple downloading in web (cherry picked from commit 9998273e7fc118a4a2c283996890c4c35437221e) --- .../download_manager/download_manager.dart | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 lib/utils/manager/download_manager/download_manager.dart diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart new file mode 100644 index 0000000000..03cad42c97 --- /dev/null +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -0,0 +1,199 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart' hide Task; +import 'package:dio/dio.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_info.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/download_manager/downloading_worker_queue.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/task_queue/task.dart'; +import 'package:matrix/matrix.dart'; + +typedef FutureVoidCallback = Future Function(); + +class DownloadManager { + DownloadManager._(); + + static final DownloadManager _instance = DownloadManager._(); + + factory DownloadManager() => _instance; + + final workingQueue = getIt.get(); + + final Map _eventIdMapDownloadFileInfo = {}; + + void cancelDownload(String eventId) { + final cancelToken = _eventIdMapDownloadFileInfo[eventId]?.cancelToken; + if (cancelToken != null) { + try { + cancelToken.cancel(); + _eventIdMapDownloadFileInfo[eventId]?.downloadStateStreamController.add( + const Right( + DownloadFileInitial(), + ), + ); + } catch (e) { + Logs().e( + 'DownloadManager::cancelDownload(): $e', + ); + _eventIdMapDownloadFileInfo[eventId]?.downloadStateStreamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); + } finally { + clear(eventId); + } + } + } + + void _initDownloadFileInfo( + Event event, + ) { + final streamController = StreamController>(); + + _eventIdMapDownloadFileInfo[event.eventId] = DownloadFileInfo( + eventId: event.eventId, + cancelToken: CancelToken(), + downloadStateStreamController: streamController, + downloadStream: streamController.stream.asBroadcastStream(), + ); + } + + Stream>? getDownloadStateStream(String eventId) { + return _eventIdMapDownloadFileInfo[eventId]?.downloadStream; + } + + Future clear(String eventId) async { + try { + await _eventIdMapDownloadFileInfo[eventId] + ?.downloadStateStreamController + .close(); + } catch (e) { + Logs().e( + 'DownloadManager::_clear(): $e', + ); + _eventIdMapDownloadFileInfo[eventId]?.downloadStateStreamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); + } finally { + _eventIdMapDownloadFileInfo.remove(eventId); + Logs().i( + 'DownloadManager::clear with $eventId successfully', + ); + } + } + + Future download({ + required Event event, + bool getThumbnail = false, + }) async { + _initDownloadFileInfo(event); + final streamController = _eventIdMapDownloadFileInfo[event.eventId] + ?.downloadStateStreamController; + final cancelToken = _eventIdMapDownloadFileInfo[event.eventId]?.cancelToken; + if (streamController == null || cancelToken == null) { + Logs().e( + 'DownloadManager::download(): streamController or cancelToken is null', + ); + _eventIdMapDownloadFileInfo[event.eventId] + ?.downloadStateStreamController + .add( + Left( + DownloadFileFailureState( + exception: Exception( + 'streamController or cancelToken is null', + ), + ), + ), + ); + return; + } + streamController.add( + const Right( + DownloadFileInitial(), + ), + ); + _addTaskToWorkerQueue( + event: event, + getThumbnail: getThumbnail, + streamController: streamController, + cancelToken: cancelToken, + ); + } + + void _addTaskToWorkerQueue({ + required Event event, + bool getThumbnail = false, + required StreamController> streamController, + required CancelToken cancelToken, + }) { + if (!PlatformInfos.isWeb) { + _addTaskToWorkerQueueNative( + event, + getThumbnail, + streamController, + cancelToken, + ); + return; + } + + _addTaskToWorkerQueueWeb(event, streamController); + } + + void _addTaskToWorkerQueueNative( + Event event, + bool getThumbnail, + StreamController> streamController, + CancelToken cancelToken, + ) { + workingQueue.addTask( + Task( + id: event.eventId, + runnable: () async { + try { + await event.getFileInfo( + getThumbnail: getThumbnail, + downloadStreamController: streamController, + cancelToken: cancelToken, + ); + } catch (e) { + Logs().e('DownloadManager::download(): $e'); + streamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); + } + }, + onTaskCompleted: () => clear(event.eventId), + ), + ); + } + + void _addTaskToWorkerQueueWeb( + Event event, + StreamController> streamController, + ) { + workingQueue.addTask( + Task( + id: event.eventId, + runnable: () async { + final matrixFile = await event.downloadAndDecryptAttachment(); + streamController.add( + Right( + DownloadMatrixFileSuccessState(matrixFile: matrixFile), + ), + ); + }, + onTaskCompleted: () => clear(event.eventId), + ), + ); + } +} From cea8e2b343e3065177940a19217cb92dbaa39c4a Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:26:18 +0700 Subject: [PATCH 021/183] TW-1586: change name of method getFileInfo to getMediaFileInfo (cherry picked from commit a511b595fbba637a1cd7f568f55164f97fe007df) --- lib/pages/image_viewer/image_viewer_view.dart | 2 +- lib/presentation/mixins/handle_video_download_mixin.dart | 2 +- lib/widgets/mxc_image.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index a43373d498..28e06aa4fa 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -110,7 +110,7 @@ class _ImageWidget extends StatelessWidget { ); } else { return FutureBuilder( - future: event.getFileInfo( + future: event.getMediaFileInfo( getThumbnail: false, ), builder: (context, snapshot) { diff --git a/lib/presentation/mixins/handle_video_download_mixin.dart b/lib/presentation/mixins/handle_video_download_mixin.dart index b131de7ec9..bf329b0838 100644 --- a/lib/presentation/mixins/handle_video_download_mixin.dart +++ b/lib/presentation/mixins/handle_video_download_mixin.dart @@ -32,7 +32,7 @@ mixin HandleVideoDownloadMixin { } return url; } else { - final videoFile = await event.getFileInfo( + final videoFile = await event.getMediaFileInfo( progressCallback: progressCallback, cancelToken: cancelToken, ); diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 4330b43870..477e59e509 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -167,7 +167,7 @@ class _MxcImageState extends State if (event != null) { if (!PlatformInfos.isWeb) { - final fileInfo = await event.getFileInfo( + final fileInfo = await event.getMediaFileInfo( getThumbnail: widget.isThumbnail, ); if (fileInfo != null && fileInfo.filePath.isNotEmpty) { From ea424e0ac7059c2d5947b7a42f2d3676dafb728c Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:27:03 +0700 Subject: [PATCH 022/183] TW-1586: register DownloadWorkerQueue and DownloadManager inside getIt (cherry picked from commit 481ba86df573a34add64a8950c076a7f8f1dae5b) --- lib/di/global/get_it_initializer.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index e557b5197d..fb8c4c6c49 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -81,6 +81,8 @@ import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dar import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/pages/chat/chat_pinned_events/pinned_events_controller.dart'; import 'package:fluffychat/utils/famedlysdk_store.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/manager/download_manager/downloading_worker_queue.dart'; import 'package:fluffychat/utils/power_level_manager.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:get_it/get_it.dart'; @@ -100,6 +102,7 @@ class GetItInitializer { bindingGlobal(); bindingQueue(); bindingAPI(); + bindingManager(); bindingDatasource(); bindingDatasourceImpl(); bindingRepositories(); @@ -137,6 +140,15 @@ class GetItInitializer { getIt.registerSingleton(ServerConfigAPI()); } + void bindingManager() { + getIt.registerSingleton( + DownloadWorkerQueue(), + ); + getIt.registerSingleton( + DownloadManager(), + ); + } + void bindingDatasource() { getIt.registerFactory( () => HiveToMConfigurationDatasource(), From 523bbf0b512b578481fbaf6b3bbd94dbb5632c9f Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:27:56 +0700 Subject: [PATCH 023/183] TW-1586: create DownloadFileInfo and DownloadFileState model for download manager (cherry picked from commit 8712a977bf554621fe2ea30d4688972d0f955a27) --- .../download_manager/download_file_info.dart | 29 ++++++++ .../download_manager/download_file_state.dart | 71 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 lib/utils/manager/download_manager/download_file_info.dart create mode 100644 lib/utils/manager/download_manager/download_file_state.dart diff --git a/lib/utils/manager/download_manager/download_file_info.dart b/lib/utils/manager/download_manager/download_file_info.dart new file mode 100644 index 0000000000..f55a68d017 --- /dev/null +++ b/lib/utils/manager/download_manager/download_file_info.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; + +class DownloadFileInfo with EquatableMixin { + final String eventId; + + final CancelToken cancelToken; + + final StreamController> + downloadStateStreamController; + + final Stream> downloadStream; + + DownloadFileInfo({ + required this.eventId, + required this.cancelToken, + required this.downloadStateStreamController, + required this.downloadStream, + }); + + @override + List get props => + [eventId, cancelToken, downloadStateStreamController, downloadStream]; +} diff --git a/lib/utils/manager/download_manager/download_file_state.dart b/lib/utils/manager/download_manager/download_file_state.dart new file mode 100644 index 0000000000..6243c44c16 --- /dev/null +++ b/lib/utils/manager/download_manager/download_file_state.dart @@ -0,0 +1,71 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:matrix/matrix.dart'; + +class DownloadFileState extends Success { + const DownloadFileState(); + + @override + List get props => []; +} + +class DownloadFileInitial extends Success { + const DownloadFileInitial(); + + @override + List get props => []; +} + +class DownloadingFileState extends DownloadFileState { + final int receive; + + final int total; + + const DownloadingFileState({ + required this.receive, + required this.total, + }); + + @override + List get props => [receive, total]; +} + +class DownloadNativeFileSuccessState extends DownloadFileState { + const DownloadNativeFileSuccessState({ + required this.filePath, + }); + + final String filePath; + + @override + List get props => [filePath]; +} + +class DownloadMatrixFileSuccessState extends DownloadFileState { + const DownloadMatrixFileSuccessState({ + required this.matrixFile, + }); + + final MatrixFile matrixFile; + + @override + List get props => [matrixFile]; +} + +class DecryptingFileState extends DownloadFileState { + const DecryptingFileState(); + + @override + List get props => []; +} + +class DownloadFileFailureState extends Failure { + final dynamic exception; + + const DownloadFileFailureState({ + required this.exception, + }); + + @override + List get props => [exception]; +} From 083f6b313f924ec52c8177e6ba784ab3ba0eef76 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:29:07 +0700 Subject: [PATCH 024/183] TW-1586: create download in app folder (cherry picked from commit 080d6616e0db0597a17eaae2f8c7f730c58a6b1a) --- lib/utils/storage_directory_utils.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/utils/storage_directory_utils.dart b/lib/utils/storage_directory_utils.dart index 4180a961f3..5de7d402de 100644 --- a/lib/utils/storage_directory_utils.dart +++ b/lib/utils/storage_directory_utils.dart @@ -7,6 +7,8 @@ class StorageDirectoryUtils { static StorageDirectoryUtils get instance => _instance; + static String? _tempDirectoryPath; + Future getFileStoreDirectory() async { try { try { @@ -18,4 +20,9 @@ class StorageDirectoryUtils { return (await getDownloadsDirectory())!.path; } } + + Future getDownloadFolderInApp() async { + _tempDirectoryPath ??= (await getTemporaryDirectory()).path; + return '$_tempDirectoryPath/Downloads'; + } } From e94b649bf18e26144c67fdc0e9bcab11564aa98b Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:30:32 +0700 Subject: [PATCH 025/183] TW-1586: change openFileMethod to have less param (cherry picked from commit 9b9c944f4ef9fda43276cad7f829bca99764f209) --- ...andle_download_and_preview_file_mixin.dart | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index 20c74c4268..aed2c948c5 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -2,7 +2,6 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_failure.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_loading.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_success.dart'; -import 'package:fluffychat/domain/model/download_file/download_file_for_preview_response.dart'; import 'package:fluffychat/domain/model/preview_file/document_uti.dart'; import 'package:fluffychat/domain/model/preview_file/supported_preview_file_types.dart'; import 'package:fluffychat/domain/usecase/download_file_for_preview_interactor.dart'; @@ -40,7 +39,7 @@ mixin HandleDownloadAndPreviewFileMixin { required Event event, required BuildContext context, }) { - return _handlePreviewWeb(event: event, context: context); + return handlePreviewWeb(event: event, context: context); } void onFileTappedMobile({ @@ -107,7 +106,7 @@ mixin HandleDownloadAndPreviewFileMixin { } } - void _handlePreviewWeb({ + void handlePreviewWeb({ required Event event, required BuildContext context, }) async { @@ -143,9 +142,9 @@ mixin HandleDownloadAndPreviewFileMixin { TwakeDialog.hideLoadingDialog(context); }, (success) { if (success is DownloadFileForPreviewSuccess) { - _openDownloadedFileForPreview( - downloadFileForPreviewResponse: - success.downloadFileForPreviewResponse, + openDownloadedFileForPreview( + filePath: success.downloadFileForPreviewResponse.filePath, + mimeType: success.downloadFileForPreviewResponse.mimeType, ); TwakeDialog.hideLoadingDialog(context); } else if (success is DownloadFileForPreviewLoading) { @@ -155,17 +154,17 @@ mixin HandleDownloadAndPreviewFileMixin { }); } - void _openDownloadedFileForPreview({ - required DownloadFileForPreviewResponse downloadFileForPreviewResponse, + void openDownloadedFileForPreview({ + required String filePath, + required String? mimeType, }) async { - final mimeType = downloadFileForPreviewResponse.mimeType; if (PlatformInfos.isAndroid && SupportedPreviewFileTypes.apkMimeTypes.contains(mimeType)) { - await Share.shareXFiles([XFile(downloadFileForPreviewResponse.filePath)]); + await Share.shareXFiles([XFile(filePath)]); return; } final openResults = await OpenFile.open( - downloadFileForPreviewResponse.filePath, + filePath, type: mimeType, uti: DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]) .value, @@ -175,7 +174,7 @@ mixin HandleDownloadAndPreviewFileMixin { ); if (openResults.type != ResultType.done) { - await Share.shareXFiles([XFile(downloadFileForPreviewResponse.filePath)]); + await Share.shareXFiles([XFile(filePath)]); return; } } From b4f0d80a43a2d7f5dd3e0b5e00ebbcb1f913685e Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 13:31:28 +0700 Subject: [PATCH 026/183] TW-1586: update the view of downloading file: process, cancel and multiple downloading (cherry picked from commit cd95d8aef9258b24779a8299e83dc8fbfe3dc6ea) --- lib/pages/chat/events/message_content.dart | 12 -- .../chat/events/message_download_content.dart | 192 +++++++++++++++--- .../downloading_state_presentation_model.dart | 46 +++++ .../matrix_sdk_extensions/int_extension.dart | 12 +- .../download_file_tile_widget.dart | 166 +++++++++++++++ lib/widgets/file_widget/file_tile_widget.dart | 55 +++-- .../file_widget/message_file_tile_style.dart | 13 +- 7 files changed, 444 insertions(+), 52 deletions(-) create mode 100644 lib/presentation/model/chat/downloading_state_presentation_model.dart create mode 100644 lib/widgets/file_widget/download_file_tile_widget.dart diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index dac8af6376..10448d24de 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -85,10 +85,6 @@ class MessageContent extends StatelessWidget } return MessageDownloadContent( event, - onFileTapped: (event) => onFileTapped( - context: context, - event: event, - ), ); case MessageTypes.Video: if (event.isVideoAvailable) { @@ -105,10 +101,6 @@ class MessageContent extends StatelessWidget children: [ MessageDownloadContent( event, - onFileTapped: (event) => onFileTapped( - context: context, - event: event, - ), ), Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, @@ -124,10 +116,6 @@ class MessageContent extends StatelessWidget children: [ MessageDownloadContent( event, - onFileTapped: (event) => onFileTapped( - context: context, - event: event, - ), ), Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index c78349c80c..8208addb42 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,45 +1,191 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -class MessageDownloadContent extends StatelessWidget { +class MessageDownloadContent extends StatefulWidget { final Event event; - final void Function(Event event)? onFileTapped; final String? highlightText; const MessageDownloadContent( this.event, { Key? key, - this.onFileTapped, this.highlightText, }) : super(key: key); @override - Widget build(BuildContext context) { - final filename = event.filename; - final filetype = event.fileType; - final sizeString = event.sizeString; + State createState() => _MessageDownloadContentState(); +} + +class _MessageDownloadContentState extends State + with HandleDownloadAndPreviewFileMixin { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + StreamSubscription>? streamSubscription; + + @override + void initState() { + super.initState(); + checkDownloadFileState(); + } + + void checkDownloadFileState() async { + if (!PlatformInfos.isWeb) { + final filePath = await widget.event.getFileNameInAppDownload(); + final file = File(filePath); + if (await file.exists() && + await file.length() == widget.event.getFileSize()) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePath, + ); + return; + } + } + setupListeningForStreamSubcription(); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } - Logs().i( - 'filename: $filename, filetype: $filetype, sizeString: $sizeString, content: ${event.content}', + void setupListeningForStreamSubcription() { + streamSubscription = downloadManager + .getDownloadStateStream(widget.event.eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess(Either event) { + event.fold( + (failure) { + Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadNativeFileSuccessState) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: success.filePath, + ); + } else if (success is DownloadMatrixFileSuccessState) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + } + }, ); - return InkWell( - onTap: onFileTapped != null - ? () { - onFileTapped?.call(event); - } - : null, - child: FileTileWidget( - mimeType: event.mimeType, - fileType: filetype, - filename: filename, - highlightText: highlightText, - sizeString: sizeString, - style: MessageFileTileStyle(), - ), + } + + @override + void dispose() { + streamSubscription?.cancel(); + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final filename = widget.event.filename; + final filetype = widget.event.fileType; + final sizeString = widget.event.sizeString; + return ValueListenableBuilder( + valueListenable: downloadFileStateNotifier, + builder: (context, DownloadPresentationState state, child) { + if (state is DownloadedPresentationState) { + return InkWell( + onTap: () async { + openDownloadedFileForPreview( + filePath: state.filePath, + mimeType: widget.event.mimeType, + ); + }, + child: FileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + ), + ); + } else if (state is DownloadingPresentationState) { + return DownloadFileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + downloadFileStateNotifier: downloadFileStateNotifier, + onCancelDownload: () { + downloadFileStateNotifier.value = + const NotDownloadPresentationState(); + downloadManager.cancelDownload(widget.event.eventId); + }, + ); + } else if (state is FileWebDownloadedPresentationState) { + return InkWell( + onTap: () { + handlePreviewWeb( + event: widget.event, + context: context, + ); + }, + child: FileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + ), + ); + } + + return InkWell( + onTap: () { + downloadFileStateNotifier.value = + const DownloadingPresentationState(); + downloadManager.download( + event: widget.event, + ); + setupListeningForStreamSubcription(); + }, + child: DownloadFileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + downloadFileStateNotifier: downloadFileStateNotifier, + style: const MessageFileTileStyle(), + ), + ); + }, ); } } diff --git a/lib/presentation/model/chat/downloading_state_presentation_model.dart b/lib/presentation/model/chat/downloading_state_presentation_model.dart new file mode 100644 index 0000000000..4086a76175 --- /dev/null +++ b/lib/presentation/model/chat/downloading_state_presentation_model.dart @@ -0,0 +1,46 @@ +import 'package:equatable/equatable.dart'; +import 'package:matrix/matrix.dart'; + +abstract class DownloadPresentationState with EquatableMixin { + const DownloadPresentationState(); + + @override + List get props => []; +} + +class NotDownloadPresentationState extends DownloadPresentationState { + const NotDownloadPresentationState() : super(); +} + +class DownloadedPresentationState extends DownloadPresentationState { + final String filePath; + + const DownloadedPresentationState({required this.filePath}) : super(); + + @override + List get props => [filePath]; +} + +class FileWebDownloadedPresentationState extends DownloadPresentationState { + final MatrixFile matrixFile; + + const FileWebDownloadedPresentationState({required this.matrixFile}) + : super(); + + @override + List get props => [matrixFile]; +} + +class DownloadingPresentationState extends DownloadPresentationState { + final int? receive; + + final int? total; + + const DownloadingPresentationState({ + this.receive, + this.total, + }); + + @override + List get props => [receive, total]; +} diff --git a/lib/utils/matrix_sdk_extensions/int_extension.dart b/lib/utils/matrix_sdk_extensions/int_extension.dart index c485c1b055..e6189cad72 100644 --- a/lib/utils/matrix_sdk_extensions/int_extension.dart +++ b/lib/utils/matrix_sdk_extensions/int_extension.dart @@ -1,5 +1,11 @@ -extension DoubleExtension on int { - String bytesToMB() { - return (this / (1024 * 1024)).toString(); +extension IntExtension on int { + String bytesToMB({int? placeDecimal}) { + return (this / (1024 * 1024)).toStringAsFixed(placeDecimal ?? 0); } + + String bytesToKB({int? placeDecimal}) { + return (this / 1024).toStringAsFixed(placeDecimal ?? 0); + } + + static const oneKB = 1024 * 1024; } diff --git a/lib/widgets/file_widget/download_file_tile_widget.dart b/lib/widgets/file_widget/download_file_tile_widget.dart new file mode 100644 index 0000000000..f57748e62a --- /dev/null +++ b/lib/widgets/file_widget/download_file_tile_widget.dart @@ -0,0 +1,166 @@ +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:flutter/material.dart'; + +class DownloadFileTileWidget extends StatefulWidget { + const DownloadFileTileWidget({ + super.key, + this.style = const MessageFileTileStyle(), + required this.mimeType, + required this.filename, + this.highlightText, + this.fileType, + this.sizeString, + required this.downloadFileStateNotifier, + this.onCancelDownload, + }); + + final TwakeMimeType mimeType; + final String filename; + final MessageFileTileStyle style; + final String? highlightText; + final String? sizeString; + final String? fileType; + final ValueNotifier downloadFileStateNotifier; + final VoidCallback? onCancelDownload; + + @override + State createState() => _DownloadFileTileWidgetState(); +} + +class _DownloadFileTileWidgetState extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { + AnimationController? _controller; + Animation? _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + + _animation = CurvedAnimation( + parent: _controller!, + curve: Curves.linear, + ); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Container( + padding: widget.style.paddingFileTileAll, + decoration: ShapeDecoration( + color: widget.style.backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: widget.style.borderRadius, + ), + ), + child: Row( + crossAxisAlignment: widget.style.crossAxisAlignment, + children: [ + ValueListenableBuilder( + valueListenable: widget.downloadFileStateNotifier, + builder: (context, downloadFileState, child) { + double? downloadProgress; + if (downloadFileState is DownloadingPresentationState) { + if (downloadFileState.total == null || + downloadFileState.receive == null) { + downloadProgress = null; + } else { + downloadProgress = + downloadFileState.receive! / downloadFileState.total!; + } + } else if (downloadFileState is NotDownloadPresentationState) { + downloadProgress = 0; + } + if (downloadProgress == 0) { + return Padding( + padding: widget.style.paddingDownloadFileIcon, + child: Icon( + Icons.file_download_rounded, + size: widget.style.iconSize, + ), + ); + } + return Stack( + alignment: Alignment.center, + children: [ + RotationTransition( + turns: _animation!, + child: CircularProgressIndicator( + strokeWidth: widget.style.strokeWidthLoading, + value: downloadProgress, + ), + ), + IconButton( + onPressed: + PlatformInfos.isWeb ? null : widget.onCancelDownload, + icon: Icon( + Icons.close, + size: widget.style.cancelButtonSize, + ), + ), + ], + ); + }, + ), + widget.style.paddingRightIcon, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 4.0), + FileNameText( + filename: widget.filename, + highlightText: widget.highlightText, + style: widget.style, + ), + Row( + children: [ + if (widget.sizeString != null) + TextInformationOfFile( + value: widget.sizeString!, + style: widget.style, + downloadFileStateNotifier: + widget.downloadFileStateNotifier, + ), + TextInformationOfFile( + value: " · ", + style: widget.style, + ), + Flexible( + child: TextInformationOfFile( + value: widget.mimeType.getFileType( + context, + fileType: widget.fileType, + ), + style: widget.style, + ), + ), + ], + ), + widget.style.paddingBottomText, + ], + ), + ), + ], + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/file_widget/file_tile_widget.dart b/lib/widgets/file_widget/file_tile_widget.dart index 5203f63e04..e086beffc7 100644 --- a/lib/widgets/file_widget/file_tile_widget.dart +++ b/lib/widgets/file_widget/file_tile_widget.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/int_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; import 'package:flutter/material.dart'; @@ -69,7 +71,7 @@ class FileTileWidget extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ const SizedBox(height: 4.0), - _FileNameText( + FileNameText( filename: filename, highlightText: highlightText, style: style, @@ -77,16 +79,16 @@ class FileTileWidget extends StatelessWidget { Row( children: [ if (sizeString != null) - _TextInformationOfFile( + TextInformationOfFile( value: sizeString!, style: style, ), - _TextInformationOfFile( + TextInformationOfFile( value: " · ", style: style, ), Flexible( - child: _TextInformationOfFile( + child: TextInformationOfFile( value: mimeType.getFileType( context, fileType: fileType, @@ -106,8 +108,9 @@ class FileTileWidget extends StatelessWidget { } } -class _FileNameText extends StatelessWidget { - const _FileNameText({ +class FileNameText extends StatelessWidget { + const FileNameText({ + super.key, required this.filename, this.highlightText, this.style = const FileTileWidgetStyle(), @@ -133,21 +136,47 @@ class _FileNameText extends StatelessWidget { } } -class _TextInformationOfFile extends StatelessWidget { +class TextInformationOfFile extends StatelessWidget { final String value; final FileTileWidgetStyle style; - const _TextInformationOfFile({ + final ValueNotifier? downloadFileStateNotifier; + + const TextInformationOfFile({ + super.key, required this.value, + this.downloadFileStateNotifier, this.style = const FileTileWidgetStyle(), }); @override Widget build(BuildContext context) { - return Text( - value, - style: style.textInformationStyle(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, + return Row( + children: [ + if (downloadFileStateNotifier != null) + ValueListenableBuilder( + valueListenable: downloadFileStateNotifier!, + builder: ((context, downloadFileState, child) { + if (downloadFileState is DownloadingPresentationState && + downloadFileState.total != null && + downloadFileState.receive != null && + downloadFileState.total! >= IntExtension.oneKB) { + return Text( + '${downloadFileState.receive!.bytesToMB(placeDecimal: 1)} MB / ', + style: style.textInformationStyle(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + return const SizedBox.shrink(); + }), + ), + Text( + value, + style: style.textInformationStyle(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ); } } diff --git a/lib/widgets/file_widget/message_file_tile_style.dart b/lib/widgets/file_widget/message_file_tile_style.dart index 6811a50d7c..1f747b2587 100644 --- a/lib/widgets/file_widget/message_file_tile_style.dart +++ b/lib/widgets/file_widget/message_file_tile_style.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'file_tile_widget_style.dart'; class MessageFileTileStyle extends FileTileWidgetStyle { + const MessageFileTileStyle(); + @override double get iconSize => 36; @@ -24,8 +26,17 @@ class MessageFileTileStyle extends FileTileWidgetStyle { } @override - Widget get paddingBottomText => const SizedBox(height: 4.0); + Widget get paddingBottomText => const SizedBox(height: 8.0); @override Widget get paddingRightIcon => const SizedBox(width: 4.0); + + EdgeInsets get paddingDownloadFileIcon => const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 4.0, + ); + + double get strokeWidthLoading => 2; + + double get cancelButtonSize => 32; } From d0d1ff05f419779e88891e4c2f9f61b0aa189acb Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 25 Dec 2023 13:14:06 +0700 Subject: [PATCH 027/183] Integrate login on mobile (cherry picked from commit 0372daf91446ecb3ba3b5436fabb40a8f3e55341) --- config.sample.json | 2 + lib/config/app_config.dart | 17 +++++ .../receive_sharing_intent_mixin.dart | 2 + lib/pages/twake_id/twake_id.dart | 68 +++++++++++++++++++ lib/pages/twake_id/twake_id_view.dart | 36 ++++++++++ 5 files changed, 125 insertions(+) create mode 100644 lib/pages/twake_id/twake_id.dart create mode 100644 lib/pages/twake_id/twake_id_view.dart diff --git a/config.sample.json b/config.sample.json index 2bdd48b43b..c10e8633db 100644 --- a/config.sample.json +++ b/config.sample.json @@ -7,5 +7,7 @@ "hide_redacted_events": false, "hide_unknown_events": false, "issue_id": "", + "registration_url": "https://example.com/", + "twake_workplace_homeserver": "https://example.com/", "app_grid_dashboard_available": true } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index bb0d6816a9..b238f95dfd 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -21,6 +21,10 @@ abstract class AppConfig { static double bubbleSizeFactor = 1; static double fontSizeFactor = 1; + static String registrationUrl = 'https://example.com/'; + + static String twakeWorkplaceHomeserver = 'https://example.com/'; + static double toolbarHeight(BuildContext context) => responsive.isMobile(context) ? 48 : 56; static const Color chatColor = primaryColor; @@ -95,6 +99,19 @@ abstract class AppConfig { "configurations/app_dashboard.json"; static void loadFromJson(Map json) { + if (json['register_site'] != null && json['registration_url'] is String) { + if (json['registration_url'] != '') { + registrationUrl = json['registration_url']; + } + } + + if (json['twake_workplace_homeserver'] != null && + json['twake_workplace_homeserver'] is String) { + if (json['twake_workplace_homeserver'] != '') { + twakeWorkplaceHomeserver = json['twake_workplace_homeserver']; + } + } + if (json['chat_color'] != null) { try { colorSchemeSeed = Color(json['chat_color']); diff --git a/lib/pages/chat_list/receive_sharing_intent_mixin.dart b/lib/pages/chat_list/receive_sharing_intent_mixin.dart index 2c16760ca7..a5b78f84c5 100644 --- a/lib/pages/chat_list/receive_sharing_intent_mixin.dart +++ b/lib/pages/chat_list/receive_sharing_intent_mixin.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:fluffychat/config/app_config.dart'; +import 'package:matrix/matrix.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:uni_links/uni_links.dart'; @@ -49,6 +50,7 @@ mixin ReceiveSharingIntentMixin on State { } void _processIncomingUris(String? text) async { + Logs().d("ReceiveSharingIntentMixin: _processIncomingUris: $text"); if (text == null) return; TwakeApp.router.go('/share'); WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/pages/twake_id/twake_id.dart b/lib/pages/twake_id/twake_id.dart new file mode 100644 index 0000000000..1ccc7cf999 --- /dev/null +++ b/lib/pages/twake_id/twake_id.dart @@ -0,0 +1,68 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/twake_id/twake_id_view.dart'; +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +class TwakeId extends StatefulWidget { + const TwakeId({super.key}); + + @override + State createState() => TwakeIdController(); +} + +class TwakeIdController extends State { + void goToHomeserverPicker() { + context.push('/home/homeserverpicker'); + } + + static const String postLoginRedirectUrlPathParams = + 'post_login_redirect_url'; + + static const String postRegisteredRedirectUrlPathParams = + 'post_registered_redirect_url'; + + String loginUrl = + "${AppConfig.registrationUrl}?$postLoginRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; + + MatrixState get matrix => Matrix.of(context); + + void onClickSignIn() async { + matrix.loginHomeserverSummary = + await matrix.getLoginClient().checkHomeserver( + Uri.parse(AppConfig.twakeWorkplaceHomeserver), + ); + final uri = await FlutterWebAuth2.authenticate( + url: loginUrl, + callbackUrlScheme: AppConfig.appOpenUrlScheme, + options: const FlutterWebAuth2Options( + intentFlags: ephemeralIntentFlags, + ), + ); + Logs().d("TwakeIdController: onClickSignIn: uri: $uri"); + _handleLoginToken(uri); + } + + void _handleLoginToken(String uri) async { + final token = Uri.parse(uri).queryParameters['loginToken']; + Logs().d("TwakeIdController: _handleLoginToken: token: $token"); + if (token?.isEmpty ?? false) return; + Matrix.of(context).loginType = LoginType.mLoginToken; + await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => Matrix.of(context).getLoginClient().login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ), + ); + } + + @override + Widget build(BuildContext context) { + return TwakeIdView(controller: this); + } +} diff --git a/lib/pages/twake_id/twake_id_view.dart b/lib/pages/twake_id/twake_id_view.dart new file mode 100644 index 0000000000..62578c5650 --- /dev/null +++ b/lib/pages/twake_id/twake_id_view.dart @@ -0,0 +1,36 @@ +import 'package:fluffychat/pages/twake_id/twake_id.dart'; +import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class TwakeIdView extends StatelessWidget { + final TwakeIdController controller; + + const TwakeIdView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return TwakeIdScreen( + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + overlayColor: MaterialStateProperty.all(Colors.transparent), + signInTitle: L10n.of(context)!.signIn, + createTwakeIdTitle: L10n.of(context)!.createTwakeId, + useCompanyServerTitle: L10n.of(context)!.useYourCompanyServer, + description: L10n.of(context)!.descriptionTwakeId, + onUseCompanyServerOnTap: controller.goToHomeserverPicker, + onSignInOnTap: controller.onClickSignIn, + backButton: Padding( + padding: const EdgeInsets.only(left: 8), + child: TwakeIconButton( + icon: Icons.arrow_back, + onTap: () => context.pop(), + tooltip: L10n.of(context)!.back, + ), + ), + ); + } +} From 1127b6f84baaaa988f31edb96661c8a69fd024fa Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 25 Dec 2023 15:20:32 +0700 Subject: [PATCH 028/183] Create auto homeserver picker for web (cherry picked from commit 474ca6291aba499dfa62315019d94ee1eb7dcfd0) --- .../auto_homeserver_picker.dart | 117 ++++++++++++++++++ .../auto_homeserver_picker_view.dart | 112 +++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart create mode 100644 lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart new file mode 100644 index 0000000000..43b05c6d77 --- /dev/null +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart @@ -0,0 +1,117 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart'; +import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/utils/exception/check_homeserver_exception.dart'; +import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +class AutoHomeserverPicker extends StatefulWidget { + final bool? loggedOut; + + const AutoHomeserverPicker({ + super.key, + this.loggedOut, + }); + + @override + State createState() => AutoHomeserverPickerController(); +} + +class AutoHomeserverPickerController extends State + with ConnectPageMixin { + static const Duration autoHomeserverPickerTimeout = Duration(seconds: 30); + + final showButtonRetryNotifier = ValueNotifier(false); + + MatrixState get matrix => Matrix.of(context); + + void _autoCheckHomeserver() async { + try { + matrix.loginHomeserverSummary = await matrix + .getLoginClient() + .checkHomeserver( + Uri.parse(AppConfig.homeserver), + ) + .timeout( + autoHomeserverPickerTimeout, + onTimeout: () { + throw CheckHomeserverTimeoutException(); + }, + ); + + final ssoSupported = matrix.loginHomeserverSummary!.loginFlows + .any((flow) => flow.type == 'm.login.sso'); + + try { + await Matrix.of(context).getLoginClient().register().timeout( + autoHomeserverPickerTimeout, + onTimeout: () { + throw CheckHomeserverTimeoutException(); + }, + ); + matrix.loginRegistrationSupported = true; + } on MatrixException catch (e) { + matrix.loginRegistrationSupported = e.requireAdditionalAuthentication; + } + + if (!ssoSupported && matrix.loginRegistrationSupported == false) { + // Server does not support SSO or registration. We can skip to login page: + context.push('/login'); + } else if (ssoSupported && matrix.loginRegistrationSupported == false) { + Map? rawLoginTypes; + await Matrix.of(context) + .getLoginClient() + .request( + RequestType.GET, + '/client/r0/login', + ) + .then((loginTypes) => rawLoginTypes = loginTypes) + .timeout( + autoHomeserverPickerTimeout, + onTimeout: () { + throw CheckHomeserverTimeoutException(); + }, + ); + final identitiesProvider = + identityProviders(rawLoginTypes: rawLoginTypes); + if (supportsSso(context) && identitiesProvider?.length == 1) { + ssoLoginAction(context: context, id: identitiesProvider!.single.id!); + } + } else { + context.push('/connect'); + } + } catch (e) { + showButtonRetryNotifier.toggle(); + Logs().d( + "AutoHomeserverPickerController: _autoCheckHomeserver: Error: $e", + ); + } + } + + void retryCheckHomeserver() { + showButtonRetryNotifier.toggle(); + _autoCheckHomeserver(); + } + + @override + void initState() { + if (widget.loggedOut == null) { + _autoCheckHomeserver(); + } else { + if (widget.loggedOut == true) { + showButtonRetryNotifier.toggle(); + } + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AutoHomeserverPickerView( + controller: this, + ); + } +} diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart new file mode 100644 index 0000000000..c59650f917 --- /dev/null +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart @@ -0,0 +1,112 @@ +import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; +import 'package:fluffychat/pages/twake_welcome/twake_welcome_view_style.dart'; +import 'package:fluffychat/resource/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/twake_screen/twake_welcome_screen_style.dart'; + +class AutoHomeserverPickerView extends StatelessWidget { + final AutoHomeserverPickerController controller; + const AutoHomeserverPickerView({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + gradient: LinagoraSysColors.material().linearGradientStartingPage, + ), + child: Stack( + alignment: Alignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context)!.welcomeTo, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: LinagoraSysColors.material().onSurfaceVariant, + ), + ), + SvgPicture.asset( + ImagePaths.logoTwakeWelcome, + width: TwakeWelcomeViewStyle.logoWidth, + height: TwakeWelcomeViewStyle.logoHeight, + ), + Padding( + padding: TwakeWelcomeScreenStyle.descriptionPadding, + child: Text( + L10n.of(context)!.descriptionWelcomeTo, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: LinagoraSysColors.material().onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 32), + ValueListenableBuilder( + valueListenable: controller.showButtonRetryNotifier, + builder: (context, isShowRetry, child) { + if (isShowRetry) return child!; + return const CircularProgressIndicator.adaptive(); + }, + child: const SizedBox(), + ), + ], + ), + Positioned( + bottom: 0, + child: ValueListenableBuilder( + valueListenable: controller.showButtonRetryNotifier, + builder: (context, isShowRetry, child) { + if (isShowRetry) return child!; + return const SizedBox(); + }, + child: Padding( + padding: TwakeWelcomeScreenStyle.buttonPadding, + child: InkWell( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + onTap: controller.retryCheckHomeserver, + child: Container( + height: TwakeWelcomeScreenStyle.buttonHeight, + padding: const EdgeInsets.symmetric( + horizontal: 40, + ), + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: LinagoraSysColors.material().primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + TwakeWelcomeScreenStyle.buttonRadius, + ), + ), + ), + child: Center( + child: Text( + L10n.of(context)!.startMessaging, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: LinagoraSysColors.material().onPrimary, + ), + ), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} From d5b282c6d4d0877583b2733a0649a48699b81792 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 25 Dec 2023 15:21:18 +0700 Subject: [PATCH 029/183] Handle login and logout on web (cherry picked from commit d88d289705c5f8030edb65d222596a85055ffd48) --- lib/config/app_config.dart | 8 ++++++++ lib/config/go_routes/go_router.dart | 5 ++++- lib/pages/connect/connect_page_mixin.dart | 14 ++++++++++++-- .../exception/check_homeserver_exception.dart | 10 ++++++++++ lib/widgets/matrix.dart | 2 +- 5 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 lib/utils/exception/check_homeserver_exception.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index b238f95dfd..552b6b1d41 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -25,6 +25,8 @@ abstract class AppConfig { static String twakeWorkplaceHomeserver = 'https://example.com/'; + static String homeserver = 'https://example.com/'; + static double toolbarHeight(BuildContext context) => responsive.isMobile(context) ? 48 : 56; static const Color chatColor = primaryColor; @@ -99,6 +101,12 @@ abstract class AppConfig { "configurations/app_dashboard.json"; static void loadFromJson(Map json) { + if (json['homeserver'] != null && json['homeserver'] is String) { + if (json['homeserver'] != '') { + homeserver = json['homeserver']; + } + } + if (json['register_site'] != null && json['registration_url'] is String) { if (json['registration_url'] != '') { registrationUrl = json['registration_url']; diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 818b060b06..3d21235325 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/config/first_column_inner_routes.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/add_story/add_story.dart'; import 'package:fluffychat/pages/archive/archive.dart'; +import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; import 'package:fluffychat/pages/chat/chat_pinned_events/pinned_events_argument.dart'; import 'package:fluffychat/pages/chat/chat_pinned_events/pinned_messages.dart'; import 'package:fluffychat/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart'; @@ -78,7 +79,9 @@ abstract class AppRoutes { context, PlatformInfos.isMobile ? const TwakeWelcome() - : const HomeserverPicker(), + : AutoHomeserverPicker( + loggedOut: state.extra is bool ? state.extra as bool? : null, + ), ), redirect: loggedInRedirect, routes: [ diff --git a/lib/pages/connect/connect_page_mixin.dart b/lib/pages/connect/connect_page_mixin.dart index ef35119d1a..0471c680eb 100644 --- a/lib/pages/connect/connect_page_mixin.dart +++ b/lib/pages/connect/connect_page_mixin.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; import 'package:fluffychat/pages/connect/connect_page.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/exception/check_homeserver_exception.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -111,11 +113,19 @@ mixin ConnectPageMixin { if (token?.isEmpty ?? false) return; Matrix.of(context).loginType = LoginType.mLoginToken; await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context).getLoginClient().login( + future: () => Matrix.of(context) + .getLoginClient() + .login( LoginType.mLoginToken, token: token, initialDeviceDisplayName: PlatformInfos.clientName, - ), + ) + .timeout( + AutoHomeserverPickerController.autoHomeserverPickerTimeout, + onTimeout: () { + throw CheckHomeserverTimeoutException(); + }, + ), ); } diff --git a/lib/utils/exception/check_homeserver_exception.dart b/lib/utils/exception/check_homeserver_exception.dart new file mode 100644 index 0000000000..08e3a10e61 --- /dev/null +++ b/lib/utils/exception/check_homeserver_exception.dart @@ -0,0 +1,10 @@ +class CheckHomeserverTimeoutException implements Exception { + final dynamic error; + + CheckHomeserverTimeoutException({ + this.error, + }); + + @override + String toString() => error; +} diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 18fa260a2b..e9bbfdd10f 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -370,7 +370,7 @@ class MatrixState extends State if (PlatformInfos.isMobile) { TwakeApp.router.go('/home/twakeWelcome'); } else { - TwakeApp.router.go('/home'); + TwakeApp.router.go('/home', extra: true); } } } From 30a1efe03b676630052d402c6f7d0eaca25f053f Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 09:09:52 +0700 Subject: [PATCH 030/183] TW-1193: Create TwakeChatPresentationAccount for multiple account (cherry picked from commit 6bc413a692408bebe69dbf753b7f184b3cc86391) --- .../twake_chat_presentation_account.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 lib/presentation/multiple_account/twake_chat_presentation_account.dart diff --git a/lib/presentation/multiple_account/twake_chat_presentation_account.dart b/lib/presentation/multiple_account/twake_chat_presentation_account.dart new file mode 100644 index 0000000000..b8bef0d7b6 --- /dev/null +++ b/lib/presentation/multiple_account/twake_chat_presentation_account.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; + +class TwakeChatPresentationAccount extends TwakePresentationAccount { + const TwakeChatPresentationAccount({ + required String accountName, + required String accountId, + required Widget avatar, + required AccountActiveStatus accountActiveStatus, + }) : super( + accountName: accountName, + accountId: accountId, + avatar: avatar, + accountActiveStatus: accountActiveStatus, + ); +} From 15203f7200ee9bc61569f87722b3278b8076b27f Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 11:26:57 +0700 Subject: [PATCH 031/183] TW-1193: Show bottom sheet multiple account (cherry picked from commit 3769be1884b4fc0943c92d0e3752bc2c9cecd3e9) --- assets/l10n/intl_en.arb | 2 + lib/pages/chat_list/chat_list.dart | 52 +- lib/pages/chat_list/chat_list_header.dart | 18 +- lib/pages/chat_list/chat_list_view.dart | 6 +- .../twake_components/twake_header.dart | 122 +++- .../twake_components/twake_header_style.dart | 9 + macos/Flutter/GeneratedPluginRegistrant.swift | 8 +- pubspec.lock | 653 +++++++++--------- 8 files changed, 485 insertions(+), 385 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 644d71e7b7..3074b311ab 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2980,6 +2980,8 @@ "descriptionTwakeId": "An open source messenger encrypt\nyour data with matrix protocol", "countFilesSendPerDialog": "The maximum files when sending is {count}.", "sendFiles": "Send {count} files", + "addAnotherAccount": "Add another account", + "accountSettings": "Account settings", "failedToSendFiles": "Failed to send files", "noResults": "No Results", "addAnotherAccount": "Add another account", diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 10fa9d7132..6fe3950b5b 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -79,6 +79,10 @@ class ChatListController extends State final ValueNotifier selectModeNotifier = ValueNotifier(SelectMode.normal); + final ValueNotifier currentProfileNotifier = ValueNotifier( + Profile(userId: ''), + ); + final ValueNotifier> conversationSelectionNotifier = ValueNotifier([]); @@ -102,7 +106,9 @@ class ChatListController extends State bool scrolledToTop = true; - Client get client => Matrix.of(context).client; + Client get client => matrixState.client; + + MatrixState get matrixState => Matrix.of(context); ActiveFilter activeFilter = AppConfig.separateChatTypes ? ActiveFilter.messages @@ -123,6 +129,15 @@ class ChatListController extends State // Needs to match GroupsSpacesEntry for 'separate group' checking. List get spaces => client.rooms.where((r) => r.isSpace).toList(); + List get bundles => matrixState.accountBundles.keys.toList() + ..sort( + (pre, next) => pre!.isValidMatrixId == next!.isValidMatrixId + ? 0 + : pre.isValidMatrixId && !next.isValidMatrixId + ? -1 + : 1, + ); + ValueNotifier activeRoomIdNotifier = ValueNotifier(null); bool get isSelectMode => selectModeNotifier.value == SelectMode.select; @@ -740,6 +755,40 @@ class ChatListController extends State } } + void _getCurrentProfile(Client client) async { + final profile = await client.getProfileFromUserId( + client.userID!, + getFromRooms: false, + ); + Logs().d( + 'ChatList::_getCurrentProfile() - currentProfile: $profile', + ); + currentProfileNotifier.value = profile; + } + + Future> getProfileBundles() async { + final profiles = await Future.wait( + bundles.expand((bundle) { + return (matrixState.accountBundles[bundle]!).map((clientBundle) async { + if (clientBundle != null) { + return await clientBundle.fetchOwnProfile(); + } + return null; + }); + }), + ); + + return profiles.toList(); + } + + void onGoToAccountSettings() { + context.push('/rooms/profile'); + } + + void onAddAnotherAccount() { + context.go('/rooms/addaccount'); + } + @override void initState() { if (kIsWeb) { @@ -749,6 +798,7 @@ class ChatListController extends State scrollController.addListener(_onScroll); _waitForFirstSync(); _hackyWebRTCFixForWeb(); + _getCurrentProfile(client); // TODO: 28Dec2023 Disable callkeep for util we support audio/video calls // CallKeepManager().initialize(); WidgetsBinding.instance.addPostFrameCallback((_) async { diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 1cc239c0ab..7124a329e6 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -1,23 +1,15 @@ +import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header_style.dart'; -import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; -import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; import 'package:fluffychat/widgets/twake_components/twake_header.dart'; import 'package:flutter/material.dart'; class ChatListHeader extends StatelessWidget { - final ValueNotifier selectModeNotifier; - final ValueNotifier> - conversationSelectionNotifier; - final VoidCallback onToggleSelectMode; - final VoidCallback? onOpenSearchPage; + final ChatListController controller; final VoidCallback onClearSelection; const ChatListHeader({ Key? key, - this.onOpenSearchPage, - required this.selectModeNotifier, - required this.onToggleSelectMode, - required this.conversationSelectionNotifier, + required this.controller, required this.onClearSelection, }) : super(key: key); @@ -26,9 +18,7 @@ class ChatListHeader extends StatelessWidget { return Column( children: [ TwakeHeader( - conversationSelectionNotifier: conversationSelectionNotifier, - selectModeNotifier: selectModeNotifier, - toggleSelectMode: onToggleSelectMode, + controller: controller, onClearSelection: onClearSelection, ), Container( diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 58acc99964..d86e5d6eab 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -43,12 +43,8 @@ class ChatListView extends StatelessWidget { appBar: PreferredSize( preferredSize: ChatListViewStyle.preferredSizeAppBar(context), child: ChatListHeader( - selectModeNotifier: controller.selectModeNotifier, - onToggleSelectMode: controller.toggleSelectMode, + controller: controller, onOpenSearchPage: onOpenSearchPage, - conversationSelectionNotifier: - controller.conversationSelectionNotifier, - onClearSelection: controller.onClickClearSelection, ), ), bottomNavigationBar: ValueListenableBuilder( diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index fcc10b5183..029652f958 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -1,25 +1,27 @@ +import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; -import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; +import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; +import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mixins/show_dialog_mixin.dart'; import 'package:fluffychat/widgets/twake_components/twake_header_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; +import 'package:matrix/matrix.dart'; class TwakeHeader extends StatelessWidget with ShowDialogMixin implements PreferredSizeWidget { - final ValueNotifier selectModeNotifier; - final ValueNotifier> - conversationSelectionNotifier; - final VoidCallback toggleSelectMode; + final ChatListController controller; final VoidCallback onClearSelection; const TwakeHeader({ Key? key, - required this.selectModeNotifier, - required this.toggleSelectMode, - required this.conversationSelectionNotifier, + required this.controller, required this.onClearSelection, }) : super(key: key); @@ -31,7 +33,7 @@ class TwakeHeader extends StatelessWidget automaticallyImplyLeading: false, leadingWidth: TwakeHeaderStyle.leadingWidth, title: ValueListenableBuilder( - valueListenable: selectModeNotifier, + valueListenable: controller.selectModeNotifier, builder: (context, selectMode, _) { return Align( alignment: TwakeHeaderStyle.alignment, @@ -72,7 +74,8 @@ class TwakeHeader extends StatelessWidget ), ), ValueListenableBuilder( - valueListenable: conversationSelectionNotifier, + valueListenable: + controller.conversationSelectionNotifier, builder: (context, conversationSelection, _) { return Padding( padding: @@ -104,30 +107,24 @@ class TwakeHeader extends StatelessWidget padding: TwakeHeaderStyle.actionsPadding, child: Align( alignment: Alignment.centerRight, - child: !TwakeHeaderStyle.isDesktop(context) - ? InkWell( - borderRadius: BorderRadius.circular( - TwakeHeaderStyle.textBorderRadius, - ), - onTap: toggleSelectMode, - child: Padding( - padding: TwakeHeaderStyle.textButtonPadding, - child: Text( - selectMode == SelectMode.normal - ? L10n.of(context)!.edit - : L10n.of(context)!.done, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ), - ), - ) - : const SizedBox.shrink(), + child: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: () => _displayMultipleAccountPicker(context), + child: ValueListenableBuilder( + valueListenable: controller.currentProfileNotifier, + builder: (context, profile, _) { + return Avatar( + mxContent: profile.avatarUrl, + name: profile.displayName ?? + Matrix.of(context).client.userID!.localpart, + size: TwakeHeaderStyle.avatarSize, + fontSize: TwakeHeaderStyle.avatarFontSizeInAppBar, + ); + }, + ), + ), ), ), ), @@ -140,6 +137,63 @@ class TwakeHeader extends StatelessWidget ); } + void _displayMultipleAccountPicker(BuildContext context) async { + final multipleAccount = await _getMultipleAccount(); + MultipleAccountPicker.showMultipleAccountPicker( + accounts: multipleAccount, + context: context, + onAddAnotherAccount: controller.onAddAnotherAccount, + onGoToAccountSettings: controller.onGoToAccountSettings, + onSetAccountAsActive: (_) {}, + titleAddAnotherAccount: L10n.of(context)!.addAnotherAccount, + titleAccountSettings: L10n.of(context)!.accountSettings, + logoApp: Padding( + padding: TwakeHeaderStyle.logoAppOfMultiplePadding, + child: SvgPicture.asset( + ImagePaths.icTwakeImageLogo, + width: TwakeHeaderStyle.logoAppOfMultipleWidth, + height: TwakeHeaderStyle.logoAppOfMultipleHeight, + ), + ), + accountNameStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: LinagoraSysColors.material().onSurface, + ), + accountIdStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: LinagoraRefColors.material().tertiary[20], + ), + addAnotherAccountStyle: Theme.of(context).textTheme.labelLarge!.copyWith( + color: LinagoraSysColors.material().onPrimary, + ), + titleAccountSettingsStyle: + Theme.of(context).textTheme.labelLarge!.copyWith( + color: LinagoraSysColors.material().primary, + ), + ); + } + + Future> _getMultipleAccount() async { + final profileBundles = await controller.getProfileBundles(); + return profileBundles + .where((profileBundle) => profileBundle != null) + .map( + (profileBundle) => TwakeChatPresentationAccount( + accountId: profileBundle!.userId, + accountName: profileBundle.displayName ?? '', + avatar: Avatar( + mxContent: profileBundle.avatarUrl, + name: profileBundle.displayName ?? '', + size: TwakeHeaderStyle.avatarOfMultipleAccountSize, + fontSize: TwakeHeaderStyle.avatarFontSizeInAppBar, + ), + accountActiveStatus: profileBundle.userId == + controller.currentProfileNotifier.value.userId + ? AccountActiveStatus.active + : AccountActiveStatus.inactive, + ), + ) + .toList(); + } + @override Size get preferredSize => const Size.fromHeight(TwakeHeaderStyle.toolbarHeight); diff --git a/lib/widgets/twake_components/twake_header_style.dart b/lib/widgets/twake_components/twake_header_style.dart index f3675da4c4..922c98bff9 100644 --- a/lib/widgets/twake_components/twake_header_style.dart +++ b/lib/widgets/twake_components/twake_header_style.dart @@ -12,11 +12,20 @@ class TwakeHeaderStyle { static const double textBorderRadius = 24.0; static const int flexTitle = 6; static const int flexActions = 3; + static const double avatarSize = 36; + + static double get avatarFontSizeInAppBar => 14.0; + static const double avatarOfMultipleAccountSize = 48.0; + static const double logoAppOfMultipleHeight = 28.0; + static const double logoAppOfMultipleWidth = 152.0; static bool isDesktop(BuildContext context) => responsive.isDesktop(context); static AlignmentGeometry alignment = AlignmentDirectional.centerStart; + static const EdgeInsetsDirectional logoAppOfMultiplePadding = + EdgeInsetsDirectional.all(16); + static const EdgeInsetsDirectional actionsPadding = EdgeInsetsDirectional.only( end: 16, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index bfe32c0615..f2af6f2ea8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import appkit_ui_element_colors import audio_session import connectivity_plus import desktop_drop @@ -16,6 +17,7 @@ import file_saver import file_selector_macos import firebase_core import flutter_app_badger +import flutter_image_compress_macos import flutter_local_notifications import flutter_secure_storage_macos import flutter_web_auth_2 @@ -37,11 +39,13 @@ import sqflite import super_native_extensions import url_launcher_macos import video_compress +import video_player_avfoundation import wakelock_macos import wakelock_plus import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) @@ -53,6 +57,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) + FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) @@ -63,7 +68,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin")) RecordMacosPlugin.register(with: registry.registrar(forPlugin: "RecordMacosPlugin")) @@ -74,6 +79,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) diff --git a/pubspec.lock b/pubspec.lock index b93823499a..81a1652fd6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: "direct main" description: name: adaptive_dialog - sha256: "3b8abc7d1ba0834061759ee0be8e623eff5bffbcd1e6df5608a11983cfad6b2b" + sha256: "910debe8766eff4b378ed5164bb470debb87c53a3bdf6adee03c79f64fbf7348" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.10.1" after_layout: dependency: "direct main" description: @@ -37,18 +37,34 @@ packages: dependency: "direct main" description: name: animations - sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + sha256: "708e4b68c23228c264b038fe7003a2f5d01ce85fc64d8cae090e86b27fcea6c5" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.10" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + appkit_ui_element_colors: + dependency: transitive + description: + name: appkit_ui_element_colors + sha256: c3e50f900aae314d339de489535736238627071457c4a4a2dbbb1545b4f04f22 + url: "https://pub.dev" + source: hosted + version: "1.0.0" archive: dependency: transitive description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.4.9" args: dependency: transitive description: @@ -69,10 +85,10 @@ packages: dependency: transitive description: name: audio_session - sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" + sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" url: "https://pub.dev" source: hosted - version: "0.1.16" + version: "0.1.18" auto_size_text: dependency: transitive description: @@ -125,34 +141,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.2.11" built_collection: dependency: transitive description: @@ -165,10 +181,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.8.1" cached_network_image: dependency: "direct main" description: @@ -205,42 +221,42 @@ packages: dependency: transitive description: name: camera - sha256: b4cede7c66f44fa476272d21bfe143d5f32e75de1ea56f737e3eaf982da23bab + sha256: "7fa53bb1c2059e58bf86b7ab506e3b2a78e42f82d365b44b013239b975a166ef" url: "https://pub.dev" source: hosted - version: "0.10.5+3" + version: "0.10.5+7" camera_android: dependency: transitive description: name: camera_android - sha256: "61d62676708f187fb89fb14b371f87470343ba3cb26d08fc358e4f8a18e13150" + sha256: "7215e38fa0be58cc3203a6e48de3636fb9b1bf93d6eeedf667f882d51b3a4bf3" url: "https://pub.dev" source: hosted - version: "0.10.8+6" + version: "0.10.8+15" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "332747f20cf911980e38c8442108102d4456752711781108fda237635baf362c" + sha256: "3c8dd395f18722f01b5f325ddd7f5256e9bcdce538fb9243b378ba759df3283c" url: "https://pub.dev" source: hosted - version: "0.9.13+3" + version: "0.9.13+8" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: "60fa0bb62a4f3bf3a7c413e31e4cd01b69c779ccc8e4668904a24581b86c316b" + sha256: b6a568984254cadaca41a6b896d87d3b2e79a2e5791afa036f8d524c6783b93a url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.7.0" camera_web: dependency: transitive description: name: camera_web - sha256: "894df2a4e9ddd77ffecee9553d5980eeabb8bf09d98e53934859e67dc367933b" + sha256: d4c2c571c7af04f8b10702ca16bb9ed2a26e64534171e8f75c9349b2c004d8f1 url: "https://pub.dev" source: hosted - version: "0.3.2+1" + version: "0.3.2+3" canonical_json: dependency: transitive description: @@ -277,18 +293,18 @@ packages: dependency: "direct main" description: name: chewie - sha256: "60701da1f22ed20cd2d40e856fd1f2249dacf5b629d9fa50676443a18a4857b8" + sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.7.4" cli_util: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -301,10 +317,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.9.0" collection: dependency: "direct main" description: @@ -358,10 +374,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.3+8" crypto: dependency: "direct main" description: @@ -382,10 +398,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" dart_style: dependency: transitive description: @@ -398,10 +414,10 @@ packages: dependency: transitive description: name: dart_webrtc - sha256: "3f581ea799829fabd6e0b99bd2210146e4d107c7b3ac8495af3510737a5c5c1a" + sha256: "5897a3bdd6c7fded07e80e250260ca4c9cd61f9080911aa308b516e1206745a9" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" dartz: dependency: "direct main" description: @@ -414,10 +430,10 @@ packages: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" debounce_throttle: dependency: "direct main" description: @@ -454,10 +470,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659" + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" url: "https://pub.dev" source: hosted - version: "9.0.3" + version: "9.1.1" device_info_plus_platform_interface: dependency: transitive description: @@ -470,10 +486,10 @@ packages: dependency: "direct main" description: name: dio - sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197 + sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.4.0" dio_cache_interceptor: dependency: "direct main" description: @@ -486,26 +502,26 @@ packages: dependency: "direct main" description: name: dio_cache_interceptor_hive_store - sha256: "7a376b1db0a153e16ad51ce0cf1d2549ca14a2ddf462523c362fac9e077c5f14" + sha256: "449b36541216cb20543228081125ad2995eb9712ec35bd030d85663ea1761895" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d + sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b url: "https://pub.dev" source: hosted - version: "1.6.6" + version: "1.6.9" emoji_picker_flutter: dependency: "direct main" description: name: emoji_picker_flutter - sha256: "1ca31245cc1f7ab5304c68ccda8039f52b9f2372aa4d10803117160fad3faf12" + sha256: "009c51efc763d5a6ba05a5628b8b2184c327cd117d66ea9c3e7edf2ff269c423" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "1.6.3" emoji_proposal: dependency: "direct main" description: @@ -582,10 +598,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "5.5.0" file_saver: dependency: "direct main" description: @@ -606,58 +622,58 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "43e5c719f671b9181bef1bf2851135c3ad993a9a6c804a4ccb07579cfee84e34" + sha256: b7556052dbcc25ef88f6eba45ab98aa5600382af8dfdabc9d644a93d97b7be7f url: "https://pub.dev" source: hosted - version: "0.5.0+2" + version: "0.5.0+4" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: "54542b6b35e3ced6246df5fae13cf0b879d14669d0fdff1a53a098f16e23328b" + sha256: "2f48db7e338b2255101c35c604b7ca5ab588dce032db7fc418a2fe5f28da63f8" url: "https://pub.dev" source: hosted - version: "0.5.1+4" + version: "0.5.1+7" file_selector_linux: dependency: "direct overridden" description: name: file_selector_linux - sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.2+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+3" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.1" file_selector_web: dependency: transitive description: name: file_selector_web - sha256: e292740c469df0aeeaba0895bf622bea351a05e87d22864c826bf21c4780e1d7 + sha256: c0f025d460de3301b7bbbf837fc8d0759df85f182c635f1dd94934b4cdc92352 url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.3" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.3+1" firebase_core: dependency: transitive description: @@ -699,10 +715,10 @@ packages: dependency: "direct main" description: name: flutter_adaptive_scaffold - sha256: "4f448902314bc9b6cf820c85d5bad4de6489c0eff75dcedf5098f3a53ec981ee" + sha256: "3e78be8b9c95b1c9832b2f8ec4a845adac205c4bb5e7bd3fb204b07990229167" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.7+1" flutter_app_badger: dependency: "direct main" description: @@ -768,34 +784,42 @@ packages: dependency: "direct main" description: name: flutter_image_compress - sha256: "2725cce5c58fdeaf1db8f4203688228bb67e3523a66305ccaa6f99071beb6dc2" + sha256: f159d2e8c4ed04b8e36994124fd4a5017a0f01e831ae3358c74095c340e9ae5e url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.0" flutter_image_compress_common: dependency: transitive description: name: flutter_image_compress_common - sha256: "8e7299afe109dc4b97fda34bf0f4967cc1fc10bc8050c374d449cab262d095b3" + sha256: "7cad12802628706655920089cfe9ee1d1098300e7f39a079eb160458bbc47652" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: fea1e3d71150d03373916b832c49b5c2f56c3e7e13da82a929274a2c6f88251e + url: "https://pub.dev" + source: hosted + version: "1.0.1" flutter_image_compress_platform_interface: dependency: transitive description: name: flutter_image_compress_platform_interface - sha256: "3c7e86da7540b1adfa919b461885a41a018d4a26544d0fcbeaa769f6542e603d" + sha256: eb4f055138b29b04498ebcb6d569aaaee34b64d75fb74ea0d40f9790bf47ee9d url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" flutter_image_compress_web: dependency: transitive description: name: flutter_image_compress_web - sha256: e879189dc7f246dcf8f06c07ee849231341508bf51e8ed7d5dcbe778ddde0e81 + sha256: da41cc3859f19d11c7d10be615f6a9dcf0907e7daffde7442bf4cc2486663660 url: "https://pub.dev" source: hosted - version: "0.1.3+1" + version: "0.1.3+2" flutter_inappwebview: dependency: "direct main" description: @@ -808,10 +832,10 @@ packages: dependency: "direct main" description: name: flutter_keyboard_visibility - sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "5.4.1" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -864,10 +888,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_local_notifications: dependency: "direct main" description: @@ -926,7 +950,7 @@ packages: description: path: "." ref: master - resolved-ref: "3606f467a6b355b6c5a02b24682ecceccfb86421" + resolved-ref: "96d16afde0eab53afff59642ab382cbcb522a616" url: "https://github.com/linagora/flutter_matrix_html.git" source: git version: "1.2.0" @@ -934,18 +958,18 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: ba45d8cfbd778478a74696b012f33ffb6b1760c9bc531b21e2964444a4870dae + sha256: "141b20f15a2c4fe6e33c49257ca1bc114fc5c500b04fcbc8d75016bb86af672f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.8" flutter_olm: dependency: "direct main" description: name: flutter_olm - sha256: fef0c9476d02c0df25ef0a66680bc23ac529a36b4911505910bcd8711b449c81 + sha256: f98cafb434b3858d46446a93eb269a8e97eb38ccc5f8e0281b167d66d95c15dc url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" flutter_openssl_crypto: dependency: "direct main" description: @@ -958,18 +982,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.15" - flutter_portal: - dependency: "direct main" - description: - name: flutter_portal - sha256: "4601b3dc24f385b3761721bd852a3f6c09cddd4e943dd184ed58ee1f43006257" - url: "https://pub.dev" - source: hosted - version: "1.1.4" + version: "2.0.17" flutter_ringtone_player: dependency: "direct main" description: @@ -990,10 +1006,10 @@ packages: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.0" flutter_secure_storage_macos: dependency: transitive description: @@ -1006,18 +1022,18 @@ packages: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_windows: dependency: "direct overridden" description: @@ -1027,22 +1043,14 @@ packages: url: "https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git" source: git version: "1.1.2" - flutter_slidable: - dependency: "direct main" - description: - name: flutter_slidable - sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c" - url: "https://pub.dev" - source: hosted - version: "3.0.1" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -1052,10 +1060,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: e2070dea278f09ae30885872138ccae75292b33b7af2c241fec5ceafd980c374 + sha256: "1f6b248bb4f3ebb4cf1ee0354aa23c77be457fb2d26d6847ecc33a917f65e58e" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.0.1" flutter_web_auth_2: dependency: "direct main" description: @@ -1068,10 +1076,10 @@ packages: dependency: transitive description: name: flutter_web_auth_2_platform_interface - sha256: "9124824cbd21e12680bf58190e27b77f251c897e80ec81cd557ec1fde9aecabf" + sha256: e8669e262005a8354389ba2971f0fc1c36188481234ff50d013aaf993f30f739 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -1081,18 +1089,18 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: "6a26a2ca8e1759fe60705d745444184e384508b82feb68547d66d411dbc4ad2d" + sha256: "577216727181cb13776a65d3e7cb33e783e740c5496335011aed4a038b28c3fe" url: "https://pub.dev" source: hosted - version: "0.9.37" + version: "0.9.47" fluttertoast: dependency: "direct main" description: name: fluttertoast - sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c" + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "8.2.4" frontend_server_client: dependency: transitive description: @@ -1114,6 +1122,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.4" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: b8f520252c5c66851295bcc263bc8ae7555501938427f72216ba7688702e261d + url: "https://pub.dev" + source: hosted + version: "7.7.1" geolocator_android: dependency: "direct overridden" description: @@ -1122,6 +1138,14 @@ packages: url: "https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss/" source: hosted version: "1.0.1" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: "1e8e398cc92151d946a4bbd34e2075885333e42d35ca33e418e7ce7b0a29991e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" geolocator_platform_interface: dependency: transitive description: @@ -1130,14 +1154,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "0b9e0ec13ce2211085cae0055b3516c975bd6cfe2878a20c8f13611f1a259855" + url: "https://pub.dev" + source: hosted + version: "2.0.6" get_it: dependency: "direct main" description: name: get_it - sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" + sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3 url: "https://pub.dev" source: hosted - version: "7.6.0" + version: "7.6.4" glob: dependency: transitive description: @@ -1150,10 +1182,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b3cadd2cd59a4103fd5f6bc572ca75111264698784e927aa471921c3477d5475 + sha256: e1a30a66d734f9e498b1b6522d6a75ded28242bad2359a9158df38a1c30bcf1f url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.2.0" google_fonts: dependency: "direct main" description: @@ -1162,6 +1194,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.4" + gradient_borders: + dependency: transitive + description: + name: gradient_borders + sha256: "69eeaff519d145a4c6c213ada1abae386bcc8981a4970d923e478ce7ba19e309" + url: "https://pub.dev" + source: hosted + version: "1.0.0" graphs: dependency: transitive description: @@ -1254,10 +1294,10 @@ packages: dependency: "direct main" description: name: image - sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" url: "https://pub.dev" source: hosted - version: "4.0.17" + version: "4.1.3" image_gallery_saver: dependency: "direct main" description: @@ -1270,18 +1310,18 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "47da2161c2e9f8f8a9cbbd89d466d174333fbdd769aeed848912e0b16d9cb369" + sha256: ecdc963d2aa67af5195e723a40580f802d4392e31457a12a562b3e2bd6a396fe url: "https://pub.dev" source: hosted - version: "0.8.8" + version: "0.8.9+1" image_picker_for_web: dependency: transitive description: @@ -1294,10 +1334,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 + sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf url: "https://pub.dev" source: hosted - version: "0.8.8+2" + version: "0.8.9" image_picker_linux: dependency: transitive description: @@ -1359,15 +1399,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.1" - inview_notifier_list: - dependency: "direct main" - description: - path: "." - ref: master - resolved-ref: f7a29ac1c4de3ea26d749c38781a6f5e9b1b932d - url: "git@github.com:linagora/inview_notifier_list.git" - source: git - version: "3.0.0" io: dependency: transitive description: @@ -1380,10 +1411,10 @@ packages: dependency: transitive description: name: irondash_engine_context - sha256: "294a0e21c4358ff17264e6b811c615b664cebb33b49ad2ddb54f8110e7714510" + sha256: "8b8c58f2b6414b0db739b329ca08baf3b66d4fcfc3c315525ddac61639addb90" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.0" irondash_message_channel: dependency: transitive description: @@ -1428,34 +1459,34 @@ packages: dependency: "direct main" description: name: just_audio - sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b" + sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823 url: "https://pub.dev" source: hosted - version: "0.9.35" + version: "0.9.36" just_audio_mpv: dependency: "direct main" description: name: just_audio_mpv - sha256: "98ac36712f3fe4fb0cf545f29c250fbd55e52c8445a4b0a4ee0bc9322f192797" + sha256: d6e4e9fd20bfb9d2fd5e3dcd7906c90ed07f83d1d2f44f31204160821ab62fed url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.7" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df + sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1 url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13 + sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70" url: "https://pub.dev" source: hosted - version: "0.4.8" + version: "0.4.9" keyboard_shortcuts: dependency: "direct main" description: @@ -1477,8 +1508,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "492b6a564e3df87dc7f0796c84e1641358460d49" + ref: multiple-account-picker + resolved-ref: "908bb487d1073f3dd5727aaeb5ab3d9ddae4c0e4" url: "git@github.com:linagora/linagora-design-flutter.git" source: git version: "0.0.1" @@ -1510,34 +1541,34 @@ packages: dependency: "direct main" description: name: lottie - sha256: b8bdd54b488c54068c57d41ae85d02808da09e2bee8b8dd1f59f441e7efa60cd + sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" macos_ui: dependency: transitive description: name: macos_ui - sha256: b739149b812c47e5ff10a00c9fdf7315f22ac5cd1fdbd447a6b7ffee31472717 + sha256: cc499122655c61728185561e9006af4b239f9526f98d7b2cbf42124e9044a0ff url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.2" macos_window_utils: dependency: transitive description: name: macos_window_utils - sha256: b78a210aa70ca7ccad6e7b7b810fb4689c507f4a46e299214900b2a1eb70ea23 + sha256: b3dfd47bbc605f0e315af684b50370a8f84932267aaa542098063fa384d593bd url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.4.0" markdown: dependency: transitive description: name: markdown - sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90" + sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.1.1" matcher: dependency: transitive description: @@ -1559,7 +1590,7 @@ packages: description: path: "." ref: "twake-supported-0.22.6" - resolved-ref: "22311972c4d781133b893ae032dcb509b1351075" + resolved-ref: e5e67b2d0158b9398eb6430841c2bed7f61d8997 url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" @@ -1567,10 +1598,10 @@ packages: dependency: transitive description: name: matrix_api_lite - sha256: e5304b33b16d60863533836717be808845bf94cd0e3a339ef146d9321e6b59b7 + sha256: "62bdd1dffb956e956863ba21e52109157502342b749e4728f4105f0c6d73a254" url: "https://pub.dev" source: hosted - version: "1.7.1" + version: "1.7.2" matrix_homeserver_recommendations: dependency: "direct main" description: @@ -1600,66 +1631,66 @@ packages: dependency: transitive description: name: media_kit_libs_android_video - sha256: "498a5062bc5f000bd23ada3be788ea886ab32c52f7a8252dde1264ca019b819b" + sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.6" media_kit_libs_ios_video: dependency: transitive description: name: media_kit_libs_ios_video - sha256: fed403dc9d54462e51ee80e0cb23c12a53fadea9a8fa18aca2de9054176d1159 + sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.4" media_kit_libs_linux: dependency: transitive description: name: media_kit_libs_linux - sha256: "3b7c272179639a914dc8a50bf8a3f2df0e9a503bd727c88fab499dbdf6cb1eb8" + sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" media_kit_libs_macos_video: dependency: transitive description: name: media_kit_libs_macos_video - sha256: c06e831f3c22a45296d375788d9bc07871b448f8e9ec98d77b11e5e118a83fb2 + sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.4" media_kit_libs_video: dependency: "direct main" description: name: media_kit_libs_video - sha256: d961c49bc0d454524014b76fd66db1aa06e673f03b616f5fdbc59c405178a878 + sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" media_kit_libs_windows_video: dependency: transitive description: name: media_kit_libs_windows_video - sha256: "923f068344d7d200184e0aaa2597f3de6c05982a3b1f18035d842ab53f2a1350" + sha256: "7bace5f35d9afcc7f9b5cdadb7541d2191a66bb3fc71bfa11c1395b3360f6122" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" media_kit_native_event_loop: dependency: transitive description: name: media_kit_native_event_loop - sha256: e37ce6fb5fa71b8cf513c6a6cd591367743a342a385e7da621a047dd8ef6f4a4 + sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.0.8" media_kit_video: dependency: "direct main" description: name: media_kit_video - sha256: cd3ab78e7626146f115134b82c4029ac5987ba6351719c9067d86789723e0c12 + sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882 url: "https://pub.dev" source: hosted - version: "1.1.8" + version: "1.2.4" meta: dependency: transitive description: @@ -1684,14 +1715,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - mockito: - dependency: "direct dev" - description: - name: mockito - sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" - url: "https://pub.dev" - source: hosted - version: "5.4.2" mpv_dart: dependency: transitive description: @@ -1704,18 +1727,18 @@ packages: dependency: "direct dev" description: name: msix - sha256: "76c87b8207323803169626a55afd78bbb8413c984df349a76598b9fbf9224677" + sha256: "519b183d15dc9f9c594f247e2d2339d855cf0eaacc30e19b128e14f3ecc62047" url: "https://pub.dev" source: hosted - version: "3.16.1" + version: "3.16.7" native_imaging: dependency: "direct main" description: name: native_imaging - sha256: "9f96eafb6d84ec934262caf36b60e236d1c4507ed6555a1effc117d463ef5932" + sha256: "182ccd8e0815a8a2158500ef66c828c030f6b9e05783e41e22f33bbcfd46a3d5" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" nested: dependency: transitive description: @@ -1776,10 +1799,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1808,50 +1831,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" permission_handler: dependency: "direct main" description: @@ -1864,10 +1887,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" url: "https://pub.dev" source: hosted - version: "10.3.3" + version: "10.3.6" permission_handler_apple: dependency: transitive description: @@ -1880,10 +1903,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" url: "https://pub.dev" source: hosted - version: "3.11.3" + version: "3.12.0" permission_handler_windows: dependency: transitive description: @@ -1896,10 +1919,10 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" photo_manager: dependency: "direct main" description: @@ -1928,10 +1951,10 @@ packages: dependency: transitive description: name: pixel_snap - sha256: "5de3662b926c9bc189578cf90f9d5b350ee61bc8e20e8a91fa1dfdd26c9f5ece" + sha256: d31591a4f4aa8ed5dc6fc00b8d027338a5614dfbf5ca658b69d1faa7aba80af7 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.4" platform: dependency: transitive description: @@ -1944,50 +1967,26 @@ packages: dependency: transitive description: name: platform_detect - sha256: "14afcb6ffcd93745e39a288db53d1d6522ea25d71f7993c13a367a86c437b54d" + sha256: "08f4ee79c0e1c4858d37e06b22352a3ebdef5466b613749a3adb03e703d4f5b0" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.11" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.7" pointer_interceptor: dependency: transitive description: name: pointer_interceptor - sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a - url: "https://pub.dev" - source: hosted - version: "0.10.1" - pointer_interceptor_ios: - dependency: transitive - description: - name: pointer_interceptor_ios - sha256: "2e73c39452830adc4695757130676a39412a3b7f3c34e3f752791b5384770877" - url: "https://pub.dev" - source: hosted - version: "0.10.0+2" - pointer_interceptor_platform_interface: - dependency: transitive - description: - name: pointer_interceptor_platform_interface - sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" - url: "https://pub.dev" - source: hosted - version: "0.10.0+1" - pointer_interceptor_web: - dependency: transitive - description: - name: pointer_interceptor_web - sha256: "9386e064097fd16419e935c23f08f35b58e6aaec155dd39bd6a003b88f9c14b4" + sha256: adf7a637f97c077041d36801b43be08559fd4322d2127b3f20bb7be1b9eebc22 url: "https://pub.dev" source: hosted - version: "0.10.1+2" + version: "0.9.3+7" pointycastle: dependency: transitive description: @@ -2040,10 +2039,10 @@ packages: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -2169,10 +2168,10 @@ packages: dependency: transitive description: name: remove_emoji - sha256: d75024ae134328c38871c0fe73ada15ebeb635fca8903d039f5090a3e902c2b2 + sha256: ed9e8463e8c9ca05b86fcddd4c0dbd2c2605a50d267f4ffa05496607924809e3 url: "https://pub.dev" source: hosted - version: "0.0.9" + version: "0.0.10" rxdart: dependency: "direct main" description: @@ -2337,10 +2336,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_ios: dependency: transitive description: @@ -2353,10 +2352,10 @@ packages: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shared_preferences_macos: dependency: transitive description: @@ -2369,26 +2368,26 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shelf: dependency: transitive description: @@ -2413,14 +2412,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - skeletonizer: - dependency: "direct main" - description: - name: skeletonizer - sha256: "2eb80153c80507359ff05f6a18ed50ae0bafa1b999aa867a8cef0a53387b5650" - url: "https://pub.dev" - source: hosted - version: "1.1.0" skeletons: dependency: "direct main" description: @@ -2446,10 +2437,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_helper: dependency: transitive description: @@ -2486,10 +2477,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.0+2" stack_trace: dependency: transitive description: @@ -2525,19 +2516,21 @@ packages: super_clipboard: dependency: "direct main" description: - name: super_clipboard - sha256: "77f044320934386e0b7a3911e05312426d7f33deb6e8cdb28886663430b0e5b0" - url: "https://pub.dev" - source: hosted - version: "0.8.4" + path: super_clipboard + ref: main + resolved-ref: fdaf87f6a30ac049d401967a8c5ec400126f288d + url: "https://github.com/superlistapp/super_native_extensions.git" + source: git + version: "0.8.1" super_native_extensions: - dependency: transitive + dependency: "direct overridden" description: - name: super_native_extensions - sha256: "4699f5b00320290475953c914f823a8b44e10ed8c1e38ce5c8e8426336d11c15" - url: "https://pub.dev" - source: hosted - version: "0.8.4" + path: super_native_extensions + ref: main + resolved-ref: fdaf87f6a30ac049d401967a8c5ec400126f288d + url: "https://github.com/superlistapp/super_native_extensions.git" + source: git + version: "0.8.1" sync_http: dependency: transitive description: @@ -2550,10 +2543,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -2686,10 +2679,10 @@ packages: dependency: "direct main" description: name: universal_html - sha256: a5cc5a84188e5d3e58f3ed77fe3dd4575dc1f68aa7c89e51b5b4105b9aab3b9d + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" universal_io: dependency: transitive description: @@ -2726,66 +2719,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 url: "https://pub.dev" source: hosted - version: "6.1.12" + version: "6.2.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" url: "https://pub.dev" source: hosted - version: "6.0.37" + version: "6.2.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.2.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.2.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.1" uuid: dependency: transitive description: @@ -2806,26 +2799,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.9+1" vector_math: dependency: transitive description: @@ -2838,10 +2831,10 @@ packages: dependency: "direct main" description: name: vibration - sha256: "2938d4bf4ecfdb1cdac6b8f20f40cd3e7b7783edd6ca551a46d144a134473626" + sha256: "778ace40e84852e6cf6017cdbaf6790a837d73ff3dd50b27da9ac232a19de8fc" url: "https://pub.dev" source: hosted - version: "1.7.7" + version: "1.8.4" video_compress: dependency: "direct main" description: @@ -2855,42 +2848,42 @@ packages: dependency: "direct main" description: name: video_player - sha256: "74b86e63529cf5885130c639d74cd2f9232e7c8a66cbecbddd1dcb9dbd060d1e" + sha256: e16f0a83601a78d165dabc17e4dac50997604eb9e4cc76e10fa219046b70cef3 url: "https://pub.dev" source: hosted - version: "2.7.2" + version: "2.8.1" video_player_android: dependency: transitive description: name: video_player_android - sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 + sha256: "3fe89ab07fdbce786e7eb25b58532d6eaf189ceddc091cb66cba712f8d9e8e55" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: f5f5b7fe8c865be8a57fe80c2dca130772e1db775b7af4e5c5aa1905069cfc6c + sha256: "01a57940e1dabc8769ccd457c4ae9ea50274e7d5a7617f7820dae5fe1d8436ae" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.5.3" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" + sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.1" video_player_web: dependency: transitive description: name: video_player_web - sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" + sha256: ab7a462b07d9ca80bed579e30fb3bce372468f1b78642e0911b10600f2c5cb5b url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.1.2" video_thumbnail: dependency: "direct main" description: @@ -2970,10 +2963,10 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: aac3f3258f01781ec9212df94eecef1eb9ba9350e106728def405baa096ba413 + sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.4" wakelock_plus_platform_interface: dependency: transitive description: @@ -3035,10 +3028,10 @@ packages: dependency: "direct main" description: name: webrtc_interface - sha256: "0dd96f4d7fb6ba9895930644cebd3f1adb5179caa83cb1760061b2fe9cba5aad" + sha256: "2efbd3e4e5ebeb2914253bcc51dafd3053c4b87b43f3076c74835a9deecbae3a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" wechat_camera_picker: dependency: "direct main" description: @@ -3051,18 +3044,18 @@ packages: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.1.1" win32_registry: dependency: transitive description: name: win32_registry - sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" window_to_front: dependency: transitive description: @@ -3091,10 +3084,10 @@ packages: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: From 970523bbda6aa52a56c4717c952094da506fa204 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 15:02:16 +0700 Subject: [PATCH 032/183] TW-1193: Update go_router for add another account (cherry picked from commit 896af446071ec6c4f65ee433e84df84f78788e61) --- lib/pages/chat_list/chat_list.dart | 9 ++++++++- lib/pages/twake_id/twake_id.dart | 28 ++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 6fe3950b5b..8a67dc4b94 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -9,6 +9,8 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; import 'package:fluffychat/domain/model/room/room_extension.dart'; import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; +import 'package:fluffychat/pages/twake_id/twake_id.dart'; +import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; @@ -786,7 +788,12 @@ class ChatListController extends State } void onAddAnotherAccount() { - context.go('/rooms/addaccount'); + context.go( + '/rooms/addaccount', + extra: const TwakeIdArg( + twakeIdType: TwakeIdType.multiLogin, + ), + ); } @override diff --git a/lib/pages/twake_id/twake_id.dart b/lib/pages/twake_id/twake_id.dart index 1ccc7cf999..ea4b02cec0 100644 --- a/lib/pages/twake_id/twake_id.dart +++ b/lib/pages/twake_id/twake_id.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:equatable/equatable.dart'; import 'package:fluffychat/pages/twake_id/twake_id_view.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -8,8 +9,27 @@ import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +enum TwakeIdType { + login, + multiLogin, +} + +class TwakeIdArg extends Equatable { + final TwakeIdType twakeIdType; + + const TwakeIdArg({ + this.twakeIdType = TwakeIdType.login, + }); + + bool get isAddAnotherAccount => twakeIdType == TwakeIdType.multiLogin; + + @override + List get props => [twakeIdType]; +} + class TwakeId extends StatefulWidget { - const TwakeId({super.key}); + final TwakeIdArg? arg; + const TwakeId({super.key, this.arg}); @override State createState() => TwakeIdController(); @@ -17,7 +37,11 @@ class TwakeId extends StatefulWidget { class TwakeIdController extends State { void goToHomeserverPicker() { - context.push('/home/homeserverpicker'); + if (widget.arg?.isAddAnotherAccount == true) { + context.push('/rooms/homeserverpicker'); + } else { + context.push('/home/homeserverpicker'); + } } static const String postLoginRedirectUrlPathParams = From 1ff0270c3922c3e3d17b6ae3b7c228c18c169f16 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 16:08:58 +0700 Subject: [PATCH 033/183] TW-1193: Set account as active (cherry picked from commit 285128ba6c2227117dec888ce4eb8d34a729eb1c) --- lib/pages/chat_list/chat_list.dart | 39 ++++++++++++++----- .../multiple_account/profile_bundle.dart | 18 +++++++++ .../twake_chat_presentation_account.dart | 13 +++++++ .../twake_components/twake_header.dart | 20 +++++++--- 4 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 lib/presentation/multiple_account/profile_bundle.dart diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 8a67dc4b94..7f03717d4d 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -20,6 +20,8 @@ import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/presentation/mixins/go_to_group_chat_mixin.dart'; import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; +import 'package:fluffychat/presentation/multiple_account/profile_bundle.dart'; +import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -35,6 +37,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; +import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; import 'package:matrix/matrix.dart'; import '../../../utils/account_bundles.dart'; @@ -462,14 +465,13 @@ class ChatListController extends State void setActiveClient(Client client) { context.go('/rooms'); - setState(() { - activeFilter = AppConfig.separateChatTypes - ? ActiveFilter.messages - : ActiveFilter.allChats; - activeSpaceId = null; - conversationSelectionNotifier.value.clear(); - Matrix.of(context).setActiveClient(client); - }); + _getCurrentProfile(client); + activeFilter = AppConfig.separateChatTypes + ? ActiveFilter.messages + : ActiveFilter.allChats; + activeSpaceId = null; + conversationSelectionNotifier.value.clear(); + Matrix.of(context).setActiveClient(client); _clientStream.add(client); } @@ -768,12 +770,16 @@ class ChatListController extends State currentProfileNotifier.value = profile; } - Future> getProfileBundles() async { + Future> getProfileBundles() async { final profiles = await Future.wait( bundles.expand((bundle) { return (matrixState.accountBundles[bundle]!).map((clientBundle) async { if (clientBundle != null) { - return await clientBundle.fetchOwnProfile(); + final profileBundle = await clientBundle.fetchOwnProfile(); + return ProfileBundlePresentation( + profileBundle: profileBundle, + client: clientBundle, + ); } return null; }); @@ -783,6 +789,19 @@ class ChatListController extends State return profiles.toList(); } + void onSetAccountAsActive({ + required List multipleAccounts, + required TwakePresentationAccount account, + }) { + final client = multipleAccounts + .firstWhereOrNull( + (element) => element.accountId == account.accountId, + ) + ?.clientAccount; + if (client == null) return; + setActiveClient(client); + } + void onGoToAccountSettings() { context.push('/rooms/profile'); } diff --git a/lib/presentation/multiple_account/profile_bundle.dart b/lib/presentation/multiple_account/profile_bundle.dart new file mode 100644 index 0000000000..05ae3e8857 --- /dev/null +++ b/lib/presentation/multiple_account/profile_bundle.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; +import 'package:matrix/matrix.dart'; + +class ProfileBundlePresentation extends Equatable { + final Profile profileBundle; + final Client client; + + const ProfileBundlePresentation({ + required this.profileBundle, + required this.client, + }); + + @override + List get props => [ + profileBundle, + client, + ]; +} diff --git a/lib/presentation/multiple_account/twake_chat_presentation_account.dart b/lib/presentation/multiple_account/twake_chat_presentation_account.dart index b8bef0d7b6..6585764896 100644 --- a/lib/presentation/multiple_account/twake_chat_presentation_account.dart +++ b/lib/presentation/multiple_account/twake_chat_presentation_account.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; +import 'package:matrix/matrix.dart'; class TwakeChatPresentationAccount extends TwakePresentationAccount { + final Client clientAccount; + const TwakeChatPresentationAccount({ + required this.clientAccount, required String accountName, required String accountId, required Widget avatar, @@ -13,4 +17,13 @@ class TwakeChatPresentationAccount extends TwakePresentationAccount { avatar: avatar, accountActiveStatus: accountActiveStatus, ); + + @override + List get props => [ + clientAccount, + accountName, + accountId, + avatar, + accountActiveStatus, + ]; } diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index 029652f958..8994063e47 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -139,12 +139,19 @@ class TwakeHeader extends StatelessWidget void _displayMultipleAccountPicker(BuildContext context) async { final multipleAccount = await _getMultipleAccount(); + multipleAccount.sort((pre, next) { + return pre.accountActiveStatus.index + .compareTo(next.accountActiveStatus.index); + }); MultipleAccountPicker.showMultipleAccountPicker( accounts: multipleAccount, context: context, onAddAnotherAccount: controller.onAddAnotherAccount, onGoToAccountSettings: controller.onGoToAccountSettings, - onSetAccountAsActive: (_) {}, + onSetAccountAsActive: (account) => controller.onSetAccountAsActive( + multipleAccounts: multipleAccount, + account: account, + ), titleAddAnotherAccount: L10n.of(context)!.addAnotherAccount, titleAccountSettings: L10n.of(context)!.accountSettings, logoApp: Padding( @@ -177,15 +184,16 @@ class TwakeHeader extends StatelessWidget .where((profileBundle) => profileBundle != null) .map( (profileBundle) => TwakeChatPresentationAccount( - accountId: profileBundle!.userId, - accountName: profileBundle.displayName ?? '', + clientAccount: profileBundle!.client, + accountId: profileBundle.profileBundle.userId, + accountName: profileBundle.profileBundle.displayName ?? '', avatar: Avatar( - mxContent: profileBundle.avatarUrl, - name: profileBundle.displayName ?? '', + mxContent: profileBundle.profileBundle.avatarUrl, + name: profileBundle.profileBundle.displayName ?? '', size: TwakeHeaderStyle.avatarOfMultipleAccountSize, fontSize: TwakeHeaderStyle.avatarFontSizeInAppBar, ), - accountActiveStatus: profileBundle.userId == + accountActiveStatus: profileBundle.profileBundle.userId == controller.currentProfileNotifier.value.userId ? AccountActiveStatus.active : AccountActiveStatus.inactive, From 9dc1e72c00c06e76abef38ff09b3fb791f528dfb Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 16:41:52 +0700 Subject: [PATCH 034/183] TW-1193: Go to settings and show for the active account only (cherry picked from commit 2d99fdab3dae3a53d164b90e95efbaee6304fdd4) --- lib/pages/chat_list/chat_list.dart | 5 ++- .../app_adaptive_scaffold_body.dart | 1 + .../app_adaptive_scaffold_body_view.dart | 5 +++ .../twake_components/twake_header.dart | 45 +++++++++++-------- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 7f03717d4d..e4e8131ade 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -52,11 +52,14 @@ class ChatList extends StatefulWidget { final VoidCallback? onOpenSearchPage; + final VoidCallback? onOpenSettings; + const ChatList({ Key? key, required this.activeRoomIdNotifier, this.bottomNavigationBar, this.onOpenSearchPage, + this.onOpenSettings, }) : super(key: key); @override @@ -803,7 +806,7 @@ class ChatListController extends State } void onGoToAccountSettings() { - context.push('/rooms/profile'); + widget.onOpenSettings?.call(); } void onAddAnotherAccount() { diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index b84414d5a7..27084ab14e 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -149,5 +149,6 @@ class AppAdaptiveScaffoldBodyController extends State { onDestinationSelected: onDestinationSelected, onClientSelected: clientSelected, onPopInvoked: _onPopInvoked, + onOpenSettings: _onOpenSettingsPage, ); } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart index b982c1a904..ca1d373feb 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart @@ -24,6 +24,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { final OnClientSelectedSetting onClientSelected; final PageController pageController; final OnPopInvoked onPopInvoked; + final VoidCallback onOpenSettings; final ValueNotifier activeRoomIdNotifier; @@ -46,6 +47,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { required this.onClientSelected, required this.destinations, required this.onPopInvoked, + required this.onOpenSettings, }) : super(key: key ?? scaffoldWithNestedNavigationKey); @override @@ -179,6 +181,7 @@ class _ColumnPageView extends StatelessWidget { final OnClientSelectedSetting onClientSelected; final ValueKey bottomNavigationKey; final ValueNotifier activeRoomIdNotifier; + final VoidCallback onOpenSettings; const _ColumnPageView({ required this.activeNavigationBarNotifier, @@ -190,6 +193,7 @@ class _ColumnPageView extends StatelessWidget { required this.onClientSelected, required this.destinations, required this.bottomNavigationKey, + required this.onOpenSettings, }); @override @@ -211,6 +215,7 @@ class _ColumnPageView extends StatelessWidget { ), onOpenSearchPage: onOpenSearchPage, activeRoomIdNotifier: activeRoomIdNotifier, + onOpenSettings: onOpenSettings, ), _triggerPageViewBuilder( navigatorBarType: AdaptiveDestinationEnum.settings, diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index 8994063e47..a6e7cccc5d 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mixins/show_dialog_mixin.dart'; @@ -107,24 +108,32 @@ class TwakeHeader extends StatelessWidget padding: TwakeHeaderStyle.actionsPadding, child: Align( alignment: Alignment.centerRight, - child: InkWell( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - onTap: () => _displayMultipleAccountPicker(context), - child: ValueListenableBuilder( - valueListenable: controller.currentProfileNotifier, - builder: (context, profile, _) { - return Avatar( - mxContent: profile.avatarUrl, - name: profile.displayName ?? - Matrix.of(context).client.userID!.localpart, - size: TwakeHeaderStyle.avatarSize, - fontSize: TwakeHeaderStyle.avatarFontSizeInAppBar, - ); - }, - ), - ), + child: PlatformInfos.isMobile + ? InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: () => + _displayMultipleAccountPicker(context), + child: ValueListenableBuilder( + valueListenable: + controller.currentProfileNotifier, + builder: (context, profile, _) { + return Avatar( + mxContent: profile.avatarUrl, + name: profile.displayName ?? + Matrix.of(context) + .client + .userID! + .localpart, + size: TwakeHeaderStyle.avatarSize, + fontSize: + TwakeHeaderStyle.avatarFontSizeInAppBar, + ); + }, + ), + ) + : const SizedBox.shrink(), ), ), ), From 6ea34b9908ea4063d061ea26d264fb07d5007ed8 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 17:26:21 +0700 Subject: [PATCH 035/183] TW-1193: Handle set active account when add new account (cherry picked from commit 2f6107aeda3b08bdfc04f29d88612910cf3125e2) --- lib/config/go_routes/go_router.dart | 2 + lib/pages/chat_list/chat_list.dart | 83 +++++++---- lib/pages/chat_list/chat_list_body_view.dart | 15 +- .../app_adaptive_scaffold_body.dart | 4 + .../app_adaptive_scaffold_body_view.dart | 10 ++ lib/widgets/matrix.dart | 11 +- pubspec.lock | 137 +++++++++++------- 7 files changed, 173 insertions(+), 89 deletions(-) diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 3d21235325..2ce9c7515b 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -171,6 +171,8 @@ abstract class AppRoutes { ? const ChatBlank() : AppAdaptiveScaffoldBody( activeRoomId: state.pathParameters['roomid'], + client: + state.extra is Client? ? state.extra as Client? : null, ), name: '/rooms', ), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index e4e8131ade..d04de82db9 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -17,7 +17,6 @@ import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_security/settings_security.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; -import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/presentation/mixins/go_to_group_chat_mixin.dart'; import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; import 'package:fluffychat/presentation/multiple_account/profile_bundle.dart'; @@ -54,12 +53,15 @@ class ChatList extends StatefulWidget { final VoidCallback? onOpenSettings; + final Client? newClient; + const ChatList({ Key? key, required this.activeRoomIdNotifier, this.bottomNavigationBar, this.onOpenSearchPage, this.onOpenSettings, + this.newClient, }) : super(key: key); @override @@ -114,7 +116,7 @@ class ChatListController extends State bool scrolledToTop = true; - Client get client => matrixState.client; + Client get activeClient => matrixState.client; MatrixState get matrixState => Matrix.of(context); @@ -122,7 +124,8 @@ class ChatListController extends State ? ActiveFilter.messages : ActiveFilter.allChats; - List get _filteredRooms => client.filteredRoomsForAll(activeFilter); + List get _filteredRooms => + activeClient.filteredRoomsForAll(activeFilter); List get filteredRoomsForAll => _filteredRooms.where((room) => !room.isFavourite).toList(); @@ -135,7 +138,7 @@ class ChatListController extends State Stream get clientStream => _clientStream.stream; // Needs to match GroupsSpacesEntry for 'separate group' checking. - List get spaces => client.rooms.where((r) => r.isSpace).toList(); + List get spaces => activeClient.rooms.where((r) => r.isSpace).toList(); List get bundles => matrixState.accountBundles.keys.toList() ..sort( @@ -205,7 +208,7 @@ class ChatListController extends State } void editSpace(BuildContext context, String spaceId) async { - await client.getRoomById(spaceId)!.postLoad(); + await activeClient.getRoomById(spaceId)!.postLoad(); if (mounted) { context.go('/spaces/$spaceId'); } @@ -285,9 +288,11 @@ class ChatListController extends State future: () async { final markUnread = anySelectedRoomNotMarkedUnread; for (final conversation in conversationSelectionNotifier.value) { - final room = client.getRoomById(conversation.roomId)!; + final room = activeClient.getRoomById(conversation.roomId)!; if (room.markedUnread == markUnread) continue; - await client.getRoomById(conversation.roomId)!.markUnread(markUnread); + await activeClient + .getRoomById(conversation.roomId)! + .markUnread(markUnread); } }, ); @@ -298,9 +303,9 @@ class ChatListController extends State future: () async { final makeFavorite = anySelectedRoomNotFavorite; for (final conversation in conversationSelectionNotifier.value) { - final room = client.getRoomById(conversation.roomId)!; + final room = activeClient.getRoomById(conversation.roomId)!; if (room.isFavourite == makeFavorite) continue; - await client + await activeClient .getRoomById(conversation.roomId)! .setFavourite(makeFavorite); } @@ -312,9 +317,9 @@ class ChatListController extends State await TwakeDialog.showFutureLoadingDialogFullScreen( future: () async { for (final conversation in conversationSelectionNotifier.value) { - final room = client.getRoomById(conversation.roomId)!; + final room = activeClient.getRoomById(conversation.roomId)!; if (room.pushRuleState == pushRuleState) continue; - await client + await activeClient .getRoomById(conversation.roomId)! .setPushRuleState(pushRuleState); } @@ -353,8 +358,8 @@ class ChatListController extends State ); if (input == null) return; await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => client.setPresence( - client.userID!, + future: () => activeClient.setPresence( + activeClient.userID!, PresenceType.online, statusMsg: input.single, ), @@ -365,7 +370,7 @@ class ChatListController extends State while (conversationSelectionNotifier.value.isNotEmpty) { final conversation = conversationSelectionNotifier.value.first; try { - await client.getRoomById(conversation.roomId)!.leave(); + await activeClient.getRoomById(conversation.roomId)!.leave(); } finally { toggleSelection(conversation.roomId); } @@ -394,7 +399,7 @@ class ChatListController extends State if (selectedSpace == null) return; final result = await TwakeDialog.showFutureLoadingDialogFullScreen( future: () async { - final space = client.getRoomById(selectedSpace)!; + final space = activeClient.getRoomById(selectedSpace)!; if (space.canSendDefaultStates) { for (final conversation in conversationSelectionNotifier.value) { await space.setSpaceChild(conversation.roomId); @@ -419,34 +424,34 @@ class ChatListController extends State } Future _waitForFirstSync() async { - await client.roomsLoading; - await client.accountDataLoading; - if (client.userID != null) { - await setupAdditionalDioCacheOption(client.userID!); + await activeClient.roomsLoading; + await activeClient.accountDataLoading; + if (activeClient.userID != null) { + await setupAdditionalDioCacheOption(activeClient.userID!); } - if (client.prevBatch == null) { - await client.onSync.stream.first; - await client.initCompleter?.future; + if (activeClient.prevBatch == null) { + await activeClient.onSync.stream.first; + await activeClient.initCompleter?.future; // Display first login bootstrap if enabled - if (client.encryption?.keyManager.enabled == true) { + if (activeClient.encryption?.keyManager.enabled == true) { Logs().d( 'ChatList::_waitForFirstSync: Showing bootstrap dialog when encryption is enabled', ); - if (await client.encryption?.keyManager.isCached() == false || - await client.encryption?.crossSigning.isCached() == false || - client.isUnknownSession && mounted) { + if (await activeClient.encryption?.keyManager.isCached() == false || + await activeClient.encryption?.crossSigning.isCached() == false || + activeClient.isUnknownSession && mounted) { final recoveryWords = await _getRecoveryWords(); if (recoveryWords != null) { await TomBootstrapDialog( - client: client, + client: activeClient, recoveryWords: recoveryWords, ).show(); } else { Logs().d( 'ChatListController::_waitForFirstSync(): no recovery existed then call bootstrap', ); - await BootstrapDialog(client: client).show(); + await BootstrapDialog(client: activeClient).show(); } } } else { @@ -455,7 +460,7 @@ class ChatListController extends State ); final recoveryWords = await _getRecoveryWords(); await TomBootstrapDialog( - client: client, + client: activeClient, wipeRecovery: recoveryWords != null, ).show(); } @@ -818,6 +823,23 @@ class ChatListController extends State ); } + void initSetActiveClient() { + if (widget.newClient != null) { + setActiveClient(widget.newClient!); + } else { + _getCurrentProfile(activeClient); + } + } + + @override + void didUpdateWidget(covariant ChatList oldWidget) { + Logs().d("Chat::didUpdateWidget(): Client ${widget.newClient?.clientName}"); + if (widget.newClient != activeClient) { + initSetActiveClient(); + } + super.didUpdateWidget(oldWidget); + } + @override void initState() { if (kIsWeb) { @@ -825,9 +847,10 @@ class ChatListController extends State } activeRoomIdNotifier.value = widget.activeRoomIdNotifier.value; scrollController.addListener(_onScroll); + initSetActiveClient(); _waitForFirstSync(); _hackyWebRTCFixForWeb(); - _getCurrentProfile(client); + _getCurrentProfile(activeClient); // TODO: 28Dec2023 Disable callkeep for util we support audio/video calls // CallKeepManager().initialize(); WidgetsBinding.instance.addPostFrameCallback((_) async { diff --git a/lib/pages/chat_list/chat_list_body_view.dart b/lib/pages/chat_list/chat_list_body_view.dart index 4607502a00..adbc9d86b1 100644 --- a/lib/pages/chat_list/chat_list_body_view.dart +++ b/lib/pages/chat_list/chat_list_body_view.dart @@ -44,11 +44,11 @@ class ChatListBodyView extends StatelessWidget { child: SlidableAutoCloseBehavior( child: StreamBuilder( key: ValueKey( - controller.client.userID.toString() + + controller.activeClient.userID.toString() + controller.activeFilter.toString() + controller.activeSpaceId.toString(), ), - stream: controller.client.onSync.stream + stream: controller.activeClient.onSync.stream .where((s) => s.hasRoomUpdate) .rateLimit(const Duration(seconds: 1)), builder: (context, _) { @@ -60,7 +60,7 @@ class ChatListBodyView extends StatelessWidget { ); } if (controller.waitForFirstSync && - controller.client.prevBatch != null) { + controller.activeClient.prevBatch != null) { if (controller.chatListBodyIsEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.start, @@ -80,7 +80,7 @@ class ChatListBodyView extends StatelessWidget { Padding( padding: ChatListBodyViewStyle.paddingOwnProfile, child: FutureBuilder( - future: controller.client + future: controller.activeClient .fetchOwnProfile(getFromRooms: false), builder: (context, snapshotProfile) { if (snapshotProfile.connectionState != @@ -101,8 +101,7 @@ class ChatListBodyView extends StatelessWidget { .paddingTextStartNewChatMessage, child: Text( L10n.of(context)!.startNewChatMessage, - style: - Theme.of(context).textTheme.bodyMedium, + style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), ), @@ -153,8 +152,8 @@ class ChatListBodyView extends StatelessWidget { controller.filteredRoomsForPin.length, ), isExpanded: isExpanded, - onTap: controller - .expandRoomsForPinNotifier.toggle, + onTap: + controller.expandRoomsForPinNotifier.toggle, ), if (isExpanded) child!, ], diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index 27084ab14e..83a689e417 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; typedef OnOpenSearchPage = void Function(); typedef OnCloseSearchPage = void Function(); @@ -19,10 +20,12 @@ typedef OnPopInvoked = void Function(bool); class AppAdaptiveScaffoldBody extends StatefulWidget { final String? activeRoomId; + final Client? client; const AppAdaptiveScaffoldBody({ super.key, this.activeRoomId, + this.client, }); @override @@ -150,5 +153,6 @@ class AppAdaptiveScaffoldBodyController extends State { onClientSelected: clientSelected, onPopInvoked: _onPopInvoked, onOpenSettings: _onOpenSettingsPage, + client: widget.client, ); } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart index ca1d373feb..33427d4ce1 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart' hide WidgetBuilder; +import 'package:matrix/matrix.dart'; class AppAdaptiveScaffoldBodyView extends StatelessWidget { final List destinations; @@ -25,6 +26,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { final PageController pageController; final OnPopInvoked onPopInvoked; final VoidCallback onOpenSettings; + final Client? client; final ValueNotifier activeRoomIdNotifier; @@ -48,6 +50,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { required this.destinations, required this.onPopInvoked, required this.onOpenSettings, + this.client, }) : super(key: key ?? scaffoldWithNestedNavigationKey); @override @@ -127,6 +130,8 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { onClientSelected: onClientSelected, destinations: destinations, bottomNavigationKey: bottomNavigationKey, + onOpenSettings: onOpenSettings, + client: client, ); }, ); @@ -153,6 +158,8 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { onClientSelected: onClientSelected, destinations: destinations, bottomNavigationKey: bottomNavigationKey, + onOpenSettings: onOpenSettings, + client: client, ), ), ], @@ -182,6 +189,7 @@ class _ColumnPageView extends StatelessWidget { final ValueKey bottomNavigationKey; final ValueNotifier activeRoomIdNotifier; final VoidCallback onOpenSettings; + final Client? client; const _ColumnPageView({ required this.activeNavigationBarNotifier, @@ -194,6 +202,7 @@ class _ColumnPageView extends StatelessWidget { required this.destinations, required this.bottomNavigationKey, required this.onOpenSettings, + required this.client, }); @override @@ -216,6 +225,7 @@ class _ColumnPageView extends StatelessWidget { onOpenSearchPage: onOpenSearchPage, activeRoomIdNotifier: activeRoomIdNotifier, onOpenSettings: onOpenSettings, + newClient: client, ), _triggerPageViewBuilder( navigatorBarType: AdaptiveDestinationEnum.settings, diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index e9bbfdd10f..c312a4b04d 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -172,15 +172,22 @@ class MatrixState extends State .where((l) => l == LoginState.loggedIn) .first .then((_) { - Logs().d('MatrixState::getLoginClient() Login successful'); + Logs().d( + 'MatrixState::getLoginClient() Login successful - Client ${_loginClientCandidate!.clientName}', + ); if (!widget.clients.contains(_loginClientCandidate)) { widget.clients.add(_loginClientCandidate!); } ClientManager.addClientNameToStore(_loginClientCandidate!.clientName); Logs().d('MatrixState::getLoginClient() Registering subs'); _registerSubs(_loginClientCandidate!.clientName); + TwakeApp.router.go( + '/rooms', + extra: getClientByName( + _loginClientCandidate!.clientName, + ), + ); _loginClientCandidate = null; - TwakeApp.router.go('/rooms'); }); return candidate; } diff --git a/pubspec.lock b/pubspec.lock index 81a1652fd6..e7edcba808 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -832,10 +832,10 @@ packages: dependency: "direct main" description: name: flutter_keyboard_visibility - sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "6.0.0" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -986,6 +986,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.17" + flutter_portal: + dependency: "direct main" + description: + name: flutter_portal + sha256: "4601b3dc24f385b3761721bd852a3f6c09cddd4e943dd184ed58ee1f43006257" + url: "https://pub.dev" + source: hosted + version: "1.1.4" flutter_ringtone_player: dependency: "direct main" description: @@ -1043,6 +1051,14 @@ packages: url: "https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git" source: git version: "1.1.2" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + url: "https://pub.dev" + source: hosted + version: "3.1.0" flutter_svg: dependency: "direct main" description: @@ -1060,10 +1076,10 @@ packages: dependency: "direct main" description: name: flutter_typeahead - sha256: "1f6b248bb4f3ebb4cf1ee0354aa23c77be457fb2d26d6847ecc33a917f65e58e" + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.2.0" flutter_web_auth_2: dependency: "direct main" description: @@ -1122,14 +1138,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.4" - geolocator: - dependency: "direct main" - description: - name: geolocator - sha256: b8f520252c5c66851295bcc263bc8ae7555501938427f72216ba7688702e261d - url: "https://pub.dev" - source: hosted - version: "7.7.1" geolocator_android: dependency: "direct overridden" description: @@ -1138,14 +1146,6 @@ packages: url: "https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss/" source: hosted version: "1.0.1" - geolocator_apple: - dependency: transitive - description: - name: geolocator_apple - sha256: "1e8e398cc92151d946a4bbd34e2075885333e42d35ca33e418e7ce7b0a29991e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" geolocator_platform_interface: dependency: transitive description: @@ -1154,14 +1154,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" - geolocator_web: - dependency: transitive - description: - name: geolocator_web - sha256: "0b9e0ec13ce2211085cae0055b3516c975bd6cfe2878a20c8f13611f1a259855" - url: "https://pub.dev" - source: hosted - version: "2.0.6" get_it: dependency: "direct main" description: @@ -1399,6 +1391,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.1" + inview_notifier_list: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: f7a29ac1c4de3ea26d749c38781a6f5e9b1b932d + url: "git@github.com:linagora/inview_notifier_list.git" + source: git + version: "3.0.0" io: dependency: transitive description: @@ -1411,18 +1412,18 @@ packages: dependency: transitive description: name: irondash_engine_context - sha256: "8b8c58f2b6414b0db739b329ca08baf3b66d4fcfc3c315525ddac61639addb90" + sha256: "4f5e2629296430cce08cdff42e47cef07b8f74a64fdbdfb0525d147bc1a969a2" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.2" irondash_message_channel: dependency: transitive description: name: irondash_message_channel - sha256: "131d64d97a3612bc3617aefc878f3e3a8e23e0ce18b3bba8e78cb1930befcec1" + sha256: dd581214215dca054bd9873209d690ec3609288c28774cb509dbd86b21180cf8 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.6.0" isolate: dependency: transitive description: @@ -1508,8 +1509,8 @@ packages: dependency: "direct main" description: path: "." - ref: multiple-account-picker - resolved-ref: "908bb487d1073f3dd5727aaeb5ab3d9ddae4c0e4" + ref: master + resolved-ref: "7ed486ea00f7e91254555a4a828ff3a625b4e1ae" url: "git@github.com:linagora/linagora-design-flutter.git" source: git version: "0.0.1" @@ -1715,6 +1716,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" + source: hosted + version: "5.4.2" mpv_dart: dependency: transitive description: @@ -1983,10 +1992,34 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: adf7a637f97c077041d36801b43be08559fd4322d2127b3f20bb7be1b9eebc22 + sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a + url: "https://pub.dev" + source: hosted + version: "0.10.1" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: "2e73c39452830adc4695757130676a39412a3b7f3c34e3f752791b5384770877" + url: "https://pub.dev" + source: hosted + version: "0.10.0+2" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: "9386e064097fd16419e935c23f08f35b58e6aaec155dd39bd6a003b88f9c14b4" url: "https://pub.dev" source: hosted - version: "0.9.3+7" + version: "0.10.1+2" pointycastle: dependency: transitive description: @@ -2412,6 +2445,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + skeletonizer: + dependency: "direct main" + description: + name: skeletonizer + sha256: "2eb80153c80507359ff05f6a18ed50ae0bafa1b999aa867a8cef0a53387b5650" + url: "https://pub.dev" + source: hosted + version: "1.1.0" skeletons: dependency: "direct main" description: @@ -2516,21 +2557,19 @@ packages: super_clipboard: dependency: "direct main" description: - path: super_clipboard - ref: main - resolved-ref: fdaf87f6a30ac049d401967a8c5ec400126f288d - url: "https://github.com/superlistapp/super_native_extensions.git" - source: git - version: "0.8.1" + name: super_clipboard + sha256: "77f044320934386e0b7a3911e05312426d7f33deb6e8cdb28886663430b0e5b0" + url: "https://pub.dev" + source: hosted + version: "0.8.4" super_native_extensions: - dependency: "direct overridden" + dependency: transitive description: - path: super_native_extensions - ref: main - resolved-ref: fdaf87f6a30ac049d401967a8c5ec400126f288d - url: "https://github.com/superlistapp/super_native_extensions.git" - source: git - version: "0.8.1" + name: super_native_extensions + sha256: f96db6b137a0b135e43034289bb55ca6447b65225076036e81f97ebb6381ffeb + url: "https://pub.dev" + source: hosted + version: "0.8.5" sync_http: dependency: transitive description: From 2aa0cb7bf9493802e918dc29fa0171982b762246 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 23:21:44 +0700 Subject: [PATCH 036/183] TW-1193: Check only support one account on a single homeserver before login (cherry picked from commit 58b2956758c5477e7659090bcff294ddc81a96f6) --- assets/l10n/intl_en.arb | 4 ++-- devtools_options.yaml | 1 + lib/pages/connect/connect_page_mixin.dart | 11 +++++++++++ lib/pages/homeserver_picker/homeserver_picker.dart | 10 ++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 devtools_options.yaml diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 3074b311ab..73047ba7cb 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2864,6 +2864,8 @@ "@editProfileDescriptions": {}, "workIdentitiesInfo": "WORK IDENTITIES INFO", "@workIdentitiesInfo": {}, + "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name.", + "@editWorkIdentitiesDescriptions": {}, "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard.", "@copiedMatrixIdToClipboard": {}, "changeProfileAvatar": "Change profile avatar", @@ -2984,8 +2986,6 @@ "accountSettings": "Account settings", "failedToSendFiles": "Failed to send files", "noResults": "No Results", - "addAnotherAccount": "Add another account", - "accountSettings": "Account settings", "isSingleAccountOnHomeserver": "We do not yet support multiple accounts on a single homeserver", "messageSelected": "{count, plural, =0{No Messages} =1{1 Message} other{{count} Messages}} selected", "@messageSelected": { diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000000..7e7e7f67de --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/lib/pages/connect/connect_page_mixin.dart b/lib/pages/connect/connect_page_mixin.dart index 0471c680eb..80cd712865 100644 --- a/lib/pages/connect/connect_page_mixin.dart +++ b/lib/pages/connect/connect_page_mixin.dart @@ -190,4 +190,15 @@ mixin ConnectPageMixin { } return list; } + + bool isSingleAccountOnHomeserver( + MatrixState matrix, + Uri homeserver, + ) { + if (matrix.client.isLogged() && matrix.client.homeserver == homeserver) { + return true; + } else { + return false; + } + } } diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index a1b5052bab..c0c1c55453 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_state.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -145,6 +146,15 @@ class HomeserverPickerController extends State } final matrix = Matrix.of(context); + if (isSingleAccountOnHomeserver(matrix, homeserver)) { + TwakeSnackBar.show( + context, + L10n.of(context)!.isSingleAccountOnHomeserver, + ); + state = HomeserverState.wrongServerName; + return; + } + matrix.loginHomeserverSummary = await matrix.getLoginClient().checkHomeserver(homeserver); final ssoSupported = matrix.loginHomeserverSummary!.loginFlows From 375a5ba996b74261c7f7579b4ed3669b239a96c6 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 23:41:50 +0700 Subject: [PATCH 037/183] TW-1193: Add boolean twakeSupported property in Matrix (cherry picked from commit dae6add6f84439c3d0406572e5f01648216f86a2) --- lib/widgets/matrix.dart | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index c312a4b04d..8ca2d0f368 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -70,6 +70,8 @@ class Matrix extends StatefulWidget { class MatrixState extends State with WidgetsBindingObserver, ReceiveSharingIntentMixin { + final tomConfigurationRepository = getIt.get(); + int _activeClient = -1; String? activeBundle; Store store = Store(); @@ -79,6 +81,7 @@ class MatrixState extends State String? loginUsername; LoginType? loginType; bool? loginRegistrationSupported; + bool? _twakeSupported; BackgroundPush? backgroundPush; @@ -95,6 +98,8 @@ class MatrixState extends State // TODO: 28Dec2023 Disable until support voip bool get webrtcIsSupported => false; + bool get twakeIsSupported => _twakeSupported ?? false; + VoipPlugin? voipPlugin; bool get isMultiAccount => widget.clients.length > 1; @@ -105,14 +110,15 @@ class MatrixState extends State late String currentClientSecret; RequestTokenResponse? currentThreepidCreds; - void setActiveClient(Client? cl) { - final i = widget.clients.indexWhere((c) => c == cl); - if (i != -1) { - _activeClient = i; + void setActiveClient(Client? newClient) { + _checkHomeserverExists(newClient); + final index = widget.clients.indexWhere((client) => client == newClient); + if (index != -1) { + _activeClient = index; // TODO: Multi-client VoiP support createVoipPlugin(); } else { - Logs().w('Tried to set an unknown client ${cl!.userID} as active'); + Logs().w('Tried to set an unknown client ${newClient!.userID} as active'); } } @@ -480,8 +486,6 @@ class MatrixState extends State void _retrieveLocalToMConfiguration() async { try { - final tomConfigurationRepository = - getIt.get(); final toMConfigurations = await tomConfigurationRepository .getTomConfigurations(client.clientName); setUpToMServices( @@ -490,6 +494,7 @@ class MatrixState extends State ); authUrl = toMConfigurations.authUrl; loginType = toMConfigurations.loginType; + setTakeSupported(supported: true); } catch (e) { Logs().e('MatrixState::_retrieveToMConfiguration: $e'); } @@ -603,6 +608,23 @@ class MatrixState extends State } } + void setTakeSupported({ + required bool supported, + }) { + _twakeSupported = supported; + } + + void _checkHomeserverExists(Client? client) async { + if (client == null) return; + try { + await tomConfigurationRepository.getTomConfigurations(client.clientName); + setTakeSupported(supported: true); + } catch (e) { + setTakeSupported(supported: false); + Logs().e('Matrix::_checkHomeserverExists: error - $e'); + } + } + void onWindowFocus(html.Event e) { didChangeAppLifecycleState(AppLifecycleState.resumed); } From bfba835251ab907d8f5bbd4a886303856f754a1d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Dec 2023 23:52:01 +0700 Subject: [PATCH 038/183] TW-1193: Update configuration for TomConfigurationsBox (cherry picked from commit 897b88a1a92fda89cb704db813bd560b40007614) --- .../datasource/tom_configurations_datasource.dart | 4 ++-- .../tom_configurations_datasource_impl.dart | 8 ++++---- .../tom_configurations_repository_impl.dart | 8 ++++---- .../repository/tom_configurations_repository.dart | 4 ++-- lib/pages/chat_list/chat_list.dart | 4 +++- lib/widgets/matrix.dart | 15 ++++++++++----- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lib/data/datasource/tom_configurations_datasource.dart b/lib/data/datasource/tom_configurations_datasource.dart index eb30b66442..a39ae18400 100644 --- a/lib/data/datasource/tom_configurations_datasource.dart +++ b/lib/data/datasource/tom_configurations_datasource.dart @@ -1,10 +1,10 @@ import 'package:fluffychat/domain/model/tom_configurations.dart'; abstract class ToMConfigurationsDatasource { - Future getTomConfigurations(String clientName); + Future getTomConfigurations(String userId); Future saveTomConfigurations( - String clientName, + String userId, ToMConfigurations toMConfigurations, ); } diff --git a/lib/data/datasource_impl/tom_configurations_datasource_impl.dart b/lib/data/datasource_impl/tom_configurations_datasource_impl.dart index 35b4d5c2b3..b6914654ed 100644 --- a/lib/data/datasource_impl/tom_configurations_datasource_impl.dart +++ b/lib/data/datasource_impl/tom_configurations_datasource_impl.dart @@ -8,11 +8,11 @@ import 'package:matrix/matrix.dart'; class HiveToMConfigurationDatasource implements ToMConfigurationsDatasource { @override - Future getTomConfigurations(String clientName) async { + Future getTomConfigurations(String userId) async { final hiveCollectionToMDatabase = await getIt.getAsync(); final cachedConfiguration = - await hiveCollectionToMDatabase.tomConfigurationsBox.get(clientName); + await hiveCollectionToMDatabase.tomConfigurationsBox.get(userId); if (cachedConfiguration != null) { final toMConfigurationsHiveObj = ToMConfigurationsHiveObj.fromJson(copyMap(cachedConfiguration)); @@ -35,13 +35,13 @@ class HiveToMConfigurationDatasource implements ToMConfigurationsDatasource { @override Future saveTomConfigurations( - String clientName, + String userId, ToMConfigurations toMConfigurations, ) async { final hiveCollectionToMDatabase = await getIt.getAsync(); return hiveCollectionToMDatabase.tomConfigurationsBox.put( - clientName, + userId, ToMConfigurationsHiveObj.fromToMConfigurations(toMConfigurations) .toJson(), ); diff --git a/lib/data/repository/tom_configurations_repository_impl.dart b/lib/data/repository/tom_configurations_repository_impl.dart index d47ed9fe4c..4a8bcdc115 100644 --- a/lib/data/repository/tom_configurations_repository_impl.dart +++ b/lib/data/repository/tom_configurations_repository_impl.dart @@ -8,17 +8,17 @@ class ToMConfigurationsRepositoryImpl implements ToMConfigurationsRepository { getIt.get(); @override - Future getTomConfigurations(String clientName) { - return tomConfigurationsDatasource.getTomConfigurations(clientName); + Future getTomConfigurations(String userId) { + return tomConfigurationsDatasource.getTomConfigurations(userId); } @override Future saveTomConfigurations( - String clientName, + String userId, ToMConfigurations toMConfigurations, ) { return tomConfigurationsDatasource.saveTomConfigurations( - clientName, + userId, toMConfigurations, ); } diff --git a/lib/domain/repository/tom_configurations_repository.dart b/lib/domain/repository/tom_configurations_repository.dart index 5a99887a8d..673f1ee6ce 100644 --- a/lib/domain/repository/tom_configurations_repository.dart +++ b/lib/domain/repository/tom_configurations_repository.dart @@ -1,10 +1,10 @@ import 'package:fluffychat/domain/model/tom_configurations.dart'; abstract class ToMConfigurationsRepository { - Future getTomConfigurations(String clientName); + Future getTomConfigurations(String userId); Future saveTomConfigurations( - String clientName, + String userId, ToMConfigurations toMConfigurations, ); } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index d04de82db9..c5b67103d4 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -833,7 +833,9 @@ class ChatListController extends State @override void didUpdateWidget(covariant ChatList oldWidget) { - Logs().d("Chat::didUpdateWidget(): Client ${widget.newClient?.clientName}"); + Logs().d( + "ChatList::didUpdateWidget(): Client ${widget.newClient?.clientName}", + ); if (widget.newClient != activeClient) { initSetActiveClient(); } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 8ca2d0f368..348d6a347c 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -485,9 +485,10 @@ class MatrixState extends State } void _retrieveLocalToMConfiguration() async { + if (client.userID == null) return; try { - final toMConfigurations = await tomConfigurationRepository - .getTomConfigurations(client.clientName); + final toMConfigurations = + await tomConfigurationRepository.getTomConfigurations(client.userID!); setUpToMServices( toMConfigurations.tomServerInformation, toMConfigurations.identityServerInformation, @@ -594,10 +595,14 @@ class MatrixState extends State Logs().e( 'Matrix::_storeToMConfiguration: clientName - ${client.clientName}', ); + Logs().e( + 'Matrix::_storeToMConfiguration: userId - ${client.userID}', + ); + if (client.userID == null) return; final ToMConfigurationsRepository configurationRepository = getIt.get(); configurationRepository.saveTomConfigurations( - client.clientName, + client.userID!, config, ); Logs().e( @@ -615,9 +620,9 @@ class MatrixState extends State } void _checkHomeserverExists(Client? client) async { - if (client == null) return; + if (client == null && client?.userID == null) return; try { - await tomConfigurationRepository.getTomConfigurations(client.clientName); + await tomConfigurationRepository.getTomConfigurations(client!.userID!); setTakeSupported(supported: true); } catch (e) { setTakeSupported(supported: false); From 013b67e65c5ab5005917bcf6e57711b2713c143a Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Dec 2023 01:07:17 +0700 Subject: [PATCH 039/183] TW-1193: Migrating and clear all old version (cherry picked from commit e4a20c8edff495916527c440a307cf36608f324d) --- .../hive/hive_collection_tom_database.dart | 4 ++ lib/di/global/get_it_initializer.dart | 19 ++++++-- lib/main.dart | 12 ++++- lib/utils/client_manager.dart | 7 --- lib/utils/famedlysdk_store.dart | 10 ++++ .../flutter_hive_collections_database.dart | 14 ++++++ lib/widgets/matrix.dart | 46 +++++++++++++++++-- pubspec.lock | 4 +- 8 files changed, 96 insertions(+), 20 deletions(-) diff --git a/lib/data/hive/hive_collection_tom_database.dart b/lib/data/hive/hive_collection_tom_database.dart index c60c79c703..89e8c81b13 100644 --- a/lib/data/hive/hive_collection_tom_database.dart +++ b/lib/data/hive/hive_collection_tom_database.dart @@ -130,4 +130,8 @@ class HiveCollectionToMDatabase { await _collection.deleteFromDisk(); } } + + Future clearCache() async { + await tomConfigurationsBox.clear(); + } } diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index fb8c4c6c49..98f3b4b149 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -86,6 +86,7 @@ import 'package:fluffychat/utils/manager/download_manager/downloading_worker_que import 'package:fluffychat/utils/power_level_manager.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:get_it/get_it.dart'; +import 'package:matrix/matrix.dart'; final getIt = GetIt.instance; @@ -98,7 +99,7 @@ class GetItInitializer { GetItInitializer._internal(); - void setUp() { + void setUp() async { bindingGlobal(); bindingQueue(); bindingAPI(); @@ -108,19 +109,29 @@ class GetItInitializer { bindingRepositories(); bindingInteractor(); _bindingControllers(); + Logs().d('GetItInitializer::setUp(): Setup successfully'); } void bindingGlobal() { + HiveDI().bind(); setupDioCache(); NetworkDI().bind(); - HiveDI().bind(); NetworkConnectivityDI().bind(); getIt.registerSingleton(ResponsiveUtils()); - getIt.registerSingleton(PowerLevelManager()); getIt.registerSingleton(TwakeEventDispatcher()); getIt.registerSingleton(Store()); - getIt.registerFactory(() => LanguageCacheManager()); getIt.registerFactory(() => AppConfigLoader()); + bindingCachingManager(); + } + + void bindingCachingManager() { + getIt.registerSingleton(PowerLevelManager()); + getIt.registerFactory( + () => MultipleAccountCacheManager(), + ); + getIt.registerFactory( + () => LanguageCacheManager(), + ); } void bindingQueue() { diff --git a/lib/main.dart b/lib/main.dart index f25be10513..835cab8e27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,8 +8,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_app_lock/flutter_app_lock.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:go_router/go_router.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:media_kit/media_kit.dart'; +import 'package:path_provider/path_provider.dart'; import 'utils/background_push.dart'; import 'widgets/twake_app.dart'; import 'widgets/lock_screen.dart'; @@ -21,6 +23,14 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); GoRouter.optionURLReflectsImperativeAPIs = true; + if (PlatformInfos.isLinux) { + Hive.init((await getApplicationSupportDirectory()).path); + } else { + await Hive.initFlutter(); + } + + GetItInitializer().setUp(); + Logs().nativeColors = !PlatformInfos.isIOS; final clients = await ClientManager.getClients(); // Preload first client @@ -29,8 +39,6 @@ void main() async { await firstClient?.roomsLoading; await firstClient?.accountDataLoading; - GetItInitializer().setUp(); - // If the app starts in detached mode, we assume that it is in // background fetch mode for processing push notifications. This is // currently only supported on Android. diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 12d97fb7ec..7b9cf5418c 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -2,10 +2,8 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; -import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:fluffychat/utils/custom_http_client.dart'; import 'package:fluffychat/utils/custom_image_resizer.dart'; @@ -16,11 +14,6 @@ import 'famedlysdk_store.dart'; abstract class ClientManager { static const String clientNamespace = 'im.fluffychat.store.clients'; static Future> getClients({bool initialize = true}) async { - if (PlatformInfos.isLinux) { - Hive.init((await getApplicationSupportDirectory()).path); - } else { - await Hive.initFlutter(); - } final clientNames = {}; try { final rawClientNames = await Store().getItem(clientNamespace); diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 2f16b5963c..5c643274a1 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -40,4 +40,14 @@ class Store { await _prefs!.remove(key); return; } + + Future getInt(String key) async { + await _setupLocalStorage(); + return _prefs!.getInt(key); + } + + Future setInt(String key, int value) async { + await _setupLocalStorage(); + return _prefs!.setInt(key, value); + } } diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart index a91eeac942..1f61a0424f 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -19,14 +19,18 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { String name, String path, { HiveCipher? key, + StartMigrationProcess? startMigrationProcess, }) : super( name, path, key: key, + startMigrationProcess: startMigrationProcess, ); static const String cipherStorageKey = 'hive_encryption_key'; + static bool canMigrateToMDatabase = false; + static Future databaseBuilder( Client client, ) async { @@ -73,6 +77,15 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { 'hive_collections_${client.clientName.replaceAll(' ', '_').toLowerCase()}', await _findDatabasePath(client), key: hiverCipher, + startMigrationProcess: (currentVersion, newVersion) async { + Logs().d( + 'FlutterHiveCollectionsDatabase::startMigrationProcess() Starting migration process', + ); + Logs().d( + 'FlutterHiveCollectionsDatabase::startMigrationProcess() CurrentVersion - $currentVersion || NewVersion - $newVersion', + ); + canMigrateToMDatabase = true; + }, ); try { Logs().i('FlutterHiveCollectionsDatabase()::databaseBuilder()::open()'); @@ -119,6 +132,7 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { @override int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0; + @override bool get supportsFileStoring => !kIsWeb; diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 348d6a347c..d7f09b7e12 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -18,6 +18,7 @@ import 'package:fluffychat/domain/repository/tom_configurations_repository.dart' import 'package:fluffychat/pages/chat_list/receive_sharing_intent_mixin.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; @@ -283,6 +284,7 @@ class MatrixState extends State void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + _migrateToMDatabase(client); if (PlatformInfos.isWeb) { html.window.addEventListener('focus', onWindowFocus); html.window.addEventListener('blur', onWindowBlur); @@ -327,9 +329,6 @@ class MatrixState extends State ); return; } - if (PlatformInfos.isMobile) { - await HiveCollectionToMDatabase.databaseBuilder(); - } onRoomKeyRequestSub[name] ??= c.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async { if (widget.clients.any( @@ -484,11 +483,24 @@ class MatrixState extends State voipPlugin = webrtcIsSupported ? VoipPlugin(client) : null; } + Future getTomConfigurations(String userID) async { + try { + final tomConfigurationRepository = + getIt.get(); + final toMConfigurations = + await tomConfigurationRepository.getTomConfigurations(userID); + return toMConfigurations; + } catch (e) { + Logs().e('MatrixState::_getTomConfigurations: $e'); + } + return null; + } + void _retrieveLocalToMConfiguration() async { if (client.userID == null) return; try { - final toMConfigurations = - await tomConfigurationRepository.getTomConfigurations(client.userID!); + final toMConfigurations = await getTomConfigurations(client.userID!); + if (toMConfigurations == null) return; setUpToMServices( toMConfigurations.tomServerInformation, toMConfigurations.identityServerInformation, @@ -617,9 +629,15 @@ class MatrixState extends State required bool supported, }) { _twakeSupported = supported; + Logs().d( + 'Matrix::setTakeSupported: _twakeSupported - $_twakeSupported', + ); } void _checkHomeserverExists(Client? client) async { + Logs().d( + 'Matrix::_checkHomeserverExists: _twakeSupported - $_twakeSupported', + ); if (client == null && client?.userID == null) return; try { await tomConfigurationRepository.getTomConfigurations(client!.userID!); @@ -630,6 +648,24 @@ class MatrixState extends State } } + void _migrateToMDatabase(Client client) async { + if (!FlutterHiveCollectionsDatabase.canMigrateToMDatabase) return; + Logs().d( + 'Matrix::_checkHomeserverExists: Start migration to ToMDatabase', + ); + if (client.userID == null) return; + final hiveCollectionToMDatabase = + await getIt.getAsync(); + final currentToMConfigurations = await getTomConfigurations(client.userID!); + if (currentToMConfigurations != null) { + await hiveCollectionToMDatabase.clearCache(); + _storeToMConfiguration(client, currentToMConfigurations); + } + Logs().d( + 'Matrix::_checkHomeserverExists: Finish migration to ToMDatabase', + ); + } + void onWindowFocus(html.Event e) { didChangeAppLifecycleState(AppLifecycleState.resumed); } diff --git a/pubspec.lock b/pubspec.lock index e7edcba808..d2d1cbb1b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1590,8 +1590,8 @@ packages: dependency: "direct main" description: path: "." - ref: "twake-supported-0.22.6" - resolved-ref: e5e67b2d0158b9398eb6430841c2bed7f61d8997 + ref: add-callback-migrating-database + resolved-ref: "10d717db2f368bbafb035290a4bc7df558c2e3df" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" From d3e1466230b2383ef3eeaab8e6acfcd559442380 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Dec 2023 01:38:53 +0700 Subject: [PATCH 040/183] TW-1193: Logout account and try logoutSSO (cherry picked from commit 7711ac7fbf288f379ba5ab9d0ad2c66830573499) --- lib/config/go_routes/go_router.dart | 18 +++++-- lib/pages/chat_list/chat_list.dart | 5 +- .../settings_dashboard/settings/settings.dart | 8 ++-- .../app_adaptive_scaffold_body.dart | 48 ++++++++++++++++--- lib/widgets/matrix.dart | 14 ++++-- 5 files changed, 75 insertions(+), 18 deletions(-) diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 2ce9c7515b..b699acab7a 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -155,7 +155,9 @@ abstract class AppRoutes { state.fullPath?.startsWith('/rooms/settings') == false ? AppAdaptiveScaffold( body: AppAdaptiveScaffoldBody( - activeRoomId: state.pathParameters['roomid'], + args: AppAdaptiveScaffoldBodyArgs( + activeRoomId: state.pathParameters['roomid'], + ), ), secondaryBody: child, ) @@ -170,9 +172,17 @@ abstract class AppRoutes { !_responsive.isMobile(context) ? const ChatBlank() : AppAdaptiveScaffoldBody( - activeRoomId: state.pathParameters['roomid'], - client: - state.extra is Client? ? state.extra as Client? : null, + args: AppAdaptiveScaffoldBodyArgs( + activeRoomId: state.pathParameters['roomid'], + client: state.extra is AppAdaptiveScaffoldBodyArgs + ? (state.extra as AppAdaptiveScaffoldBodyArgs).client + : null, + isLogoutMultipleAccount: + state.extra is AppAdaptiveScaffoldBodyArgs + ? (state.extra as AppAdaptiveScaffoldBodyArgs) + .isLogoutMultipleAccount + : false, + ), ), name: '/rooms', ), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index c5b67103d4..eefea4818f 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -834,7 +834,10 @@ class ChatListController extends State @override void didUpdateWidget(covariant ChatList oldWidget) { Logs().d( - "ChatList::didUpdateWidget(): Client ${widget.newClient?.clientName}", + "ChatList::didUpdateWidget(): OldClient $activeClient", + ); + Logs().d( + "ChatList::didUpdateWidget(): NewClient ${widget.newClient?.clientName}", ); if (widget.newClient != activeClient) { initSetActiveClient(); diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index c04758150a..7ecedb0266 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -82,12 +82,12 @@ class SettingsController extends State with ConnectPageMixin { OkCancelResult.cancel) { return; } - if (PlatformInfos.isMobile) { + final matrix = Matrix.of(context); + if (matrix.twakeIsSupported) { await tryLogoutSso(context); + final hiveCollectionToMDatabase = getIt.get(); + await hiveCollectionToMDatabase.clear(); } - final hiveCollectionToMDatabase = getIt.get(); - await hiveCollectionToMDatabase.clear(); - final matrix = Matrix.of(context); await TwakeDialog.showFutureLoadingDialogFullScreen( future: () async { try { diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index 83a689e417..2dca50cd55 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:fluffychat/config/first_column_inner_routes.dart'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; @@ -18,14 +19,31 @@ typedef OnClientSelectedSetting = void Function( typedef OnDestinationSelected = void Function(int index); typedef OnPopInvoked = void Function(bool); -class AppAdaptiveScaffoldBody extends StatefulWidget { +class AppAdaptiveScaffoldBodyArgs extends Equatable { final String? activeRoomId; final Client? client; + final bool isLogoutMultipleAccount; - const AppAdaptiveScaffoldBody({ - super.key, + const AppAdaptiveScaffoldBodyArgs({ this.activeRoomId, this.client, + this.isLogoutMultipleAccount = false, + }); + + @override + List get props => [ + activeRoomId, + client, + isLogoutMultipleAccount, + ]; +} + +class AppAdaptiveScaffoldBody extends StatefulWidget { + final AppAdaptiveScaffoldBodyArgs args; + + const AppAdaptiveScaffoldBody({ + super.key, + required this.args, }); @override @@ -119,17 +137,35 @@ class AppAdaptiveScaffoldBodyController extends State { } } + void _onLogoutMultipleAccountSuccess( + covariant AppAdaptiveScaffoldBody oldWidget, + ) { + Logs().d( + 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():oldWidget.isLogoutMultipleAccount: ${oldWidget.args.isLogoutMultipleAccount}', + ); + Logs().d( + 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():newIsLogoutMultipleAccount: ${widget.args.isLogoutMultipleAccount}', + ); + if (oldWidget.args.isLogoutMultipleAccount != + widget.args.isLogoutMultipleAccount && + widget.args.isLogoutMultipleAccount) { + activeNavigationBar.value = AdaptiveDestinationEnum.rooms; + pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); + } + } + MatrixState get matrix => Matrix.of(context); @override void initState() { - activeRoomIdNotifier.value = widget.activeRoomId; + activeRoomIdNotifier.value = widget.args.activeRoomId; super.initState(); } @override void didUpdateWidget(covariant AppAdaptiveScaffoldBody oldWidget) { - activeRoomIdNotifier.value = widget.activeRoomId; + activeRoomIdNotifier.value = widget.args.activeRoomId; + _onLogoutMultipleAccountSuccess(oldWidget); super.didUpdateWidget(oldWidget); } @@ -153,6 +189,6 @@ class AppAdaptiveScaffoldBodyController extends State { onClientSelected: clientSelected, onPopInvoked: _onPopInvoked, onOpenSettings: _onOpenSettingsPage, - client: widget.client, + client: widget.args.client, ); } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index d7f09b7e12..b2f22837d7 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -24,6 +24,7 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; +import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -190,8 +191,10 @@ class MatrixState extends State _registerSubs(_loginClientCandidate!.clientName); TwakeApp.router.go( '/rooms', - extra: getClientByName( - _loginClientCandidate!.clientName, + extra: AppAdaptiveScaffoldBodyArgs( + client: getClientByName( + _loginClientCandidate!.clientName, + ), ), ); _loginClientCandidate = null; @@ -370,7 +373,12 @@ class MatrixState extends State ); if (state != LoginState.loggedIn) { - TwakeApp.router.go('/rooms'); + TwakeApp.router.go( + '/rooms', + extra: const AppAdaptiveScaffoldBodyArgs( + isLogoutMultipleAccount: true, + ), + ); } } else { if (state == LoginState.loggedIn) { From d73b9a15ddc066fd74c51a6266ef51168887b01f Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Dec 2023 13:39:42 +0700 Subject: [PATCH 041/183] TW-1193: Store/get/delete active account in persistent storage (cherry picked from commit 1a95832d1b9d0b77ff273d365c911a75408afcc9) --- devtools_options.yaml | 1 - lib/config/app_config.dart | 1 + lib/config/go_routes/go_router.dart | 23 +- .../multiple_account_datasource.dart | 7 + .../multiple_account_datasource_impl.dart | 23 ++ .../hive/hive_collection_tom_database.dart | 4 - .../multiple_account_cache_manager.dart | 20 + .../multiple_account_repository_impl.dart | 23 ++ lib/di/global/get_it_initializer.dart | 13 +- .../multiple_account_repository.dart | 7 + lib/migrate_steps/migrate_steps.dart | 5 + .../migrate_v6_to_v7/migrate_v6_to_v7.dart | 25 ++ lib/pages/chat_list/chat_list.dart | 98 +---- lib/pages/chat_list/chat_list_header.dart | 1 - lib/pages/chat_list/chat_list_view.dart | 15 +- .../chat_list/client_chooser_button.dart | 384 ------------------ lib/pages/connect/connect_page_mixin.dart | 11 - .../homeserver_picker/homeserver_picker.dart | 6 +- .../multiple_accounts_picker.dart | 140 +++++++ .../settings_dashboard/settings/settings.dart | 2 +- lib/pages/twake_id/twake_id.dart | 10 +- .../enum/settings/settings_action_enum.dart | 8 + .../client_profile_extension.dart | 24 ++ ....dart => client_profile_presentation.dart} | 10 +- lib/utils/dialog/twake_dialog.dart | 3 +- lib/utils/famedlysdk_store.dart | 4 +- .../flutter_hive_collections_database.dart | 29 +- .../app_adaptive_scaffold_body.dart | 48 +-- .../app_adaptive_scaffold_body_view.dart | 17 +- .../app_adaptive_scaffold_body_args.dart | 14 + .../agruments/logged_in_body_args.dart | 12 + .../layouts/agruments/logout_body_args.dart | 12 + .../switch_active_account_body_args.dart | 12 + lib/widgets/matrix.dart | 206 +++++++--- lib/widgets/set_active_client_state.dart | 6 + .../twake_components/twake_header.dart | 73 +--- 36 files changed, 596 insertions(+), 701 deletions(-) delete mode 100644 devtools_options.yaml create mode 100644 lib/data/datasource/multiple_account/multiple_account_datasource.dart create mode 100644 lib/data/datasource_impl/multiple_account/multiple_account_datasource_impl.dart create mode 100644 lib/data/local/multiple_account/multiple_account_cache_manager.dart create mode 100644 lib/data/repository/multiple_account/multiple_account_repository_impl.dart create mode 100644 lib/domain/repository/multiple_account/multiple_account_repository.dart create mode 100644 lib/migrate_steps/migrate_steps.dart create mode 100644 lib/migrate_steps/migrate_v6_to_v7/migrate_v6_to_v7.dart delete mode 100644 lib/pages/chat_list/client_chooser_button.dart create mode 100644 lib/pages/multiple_accounts/multiple_accounts_picker.dart create mode 100644 lib/presentation/enum/settings/settings_action_enum.dart create mode 100644 lib/presentation/extensions/multiple_accounts/client_profile_extension.dart rename lib/presentation/multiple_account/{profile_bundle.dart => client_profile_presentation.dart} (54%) create mode 100644 lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart create mode 100644 lib/widgets/layouts/agruments/logged_in_body_args.dart create mode 100644 lib/widgets/layouts/agruments/logout_body_args.dart create mode 100644 lib/widgets/layouts/agruments/switch_active_account_body_args.dart create mode 100644 lib/widgets/set_active_client_state.dart diff --git a/devtools_options.yaml b/devtools_options.yaml deleted file mode 100644 index 7e7e7f67de..0000000000 --- a/devtools_options.yaml +++ /dev/null @@ -1 +0,0 @@ -extensions: diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 552b6b1d41..65146afe11 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -94,6 +94,7 @@ abstract class AppConfig { static const String iOSKeychainSharingId = 'KUT463DS29.app.twake.ios.chat'; static const String iOSKeychainSharingAccount = 'app.twake.ios.chat.sessions'; static const int maxFilesSendPerDialog = 6; + static const bool supportMultipleAccountsInTheSameHomeserver = false; static String? issueId; diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index b699acab7a..4d77f4d177 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -43,6 +43,7 @@ import 'package:fluffychat/pages/settings_dashboard/settings_security/settings_s import 'package:fluffychat/pages/settings_dashboard/settings_stories/settings_stories.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_style/settings_style.dart'; import 'package:fluffychat/pages/sign_up/signup.dart'; +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; import 'package:fluffychat/widgets/log_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; @@ -155,9 +156,10 @@ abstract class AppRoutes { state.fullPath?.startsWith('/rooms/settings') == false ? AppAdaptiveScaffold( body: AppAdaptiveScaffoldBody( - args: AppAdaptiveScaffoldBodyArgs( - activeRoomId: state.pathParameters['roomid'], - ), + activeRoomId: state.pathParameters['roomid'], + args: state.extra is AbsAppAdaptiveScaffoldBodyArgs + ? state.extra as AbsAppAdaptiveScaffoldBodyArgs + : null, ), secondaryBody: child, ) @@ -172,17 +174,10 @@ abstract class AppRoutes { !_responsive.isMobile(context) ? const ChatBlank() : AppAdaptiveScaffoldBody( - args: AppAdaptiveScaffoldBodyArgs( - activeRoomId: state.pathParameters['roomid'], - client: state.extra is AppAdaptiveScaffoldBodyArgs - ? (state.extra as AppAdaptiveScaffoldBodyArgs).client - : null, - isLogoutMultipleAccount: - state.extra is AppAdaptiveScaffoldBodyArgs - ? (state.extra as AppAdaptiveScaffoldBodyArgs) - .isLogoutMultipleAccount - : false, - ), + activeRoomId: state.pathParameters['roomid'], + args: state.extra is AbsAppAdaptiveScaffoldBodyArgs + ? state.extra as AbsAppAdaptiveScaffoldBodyArgs + : null, ), name: '/rooms', ), diff --git a/lib/data/datasource/multiple_account/multiple_account_datasource.dart b/lib/data/datasource/multiple_account/multiple_account_datasource.dart new file mode 100644 index 0000000000..890457120a --- /dev/null +++ b/lib/data/datasource/multiple_account/multiple_account_datasource.dart @@ -0,0 +1,7 @@ +abstract class MultipleAccountDatasource { + Future storePersistActiveAccount(String userId); + + Future getPersistActiveAccount(); + + Future deletePersistActiveAccount(); +} diff --git a/lib/data/datasource_impl/multiple_account/multiple_account_datasource_impl.dart b/lib/data/datasource_impl/multiple_account/multiple_account_datasource_impl.dart new file mode 100644 index 0000000000..2e67173c68 --- /dev/null +++ b/lib/data/datasource_impl/multiple_account/multiple_account_datasource_impl.dart @@ -0,0 +1,23 @@ +import 'package:fluffychat/data/datasource/multiple_account/multiple_account_datasource.dart'; +import 'package:fluffychat/data/local/multiple_account/multiple_account_cache_manager.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; + +class MultipleAccountDatasourceImpl implements MultipleAccountDatasource { + final MultipleAccountCacheManager _multipleAccountCacheManager = + getIt.get(); + + @override + Future getPersistActiveAccount() { + return _multipleAccountCacheManager.getPersistActiveAccount(); + } + + @override + Future storePersistActiveAccount(String userId) { + return _multipleAccountCacheManager.storePersistActiveAccount(userId); + } + + @override + Future deletePersistActiveAccount() { + return _multipleAccountCacheManager.deletePersistActiveAccount(); + } +} diff --git a/lib/data/hive/hive_collection_tom_database.dart b/lib/data/hive/hive_collection_tom_database.dart index 89e8c81b13..c60c79c703 100644 --- a/lib/data/hive/hive_collection_tom_database.dart +++ b/lib/data/hive/hive_collection_tom_database.dart @@ -130,8 +130,4 @@ class HiveCollectionToMDatabase { await _collection.deleteFromDisk(); } } - - Future clearCache() async { - await tomConfigurationsBox.clear(); - } } diff --git a/lib/data/local/multiple_account/multiple_account_cache_manager.dart b/lib/data/local/multiple_account/multiple_account_cache_manager.dart new file mode 100644 index 0000000000..2310b4d6be --- /dev/null +++ b/lib/data/local/multiple_account/multiple_account_cache_manager.dart @@ -0,0 +1,20 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/famedlysdk_store.dart'; + +class MultipleAccountCacheManager { + final pres = getIt.get(); + + static const String persistActiveAccountKey = 'persist_active_account_key'; + + Future storePersistActiveAccount(String userId) async { + await pres.setItem(persistActiveAccountKey, userId); + } + + Future getPersistActiveAccount() async { + return await pres.getItem(persistActiveAccountKey); + } + + Future deletePersistActiveAccount() async { + await pres.deleteItem(persistActiveAccountKey); + } +} diff --git a/lib/data/repository/multiple_account/multiple_account_repository_impl.dart b/lib/data/repository/multiple_account/multiple_account_repository_impl.dart new file mode 100644 index 0000000000..7bd1daffe2 --- /dev/null +++ b/lib/data/repository/multiple_account/multiple_account_repository_impl.dart @@ -0,0 +1,23 @@ +import 'package:fluffychat/data/datasource/multiple_account/multiple_account_datasource.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/repository/multiple_account/multiple_account_repository.dart'; + +class MultipleAccountRepositoryImpl extends MultipleAccountRepository { + final MultipleAccountDatasource _multipleAccountDatasource = + getIt.get(); + + @override + Future getPersistActiveAccount() { + return _multipleAccountDatasource.getPersistActiveAccount(); + } + + @override + Future storePersistActiveAccount(String userId) { + return _multipleAccountDatasource.storePersistActiveAccount(userId); + } + + @override + Future deletePersistActiveAccount() { + return _multipleAccountDatasource.deletePersistActiveAccount(); + } +} diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 98f3b4b149..64ea8bd0ba 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/config/app_grid_config/app_config_loader.dart'; import 'package:fluffychat/data/datasource/localizations/localizations_datasource.dart'; import 'package:fluffychat/data/datasource/lookup_datasource.dart'; import 'package:fluffychat/data/datasource/media/media_data_source.dart'; +import 'package:fluffychat/data/datasource/multiple_account/multiple_account_datasource.dart'; import 'package:fluffychat/data/datasource/phonebook_datasouce.dart'; import 'package:fluffychat/data/datasource/recovery_words_data_source.dart'; import 'package:fluffychat/data/datasource/server_config_datasource.dart'; @@ -15,11 +16,13 @@ import 'package:fluffychat/data/datasource_impl/contact/phonebook_contact_dataso import 'package:fluffychat/data/datasource_impl/contact/tom_contacts_datasource_impl.dart'; import 'package:fluffychat/data/datasource_impl/localizations/localizations_datasource_impl.dart'; import 'package:fluffychat/data/datasource_impl/media/media_data_source_impl.dart'; +import 'package:fluffychat/data/datasource_impl/multiple_account/multiple_account_datasource_impl.dart'; import 'package:fluffychat/data/datasource_impl/recovery_words_data_source_impl.dart'; import 'package:fluffychat/data/datasource_impl/server_config_datasource_impl.dart'; import 'package:fluffychat/data/datasource_impl/server_search_datasource_impl.dart'; import 'package:fluffychat/data/datasource_impl/tom_configurations_datasource_impl.dart'; import 'package:fluffychat/data/local/localizations/language_cache_manager.dart'; +import 'package:fluffychat/data/local/multiple_account/multiple_account_cache_manager.dart'; import 'package:fluffychat/data/network/contact/lookup_api.dart'; import 'package:fluffychat/data/network/contact/tom_contact_api.dart'; import 'package:fluffychat/data/network/dio_cache_option.dart'; @@ -32,6 +35,7 @@ import 'package:fluffychat/data/repository/contact/phonebook_contact_repository_ import 'package:fluffychat/data/repository/contact/tom_contact_repository_impl.dart'; import 'package:fluffychat/data/repository/localizations/localizations_repository_impl.dart'; import 'package:fluffychat/data/repository/media/media_repository_impl.dart'; +import 'package:fluffychat/data/repository/multiple_account/multiple_account_repository_impl.dart'; import 'package:fluffychat/data/repository/recovery_words_repository_impl.dart'; import 'package:fluffychat/data/repository/server_config_repository_impl.dart'; import 'package:fluffychat/data/repository/server_search_repository_impl.dart'; @@ -43,6 +47,7 @@ import 'package:fluffychat/domain/contact_manager/contacts_manager.dart'; import 'package:fluffychat/domain/repository/contact_repository.dart'; import 'package:fluffychat/domain/repository/localizations/localizations_repository.dart'; import 'package:fluffychat/domain/repository/lookup_repository.dart'; +import 'package:fluffychat/domain/repository/multiple_account/multiple_account_repository.dart'; import 'package:fluffychat/domain/repository/phonebook_contact_repository.dart'; import 'package:fluffychat/domain/repository/recovery_words_repository.dart'; import 'package:fluffychat/domain/repository/server_config_repository.dart'; @@ -99,7 +104,7 @@ class GetItInitializer { GetItInitializer._internal(); - void setUp() async { + void setUp() { bindingGlobal(); bindingQueue(); bindingAPI(); @@ -178,6 +183,9 @@ class GetItInitializer { getIt.registerFactory( () => ServerConfigDatasourceImpl(), ); + getIt.registerFactory( + () => MultipleAccountDatasourceImpl(), + ); } void bindingDatasourceImpl() { @@ -234,6 +242,9 @@ class GetItInitializer { getIt.registerFactory( () => ServerSearchRepositoryImpl(), ); + getIt.registerFactory( + () => MultipleAccountRepositoryImpl(), + ); getIt.registerFactory( () => ServerConfigRepositoryImpl(), ); diff --git a/lib/domain/repository/multiple_account/multiple_account_repository.dart b/lib/domain/repository/multiple_account/multiple_account_repository.dart new file mode 100644 index 0000000000..44b9acd5e4 --- /dev/null +++ b/lib/domain/repository/multiple_account/multiple_account_repository.dart @@ -0,0 +1,7 @@ +abstract class MultipleAccountRepository { + Future storePersistActiveAccount(String userId); + + Future getPersistActiveAccount(); + + Future deletePersistActiveAccount(); +} diff --git a/lib/migrate_steps/migrate_steps.dart b/lib/migrate_steps/migrate_steps.dart new file mode 100644 index 0000000000..179426fc37 --- /dev/null +++ b/lib/migrate_steps/migrate_steps.dart @@ -0,0 +1,5 @@ +abstract class MigrateSteps { + const MigrateSteps(); + + Future onMigrate(int currentVersion, int newVersion) async {} +} diff --git a/lib/migrate_steps/migrate_v6_to_v7/migrate_v6_to_v7.dart b/lib/migrate_steps/migrate_v6_to_v7/migrate_v6_to_v7.dart new file mode 100644 index 0000000000..527cf4eb64 --- /dev/null +++ b/lib/migrate_steps/migrate_v6_to_v7/migrate_v6_to_v7.dart @@ -0,0 +1,25 @@ +import 'package:fluffychat/data/hive/hive_collection_tom_database.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/repository/multiple_account/multiple_account_repository.dart'; +import 'package:fluffychat/migrate_steps/migrate_steps.dart'; +import 'package:matrix/matrix.dart'; + +class MigrateV6ToV7 extends MigrateSteps { + @override + Future onMigrate(int currentVersion, int newVersion) async { + Logs().d( + 'MigrateV6ToV7::onMigrate() Starting migration from v6 to v7', + ); + final hiveCollectionToMDatabase = + await getIt.getAsync(); + await hiveCollectionToMDatabase.clear(); + Logs().d( + 'MigrateV6ToV7::onMigrate(): Delete ToM database success', + ); + final multipleAccountRepository = getIt.get(); + await multipleAccountRepository.deletePersistActiveAccount(); + Logs().d( + 'MigrateV6ToV7::onMigrate(): Delete persist active account success', + ); + } +} diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index eefea4818f..f3302af243 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; import 'package:fluffychat/domain/model/room/room_extension.dart'; import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; -import 'package:fluffychat/pages/twake_id/twake_id.dart'; +import 'package:fluffychat/pages/multiple_accounts/multiple_accounts_picker.dart'; import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; @@ -19,8 +19,6 @@ import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/presentation/mixins/go_to_group_chat_mixin.dart'; import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; -import 'package:fluffychat/presentation/multiple_account/profile_bundle.dart'; -import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -28,6 +26,7 @@ import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; @@ -36,7 +35,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; import 'package:matrix/matrix.dart'; import '../../../utils/account_bundles.dart'; @@ -53,7 +51,7 @@ class ChatList extends StatefulWidget { final VoidCallback? onOpenSettings; - final Client? newClient; + final AbsAppAdaptiveScaffoldBodyArgs? adaptiveScaffoldBodyArgs; const ChatList({ Key? key, @@ -61,7 +59,7 @@ class ChatList extends StatefulWidget { this.bottomNavigationBar, this.onOpenSearchPage, this.onOpenSettings, - this.newClient, + this.adaptiveScaffoldBodyArgs, }) : super(key: key); @override @@ -140,15 +138,6 @@ class ChatListController extends State // Needs to match GroupsSpacesEntry for 'separate group' checking. List get spaces => activeClient.rooms.where((r) => r.isSpace).toList(); - List get bundles => matrixState.accountBundles.keys.toList() - ..sort( - (pre, next) => pre!.isValidMatrixId == next!.isValidMatrixId - ? 0 - : pre.isValidMatrixId && !next.isValidMatrixId - ? -1 - : 1, - ); - ValueNotifier activeRoomIdNotifier = ValueNotifier(null); bool get isSelectMode => selectModeNotifier.value == SelectMode.select; @@ -471,18 +460,6 @@ class ChatListController extends State }); } - void setActiveClient(Client client) { - context.go('/rooms'); - _getCurrentProfile(client); - activeFilter = AppConfig.separateChatTypes - ? ActiveFilter.messages - : ActiveFilter.allChats; - activeSpaceId = null; - conversationSelectionNotifier.value.clear(); - Matrix.of(context).setActiveClient(client); - _clientStream.add(client); - } - void setActiveBundle(String bundle) { context.go('/rooms'); setState(() { @@ -778,69 +755,32 @@ class ChatListController extends State currentProfileNotifier.value = profile; } - Future> getProfileBundles() async { - final profiles = await Future.wait( - bundles.expand((bundle) { - return (matrixState.accountBundles[bundle]!).map((clientBundle) async { - if (clientBundle != null) { - final profileBundle = await clientBundle.fetchOwnProfile(); - return ProfileBundlePresentation( - profileBundle: profileBundle, - client: clientBundle, - ); - } - return null; - }); - }), - ); - - return profiles.toList(); - } - - void onSetAccountAsActive({ - required List multipleAccounts, - required TwakePresentationAccount account, - }) { - final client = multipleAccounts - .firstWhereOrNull( - (element) => element.accountId == account.accountId, - ) - ?.clientAccount; - if (client == null) return; - setActiveClient(client); - } - void onGoToAccountSettings() { widget.onOpenSettings?.call(); } - void onAddAnotherAccount() { - context.go( - '/rooms/addaccount', - extra: const TwakeIdArg( - twakeIdType: TwakeIdType.multiLogin, - ), + void onClickAvatar() { + MultipleAccountsPickerController(context: context) + .showMultipleAccountsPicker( + activeClient, + onGoToAccountSettings: onGoToAccountSettings, ); } - void initSetActiveClient() { - if (widget.newClient != null) { - setActiveClient(widget.newClient!); - } else { - _getCurrentProfile(activeClient); - } - } - @override void didUpdateWidget(covariant ChatList oldWidget) { Logs().d( - "ChatList::didUpdateWidget(): OldClient $activeClient", + "ChatList::didUpdateWidget(): Old Args ${oldWidget.adaptiveScaffoldBodyArgs} - UserId ${oldWidget.adaptiveScaffoldBodyArgs?.newActiveClient?.userID}", ); + final newActiveClient = widget.adaptiveScaffoldBodyArgs?.newActiveClient; Logs().d( - "ChatList::didUpdateWidget(): NewClient ${widget.newClient?.clientName}", + "ChatList::didUpdateWidget(): New Args ${widget.adaptiveScaffoldBodyArgs} - UserId ${newActiveClient?.userID}", ); - if (widget.newClient != activeClient) { - initSetActiveClient(); + if (newActiveClient != null && newActiveClient.userID != null) { + setState(() { + _getCurrentProfile(newActiveClient); + _clientStream.add(newActiveClient); + }); } super.didUpdateWidget(oldWidget); } @@ -852,7 +792,6 @@ class ChatListController extends State } activeRoomIdNotifier.value = widget.activeRoomIdNotifier.value; scrollController.addListener(_onScroll); - initSetActiveClient(); _waitForFirstSync(); _hackyWebRTCFixForWeb(); _getCurrentProfile(activeClient); @@ -861,9 +800,10 @@ class ChatListController extends State WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { Matrix.of(context).backgroundPush?.setupPush(); + await matrixState.retrievePersistedActiveClient(); + _getCurrentProfile(activeClient); } }); - _checkTorBrowser(); super.initState(); } diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 7124a329e6..1e87a4b492 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header_style.dart'; import 'package:fluffychat/widgets/twake_components/twake_header.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index d86e5d6eab..a880d015bd 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:matrix/matrix.dart'; class ChatListView extends StatelessWidget { final ChatListController controller; @@ -43,8 +44,13 @@ class ChatListView extends StatelessWidget { appBar: PreferredSize( preferredSize: ChatListViewStyle.preferredSizeAppBar(context), child: ChatListHeader( - controller: controller, onOpenSearchPage: onOpenSearchPage, + selectModeNotifier: controller.selectModeNotifier, + conversationSelectionNotifier: + controller.conversationSelectionNotifier, + currentProfileNotifier: controller.currentProfileNotifier, + onClickClearSelection: controller.onClickClearSelection, + onClickAvatar: controller.onClickAvatar, ), ), bottomNavigationBar: ValueListenableBuilder( @@ -64,7 +70,12 @@ class ChatListView extends StatelessWidget { } }, ), - body: ChatListBodyView(controller), + body: StreamBuilder( + stream: controller.clientStream, + builder: (context, snapshot) { + return ChatListBodyView(controller); + }, + ), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, floatingActionButton: ValueListenableBuilder( valueListenable: controller.selectModeNotifier, diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart deleted file mode 100644 index 33eade999d..0000000000 --- a/lib/pages/chat_list/client_chooser_button.dart +++ /dev/null @@ -1,384 +0,0 @@ -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:fluffychat/pages/chat_list/client_chooser_button_style.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; -import 'package:matrix/matrix.dart'; - -import '../../utils/fluffy_share.dart'; -import 'chat_list.dart'; - -class ClientChooserButton extends StatelessWidget { - final ChatListController controller; - - const ClientChooserButton(this.controller, {Key? key}) : super(key: key); - - List> _bundleMenuItems(BuildContext context) { - final matrix = Matrix.of(context); - final bundles = matrix.accountBundles.keys.toList() - ..sort( - (a, b) => a!.isValidMatrixId == b!.isValidMatrixId - ? 0 - : a.isValidMatrixId && !b.isValidMatrixId - ? -1 - : 1, - ); - return >[ - // PopupMenuItem( - // value: SettingsAction.newStory, - // child: Row( - // children: [ - // const Icon(Icons.camera_outlined), - // const SizedBox(width: 18), - // Text(L10n.of(context)!.yourStory), - // ], - // ), - // ), - // PopupMenuItem( - // value: SettingsAction.invite, - // child: Row( - // children: [ - // Icon(Icons.adaptive.share_outlined), - // const SizedBox(width: 18), - // Text(L10n.of(context)!.inviteContact), - // ], - // ), - // ), - PopupMenuItem( - value: SettingsAction.archive, - child: Row( - children: [ - const Icon(Icons.archive_outlined), - const SizedBox(width: 18), - Text(L10n.of(context)!.archive), - ], - ), - ), - PopupMenuItem( - value: SettingsAction.settings, - child: Row( - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 18), - Text(L10n.of(context)!.settings), - ], - ), - ), - const PopupMenuItem( - value: null, - child: Divider(height: 1), - ), - for (final bundle in bundles) ...[ - if (matrix.accountBundles[bundle]!.length != 1 || - matrix.accountBundles[bundle]!.single!.userID != bundle) - PopupMenuItem( - value: null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - bundle!, - style: TextStyle( - color: Theme.of(context).textTheme.titleMedium!.color, - fontSize: 14, - ), - ), - const Divider(height: 1), - ], - ), - ), - ...matrix.accountBundles[bundle]!.map((client) { - return PopupMenuItem( - value: client, - child: ProfileWidget( - controller: controller, - bundle: bundle, - client: client!, - ), - ); - }).toList(), - ], - // PopupMenuItem( - // value: SettingsAction.addAccount, - // child: Row( - // children: [ - // const Icon(Icons.person_add_outlined), - // const SizedBox(width: 18), - // Text(L10n.of(context)!.addAccount), - // ], - // ), - // ), - ]; - } - - @override - Widget build(BuildContext context) { - final matrix = Matrix.of(context); - - int clientCount = 0; - matrix.accountBundles.forEach((key, value) => clientCount += value.length); - return FutureBuilder( - future: matrix.client.fetchOwnProfile(getFromRooms: false), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - - return Stack( - alignment: Alignment.center, - children: [ - ...List.generate( - clientCount, - (index) => KeyBoardShortcuts( - keysToPress: _buildKeyboardShortcut(index + 1), - helpLabel: L10n.of(context)!.switchToAccount(index + 1), - onKeysPressed: () => _handleKeyboardShortcut( - matrix, - index, - context, - ), - child: Container(), - ), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context)!.nextAccount, - onKeysPressed: () => _nextAccount(matrix, context), - child: Container(), - ), - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.shiftLeft, - LogicalKeyboardKey.tab, - }, - helpLabel: L10n.of(context)!.previousAccount, - onKeysPressed: () => _previousAccount(matrix, context), - child: Container(), - ), - PopupMenuButton( - onSelected: (o) => _clientSelected(o, context), - itemBuilder: _bundleMenuItems, - child: Padding( - padding: const EdgeInsets.only(left: 16, top: 8, bottom: 8), - child: Row( - children: [ - Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - matrix.client.userID!.localpart, - size: ClientChooserButtonStyle.avatarSizeInAppBar, - fontSize: ClientChooserButtonStyle.avatarFontSizeInAppBar, - ), - Icon( - Icons.keyboard_arrow_down, - size: ClientChooserButtonStyle.dropDownIconSize, - ), - ], - ), - ), - ), - ], - ); - }, - ); - } - - Set? _buildKeyboardShortcut(int index) { - if (index > 0 && index < 10) { - return { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey(0x00000000030 + index), - }; - } else { - return null; - } - } - - void _clientSelected( - Object object, - BuildContext context, - ) async { - if (object is Client) { - controller.setActiveClient(object); - } else if (object is String) { - controller.setActiveBundle(object); - } else if (object is SettingsAction) { - switch (object) { - case SettingsAction.addAccount: - final consent = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context)!.addAccount, - message: L10n.of(context)!.enableMultiAccounts, - okLabel: L10n.of(context)!.next, - cancelLabel: L10n.of(context)!.cancel, - ); - if (consent != OkCancelResult.ok) return; - context.go('/settings/addaccount'); - break; - case SettingsAction.newStory: - context.go('/stories/create'); - break; - case SettingsAction.invite: - FluffyShare.share( - L10n.of(context)!.inviteText( - Matrix.of(context).client.userID!, - 'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat', - ), - context, - ); - break; - case SettingsAction.settings: - context.go('/rooms/settings'); - break; - case SettingsAction.archive: - context.go('/rooms/archive'); - break; - default: - break; - } - } - } - - void _handleKeyboardShortcut( - MatrixState matrix, - int index, - BuildContext context, - ) { - final bundles = matrix.accountBundles.keys.toList() - ..sort( - (a, b) => a!.isValidMatrixId == b!.isValidMatrixId - ? 0 - : a.isValidMatrixId && !b.isValidMatrixId - ? -1 - : 1, - ); - // beginning from end if negative - if (index < 0) { - int clientCount = 0; - matrix.accountBundles - .forEach((key, value) => clientCount += value.length); - _handleKeyboardShortcut(matrix, clientCount, context); - } - for (final bundleName in bundles) { - final bundle = matrix.accountBundles[bundleName]; - if (bundle != null) { - if (index < bundle.length) { - return _clientSelected(bundle[index]!, context); - } else { - index -= bundle.length; - } - } - } - // if index too high, restarting from 0 - _handleKeyboardShortcut(matrix, 0, context); - } - - int? _shortcutIndexOfClient(MatrixState matrix, Client client) { - int index = 0; - - final bundles = matrix.accountBundles.keys.toList() - ..sort( - (a, b) => a!.isValidMatrixId == b!.isValidMatrixId - ? 0 - : a.isValidMatrixId && !b.isValidMatrixId - ? -1 - : 1, - ); - for (final bundleName in bundles) { - final bundle = matrix.accountBundles[bundleName]; - if (bundle == null) return null; - if (bundle.contains(client)) { - return index + bundle.indexOf(client); - } else { - index += bundle.length; - } - } - return null; - } - - void _nextAccount(MatrixState matrix, BuildContext context) { - final client = matrix.client; - final lastIndex = _shortcutIndexOfClient(matrix, client); - _handleKeyboardShortcut(matrix, lastIndex! + 1, context); - } - - void _previousAccount(MatrixState matrix, BuildContext context) { - final client = matrix.client; - final lastIndex = _shortcutIndexOfClient(matrix, client); - _handleKeyboardShortcut(matrix, lastIndex! - 1, context); - } -} - -class ProfileWidget extends StatefulWidget { - const ProfileWidget({ - super.key, - required this.controller, - required this.bundle, - required this.client, - }); - - final ChatListController controller; - final String? bundle; - final Client client; - - @override - State createState() => _ProfileWidgetState(); -} - -class _ProfileWidgetState extends State { - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: widget.client.fetchOwnProfile(getFromRooms: false), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } - return Row( - children: [ - Avatar( - mxContent: snapshot.data?.avatarUrl, - name: - snapshot.data?.displayName ?? widget.client.userID!.localpart, - size: 32, - fontSize: 12, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - snapshot.data?.displayName ?? widget.client.userID!.localpart!, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), - // IconButton( - // icon: const Icon(Icons.edit_outlined), - // onPressed: () => widget.controller.editBundlesForAccount( - // widget.client.userID, - // widget.bundle, - // ), - // ), - ], - ); - }, - ); - } -} - -enum SettingsAction { - addAccount, - newStory, - newSpace, - invite, - settings, - archive, -} diff --git a/lib/pages/connect/connect_page_mixin.dart b/lib/pages/connect/connect_page_mixin.dart index 80cd712865..0471c680eb 100644 --- a/lib/pages/connect/connect_page_mixin.dart +++ b/lib/pages/connect/connect_page_mixin.dart @@ -190,15 +190,4 @@ mixin ConnectPageMixin { } return list; } - - bool isSingleAccountOnHomeserver( - MatrixState matrix, - Uri homeserver, - ) { - if (matrix.client.isLogged() && matrix.client.homeserver == homeserver) { - return true; - } else { - return false; - } - } } diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index c0c1c55453..801f4879d1 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -146,7 +146,11 @@ class HomeserverPickerController extends State } final matrix = Matrix.of(context); - if (isSingleAccountOnHomeserver(matrix, homeserver)) { + final homeserverExists = + homeserver == matrix.client.homeserver && matrix.client.isLogged(); + + if (homeserverExists && + !AppConfig.supportMultipleAccountsInTheSameHomeserver) { TwakeSnackBar.show( context, L10n.of(context)!.isSingleAccountOnHomeserver, diff --git a/lib/pages/multiple_accounts/multiple_accounts_picker.dart b/lib/pages/multiple_accounts/multiple_accounts_picker.dart new file mode 100644 index 0000000000..de87e3b537 --- /dev/null +++ b/lib/pages/multiple_accounts/multiple_accounts_picker.dart @@ -0,0 +1,140 @@ +import 'package:collection/collection.dart'; +import 'package:fluffychat/pages/twake_id/twake_id.dart'; +import 'package:fluffychat/presentation/extensions/multiple_accounts/client_profile_extension.dart'; +import 'package:fluffychat/presentation/multiple_account/client_profile_presentation.dart'; +import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; +import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/widgets/layouts/agruments/switch_active_account_body_args.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/twake_components/twake_header_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class MultipleAccountsPickerController { + final BuildContext context; + + MultipleAccountsPickerController({ + required this.context, + }); + + MatrixState get _matrixState => Matrix.of(context); + + Future> _getClientProfiles() async { + final profiles = await Future.wait( + _matrixState.widget.clients.map((client) async { + final profileBundle = await client.fetchOwnProfile(); + Logs().d( + 'MultipleAccountsPicker::getProfileBundles() - ClientName - ${client.clientName}', + ); + Logs().d( + 'MultipleAccountsPicker::getProfileBundles() - UserId - ${client.userID}', + ); + return ClientProfilePresentation( + profile: profileBundle, + client: client, + ); + }), + ); + + return profiles.toList(); + } + + Future> _getMultipleAccounts( + Client currentActiveClient, + ) async { + final profileBundles = await _getClientProfiles(); + return profileBundles + .where((clientProfile) => clientProfile != null) + .map( + (clientProfile) => clientProfile!.toTwakeChatPresentationAccount( + currentActiveClient, + ), + ) + .toList(); + } + + void showMultipleAccountsPicker( + Client currentActiveClient, { + required VoidCallback onGoToAccountSettings, + }) async { + final multipleAccount = await _getMultipleAccounts( + currentActiveClient, + ); + multipleAccount.sort((pre, next) { + return pre.accountActiveStatus.index + .compareTo(next.accountActiveStatus.index); + }); + MultipleAccountPicker.showMultipleAccountPicker( + accounts: multipleAccount, + context: context, + onAddAnotherAccount: _onAddAnotherAccount, + onGoToAccountSettings: onGoToAccountSettings, + onSetAccountAsActive: (account) => _onSetAccountAsActive.call( + multipleAccounts: multipleAccount, + account: account, + ), + titleAddAnotherAccount: L10n.of(context)!.addAnotherAccount, + titleAccountSettings: L10n.of(context)!.accountSettings, + logoApp: Padding( + padding: TwakeHeaderStyle.logoAppOfMultiplePadding, + child: SvgPicture.asset( + ImagePaths.icTwakeImageLogo, + width: TwakeHeaderStyle.logoAppOfMultipleWidth, + height: TwakeHeaderStyle.logoAppOfMultipleHeight, + ), + ), + accountNameStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: LinagoraSysColors.material().onSurface, + ), + accountIdStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: LinagoraRefColors.material().tertiary[20], + ), + addAnotherAccountStyle: Theme.of(context).textTheme.labelLarge!.copyWith( + color: LinagoraSysColors.material().onPrimary, + ), + titleAccountSettingsStyle: + Theme.of(context).textTheme.labelLarge!.copyWith( + color: LinagoraSysColors.material().primary, + ), + ); + } + + void _onSetAccountAsActive({ + required List multipleAccounts, + required TwakePresentationAccount account, + }) async { + final client = multipleAccounts + .firstWhereOrNull( + (element) => element.accountId == account.accountId, + ) + ?.clientAccount; + if (client == null || client == _matrixState.client) return; + _setActiveClient(client); + } + + void _onAddAnotherAccount() { + context.go( + '/rooms/addaccount', + extra: const TwakeIdArg( + twakeIdType: TwakeIdType.otherAccounts, + ), + ); + } + + void _setActiveClient(Client newClient) async { + final result = await _matrixState.setActiveClient(newClient); + if (result.isSuccess) { + context.go( + '/rooms', + extra: SwitchActiveAccountBodyArgs( + newActiveClient: newClient, + ), + ); + } + } +} diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 7ecedb0266..82853926c9 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -83,7 +83,7 @@ class SettingsController extends State with ConnectPageMixin { return; } final matrix = Matrix.of(context); - if (matrix.twakeIsSupported) { + if (matrix.twakeSupported == true) { await tryLogoutSso(context); final hiveCollectionToMDatabase = getIt.get(); await hiveCollectionToMDatabase.clear(); diff --git a/lib/pages/twake_id/twake_id.dart b/lib/pages/twake_id/twake_id.dart index ea4b02cec0..bc45d15eb0 100644 --- a/lib/pages/twake_id/twake_id.dart +++ b/lib/pages/twake_id/twake_id.dart @@ -10,18 +10,18 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; enum TwakeIdType { - login, - multiLogin, + firstAccount, + otherAccounts, } class TwakeIdArg extends Equatable { final TwakeIdType twakeIdType; const TwakeIdArg({ - this.twakeIdType = TwakeIdType.login, + this.twakeIdType = TwakeIdType.firstAccount, }); - bool get isAddAnotherAccount => twakeIdType == TwakeIdType.multiLogin; + bool get isAddAnotherAccount => twakeIdType == TwakeIdType.otherAccounts; @override List get props => [twakeIdType]; @@ -38,7 +38,7 @@ class TwakeId extends StatefulWidget { class TwakeIdController extends State { void goToHomeserverPicker() { if (widget.arg?.isAddAnotherAccount == true) { - context.push('/rooms/homeserverpicker'); + context.push('/rooms/addhomeserver'); } else { context.push('/home/homeserverpicker'); } diff --git a/lib/presentation/enum/settings/settings_action_enum.dart b/lib/presentation/enum/settings/settings_action_enum.dart new file mode 100644 index 0000000000..22deb0585b --- /dev/null +++ b/lib/presentation/enum/settings/settings_action_enum.dart @@ -0,0 +1,8 @@ +enum SettingsAction { + addAccount, + newStory, + newSpace, + invite, + settings, + archive, +} diff --git a/lib/presentation/extensions/multiple_accounts/client_profile_extension.dart b/lib/presentation/extensions/multiple_accounts/client_profile_extension.dart new file mode 100644 index 0000000000..9c67851060 --- /dev/null +++ b/lib/presentation/extensions/multiple_accounts/client_profile_extension.dart @@ -0,0 +1,24 @@ +import 'package:fluffychat/presentation/multiple_account/client_profile_presentation.dart'; +import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; +import 'package:matrix/matrix.dart'; + +extension ClientProfileExtension on ClientProfilePresentation { + TwakeChatPresentationAccount toTwakeChatPresentationAccount( + Client currentActiveClient, + ) { + return TwakeChatPresentationAccount( + clientAccount: client, + accountId: profile.userId, + accountName: profile.displayName ?? '', + avatar: Avatar( + mxContent: profile.avatarUrl, + name: profile.displayName ?? '', + ), + accountActiveStatus: profile.userId == currentActiveClient.userID + ? AccountActiveStatus.active + : AccountActiveStatus.inactive, + ); + } +} diff --git a/lib/presentation/multiple_account/profile_bundle.dart b/lib/presentation/multiple_account/client_profile_presentation.dart similarity index 54% rename from lib/presentation/multiple_account/profile_bundle.dart rename to lib/presentation/multiple_account/client_profile_presentation.dart index 05ae3e8857..89b84e059f 100644 --- a/lib/presentation/multiple_account/profile_bundle.dart +++ b/lib/presentation/multiple_account/client_profile_presentation.dart @@ -1,18 +1,18 @@ import 'package:equatable/equatable.dart'; import 'package:matrix/matrix.dart'; -class ProfileBundlePresentation extends Equatable { - final Profile profileBundle; +class ClientProfilePresentation extends Equatable { + final Profile profile; final Client client; - const ProfileBundlePresentation({ - required this.profileBundle, + const ClientProfilePresentation({ + required this.profile, required this.client, }); @override List get props => [ - profileBundle, + profile, client, ]; } diff --git a/lib/utils/dialog/twake_dialog.dart b/lib/utils/dialog/twake_dialog.dart index e2e4f33c5d..c87f7cbecf 100644 --- a/lib/utils/dialog/twake_dialog.dart +++ b/lib/utils/dialog/twake_dialog.dart @@ -56,6 +56,7 @@ class TwakeDialog { static Future showDialogFullScreen({ required Widget Function() builder, + bool barrierDismissible = true, }) { final twakeContext = TwakeApp.routerKey.currentContext; if (twakeContext == null) { @@ -67,7 +68,7 @@ class TwakeDialog { return showDialog( context: twakeContext, builder: (context) => builder(), - barrierDismissible: true, + barrierDismissible: barrierDismissible, useRootNavigator: false, ); } diff --git a/lib/utils/famedlysdk_store.dart b/lib/utils/famedlysdk_store.dart index 5c643274a1..e1b0f1af5e 100644 --- a/lib/utils/famedlysdk_store.dart +++ b/lib/utils/famedlysdk_store.dart @@ -41,12 +41,12 @@ class Store { return; } - Future getInt(String key) async { + Future getItemInt(String key) async { await _setupLocalStorage(); return _prefs!.getInt(key); } - Future setInt(String key, int value) async { + Future setItemInt(String key, int value) async { await _setupLocalStorage(); return _prefs!.setInt(key, value); } diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart index 1f61a0424f..6e485e40ca 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_manager.dart'; import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_restore_token.dart'; import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_session.dart'; +import 'package:fluffychat/migrate_steps/migrate_v6_to_v7/migrate_v6_to_v7.dart'; import 'package:flutter/foundation.dart' hide Key; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -19,17 +20,18 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { String name, String path, { HiveCipher? key, - StartMigrationProcess? startMigrationProcess, + OnStartMigrating? onStartMigrating, }) : super( name, path, key: key, - startMigrationProcess: startMigrationProcess, + onStartMigrating: onStartMigrating, ); - static const String cipherStorageKey = 'hive_encryption_key'; + @override + int get version => 7; - static bool canMigrateToMDatabase = false; + static const String cipherStorageKey = 'hive_encryption_key'; static Future databaseBuilder( Client client, @@ -77,15 +79,7 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { 'hive_collections_${client.clientName.replaceAll(' ', '_').toLowerCase()}', await _findDatabasePath(client), key: hiverCipher, - startMigrationProcess: (currentVersion, newVersion) async { - Logs().d( - 'FlutterHiveCollectionsDatabase::startMigrationProcess() Starting migration process', - ); - Logs().d( - 'FlutterHiveCollectionsDatabase::startMigrationProcess() CurrentVersion - $currentVersion || NewVersion - $newVersion', - ); - canMigrateToMDatabase = true; - }, + onStartMigrating: _onStartMigrating, ); try { Logs().i('FlutterHiveCollectionsDatabase()::databaseBuilder()::open()'); @@ -245,4 +239,13 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { } return super.clear(supportDeleteCollections: supportDeleteCollections); } + + static void _onStartMigrating(int currentVersion, int newVersion) async { + Logs().d( + 'FlutterHiveCollectionsDatabase::startMigrationProcess() CurrentVersion - $currentVersion || NewVersion - $newVersion', + ); + if (currentVersion == 6 && newVersion == 7) { + await MigrateV6ToV7().onMigrate(currentVersion, newVersion); + } + } } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index 2dca50cd55..8ce825a1c3 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -1,10 +1,11 @@ -import 'package:equatable/equatable.dart'; import 'package:fluffychat/config/first_column_inner_routes.dart'; -import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_action_enum.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart'; +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -19,31 +20,14 @@ typedef OnClientSelectedSetting = void Function( typedef OnDestinationSelected = void Function(int index); typedef OnPopInvoked = void Function(bool); -class AppAdaptiveScaffoldBodyArgs extends Equatable { - final String? activeRoomId; - final Client? client; - final bool isLogoutMultipleAccount; - - const AppAdaptiveScaffoldBodyArgs({ - this.activeRoomId, - this.client, - this.isLogoutMultipleAccount = false, - }); - - @override - List get props => [ - activeRoomId, - client, - isLogoutMultipleAccount, - ]; -} - class AppAdaptiveScaffoldBody extends StatefulWidget { - final AppAdaptiveScaffoldBodyArgs args; + final AbsAppAdaptiveScaffoldBodyArgs? args; + final String? activeRoomId; const AppAdaptiveScaffoldBody({ super.key, required this.args, + this.activeRoomId, }); @override @@ -137,18 +121,14 @@ class AppAdaptiveScaffoldBodyController extends State { } } - void _onLogoutMultipleAccountSuccess( - covariant AppAdaptiveScaffoldBody oldWidget, - ) { + void _handleLogout(AppAdaptiveScaffoldBody oldWidget) { Logs().d( - 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():oldWidget.isLogoutMultipleAccount: ${oldWidget.args.isLogoutMultipleAccount}', + 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():oldWidget - ${oldWidget.args}', ); Logs().d( - 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():newIsLogoutMultipleAccount: ${widget.args.isLogoutMultipleAccount}', + 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():newWidget - ${widget.args}', ); - if (oldWidget.args.isLogoutMultipleAccount != - widget.args.isLogoutMultipleAccount && - widget.args.isLogoutMultipleAccount) { + if (oldWidget.args != widget.args && widget.args is LogoutBodyArgs) { activeNavigationBar.value = AdaptiveDestinationEnum.rooms; pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); } @@ -158,14 +138,14 @@ class AppAdaptiveScaffoldBodyController extends State { @override void initState() { - activeRoomIdNotifier.value = widget.args.activeRoomId; + activeRoomIdNotifier.value = widget.activeRoomId; super.initState(); } @override void didUpdateWidget(covariant AppAdaptiveScaffoldBody oldWidget) { - activeRoomIdNotifier.value = widget.args.activeRoomId; - _onLogoutMultipleAccountSuccess(oldWidget); + activeRoomIdNotifier.value = widget.activeRoomId; + _handleLogout(oldWidget); super.didUpdateWidget(oldWidget); } @@ -189,6 +169,6 @@ class AppAdaptiveScaffoldBodyController extends State { onClientSelected: clientSelected, onPopInvoked: _onPopInvoked, onOpenSettings: _onOpenSettingsPage, - client: widget.args.client, + adaptiveScaffoldBodyArgs: widget.args, ); } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart index 33427d4ce1..b1030db584 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart @@ -9,12 +9,12 @@ import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_pri import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_view_style.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view_style.dart'; +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart' hide WidgetBuilder; -import 'package:matrix/matrix.dart'; class AppAdaptiveScaffoldBodyView extends StatelessWidget { final List destinations; @@ -26,7 +26,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { final PageController pageController; final OnPopInvoked onPopInvoked; final VoidCallback onOpenSettings; - final Client? client; + final AbsAppAdaptiveScaffoldBodyArgs? adaptiveScaffoldBodyArgs; final ValueNotifier activeRoomIdNotifier; @@ -50,7 +50,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { required this.destinations, required this.onPopInvoked, required this.onOpenSettings, - this.client, + this.adaptiveScaffoldBodyArgs, }) : super(key: key ?? scaffoldWithNestedNavigationKey); @override @@ -131,7 +131,8 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { destinations: destinations, bottomNavigationKey: bottomNavigationKey, onOpenSettings: onOpenSettings, - client: client, + adaptiveScaffoldBodyArgs: + adaptiveScaffoldBodyArgs, ); }, ); @@ -159,7 +160,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { destinations: destinations, bottomNavigationKey: bottomNavigationKey, onOpenSettings: onOpenSettings, - client: client, + adaptiveScaffoldBodyArgs: adaptiveScaffoldBodyArgs, ), ), ], @@ -189,7 +190,7 @@ class _ColumnPageView extends StatelessWidget { final ValueKey bottomNavigationKey; final ValueNotifier activeRoomIdNotifier; final VoidCallback onOpenSettings; - final Client? client; + final AbsAppAdaptiveScaffoldBodyArgs? adaptiveScaffoldBodyArgs; const _ColumnPageView({ required this.activeNavigationBarNotifier, @@ -202,7 +203,7 @@ class _ColumnPageView extends StatelessWidget { required this.destinations, required this.bottomNavigationKey, required this.onOpenSettings, - required this.client, + required this.adaptiveScaffoldBodyArgs, }); @override @@ -225,7 +226,7 @@ class _ColumnPageView extends StatelessWidget { onOpenSearchPage: onOpenSearchPage, activeRoomIdNotifier: activeRoomIdNotifier, onOpenSettings: onOpenSettings, - newClient: client, + adaptiveScaffoldBodyArgs: adaptiveScaffoldBodyArgs, ), _triggerPageViewBuilder( navigatorBarType: AdaptiveDestinationEnum.settings, diff --git a/lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart b/lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart new file mode 100644 index 0000000000..c35cdc65d7 --- /dev/null +++ b/lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; +import 'package:matrix/matrix.dart'; + +abstract class AbsAppAdaptiveScaffoldBodyArgs extends Equatable { + final Client? newActiveClient; + + const AbsAppAdaptiveScaffoldBodyArgs({ + required this.newActiveClient, + }); + @override + List get props => [ + newActiveClient, + ]; +} diff --git a/lib/widgets/layouts/agruments/logged_in_body_args.dart b/lib/widgets/layouts/agruments/logged_in_body_args.dart new file mode 100644 index 0000000000..b70b4c2e1e --- /dev/null +++ b/lib/widgets/layouts/agruments/logged_in_body_args.dart @@ -0,0 +1,12 @@ +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; + +class LoggedInBodyArgs extends AbsAppAdaptiveScaffoldBodyArgs { + const LoggedInBodyArgs({ + required super.newActiveClient, + }); + + @override + List get props => [ + newActiveClient, + ]; +} diff --git a/lib/widgets/layouts/agruments/logout_body_args.dart b/lib/widgets/layouts/agruments/logout_body_args.dart new file mode 100644 index 0000000000..648176a5e3 --- /dev/null +++ b/lib/widgets/layouts/agruments/logout_body_args.dart @@ -0,0 +1,12 @@ +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; + +class LogoutBodyArgs extends AbsAppAdaptiveScaffoldBodyArgs { + const LogoutBodyArgs({ + required super.newActiveClient, + }); + + @override + List get props => [ + newActiveClient, + ]; +} diff --git a/lib/widgets/layouts/agruments/switch_active_account_body_args.dart b/lib/widgets/layouts/agruments/switch_active_account_body_args.dart new file mode 100644 index 0000000000..4acc62c52a --- /dev/null +++ b/lib/widgets/layouts/agruments/switch_active_account_body_args.dart @@ -0,0 +1,12 @@ +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; + +class SwitchActiveAccountBodyArgs extends AbsAppAdaptiveScaffoldBodyArgs { + const SwitchActiveAccountBodyArgs({ + required super.newActiveClient, + }); + + @override + List get props => [ + newActiveClient, + ]; +} diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index b2f22837d7..96d8f60747 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -6,6 +6,7 @@ import 'package:universal_html/html.dart' as html hide File; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; +import 'package:equatable/equatable.dart'; import 'package:fluffychat/data/hive/hive_collection_tom_database.dart'; import 'package:fluffychat/data/network/interceptor/authorization_interceptor.dart'; import 'package:fluffychat/data/network/interceptor/dynamic_url_interceptor.dart'; @@ -14,17 +15,19 @@ import 'package:fluffychat/di/global/network_di.dart'; import 'package:fluffychat/domain/model/extensions/homeserver_summary_extensions.dart'; import 'package:fluffychat/domain/model/tom_configurations.dart'; import 'package:fluffychat/domain/model/tom_server_information.dart'; +import 'package:fluffychat/domain/repository/multiple_account/multiple_account_repository.dart'; import 'package:fluffychat/domain/repository/tom_configurations_repository.dart'; import 'package:fluffychat/pages/chat_list/receive_sharing_intent_mixin.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; -import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; +import 'package:fluffychat/widgets/set_active_client_state.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -83,7 +86,13 @@ class MatrixState extends State String? loginUsername; LoginType? loginType; bool? loginRegistrationSupported; - bool? _twakeSupported; + + bool get twakeSupported { + final tomServerUrlInterceptor = getIt.get( + instanceName: NetworkDI.tomServerUrlInterceptorName, + ); + return tomServerUrlInterceptor.baseUrl != null; + } BackgroundPush? backgroundPush; @@ -100,8 +109,6 @@ class MatrixState extends State // TODO: 28Dec2023 Disable until support voip bool get webrtcIsSupported => false; - bool get twakeIsSupported => _twakeSupported ?? false; - VoipPlugin? voipPlugin; bool get isMultiAccount => widget.clients.length > 1; @@ -112,15 +119,18 @@ class MatrixState extends State late String currentClientSecret; RequestTokenResponse? currentThreepidCreds; - void setActiveClient(Client? newClient) { - _checkHomeserverExists(newClient); + Future setActiveClient(Client? newClient) async { final index = widget.clients.indexWhere((client) => client == newClient); if (index != -1) { _activeClient = index; // TODO: Multi-client VoiP support createVoipPlugin(); + _setUpToMServicesWhenChangingActiveClient(newClient); + await _storePersistActiveAccount(newClient!); + return SetActiveClientState.success; } else { Logs().w('Tried to set an unknown client ${newClient!.userID} as active'); + return SetActiveClientState.unknownClient; } } @@ -189,15 +199,20 @@ class MatrixState extends State ClientManager.addClientNameToStore(_loginClientCandidate!.clientName); Logs().d('MatrixState::getLoginClient() Registering subs'); _registerSubs(_loginClientCandidate!.clientName); - TwakeApp.router.go( - '/rooms', - extra: AppAdaptiveScaffoldBodyArgs( - client: getClientByName( - _loginClientCandidate!.clientName, - ), - ), + final activeClient = getClientByName( + _loginClientCandidate!.clientName, ); - _loginClientCandidate = null; + if (activeClient == null) return; + final result = await setActiveClient(activeClient); + if (result.isSuccess) { + TwakeApp.router.go( + '/rooms', + extra: LoggedInBodyArgs( + newActiveClient: activeClient, + ), + ); + _loginClientCandidate = null; + } }); return candidate; } @@ -286,20 +301,22 @@ class MatrixState extends State @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); - _migrateToMDatabase(client); - if (PlatformInfos.isWeb) { - html.window.addEventListener('focus', onWindowFocus); - html.window.addEventListener('blur', onWindowBlur); - } - initMatrix(); - initReceiveSharingIntent(); - if (PlatformInfos.isWeb) { - initConfig().then((_) => initSettings()); - } else { - initSettings(); - } - initLoadingDialog(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addObserver(this); + _migrateToMDatabase(client); + if (PlatformInfos.isWeb) { + html.window.addEventListener('focus', onWindowFocus); + html.window.addEventListener('blur', onWindowBlur); + } + initMatrix(); + initReceiveSharingIntent(); + if (PlatformInfos.isWeb) { + initConfig().then((_) => initSettings()); + } else { + initSettings(); + } + initLoadingDialog(); + }); } void initLoadingDialog() { @@ -332,6 +349,9 @@ class MatrixState extends State ); return; } + if (PlatformInfos.isMobile) { + await HiveCollectionToMDatabase.databaseBuilder(); + } onRoomKeyRequestSub[name] ??= c.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async { if (widget.clients.any( @@ -371,12 +391,12 @@ class MatrixState extends State TwakeApp.routerKey.currentContext!, L10n.of(context)!.oneClientLoggedOut, ); - - if (state != LoginState.loggedIn) { + final result = await setActiveClient(widget.clients.first); + if (state != LoginState.loggedIn && result.isSuccess) { TwakeApp.router.go( '/rooms', - extra: const AppAdaptiveScaffoldBodyArgs( - isLogoutMultipleAccount: true, + extra: LogoutBodyArgs( + newActiveClient: widget.clients.first, ), ); } @@ -384,7 +404,13 @@ class MatrixState extends State if (state == LoginState.loggedIn) { Logs().v('[MATRIX] Log in successful'); setUpToMServicesInLogin(c); - TwakeApp.router.go('/rooms'); + _storePersistActiveAccount(c); + TwakeApp.router.go( + '/rooms', + extra: LoggedInBodyArgs( + newActiveClient: c, + ), + ); } else { Logs().v('[MATRIX] Log out successful'); if (PlatformInfos.isMobile) { @@ -412,6 +438,20 @@ class MatrixState extends State } } + void _deletePersistActiveAccount(LoginState state) async { + try { + final multipleAccountRepository = getIt.get(); + await multipleAccountRepository.deletePersistActiveAccount(); + Logs().d( + 'MatrixState::_handleLogoutWithMultipleAccount: Delete persist active account success', + ); + } catch (e) { + Logs().e( + 'MatrixState::_handleLogoutWithMultipleAccount: Error - $e', + ); + } + } + void _cancelSubs(String name) { onRoomKeyRequestSub[name]?.cancel(); onRoomKeyRequestSub.remove(name); @@ -482,7 +522,6 @@ class MatrixState extends State createVoipPlugin(); } - // TODO: 28Dec2023 Disable until support voip void createVoipPlugin() async { if (await store.getItemBool(SettingKeys.experimentalVoip) == false) { voipPlugin = null; @@ -515,7 +554,6 @@ class MatrixState extends State ); authUrl = toMConfigurations.authUrl; loginType = toMConfigurations.loginType; - setTakeSupported(supported: true); } catch (e) { Logs().e('MatrixState::_retrieveToMConfiguration: $e'); } @@ -574,16 +612,14 @@ class MatrixState extends State authorizationInterceptor.accessToken = client.accessToken; } - void _setUpToMServer(ToMServerInformation tomServer) { - if (tomServer.baseUrl != null) { - final tomServerUrlInterceptor = getIt.get( - instanceName: NetworkDI.tomServerUrlInterceptorName, - ); - Logs().d( - 'MatrixState::_setUpToMServer: ${tomServerUrlInterceptor.hashCode}', - ); - tomServerUrlInterceptor.changeBaseUrl(tomServer.baseUrl!.toString()); - } + void _setUpToMServer(ToMServerInformation? tomServer) { + final tomServerUrlInterceptor = getIt.get( + instanceName: NetworkDI.tomServerUrlInterceptorName, + ); + Logs().d( + 'MatrixState::_setUpToMServer: ${tomServerUrlInterceptor.hashCode}', + ); + tomServerUrlInterceptor.changeBaseUrl(tomServer?.baseUrl?.toString()); } void _setUpHomeServer(Uri homeServerUri) { @@ -633,27 +669,70 @@ class MatrixState extends State } } - void setTakeSupported({ - required bool supported, - }) { - _twakeSupported = supported; + void _setUpToMServicesWhenChangingActiveClient(Client? client) async { Logs().d( - 'Matrix::setTakeSupported: _twakeSupported - $_twakeSupported', - ); - } - - void _checkHomeserverExists(Client? client) async { - Logs().d( - 'Matrix::_checkHomeserverExists: _twakeSupported - $_twakeSupported', + 'Matrix::_checkHomeserverExists: Old twakeSupported - $twakeSupported', ); if (client == null && client?.userID == null) return; try { - await tomConfigurationRepository.getTomConfigurations(client!.userID!); - setTakeSupported(supported: true); + final toMConfigurations = await getTomConfigurations(client!.userID!); + Logs().d( + 'Matrix::_checkHomeserverExists: toMConfigurations - $toMConfigurations', + ); + if (toMConfigurations == null) { + _setUpToMServer(null); + } else { + _setUpToMServer(toMConfigurations.tomServerInformation); + } } catch (e) { - setTakeSupported(supported: false); + _setUpToMServer(null); Logs().e('Matrix::_checkHomeserverExists: error - $e'); } + Logs().d( + 'Matrix::_checkHomeserverExists: New twakeSupported - $twakeSupported', + ); + } + + Future retrievePersistedActiveClient() async { + try { + final multipleAccountRepository = getIt.get(); + final persistActiveAccount = + await multipleAccountRepository.getPersistActiveAccount(); + if (persistActiveAccount == null) { + _storePersistActiveAccount(client); + return; + } else { + final newActiveClient = getClientByUserId(persistActiveAccount); + if (newActiveClient != null) { + setActiveClient(newActiveClient); + } + } + } catch (e) { + Logs().e( + 'Matrix::_retrievePersistedActiveAccount(): Error - $e', + ); + } + } + + Future _storePersistActiveAccount(Client newClient) async { + if (newClient.userID == null) return; + try { + Logs().e( + 'Matrix::_storePersistActiveAccount: clientName - ${newClient.clientName}', + ); + Logs().e( + 'Matrix::_storePersistActiveAccount: userId - ${newClient.userID}', + ); + final MultipleAccountRepository multipleAccountRepository = + getIt.get(); + await multipleAccountRepository.storePersistActiveAccount( + newClient.userID!, + ); + } catch (e) { + Logs().e( + 'Matrix::_storePersistActiveAccount(): Error - $e', + ); + } } void _migrateToMDatabase(Client client) async { @@ -800,9 +879,12 @@ class FixedThreepidCreds extends ThreepidCreds { } } -class _AccountBundleWithClient { +class _AccountBundleWithClient extends Equatable { final Client? client; final AccountBundle? bundle; - _AccountBundleWithClient({this.client, this.bundle}); + const _AccountBundleWithClient({this.client, this.bundle}); + + @override + List get props => [client, bundle]; } diff --git a/lib/widgets/set_active_client_state.dart b/lib/widgets/set_active_client_state.dart new file mode 100644 index 0000000000..5c03b74982 --- /dev/null +++ b/lib/widgets/set_active_client_state.dart @@ -0,0 +1,6 @@ +enum SetActiveClientState { + success, + unknownClient; + + bool get isSuccess => this == SetActiveClientState.success; +} diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index a6e7cccc5d..f6792a9c60 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -1,7 +1,4 @@ -import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; -import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; -import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -9,9 +6,7 @@ import 'package:fluffychat/widgets/mixins/show_dialog_mixin.dart'; import 'package:fluffychat/widgets/twake_components/twake_header_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; -import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; import 'package:matrix/matrix.dart'; class TwakeHeader extends StatelessWidget @@ -113,8 +108,7 @@ class TwakeHeader extends StatelessWidget hoverColor: Colors.transparent, splashColor: Colors.transparent, highlightColor: Colors.transparent, - onTap: () => - _displayMultipleAccountPicker(context), + onTap: controller.onClickAvatar, child: ValueListenableBuilder( valueListenable: controller.currentProfileNotifier, @@ -146,71 +140,6 @@ class TwakeHeader extends StatelessWidget ); } - void _displayMultipleAccountPicker(BuildContext context) async { - final multipleAccount = await _getMultipleAccount(); - multipleAccount.sort((pre, next) { - return pre.accountActiveStatus.index - .compareTo(next.accountActiveStatus.index); - }); - MultipleAccountPicker.showMultipleAccountPicker( - accounts: multipleAccount, - context: context, - onAddAnotherAccount: controller.onAddAnotherAccount, - onGoToAccountSettings: controller.onGoToAccountSettings, - onSetAccountAsActive: (account) => controller.onSetAccountAsActive( - multipleAccounts: multipleAccount, - account: account, - ), - titleAddAnotherAccount: L10n.of(context)!.addAnotherAccount, - titleAccountSettings: L10n.of(context)!.accountSettings, - logoApp: Padding( - padding: TwakeHeaderStyle.logoAppOfMultiplePadding, - child: SvgPicture.asset( - ImagePaths.icTwakeImageLogo, - width: TwakeHeaderStyle.logoAppOfMultipleWidth, - height: TwakeHeaderStyle.logoAppOfMultipleHeight, - ), - ), - accountNameStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: LinagoraSysColors.material().onSurface, - ), - accountIdStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: LinagoraRefColors.material().tertiary[20], - ), - addAnotherAccountStyle: Theme.of(context).textTheme.labelLarge!.copyWith( - color: LinagoraSysColors.material().onPrimary, - ), - titleAccountSettingsStyle: - Theme.of(context).textTheme.labelLarge!.copyWith( - color: LinagoraSysColors.material().primary, - ), - ); - } - - Future> _getMultipleAccount() async { - final profileBundles = await controller.getProfileBundles(); - return profileBundles - .where((profileBundle) => profileBundle != null) - .map( - (profileBundle) => TwakeChatPresentationAccount( - clientAccount: profileBundle!.client, - accountId: profileBundle.profileBundle.userId, - accountName: profileBundle.profileBundle.displayName ?? '', - avatar: Avatar( - mxContent: profileBundle.profileBundle.avatarUrl, - name: profileBundle.profileBundle.displayName ?? '', - size: TwakeHeaderStyle.avatarOfMultipleAccountSize, - fontSize: TwakeHeaderStyle.avatarFontSizeInAppBar, - ), - accountActiveStatus: profileBundle.profileBundle.userId == - controller.currentProfileNotifier.value.userId - ? AccountActiveStatus.active - : AccountActiveStatus.inactive, - ), - ) - .toList(); - } - @override Size get preferredSize => const Size.fromHeight(TwakeHeaderStyle.toolbarHeight); From f661cea1f5e0ca2981cb7a887661dbca858a3b66 Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Fri, 5 Jan 2024 12:08:20 +0100 Subject: [PATCH 042/183] TW-1152: Search recent chats and contact by mail (cherry picked from commit f0c0a509f5d824b4a20e8819fa2a04860058a03c) --- lib/presentation/model/search/presentation_search.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/presentation/model/search/presentation_search.dart b/lib/presentation/model/search/presentation_search.dart index 7657cd4c17..5fa9d1c7fe 100644 --- a/lib/presentation/model/search/presentation_search.dart +++ b/lib/presentation/model/search/presentation_search.dart @@ -5,7 +5,7 @@ import 'package:matrix/matrix.dart'; abstract class PresentationSearch extends Equatable { final String? displayName; - + final String? email; final String? directChatMatrixID; String get id; @@ -15,6 +15,7 @@ abstract class PresentationSearch extends Equatable { const PresentationSearch({ this.displayName, + this.email, this.directChatMatrixID, }); @@ -27,14 +28,14 @@ abstract class PresentationSearch extends Equatable { class ContactPresentationSearch extends PresentationSearch { final String? matrixId; - final String? email; const ContactPresentationSearch({ this.matrixId, - this.email, + String? email, String? displayName, }) : super( displayName: displayName, + email: email, ); @override From 40bc0234c27d631817b2f22b3d4cb8c59314cb27 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 4 Jan 2024 15:59:07 +0700 Subject: [PATCH 043/183] Support and handle recovery data when logged new account (cherry picked from commit 6d43a1b415a7b8ee40ed874f9a9a4a1ce301127c) --- lib/pages/chat_list/chat_list.dart | 11 +++++++++++ .../agruments/logged_in_other_account_body_args.dart | 12 ++++++++++++ lib/widgets/matrix.dart | 3 ++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 lib/widgets/layouts/agruments/logged_in_other_account_body_args.dart diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index f3302af243..fc7f696734 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -27,6 +27,7 @@ import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; @@ -767,6 +768,15 @@ class ChatListController extends State ); } + void _handleRecovery() { + if (widget.adaptiveScaffoldBodyArgs is LoggedInOtherAccountBodyArgs) { + Logs().d( + "ChatList::_handleAnotherAccountAdded(): Handle recovery data for another account", + ); + _waitForFirstSync(); + } + } + @override void didUpdateWidget(covariant ChatList oldWidget) { Logs().d( @@ -780,6 +790,7 @@ class ChatListController extends State setState(() { _getCurrentProfile(newActiveClient); _clientStream.add(newActiveClient); + _handleRecovery(); }); } super.didUpdateWidget(oldWidget); diff --git a/lib/widgets/layouts/agruments/logged_in_other_account_body_args.dart b/lib/widgets/layouts/agruments/logged_in_other_account_body_args.dart new file mode 100644 index 0000000000..c48f847f34 --- /dev/null +++ b/lib/widgets/layouts/agruments/logged_in_other_account_body_args.dart @@ -0,0 +1,12 @@ +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; + +class LoggedInOtherAccountBodyArgs extends AbsAppAdaptiveScaffoldBodyArgs { + const LoggedInOtherAccountBodyArgs({ + required super.newActiveClient, + }); + + @override + List get props => [ + newActiveClient, + ]; +} diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 96d8f60747..35b8af2b68 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -26,6 +26,7 @@ import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; import 'package:fluffychat/widgets/set_active_client_state.dart'; import 'package:fluffychat/widgets/twake_app.dart'; @@ -207,7 +208,7 @@ class MatrixState extends State if (result.isSuccess) { TwakeApp.router.go( '/rooms', - extra: LoggedInBodyArgs( + extra: LoggedInOtherAccountBodyArgs( newActiveClient: activeClient, ), ); From a9030d48427c05ee0adc3d865fcc48eae740f2c7 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 9 Jan 2024 19:07:40 +0700 Subject: [PATCH 044/183] TW-1315: Integrate sign up for mobile (cherry picked from commit 1590a42dd7073d325898f802e384df379a1653f6) --- lib/pages/twake_id/twake_id.dart | 45 ++++++++++++++++++--------- lib/pages/twake_id/twake_id_view.dart | 1 + 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/pages/twake_id/twake_id.dart b/lib/pages/twake_id/twake_id.dart index bc45d15eb0..a2a540fb15 100644 --- a/lib/pages/twake_id/twake_id.dart +++ b/lib/pages/twake_id/twake_id.dart @@ -29,6 +29,7 @@ class TwakeIdArg extends Equatable { class TwakeId extends StatefulWidget { final TwakeIdArg? arg; + const TwakeId({super.key, this.arg}); @override @@ -53,25 +54,17 @@ class TwakeIdController extends State { String loginUrl = "${AppConfig.registrationUrl}?$postLoginRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; + String signupUrl = + "${AppConfig.registrationUrl}?$postRegisteredRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; + MatrixState get matrix => Matrix.of(context); - void onClickSignIn() async { - matrix.loginHomeserverSummary = - await matrix.getLoginClient().checkHomeserver( - Uri.parse(AppConfig.twakeWorkplaceHomeserver), - ); - final uri = await FlutterWebAuth2.authenticate( - url: loginUrl, - callbackUrlScheme: AppConfig.appOpenUrlScheme, - options: const FlutterWebAuth2Options( - intentFlags: ephemeralIntentFlags, - ), - ); - Logs().d("TwakeIdController: onClickSignIn: uri: $uri"); - _handleLoginToken(uri); + void onClickSignIn() { + Logs().d("TwakeIdController::onClickSignIn: Login Url - $loginUrl"); + _redirectRegistrationUrl(loginUrl); } - void _handleLoginToken(String uri) async { + void _handleTokenFromRegistrationSite(String uri) async { final token = Uri.parse(uri).queryParameters['loginToken']; Logs().d("TwakeIdController: _handleLoginToken: token: $token"); if (token?.isEmpty ?? false) return; @@ -85,6 +78,28 @@ class TwakeIdController extends State { ); } + void _redirectRegistrationUrl(String url) async { + matrix.loginHomeserverSummary = + await matrix.getLoginClient().checkHomeserver( + Uri.parse(AppConfig.twakeWorkplaceHomeserver), + ); + final uri = await FlutterWebAuth2.authenticate( + url: url, + callbackUrlScheme: AppConfig.appOpenUrlScheme, + options: const FlutterWebAuth2Options( + intentFlags: ephemeralIntentFlags, + ), + ); + Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); + _handleTokenFromRegistrationSite(uri); + } + + void onClickCreateTwakeId() { + Logs() + .d("TwakeIdController::onClickCreateTwakeId: Signup Url - $signupUrl"); + _redirectRegistrationUrl(signupUrl); + } + @override Widget build(BuildContext context) { return TwakeIdView(controller: this); diff --git a/lib/pages/twake_id/twake_id_view.dart b/lib/pages/twake_id/twake_id_view.dart index 62578c5650..48b9f554de 100644 --- a/lib/pages/twake_id/twake_id_view.dart +++ b/lib/pages/twake_id/twake_id_view.dart @@ -23,6 +23,7 @@ class TwakeIdView extends StatelessWidget { description: L10n.of(context)!.descriptionTwakeId, onUseCompanyServerOnTap: controller.goToHomeserverPicker, onSignInOnTap: controller.onClickSignIn, + onCreateTwakeIdOnTap: controller.onClickCreateTwakeId, backButton: Padding( padding: const EdgeInsets.only(left: 8), child: TwakeIconButton( From 8f728797ded562be584c2ccba95643ca31cb2d25 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 18 Jan 2024 15:36:22 +0700 Subject: [PATCH 045/183] TW-1355: Rework TwakeId screen (cherry picked from commit 221cbdfd0ee64f4efd84917d06c92d4e79d0ec4f) --- .../auto_homeserver_picker_view.dart | 15 ++- .../auto_homeserver_picker_view_style.dart | 15 +++ .../multiple_accounts_picker.dart | 6 +- lib/pages/twake_id/twake_id.dart | 107 ------------------ lib/pages/twake_id/twake_id_view.dart | 37 ------ .../twake_welcome/twake_id_view_style.dart | 4 + lib/pages/twake_welcome/twake_welcome.dart | 89 ++++++++++++++- .../twake_welcome/twake_welcome_view.dart | 9 +- 8 files changed, 122 insertions(+), 160 deletions(-) create mode 100644 lib/pages/auto_homeserver_picker/auto_homeserver_picker_view_style.dart delete mode 100644 lib/pages/twake_id/twake_id.dart delete mode 100644 lib/pages/twake_id/twake_id_view.dart create mode 100644 lib/pages/twake_welcome/twake_id_view_style.dart diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart index c59650f917..6dcfea9c10 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart @@ -1,11 +1,10 @@ import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; -import 'package:fluffychat/pages/twake_welcome/twake_welcome_view_style.dart'; +import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_view_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:linagora_design_flutter/twake_screen/twake_welcome_screen_style.dart'; class AutoHomeserverPickerView extends StatelessWidget { final AutoHomeserverPickerController controller; @@ -33,11 +32,11 @@ class AutoHomeserverPickerView extends StatelessWidget { ), SvgPicture.asset( ImagePaths.logoTwakeWelcome, - width: TwakeWelcomeViewStyle.logoWidth, - height: TwakeWelcomeViewStyle.logoHeight, + width: AutoHomeserverPickerViewStyle.logoWidth, + height: AutoHomeserverPickerViewStyle.logoHeight, ), Padding( - padding: TwakeWelcomeScreenStyle.descriptionPadding, + padding: AutoHomeserverPickerViewStyle.descriptionPadding, child: Text( L10n.of(context)!.descriptionWelcomeTo, textAlign: TextAlign.center, @@ -66,7 +65,7 @@ class AutoHomeserverPickerView extends StatelessWidget { return const SizedBox(); }, child: Padding( - padding: TwakeWelcomeScreenStyle.buttonPadding, + padding: AutoHomeserverPickerViewStyle.buttonPadding, child: InkWell( highlightColor: Colors.transparent, splashColor: Colors.transparent, @@ -74,7 +73,7 @@ class AutoHomeserverPickerView extends StatelessWidget { hoverColor: Colors.transparent, onTap: controller.retryCheckHomeserver, child: Container( - height: TwakeWelcomeScreenStyle.buttonHeight, + height: AutoHomeserverPickerViewStyle.buttonHeight, padding: const EdgeInsets.symmetric( horizontal: 40, ), @@ -83,7 +82,7 @@ class AutoHomeserverPickerView extends StatelessWidget { color: LinagoraSysColors.material().primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( - TwakeWelcomeScreenStyle.buttonRadius, + AutoHomeserverPickerViewStyle.buttonRadius, ), ), ), diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view_style.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view_style.dart new file mode 100644 index 0000000000..7c255a5d2c --- /dev/null +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view_style.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class AutoHomeserverPickerViewStyle { + static const double logoWidth = 257; + static const double logoHeight = 179; + static const double buttonHeight = 56; + static const double buttonRadius = 100; + + static const EdgeInsets descriptionPadding = + EdgeInsets.symmetric(horizontal: 8); + + static const EdgeInsets buttonPadding = EdgeInsets.only( + bottom: 44, + ); +} diff --git a/lib/pages/multiple_accounts/multiple_accounts_picker.dart b/lib/pages/multiple_accounts/multiple_accounts_picker.dart index de87e3b537..6d6aad7257 100644 --- a/lib/pages/multiple_accounts/multiple_accounts_picker.dart +++ b/lib/pages/multiple_accounts/multiple_accounts_picker.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:fluffychat/pages/twake_id/twake_id.dart'; +import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; import 'package:fluffychat/presentation/extensions/multiple_accounts/client_profile_extension.dart'; import 'package:fluffychat/presentation/multiple_account/client_profile_presentation.dart'; import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; @@ -120,8 +120,8 @@ class MultipleAccountsPickerController { void _onAddAnotherAccount() { context.go( '/rooms/addaccount', - extra: const TwakeIdArg( - twakeIdType: TwakeIdType.otherAccounts, + extra: const TwakeWelcomeArg( + twakeIdType: TwakeWelcomeType.otherAccounts, ), ); } diff --git a/lib/pages/twake_id/twake_id.dart b/lib/pages/twake_id/twake_id.dart deleted file mode 100644 index a2a540fb15..0000000000 --- a/lib/pages/twake_id/twake_id.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:fluffychat/config/app_config.dart'; -import 'package:equatable/equatable.dart'; -import 'package:fluffychat/pages/twake_id/twake_id_view.dart'; -import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -enum TwakeIdType { - firstAccount, - otherAccounts, -} - -class TwakeIdArg extends Equatable { - final TwakeIdType twakeIdType; - - const TwakeIdArg({ - this.twakeIdType = TwakeIdType.firstAccount, - }); - - bool get isAddAnotherAccount => twakeIdType == TwakeIdType.otherAccounts; - - @override - List get props => [twakeIdType]; -} - -class TwakeId extends StatefulWidget { - final TwakeIdArg? arg; - - const TwakeId({super.key, this.arg}); - - @override - State createState() => TwakeIdController(); -} - -class TwakeIdController extends State { - void goToHomeserverPicker() { - if (widget.arg?.isAddAnotherAccount == true) { - context.push('/rooms/addhomeserver'); - } else { - context.push('/home/homeserverpicker'); - } - } - - static const String postLoginRedirectUrlPathParams = - 'post_login_redirect_url'; - - static const String postRegisteredRedirectUrlPathParams = - 'post_registered_redirect_url'; - - String loginUrl = - "${AppConfig.registrationUrl}?$postLoginRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; - - String signupUrl = - "${AppConfig.registrationUrl}?$postRegisteredRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; - - MatrixState get matrix => Matrix.of(context); - - void onClickSignIn() { - Logs().d("TwakeIdController::onClickSignIn: Login Url - $loginUrl"); - _redirectRegistrationUrl(loginUrl); - } - - void _handleTokenFromRegistrationSite(String uri) async { - final token = Uri.parse(uri).queryParameters['loginToken']; - Logs().d("TwakeIdController: _handleLoginToken: token: $token"); - if (token?.isEmpty ?? false) return; - Matrix.of(context).loginType = LoginType.mLoginToken; - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context).getLoginClient().login( - LoginType.mLoginToken, - token: token, - initialDeviceDisplayName: PlatformInfos.clientName, - ), - ); - } - - void _redirectRegistrationUrl(String url) async { - matrix.loginHomeserverSummary = - await matrix.getLoginClient().checkHomeserver( - Uri.parse(AppConfig.twakeWorkplaceHomeserver), - ); - final uri = await FlutterWebAuth2.authenticate( - url: url, - callbackUrlScheme: AppConfig.appOpenUrlScheme, - options: const FlutterWebAuth2Options( - intentFlags: ephemeralIntentFlags, - ), - ); - Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); - _handleTokenFromRegistrationSite(uri); - } - - void onClickCreateTwakeId() { - Logs() - .d("TwakeIdController::onClickCreateTwakeId: Signup Url - $signupUrl"); - _redirectRegistrationUrl(signupUrl); - } - - @override - Widget build(BuildContext context) { - return TwakeIdView(controller: this); - } -} diff --git a/lib/pages/twake_id/twake_id_view.dart b/lib/pages/twake_id/twake_id_view.dart deleted file mode 100644 index 48b9f554de..0000000000 --- a/lib/pages/twake_id/twake_id_view.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:fluffychat/pages/twake_id/twake_id.dart'; -import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:linagora_design_flutter/linagora_design_flutter.dart'; - -class TwakeIdView extends StatelessWidget { - final TwakeIdController controller; - - const TwakeIdView({super.key, required this.controller}); - - @override - Widget build(BuildContext context) { - return TwakeIdScreen( - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - highlightColor: Colors.transparent, - overlayColor: MaterialStateProperty.all(Colors.transparent), - signInTitle: L10n.of(context)!.signIn, - createTwakeIdTitle: L10n.of(context)!.createTwakeId, - useCompanyServerTitle: L10n.of(context)!.useYourCompanyServer, - description: L10n.of(context)!.descriptionTwakeId, - onUseCompanyServerOnTap: controller.goToHomeserverPicker, - onSignInOnTap: controller.onClickSignIn, - onCreateTwakeIdOnTap: controller.onClickCreateTwakeId, - backButton: Padding( - padding: const EdgeInsets.only(left: 8), - child: TwakeIconButton( - icon: Icons.arrow_back, - onTap: () => context.pop(), - tooltip: L10n.of(context)!.back, - ), - ), - ); - } -} diff --git a/lib/pages/twake_welcome/twake_id_view_style.dart b/lib/pages/twake_welcome/twake_id_view_style.dart new file mode 100644 index 0000000000..7f5f1b5474 --- /dev/null +++ b/lib/pages/twake_welcome/twake_id_view_style.dart @@ -0,0 +1,4 @@ +class TwakeIdViewStyle { + static const double logoWidth = 257; + static const double logoHeight = 179; +} diff --git a/lib/pages/twake_welcome/twake_welcome.dart b/lib/pages/twake_welcome/twake_welcome.dart index 9dd09b257c..987a970464 100644 --- a/lib/pages/twake_welcome/twake_welcome.dart +++ b/lib/pages/twake_welcome/twake_welcome.dart @@ -1,9 +1,35 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:equatable/equatable.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome_view.dart'; +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +enum TwakeWelcomeType { + firstAccount, + otherAccounts, +} + +class TwakeWelcomeArg extends Equatable { + final TwakeWelcomeType twakeIdType; + + const TwakeWelcomeArg({ + this.twakeIdType = TwakeWelcomeType.firstAccount, + }); + + bool get isAddAnotherAccount => twakeIdType == TwakeWelcomeType.otherAccounts; + + @override + List get props => [twakeIdType]; +} class TwakeWelcome extends StatefulWidget { - const TwakeWelcome({super.key}); + final TwakeWelcomeArg? arg; + const TwakeWelcome({super.key, this.arg}); @override State createState() => TwakeWelcomeController(); @@ -11,7 +37,66 @@ class TwakeWelcome extends StatefulWidget { class TwakeWelcomeController extends State { void goToHomeserverPicker() { - context.push('/home/homeserverpicker'); + if (widget.arg?.isAddAnotherAccount == true) { + context.push('/rooms/addhomeserver'); + } else { + context.push('/home/homeserverpicker'); + } + } + + static const String postLoginRedirectUrlPathParams = + 'post_login_redirect_url'; + + static const String postRegisteredRedirectUrlPathParams = + 'post_registered_redirect_url'; + + String loginUrl = + "${AppConfig.registrationUrl}?$postLoginRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; + + String signupUrl = + "${AppConfig.registrationUrl}?$postRegisteredRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; + + MatrixState get matrix => Matrix.of(context); + + void onClickSignIn() { + Logs().d("TwakeIdController::onClickSignIn: Login Url - $loginUrl"); + _redirectRegistrationUrl(loginUrl); + } + + void _handleTokenFromRegistrationSite(String uri) async { + final token = Uri.parse(uri).queryParameters['loginToken']; + Logs().d("TwakeIdController: _handleLoginToken: token: $token"); + if (token?.isEmpty ?? false) return; + Matrix.of(context).loginType = LoginType.mLoginToken; + await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => Matrix.of(context).getLoginClient().login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ), + ); + } + + void _redirectRegistrationUrl(String url) async { + matrix.loginHomeserverSummary = + await matrix.getLoginClient().checkHomeserver( + Uri.parse(AppConfig.twakeWorkplaceHomeserver), + ); + final uri = await FlutterWebAuth2.authenticate( + url: url, + callbackUrlScheme: AppConfig.appOpenUrlScheme, + options: const FlutterWebAuth2Options( + intentFlags: ephemeralIntentFlags, + ), + ); + Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); + _handleTokenFromRegistrationSite(uri); + } + + void onClickCreateTwakeId() { + Logs() + .d("TwakeIdController::onClickCreateTwakeId: Signup Url - $signupUrl"); + _redirectRegistrationUrl(signupUrl); } @override diff --git a/lib/pages/twake_welcome/twake_welcome_view.dart b/lib/pages/twake_welcome/twake_welcome_view.dart index b8a284af89..78d282988b 100644 --- a/lib/pages/twake_welcome/twake_welcome_view.dart +++ b/lib/pages/twake_welcome/twake_welcome_view.dart @@ -1,5 +1,5 @@ +import 'package:fluffychat/pages/twake_welcome/twake_id_view_style.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; -import 'package:fluffychat/pages/twake_welcome/twake_welcome_view_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -18,13 +18,16 @@ class TwakeWelcomeView extends StatelessWidget { hoverColor: Colors.transparent, highlightColor: Colors.transparent, overlayColor: MaterialStateProperty.all(Colors.transparent), + signInTitle: L10n.of(context)!.signIn, + createTwakeIdTitle: L10n.of(context)!.createTwakeId, useCompanyServerTitle: L10n.of(context)!.useYourCompanyServer, description: L10n.of(context)!.descriptionTwakeId, onUseCompanyServerOnTap: controller.goToHomeserverPicker, + onSignInOnTap: controller.onClickSignIn, logo: SvgPicture.asset( ImagePaths.logoTwakeWelcome, - width: TwakeWelcomeViewStyle.logoWidth, - height: TwakeWelcomeViewStyle.logoHeight, + width: TwakeIdViewStyle.logoWidth, + height: TwakeIdViewStyle.logoHeight, ), ); } From 950b84d877f05b4b0a31e4c644e224f97ee6667c Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 8 Jan 2024 16:43:36 +0700 Subject: [PATCH 046/183] TW-1293: Remove setState and update chat backup switch toggle (cherry picked from commit 89ff3fae003c9381db92ee38e2e54d7efe7fda7f) --- .../settings_dashboard/settings/settings.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 82853926c9..9f51b293a3 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -54,6 +54,10 @@ class SettingsController extends State with ConnectPageMixin { final ValueNotifier optionsSelectNotifier = ValueNotifier(null); + final ValueNotifier showChatBackupSwitch = ValueNotifier(null); + + MatrixState get matrix => Matrix.of(context); + String get displayName => displayNameNotifier.value ?? client.mxid(context).localpart ?? @@ -63,7 +67,7 @@ class SettingsController extends State with ConnectPageMixin { settingEnum == optionsSelectNotifier.value; void logoutAction() async { - final noBackup = showChatBackupBanner == true; + final noBackup = showChatBackupSwitch.value == true; final twakeContext = TwakeApp.routerKey.currentContext; if (twakeContext == null) { Logs().e( @@ -134,16 +138,11 @@ class SettingsController extends State with ConnectPageMixin { client.encryption?.crossSigning.enabled == false || crossSigning == false; final isUnknownSession = client.isUnknownSession; - setState(() { - showChatBackupBanner = needsBootstrap || isUnknownSession; - }); + showChatBackupSwitch.value = needsBootstrap || isUnknownSession; } - bool? crossSigningCached; - bool? showChatBackupBanner; - void firstRunBootstrapAction([_]) async { - if (showChatBackupBanner != true) { + if (showChatBackupSwitch.value != true) { showOkAlertDialog( context: context, title: L10n.of(context)!.chatBackup, From d658d1c4f15d77e308d5d2d164248634bc2086b3 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 8 Jan 2024 16:44:45 +0700 Subject: [PATCH 047/183] TW-1293: Enable chat backup switch toggle in Settings (cherry picked from commit e10ce9a949a5782e65c50c2817870e3963b22af5) --- .../settings/settings_view.dart | 19 +++++++++++++++++++ .../settings/settings_view_style.dart | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart index 93bbbcdf79..699e401eec 100644 --- a/lib/pages/settings_dashboard/settings/settings_view.dart +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -149,6 +149,25 @@ class SettingsView extends StatelessWidget { ), ), const Divider(thickness: 1), + if (!controller.matrix.twakeSupported) + ValueListenableBuilder( + valueListenable: controller.showChatBackupSwitch, + builder: (context, backUpAvailable, child) { + return SwitchListTile( + controlAffinity: ListTileControlAffinity.trailing, + contentPadding: SettingsViewStyle.backupSwitchPadding, + value: backUpAvailable == false, + secondary: const Icon(Icons.backup_outlined), + title: Text(L10n.of(context)!.chatBackup), + onChanged: controller.firstRunBootstrapAction, + ); + }, + child: ListTile( + leading: const Icon(Icons.backup_outlined), + title: Text(L10n.of(context)!.chatBackup), + trailing: const CircularProgressIndicator.adaptive(), + ), + ), Column( children: controller.getListSettingItem.map((item) { return Padding( diff --git a/lib/pages/settings_dashboard/settings/settings_view_style.dart b/lib/pages/settings_dashboard/settings/settings_view_style.dart index 9d1bcdac0a..f83c59116e 100644 --- a/lib/pages/settings_dashboard/settings/settings_view_style.dart +++ b/lib/pages/settings_dashboard/settings/settings_view_style.dart @@ -24,6 +24,11 @@ class SettingsViewStyle { horizontal: 8, ); + static EdgeInsetsDirectional backupSwitchPadding = + const EdgeInsetsDirectional.symmetric( + horizontal: 24, + ); + static EdgeInsetsDirectional avatarPadding = const EdgeInsetsDirectional.only(end: 8); } From 0143cc3082ef8e16364daf9948af04a4142d5bc8 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 8 Jan 2024 17:18:56 +0700 Subject: [PATCH 048/183] TW-1293: Setup ToM services after add another account success TW-1293: Update UI for loading dialog (cherry picked from commit 241a054f8bb66436603de7fbacdfa2612bff0fbb) --- lib/pages/bootstrap/bootstrap_dialog.dart | 24 ++- .../settings_dashboard/settings/settings.dart | 12 ++ lib/widgets/matrix.dart | 184 ++++++++++-------- 3 files changed, 135 insertions(+), 85 deletions(-) diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 20c4ec7107..bca0fa0442 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -4,7 +4,6 @@ import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/adaptive_flat_button.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -95,7 +94,7 @@ class BootstrapDialogState extends State { Widget build(BuildContext context) { _wipe ??= widget.wipe; final buttons = []; - Widget body = const CupertinoActivityIndicator(); + Widget body = const CircularProgressIndicator.adaptive(); titleText = L10n.of(context)!.loadingPleaseWait; if (bootstrap.newSsssKey?.recoveryKey != null && @@ -446,11 +445,22 @@ class BootstrapDialogState extends State { } } - final title = Text(titleText!); - return CupertinoAlertDialog( - title: title, - content: body, - actions: buttons, + return AlertDialog( + content: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: body, + ), + Expanded( + child: Text( + titleText!, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + actions: buttons.isNotEmpty ? buttons : null, ); } } diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 9f51b293a3..8a9fa0b53b 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -133,12 +133,24 @@ class SettingsController extends State with ConnectPageMixin { } final crossSigning = await client.encryption?.crossSigning.isCached() ?? false; + Logs().d( + "SettingsController::checkBootstrap() - crossSigning: $crossSigning", + ); final needsBootstrap = await client.encryption?.keyManager.isCached() == false || client.encryption?.crossSigning.enabled == false || crossSigning == false; + Logs().d( + "SettingsController::checkBootstrap() - needsBootstrap: $needsBootstrap", + ); final isUnknownSession = client.isUnknownSession; + Logs().d( + "SettingsController::checkBootstrap() - isUnknownSession: $isUnknownSession", + ); showChatBackupSwitch.value = needsBootstrap || isUnknownSession; + Logs().d( + "SettingsController::checkBootstrap() - showChatBackupSwitch: ${showChatBackupSwitch.value}", + ); } void firstRunBootstrapAction([_]) async { diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 35b8af2b68..951e201369 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -187,37 +187,16 @@ class MatrixState extends State final candidate = _loginClientCandidate ??= ClientManager.createClient( '${AppConfig.applicationName}-${DateTime.now().millisecondsSinceEpoch}', )..onLoginStateChanged - .stream - .where((l) => l == LoginState.loggedIn) - .first - .then((_) { - Logs().d( - 'MatrixState::getLoginClient() Login successful - Client ${_loginClientCandidate!.clientName}', - ); - if (!widget.clients.contains(_loginClientCandidate)) { - widget.clients.add(_loginClientCandidate!); - } - ClientManager.addClientNameToStore(_loginClientCandidate!.clientName); - Logs().d('MatrixState::getLoginClient() Registering subs'); - _registerSubs(_loginClientCandidate!.clientName); - final activeClient = getClientByName( - _loginClientCandidate!.clientName, - ); - if (activeClient == null) return; - final result = await setActiveClient(activeClient); - if (result.isSuccess) { - TwakeApp.router.go( - '/rooms', - extra: LoggedInOtherAccountBodyArgs( - newActiveClient: activeClient, - ), - ); - _loginClientCandidate = null; - } - }); + .stream + .where((l) => l == LoginState.loggedIn) + .first + .then((_) => _handleAddAnotherAccount()); return candidate; } + Client? getClientByUserId(String userId) => + widget.clients.firstWhereOrNull((c) => c.userID == userId); + Client? getClientByName(String name) => widget.clients.firstWhereOrNull((c) => c.clientName == name); @@ -343,8 +322,8 @@ class MatrixState extends State } void _registerSubs(String name) async { - final c = getClientByName(name); - if (c == null) { + final currentClient = getClientByName(name); + if (currentClient == null) { Logs().w( 'Attempted to register subscriptions for non-existing client $name', ); @@ -353,8 +332,8 @@ class MatrixState extends State if (PlatformInfos.isMobile) { await HiveCollectionToMDatabase.databaseBuilder(); } - onRoomKeyRequestSub[name] ??= - c.onRoomKeyRequest.stream.listen((RoomKeyRequest request) async { + onRoomKeyRequestSub[name] ??= currentClient.onRoomKeyRequest.stream + .listen((RoomKeyRequest request) async { if (widget.clients.any( ((cl) => cl.userID == request.requestingDevice.userId && @@ -366,7 +345,8 @@ class MatrixState extends State await request.forwardKey(); } }); - onKeyVerificationRequestSub[name] ??= c.onKeyVerificationRequest.stream + onKeyVerificationRequestSub[name] ??= currentClient + .onKeyVerificationRequest.stream .listen((KeyVerification request) async { var hidPopup = false; request.onUpdate = () { @@ -381,64 +361,112 @@ class MatrixState extends State hidPopup = true; await KeyVerificationDialog(request: request).show(context); }); - onLoginStateChanged[name] ??= - c.onLoginStateChanged.stream.listen((state) async { - final loggedInWithMultipleClients = widget.clients.length > 1; - if (loggedInWithMultipleClients && state != LoginState.loggedIn) { - _cancelSubs(c.clientName); - widget.clients.remove(c); - ClientManager.removeClientNameFromStore(c.clientName); - TwakeSnackBar.show( - TwakeApp.routerKey.currentContext!, - L10n.of(context)!.oneClientLoggedOut, - ); - final result = await setActiveClient(widget.clients.first); - if (state != LoginState.loggedIn && result.isSuccess) { - TwakeApp.router.go( - '/rooms', - extra: LogoutBodyArgs( - newActiveClient: widget.clients.first, - ), - ); - } - } else { - if (state == LoginState.loggedIn) { - Logs().v('[MATRIX] Log in successful'); - setUpToMServicesInLogin(c); - _storePersistActiveAccount(c); - TwakeApp.router.go( - '/rooms', - extra: LoggedInBodyArgs( - newActiveClient: c, - ), - ); - } else { - Logs().v('[MATRIX] Log out successful'); - if (PlatformInfos.isMobile) { - TwakeApp.router.go('/home/twakeWelcome'); - } else { - TwakeApp.router.go('/home', extra: true); - } - } - } - }); - onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler); + onLoginStateChanged[name] ??= currentClient.onLoginStateChanged.stream + .listen((state) => _listenLoginStateChanged(state, currentClient)); + onUiaRequest[name] ??= + currentClient.onUiaRequest.stream.listen(uiaRequestHandler); if (PlatformInfos.isWeb || PlatformInfos.isLinux) { - c.onSync.stream.first.then((s) { + currentClient.onSync.stream.first.then((s) { html.Notification.requestPermission(); - onNotification[name] ??= c.onEvent.stream + onNotification[name] ??= currentClient.onEvent.stream .where( (e) => e.type == EventUpdateType.timeline && [EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] .contains(e.content['type']) && - e.content['sender'] != c.userID, + e.content['sender'] != currentClient.userID, ) .listen(showLocalNotification); }); } } + void _listenLoginStateChanged(LoginState state, Client client) async { + final loggedInWithMultipleClients = widget.clients.length > 1; + if (loggedInWithMultipleClients && state != LoginState.loggedIn) { + _handleLogoutWithMultipleAccount(state, client); + } else { + if (state == LoginState.loggedIn) { + Logs().v('[MATRIX]:_listenLoginStateChanged:: First Log in successful'); + _handleFirstLoggedIn(client); + } else { + Logs().v('[MATRIX]:_listenLoginStateChanged:: Log out successful'); + if (PlatformInfos.isMobile) { + _deletePersistActiveAccount(state); + TwakeApp.router.go('/home/twakeid'); + } else { + TwakeApp.router.go('/home', extra: true); + } + } + } + } + + void _handleLogoutWithMultipleAccount( + LoginState state, + Client currentClient, + ) async { + _cancelSubs(currentClient.clientName); + widget.clients.remove(currentClient); + ClientManager.removeClientNameFromStore(currentClient.clientName); + TwakeSnackBar.show( + TwakeApp.routerKey.currentContext!, + L10n.of(context)!.oneClientLoggedOut, + ); + final result = await setActiveClient(widget.clients.first); + Logs().v( + '[MATRIX]:_handleLogoutWithMultipleAccount:: Log out Client ${currentClient.clientName} successful', + ); + if (state != LoginState.loggedIn && result.isSuccess) { + TwakeApp.router.go( + '/rooms', + extra: LogoutBodyArgs( + newActiveClient: widget.clients.first, + ), + ); + } + } + + void _handleFirstLoggedIn(Client newActiveClient) { + setUpToMServicesInLogin(newActiveClient); + _storePersistActiveAccount(newActiveClient); + TwakeApp.router.go( + '/rooms', + extra: LoggedInBodyArgs( + newActiveClient: newActiveClient, + ), + ); + } + + Future _handleAddAnotherAccount() async { + Logs().d( + 'MatrixState::_handleAddAnotherAccount() - Add another account successful', + ); + Logs().d( + 'MatrixState::_handleAddAnotherAccount() - New Client ${_loginClientCandidate!.clientName}', + ); + if (!widget.clients.contains(_loginClientCandidate)) { + widget.clients.add(_loginClientCandidate!); + } + ClientManager.addClientNameToStore(_loginClientCandidate!.clientName); + Logs().d('MatrixState::_handleAddAnotherAccount() - Registering subs'); + _registerSubs(_loginClientCandidate!.clientName); + final activeClient = getClientByName( + _loginClientCandidate!.clientName, + ); + if (activeClient == null) return; + setUpToMServicesInLogin(activeClient); + final result = await setActiveClient(activeClient); + if (result.isSuccess) { + TwakeApp.router.go( + '/rooms', + extra: LoggedInOtherAccountBodyArgs( + newActiveClient: activeClient, + ), + ); + _loginClientCandidate = null; + } + } + void _deletePersistActiveAccount(LoginState state) async { try { final multipleAccountRepository = getIt.get(); From 01a7670630263dcd2cbb0943753fb00f3a27f41d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 19 Jan 2024 18:13:15 +0700 Subject: [PATCH 049/183] TW-1367: Add public platform in configuration env (cherry picked from commit 9d041946093fed70dcfb5cf17831701220462796) --- lib/config/app_config.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 65146afe11..4e7d5dd8b3 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -27,6 +27,14 @@ abstract class AppConfig { static String homeserver = 'https://example.com/'; + static const String postLoginRedirectUrlPathParams = + 'post_login_redirect_url'; + + static const String postRegisteredRedirectUrlPathParams = + 'post_registered_redirect_url'; + + static String? platform; + static double toolbarHeight(BuildContext context) => responsive.isMobile(context) ? 48 : 56; static const Color chatColor = primaryColor; @@ -161,5 +169,8 @@ abstract class AppConfig { if (json['app_grid_dashboard_available'] is bool) { appGridDashboardAvailable = json['app_grid_dashboard_available']; } + if (json['platform'] is String?) { + platform = json['platform']; + } } } From 489dd220711fb7f90d9cf21cd916aabc48a05502 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 19 Jan 2024 18:14:47 +0700 Subject: [PATCH 050/183] TW-1367: Handle go to the register public site automatically (cherry picked from commit 1c9739271d7eefeec7e788df464c8b56b309a89c) --- lib/config/app_config.dart | 6 - .../auto_homeserver_picker.dart | 70 +++- lib/pages/chat_list/chat_list_body_view.dart | 7 +- lib/pages/chat_list/chat_list_header.dart | 7 +- lib/pages/chat_list/chat_list_view.dart | 7 +- lib/pages/connect/connect_page_mixin.dart | 63 ++++ .../twake_welcome/twake_id_view_style.dart | 4 - lib/pages/twake_welcome/twake_welcome.dart | 27 +- .../twake_welcome/twake_welcome_view.dart | 7 +- .../app_adaptive_scaffold_body.dart | 2 +- lib/widgets/matrix.dart | 21 +- .../twake_components/twake_header.dart | 1 + pubspec.lock | 314 +++++++++--------- pubspec.yaml | 2 +- .../contacts/contacts_manager_test.mocks.dart | 1 + 15 files changed, 318 insertions(+), 221 deletions(-) delete mode 100644 lib/pages/twake_welcome/twake_id_view_style.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 4e7d5dd8b3..ab41d9933d 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -27,12 +27,6 @@ abstract class AppConfig { static String homeserver = 'https://example.com/'; - static const String postLoginRedirectUrlPathParams = - 'post_login_redirect_url'; - - static const String postRegisteredRedirectUrlPathParams = - 'post_registered_redirect_url'; - static String? platform; static double toolbarHeight(BuildContext context) => diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart index 43b05c6d77..840b377f5b 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart @@ -24,11 +24,18 @@ class AutoHomeserverPickerController extends State with ConnectPageMixin { static const Duration autoHomeserverPickerTimeout = Duration(seconds: 30); + static const _saasPlatform = 'saas'; + final showButtonRetryNotifier = ValueNotifier(false); MatrixState get matrix => Matrix.of(context); - void _autoCheckHomeserver() async { + bool get _isSaasPlatform => + AppConfig.platform != null && + AppConfig.platform!.isNotEmpty && + AppConfig.platform == _saasPlatform; + + void _autoConnectHomeserver() async { try { matrix.loginHomeserverSummary = await matrix .getLoginClient() @@ -93,18 +100,71 @@ class AutoHomeserverPickerController extends State void retryCheckHomeserver() { showButtonRetryNotifier.toggle(); - _autoCheckHomeserver(); + if (_isSaasPlatform) { + _autoConnectSaas(); + } else { + _autoConnectHomeserver(); + } } - @override - void initState() { + void _autoConnectSaas() async { + matrix.loginHomeserverSummary = + await matrix.getLoginClient().checkHomeserver( + Uri.parse(AppConfig.twakeWorkplaceHomeserver), + ); + Map? rawLoginTypes; + await Matrix.of(context) + .getLoginClient() + .request( + RequestType.GET, + '/client/r0/login', + ) + .then((loginTypes) => rawLoginTypes = loginTypes) + .timeout( + autoHomeserverPickerTimeout, + onTimeout: () { + throw CheckHomeserverTimeoutException(); + }, + ); + final identitiesProvider = identityProviders(rawLoginTypes: rawLoginTypes); + if (identitiesProvider?.length == 1) { + registerPublicPlatformAction( + context: context, + id: identitiesProvider!.single.id!, + saasRegistrationErrorCallback: (object) { + Logs().e( + "AutoHomeserverPickerController: _saasAutoRegistration: Error - $object", + ); + }, + saasRegistrationTimeoutCallback: () { + Logs().e( + "AutoHomeserverPickerController: _saasAutoRegistration: Timeout", + ); + }, + ); + } + } + + void _setupAutoHomeserverPicker() { if (widget.loggedOut == null) { - _autoCheckHomeserver(); + Logs().d( + "AutoHomeserverPickerController: _initializeAutoHomeserverPicker: PlatForm ${AppConfig.platform}", + ); + if (_isSaasPlatform) { + _autoConnectSaas(); + } else { + _autoConnectHomeserver(); + } } else { if (widget.loggedOut == true) { showButtonRetryNotifier.toggle(); } } + } + + @override + void initState() { + _setupAutoHomeserverPicker(); super.initState(); } diff --git a/lib/pages/chat_list/chat_list_body_view.dart b/lib/pages/chat_list/chat_list_body_view.dart index adbc9d86b1..a62988a163 100644 --- a/lib/pages/chat_list/chat_list_body_view.dart +++ b/lib/pages/chat_list/chat_list_body_view.dart @@ -101,7 +101,8 @@ class ChatListBodyView extends StatelessWidget { .paddingTextStartNewChatMessage, child: Text( L10n.of(context)!.startNewChatMessage, - style: Theme.of(context).textTheme.bodyMedium, + style: + Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), ), @@ -152,8 +153,8 @@ class ChatListBodyView extends StatelessWidget { controller.filteredRoomsForPin.length, ), isExpanded: isExpanded, - onTap: - controller.expandRoomsForPinNotifier.toggle, + onTap: controller + .expandRoomsForPinNotifier.toggle, ), if (isExpanded) child!, ], diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 1e87a4b492..001541bd2f 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -1,15 +1,16 @@ +import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header_style.dart'; import 'package:fluffychat/widgets/twake_components/twake_header.dart'; import 'package:flutter/material.dart'; class ChatListHeader extends StatelessWidget { final ChatListController controller; - final VoidCallback onClearSelection; + final VoidCallback? onOpenSearchPage; const ChatListHeader({ Key? key, required this.controller, - required this.onClearSelection, + this.onOpenSearchPage, }) : super(key: key); @override @@ -18,7 +19,7 @@ class ChatListHeader extends StatelessWidget { children: [ TwakeHeader( controller: controller, - onClearSelection: onClearSelection, + onClearSelection: controller.onClickClearSelection, ), Container( height: ChatListHeaderStyle.searchBarContainerHeight, diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index a880d015bd..5fd0bfd90d 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -45,12 +45,7 @@ class ChatListView extends StatelessWidget { preferredSize: ChatListViewStyle.preferredSizeAppBar(context), child: ChatListHeader( onOpenSearchPage: onOpenSearchPage, - selectModeNotifier: controller.selectModeNotifier, - conversationSelectionNotifier: - controller.conversationSelectionNotifier, - currentProfileNotifier: controller.currentProfileNotifier, - onClickClearSelection: controller.onClickClearSelection, - onClickAvatar: controller.onClickAvatar, + controller: controller, ), ), bottomNavigationBar: ValueListenableBuilder( diff --git a/lib/pages/connect/connect_page_mixin.dart b/lib/pages/connect/connect_page_mixin.dart index 0471c680eb..ac8591f97a 100644 --- a/lib/pages/connect/connect_page_mixin.dart +++ b/lib/pages/connect/connect_page_mixin.dart @@ -14,7 +14,12 @@ import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; +typedef OnSAASRegistrationTimeoutCallback = void Function(); +typedef OnSAASRegistrationErrorCallback = void Function(Object?); + mixin ConnectPageMixin { + static const saasRegistrationTimeout = Duration(seconds: 120); + static const windowNameValue = '_self'; bool supportsFlow({ @@ -53,6 +58,15 @@ mixin ConnectPageMixin { return '$ssoRedirectUri?redirectUrl=$redirectUrlEncode'; } + String generatePublicPlatformAuthenticationUrl({ + required BuildContext context, + required String id, + required String redirectUrl, + }) { + final redirectUrlEncode = Uri.encodeQueryComponent(redirectUrl); + return '${AppConfig.registrationUrl}?post_registered_redirect_url=$redirectUrlEncode'; + } + String? _getLogoutUrl( BuildContext context, { required String redirectUrl, @@ -150,6 +164,36 @@ mixin ConnectPageMixin { } } + Future registerPublicPlatformAction({ + required BuildContext context, + required String id, + OnSAASRegistrationTimeoutCallback? saasRegistrationTimeoutCallback, + OnSAASRegistrationErrorCallback? saasRegistrationErrorCallback, + }) async { + final redirectUrl = _generateRedirectUrl( + Matrix.of(context).client.homeserver.toString(), + ); + final url = generatePublicPlatformAuthenticationUrl( + context: context, + id: id, + redirectUrl: redirectUrl, + ); + final urlScheme = _getRedirectUrlScheme(redirectUrl); + final uri = await FlutterWebAuth2.authenticate( + url: url, + callbackUrlScheme: urlScheme, + options: const FlutterWebAuth2Options( + intentFlags: ephemeralIntentFlags, + windowName: windowNameValue, + ), + ); + Logs().d("ConnectPageMixin:_redirectRegistrationUrl: URI - $uri"); + handleTokenFromRegistrationSite( + matrix: Matrix.of(context), + uri: uri, + ); + } + String _generatePostLogoutRedirectUrl() { if (kIsWeb) { if (AppConfig.issueId != null && AppConfig.issueId!.isNotEmpty) { @@ -190,4 +234,23 @@ mixin ConnectPageMixin { } return list; } + + void handleTokenFromRegistrationSite({ + required MatrixState matrix, + required String uri, + }) async { + final token = Uri.parse(uri).queryParameters['loginToken']; + Logs().d( + "ConnectPageMixin: handleTokenFromRegistrationSite: token: $token", + ); + if (token == null || token.isEmpty == true) return; + matrix.loginType = LoginType.mLoginToken; + await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => matrix.getLoginClient().login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ), + ); + } } diff --git a/lib/pages/twake_welcome/twake_id_view_style.dart b/lib/pages/twake_welcome/twake_id_view_style.dart deleted file mode 100644 index 7f5f1b5474..0000000000 --- a/lib/pages/twake_welcome/twake_id_view_style.dart +++ /dev/null @@ -1,4 +0,0 @@ -class TwakeIdViewStyle { - static const double logoWidth = 257; - static const double logoHeight = 179; -} diff --git a/lib/pages/twake_welcome/twake_welcome.dart b/lib/pages/twake_welcome/twake_welcome.dart index 987a970464..0e77fa07dd 100644 --- a/lib/pages/twake_welcome/twake_welcome.dart +++ b/lib/pages/twake_welcome/twake_welcome.dart @@ -1,8 +1,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:equatable/equatable.dart'; +import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome_view.dart'; -import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; @@ -29,13 +28,14 @@ class TwakeWelcomeArg extends Equatable { class TwakeWelcome extends StatefulWidget { final TwakeWelcomeArg? arg; + const TwakeWelcome({super.key, this.arg}); @override State createState() => TwakeWelcomeController(); } -class TwakeWelcomeController extends State { +class TwakeWelcomeController extends State with ConnectPageMixin { void goToHomeserverPicker() { if (widget.arg?.isAddAnotherAccount == true) { context.push('/rooms/addhomeserver'); @@ -63,20 +63,6 @@ class TwakeWelcomeController extends State { _redirectRegistrationUrl(loginUrl); } - void _handleTokenFromRegistrationSite(String uri) async { - final token = Uri.parse(uri).queryParameters['loginToken']; - Logs().d("TwakeIdController: _handleLoginToken: token: $token"); - if (token?.isEmpty ?? false) return; - Matrix.of(context).loginType = LoginType.mLoginToken; - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context).getLoginClient().login( - LoginType.mLoginToken, - token: token, - initialDeviceDisplayName: PlatformInfos.clientName, - ), - ); - } - void _redirectRegistrationUrl(String url) async { matrix.loginHomeserverSummary = await matrix.getLoginClient().checkHomeserver( @@ -90,12 +76,13 @@ class TwakeWelcomeController extends State { ), ); Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); - _handleTokenFromRegistrationSite(uri); + handleTokenFromRegistrationSite(matrix: matrix, uri: uri); } void onClickCreateTwakeId() { - Logs() - .d("TwakeIdController::onClickCreateTwakeId: Signup Url - $signupUrl"); + Logs().d( + "TwakeIdController::onClickCreateTwakeId: Signup Url - $signupUrl", + ); _redirectRegistrationUrl(signupUrl); } diff --git a/lib/pages/twake_welcome/twake_welcome_view.dart b/lib/pages/twake_welcome/twake_welcome_view.dart index 78d282988b..6f2283121c 100644 --- a/lib/pages/twake_welcome/twake_welcome_view.dart +++ b/lib/pages/twake_welcome/twake_welcome_view.dart @@ -1,5 +1,5 @@ -import 'package:fluffychat/pages/twake_welcome/twake_id_view_style.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; +import 'package:fluffychat/pages/twake_welcome/twake_welcome_view_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -24,10 +24,11 @@ class TwakeWelcomeView extends StatelessWidget { description: L10n.of(context)!.descriptionTwakeId, onUseCompanyServerOnTap: controller.goToHomeserverPicker, onSignInOnTap: controller.onClickSignIn, + onCreateTwakeIdOnTap: controller.onClickCreateTwakeId, logo: SvgPicture.asset( ImagePaths.logoTwakeWelcome, - width: TwakeIdViewStyle.logoWidth, - height: TwakeIdViewStyle.logoHeight, + width: TwakeWelcomeViewStyle.logoWidth, + height: TwakeWelcomeViewStyle.logoHeight, ), ); } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index 8ce825a1c3..90b149efcf 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -129,7 +129,7 @@ class AppAdaptiveScaffoldBodyController extends State { 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():newWidget - ${widget.args}', ); if (oldWidget.args != widget.args && widget.args is LogoutBodyArgs) { - activeNavigationBar.value = AdaptiveDestinationEnum.rooms; + activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); } } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 951e201369..eda9f7bd5d 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -283,7 +283,6 @@ class MatrixState extends State super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addObserver(this); - _migrateToMDatabase(client); if (PlatformInfos.isWeb) { html.window.addEventListener('focus', onWindowFocus); html.window.addEventListener('blur', onWindowBlur); @@ -393,7 +392,7 @@ class MatrixState extends State Logs().v('[MATRIX]:_listenLoginStateChanged:: Log out successful'); if (PlatformInfos.isMobile) { _deletePersistActiveAccount(state); - TwakeApp.router.go('/home/twakeid'); + TwakeApp.router.go('/home/twakeWelcome'); } else { TwakeApp.router.go('/home', extra: true); } @@ -764,24 +763,6 @@ class MatrixState extends State } } - void _migrateToMDatabase(Client client) async { - if (!FlutterHiveCollectionsDatabase.canMigrateToMDatabase) return; - Logs().d( - 'Matrix::_checkHomeserverExists: Start migration to ToMDatabase', - ); - if (client.userID == null) return; - final hiveCollectionToMDatabase = - await getIt.getAsync(); - final currentToMConfigurations = await getTomConfigurations(client.userID!); - if (currentToMConfigurations != null) { - await hiveCollectionToMDatabase.clearCache(); - _storeToMConfiguration(client, currentToMConfigurations); - } - Logs().d( - 'Matrix::_checkHomeserverExists: Finish migration to ToMDatabase', - ); - } - void onWindowFocus(html.Event e) { didChangeAppLifecycleState(AppLifecycleState.resumed); } diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index f6792a9c60..0ceac1313b 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; diff --git a/pubspec.lock b/pubspec.lock index d2d1cbb1b1..181a631d79 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: animations - sha256: "708e4b68c23228c264b038fe7003a2f5d01ce85fc64d8cae090e86b27fcea6c5" + sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.0.11" ansicolor: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -157,18 +157,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -181,34 +181,34 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.1" cached_network_image: dependency: "direct main" description: name: cached_network_image - sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" callkeep: dependency: "direct main" description: @@ -221,42 +221,42 @@ packages: dependency: transitive description: name: camera - sha256: "7fa53bb1c2059e58bf86b7ab506e3b2a78e42f82d365b44b013239b975a166ef" + sha256: "9499cbc2e51d8eb0beadc158b288380037618ce4e30c9acbc4fae1ac3ecb5797" url: "https://pub.dev" source: hosted - version: "0.10.5+7" + version: "0.10.5+9" camera_android: dependency: transitive description: name: camera_android - sha256: "7215e38fa0be58cc3203a6e48de3636fb9b1bf93d6eeedf667f882d51b3a4bf3" + sha256: "15a6543878a41c141807ffab496f66b7fef6da0f23372f5513fc6349e60f437e" url: "https://pub.dev" source: hosted - version: "0.10.8+15" + version: "0.10.8+17" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "3c8dd395f18722f01b5f325ddd7f5256e9bcdce538fb9243b378ba759df3283c" + sha256: "608b56b0880722f703871329c4d7d4c2f379c8e2936940851df7fc041abc6f51" url: "https://pub.dev" source: hosted - version: "0.9.13+8" + version: "0.9.13+10" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: b6a568984254cadaca41a6b896d87d3b2e79a2e5791afa036f8d524c6783b93a + sha256: a250314a48ea337b35909a4c9d5416a208d736dcb01d0b02c6af122be66660b0 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.4" camera_web: dependency: transitive description: name: camera_web - sha256: d4c2c571c7af04f8b10702ca16bb9ed2a26e64534171e8f75c9349b2c004d8f1 + sha256: f18ccfb33b2a7c49a52ad5aa3f07330b7422faaecbdfd9b9fe8e51182f6ad67d url: "https://pub.dev" source: hosted - version: "0.3.2+3" + version: "0.3.2+4" canonical_json: dependency: transitive description: @@ -293,10 +293,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" + sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" url: "https://pub.dev" source: hosted - version: "1.7.4" + version: "1.7.5" cli_util: dependency: transitive description: @@ -317,10 +317,10 @@ packages: dependency: transitive description: name: code_builder - sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -414,10 +414,10 @@ packages: dependency: transitive description: name: dart_webrtc - sha256: "5897a3bdd6c7fded07e80e250260ca4c9cd61f9080911aa308b516e1206745a9" + sha256: "5cbc40bd9b33d0c9b8004cff52e9883c71f0f54799afc8faca77535eeb9ef857" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.1" dartz: dependency: "direct main" description: @@ -470,10 +470,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -486,10 +486,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" dio_cache_interceptor: dependency: "direct main" description: @@ -510,18 +510,18 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d url: "https://pub.dev" source: hosted - version: "1.6.9" + version: "1.7.0" emoji_picker_flutter: dependency: "direct main" description: name: emoji_picker_flutter - sha256: "009c51efc763d5a6ba05a5628b8b2184c327cd117d66ea9c3e7edf2ff269c423" + sha256: "8506341d62efd116d6fb1481450bffdbac659d3d90d46d9cc610bfae5f33cc54" url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.6.4" emoji_proposal: dependency: "direct main" description: @@ -582,10 +582,10 @@ packages: dependency: "direct overridden" description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "0396b3798d91e7330911407d0cebc6bcb776aff56da567deb1ef3f40a469e72a" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.0.0" file: dependency: transitive description: @@ -622,18 +622,18 @@ packages: dependency: transitive description: name: file_selector_android - sha256: b7556052dbcc25ef88f6eba45ab98aa5600382af8dfdabc9d644a93d97b7be7f + sha256: "1cd66575f063b689e041aec836905ba7be18d76c9f0634d0d75daec825f67095" url: "https://pub.dev" source: hosted - version: "0.5.0+4" + version: "0.5.0+7" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: "2f48db7e338b2255101c35c604b7ca5ab588dce032db7fc418a2fe5f28da63f8" + sha256: b015154e6d9fddbc4d08916794df170b44531798c8dd709a026df162d07ad81d url: "https://pub.dev" source: hosted - version: "0.5.1+7" + version: "0.5.1+8" file_selector_linux: dependency: "direct overridden" description: @@ -654,10 +654,10 @@ packages: dependency: transitive description: name: file_selector_platform_interface - sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" file_selector_web: dependency: transitive description: @@ -715,10 +715,10 @@ packages: dependency: "direct main" description: name: flutter_adaptive_scaffold - sha256: "3e78be8b9c95b1c9832b2f8ec4a845adac205c4bb5e7bd3fb204b07990229167" + sha256: "4257142551ec97761d44f4258b8ad53ac76593dd0992197b876769df19f8a018" url: "https://pub.dev" source: hosted - version: "0.1.7+1" + version: "0.1.8" flutter_app_badger: dependency: "direct main" description: @@ -784,42 +784,50 @@ packages: dependency: "direct main" description: name: flutter_image_compress - sha256: f159d2e8c4ed04b8e36994124fd4a5017a0f01e831ae3358c74095c340e9ae5e + sha256: "4edadb0ca2f957b85190e9c3aa728569b91b64b6e06e0eec5b622d47a8692ab2" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" flutter_image_compress_common: dependency: transitive description: name: flutter_image_compress_common - sha256: "7cad12802628706655920089cfe9ee1d1098300e7f39a079eb160458bbc47652" + sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.5" flutter_image_compress_macos: dependency: transitive description: name: flutter_image_compress_macos - sha256: fea1e3d71150d03373916b832c49b5c2f56c3e7e13da82a929274a2c6f88251e + sha256: "26df6385512e92b3789dc76b613b54b55c457a7f1532e59078b04bf189782d47" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: "70360371698be994786e5dd2e364a6525b1c5a4f843bff8af9b8a2fbe808d8d8" + url: "https://pub.dev" + source: hosted + version: "0.0.2" flutter_image_compress_platform_interface: dependency: transitive description: name: flutter_image_compress_platform_interface - sha256: eb4f055138b29b04498ebcb6d569aaaee34b64d75fb74ea0d40f9790bf47ee9d + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.5" flutter_image_compress_web: dependency: transitive description: name: flutter_image_compress_web - sha256: da41cc3859f19d11c7d10be615f6a9dcf0907e7daffde7442bf4cc2486663660 + sha256: f02fe352b17f82b72f481de45add240db062a2585850bea1667e82cc4cd6c311 url: "https://pub.dev" source: hosted - version: "0.1.3+2" + version: "0.1.4+1" flutter_inappwebview: dependency: "direct main" description: @@ -950,7 +958,7 @@ packages: description: path: "." ref: master - resolved-ref: "96d16afde0eab53afff59642ab382cbcb522a616" + resolved-ref: "27e36218e126e9e1c382e45fde4a683b10e83f91" url: "https://github.com/linagora/flutter_matrix_html.git" source: git version: "1.2.0" @@ -958,10 +966,10 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "141b20f15a2c4fe6e33c49257ca1bc114fc5c500b04fcbc8d75016bb86af672f" + sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.3.10" flutter_olm: dependency: "direct main" description: @@ -1105,10 +1113,10 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: "577216727181cb13776a65d3e7cb33e783e740c5496335011aed4a038b28c3fe" + sha256: "2f17fb96e0c9c6ff75f6b1c36d94755461fc7f36a5c28386f5ee5a18b98688c8" url: "https://pub.dev" source: hosted - version: "0.9.47" + version: "0.9.48+hotfix.1" fluttertoast: dependency: "direct main" description: @@ -1158,10 +1166,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3 + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 url: "https://pub.dev" source: hosted - version: "7.6.4" + version: "7.6.7" glob: dependency: transitive description: @@ -1286,10 +1294,10 @@ packages: dependency: "direct main" description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.7" image_gallery_saver: dependency: "direct main" description: @@ -1302,34 +1310,34 @@ packages: dependency: "direct main" description: name: image_picker - sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77 + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.7" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: ecdc963d2aa67af5195e723a40580f802d4392e31457a12a562b3e2bd6a396fe + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.9+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 url: "https://pub.dev" source: hosted - version: "0.8.9" + version: "0.8.9+1" image_picker_linux: dependency: transitive description: @@ -1350,10 +1358,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.9.3" image_picker_windows: dependency: transitive description: @@ -1550,26 +1558,26 @@ packages: dependency: transitive description: name: macos_ui - sha256: cc499122655c61728185561e9006af4b239f9526f98d7b2cbf42124e9044a0ff + sha256: d351f0bada7e5b0cee8cf394299878a6c04e5cfcd784fa1d40e44299501124d8 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.5" macos_window_utils: dependency: transitive description: name: macos_window_utils - sha256: b3dfd47bbc605f0e315af684b50370a8f84932267aaa542098063fa384d593bd + sha256: "230be594d26f6dee92c5a1544f4242d25138a5bfb9f185b27f14de3949ef0be8" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" markdown: dependency: transitive description: name: markdown - sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "7.2.2" matcher: dependency: transitive description: @@ -1590,8 +1598,8 @@ packages: dependency: "direct main" description: path: "." - ref: add-callback-migrating-database - resolved-ref: "10d717db2f368bbafb035290a4bc7df558c2e3df" + ref: "twake-supported-0.22.6" + resolved-ref: "22311972c4d781133b893ae032dcb509b1351075" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" @@ -1599,10 +1607,10 @@ packages: dependency: transitive description: name: matrix_api_lite - sha256: "62bdd1dffb956e956863ba21e52109157502342b749e4728f4105f0c6d73a254" + sha256: "0e92d3402b4cbb8ab9283fd2fbe44147facf6f73de88f5adf0b3123bc5114bc1" url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.7.3" matrix_homeserver_recommendations: dependency: "direct main" description: @@ -1712,10 +1720,10 @@ packages: dependency: "direct main" description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mockito: dependency: "direct dev" description: @@ -1840,10 +1848,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: @@ -1856,10 +1864,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -1872,10 +1880,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -1936,18 +1944,18 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61" + sha256: df594f989f0c31cdb3ed48f3d49cb9ffadf11cc3700d2c3460b1912c93432621 url: "https://pub.dev" source: hosted - version: "3.0.0-dev.5" + version: "3.0.0" photo_manager_image_provider: dependency: "direct main" description: name: photo_manager_image_provider - sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0 + sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" pin_code_text_field: dependency: "direct main" description: @@ -1960,10 +1968,10 @@ packages: dependency: transitive description: name: pixel_snap - sha256: d31591a4f4aa8ed5dc6fc00b8d027338a5614dfbf5ca658b69d1faa7aba80af7 + sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" platform: dependency: transitive description: @@ -1984,10 +1992,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pointer_interceptor: dependency: transitive description: @@ -2024,10 +2032,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" polylabel: dependency: transitive description: @@ -2072,10 +2080,10 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -2337,10 +2345,10 @@ packages: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" share_plus_web: dependency: transitive description: @@ -2401,10 +2409,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: @@ -2510,18 +2518,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.dev" source: hosted - version: "2.5.0+2" + version: "2.5.3" stack_trace: dependency: transitive description: @@ -2758,26 +2766,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.4" url_launcher_linux: dependency: transitive description: @@ -2798,18 +2806,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" url_launcher_windows: dependency: transitive description: @@ -2822,10 +2830,10 @@ packages: dependency: transitive description: name: uuid - sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.3.3" value_layout_builder: dependency: transitive description: @@ -2838,26 +2846,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.10+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.10+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.10+1" vector_math: dependency: transitive description: @@ -2887,42 +2895,42 @@ packages: dependency: "direct main" description: name: video_player - sha256: e16f0a83601a78d165dabc17e4dac50997604eb9e4cc76e10fa219046b70cef3 + sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23 url: "https://pub.dev" source: hosted - version: "2.8.1" + version: "2.8.3" video_player_android: dependency: transitive description: name: video_player_android - sha256: "3fe89ab07fdbce786e7eb25b58532d6eaf189ceddc091cb66cba712f8d9e8e55" + sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.12" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "01a57940e1dabc8769ccd457c4ae9ea50274e7d5a7617f7820dae5fe1d8436ae" + sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.6" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.2" video_player_web: dependency: transitive description: name: video_player_web - sha256: ab7a462b07d9ca80bed579e30fb3bce372468f1b78642e0911b10600f2c5cb5b + sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" video_thumbnail: dependency: "direct main" description: @@ -3075,18 +3083,26 @@ packages: dependency: "direct main" description: name: wechat_camera_picker - sha256: ffc6f987b62d7e1104ec08c4797a620cb1cc9698cbfb6d19865290e835ea1777 + sha256: "682d4cd5606d5f95af2f6efe3224ebb5fe5ac014980eeaba0eddd211219c6f4a" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + wechat_picker_library: + dependency: transitive + description: + name: wechat_picker_library + sha256: a47cdb227955f64494fe55bc42d91a76bfc626a446075d4284a070f1e1297b4e url: "https://pub.dev" source: hosted - version: "4.2.0-dev.3" + version: "1.0.0" win32: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8e890cb937..895db17fee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -232,7 +232,7 @@ msix_config: dependency_overrides: # Until all dependencies are compatible. Missing: file_picker_cross, flutter_matrix_html - ffi: ^2.0.0 + ffi: 2.0.0 # This otherwise breaks on linux with flutter 3.7.0, let's override it for now. file_selector: ^0.9.2+2 file_selector_linux: ^0.9.1 diff --git a/test/domain/contacts/contacts_manager_test.mocks.dart b/test/domain/contacts/contacts_manager_test.mocks.dart index 4a4d1fe7be..07a953663c 100644 --- a/test/domain/contacts/contacts_manager_test.mocks.dart +++ b/test/domain/contacts/contacts_manager_test.mocks.dart @@ -54,6 +54,7 @@ class MockGetTomContactsInteractor extends _i1.Mock Invocation.getter(#contactRepository), ), ) as _i2.ContactRepository); + @override _i4.Stream<_i5.Either<_i6.Failure, _i7.Success>> execute( {required int? limit}) => From 2447f4630e0a8b48ba4e8834372b5ab39429742a Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 25 Mar 2024 16:48:47 +0700 Subject: [PATCH 051/183] hot-fix: remove default `colorFilter` of `TwakeIconButton` (cherry picked from commit e8d8e504700bbb402072510c7c89d0bf6761f17b) --- lib/pages/chat/chat_view.dart | 4 ++++ lib/widgets/twake_components/twake_icon_button.dart | 13 ++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 5da6aa216b..ff471138e9 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -46,6 +46,10 @@ class ChatView extends StatelessWidget with MessageContentMixin { icon: !controller.isUnpinEvent(controller.selectedEvents.first) ? Icons.push_pin_outlined : null, + iconColor: + controller.isUnpinEvent(controller.selectedEvents.first) + ? Theme.of(context).colorScheme.onSurfaceVariant + : null, imagePath: controller.isUnpinEvent(controller.selectedEvents.first) ? ImagePaths.icUnpin diff --git a/lib/widgets/twake_components/twake_icon_button.dart b/lib/widgets/twake_components/twake_icon_button.dart index cc2b4dacd2..5184d53eed 100644 --- a/lib/widgets/twake_components/twake_icon_button.dart +++ b/lib/widgets/twake_components/twake_icon_button.dart @@ -103,13 +103,12 @@ class TwakeIconButton extends StatelessWidget { imagePath!, height: imageSize, width: imageSize, - colorFilter: ColorFilter.mode( - iconColor ?? - Theme.of(context) - .colorScheme - .onSurfaceVariant, - BlendMode.srcIn, - ), + colorFilter: iconColor != null + ? ColorFilter.mode( + iconColor!, + BlendMode.srcIn, + ) + : null, ) : null, ), From 24c986650027b53b24012586e9d8598d79aad80d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 25 Mar 2024 09:29:52 +0700 Subject: [PATCH 052/183] TW-1581: Preview unknown file instead of sharing (cherry picked from commit 5ae20ba9047d2705588def3886079fab823f0e81) --- .../chat/events/message_download_content.dart | 2 +- ...andle_download_and_preview_file_mixin.dart | 43 ++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 8208addb42..fd771cf933 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -118,7 +118,7 @@ class _MessageDownloadContentState extends State if (state is DownloadedPresentationState) { return InkWell( onTap: () async { - openDownloadedFileForPreview( + handleDownloadFileForPreviewSuccess( filePath: state.filePath, mimeType: widget.event.mimeType, ); diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index aed2c948c5..5443b0e21e 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -142,7 +142,7 @@ mixin HandleDownloadAndPreviewFileMixin { TwakeDialog.hideLoadingDialog(context); }, (success) { if (success is DownloadFileForPreviewSuccess) { - openDownloadedFileForPreview( + handleDownloadFileForPreviewSuccess( filePath: success.downloadFileForPreviewResponse.filePath, mimeType: success.downloadFileForPreviewResponse.mimeType, ); @@ -154,12 +154,32 @@ mixin HandleDownloadAndPreviewFileMixin { }); } - void openDownloadedFileForPreview({ + void handleDownloadFileForPreviewSuccess({ + required String filePath, + required String? mimeType, + }) { + if (PlatformInfos.isAndroid) { + _openDownloadedFileForPreviewAndroid( + filePath: filePath, + mimeType: mimeType, + ); + return; + } + + if (PlatformInfos.isIOS) { + _openDownloadedFileForPreviewIos( + filePath: filePath, + mimeType: mimeType, + ); + return; + } + } + + void _openDownloadedFileForPreviewAndroid({ required String filePath, required String? mimeType, }) async { - if (PlatformInfos.isAndroid && - SupportedPreviewFileTypes.apkMimeTypes.contains(mimeType)) { + if (SupportedPreviewFileTypes.apkMimeTypes.contains(mimeType)) { await Share.shareXFiles([XFile(filePath)]); return; } @@ -170,7 +190,7 @@ mixin HandleDownloadAndPreviewFileMixin { .value, ); Logs().d( - 'ChatController:_openDownloadedFileForPreview(): ${openResults.message}', + 'ChatController:_openDownloadedFileForPreviewAndroid(): ${openResults.message}', ); if (openResults.type != ResultType.done) { @@ -179,6 +199,19 @@ mixin HandleDownloadAndPreviewFileMixin { } } + void _openDownloadedFileForPreviewIos({ + required String filePath, + required String? mimeType, + }) async { + Logs().d( + 'ChatController:_openDownloadedFileForPreviewIos(): $filePath', + ); + await OpenFile.open( + filePath, + type: mimeType, + ); + } + void previewPdfWeb(BuildContext context, Event event) async { final pdf = await event.getFile(context); if (pdf.result == null || event.sizeString != pdf.result?.sizeString) { From 5df30123f8e58ec1fa00c26dc9bd22d52fdf3807 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 25 Mar 2024 11:53:23 +0700 Subject: [PATCH 053/183] hot-fix: cancel the stream subscription if not used anymore (cherry picked from commit e74a88bc2a3010c719ad25214a3149feee4c7308) --- .../chat_pinned_events/pinned_messages.dart | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/lib/pages/chat/chat_pinned_events/pinned_messages.dart b/lib/pages/chat/chat_pinned_events/pinned_messages.dart index d6de6abfa2..5364944819 100644 --- a/lib/pages/chat/chat_pinned_events/pinned_messages.dart +++ b/lib/pages/chat/chat_pinned_events/pinned_messages.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart' hide State; import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/room/update_pinned_events_state.dart'; import 'package:fluffychat/domain/enums/pinned_messages_action_enum.dart'; @@ -64,8 +68,17 @@ class PinnedMessagesController extends State final ValueNotifier openingPopupMenu = ValueNotifier(false); + StreamSubscription>? unpinMessagesStreamSubcription; + + StreamSubscription>? unpinAllStreamSubcription; + + StreamSubscription>? + unpinSelectedEventsStreamSubcription; + + StreamSubscription? onEventStreamSubscription; + void unpin(String eventId) { - updatePinnedMessagesInteractor + unpinMessagesStreamSubcription = updatePinnedMessagesInteractor .execute(room: room!, eventIds: [eventId]).listen((event) { event.fold((failure) { _showErrorSnackbar(failure); @@ -85,7 +98,7 @@ class PinnedMessagesController extends State } void unpinAll() { - updatePinnedMessagesInteractor + unpinAllStreamSubcription = updatePinnedMessagesInteractor .execute( room: room!, eventIds: [], @@ -107,7 +120,7 @@ class PinnedMessagesController extends State } void unpinSelectedEvents() { - updatePinnedMessagesInteractor + unpinSelectedEventsStreamSubcription = updatePinnedMessagesInteractor .execute( room: room!, eventIds: selectedPinnedEventsIds, @@ -382,7 +395,7 @@ class PinnedMessagesController extends State void _listenRoomUpdateEvent() { if (room == null) return; - client.onEvent.stream.listen((eventUpdate) { + onEventStreamSubscription = client.onEvent.stream.listen((eventUpdate) { Logs().d( 'PinnedMessages::_listenRoomUpdateEvent():: Event Update Content ${eventUpdate.content}', ); @@ -425,17 +438,7 @@ class PinnedMessagesController extends State } void _updateEventsNotifier(List events) { - try { - eventsNotifier.value = events; - } on FlutterError catch (exception) { - Logs().e( - 'PinnedMessages::_updateEventsNotifier():: FlutterError $exception', - ); - } catch (exception) { - Logs().e( - 'PinnedMessages::_updateEventsNotifier():: ErrorCode $exception', - ); - } + eventsNotifier.value = events; } @override @@ -450,8 +453,13 @@ class PinnedMessagesController extends State @override void dispose() { scrollController.dispose(); - eventsNotifier.dispose(); isHoverNotifier.dispose(); + Future.wait([ + unpinAllStreamSubcription?.cancel() ?? Future.value(), + unpinMessagesStreamSubcription?.cancel() ?? Future.value(), + unpinSelectedEventsStreamSubcription?.cancel() ?? Future.value(), + onEventStreamSubscription?.cancel() ?? Future.value(), + ]).whenComplete(() => eventsNotifier.dispose()); super.dispose(); } From 5fe6557a453ea73f3fdef1e7e2b87592106d7f0d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 27 Mar 2024 11:34:36 +0700 Subject: [PATCH 054/183] Update configuration for public platform (cherry picked from commit e39438553d8381bbc1d4e4c08b60ffde8f6f51dd) --- config.sample.json | 4 +++- lib/config/app_config.dart | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config.sample.json b/config.sample.json index c10e8633db..2b2cc330b3 100644 --- a/config.sample.json +++ b/config.sample.json @@ -9,5 +9,7 @@ "issue_id": "", "registration_url": "https://example.com/", "twake_workplace_homeserver": "https://example.com/", - "app_grid_dashboard_available": true + "app_grid_dashboard_available": true, + "homeserver": "https://example.com/", + "platform": "platform" } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index ab41d9933d..4b5e50112c 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -110,7 +110,8 @@ abstract class AppConfig { } } - if (json['register_site'] != null && json['registration_url'] is String) { + if (json['registration_url'] != null && + json['registration_url'] is String) { if (json['registration_url'] != '') { registrationUrl = json['registration_url']; } From 408773bbb85b8ec7a083fa94b9e8320738030c2b Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 25 Mar 2024 10:45:35 +0700 Subject: [PATCH 055/183] TW-1183: Create `message_text_content` widget (cherry picked from commit 1bb2a6ea31ccc649593d626ce7758f86fbba90aa) --- .../chat/events/message_text_context.dart | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/pages/chat/events/message_text_context.dart diff --git a/lib/pages/chat/events/message_text_context.dart b/lib/pages/chat/events/message_text_context.dart new file mode 100644 index 0000000000..858bf2d94e --- /dev/null +++ b/lib/pages/chat/events/message_text_context.dart @@ -0,0 +1,51 @@ +import 'package:fluffychat/pages/chat/events/html_message.dart'; +import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart' hide Visibility; + +class MessageTextContent extends StatelessWidget { + final Event event; + final Color textColor; + final double fontSize; + final Widget endOfBubbleWidget; + + const MessageTextContent({ + super.key, + required this.event, + required this.textColor, + required this.fontSize, + required this.endOfBubbleWidget, + }); + + @override + Widget build(BuildContext context) { + var html = event.formattedText; + + if (event.messageType == MessageTypes.Emote) { + html = '* $html'; + } + final bigEmotes = + event.onlyEmotes && event.numberEmotes > 0 && event.numberEmotes <= 10; + + return Padding( + padding: MessageContentStyle.emojiPadding, + child: HtmlMessage( + html: html, + defaultTextStyle: Theme.of(context).textTheme.bodyLarge, + linkStyle: TextStyle( + color: Theme.of(context).colorScheme.secondary, + decorationColor: textColor.withAlpha(150), + ), + room: event.room, + emoteSize: bigEmotes ? fontSize * 3 : fontSize * 1.5, + bottomWidgetSpan: Visibility( + visible: false, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: endOfBubbleWidget, + ), + ), + ); + } +} From f5dff922a558f24cc799a13c0f76f102055a9749 Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 25 Mar 2024 10:46:25 +0700 Subject: [PATCH 056/183] TW-1183: Change message item of `twake_link_preview` (cherry picked from commit 0d3fe0beb4d8327d9cc3598ace8777aad701b54b) --- .../twake_link_preview.dart | 24 ++----------- .../twake_preview_link/twake_link_view.dart | 35 +++---------------- 2 files changed, 7 insertions(+), 52 deletions(-) diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart index 5524486d57..6ce48ef48c 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart @@ -15,30 +15,16 @@ class TwakeLinkPreview extends StatefulWidget { final Uri uri; final int? preferredPointInTime; final String text; - final Widget childWidget; - final TextStyle? textStyle; - final TextStyle? linkStyle; - final TextAlign? textAlign; - final LinkTapHandler? onLinkTap; - final int? maxLines; - final double? fontSize; + final Widget messageContentWidget; final bool ownMessage; - final TextSpanBuilder? textSpanBuilder; const TwakeLinkPreview({ super.key, required this.uri, this.preferredPointInTime, required this.text, - required this.childWidget, + required this.messageContentWidget, required this.ownMessage, - this.textStyle, - this.linkStyle, - this.textAlign, - this.onLinkTap, - this.maxLines, - this.fontSize, - this.textSpanBuilder, }); @override @@ -69,12 +55,8 @@ class TwakeLinkPreviewController extends State return TwakeLinkView( key: twakeLinkViewKey, text: widget.text, - textStyle: widget.textStyle, - linkStyle: widget.linkStyle, - childWidget: widget.childWidget, firstValidUrl: firstValidUrl, - onLinkTap: (url) => UrlLauncher(context, url: url.toString()).launchUrl(), - textSpanBuilder: widget.textSpanBuilder, + messageContentWidget: widget.messageContentWidget, previewItemWidget: ValueListenableBuilder( valueListenable: getPreviewUrlStateNotifier, builder: (context, state, child) { diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart index 3540357196..156190984d 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart @@ -1,32 +1,18 @@ -import 'package:fluffychat/widgets/clean_rich_text.dart'; import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_view_style.dart'; import 'package:flutter/material.dart'; -import 'package:matrix_link_text/link_text.dart'; class TwakeLinkView extends StatelessWidget { final String text; - final Widget childWidget; + final Widget messageContentWidget; final Widget previewItemWidget; - final TextStyle? textStyle; - final TextStyle? linkStyle; - final TextAlign? textAlign; - final LinkTapHandler? onLinkTap; - final int? maxLines; final String? firstValidUrl; - final TextSpanBuilder? textSpanBuilder; const TwakeLinkView({ Key? key, required this.text, - required this.childWidget, + required this.messageContentWidget, required this.previewItemWidget, - this.textStyle, - this.linkStyle, - this.textAlign = TextAlign.start, - this.onLinkTap, - this.maxLines, this.firstValidUrl, - this.textSpanBuilder, }) : super(key: key); @override @@ -47,7 +33,7 @@ class TwakeLinkView extends StatelessWidget { const SizedBox(height: 2), Padding( padding: TwakeLinkViewStyle.paddingWidgetNoPreview, - child: _buildCleanRichText(context), + child: messageContentWidget, ), ], ); @@ -56,20 +42,7 @@ class TwakeLinkView extends StatelessWidget { Widget _buildWidgetNoPreview(BuildContext context) { return Padding( padding: TwakeLinkViewStyle.paddingWidgetNoPreview, - child: _buildCleanRichText(context), - ); - } - - Widget _buildCleanRichText(BuildContext context) { - return TwakeCleanRichText( - text: text, - childWidget: childWidget, - textStyle: textStyle, - linkStyle: linkStyle, - textAlign: textAlign ?? TextAlign.start, - onLinkTap: onLinkTap, - maxLines: maxLines, - textSpanBuilder: textSpanBuilder, + child: messageContentWidget, ); } } From 46b26df4b5a7ed52dff3d5b55efdc96f2e070f93 Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 25 Mar 2024 10:46:54 +0700 Subject: [PATCH 057/183] TW-1183: Update `message_content` (cherry picked from commit 3778c011e26372d4304714ec75a67637e382ade2) --- .../chat/events/formatted_text_widget.dart | 44 +++++++++++++ lib/pages/chat/events/message_content.dart | 62 ++++++------------- .../chat/events/message_content_style.dart | 5 ++ .../chat/events/message_text_context.dart | 51 --------------- .../twake_link_preview.dart | 54 ++++++++++++---- .../twake_preview_link/twake_link_view.dart | 25 +++----- .../twake_link_view_style.dart | 4 +- 7 files changed, 121 insertions(+), 124 deletions(-) create mode 100644 lib/pages/chat/events/formatted_text_widget.dart delete mode 100644 lib/pages/chat/events/message_text_context.dart diff --git a/lib/pages/chat/events/formatted_text_widget.dart b/lib/pages/chat/events/formatted_text_widget.dart new file mode 100644 index 0000000000..8adba6f5af --- /dev/null +++ b/lib/pages/chat/events/formatted_text_widget.dart @@ -0,0 +1,44 @@ +import 'package:fluffychat/pages/chat/events/html_message.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart' hide Visibility; + +class FormattedTextWidget extends StatelessWidget { + final Event event; + final double fontSize; + final Widget endOfBubbleWidget; + final TextStyle? linkStyle; + + const FormattedTextWidget({ + super.key, + required this.event, + required this.fontSize, + required this.endOfBubbleWidget, + this.linkStyle, + }); + + @override + Widget build(BuildContext context) { + var html = event.formattedText; + + if (event.messageType == MessageTypes.Emote) { + html = '* $html'; + } + final bigEmotes = + event.onlyEmotes && event.numberEmotes > 0 && event.numberEmotes <= 10; + + return HtmlMessage( + html: html, + defaultTextStyle: Theme.of(context).textTheme.bodyLarge, + linkStyle: linkStyle, + room: event.room, + emoteSize: bigEmotes ? fontSize * 3 : fontSize * 1.5, + bottomWidgetSpan: Visibility( + visible: false, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: endOfBubbleWidget, + ), + ); + } +} diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 10448d24de..7c27eb1b80 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat/events/call_invite_content.dart'; import 'package:fluffychat/pages/chat/events/encrypted_content.dart'; import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/pages/chat/events/formatted_text_widget.dart'; import 'package:fluffychat/pages/chat/events/redacted_content.dart'; import 'package:fluffychat/pages/chat/events/sending_image_info_widget.dart'; import 'package:fluffychat/pages/chat/events/sending_video_widget.dart'; @@ -23,7 +24,6 @@ import 'package:matrix/matrix.dart' hide Visibility; import 'audio_player.dart'; import 'cute_events.dart'; -import 'html_message.dart'; import 'image_bubble.dart'; import 'map_bubble.dart'; import 'message_download_content.dart'; @@ -132,33 +132,14 @@ class MessageContent extends StatelessWidget !event.redacted && event.isRichMessage && containedLink.isEmpty) { - var html = event.formattedText; - - if (event.messageType == MessageTypes.Emote) { - html = '* $html'; - } - final bigEmotes = event.onlyEmotes && - event.numberEmotes > 0 && - event.numberEmotes <= 10; return Padding( padding: MessageContentStyle.emojiPadding, - child: HtmlMessage( - html: html, - defaultTextStyle: Theme.of(context).textTheme.bodyLarge, - linkStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, - decoration: TextDecoration.underline, - decorationColor: textColor.withAlpha(150), - ), - room: event.room, - emoteSize: bigEmotes ? fontSize * 3 : fontSize * 1.5, - bottomWidgetSpan: Visibility( - visible: false, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: endOfBubbleWidget, - ), + child: FormattedTextWidget( + event: event, + linkStyle: + MessageContentStyle.linkStyleMessageContent(context), + fontSize: fontSize, + endOfBubbleWidget: endOfBubbleWidget, ), ); } @@ -215,31 +196,24 @@ class MessageContent extends StatelessWidget hideReply: true, ), builder: (context, snapshot) { - final text = snapshot.data ?? + final localizedBody = snapshot.data ?? event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)!), hideReply: true, ); return TwakeLinkPreview( key: ValueKey('TwakeLinkPreview%${event.eventId}%'), - text: text, - textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - linkStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), - childWidget: Visibility( - visible: false, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: endOfBubbleWidget, - ), - uri: Uri.parse(text.getFirstValidUrl() ?? ''), + event: event, + localizedBody: localizedBody, ownMessage: ownMessage, - onLinkTap: (url) => - UrlLauncher(context, url: url.toString()).launchUrl(), + endOfBubbleWidget: endOfBubbleWidget, + fontSize: fontSize, + linkStyle: + MessageContentStyle.linkStyleMessageContent(context), + richTextStyle: + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ); }, ); diff --git a/lib/pages/chat/events/message_content_style.dart b/lib/pages/chat/events/message_content_style.dart index 9fe099a541..6bf34d64d3 100644 --- a/lib/pages/chat/events/message_content_style.dart +++ b/lib/pages/chat/events/message_content_style.dart @@ -75,4 +75,9 @@ class MessageContentStyle { left: 8.0, right: 8.0, ); + + static TextStyle? linkStyleMessageContent(BuildContext context) => + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ); } diff --git a/lib/pages/chat/events/message_text_context.dart b/lib/pages/chat/events/message_text_context.dart deleted file mode 100644 index 858bf2d94e..0000000000 --- a/lib/pages/chat/events/message_text_context.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:fluffychat/pages/chat/events/html_message.dart'; -import 'package:fluffychat/pages/chat/events/message_content_style.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart' hide Visibility; - -class MessageTextContent extends StatelessWidget { - final Event event; - final Color textColor; - final double fontSize; - final Widget endOfBubbleWidget; - - const MessageTextContent({ - super.key, - required this.event, - required this.textColor, - required this.fontSize, - required this.endOfBubbleWidget, - }); - - @override - Widget build(BuildContext context) { - var html = event.formattedText; - - if (event.messageType == MessageTypes.Emote) { - html = '* $html'; - } - final bigEmotes = - event.onlyEmotes && event.numberEmotes > 0 && event.numberEmotes <= 10; - - return Padding( - padding: MessageContentStyle.emojiPadding, - child: HtmlMessage( - html: html, - defaultTextStyle: Theme.of(context).textTheme.bodyLarge, - linkStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, - decorationColor: textColor.withAlpha(150), - ), - room: event.room, - emoteSize: bigEmotes ? fontSize * 3 : fontSize * 1.5, - bottomWidgetSpan: Visibility( - visible: false, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: endOfBubbleWidget, - ), - ), - ); - } -} diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart index 6ce48ef48c..fed7bc03a3 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart @@ -1,30 +1,36 @@ import 'package:fluffychat/domain/app_state/preview_url/get_preview_url_success.dart'; +import 'package:fluffychat/pages/chat/events/formatted_text_widget.dart'; import 'package:fluffychat/presentation/extensions/media/url_preview_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/clean_rich_text.dart'; import 'package:fluffychat/widgets/mixins/get_preview_url_mixin.dart'; import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_preview_item.dart'; import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_preview_item_style.dart'; import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_view.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; -import 'package:matrix_link_text/link_text.dart'; +import 'package:matrix/matrix.dart' hide Visibility; import 'package:skeletonizer/skeletonizer.dart'; class TwakeLinkPreview extends StatefulWidget { - final Uri uri; - final int? preferredPointInTime; - final String text; - final Widget messageContentWidget; + final Event event; + final String localizedBody; final bool ownMessage; + final Widget endOfBubbleWidget; + final double fontSize; + final TextStyle? linkStyle; + final TextStyle? richTextStyle; const TwakeLinkPreview({ super.key, - required this.uri, - this.preferredPointInTime, - required this.text, - required this.messageContentWidget, + required this.event, + required this.localizedBody, + required this.endOfBubbleWidget, required this.ownMessage, + required this.fontSize, + this.linkStyle, + this.richTextStyle, }); @override @@ -33,7 +39,9 @@ class TwakeLinkPreview extends StatefulWidget { class TwakeLinkPreviewController extends State with GetPreviewUrlMixin { - String? get firstValidUrl => widget.text.getFirstValidUrl(); + String? get firstValidUrl => widget.localizedBody.getFirstValidUrl(); + + Uri get uri => Uri.parse(firstValidUrl ?? ''); static const twakeLinkViewKey = ValueKey('TwakeLinkPreviewKey'); @@ -45,7 +53,7 @@ class TwakeLinkPreviewController extends State @override void initState() { if (firstValidUrl != null) { - getPreviewUrl(uri: widget.uri); + getPreviewUrl(uri: uri); } super.initState(); } @@ -54,9 +62,29 @@ class TwakeLinkPreviewController extends State Widget build(BuildContext context) { return TwakeLinkView( key: twakeLinkViewKey, - text: widget.text, firstValidUrl: firstValidUrl, - messageContentWidget: widget.messageContentWidget, + body: widget.event.formattedText.isNotEmpty + ? FormattedTextWidget( + event: widget.event, + linkStyle: widget.linkStyle, + fontSize: widget.fontSize, + endOfBubbleWidget: widget.endOfBubbleWidget, + ) + : TwakeCleanRichText( + text: widget.localizedBody, + childWidget: Visibility( + visible: false, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: widget.endOfBubbleWidget, + ), + textStyle: widget.richTextStyle, + linkStyle: widget.linkStyle, + textAlign: TextAlign.start, + onLinkTap: (url) => + UrlLauncher(context, url: url.toString()).launchUrl(), + ), previewItemWidget: ValueListenableBuilder( valueListenable: getPreviewUrlStateNotifier, builder: (context, state, child) { diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart index 156190984d..031cd0383b 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_view.dart @@ -2,15 +2,13 @@ import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_lin import 'package:flutter/material.dart'; class TwakeLinkView extends StatelessWidget { - final String text; - final Widget messageContentWidget; + final Widget body; final Widget previewItemWidget; final String? firstValidUrl; const TwakeLinkView({ Key? key, - required this.text, - required this.messageContentWidget, + required this.body, required this.previewItemWidget, this.firstValidUrl, }) : super(key: key); @@ -18,31 +16,28 @@ class TwakeLinkView extends StatelessWidget { @override Widget build(BuildContext context) { if (firstValidUrl == null) { - return _buildWidgetNoPreview(context); + return _buildMessageBody(); } - return _buildWidgetWithPreview(context, firstValidUrl!); + return _buildMessageWithPreview(context); } - Widget _buildWidgetWithPreview(BuildContext context, String url) { + Widget _buildMessageWithPreview(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ previewItemWidget, - const SizedBox(height: 2), - Padding( - padding: TwakeLinkViewStyle.paddingWidgetNoPreview, - child: messageContentWidget, - ), + const SizedBox(height: TwakeLinkViewStyle.previewToBodySpacing), + _buildMessageBody(), ], ); } - Widget _buildWidgetNoPreview(BuildContext context) { + Widget _buildMessageBody() { return Padding( - padding: TwakeLinkViewStyle.paddingWidgetNoPreview, - child: messageContentWidget, + padding: TwakeLinkViewStyle.paddingMessageBody, + child: body, ); } } diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_view_style.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_view_style.dart index b34d5d39f6..6205eb75c1 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_view_style.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_view_style.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class TwakeLinkViewStyle { - static const EdgeInsetsDirectional paddingWidgetNoPreview = + static const EdgeInsetsDirectional paddingMessageBody = EdgeInsetsDirectional.only(start: 8.0); static const EdgeInsetsDirectional paddingCleanRichText = @@ -10,4 +10,6 @@ class TwakeLinkViewStyle { end: 8.0, top: 8.0, ); + + static const double previewToBodySpacing = 2.0; } From 9ee855975fb48de47cedfedef05b372190b28eb4 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 27 Mar 2024 18:17:12 +0700 Subject: [PATCH 058/183] Hot fix: Wrong redirect to public public platform on web (cherry picked from commit d4f517a5c0b73f2f1b4f0d483fa4a2698b9df4d1) --- lib/pages/connect/connect_page_mixin.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/connect/connect_page_mixin.dart b/lib/pages/connect/connect_page_mixin.dart index ac8591f97a..f28b16ed9c 100644 --- a/lib/pages/connect/connect_page_mixin.dart +++ b/lib/pages/connect/connect_page_mixin.dart @@ -22,6 +22,8 @@ mixin ConnectPageMixin { static const windowNameValue = '_self'; + static const redirectPublicPlatformOnWeb = 'post_login_redirect_url'; + bool supportsFlow({ required BuildContext context, required String flowType, @@ -64,7 +66,7 @@ mixin ConnectPageMixin { required String redirectUrl, }) { final redirectUrlEncode = Uri.encodeQueryComponent(redirectUrl); - return '${AppConfig.registrationUrl}?post_registered_redirect_url=$redirectUrlEncode'; + return '${AppConfig.registrationUrl}?$redirectPublicPlatformOnWeb=$redirectUrlEncode'; } String? _getLogoutUrl( From c5c58936887f889be22aa61d7451c9f126952daf Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 26 Mar 2024 12:07:54 +0700 Subject: [PATCH 059/183] TW-1586: handle multiple download files in web (cherry picked from commit aa59e776145b2314cb332061a7c49c3209903d48) --- .../download_manager/download_manager.dart | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart index 03cad42c97..76f577bcb5 100644 --- a/lib/utils/manager/download_manager/download_manager.dart +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -185,12 +185,21 @@ class DownloadManager { Task( id: event.eventId, runnable: () async { - final matrixFile = await event.downloadAndDecryptAttachment(); - streamController.add( - Right( - DownloadMatrixFileSuccessState(matrixFile: matrixFile), - ), - ); + try { + final matrixFile = await event.downloadAndDecryptAttachment(); + streamController.add( + Right( + DownloadMatrixFileSuccessState(matrixFile: matrixFile), + ), + ); + } catch (e) { + Logs().e('DownloadManager::download(): $e'); + streamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); + } }, onTaskCompleted: () => clear(event.eventId), ), From 6d3629adbf9a818e8c8313cbb9d372c7d9208273 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 26 Mar 2024 12:13:32 +0700 Subject: [PATCH 060/183] TW-1586: handle display multiple download files in web (cherry picked from commit f28f91f5fbdb8ed89e5d809121e9e56d5555048f) --- .../chat/events/message_download_content.dart | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index fd771cf933..4a93b602dc 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -78,6 +79,7 @@ class _MessageDownloadContentState extends State (failure) { Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); downloadFileStateNotifier.value = const NotDownloadPresentationState(); + streamSubscription?.cancel(); }, (success) { if (success is DownloadingFileState) { @@ -92,17 +94,37 @@ class _MessageDownloadContentState extends State filePath: success.filePath, ); } else if (success is DownloadMatrixFileSuccessState) { - downloadFileStateNotifier.value = FileWebDownloadedPresentationState( - matrixFile: success.matrixFile, - ); + _handleDownloadMatrixFileSuccessState(success); } }, ); } + void _handleDownloadMatrixFileSuccessState( + DownloadMatrixFileSuccessState success, + ) { + streamSubscription?.cancel(); + if (mounted) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + handlePreviewWeb(event: widget.event, context: context); + return; + } + + if (TwakeApp.routerKey.currentContext != null) { + handlePreviewWeb( + event: widget.event, + context: TwakeApp.routerKey.currentContext!, + ); + } + } + @override void dispose() { - streamSubscription?.cancel(); + if (!PlatformInfos.isWeb) { + streamSubscription?.cancel(); + } downloadFileStateNotifier.dispose(); super.dispose(); } @@ -150,7 +172,7 @@ class _MessageDownloadContentState extends State } else if (state is FileWebDownloadedPresentationState) { return InkWell( onTap: () { - handlePreviewWeb( + onFileTapped( event: widget.event, context: context, ); @@ -164,6 +186,25 @@ class _MessageDownloadContentState extends State style: const MessageFileTileStyle(), ), ); + } else if (PlatformInfos.isWeb) { + return InkWell( + onTap: () { + downloadFileStateNotifier.value = + const DownloadingPresentationState(); + downloadManager.download( + event: widget.event, + ); + setupListeningForStreamSubcription(); + }, + child: FileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + ), + ); } return InkWell( From ebdde3b98756a3dc55f4b3638f2f935e6e21704b Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 26 Mar 2024 12:13:59 +0700 Subject: [PATCH 061/183] TW-1586: handle cancel download in web (cherry picked from commit 432625a3f28752e6cdded1e889aa863bab3c98ab) --- lib/pages/image_viewer/image_viewer.dart | 2 - .../matrix_file_extension.dart | 30 +++++------ lib/utils/string_extension.dart | 52 +++++++++---------- ...andle_download_and_preview_file_mixin.dart | 12 ++--- pubspec.lock | 8 +-- pubspec.yaml | 2 +- 6 files changed, 52 insertions(+), 54 deletions(-) diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 45aa40a35e..946318b3d8 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -30,8 +30,6 @@ class ImageViewerController extends State { TapDownDetails? tapDownDetails; final double zoomScale = 3; - static const String roomPathName = '/rooms/room'; - final ValueNotifier showAppbarPreview = ValueNotifier(true); @override diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index c423441d39..64fed89d69 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -45,22 +45,22 @@ extension MatrixFileExtension on MatrixFile { return; } - Future downloadFileInWeb(BuildContext context) async { + Future downloadFileInWeb(BuildContext context) async { Logs().d("MatrixFileExtension()::downloadFileInWeb()::download on Web"); - - final directory = await FileSaver.instance.saveFile( - name, - bytes!, - extensionFromMime(mimeType), - mimeType: mimeType.toMimeTypeEnum(), - ); - - TwakeSnackBar.show( - context, - L10n.of(context)!.downloadFileInWeb(directory), - ); - - return '$directory/$name'; + try { + final directory = await FileSaver.instance.saveFile( + name: name, + bytes: bytes, + ext: extensionFromMime(mimeType), + mimeType: mimeType.toMimeTypeEnum(), + ); + return '$directory/$name'; + } catch (e) { + Logs().e( + "MatrixFileExtension()::downloadFileInWeb()::Error: $e", + ); + } + return null; } Future downloadImageInMobile(BuildContext context) async { diff --git a/lib/utils/string_extension.dart b/lib/utils/string_extension.dart index d93fd8ae7c..6db9af3811 100644 --- a/lib/utils/string_extension.dart +++ b/lib/utils/string_extension.dart @@ -156,57 +156,57 @@ extension StringCasingExtension on String { MimeType toMimeTypeEnum() { switch (this) { case 'image/jpeg': - return MimeType.JPEG; + return MimeType.jpeg; case 'image/png': - return MimeType.PNG; + return MimeType.png; case 'image/gif': - return MimeType.GIF; + return MimeType.gif; case 'image/bmp': - return MimeType.BMP; + return MimeType.bmp; case 'video/mpeg': - return MimeType.MPEG; + return MimeType.mpeg; case 'video/x-msvideo': - return MimeType.AVI; + return MimeType.avi; case 'audio/mpeg': - return MimeType.MP3; + return MimeType.mp3; case 'audio/aac': - return MimeType.AAC; + return MimeType.aac; case 'application/pdf': - return MimeType.PDF; + return MimeType.pdf; case 'application/epub+zip': - return MimeType.EPUB; + return MimeType.epub; case 'application/json': - return MimeType.JSON; + return MimeType.json; case 'font/otf': - return MimeType.OTF; + return MimeType.otf; case 'font/ttf': - return MimeType.TTF; + return MimeType.ttf; case 'application/zip': - return MimeType.ZIP; + return MimeType.zip; case 'application/vnd.oasis.opendocument.presentation': - return MimeType.OPENDOCPRESENTATION; + return MimeType.openDocPresentation; case 'application/vnd.oasis.opendocument.text': - return MimeType.OPENDOCTEXT; + return MimeType.openDocText; case 'application/vnd.oasis.opendocument.spreadsheet': - return MimeType.OPENDOCSHEETS; + return MimeType.openDocSheets; case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': - return MimeType.MICROSOFTEXCEL; + return MimeType.microsoftExcel; case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': - return MimeType.MICROSOFTPRESENTATION; + return MimeType.microsoftPresentation; case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': - return MimeType.MICROSOFTWORD; + return MimeType.microsoftWord; case 'application/vnd.etsi.asic-e+zip': - return MimeType.ASICE; + return MimeType.asice; case 'application/vnd.etsi.asic-s+zip': - return MimeType.ASICS; + return MimeType.asics; case 'application/octet-stream': - return MimeType.OTHER; + return MimeType.other; case 'text/plain': - return MimeType.TEXT; + return MimeType.text; case 'text/csv': - return MimeType.CSV; + return MimeType.csv; default: - return MimeType.OTHER; + return MimeType.other; } } diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index 5443b0e21e..e116987eb2 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -35,11 +35,11 @@ mixin HandleDownloadAndPreviewFileMixin { } } - void onFileTappedWeb({ + Future onFileTappedWeb({ required Event event, required BuildContext context, - }) { - return handlePreviewWeb(event: event, context: context); + }) async { + return await handlePreviewWeb(event: event, context: context); } void onFileTappedMobile({ @@ -106,7 +106,7 @@ mixin HandleDownloadAndPreviewFileMixin { } } - void handlePreviewWeb({ + Future handlePreviewWeb({ required Event event, required BuildContext context, }) async { @@ -116,7 +116,7 @@ mixin HandleDownloadAndPreviewFileMixin { } if (event.mimeType.isPdfFile()) { - return previewPdfWeb(context, event); + return await previewPdfWeb(context, event); } await event.saveFile(context); @@ -212,7 +212,7 @@ mixin HandleDownloadAndPreviewFileMixin { ); } - void previewPdfWeb(BuildContext context, Event event) async { + Future previewPdfWeb(BuildContext context, Event event) async { final pdf = await event.getFile(context); if (pdf.result == null || event.sizeString != pdf.result?.sizeString) { TwakeSnackBar.show(context, L10n.of(context)!.errorGettingPdf); diff --git a/pubspec.lock b/pubspec.lock index 181a631d79..a98304e835 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -486,10 +486,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" + sha256: "50fec96118958b97c727d0d8f67255d3683f16cc1f90d9bc917b5d4fe3abeca9" url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "5.4.2" dio_cache_interceptor: dependency: "direct main" description: @@ -606,10 +606,10 @@ packages: dependency: "direct main" description: name: file_saver - sha256: "9efc615b43127952aa394e98b23d9e932715e8c008dc10710177008882b3d141" + sha256: bdebc720e17b3e01aba59da69b6d47020a7e5ba7d5c75bd9194f9618d5f16ef4 url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.2.12" file_selector: dependency: "direct overridden" description: diff --git a/pubspec.yaml b/pubspec.yaml index 895db17fee..85517d6a12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -156,7 +156,7 @@ dependencies: cached_network_image: ^3.2.3 flutter_image_compress: ^2.0.4 image_gallery_saver: ^2.0.3 - file_saver: ^0.1.1 + file_saver: ^0.2.12 flutter_keyboard_visibility: ^6.0.0 media_kit: ^1.1.7 media_kit_video: ^1.1.8 From 36431e96294f38c1c765a91570dd51e46922763d Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 11:07:49 +0700 Subject: [PATCH 062/183] TW-1586: refactor MessageDownloadContent to have both widget for web and mobile (cherry picked from commit 779af2e30689568d18f0002a94b24082bf8a64ec) --- lib/pages/chat/events/message_content.dart | 13 +- .../chat/events/message_download_content.dart | 89 ++------- .../events/message_download_content_web.dart | 169 ++++++++++++++++++ lib/pages/chat_search/chat_search_view.dart | 8 +- 4 files changed, 199 insertions(+), 80 deletions(-) create mode 100644 lib/pages/chat/events/message_download_content_web.dart diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 7c27eb1b80..bd01ed84c0 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat/events/call_invite_content.dart'; import 'package:fluffychat/pages/chat/events/encrypted_content.dart'; import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/pages/chat/events/message_download_content_web.dart'; import 'package:fluffychat/pages/chat/events/formatted_text_widget.dart'; import 'package:fluffychat/pages/chat/events/redacted_content.dart'; import 'package:fluffychat/pages/chat/events/sending_image_info_widget.dart'; @@ -99,9 +100,15 @@ class MessageContent extends StatelessWidget return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - MessageDownloadContent( - event, - ), + if (PlatformInfos.isWeb) ...[ + MessageDownloadContent( + event, + ), + ] else ...[ + MessageDownloadContentWeb( + event, + ), + ], Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, child: endOfBubbleWidget, diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 4a93b602dc..e2cc6f79c2 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -10,12 +10,10 @@ import 'package:fluffychat/utils/manager/download_manager/download_file_state.da import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; -import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -51,24 +49,23 @@ class _MessageDownloadContentState extends State } void checkDownloadFileState() async { - if (!PlatformInfos.isWeb) { - final filePath = await widget.event.getFileNameInAppDownload(); - final file = File(filePath); - if (await file.exists() && - await file.length() == widget.event.getFileSize()) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: filePath, - ); - return; - } + final filePath = await widget.event.getFileNameInAppDownload(); + final file = File(filePath); + if (await file.exists() && + await file.length() == widget.event.getFileSize()) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePath, + ); + return; } - setupListeningForStreamSubcription(); + + _trySetupDownloadingStreamSubcription(); if (streamSubscription != null) { downloadFileStateNotifier.value = const DownloadingPresentationState(); } } - void setupListeningForStreamSubcription() { + void _trySetupDownloadingStreamSubcription() { streamSubscription = downloadManager .getDownloadStateStream(widget.event.eventId) ?.listen(setupDownloadingProcess); @@ -93,38 +90,14 @@ class _MessageDownloadContentState extends State downloadFileStateNotifier.value = DownloadedPresentationState( filePath: success.filePath, ); - } else if (success is DownloadMatrixFileSuccessState) { - _handleDownloadMatrixFileSuccessState(success); } }, ); } - void _handleDownloadMatrixFileSuccessState( - DownloadMatrixFileSuccessState success, - ) { - streamSubscription?.cancel(); - if (mounted) { - downloadFileStateNotifier.value = FileWebDownloadedPresentationState( - matrixFile: success.matrixFile, - ); - handlePreviewWeb(event: widget.event, context: context); - return; - } - - if (TwakeApp.routerKey.currentContext != null) { - handlePreviewWeb( - event: widget.event, - context: TwakeApp.routerKey.currentContext!, - ); - } - } - @override void dispose() { - if (!PlatformInfos.isWeb) { - streamSubscription?.cancel(); - } + streamSubscription?.cancel(); downloadFileStateNotifier.dispose(); super.dispose(); } @@ -169,42 +142,6 @@ class _MessageDownloadContentState extends State downloadManager.cancelDownload(widget.event.eventId); }, ); - } else if (state is FileWebDownloadedPresentationState) { - return InkWell( - onTap: () { - onFileTapped( - event: widget.event, - context: context, - ); - }, - child: FileTileWidget( - mimeType: widget.event.mimeType, - fileType: filetype, - filename: filename, - highlightText: widget.highlightText, - sizeString: sizeString, - style: const MessageFileTileStyle(), - ), - ); - } else if (PlatformInfos.isWeb) { - return InkWell( - onTap: () { - downloadFileStateNotifier.value = - const DownloadingPresentationState(); - downloadManager.download( - event: widget.event, - ); - setupListeningForStreamSubcription(); - }, - child: FileTileWidget( - mimeType: widget.event.mimeType, - fileType: filetype, - filename: filename, - highlightText: widget.highlightText, - sizeString: sizeString, - style: const MessageFileTileStyle(), - ), - ); } return InkWell( @@ -214,7 +151,7 @@ class _MessageDownloadContentState extends State downloadManager.download( event: widget.event, ); - setupListeningForStreamSubcription(); + _trySetupDownloadingStreamSubcription(); }, child: DownloadFileTileWidget( mimeType: widget.event.mimeType, diff --git a/lib/pages/chat/events/message_download_content_web.dart b/lib/pages/chat/events/message_download_content_web.dart new file mode 100644 index 0000000000..fcfd4e8101 --- /dev/null +++ b/lib/pages/chat/events/message_download_content_web.dart @@ -0,0 +1,169 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class MessageDownloadContentWeb extends StatefulWidget { + const MessageDownloadContentWeb( + this.event, { + Key? key, + this.highlightText, + }) : super(key: key); + + final Event event; + + final String? highlightText; + + @override + State createState() => _MessageDownloadContentWebState(); +} + +class _MessageDownloadContentWebState extends State + with HandleDownloadAndPreviewFileMixin { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + StreamSubscription>? streamSubscription; + + @override + void initState() { + super.initState(); + _trySetupDownloadingStreamSubcription(); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } + + void _trySetupDownloadingStreamSubcription() { + streamSubscription = downloadManager + .getDownloadStateStream(widget.event.eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess(Either event) { + event.fold( + (failure) { + Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadMatrixFileSuccessState) { + _handleDownloadMatrixFileSuccessState(success); + } + }, + ); + } + + void _handleDownloadMatrixFileSuccessState( + DownloadMatrixFileSuccessState success, + ) { + streamSubscription?.cancel(); + if (mounted) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + handlePreviewWeb(event: widget.event, context: context); + return; + } + + if (TwakeApp.routerKey.currentContext != null) { + handlePreviewWeb( + event: widget.event, + context: TwakeApp.routerKey.currentContext!, + ); + } + } + + @override + void dispose() { + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final filename = widget.event.filename; + final filetype = widget.event.fileType; + final sizeString = widget.event.sizeString; + return ValueListenableBuilder( + valueListenable: downloadFileStateNotifier, + builder: (context, DownloadPresentationState state, child) { + if (state is DownloadingPresentationState) { + return DownloadFileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + downloadFileStateNotifier: downloadFileStateNotifier, + onCancelDownload: () { + downloadFileStateNotifier.value = + const NotDownloadPresentationState(); + downloadManager.cancelDownload(widget.event.eventId); + }, + ); + } else if (state is FileWebDownloadedPresentationState) { + return InkWell( + onTap: () { + handlePreviewWeb( + event: widget.event, + context: context, + ); + }, + child: FileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + ), + ); + } + + return InkWell( + onTap: () { + downloadFileStateNotifier.value = + const DownloadingPresentationState(); + downloadManager.download( + event: widget.event, + ); + _trySetupDownloadingStreamSubcription(); + }, + child: FileTileWidget( + mimeType: widget.event.mimeType, + fileType: filetype, + filename: filename, + highlightText: widget.highlightText, + sizeString: sizeString, + style: const MessageFileTileStyle(), + ), + ); + }, + ); + } +} diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index 91350b9c52..b7cd1fc1b3 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; import 'package:fluffychat/pages/chat/chat_view_style.dart'; import 'package:fluffychat/pages/chat/events/message_download_content.dart'; +import 'package:fluffychat/pages/chat/events/message_download_content_web.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header_style.dart'; import 'package:fluffychat/pages/chat_search/chat_search.dart'; import 'package:fluffychat/pages/chat_search/chat_search_style.dart'; @@ -15,6 +16,7 @@ import 'package:fluffychat/presentation/same_type_events_builder/same_type_event import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/result_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/highlight_text.dart'; @@ -284,7 +286,11 @@ class _MessageContent extends StatelessWidget { Widget build(BuildContext context) { switch (event.messageType) { case MessageTypes.File: - return MessageDownloadContent(event, highlightText: searchWord); + if (PlatformInfos.isWeb) { + return MessageDownloadContentWeb(event, highlightText: searchWord); + } else { + return MessageDownloadContent(event, highlightText: searchWord); + } default: return HighlightText( text: event From 32df73ca13eb5c4cde0e98e595296c9456989fde Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 26 Mar 2024 17:15:36 +0700 Subject: [PATCH 063/183] TW-1605: Fix display thumbnail when user sent a video (cherry picked from commit d45928791a413fa37423d19d19841845ef312ee6) --- .../platform_file/platform_file_extension.dart | 17 +++++++++++++++-- .../chat/events/sending_video_widget.dart | 3 +-- lib/pages/chat_draft/draft_chat.dart | 12 ++++++++++-- .../extensions/send_file_extension.dart | 18 ++++++++++++------ lib/presentation/mixins/send_files_mixin.dart | 17 +++++++++++------ .../matrix_sdk_extensions/event_extension.dart | 5 ----- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 8 files changed, 52 insertions(+), 26 deletions(-) diff --git a/lib/domain/model/extensions/platform_file/platform_file_extension.dart b/lib/domain/model/extensions/platform_file/platform_file_extension.dart index 8190efe7a5..1be8141af4 100644 --- a/lib/domain/model/extensions/platform_file/platform_file_extension.dart +++ b/lib/domain/model/extensions/platform_file/platform_file_extension.dart @@ -2,13 +2,26 @@ import 'package:file_picker/file_picker.dart'; import 'package:matrix/matrix.dart'; extension PlatformFileListExtension on PlatformFile { - MatrixFile toMatrixFile() { + MatrixFile toMatrixFile({ + required String temporaryDirectoryPath, + }) { return MatrixFile.fromMimeType( bytes: bytes, name: name, - filePath: '', + filePath: path ?? '$temporaryDirectoryPath/$name', readStream: readStream, sizeInBytes: size, ); } + + FileInfo toFileInfo({ + required String temporaryDirectoryPath, + }) { + return FileInfo( + name, + path ?? '$temporaryDirectoryPath/$name', + size, + readStream: readStream, + ); + } } diff --git a/lib/pages/chat/events/sending_video_widget.dart b/lib/pages/chat/events/sending_video_widget.dart index 74419f9b08..5719c08502 100644 --- a/lib/pages/chat/events/sending_video_widget.dart +++ b/lib/pages/chat/events/sending_video_widget.dart @@ -1,7 +1,6 @@ import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/presentation/model/file/display_image_info.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; @@ -129,7 +128,7 @@ class VideoWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return matrixFile.bytes != null && event.isThumbnailGenerated() + return matrixFile.bytes != null && matrixFile.bytes!.isNotEmpty ? Image.memory( matrixFile.bytes!, width: imageWidth, diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index ecf5bcbe8e..78701a0a5c 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -32,6 +32,7 @@ import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; import 'package:linagora_design_flutter/images_picker/images_picker.dart' hide ImagePicker; import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; typedef OnRoomCreatedSuccess = FutureOr Function(Room room)?; @@ -328,8 +329,15 @@ class DraftChatController extends State ); if (result == null || result.files.isEmpty) return; - final matrixFilesList = - result.files.map((file) => file.toMatrixFile().detectFileType).toList(); + final temporaryDirectory = await getTemporaryDirectory(); + + final matrixFilesList = result.files + .map( + (file) => file + .toMatrixFile(temporaryDirectoryPath: temporaryDirectory.path) + .detectFileType, + ) + .toList(); await sendImagesWithCaption( context: context, diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index f2e1f6c43b..6d8b0b8583 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -470,12 +470,18 @@ extension SendFileExtension on Room { var width = fileInfo.width; var height = fileInfo.height; if (width == null || height == null) { - final imageDimension = await runBenchmarked( - '_calculateImageDimension', - () => _calculateImageBytesDimension(fileInfo.imagePlaceholderBytes), - ); - width = imageDimension.width.toInt(); - height = imageDimension.height.toInt(); + try { + final imageDimension = await _calculateImageBytesDimension( + tempThumbnailFile.readAsBytesSync(), + ); + width = imageDimension.width.toInt(); + height = imageDimension.height.toInt(); + } catch (e) { + Logs().e( + '_getThumbnailVideo():: Error while calculating image dimension', + e, + ); + } } Logs().d('Video thumbnail generated', tempThumbnailFile.path); final newThumbnail = ImageFileInfo( diff --git a/lib/presentation/mixins/send_files_mixin.dart b/lib/presentation/mixins/send_files_mixin.dart index 5a97519eed..18565d9d9b 100644 --- a/lib/presentation/mixins/send_files_mixin.dart +++ b/lib/presentation/mixins/send_files_mixin.dart @@ -42,13 +42,11 @@ mixin SendFilesMixin { withReadStream: true, allowMultiple: true, ); + final temporaryDirectory = await getTemporaryDirectory(); fileInfos ??= result?.files .map( - (xFile) => FileInfo( - xFile.name, - xFile.path ?? '${getTemporaryDirectory()}/${xFile.name}', - xFile.size, - readStream: xFile.readStream, + (xFile) => xFile.toFileInfo( + temporaryDirectoryPath: temporaryDirectory.path, ), ) .toList(); @@ -65,7 +63,14 @@ mixin SendFilesMixin { withReadStream: true, ); if (result == null || result.files.isEmpty) return []; - return result.files.map((file) => file.toMatrixFile()).toList(); + final temporaryDirectory = await getTemporaryDirectory(); + return result.files + .map( + (file) => file.toMatrixFile( + temporaryDirectoryPath: temporaryDirectory.path, + ), + ) + .toList(); } void onPickerTypeClick({ diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 5fe62f452a..8e8db0c80c 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -214,9 +214,4 @@ extension LocalizedBody on Event { hideReply: true, ); } - - bool isThumbnailGenerated() { - return fileSendingStatus == FileSendingStatus.encrypting || - fileSendingStatus == FileSendingStatus.uploading; - } } diff --git a/pubspec.lock b/pubspec.lock index a98304e835..7f78217c71 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1598,8 +1598,8 @@ packages: dependency: "direct main" description: path: "." - ref: "twake-supported-0.22.6" - resolved-ref: "22311972c4d781133b893ae032dcb509b1351075" + ref: update-duratation-for-file-info + resolved-ref: "93cbacde518ed75d73a0f6b76b213b041233689f" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" diff --git a/pubspec.yaml b/pubspec.yaml index 85517d6a12..f029dc0f8f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: matrix: git: url: git@github.com:linagora/matrix-dart-sdk.git - ref: twake-supported-0.22.6 + ref: update-duratation-for-file-info receive_sharing_intent: git: From af8e311ef3c6e4a0aa5c1d93c64b7eb25a836fdb Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 18:02:39 +0700 Subject: [PATCH 064/183] TW-1605: Fix image thumbnail from file (cherry picked from commit a3700f0cf08d57de7bc759a344b02e3609b86959) --- .../extensions/send_file_extension.dart | 21 +++++++++++++------ lib/presentation/mixins/send_files_mixin.dart | 6 ++++-- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index 6d8b0b8583..abccd5530f 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -126,7 +126,7 @@ extension SendFileExtension on Room { fileSendingStatusKey, FileSendingStatus.generatingThumbnail.name, ); - thumbnail ??= await _getThumbnailVideo(tempThumbnailFile, fileInfo); + thumbnail ??= await _getThumbnailVideo(tempThumbnailFile, fileInfo, txid); if (fileInfo.width == null || fileInfo.height == null) { fileInfo = VideoFileInfo( fileInfo.fileName, @@ -136,10 +136,18 @@ extension SendFileExtension on Room { width: thumbnail.width, height: thumbnail.height, ); - storePlaceholderFileInMem( - fileInfo: fileInfo, - txid: txid, - ); + if (fileInfo.imagePlaceholderBytes.isNotEmpty) { + storePlaceholderFileInMem( + fileInfo: fileInfo, + txid: txid, + ); + } else { + storePlaceholderFileInMem( + fileInfo: thumbnail, + txid: txid, + ); + } + fakeImageEvent = await sendFakeImagePickerFileEvent( fileInfo, txid: txid, @@ -453,6 +461,7 @@ extension SendFileExtension on Room { Future _getThumbnailVideo( File tempThumbnailFile, VideoFileInfo fileInfo, + String txid, ) async { final int fileSize; if (fileInfo.imagePlaceholderBytes.isNotEmpty) { @@ -472,7 +481,7 @@ extension SendFileExtension on Room { if (width == null || height == null) { try { final imageDimension = await _calculateImageBytesDimension( - tempThumbnailFile.readAsBytesSync(), + await tempThumbnailFile.readAsBytes(), ); width = imageDimension.width.toInt(); height = imageDimension.height.toInt(); diff --git a/lib/presentation/mixins/send_files_mixin.dart b/lib/presentation/mixins/send_files_mixin.dart index 18565d9d9b..39ee901e00 100644 --- a/lib/presentation/mixins/send_files_mixin.dart +++ b/lib/presentation/mixins/send_files_mixin.dart @@ -45,8 +45,10 @@ mixin SendFilesMixin { final temporaryDirectory = await getTemporaryDirectory(); fileInfos ??= result?.files .map( - (xFile) => xFile.toFileInfo( - temporaryDirectoryPath: temporaryDirectory.path, + (xFile) => FileInfo.fromMatrixFile( + xFile.toMatrixFile( + temporaryDirectoryPath: temporaryDirectory.path, + ), ), ) .toList(); diff --git a/pubspec.lock b/pubspec.lock index 7f78217c71..baa850bd36 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1598,8 +1598,8 @@ packages: dependency: "direct main" description: path: "." - ref: update-duratation-for-file-info - resolved-ref: "93cbacde518ed75d73a0f6b76b213b041233689f" + ref: "twake-supported-0.22.6" + resolved-ref: "8f821c1cab2506d13c2266cc8759ffd2b2818770" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" diff --git a/pubspec.yaml b/pubspec.yaml index f029dc0f8f..85517d6a12 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: matrix: git: url: git@github.com:linagora/matrix-dart-sdk.git - ref: update-duratation-for-file-info + ref: twake-supported-0.22.6 receive_sharing_intent: git: From 84826ebdc76205b482a4dcffa3d08b041fbc01e4 Mon Sep 17 00:00:00 2001 From: Nguyen Thai Date: Fri, 29 Mar 2024 09:13:37 +0700 Subject: [PATCH 065/183] Used custom configuration for preview env Bumped libolm version (cherry picked from commit a4ec11e292dbe041c20c35f8f0ca114bad529923) --- .github/workflows/gh-pages.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index f8f1fa0dca..18b5c0c8c3 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -3,7 +3,7 @@ on: env: FLUTTER_VERSION: 3.16.5 - LIBOLM_VERSION: 3.2.15 + LIBOLM_VERSION: 3.2.16 name: Deploying on GitHub Pages @@ -39,7 +39,7 @@ jobs: ssh-private-key: ${{ secrets.SSH_KEY }} - name: Setup Nix (to build libolm) - uses: cachix/install-nix-action@v19 + uses: cachix/install-nix-action@v26 - name: Build libolm run: | @@ -51,13 +51,14 @@ jobs: - name: Build Web version env: FOLDER: ${{ github.event.pull_request.number }} + TWAKE_PREVIEW_CONFIG: ${{ secrets.TWAKE_PREVIEW_CONFIG }} run: | flutter config --enable-web flutter clean flutter pub get flutter pub run build_runner build --delete-conflicting-outputs flutter build web --release --verbose --source-maps --base-href="/${GITHUB_REPOSITORY##*/}/$FOLDER/" - yq '.issue_id = strenv(FOLDER)' config.sample.json > ./build/web/config.json + echo "$TWAKE_PREVIEW_CONFIG" | yq '.issue_id = strenv(FOLDER)' > ./build/web/config.json - name: Configure environments id: configure From 38d7c77ffbdcc1bdda3c2c76856dc251b948f793 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 28 Mar 2024 15:48:50 +0700 Subject: [PATCH 066/183] Hot fix: Enable `dart-define` for configuration public platform on mobile (cherry picked from commit c048d4ec500be29eb3a78feaf7414223b54ca37b) --- lib/config/app_config.dart | 25 +++++++++++++++++++ lib/pages/twake_welcome/twake_welcome.dart | 4 +-- .../twake_welcome/twake_welcome_view.dart | 11 +++++--- lib/widgets/matrix.dart | 15 ++++++++--- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 4b5e50112c..fa0c3b65ef 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -103,6 +103,31 @@ abstract class AppConfig { static const String appGridConfigurationPath = "configurations/app_dashboard.json"; + static void loadEnvironment() { + twakeWorkplaceHomeserver = const String.fromEnvironment( + 'TWAKE_WORKPLACE_HOMESERVER', + defaultValue: 'https://example.com/', + ); + + registrationUrl = const String.fromEnvironment( + 'REGISTRATION_URL', + defaultValue: 'https://example.com/', + ); + + platform = const String.fromEnvironment( + 'PLATFORM', + defaultValue: 'platform', + ); + + homeserver = const String.fromEnvironment( + 'HOME_SERVER', + defaultValue: 'https://example.com/', + ); + } + + static bool get isSaasPlatForm => + platform != null && platform!.isNotEmpty && platform == 'saas'; + static void loadFromJson(Map json) { if (json['homeserver'] != null && json['homeserver'] is String) { if (json['homeserver'] != '') { diff --git a/lib/pages/twake_welcome/twake_welcome.dart b/lib/pages/twake_welcome/twake_welcome.dart index 0e77fa07dd..dfc9774091 100644 --- a/lib/pages/twake_welcome/twake_welcome.dart +++ b/lib/pages/twake_welcome/twake_welcome.dart @@ -50,10 +50,10 @@ class TwakeWelcomeController extends State with ConnectPageMixin { static const String postRegisteredRedirectUrlPathParams = 'post_registered_redirect_url'; - String loginUrl = + String get loginUrl => "${AppConfig.registrationUrl}?$postLoginRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; - String signupUrl = + String get signupUrl => "${AppConfig.registrationUrl}?$postRegisteredRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; MatrixState get matrix => Matrix.of(context); diff --git a/lib/pages/twake_welcome/twake_welcome_view.dart b/lib/pages/twake_welcome/twake_welcome_view.dart index 6f2283121c..82468e2b42 100644 --- a/lib/pages/twake_welcome/twake_welcome_view.dart +++ b/lib/pages/twake_welcome/twake_welcome_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome_view_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; @@ -18,13 +19,15 @@ class TwakeWelcomeView extends StatelessWidget { hoverColor: Colors.transparent, highlightColor: Colors.transparent, overlayColor: MaterialStateProperty.all(Colors.transparent), - signInTitle: L10n.of(context)!.signIn, - createTwakeIdTitle: L10n.of(context)!.createTwakeId, + signInTitle: AppConfig.isSaasPlatForm ? L10n.of(context)!.signIn : null, + createTwakeIdTitle: + AppConfig.isSaasPlatForm ? L10n.of(context)!.createTwakeId : null, useCompanyServerTitle: L10n.of(context)!.useYourCompanyServer, description: L10n.of(context)!.descriptionTwakeId, onUseCompanyServerOnTap: controller.goToHomeserverPicker, - onSignInOnTap: controller.onClickSignIn, - onCreateTwakeIdOnTap: controller.onClickCreateTwakeId, + onSignInOnTap: AppConfig.isSaasPlatForm ? controller.onClickSignIn : null, + onCreateTwakeIdOnTap: + AppConfig.isSaasPlatForm ? controller.onClickCreateTwakeId : null, logo: SvgPicture.asset( ImagePaths.logoTwakeWelcome, width: TwakeWelcomeViewStyle.logoWidth, diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index eda9f7bd5d..be2e502699 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -290,9 +290,9 @@ class MatrixState extends State initMatrix(); initReceiveSharingIntent(); if (PlatformInfos.isWeb) { - initConfig().then((_) => initSettings()); + initConfigWeb().then((_) => initSettings()); } else { - initSettings(); + initConfigMobile().then((_) => initSettings()); } initLoadingDialog(); }); @@ -307,12 +307,13 @@ class MatrixState extends State }); } - Future initConfig() async { + Future initConfigWeb() async { try { final configJsonString = utf8.decode((await http.get(Uri.parse('config.json'))).bodyBytes); final configJson = json.decode(configJsonString); AppConfig.loadFromJson(configJson); + Logs().d('[ConfigLoader] $configJson'); } on FormatException catch (_) { Logs().v('[ConfigLoader] config.json not found'); } catch (e) { @@ -320,6 +321,14 @@ class MatrixState extends State } } + Future initConfigMobile() async { + try { + AppConfig.loadEnvironment(); + } catch (e) { + Logs().v('[ConfigLoader] config.json not found', e); + } + } + void _registerSubs(String name) async { final currentClient = getClientByName(name); if (currentClient == null) { From 6a18bfcf3c117ec244dc108f8071adfa6f0c5a1d Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Sat, 16 Mar 2024 11:32:06 +0000 Subject: [PATCH 067/183] TW-1456: texts added (cherry picked from commit 7f22ba92b8e13c12e521ac635d3b4f71616ebb5b) --- assets/l10n/intl_en.arb | 5 ++++- assets/l10n/intl_fr.arb | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 73047ba7cb..d41296e95d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3010,5 +3010,8 @@ "unselect": "Unselect", "searchContacts": "Search contacts", "tapToAllowAccessToYourMicrophone": "You can enable microphone access in the Settings app to make voice in", - "showInChat": "Show in chat" + "showInChat": "Show in chat", + "phone": "Phone", + "viewProfile": "View profile", + "profileInfo": "Profile info" } diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index 2d84a36bd1..e24e199c37 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -3078,4 +3078,7 @@ "@unselect": {}, "searchContacts": "Rechercher des contacts", "@searchContacts": {} + "phone": "Téléphone", + "viewProfile": "Voir le profil", + "profileInfo": "Informations du profil" } From 78af46d245ea273a807c9ac9d1c76202bf4c2adc Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Sat, 16 Mar 2024 11:41:39 +0000 Subject: [PATCH 068/183] TW-1456: Profile info renamed Chat profile info (cherry picked from commit 046319442aa3af9691408a50385a628f9f6affbd) --- .../chat_adaptive_scaffold.dart | 2 +- .../chat_profile_info/chat_profile_info.dart | 12 ++++++------ .../chat_profile_info_navigator.dart | 18 +++++++++--------- .../chat_profile_info_shared.dart | 14 +++++++------- .../chat_profile_info_shared_view.dart | 6 +++--- .../chat_profile_info_view.dart | 6 +++--- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart index 8524f1b52b..19525e500c 100644 --- a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart +++ b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart @@ -72,7 +72,7 @@ class _RightColumnNavigator extends StatelessWidget { isInStack: isInStack, ); case RightColumnType.profileInfo: - return ProfileInfoNavigator( + return ChatProfileInfoNavigator( onBack: controller.hideRightColumn, roomId: roomId, isInStack: isInStack, diff --git a/lib/pages/chat_profile_info/chat_profile_info.dart b/lib/pages/chat_profile_info/chat_profile_info.dart index 2e772fb556..b68e439815 100644 --- a/lib/pages/chat_profile_info/chat_profile_info.dart +++ b/lib/pages/chat_profile_info/chat_profile_info.dart @@ -13,14 +13,14 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; import 'package:matrix/matrix.dart'; -class ProfileInfo extends StatefulWidget { +class ChatProfileInfo extends StatefulWidget { final VoidCallback? onBack; final String? roomId; final PresentationContact? contact; final bool isInStack; final bool isDraftInfo; - const ProfileInfo({ + const ChatProfileInfo({ super.key, required this.onBack, required this.isInStack, @@ -30,10 +30,10 @@ class ProfileInfo extends StatefulWidget { }); @override - State createState() => ProfileInfoController(); + State createState() => ChatProfileInfoController(); } -class ProfileInfoController extends State { +class ChatProfileInfoController extends State { final _lookupMatchContactInteractor = getIt.get(); @@ -66,7 +66,7 @@ class ProfileInfoController extends State { Navigator.of(context).push( CupertinoPageRoute( builder: (context) { - return ProfileInfoShared( + return ChatProfileInfoShared( roomId: widget.roomId!, closeRightColumn: widget.onBack, ); @@ -90,6 +90,6 @@ class ProfileInfoController extends State { @override Widget build(BuildContext context) { - return ProfileInfoView(this); + return ChatProfileInfoView(this); } } diff --git a/lib/pages/chat_profile_info/chat_profile_info_navigator.dart b/lib/pages/chat_profile_info/chat_profile_info_navigator.dart index 53181b093b..5a3f66159a 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_navigator.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_navigator.dart @@ -5,19 +5,19 @@ import 'package:flutter/cupertino.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info.dart'; import 'package:fluffychat/presentation/model/presentation_contact.dart'; -class ProfileInfoRoutes { +class ChatProfileInfoRoutes { static const String profileInfo = '/profileInfo'; static const String profileInfoShared = 'profileInfo/shared'; } -class ProfileInfoNavigator extends StatelessWidget { +class ChatProfileInfoNavigator extends StatelessWidget { final VoidCallback? onBack; final String? roomId; final PresentationContact? contact; final bool isInStack; final bool isDraftInfo; - const ProfileInfoNavigator({ + const ChatProfileInfoNavigator({ Key? key, this.onBack, this.roomId, @@ -29,7 +29,7 @@ class ProfileInfoNavigator extends StatelessWidget { @override Widget build(BuildContext context) { if (PlatformInfos.isMobile) { - return ProfileInfo( + return ChatProfileInfo( onBack: onBack, isInStack: isInStack, roomId: roomId, @@ -38,12 +38,12 @@ class ProfileInfoNavigator extends StatelessWidget { ); } return Navigator( - initialRoute: ProfileInfoRoutes.profileInfo, + initialRoute: ChatProfileInfoRoutes.profileInfo, onGenerateRoute: (route) => CupertinoPageRoute( builder: (context) { switch (route.name) { - case ProfileInfoRoutes.profileInfo: - return ProfileInfo( + case ChatProfileInfoRoutes.profileInfo: + return ChatProfileInfo( onBack: onBack, isInStack: isInStack, roomId: roomId, @@ -51,8 +51,8 @@ class ProfileInfoNavigator extends StatelessWidget { isDraftInfo: isDraftInfo, ); - case ProfileInfoRoutes.profileInfoShared: - return ProfileInfoShared( + case ChatProfileInfoRoutes.profileInfoShared: + return ChatProfileInfoShared( roomId: route.arguments as String, ); default: diff --git a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart index e456508360..f54fe75e6b 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart @@ -12,21 +12,21 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -class ProfileInfoShared extends StatefulWidget { +class ChatProfileInfoShared extends StatefulWidget { final String roomId; final VoidCallback? closeRightColumn; - const ProfileInfoShared({ + const ChatProfileInfoShared({ super.key, required this.roomId, this.closeRightColumn, }); @override - State createState() => ProfileInfoSharedController(); + State createState() => ChatProfileInfoSharedController(); } -class ProfileInfoSharedController extends State +class ChatProfileInfoSharedController extends State with HandleVideoDownloadMixin, PlayVideoActionMixin, @@ -67,7 +67,7 @@ class ProfileInfoSharedController extends State ? const SizedBox() : ChatDetailsMediaPage( key: const PageStorageKey( - 'ProfileInfoSharedMedia', + 'ChatProfileInfoSharedMedia', ), controller: mediaListController!, handleDownloadVideoEvent: _handleDownloadAndPlayVideo, @@ -81,7 +81,7 @@ class ProfileInfoSharedController extends State ? const SizedBox() : ChatDetailsLinksPage( key: const PageStorageKey( - 'ProfileInfoSharedLinks', + 'ChatProfileInfoSharedLinks', ), controller: linksListController!, ), @@ -164,7 +164,7 @@ class ProfileInfoSharedController extends State @override Widget build(BuildContext context) { - return ProfileInfoSharedView( + return ChatProfileInfoSharedView( controller: this, ); } diff --git a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart index 4389a7ef02..f7bdd78d9d 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; -class ProfileInfoSharedView extends StatelessWidget { - final ProfileInfoSharedController controller; +class ChatProfileInfoSharedView extends StatelessWidget { + final ChatProfileInfoSharedController controller; - const ProfileInfoSharedView({ + const ChatProfileInfoSharedView({ super.key, required this.controller, }); diff --git a/lib/pages/chat_profile_info/chat_profile_info_view.dart b/lib/pages/chat_profile_info/chat_profile_info_view.dart index 1cd4d67027..c0cfcc492d 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_view.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_view.dart @@ -19,10 +19,10 @@ import 'package:fluffychat/pages/chat_profile_info/chat_profile_info.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_style.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class ProfileInfoView extends StatelessWidget { - final ProfileInfoController controller; +class ChatProfileInfoView extends StatelessWidget { + final ChatProfileInfoController controller; - const ProfileInfoView( + const ChatProfileInfoView( this.controller, { super.key, }); From 63a8d8ffd8b81532aeb2e7e17e0adad56038129b Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Sat, 16 Mar 2024 12:10:04 +0000 Subject: [PATCH 069/183] TW-1456: profile info views created (cherry picked from commit 245e6e41608af8314ba4197da21735fbdad1eb32) --- lib/config/go_routes/go_router.dart | 20 ++ .../draft_chat_adaptive_scaffold.dart | 2 +- .../copiable_profile_row.dart | 102 ++++++++++ .../copiable_profile_row_style.dart | 11 ++ .../icon_copiable_profile_row.dart | 21 ++ .../svg_copiable_profile_row.dart | 26 +++ lib/pages/profile_info/profile_info.dart | 27 +++ .../profile_info_body/profile_info_body.dart | 81 ++++++++ .../profile_info_body_view.dart | 180 ++++++++++++++++++ .../profile_info_body_view_style.dart | 29 +++ lib/pages/profile_info/profile_info_view.dart | 64 +++++++ .../profile_info/profile_info_view_style.dart | 9 + .../user_bottom_sheet/user_bottom_sheet.dart | 168 ---------------- .../user_bottom_sheet_view.dart | 137 ------------- .../presence_extension.dart | 32 ++++ 15 files changed, 603 insertions(+), 306 deletions(-) create mode 100644 lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart create mode 100644 lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart create mode 100644 lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart create mode 100644 lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart create mode 100644 lib/pages/profile_info/profile_info.dart create mode 100644 lib/pages/profile_info/profile_info_body/profile_info_body.dart create mode 100644 lib/pages/profile_info/profile_info_body/profile_info_body_view.dart create mode 100644 lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart create mode 100644 lib/pages/profile_info/profile_info_view.dart create mode 100644 lib/pages/profile_info/profile_info_view_style.dart delete mode 100644 lib/pages/user_bottom_sheet/user_bottom_sheet.dart delete mode 100644 lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 4d77f4d177..f360dafcc1 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/pages/error_page/error_page.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pages/login/on_auth_redirect.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; +import 'package:fluffychat/pages/profile_info/profile_info.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settings_app_language.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; @@ -208,6 +209,25 @@ abstract class AppRoutes { ), ], ), + GoRoute( + path: 'profileinfo/:roomid/:userid', + pageBuilder: (context, state) { + final roomId = state.pathParameters['roomid']; + final userId = state.pathParameters['userid']; + + if (roomId == null || userId == null) { + return defaultPageBuilder(context, const ErrorPage()); + } + + return defaultPageBuilder( + context, + ProfileInfo( + roomId: roomId, + userId: userId, + ), + ); + }, + ), GoRoute( path: 'archive', pageBuilder: (context, state) => defaultPageBuilder( diff --git a/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart b/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart index e0e15ee243..29cf4d800c 100644 --- a/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart +++ b/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart @@ -30,7 +30,7 @@ class DraftChatAdaptiveScaffold extends StatelessWidget { }) { switch (type) { case RightColumnType.profileInfo: - return ProfileInfoNavigator( + return ChatProfileInfoNavigator( onBack: controller.hideRightColumn, contact: _contact, isInStack: isInStack, diff --git a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart new file mode 100644 index 0000000000..025a11821f --- /dev/null +++ b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart @@ -0,0 +1,102 @@ +import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_style.dart'; +import 'package:fluffychat/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view_style.dart'; +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class CopiableProfileRow extends StatelessWidget { + final String caption; + final String copiableText; + final Widget leadingIcon; + + const CopiableProfileRow({ + required this.leadingIcon, + required this.caption, + required this.copiableText, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: ProfileInfoBodyViewStyle.copiableRowPadding, + child: InkWell( + customBorder: RoundedRectangleBorder( + borderRadius: CopiableProfileRowStyle.rippleRadius, + ), + onTap: () { + TwakeClipboard.instance.copyText(copiableText); + TwakeSnackBar.show( + context, + L10n.of(context)!.copiedToClipboard, + ); + }, + child: Padding( + padding: CopiableProfileRowStyle.ripplePadding, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + leadingIcon, + ], + ), + const SizedBox( + width: + CopiableProfileRowStyle.spacerBetweenLeadingIconAndContent, + ), + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: LinagoraSysColors.material() + .surfaceTint + .withOpacity(CopiableProfileRowStyle.borderOpacity), + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + caption, + style: Theme.of(context).textTheme.bodySmall, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + copiableText, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + const Icon( + Icons.content_copy, + size: ChatProfileInfoStyle.copyIconSize, + ), + ], + ), + const SizedBox( + height: CopiableProfileRowStyle.textColumnBottomPadding, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart new file mode 100644 index 0000000000..be923e0827 --- /dev/null +++ b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +class CopiableProfileRowStyle { + + static const double spacerBetweenLeadingIconAndContent = 8; + static const double borderOpacity = 0.16; + static const double textColumnBottomPadding = 16; + + static const EdgeInsets ripplePadding = EdgeInsets.all(8.0); + static BorderRadiusGeometry rippleRadius = BorderRadius.circular(8); +} diff --git a/lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart b/lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart new file mode 100644 index 0000000000..c68d05251a --- /dev/null +++ b/lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart @@ -0,0 +1,21 @@ +import 'package:fluffychat/pages/profile_info/copiable_profile_row/copiable_profile_row.dart'; +import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_style.dart'; +import 'package:flutter/material.dart'; + +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class IconCopiableProfileRow extends CopiableProfileRow { + IconCopiableProfileRow({ + required IconData icon, + required super.caption, + required super.copiableText, + Key? key, + }) : super( + key: key, + leadingIcon: Icon( + icon, + size: ChatProfileInfoStyle.iconSize, + color: LinagoraSysColors.material().onSurface, + ), + ); +} diff --git a/lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart b/lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart new file mode 100644 index 0000000000..ff4d4afcd4 --- /dev/null +++ b/lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart @@ -0,0 +1,26 @@ +import 'package:fluffychat/pages/profile_info/copiable_profile_row/copiable_profile_row.dart'; +import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_style.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class SvgCopiableProfileRow extends CopiableProfileRow { + SvgCopiableProfileRow({ + required String leadingIconPath, + required super.caption, + required super.copiableText, + Key? key, + }) : super( + key: key, + leadingIcon: SvgPicture.asset( + leadingIconPath, + width: ChatProfileInfoStyle.iconSize, + height: ChatProfileInfoStyle.iconSize, + colorFilter: ColorFilter.mode( + LinagoraSysColors.material().onSurface, + BlendMode.srcIn, + ), + ), + ); +} diff --git a/lib/pages/profile_info/profile_info.dart b/lib/pages/profile_info/profile_info.dart new file mode 100644 index 0000000000..2bd2a79d38 --- /dev/null +++ b/lib/pages/profile_info/profile_info.dart @@ -0,0 +1,27 @@ +import 'package:fluffychat/pages/profile_info/profile_info_view.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class ProfileInfo extends StatefulWidget { + const ProfileInfo({ + super.key, + required this.roomId, + required this.userId, + }); + + final String roomId; + final String userId; + + @override + State createState() => ProfileInfoState(); +} + +class ProfileInfoState extends State { + Room? get room => Matrix.of(context).client.getRoomById(widget.roomId); + + User? get user => room?.unsafeGetUserFromMemoryOrFallback(widget.userId); + + @override + Widget build(BuildContext context) => ProfileInfoView(this); +} diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body.dart b/lib/pages/profile_info/profile_info_body/profile_info_body.dart new file mode 100644 index 0000000000..fd9cfa864f --- /dev/null +++ b/lib/pages/profile_info/profile_info_body/profile_info_body.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart' hide State; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; +import 'package:fluffychat/domain/usecase/contacts/lookup_match_contact_interactor.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:matrix/matrix.dart'; + +class ProfileInfoBody extends StatefulWidget { + const ProfileInfoBody({ + required this.user, + required this.parentContext, + this.onNewChatOpen, + Key? key, + }) : super(key: key); + + final User? user; + + final BuildContext parentContext; + + final void Function()? onNewChatOpen; + + @override + State createState() => ProfileInfoBodyController(); +} + +class ProfileInfoBodyController extends State { + final _lookupMatchContactInteractor = + getIt.get(); + + StreamSubscription? lookupContactNotifierSub; + + final ValueNotifier> lookupContactNotifier = + ValueNotifier>( + const Right(LookupContactsInitial()), + ); + + User? get user => widget.user; + + BuildContext get parentContext => widget.parentContext; + + bool get isOwnProfile => user?.id == user?.room.client.userID; + + void lookupMatchContactAction() { + if (user == null) return; + lookupContactNotifierSub = _lookupMatchContactInteractor + .execute( + val: user!.id, + ) + .listen( + (event) => lookupContactNotifier.value = event, + ); + } + + void openNewChat(String roomId) { + parentContext.go('/rooms/$roomId'); + if (widget.onNewChatOpen != null) widget.onNewChatOpen!(); + } + + @override + void initState() { + lookupMatchContactAction(); + super.initState(); + } + + @override + void dispose() { + lookupContactNotifier.dispose(); + lookupContactNotifierSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => ProfileInfoBodyView(controller: this); +} diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart new file mode 100644 index 0000000000..daa6e33ef3 --- /dev/null +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart @@ -0,0 +1,180 @@ +import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; +import 'package:fluffychat/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart'; +import 'package:fluffychat/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view_style.dart'; +import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/presence_extension.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:matrix/matrix.dart'; + +class ProfileInfoBodyView extends StatelessWidget { + const ProfileInfoBodyView({ + required this.controller, + Key? key, + }) : super(key: key); + final ProfileInfoBodyController controller; + + @override + Widget build(BuildContext context) { + if (controller.user == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: ProfileInfoBodyViewStyle.profileInformationsTopPadding, + child: Column( + children: [ + _ProfileInfoHeader(controller.user!), + _ProfileInfoContactRows( + user: controller.user!, + lookupContactNotifier: controller.lookupContactNotifier, + ), + ], + ), + ), + if (!controller.isOwnProfile) ...[ + Divider( + thickness: ProfileInfoBodyViewStyle.bigDividerThickness, + color: LinagoraSysColors.material().surface, + ), + Padding( + padding: ProfileInfoBodyViewStyle.newChatButtonPadding, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + onPressed: () async { + final roomIdResult = + await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => controller.user!.startDirectChat(), + ); + if (roomIdResult.error != null) return; + + controller.openNewChat(roomIdResult.result!); + }, + icon: const Icon(Icons.chat_outlined), + label: L10n.of(context)?.newChat != null + ? Row( + children: [ + Expanded( + child: Text( + L10n.of(context)!.newChat, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + ), + ], + ), + ), + ] else + const SizedBox(height: 16), + ], + ); + } +} + +class _ProfileInfoHeader extends StatelessWidget { + const _ProfileInfoHeader(this.user, {Key? key}) : super(key: key); + final User user; + + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + final presence = client.presences[user.id]; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: ProfileInfoBodyViewStyle.avatarPadding, + child: Avatar( + mxContent: user.avatarUrl, + name: user.calcDisplayname(), + ), + ), + Text( + user.calcDisplayname(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (presence != null) ...[ + const SizedBox(height: 8), + Text( + presence.getLocalizedStatusMessage(context), + style: presence.getPresenceTextStyle(context), + ), + ], + ], + ); + } +} + +class _ProfileInfoContactRows extends StatelessWidget { + const _ProfileInfoContactRows({ + required this.user, + required this.lookupContactNotifier, + Key? key, + }) : super(key: key); + final User user; + final ValueListenable lookupContactNotifier; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 16), + SvgCopiableProfileRow( + leadingIconPath: ImagePaths.icMatrixid, + caption: L10n.of(context)!.matrixId, + copiableText: user.id, + ), + ValueListenableBuilder( + valueListenable: lookupContactNotifier, + // valueListenable: controller.lookupContactNotifier, + builder: (context, contact, child) { + return contact.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is LookupMatchContactSuccess) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (success.contact.email != null) + IconCopiableProfileRow( + icon: Icons.alternate_email, + caption: L10n.of(context)!.email, + copiableText: success.contact.email!, + ), + if (success.contact.phoneNumber != null) + IconCopiableProfileRow( + icon: Icons.call, + caption: L10n.of(context)!.phone, + copiableText: success.contact.phoneNumber!, + ), + ], + ); + } + return const SizedBox.shrink(); + }, + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart new file mode 100644 index 0000000000..bd47854812 --- /dev/null +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart @@ -0,0 +1,29 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:flutter/material.dart'; + +class ProfileInfoBodyViewStyle { + static ResponsiveUtils responsive = getIt.get(); + + static const EdgeInsetsGeometry profileInformationsTopPadding = EdgeInsets.only( + top: 16.0, + left: 16.0, + right: 16.0, + ); + + static const double bigDividerThickness = 4; + + static const EdgeInsetsGeometry newChatButtonPadding = EdgeInsets.only( + bottom: 16.0, + left: 16.0, + right: 16.0, + ); + + static const EdgeInsetsGeometry copiableRowPadding = EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8, + ); + + static const EdgeInsetsGeometry avatarPadding = + EdgeInsets.symmetric(vertical: 16.0); +} diff --git a/lib/pages/profile_info/profile_info_view.dart b/lib/pages/profile_info/profile_info_view.dart new file mode 100644 index 0000000000..d642086753 --- /dev/null +++ b/lib/pages/profile_info/profile_info_view.dart @@ -0,0 +1,64 @@ +import 'package:fluffychat/pages/profile_info/profile_info.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_view_style.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_state_layer.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ProfileInfoView extends StatelessWidget { + const ProfileInfoView( + this.controller, { + super.key, + }); + + final ProfileInfoState controller; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: LinagoraSysColors.material().onPrimary, + automaticallyImplyLeading: false, + bottom: PreferredSize( + preferredSize: const Size(double.infinity, 1), + child: Container( + color: LinagoraStateLayer( + LinagoraSysColors.material().surfaceTint, + ).opacityLayer1, + height: 1, + ), + ), + title: Padding( + padding: ProfileInfoViewStyle.navigationAppBarPadding, + child: Row( + children: [ + Padding( + padding: ProfileInfoViewStyle.backIconPadding, + child: IconButton( + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + onPressed: () => context.pop(), + icon: const Icon(Icons.arrow_back), + ), + ), + Flexible( + child: Text( + L10n.of(context)!.profileInfo, + style: Theme.of(context).textTheme.titleLarge, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + body: ProfileInfoBody( + user: controller.user, + parentContext: context, + ), + ); + } +} diff --git a/lib/pages/profile_info/profile_info_view_style.dart b/lib/pages/profile_info/profile_info_view_style.dart new file mode 100644 index 0000000000..bd77134b9b --- /dev/null +++ b/lib/pages/profile_info/profile_info_view_style.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class ProfileInfoViewStyle { + + static const EdgeInsetsGeometry backIconPadding = + EdgeInsets.symmetric(vertical: 8, horizontal: 4); + static const EdgeInsets navigationAppBarPadding = + EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0); +} diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart deleted file mode 100644 index 97a32e8aaa..0000000000 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/twake_snackbar.dart'; -import 'package:flutter/material.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; -import 'package:fluffychat/widgets/permission_slider_dialog.dart'; -import '../../widgets/matrix.dart'; -import 'user_bottom_sheet_view.dart'; - -enum UserBottomSheetAction { - report, - mention, - ban, - kick, - unban, - permission, - message, - ignore, -} - -enum ChatMembersStatus { - updated, - notUpdated, -} - -class UserBottomSheet extends StatefulWidget { - final User user; - final Function? onMention; - final BuildContext outerContext; - - const UserBottomSheet({ - Key? key, - required this.user, - required this.outerContext, - this.onMention, - }) : super(key: key); - - @override - UserBottomSheetController createState() => UserBottomSheetController(); -} - -class UserBottomSheetController extends State { - void participantAction(UserBottomSheetAction action) async { - // ignore: prefer_function_declarations_over_variables - final Function askConfirmation = () async => (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, - ) == - OkCancelResult.ok); - switch (action) { - case UserBottomSheetAction.report: - final event = widget.user; - final score = await showConfirmationDialog( - context: context, - title: L10n.of(context)!.reportUser, - message: L10n.of(context)!.howOffensiveIsThisContent, - cancelLabel: L10n.of(context)!.cancel, - okLabel: L10n.of(context)!.ok, - actions: [ - AlertDialogAction( - key: -100, - label: L10n.of(context)!.extremeOffensive, - ), - AlertDialogAction( - key: -50, - label: L10n.of(context)!.offensive, - ), - AlertDialogAction( - key: 0, - label: L10n.of(context)!.inoffensive, - ), - ], - ); - if (score == null) return; - final reason = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.whyDoYouWantToReportThis, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [DialogTextField(hintText: L10n.of(context)!.reason)], - ); - if (reason == null || reason.single.isEmpty) return; - final result = await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context).client.reportContent( - event.roomId!, - event.eventId, - reason: reason.single, - score: score, - ), - ); - if (result.error != null) return; - TwakeSnackBar.show(context, L10n.of(context)!.contentHasBeenReported); - break; - case UserBottomSheetAction.mention: - Navigator.of(context, rootNavigator: false) - .pop(ChatMembersStatus.notUpdated); - widget.onMention!(); - break; - case UserBottomSheetAction.ban: - if (await askConfirmation()) { - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => widget.user.ban(), - ); - Navigator.of(context, rootNavigator: false) - .pop(ChatMembersStatus.updated); - } - break; - case UserBottomSheetAction.unban: - if (await askConfirmation()) { - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => widget.user.unban(), - ); - Navigator.of(context, rootNavigator: false) - .pop(ChatMembersStatus.updated); - } - break; - case UserBottomSheetAction.kick: - if (await askConfirmation()) { - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => widget.user.kick(), - ); - Navigator.of(context, rootNavigator: false) - .pop(ChatMembersStatus.updated); - } - break; - case UserBottomSheetAction.permission: - final newPermission = await showPermissionChooser( - context, - currentLevel: widget.user.powerLevel, - ); - if (newPermission != null) { - if (newPermission == 100 && await askConfirmation() == false) break; - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => widget.user.setPower(newPermission), - ); - context.pop(ChatMembersStatus.updated); - } - break; - case UserBottomSheetAction.message: - final roomIdResult = - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => widget.user.startDirectChat(), - ); - if (roomIdResult.error != null) return; - context.go('/rooms/${roomIdResult.result!}'); - Navigator.of(context, rootNavigator: false) - .pop(ChatMembersStatus.notUpdated); - break; - case UserBottomSheetAction.ignore: - if (await askConfirmation()) { - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context).client.ignoreUser(widget.user.id), - ); - } - } - } - - @override - Widget build(BuildContext context) => UserBottomSheetView(this); -} diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart deleted file mode 100644 index dd82f775a6..0000000000 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:fluffychat/widgets/avatar/avatar_style.dart'; -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; -import '../../utils/matrix_sdk_extensions/presence_extension.dart'; -import '../../widgets/matrix.dart'; -import 'user_bottom_sheet.dart'; - -class UserBottomSheetView extends StatelessWidget { - final UserBottomSheetController controller; - - const UserBottomSheetView(this.controller, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final user = controller.widget.user; - final client = Matrix.of(context).client; - final presence = client.presences[user.id]; - return SafeArea( - child: Scaffold( - appBar: AppBar( - leading: CloseButton( - onPressed: Navigator.of(context, rootNavigator: false).pop, - ), - title: Text( - user.calcDisplayname(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - actions: [ - if (user.id != client.userID) - Padding( - padding: const EdgeInsets.all(8.0), - child: OutlinedButton.icon( - onPressed: () => controller - .participantAction(UserBottomSheetAction.message), - icon: const Icon(Icons.chat_outlined), - label: Text(L10n.of(context)!.newChat), - ), - ), - ], - ), - body: ListView( - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Avatar( - mxContent: user.avatarUrl, - name: user.calcDisplayname(), - size: AvatarStyle.defaultSize * 2, - fontSize: 24, - ), - ), - Expanded( - child: ListTile( - contentPadding: const EdgeInsets.only(right: 16.0), - title: Text(user.id), - subtitle: presence == null - ? null - : Text(presence.getLocalizedLastActiveAgo(context)), - trailing: IconButton( - icon: Icon(Icons.adaptive.share), - onPressed: () => FluffyShare.share( - user.id, - context, - ), - ), - ), - ), - ], - ), - if (controller.widget.onMention != null) - ListTile( - trailing: const Icon(Icons.alternate_email_outlined), - title: Text(L10n.of(context)!.mention), - onTap: () => - controller.participantAction(UserBottomSheetAction.mention), - ), - if (user.canChangePowerLevel) - ListTile( - title: Text(L10n.of(context)!.setPermissionsLevel), - trailing: const Icon(Icons.edit_attributes_outlined), - onTap: () => controller - .participantAction(UserBottomSheetAction.permission), - ), - if (user.canKick) - ListTile( - title: Text(L10n.of(context)!.kickFromChat), - trailing: const Icon(Icons.exit_to_app_outlined), - onTap: () => - controller.participantAction(UserBottomSheetAction.kick), - ), - if (user.canBan && user.membership != Membership.ban) - ListTile( - title: Text(L10n.of(context)!.banFromChat), - trailing: const Icon(Icons.warning_sharp), - onTap: () => - controller.participantAction(UserBottomSheetAction.ban), - ) - else if (user.canBan && user.membership == Membership.ban) - ListTile( - title: Text(L10n.of(context)!.unbanFromChat), - trailing: const Icon(Icons.warning_outlined), - onTap: () => - controller.participantAction(UserBottomSheetAction.unban), - ), - if (user.id != client.userID && - !client.ignoredUsers.contains(user.id)) - ListTile( - textColor: Theme.of(context).colorScheme.onErrorContainer, - iconColor: Theme.of(context).colorScheme.onErrorContainer, - title: Text(L10n.of(context)!.ignore), - trailing: const Icon(Icons.block), - onTap: () => - controller.participantAction(UserBottomSheetAction.ignore), - ), - if (user.id != client.userID) - ListTile( - textColor: Theme.of(context).colorScheme.error, - iconColor: Theme.of(context).colorScheme.error, - title: Text(L10n.of(context)!.reportUser), - trailing: const Icon(Icons.shield_outlined), - onTap: () => - controller.participantAction(UserBottomSheetAction.report), - ), - ], - ), - ), - ); - } -} diff --git a/lib/utils/matrix_sdk_extensions/presence_extension.dart b/lib/utils/matrix_sdk_extensions/presence_extension.dart index bb6e1069ed..d2cbf9ce4f 100644 --- a/lib/utils/matrix_sdk_extensions/presence_extension.dart +++ b/lib/utils/matrix_sdk_extensions/presence_extension.dart @@ -1,11 +1,17 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/pages/chat/chat_app_bar_title_style.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:matrix/matrix.dart'; import '../date_time_extension.dart'; extension PresenceExtension on CachedPresence { + static ResponsiveUtils responsive = getIt.get(); + String getLocalizedLastActiveAgo(BuildContext context) { final lastActiveTimestamp = this.lastActiveTimestamp; if (lastActiveTimestamp != null) { @@ -37,4 +43,30 @@ extension PresenceExtension on CachedPresence { return Colors.red; } } + + TextStyle? getPresenceTextStyle(BuildContext context) => currentlyActive ?? false + ? _onlineStatusTextStyle(context) + : _offlineStatusTextStyle(context); + + TextStyle? _offlineStatusTextStyle(BuildContext context) => + responsive.isMobile(context) + ? Theme.of(context).textTheme.labelMedium?.copyWith( + color: LinagoraRefColors.material().tertiary[30], + letterSpacing: ChatAppBarTitleStyle.letterSpacingStatusContent, + ) + : Theme.of(context).textTheme.bodySmall?.copyWith( + color: LinagoraRefColors.material().neutral[50], + letterSpacing: ChatAppBarTitleStyle.letterSpacingRoomName, + ); + + TextStyle? _onlineStatusTextStyle(BuildContext context) => + responsive.isMobile(context) + ? Theme.of(context).textTheme.labelMedium?.copyWith( + color: LinagoraRefColors.material().secondary, + letterSpacing: ChatAppBarTitleStyle.letterSpacingStatusContent, + ) + : Theme.of(context).textTheme.bodySmall?.copyWith( + color: LinagoraRefColors.material().secondary, + letterSpacing: ChatAppBarTitleStyle.letterSpacingRoomName, + ); } From d9151b18d397fe08c519cc6c677b99a0359dbbc2 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Sat, 16 Mar 2024 12:14:36 +0000 Subject: [PATCH 070/183] TW-1456: participant list item updated with profile info (cherry picked from commit e3b54bf710bb544410944b68c65772688d9e9640) --- assets/l10n/intl_fr.arb | 2 +- .../chat_details/participant_list_item.dart | 137 ++++++++++++++++-- .../chat_profile_info_shared.dart | 3 +- .../presence_extension.dart | 7 +- 4 files changed, 130 insertions(+), 19 deletions(-) diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index e24e199c37..1b83f09bb9 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -3077,7 +3077,7 @@ "unselect": "Désélectionner", "@unselect": {}, "searchContacts": "Rechercher des contacts", - "@searchContacts": {} + "@searchContacts": {}, "phone": "Téléphone", "viewProfile": "Voir le profil", "profileInfo": "Informations du profil" diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index 05e95a4551..ca21192973 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -1,12 +1,14 @@ -import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; - class ParticipantListItem extends StatelessWidget { final User user; @@ -36,17 +38,124 @@ class ParticipantListItem extends StatelessWidget { opacity: user.membership == Membership.join ? 1 : 0.5, child: ListTile( onTap: () async { - final result = await showAdaptiveBottomSheet( - context: context, - builder: (c) => UserBottomSheet( - user: user, - outerContext: context, - ), - ); - if (result is ChatMembersStatus) { - if (result == ChatMembersStatus.updated) { - onUpdatedMembers?.call(); - } + if (PlatformInfos.isMobile) { + await showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + builder: (c) { + return Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 4, + width: 32, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + color: LinagoraSysColors.material().outline, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextButton.icon( + onPressed: () { + context.go( + '/rooms/profileinfo/${user.room.id}/${user.id}', + ); + }, + icon: Icon( + Icons.person_search, + color: LinagoraSysColors.material().onSurface, + ), + label: L10n.of(context)?.viewProfile != null + ? Row( + children: [ + Text( + L10n.of(context)!.viewProfile, + style: TextStyle( + color: LinagoraSysColors.material() + .onSurface, + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + ), + ], + ), + // TODO: share button + /*TextButton.icon( + onPressed: () { + // Action pour le premier bouton + }, + icon: Icon(Icons.share), + label: Text('Partager'), + ),*/ + ], + ), + ); + }, + ); + } else { + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + contentPadding: const EdgeInsets.all(0), + backgroundColor: LinagoraRefColors.material().primary[100], + surfaceTintColor: Colors.transparent, + content: SizedBox( + width: 448, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + children: [ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: IconButton( + onPressed: () => dialogContext.pop(), + icon: const Icon(Icons.close), + ), + ), + ), + ProfileInfoBody( + user: user, + parentContext: context, + onNewChatOpen: () { + dialogContext.pop(); + }, + ), + ], + ), + ], + ), + ), + ), + ); } }, title: Row( diff --git a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart index f54fe75e6b..6cc4fa04cc 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart @@ -23,7 +23,8 @@ class ChatProfileInfoShared extends StatefulWidget { }); @override - State createState() => ChatProfileInfoSharedController(); + State createState() => + ChatProfileInfoSharedController(); } class ChatProfileInfoSharedController extends State diff --git a/lib/utils/matrix_sdk_extensions/presence_extension.dart b/lib/utils/matrix_sdk_extensions/presence_extension.dart index d2cbf9ce4f..fda73527c5 100644 --- a/lib/utils/matrix_sdk_extensions/presence_extension.dart +++ b/lib/utils/matrix_sdk_extensions/presence_extension.dart @@ -44,9 +44,10 @@ extension PresenceExtension on CachedPresence { } } - TextStyle? getPresenceTextStyle(BuildContext context) => currentlyActive ?? false - ? _onlineStatusTextStyle(context) - : _offlineStatusTextStyle(context); + TextStyle? getPresenceTextStyle(BuildContext context) => + currentlyActive ?? false + ? _onlineStatusTextStyle(context) + : _offlineStatusTextStyle(context); TextStyle? _offlineStatusTextStyle(BuildContext context) => responsive.isMobile(context) From 5dfca218c508eedaf7c6b98c06e4e19f0fa5a523 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Mon, 18 Mar 2024 09:58:01 +0000 Subject: [PATCH 071/183] TW-1456: participant list item style created (cherry picked from commit 3bb724f2b4f599a9faf669a6c62fc93d65ce4b40) --- assets/l10n/intl_en.arb | 1 + assets/l10n/intl_fr.arb | 1 + lib/config/go_routes/go_router.dart | 4 +- .../chat_details_members_page.dart | 2 +- .../chat_details/participant_list_item.dart | 212 ----------------- .../participant_list_item.dart | 218 ++++++++++++++++++ .../participant_list_item_style.dart | 35 +++ .../copiable_profile_row_style.dart | 1 - .../profile_info_body_view.dart | 2 +- .../profile_info_body_view_style.dart | 3 +- ...ofile_info.dart => profile_info_page.dart} | 8 +- lib/pages/profile_info/profile_info_view.dart | 4 +- .../profile_info/profile_info_view_style.dart | 1 - pubspec.lock | 2 +- pubspec.yaml | 2 +- 15 files changed, 269 insertions(+), 227 deletions(-) delete mode 100644 lib/pages/chat_details/participant_list_item.dart create mode 100644 lib/pages/chat_details/participant_list_item/participant_list_item.dart create mode 100644 lib/pages/chat_details/participant_list_item/participant_list_item_style.dart rename lib/pages/profile_info/{profile_info.dart => profile_info_page.dart} (73%) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index d41296e95d..b528cb9e42 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1764,6 +1764,7 @@ "type": "text", "placeholders": {} }, + "sendMessage": "Send message", "sendOriginal": "Send original", "@sendOriginal": { "type": "text", diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index 1b83f09bb9..017130b09f 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -1594,6 +1594,7 @@ "type": "text", "placeholders": {} }, + "sendMessage": "Envoyer un message", "sendAsText": "Envoyer un texte", "@sendAsText": { "type": "text" diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index f360dafcc1..96f8f061e6 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -15,7 +15,7 @@ import 'package:fluffychat/pages/error_page/error_page.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pages/login/on_auth_redirect.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; -import 'package:fluffychat/pages/profile_info/profile_info.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_page.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settings_app_language.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; @@ -221,7 +221,7 @@ abstract class AppRoutes { return defaultPageBuilder( context, - ProfileInfo( + ProfileInfoPage( roomId: roomId, userId: userId, ), diff --git a/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart b/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart index fc07423a9a..f64e6ce794 100644 --- a/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pages/chat_details/participant_list_item.dart'; +import 'package:fluffychat/pages/chat_details/participant_list_item/participant_list_item.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart deleted file mode 100644 index ca21192973..0000000000 --- a/lib/pages/chat_details/participant_list_item.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; -import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; -import 'package:matrix/matrix.dart'; - -class ParticipantListItem extends StatelessWidget { - final User user; - - final VoidCallback? onUpdatedMembers; - - const ParticipantListItem( - this.user, { - Key? key, - this.onUpdatedMembers, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final membershipBatch = { - Membership.join: '', - Membership.ban: L10n.of(context)!.banned, - Membership.invite: L10n.of(context)!.invited, - Membership.leave: L10n.of(context)!.leftTheChat, - }; - final permissionBatch = user.powerLevel == 100 - ? L10n.of(context)!.admin - : user.powerLevel >= 50 - ? L10n.of(context)!.moderator - : ''; - - return Opacity( - opacity: user.membership == Membership.join ? 1 : 0.5, - child: ListTile( - onTap: () async { - if (PlatformInfos.isMobile) { - await showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - builder: (c) { - return Container( - padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - height: 4, - width: 32, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: LinagoraSysColors.material().outline, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: TextButton.icon( - onPressed: () { - context.go( - '/rooms/profileinfo/${user.room.id}/${user.id}', - ); - }, - icon: Icon( - Icons.person_search, - color: LinagoraSysColors.material().onSurface, - ), - label: L10n.of(context)?.viewProfile != null - ? Row( - children: [ - Text( - L10n.of(context)!.viewProfile, - style: TextStyle( - color: LinagoraSysColors.material() - .onSurface, - ), - ), - ], - ) - : const SizedBox.shrink(), - ), - ), - ], - ), - // TODO: share button - /*TextButton.icon( - onPressed: () { - // Action pour le premier bouton - }, - icon: Icon(Icons.share), - label: Text('Partager'), - ),*/ - ], - ), - ); - }, - ); - } else { - await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - contentPadding: const EdgeInsets.all(0), - backgroundColor: LinagoraRefColors.material().primary[100], - surfaceTintColor: Colors.transparent, - content: SizedBox( - width: 448, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - children: [ - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: IconButton( - onPressed: () => dialogContext.pop(), - icon: const Icon(Icons.close), - ), - ), - ), - ProfileInfoBody( - user: user, - parentContext: context, - onNewChatOpen: () { - dialogContext.pop(); - }, - ), - ], - ), - ], - ), - ), - ), - ); - } - }, - title: Row( - children: [ - Flexible( - child: Text( - user.calcDisplayname(), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - if (permissionBatch.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.primary, - ), - ), - child: Text( - permissionBatch, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - membershipBatch[user.membership]!.isEmpty - ? Container() - : Container( - padding: const EdgeInsets.all(4), - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).secondaryHeaderColor, - borderRadius: BorderRadius.circular(8), - ), - child: - Center(child: Text(membershipBatch[user.membership]!)), - ), - ], - ), - subtitle: Text(user.id), - leading: - Avatar(mxContent: user.avatarUrl, name: user.calcDisplayname()), - ), - ); - } -} diff --git a/lib/pages/chat_details/participant_list_item/participant_list_item.dart b/lib/pages/chat_details/participant_list_item/participant_list_item.dart new file mode 100644 index 0000000000..a3c745014b --- /dev/null +++ b/lib/pages/chat_details/participant_list_item/participant_list_item.dart @@ -0,0 +1,218 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/pages/chat_details/participant_list_item/participant_list_item_style.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:matrix/matrix.dart'; + +class ParticipantListItem extends StatelessWidget { + final User member; + + final VoidCallback? onUpdatedMembers; + + const ParticipantListItem( + this.member, { + Key? key, + this.onUpdatedMembers, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final membershipBatch = { + Membership.join: '', + Membership.ban: L10n.of(context)!.banned, + Membership.invite: L10n.of(context)!.invited, + Membership.leave: L10n.of(context)!.leftTheChat, + }; + final permissionBatch = member.powerLevel == 100 + ? L10n.of(context)!.admin + : member.powerLevel >= 50 + ? L10n.of(context)!.moderator + : ''; + + return Opacity( + opacity: member.membership == Membership.join ? 1 : 0.5, + child: ListTile( + onTap: () async => await _onItemTap(context), + title: Row( + children: [ + Flexible( + child: Text( + member.calcDisplayname(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (permissionBatch.isNotEmpty) + Container( + padding: ParticipantListItemStyle.permissionBatchTextPadding, + margin: ParticipantListItemStyle.permissionBatchMargin, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: ParticipantListItemStyle.permissionBatchRadius, + border: Border.all( + color: Theme.of(context).colorScheme.primary, + ), + ), + child: Text( + permissionBatch, + style: TextStyle( + fontSize: + ParticipantListItemStyle.permissionBatchTextFontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + membershipBatch[member.membership]!.isEmpty + ? Container() + : Container( + padding: ParticipantListItemStyle.membershipBatchPadding, + margin: ParticipantListItemStyle.membershipBatchMargin, + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: + ParticipantListItemStyle.membershipBatchRadius, + ), + child: Center( + child: Text(membershipBatch[member.membership]!), + ), + ), + ], + ), + subtitle: Text(member.id), + leading: + Avatar(mxContent: member.avatarUrl, name: member.calcDisplayname()), + ), + ); + } + + Future _onItemTap(BuildContext context) async { + final responsive = getIt.get(); + + if (PlatformInfos.isMobile || responsive.isMobile(context)) { + await _openProfileMenu(context); + } else { + await _openProfileDialog(context); + } + } + + Future _openProfileMenu(BuildContext context) => showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: ParticipantListItemStyle.bottomSheetTopRadius, + topRight: ParticipantListItemStyle.bottomSheetTopRadius, + ), + ), + builder: (c) { + return Container( + padding: ParticipantListItemStyle.bottomSheetContentPadding, + decoration: BoxDecoration( + color: LinagoraRefColors.material().primary[100], + borderRadius: const BorderRadius.only( + topLeft: ParticipantListItemStyle.bottomSheetTopRadius, + topRight: ParticipantListItemStyle.bottomSheetTopRadius, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: ParticipantListItemStyle.dragHandleHeight, + width: ParticipantListItemStyle.dragHandleWidth, + decoration: BoxDecoration( + borderRadius: + ParticipantListItemStyle.dragHandleBorderRadius, + color: LinagoraSysColors.material().outline, + ), + ), + ], + ), + const SizedBox( + height: ParticipantListItemStyle.spacerHeight, + ), + Row( + children: [ + Expanded( + child: TextButton.icon( + onPressed: () { + context.go( + '/rooms/profileinfo/${member.room.id}/${member.id}', + ); + }, + icon: Icon( + Icons.person_search, + color: LinagoraSysColors.material().onSurface, + ), + label: L10n.of(context)?.viewProfile != null + ? Row( + children: [ + Text( + L10n.of(context)!.viewProfile, + style: TextStyle( + color: LinagoraSysColors.material() + .onSurface, + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + ), + ], + ), + ], + ), + ); + }, + ); + + Future _openProfileDialog(BuildContext context) => showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + contentPadding: const EdgeInsets.all(0), + backgroundColor: LinagoraRefColors.material().primary[100], + surfaceTintColor: Colors.transparent, + content: SizedBox( + width: ParticipantListItemStyle.fixedDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + children: [ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: ParticipantListItemStyle.closeButtonPadding, + child: IconButton( + onPressed: () => dialogContext.pop(), + icon: const Icon(Icons.close), + ), + ), + ), + ProfileInfoBody( + user: member, + parentContext: context, + onNewChatOpen: () { + dialogContext.pop(); + }, + ), + ], + ), + ], + ), + ), + ), + ); +} diff --git a/lib/pages/chat_details/participant_list_item/participant_list_item_style.dart b/lib/pages/chat_details/participant_list_item/participant_list_item_style.dart new file mode 100644 index 0000000000..4042aa3edc --- /dev/null +++ b/lib/pages/chat_details/participant_list_item/participant_list_item_style.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; + +class ParticipantListItemStyle { + // Bottom Sheet + static const Radius bottomSheetTopRadius = Radius.circular(16); + static const Radius bottomSheetContentTopRadius = Radius.circular(16); + static const EdgeInsets bottomSheetContentPadding = EdgeInsets.all(16); + static const double spacerHeight = 16; + + // Bottom Sheet drag handle + static const double dragHandleHeight = 4; + static const double dragHandleWidth = 32; + static BorderRadiusGeometry dragHandleBorderRadius = + BorderRadius.circular(100); + + // Dialog + static const double fixedDialogWidth = 448; + static const EdgeInsets closeButtonPadding = EdgeInsets.all(16); + + // Permission batch + static const EdgeInsets permissionBatchTextPadding = EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ); + static const EdgeInsets permissionBatchMargin = + EdgeInsets.symmetric(horizontal: 8); + static BorderRadiusGeometry permissionBatchRadius = BorderRadius.circular(8); + static const double permissionBatchTextFontSize = 14; + + // Membership batch + static const EdgeInsets membershipBatchPadding = EdgeInsets.all(4); + static const EdgeInsets membershipBatchMargin = + EdgeInsets.symmetric(horizontal: 8); + static BorderRadiusGeometry membershipBatchRadius = BorderRadius.circular(8); +} diff --git a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart index be923e0827..45ec4a67ac 100644 --- a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart +++ b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row_style.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; class CopiableProfileRowStyle { - static const double spacerBetweenLeadingIconAndContent = 8; static const double borderOpacity = 0.16; static const double textColumnBottomPadding = 16; diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart index daa6e33ef3..7e42263988 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart @@ -69,7 +69,7 @@ class ProfileInfoBodyView extends StatelessWidget { children: [ Expanded( child: Text( - L10n.of(context)!.newChat, + L10n.of(context)!.sendMessage, overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart index bd47854812..0b7174ba58 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart @@ -5,7 +5,8 @@ import 'package:flutter/material.dart'; class ProfileInfoBodyViewStyle { static ResponsiveUtils responsive = getIt.get(); - static const EdgeInsetsGeometry profileInformationsTopPadding = EdgeInsets.only( + static const EdgeInsetsGeometry profileInformationsTopPadding = + EdgeInsets.only( top: 16.0, left: 16.0, right: 16.0, diff --git a/lib/pages/profile_info/profile_info.dart b/lib/pages/profile_info/profile_info_page.dart similarity index 73% rename from lib/pages/profile_info/profile_info.dart rename to lib/pages/profile_info/profile_info_page.dart index 2bd2a79d38..8329efce06 100644 --- a/lib/pages/profile_info/profile_info.dart +++ b/lib/pages/profile_info/profile_info_page.dart @@ -3,8 +3,8 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -class ProfileInfo extends StatefulWidget { - const ProfileInfo({ +class ProfileInfoPage extends StatefulWidget { + const ProfileInfoPage({ super.key, required this.roomId, required this.userId, @@ -14,10 +14,10 @@ class ProfileInfo extends StatefulWidget { final String userId; @override - State createState() => ProfileInfoState(); + State createState() => ProfileInfoPageState(); } -class ProfileInfoState extends State { +class ProfileInfoPageState extends State { Room? get room => Matrix.of(context).client.getRoomById(widget.roomId); User? get user => room?.unsafeGetUserFromMemoryOrFallback(widget.userId); diff --git a/lib/pages/profile_info/profile_info_view.dart b/lib/pages/profile_info/profile_info_view.dart index d642086753..47b1943666 100644 --- a/lib/pages/profile_info/profile_info_view.dart +++ b/lib/pages/profile_info/profile_info_view.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pages/profile_info/profile_info.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_page.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; import 'package:fluffychat/pages/profile_info/profile_info_view_style.dart'; import 'package:flutter/material.dart'; @@ -13,7 +13,7 @@ class ProfileInfoView extends StatelessWidget { super.key, }); - final ProfileInfoState controller; + final ProfileInfoPageState controller; @override Widget build(BuildContext context) { diff --git a/lib/pages/profile_info/profile_info_view_style.dart b/lib/pages/profile_info/profile_info_view_style.dart index bd77134b9b..ecc280f4e2 100644 --- a/lib/pages/profile_info/profile_info_view_style.dart +++ b/lib/pages/profile_info/profile_info_view_style.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; class ProfileInfoViewStyle { - static const EdgeInsetsGeometry backIconPadding = EdgeInsets.symmetric(vertical: 8, horizontal: 4); static const EdgeInsets navigationAppBarPadding = diff --git a/pubspec.lock b/pubspec.lock index baa850bd36..f767bf28a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -3153,4 +3153,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + flutter: ">=3.16.0" \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 85517d6a12..5733999cf4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -270,4 +270,4 @@ dependency_overrides: cider: link_template: - tag: https://github.com/linagora/twake-on-matrix/releases/tag/%tag% # initial release link template + tag: https://github.com/linagora/twake-on-matrix/releases/tag/%tag% # initial release link template \ No newline at end of file From 4d8c5ee6055430aa7be68e4607d9ba8005db49f9 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Tue, 19 Mar 2024 20:54:34 +0100 Subject: [PATCH 072/183] TW-1456: optionnal duration param added to snackbar (cherry picked from commit fecf45049b445340dae915cc26ddf6e42bf14f61) --- lib/config/go_routes/go_router.dart | 20 --- .../participant_list_item.dart | 43 +++++-- .../copiable_profile_row.dart | 3 + .../profile_info_body/profile_info_body.dart | 74 +++++++++-- .../profile_info_body_view.dart | 117 +----------------- .../profile_info_contact_rows.dart | 64 ++++++++++ .../profile_info_header.dart | 43 +++++++ lib/pages/profile_info/profile_info_page.dart | 2 + lib/pages/profile_info/profile_info_view.dart | 4 +- lib/utils/twake_snackbar.dart | 11 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- 11 files changed, 232 insertions(+), 151 deletions(-) create mode 100644 lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart create mode 100644 lib/pages/profile_info/profile_info_body/profile_info_header.dart diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 96f8f061e6..4d77f4d177 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -15,7 +15,6 @@ import 'package:fluffychat/pages/error_page/error_page.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pages/login/on_auth_redirect.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; -import 'package:fluffychat/pages/profile_info/profile_info_page.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settings_app_language.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; @@ -209,25 +208,6 @@ abstract class AppRoutes { ), ], ), - GoRoute( - path: 'profileinfo/:roomid/:userid', - pageBuilder: (context, state) { - final roomId = state.pathParameters['roomid']; - final userId = state.pathParameters['userid']; - - if (roomId == null || userId == null) { - return defaultPageBuilder(context, const ErrorPage()); - } - - return defaultPageBuilder( - context, - ProfileInfoPage( - roomId: roomId, - userId: userId, - ), - ); - }, - ), GoRoute( path: 'archive', pageBuilder: (context, state) => defaultPageBuilder( diff --git a/lib/pages/chat_details/participant_list_item/participant_list_item.dart b/lib/pages/chat_details/participant_list_item/participant_list_item.dart index a3c745014b..2408b6c8ff 100644 --- a/lib/pages/chat_details/participant_list_item/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item/participant_list_item.dart @@ -1,9 +1,11 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/chat_details/participant_list_item/participant_list_item_style.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_page.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -96,13 +98,42 @@ class ParticipantListItem extends StatelessWidget { Future _onItemTap(BuildContext context) async { final responsive = getIt.get(); - if (PlatformInfos.isMobile || responsive.isMobile(context)) { + if (responsive.isMobile(context)) { await _openProfileMenu(context); } else { await _openProfileDialog(context); } } + Future _openDialogInvite(BuildContext context) async { + if (PlatformInfos.isMobile) { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (ctx) => ProfileInfoPage( + roomId: member.room.id, + userId: member.id, + ), + ), + ); + return; + } + await showDialog( + context: context, + barrierDismissible: false, + useSafeArea: false, + useRootNavigator: !PlatformInfos.isMobile, + builder: (dialogContext) { + return ProfileInfoPage( + roomId: member.room.id, + userId: member.id, + onNewChatOpen: () { + dialogContext.pop(); + }, + ); + }, + ); + } + Future _openProfileMenu(BuildContext context) => showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( @@ -111,7 +142,7 @@ class ParticipantListItem extends StatelessWidget { topRight: ParticipantListItemStyle.bottomSheetTopRadius, ), ), - builder: (c) { + builder: (bottomSheetContext) { return Container( padding: ParticipantListItemStyle.bottomSheetContentPadding, decoration: BoxDecoration( @@ -146,10 +177,9 @@ class ParticipantListItem extends StatelessWidget { children: [ Expanded( child: TextButton.icon( - onPressed: () { - context.go( - '/rooms/profileinfo/${member.room.id}/${member.id}', - ); + onPressed: () async { + Navigator.of(bottomSheetContext).pop(); + await _openDialogInvite(context); }, icon: Icon( Icons.person_search, @@ -203,7 +233,6 @@ class ParticipantListItem extends StatelessWidget { ), ProfileInfoBody( user: member, - parentContext: context, onNewChatOpen: () { dialogContext.pop(); }, diff --git a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart index 025a11821f..01394743a1 100644 --- a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart +++ b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart @@ -9,6 +9,8 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class CopiableProfileRow extends StatelessWidget { + static const snackBarDuration = Duration(milliseconds: 500); + final String caption; final String copiableText; final Widget leadingIcon; @@ -31,6 +33,7 @@ class CopiableProfileRow extends StatelessWidget { onTap: () { TwakeClipboard.instance.copyText(copiableText); TwakeSnackBar.show( + duration: snackBarDuration, context, L10n.of(context)!.copiedToClipboard, ); diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body.dart b/lib/pages/profile_info/profile_info_body/profile_info_body.dart index fd9cfa864f..518892ceff 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body.dart @@ -7,6 +7,10 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; import 'package:fluffychat/domain/usecase/contacts/lookup_match_contact_interactor.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view.dart'; +import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -15,15 +19,12 @@ import 'package:matrix/matrix.dart'; class ProfileInfoBody extends StatefulWidget { const ProfileInfoBody({ required this.user, - required this.parentContext, this.onNewChatOpen, Key? key, }) : super(key: key); final User? user; - final BuildContext parentContext; - final void Function()? onNewChatOpen; @override @@ -43,8 +44,6 @@ class ProfileInfoBodyController extends State { User? get user => widget.user; - BuildContext get parentContext => widget.parentContext; - bool get isOwnProfile => user?.id == user?.room.client.userID; void lookupMatchContactAction() { @@ -58,9 +57,68 @@ class ProfileInfoBodyController extends State { ); } - void openNewChat(String roomId) { - parentContext.go('/rooms/$roomId'); - if (widget.onNewChatOpen != null) widget.onNewChatOpen!(); + void openNewChat() { + if (user == null) return; + final roomId = Matrix.of(context).client.getDirectChatFromUserId(user!.id); + + if (roomId == null) { + if (!PlatformInfos.isMobile && widget.onNewChatOpen != null) { + widget.onNewChatOpen!(); + } + + _goToDraftChat( + context: context, + path: "rooms", + contactPresentationSearch: user!.toContactPresentationSearch(), + ); + } else { + if (PlatformInfos.isMobile) { + Navigator.of(context) + .popUntil((route) => route.settings.name == "/rooms/room"); + } else { + if (widget.onNewChatOpen != null) widget.onNewChatOpen!(); + } + + context.go('/rooms/$roomId'); + } + } + + void _goToDraftChat({ + required BuildContext context, + required String path, + required ContactPresentationSearch contactPresentationSearch, + }) { + if (contactPresentationSearch.matrixId != + Matrix.of(context).client.userID) { + Router.neglect( + context, + () => PlatformInfos.isMobile + ? context.push( + '/$path/draftChat', + extra: { + PresentationContactConstant.receiverId: + contactPresentationSearch.matrixId ?? '', + PresentationContactConstant.email: + contactPresentationSearch.email ?? '', + PresentationContactConstant.displayName: + contactPresentationSearch.displayName ?? '', + PresentationContactConstant.status: '', + }, + ) + : context.go( + '/$path/draftChat', + extra: { + PresentationContactConstant.receiverId: + contactPresentationSearch.matrixId ?? '', + PresentationContactConstant.email: + contactPresentationSearch.email ?? '', + PresentationContactConstant.displayName: + contactPresentationSearch.displayName ?? '', + PresentationContactConstant.status: '', + }, + ), + ); + } } @override diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart index 7e42263988..997283a56b 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart @@ -1,19 +1,11 @@ -import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; -import 'package:fluffychat/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart'; -import 'package:fluffychat/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view_style.dart'; -import 'package:fluffychat/resource/image_paths.dart'; -import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/presence_extension.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_contact_rows.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_header.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; -import 'package:matrix/matrix.dart'; class ProfileInfoBodyView extends StatelessWidget { const ProfileInfoBodyView({ @@ -35,8 +27,8 @@ class ProfileInfoBodyView extends StatelessWidget { padding: ProfileInfoBodyViewStyle.profileInformationsTopPadding, child: Column( children: [ - _ProfileInfoHeader(controller.user!), - _ProfileInfoContactRows( + ProfileInfoHeader(controller.user!), + ProfileInfoContactRows( user: controller.user!, lookupContactNotifier: controller.lookupContactNotifier, ), @@ -54,15 +46,7 @@ class ProfileInfoBodyView extends StatelessWidget { children: [ Expanded( child: TextButton.icon( - onPressed: () async { - final roomIdResult = - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => controller.user!.startDirectChat(), - ); - if (roomIdResult.error != null) return; - - controller.openNewChat(roomIdResult.result!); - }, + onPressed: () => controller.openNewChat(), icon: const Icon(Icons.chat_outlined), label: L10n.of(context)?.newChat != null ? Row( @@ -87,94 +71,3 @@ class ProfileInfoBodyView extends StatelessWidget { ); } } - -class _ProfileInfoHeader extends StatelessWidget { - const _ProfileInfoHeader(this.user, {Key? key}) : super(key: key); - final User user; - - @override - Widget build(BuildContext context) { - final client = Matrix.of(context).client; - final presence = client.presences[user.id]; - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: ProfileInfoBodyViewStyle.avatarPadding, - child: Avatar( - mxContent: user.avatarUrl, - name: user.calcDisplayname(), - ), - ), - Text( - user.calcDisplayname(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (presence != null) ...[ - const SizedBox(height: 8), - Text( - presence.getLocalizedStatusMessage(context), - style: presence.getPresenceTextStyle(context), - ), - ], - ], - ); - } -} - -class _ProfileInfoContactRows extends StatelessWidget { - const _ProfileInfoContactRows({ - required this.user, - required this.lookupContactNotifier, - Key? key, - }) : super(key: key); - final User user; - final ValueListenable lookupContactNotifier; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const SizedBox(height: 16), - SvgCopiableProfileRow( - leadingIconPath: ImagePaths.icMatrixid, - caption: L10n.of(context)!.matrixId, - copiableText: user.id, - ), - ValueListenableBuilder( - valueListenable: lookupContactNotifier, - // valueListenable: controller.lookupContactNotifier, - builder: (context, contact, child) { - return contact.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is LookupMatchContactSuccess) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (success.contact.email != null) - IconCopiableProfileRow( - icon: Icons.alternate_email, - caption: L10n.of(context)!.email, - copiableText: success.contact.email!, - ), - if (success.contact.phoneNumber != null) - IconCopiableProfileRow( - icon: Icons.call, - caption: L10n.of(context)!.phone, - copiableText: success.contact.phoneNumber!, - ), - ], - ); - } - return const SizedBox.shrink(); - }, - ); - }, - ), - ], - ); - } -} diff --git a/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart b/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart new file mode 100644 index 0000000000..3d7ea59ccb --- /dev/null +++ b/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart @@ -0,0 +1,64 @@ +import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; +import 'package:fluffychat/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart'; +import 'package:fluffychat/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart'; +import 'package:fluffychat/resource/image_paths.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class ProfileInfoContactRows extends StatelessWidget { + const ProfileInfoContactRows({ + required this.user, + required this.lookupContactNotifier, + Key? key, + }) : super(key: key); + final User user; + final ValueListenable lookupContactNotifier; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 16), + SvgCopiableProfileRow( + leadingIconPath: ImagePaths.icMatrixid, + caption: L10n.of(context)!.matrixId, + copiableText: user.id, + ), + ValueListenableBuilder( + valueListenable: lookupContactNotifier, + // valueListenable: controller.lookupContactNotifier, + builder: (context, contact, child) { + return contact.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is LookupMatchContactSuccess) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (success.contact.email != null) + IconCopiableProfileRow( + icon: Icons.alternate_email, + caption: L10n.of(context)!.email, + copiableText: success.contact.email!, + ), + if (success.contact.phoneNumber != null) + IconCopiableProfileRow( + icon: Icons.call, + caption: L10n.of(context)!.phone, + copiableText: success.contact.phoneNumber!, + ), + ], + ); + } + return const SizedBox.shrink(); + }, + ); + }, + ), + ], + ); + } +} diff --git a/lib/pages/profile_info/profile_info_body/profile_info_header.dart b/lib/pages/profile_info/profile_info_body/profile_info_header.dart new file mode 100644 index 0000000000..a3cba044ad --- /dev/null +++ b/lib/pages/profile_info/profile_info_body/profile_info_header.dart @@ -0,0 +1,43 @@ +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view_style.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/presence_extension.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +class ProfileInfoHeader extends StatelessWidget { + const ProfileInfoHeader(this.user, {Key? key}) : super(key: key); + final User user; + + @override + Widget build(BuildContext context) { + final client = Matrix.of(context).client; + final presence = client.presences[user.id]; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: ProfileInfoBodyViewStyle.avatarPadding, + child: Avatar( + mxContent: user.avatarUrl, + name: user.calcDisplayname(), + ), + ), + Text( + user.calcDisplayname(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (presence != null) ...[ + const SizedBox(height: 8), + Text( + presence.getLocalizedStatusMessage(context), + style: presence.getPresenceTextStyle(context), + ), + ], + ], + ); + } +} diff --git a/lib/pages/profile_info/profile_info_page.dart b/lib/pages/profile_info/profile_info_page.dart index 8329efce06..a803dc242d 100644 --- a/lib/pages/profile_info/profile_info_page.dart +++ b/lib/pages/profile_info/profile_info_page.dart @@ -8,10 +8,12 @@ class ProfileInfoPage extends StatefulWidget { super.key, required this.roomId, required this.userId, + this.onNewChatOpen, }); final String roomId; final String userId; + final void Function()? onNewChatOpen; @override State createState() => ProfileInfoPageState(); diff --git a/lib/pages/profile_info/profile_info_view.dart b/lib/pages/profile_info/profile_info_view.dart index 47b1943666..ef19fcde71 100644 --- a/lib/pages/profile_info/profile_info_view.dart +++ b/lib/pages/profile_info/profile_info_view.dart @@ -46,7 +46,7 @@ class ProfileInfoView extends StatelessWidget { ), Flexible( child: Text( - L10n.of(context)!.profileInfo, + L10n.of(context)?.profileInfo ?? "", style: Theme.of(context).textTheme.titleLarge, overflow: TextOverflow.ellipsis, ), @@ -57,7 +57,7 @@ class ProfileInfoView extends StatelessWidget { ), body: ProfileInfoBody( user: controller.user, - parentContext: context, + onNewChatOpen: controller.widget.onNewChatOpen, ), ); } diff --git a/lib/utils/twake_snackbar.dart b/lib/utils/twake_snackbar.dart index a0e6bbc413..dae362bb2e 100644 --- a/lib/utils/twake_snackbar.dart +++ b/lib/utils/twake_snackbar.dart @@ -2,6 +2,8 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; +const Duration _snackBarDefaultDisplayDuration = Duration(milliseconds: 4000); + class TwakeSnackBarStyle { static ResponsiveUtils responsiveUtils = getIt.get(); @@ -21,11 +23,18 @@ class TwakeSnackBarStyle { } class TwakeSnackBar { - static void show(BuildContext context, String message) { + static void show( + BuildContext context, + String message, { + Duration duration = _snackBarDefaultDisplayDuration, + }) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( SnackBar( width: TwakeSnackBarStyle.widthSnackBar(context), padding: TwakeSnackBarStyle.snackBarPadding, + duration: duration, content: Text( message, style: Theme.of(context).textTheme.bodyMedium?.copyWith( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f2af6f2ea8..16840a23f3 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -83,4 +83,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) -} +} \ No newline at end of file From 4e4faea713ca59c33801c43b36d772247d83b237 Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 12:30:45 +0700 Subject: [PATCH 073/183] TW-1561: store file to download folder after sending successfully (cherry picked from commit 729b6c7c21a93ed2ffd2fc601ae48ecd7a3edc57) --- .../extensions/send_file_extension.dart | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index abccd5530f..aa02188be7 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/presentation/extensions/image_extension.dart'; import 'package:fluffychat/presentation/fake_sending_file_info.dart'; import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; import 'package:flutter/widgets.dart'; import 'package:image/image.dart' as img; import 'package:blurhash_dart/blurhash_dart.dart'; @@ -286,6 +287,13 @@ extension SendFileExtension on Room { inReplyTo: inReplyTo, editEventId: editEventId, ); + if (eventId != null) { + await _copyFileInMemToAppDownloadsFolder( + eventId: eventId, + sendingEventId: txid, + fileName: fileInfo.fileName, + ); + } await Future.wait([ tempEncryptedFile.delete(), tempThumbnailFile.delete(), @@ -294,6 +302,29 @@ extension SendFileExtension on Room { return eventId; } + Future _copyFileInMemToAppDownloadsFolder({ + required String sendingEventId, + required String eventId, + required String fileName, + }) async { + try { + final downloadInAppFolder = + await StorageDirectoryUtils.instance.getDownloadFolderInApp(); + final filePathInAppDownloads = '$downloadInAppFolder/$eventId/$fileName'; + final fileInMem = sendingFilePlaceholders[sendingEventId]?.filePath; + final file = File(filePathInAppDownloads); + if (await file.exists() || fileInMem == null) { + return; + } + await file.create(recursive: true); + await File(fileInMem).copy(filePathInAppDownloads); + Logs().d('File copied in app downloads folder', filePathInAppDownloads); + } catch (e) { + Logs().e('Error while copying file in app downloads folder', e); + rethrow; + } + } + Future sendFakeImagePickerFileEvent( FileInfo fileInfo, { String messageType = MessageTypes.Image, From 1e8ae7ee1d0faa467144b7fa50994843204be06e Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 12:32:43 +0700 Subject: [PATCH 074/183] TW-1561: get filePath from mem, then display to the screen (cherry picked from commit 059b7fc907bf1b58d09447c5a9bb06df3431042a) --- lib/pages/chat/events/message_download_content.dart | 11 +++++++++++ lib/utils/matrix_sdk_extensions/event_extension.dart | 11 ++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index e2cc6f79c2..f0935d60b9 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -49,6 +49,7 @@ class _MessageDownloadContentState extends State } void checkDownloadFileState() async { + checkFileExistInMemory(); final filePath = await widget.event.getFileNameInAppDownload(); final file = File(filePath); if (await file.exists() && @@ -65,6 +66,16 @@ class _MessageDownloadContentState extends State } } + void checkFileExistInMemory() { + final filePathInMem = widget.event.getFilePathFromMem(); + if (filePathInMem.isNotEmpty) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePathInMem, + ); + return; + } + } + void _trySetupDownloadingStreamSubcription() { streamSubscription = downloadManager .getDownloadStateStream(widget.event.eventId) diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 8e8db0c80c..1d033f023e 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -34,8 +34,12 @@ extension LocalizedBody on Event { return await matrixFile.result?.downloadFile(context); } + String get filenameEllipsized { + return filename.ellipsizeFileName; + } + String get filename { - return (content.tryGet('filename') ?? body).ellipsizeFileName; + return (content.tryGet('filename') ?? body); } String? get blurHash { @@ -113,6 +117,11 @@ extension LocalizedBody on Event { return room.sendingFilePlaceholders[txId]; } + String getFilePathFromMem() { + final matrixFile = getMatrixFile(); + return matrixFile?.filePath ?? ''; + } + Size? getOriginalResolution() { if (infoMap['w'] != null && infoMap['h'] != null) { return Size( From de22822926dd31ab383678cd63b7f7d0173553c0 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 28 Mar 2024 13:56:15 +0700 Subject: [PATCH 075/183] TW-1561: move filePath in app downloads to utils class (cherry picked from commit 5175e2ac93792da1949e0c83cdb86d0100f1b661) --- .../chat/events/message_download_content.dart | 10 +++++++--- .../extensions/send_file_extension.dart | 14 ++++++++------ .../download_file_extension.dart | 9 ++++----- .../matrix_sdk_extensions/event_extension.dart | 4 ++-- lib/utils/storage_directory_utils.dart | 9 +++++++++ 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index f0935d60b9..b29a891f76 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/utils/manager/download_manager/download_file_state.da import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; @@ -50,7 +51,10 @@ class _MessageDownloadContentState extends State void checkDownloadFileState() async { checkFileExistInMemory(); - final filePath = await widget.event.getFileNameInAppDownload(); + final filePath = await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: widget.event.eventId, + fileName: widget.event.filename, + ); final file = File(filePath); if (await file.exists() && await file.length() == widget.event.getFileSize()) { @@ -68,9 +72,9 @@ class _MessageDownloadContentState extends State void checkFileExistInMemory() { final filePathInMem = widget.event.getFilePathFromMem(); - if (filePathInMem.isNotEmpty) { + if (filePathInMem?.isNotEmpty == true) { downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: filePathInMem, + filePath: filePathInMem!, ); return; } diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index aa02188be7..221fbb8a18 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -308,16 +308,18 @@ extension SendFileExtension on Room { required String fileName, }) async { try { - final downloadInAppFolder = - await StorageDirectoryUtils.instance.getDownloadFolderInApp(); - final filePathInAppDownloads = '$downloadInAppFolder/$eventId/$fileName'; - final fileInMem = sendingFilePlaceholders[sendingEventId]?.filePath; + final filePathInAppDownloads = + await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: fileName, + ); + final sendingFilePath = sendingFilePlaceholders[sendingEventId]?.filePath; final file = File(filePathInAppDownloads); - if (await file.exists() || fileInMem == null) { + if (await file.exists() || sendingFilePath == null) { return; } await file.create(recursive: true); - await File(fileInMem).copy(filePathInAppDownloads); + await File(sendingFilePath).copy(filePathInAppDownloads); Logs().d('File copied in app downloads folder', filePathInAppDownloads); } catch (e) { Logs().e('Error while copying file in app downloads folder', e); diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 62352a65e4..ae96fb11a6 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -276,7 +276,10 @@ extension DownloadFileExtension on Event { return downloadOrRetrieveAttachment( mxcUrl, - await getFileNameInAppDownload(), + await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: filename, + ), downloadStreamController: downloadStreamController, getThumbnail: getThumbnail, cancelToken: cancelToken, @@ -349,8 +352,4 @@ extension DownloadFileExtension on Event { return fileInfo; } - - Future getFileNameInAppDownload() async { - return '${await StorageDirectoryUtils.instance.getDownloadFolderInApp()}/$eventId/$filename'; - } } diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 1d033f023e..dcb94db075 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -117,9 +117,9 @@ extension LocalizedBody on Event { return room.sendingFilePlaceholders[txId]; } - String getFilePathFromMem() { + String? getFilePathFromMem() { final matrixFile = getMatrixFile(); - return matrixFile?.filePath ?? ''; + return matrixFile?.filePath; } Size? getOriginalResolution() { diff --git a/lib/utils/storage_directory_utils.dart b/lib/utils/storage_directory_utils.dart index 5de7d402de..4282442da7 100644 --- a/lib/utils/storage_directory_utils.dart +++ b/lib/utils/storage_directory_utils.dart @@ -25,4 +25,13 @@ class StorageDirectoryUtils { _tempDirectoryPath ??= (await getTemporaryDirectory()).path; return '$_tempDirectoryPath/Downloads'; } + + Future getFilePathInAppDownloads({ + required String eventId, + required String fileName, + }) async { + final downloadInAppFolder = + await StorageDirectoryUtils.instance.getDownloadFolderInApp(); + return '$downloadInAppFolder/$eventId/$fileName'; + } } From 9d08dc9d23856d1c00762d53b2454daef9f9c141 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 28 Mar 2024 14:07:24 +0700 Subject: [PATCH 076/183] TW-1561: refactor: create checkFileInDownloadsInApp method (cherry picked from commit 56055c362ace82a07ec51d4039a407be4e470cdf) --- .../chat/events/message_download_content.dart | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index b29a891f76..cc531b5fd5 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -51,18 +51,7 @@ class _MessageDownloadContentState extends State void checkDownloadFileState() async { checkFileExistInMemory(); - final filePath = await StorageDirectoryUtils.instance.getFilePathInAppDownloads( - eventId: widget.event.eventId, - fileName: widget.event.filename, - ); - final file = File(filePath); - if (await file.exists() && - await file.length() == widget.event.getFileSize()) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: filePath, - ); - return; - } + await checkFileInDownloadsInApp(); _trySetupDownloadingStreamSubcription(); if (streamSubscription != null) { @@ -80,6 +69,22 @@ class _MessageDownloadContentState extends State } } + Future checkFileInDownloadsInApp() async { + final filePath = + await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: widget.event.eventId, + fileName: widget.event.filename, + ); + final file = File(filePath); + if (await file.exists() && + await file.length() == widget.event.getFileSize()) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePath, + ); + return; + } + } + void _trySetupDownloadingStreamSubcription() { streamSubscription = downloadManager .getDownloadStateStream(widget.event.eventId) From c310a1da58ecaf558048f004eb0314f1fc9fc62d Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 27 Mar 2024 17:38:05 +0700 Subject: [PATCH 077/183] TW-1589: Add function to check if string contains http protocol and function to remove http protocol (cherry picked from commit 3ceb7fc2b9d2060c8fbfd8114dcde0ed70f5a2b5) --- lib/utils/string_extension.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/utils/string_extension.dart b/lib/utils/string_extension.dart index 6db9af3811..864fdd1eaf 100644 --- a/lib/utils/string_extension.dart +++ b/lib/utils/string_extension.dart @@ -341,4 +341,16 @@ extension StringCasingExtension on String { String normalizePhoneNumber() { return replaceAll(RegExp(r'\D'), ''); } + + bool isContainsHttpProtocol() { + final urlRegExp = RegExp( + r'(http://|https://)(www.)?([a-zA-Z0-9]+).[a-zA-Z0-9]*.[a-z]{2,}.?([a-z]+)?', + ); + return urlRegExp.hasMatch(this); + } + + String removeHttpProtocol() { + final httpProtocolRegExp = RegExp(r'(http://|https://)'); + return replaceAll(httpProtocolRegExp, ''); + } } From 0dc2a4a29e60283f4b0568bb22cc7785c0546a3e Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 27 Mar 2024 17:42:26 +0700 Subject: [PATCH 078/183] TW-1589: Write test for `isContainsHttpProtocol` and `removeHttpProtocol` (cherry picked from commit 39d1c0fe02b3f4a3edc284719c3d3efa28e61a70) --- test/utils/string_extension_test.dart | 129 ++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/test/utils/string_extension_test.dart b/test/utils/string_extension_test.dart index 76453a8b4d..0011113c86 100644 --- a/test/utils/string_extension_test.dart +++ b/test/utils/string_extension_test.dart @@ -49,4 +49,133 @@ void main() { expect(result, equals(expectedPhoneNumber)); }); }); + + group( + '[isContainsHttpProtocol] TEST\n' + 'GIVEN a string\n' + 'USING isContainsUrl function\n' + 'IF the text contains a URL\n' + 'THEN should return true\n' + 'ELSE should return false\n', () { + final testMap = { + 'https://www.example.com': true, + 'https://www.example.com/': true, + 'https://www.example.com/watch?v=ohg5sjyrha0': true, + 'https://example.com/test/test-a-link/check/1608?test=1': true, + 'https://www.example.com/watch?v=ohg5sjyrha0&feature=related': true, + 'https://example.com/test/test-a-link/check/1608': true, + 'http://www.example.com': true, + 'http://www.example.com/': true, + 'http://www.example.com/watch?v=ohg5sjyrha0': true, + 'http://example.com/test/test-a-link/check/1608/': true, + 'http://example.com/test/test-a-link/check/1608/?test=1': true, + 'https://example': true, + 'www.example.com': false, + 'www.example.com/': false, + 'www.example.com/watch?v=ohg5sjyrha0': false, + 'example.com': false, + 'example.com/test/test-a-link/check/1608': false, + 'example.com/test/test-a-link/check/1608?test=1': false, + 'example.com/test/test-a-link/check/1608/': false, + 'example.com/test/test-a-link/check/1608/?test=1': false, + 'https:/www.example.com': false, + 'http//www.example.com': false, + 'example.com/test test-a-link/check/1608': false, + 'example.com/test/test-a-link/check/1608?test': false, + 'example.com/test/test-a-link/check/1608/?test==1': false, + 'example.com/test/test-a-link/check/1608/?test=1&': false, + 'hello world': false, + 'hello world.com': false, + 'hello world.com/test': false, + 'hello world.com/test/test-a-link': false, + 'hello world.com/test/test-a-link/check': false, + 'hello world.com/test/test-a-link/check/1608': false, + 'hello world.com/test/test-a-link/check/1608?test': false, + 'hello world.com/test/test-a-link/check/1608/?test==1': false, + 'hello world.com/test/test-a-link/check/1608/?test=1&': false, + 'this is a test string': false, + }; + + for (final entry in testMap.entries) { + test('Testing: ${entry.key} => Expected: ${entry.value}', () { + final result = entry.key.isContainsHttpProtocol(); + expect(result, entry.value); + }); + } + }); + + group( + '[removeHttpProtocol] TEST\n' + 'GIVEN a string\n' + 'USING removeHttpProtocol function\n' + 'IF the URL starts with http:// or https://\n' + 'THEN should return the URL without the protocol\n' + 'ELSE should return the string unchanged\n', () { + final testMap = { + 'https://www.example.com': 'www.example.com', + 'https://www.example.com/': 'www.example.com/', + 'https://www.example.com/watch?v=ohg5sjyrha0': + 'www.example.com/watch?v=ohg5sjyrha0', + 'https://example.com/test/test-a-link/check/1608?test=1': + 'example.com/test/test-a-link/check/1608?test=1', + 'https://www.example.com/watch?v=ohg5sjyrha0&feature=related': + 'www.example.com/watch?v=ohg5sjyrha0&feature=related', + 'https://example.com/test/test-a-link/check/1608': + 'example.com/test/test-a-link/check/1608', + 'http://www.example.com': 'www.example.com', + 'http://www.example.com/': 'www.example.com/', + 'http://www.example.com/watch?v=ohg5sjyrha0': + 'www.example.com/watch?v=ohg5sjyrha0', + 'http://example.com/test/test-a-link/check/1608/': + 'example.com/test/test-a-link/check/1608/', + 'http://example.com/test/test-a-link/check/1608/?test=1': + 'example.com/test/test-a-link/check/1608/?test=1', + 'https://example': 'example', + 'www.example.com': 'www.example.com', + 'www.example.com/': 'www.example.com/', + 'www.example.com/watch?v=ohg5sjyrha0': + 'www.example.com/watch?v=ohg5sjyrha0', + 'example.com': 'example.com', + 'example.com/test/test-a-link/check/1608': + 'example.com/test/test-a-link/check/1608', + 'example.com/test/test-a-link/check/1608?test=1': + 'example.com/test/test-a-link/check/1608?test=1', + 'example.com/test/test-a-link/check/1608/': + 'example.com/test/test-a-link/check/1608/', + 'example.com/test/test-a-link/check/1608/?test=1': + 'example.com/test/test-a-link/check/1608/?test=1', + 'https:/www.example.com': 'https:/www.example.com', + 'http//www.example.com': 'http//www.example.com', + 'example.com/test test-a-link/check/1608': + 'example.com/test test-a-link/check/1608', + 'example.com/test/test-a-link/check/1608?test': + 'example.com/test/test-a-link/check/1608?test', + 'example.com/test/test-a-link/check/1608/?test==1': + 'example.com/test/test-a-link/check/1608/?test==1', + 'example.com/test/test-a-link/check/1608/?test=1&': + 'example.com/test/test-a-link/check/1608/?test=1&', + 'hello world': 'hello world', + 'hello world.com': 'hello world.com', + 'hello world.com/test': 'hello world.com/test', + 'hello world.com/test/test-a-link': 'hello world.com/test/test-a-link', + 'hello world.com/test/test-a-link/check': + 'hello world.com/test/test-a-link/check', + 'hello world.com/test/test-a-link/check/1608': + 'hello world.com/test/test-a-link/check/1608', + 'hello world.com/test/test-a-link/check/1608?test': + 'hello world.com/test/test-a-link/check/1608?test', + 'hello world.com/test/test-a-link/check/1608/?test==1': + 'hello world.com/test/test-a-link/check/1608/?test==1', + 'hello world.com/test/test-a-link/check/1608/?test=1&': + 'hello world.com/test/test-a-link/check/1608/?test=1&', + 'this is a test string': 'this is a test string', + }; + + for (final entry in testMap.entries) { + test('Testing: ${entry.key} => Expected: ${entry.value}', () { + final result = entry.key.removeHttpProtocol(); + expect(result, entry.value); + }); + } + }); } From ab98f27240de5d0b0d9b90047529127778b711ac Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 27 Mar 2024 17:43:05 +0700 Subject: [PATCH 079/183] TW-1589: Remove http protocol before update search categories (cherry picked from commit 96c27167be44968a4465aa35fccbcf2308b453d7) --- lib/pages/search/server_search_controller.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/pages/search/server_search_controller.dart b/lib/pages/search/server_search_controller.dart index 36872a9cbd..3d809b1c66 100644 --- a/lib/pages/search/server_search_controller.dart +++ b/lib/pages/search/server_search_controller.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/domain/usecase/search/server_search_interactor.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_state.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_empty_search.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_search.dart'; +import 'package:fluffychat/utils/string_extension.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/domain/app_state/search/server_search_state.dart'; import 'package:matrix/matrix.dart'; @@ -110,6 +111,11 @@ class ServerSearchController with SearchDebouncerMixin { void updateSearchCategories(String searchTerm) { resetNextBatch(); + + if (searchTerm.isContainsHttpProtocol()) { + searchTerm = searchTerm.removeHttpProtocol(); + } + _searchCategories = ServerSideSearchCategories( searchTerm: searchTerm, searchFilter: SearchFilter( From f1f5e1aad23e63cd94507edcc7dc78ec224fecb8 Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 27 Mar 2024 20:12:06 +0700 Subject: [PATCH 080/183] TW-1589: Improve unit test (cherry picked from commit 5bde42498712ba72bfc7129cf1524c9e3330a9b3) --- .../search/server_search_controller.dart | 4 +- lib/utils/string_extension.dart | 19 +-- test/utils/string_extension_test.dart | 125 +++++++++++++++++- 3 files changed, 132 insertions(+), 16 deletions(-) diff --git a/lib/pages/search/server_search_controller.dart b/lib/pages/search/server_search_controller.dart index 3d809b1c66..69a41a3d39 100644 --- a/lib/pages/search/server_search_controller.dart +++ b/lib/pages/search/server_search_controller.dart @@ -112,8 +112,8 @@ class ServerSearchController with SearchDebouncerMixin { void updateSearchCategories(String searchTerm) { resetNextBatch(); - if (searchTerm.isContainsHttpProtocol()) { - searchTerm = searchTerm.removeHttpProtocol(); + if (searchTerm.isContainsUrlSeparator()) { + searchTerm = searchTerm.removeUrlSeparatorAndPreceding(); } _searchCategories = ServerSideSearchCategories( diff --git a/lib/utils/string_extension.dart b/lib/utils/string_extension.dart index 864fdd1eaf..b1a413d731 100644 --- a/lib/utils/string_extension.dart +++ b/lib/utils/string_extension.dart @@ -342,15 +342,18 @@ extension StringCasingExtension on String { return replaceAll(RegExp(r'\D'), ''); } - bool isContainsHttpProtocol() { - final urlRegExp = RegExp( - r'(http://|https://)(www.)?([a-zA-Z0-9]+).[a-zA-Z0-9]*.[a-z]{2,}.?([a-z]+)?', - ); - return urlRegExp.hasMatch(this); + bool isContainsUrlSeparator() { + final separatorRegExp = RegExp(r'://'); + return separatorRegExp.hasMatch(this); } - String removeHttpProtocol() { - final httpProtocolRegExp = RegExp(r'(http://|https://)'); - return replaceAll(httpProtocolRegExp, ''); + String removeUrlSeparatorAndPreceding() { + final separatorRegExp = RegExp(r'\b[^ ]*://'); + final standAloneSeparatorRegExp = RegExp(r' *:// *'); + + var replacedText = replaceAll(separatorRegExp, ''); + replacedText = replacedText.replaceAll(standAloneSeparatorRegExp, ' '); + + return replacedText; } } diff --git a/test/utils/string_extension_test.dart b/test/utils/string_extension_test.dart index 0011113c86..e83c472578 100644 --- a/test/utils/string_extension_test.dart +++ b/test/utils/string_extension_test.dart @@ -54,16 +54,28 @@ void main() { '[isContainsHttpProtocol] TEST\n' 'GIVEN a string\n' 'USING isContainsUrl function\n' - 'IF the text contains a URL\n' + 'IF the string contains a URL\n' 'THEN should return true\n' 'ELSE should return false\n', () { final testMap = { + 'https': false, + 'https:': false, + 'https:/': false, + 'https/': false, + 'https//': false, + 'https://': true, 'https://www.example.com': true, 'https://www.example.com/': true, 'https://www.example.com/watch?v=ohg5sjyrha0': true, 'https://example.com/test/test-a-link/check/1608?test=1': true, 'https://www.example.com/watch?v=ohg5sjyrha0&feature=related': true, 'https://example.com/test/test-a-link/check/1608': true, + 'http': false, + 'http:': false, + 'http:/': false, + 'http/': false, + 'http//': false, + 'http://': true, 'http://www.example.com': true, 'http://www.example.com/': true, 'http://www.example.com/watch?v=ohg5sjyrha0': true, @@ -94,12 +106,49 @@ void main() { 'hello world.com/test/test-a-link/check/1608/?test==1': false, 'hello world.com/test/test-a-link/check/1608/?test=1&': false, 'this is a test string': false, + 'ftp': false, + 'ftp:': false, + 'ftp:/': false, + 'ftp//': false, + 'ftp://': true, + 'ftp://www.example.com': true, + 'ftp://www.example.com/': true, + 'ftp://www.example.com/watch?v=ohg5sjyrha0': true, + 'ftp://example.com/test/test-a-link/check/1608?test=1': true, + 'ftp://www.example.com/watch?v=ohg5sjyrha0&feature=related': true, + 'ftp://example.com/test/test-a-link/check/1608': true, + 'sftp': false, + 'sftp:': false, + 'sftp:/': false, + 'sftp//': false, + 'sftp://': true, + 'sftp://www.example.com': true, + 'sftp://www.example.com/': true, + 'sftp://www.example.com/watch?v=ohg5sjyrha0': true, + 'sftp://example.com/test/test-a-link/check/1608?test=1': true, + 'sftp://www.example.com/watch?v=ohg5sjyrha0&feature=related': true, + 'sftp://example.com/test/test-a-link/check/1608': true, + 'ssh': false, + 'ssh:': false, + 'ssh:/': false, + 'ssh//': false, + 'ssh://': true, + 'ssh://www.example.com': true, + 'ssh://www.example.com/': true, + 'ssh://www.example.com/watch?v=ohg5sjyrha0': true, + 'ssh://example.com/test/test-a-link/check/1608?test=1': true, + 'ssh://www.example.com/watch?v=ohg5sjyrha0&feature=related': true, + 'ssh://example.com/test/test-a-link/check/1608': true, }; for (final entry in testMap.entries) { test('Testing: ${entry.key} => Expected: ${entry.value}', () { - final result = entry.key.isContainsHttpProtocol(); - expect(result, entry.value); + final result = entry.key.isContainsUrlSeparator(); + if (entry.value) { + expect(result, isTrue); + } else { + expect(result, isFalse); + } }); } }); @@ -108,10 +157,15 @@ void main() { '[removeHttpProtocol] TEST\n' 'GIVEN a string\n' 'USING removeHttpProtocol function\n' - 'IF the URL starts with http:// or https://\n' + 'IF the string starts with http:// or https://\n' 'THEN should return the URL without the protocol\n' 'ELSE should return the string unchanged\n', () { final testMap = { + 'https': 'https', + 'https:': 'https:', + 'https:/': 'https:/', + 'https//': 'https//', + 'https://': '', 'https://www.example.com': 'www.example.com', 'https://www.example.com/': 'www.example.com/', 'https://www.example.com/watch?v=ohg5sjyrha0': @@ -122,6 +176,11 @@ void main() { 'www.example.com/watch?v=ohg5sjyrha0&feature=related', 'https://example.com/test/test-a-link/check/1608': 'example.com/test/test-a-link/check/1608', + 'http': 'http', + 'http:': 'http:', + 'http:/': 'http:/', + 'http//': 'http//', + 'http://': '', 'http://www.example.com': 'www.example.com', 'http://www.example.com/': 'www.example.com/', 'http://www.example.com/watch?v=ohg5sjyrha0': @@ -168,13 +227,67 @@ void main() { 'hello world.com/test/test-a-link/check/1608/?test==1', 'hello world.com/test/test-a-link/check/1608/?test=1&': 'hello world.com/test/test-a-link/check/1608/?test=1&', + 'hello 123://world.com/test/test-a-link/check/1608/?test=1&': + 'hello world.com/test/test-a-link/check/1608/?test=1&', + 'hello this is the://world.com/test/test-a-link/check/1608/?test=1& for the text contains :// inside it': + 'hello this is world.com/test/test-a-link/check/1608/?test=1& for the text contains inside it', 'this is a test string': 'this is a test string', + 'ftp': 'ftp', + 'ftp:': 'ftp:', + 'ftp:/': 'ftp:/', + 'ftp//': 'ftp//', + 'ftp://': '', + 'ftp://www.example.com': 'www.example.com', + 'ftp://www.example.com/': 'www.example.com/', + 'ftp://www.example.com/watch?v=ohg5sjyrha0': + 'www.example.com/watch?v=ohg5sjyrha0', + 'ftp://example.com/test/test-a-link/check/1608?test=1': + 'example.com/test/test-a-link/check/1608?test=1', + 'ftp://www.example.com/watch?v=ohg5sjyrha0&feature=related': + 'www.example.com/watch?v=ohg5sjyrha0&feature=related', + 'ftp://example.com/test/test-a-link/check/1608': + 'example.com/test/test-a-link/check/1608', + 'sftp': 'sftp', + 'sftp:': 'sftp:', + 'sftp:/': 'sftp:/', + 'sftp//': 'sftp//', + 'sftp://': '', + 'sftp://www.example.com': 'www.example.com', + 'sftp://www.example.com/': 'www.example.com/', + 'sftp://www.example.com/watch?v=ohg5sjyrha0': + 'www.example.com/watch?v=ohg5sjyrha0', + 'sftp://example.com/test/test-a-link/check/1608?test=1': + 'example.com/test/test-a-link/check/1608?test=1', + 'sftp://www.example.com/watch?v=ohg5sjyrha0&feature=related': + 'www.example.com/watch?v=ohg5sjyrha0&feature=related', + 'sftp://example.com/test/test-a-link/check/1608': + 'example.com/test/test-a-link/check/1608', + 'ssh': 'ssh', + 'ssh:': 'ssh:', + 'ssh:/': 'ssh:/', + 'ssh//': 'ssh//', + 'ssh://': '', + 'ssh://www.example.com': 'www.example.com', + 'ssh://www.example.com/': 'www.example.com/', + 'ssh://www.example.com/watch?v=ohg5sjyrha0': + 'www.example.com/watch?v=ohg5sjyrha0', + 'ssh://example.com/test/test-a-link/check/1608?test=1': + 'example.com/test/test-a-link/check/1608?test=1', + 'ssh://www.example.com/watch?v=ohg5sjyrha0&feature=related': + 'www.example.com/watch?v=ohg5sjyrha0&feature=related', + 'ssh://example.com/test/test-a-link/check/1608': + 'example.com/test/test-a-link/check/1608', }; for (final entry in testMap.entries) { test('Testing: ${entry.key} => Expected: ${entry.value}', () { - final result = entry.key.removeHttpProtocol(); - expect(result, entry.value); + final result = entry.key.removeUrlSeparatorAndPreceding(); + if (entry.value.isNotEmpty) { + expect(result, isNotEmpty); + expect(result, equals(entry.value)); + } else { + expect(result, isEmpty); + } }); } }); From 66763856114c5cb6b626507055095d4900de9930 Mon Sep 17 00:00:00 2001 From: hieubt Date: Fri, 29 Mar 2024 17:15:50 +0700 Subject: [PATCH 081/183] hot-fix: Keep original size of `li` row (cherry picked from commit eda4110fdc080d21944d12452800cd14b27386e6) --- pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f767bf28a7..df5136747f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -958,7 +958,7 @@ packages: description: path: "." ref: master - resolved-ref: "27e36218e126e9e1c382e45fde4a683b10e83f91" + resolved-ref: "9a1027b074c530f6deec4a5593a7a62f8fd70f45" url: "https://github.com/linagora/flutter_matrix_html.git" source: git version: "1.2.0" @@ -3153,4 +3153,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" \ No newline at end of file + flutter: ">=3.16.0" From b0ee937688f705abd7191e1dec27a1cc88c3a902 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Sat, 30 Mar 2024 10:00:25 +0700 Subject: [PATCH 082/183] Create docs for public platform (cherry picked from commit a9682dc113c2c2c2930e9d32b8352a63dfe97bd0) --- ...ig_build_mobile_app_for_public_platform.md | 28 ++++++++++ .../config_web_app_for_public_platform.md | 47 +++++++++++++++++ lib/config/app_config.dart | 51 ++++++++++++++----- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- 4 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 docs/configurations/config_build_mobile_app_for_public_platform.md create mode 100644 docs/configurations/config_web_app_for_public_platform.md diff --git a/docs/configurations/config_build_mobile_app_for_public_platform.md b/docs/configurations/config_build_mobile_app_for_public_platform.md new file mode 100644 index 0000000000..7141595824 --- /dev/null +++ b/docs/configurations/config_build_mobile_app_for_public_platform.md @@ -0,0 +1,28 @@ +## Configuring apps with compilation environment declarations + +### Context + +- Twake Chat app can use the values of environment declarations to change its functionality or behavior. + Dart compilers can eliminate the code made unreachable due to + control flow using the environment declaration values. +- Only support for mobile app. + +### How to config + +1.Flutter +- To specify environment declarations to the Flutter tool, use the `--dart-define` option instead: + +``` +flutter run --dart-define=REGISTRATION_URL=https://example.com/ +--dart-define=TWAKE_WORKPLACE_HOMESERVER=https://example.com/ +--dart-define=PLATFORM=platfomrm +--dart-define=HOME_SERVER=https://example.com/ + +``` + +- `REGISTRATION_URL`: Registration URL for public platform +- `TWAKE_WORKPLACE_HOMESERVER`: Twake workplace homeserver +- `PLATFORM`: Platform, `saas` for the case of public platform +- `HOME_SERVER`: Homeserver + +If you want to disable it, please change the value or remove when use `--dart-define` option \ No newline at end of file diff --git a/docs/configurations/config_web_app_for_public_platform.md b/docs/configurations/config_web_app_for_public_platform.md new file mode 100644 index 0000000000..bddbebe4a9 --- /dev/null +++ b/docs/configurations/config_web_app_for_public_platform.md @@ -0,0 +1,47 @@ +## Configuration for config.json + +### Context + +- Twake Chat need to config env for Twake Chat service on web version +- Now only support to web + +### How to config + +1.Add environment +in [config.sample.json](https://github.com/linagora/twake-on-matrix/blob/main/config.sample.json) + +- Env for Twake Chat service: + +``` +{ + "application_name": "Twake Chat", + "application_welcome_message": "Welcome to Twake Chat!", + "default_homeserver": "matrix.linagora.com", + "privacy_url": "https://twake.app/en/privacy/", + "render_html": true, + "hide_redacted_events": false, + "hide_unknown_events": false, + "issue_id": "", + "registration_url": "https://example.com/", + "twake_workplace_homeserver": "https://example.com/", + "app_grid_dashboard_available": true, + "homeserver": "https://example.com/", + "platform": "platform" +} +``` + +- `application_name`: The name will be showed in App Grid +- `application_welcome_message`: The welcome message will be showed in App Grid +- `default_homeserver`: The default homeserver +- `privacy_url`: The privacy policy URL +- `render_html`: Render HTML in messages +- `hide_redacted_events`: Hide redacted events +- `hide_unknown_events`: Hide unknown events +- `issue_id`: Issue ID +- `registration_url`: Registration URL for public platform +- `twake_workplace_homeserver`: Twake workplace homeserver +- `app_grid_dashboard_available`: Enable App Grid +- `homeserver`: Homeserver +- `platform`: Platform, `saas` for the case of public platform + +If you want to disable it, please change the value or remove this from `config.sample.json` \ No newline at end of file diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index fa0c3b65ef..bde39e45ac 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -103,30 +103,53 @@ abstract class AppConfig { static const String appGridConfigurationPath = "configurations/app_dashboard.json"; + static const String _platformEnv = String.fromEnvironment( + 'PLATFORM', + defaultValue: 'platform', + ); + + static const String _twakeWorkplaceHomeserverEnv = String.fromEnvironment( + 'TWAKE_WORKPLACE_HOMESERVER', + defaultValue: 'https://example.com/', + ); + + static const String _registrationUrlEnv = String.fromEnvironment( + 'REGISTRATION_URL', + defaultValue: 'https://example.com/', + ); + + static const String _homeserverEnv = String.fromEnvironment( + 'HOME_SERVER', + defaultValue: 'https://example.com/', + ); + static void loadEnvironment() { - twakeWorkplaceHomeserver = const String.fromEnvironment( - 'TWAKE_WORKPLACE_HOMESERVER', - defaultValue: 'https://example.com/', + twakeWorkplaceHomeserver = _twakeWorkplaceHomeserverEnv; + + Logs().i( + '[Public Platform] AppConfig():: TWAKE_WORKPLACE_HOMESERVER $_twakeWorkplaceHomeserverEnv', ); - registrationUrl = const String.fromEnvironment( - 'REGISTRATION_URL', - defaultValue: 'https://example.com/', + registrationUrl = _registrationUrlEnv; + + Logs().i( + '[Public Platform] AppConfig():: REGISTRATION_URL $_registrationUrlEnv', ); - platform = const String.fromEnvironment( - 'PLATFORM', - defaultValue: 'platform', + platform = _platformEnv; + + Logs().i( + '[Public Platform] AppConfig():: Platform $_platformEnv', ); - homeserver = const String.fromEnvironment( - 'HOME_SERVER', - defaultValue: 'https://example.com/', + homeserver = _homeserverEnv; + + Logs().i( + '[Public Platform] AppConfig():: HOME_SERVER $_homeserverEnv', ); } - static bool get isSaasPlatForm => - platform != null && platform!.isNotEmpty && platform == 'saas'; + static bool get isSaasPlatForm => _platformEnv == 'saas'; static void loadFromJson(Map json) { if (json['homeserver'] != null && json['homeserver'] is String) { diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 16840a23f3..f2af6f2ea8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -83,4 +83,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin")) -} \ No newline at end of file +} From d1ec9cadc9677f519db46c43255aafd749681228 Mon Sep 17 00:00:00 2001 From: --global Date: Fri, 29 Mar 2024 17:30:41 +0700 Subject: [PATCH 083/183] improve: refactor to have CircularLoadingDownloadWidget (cherry picked from commit b2a193b6f854d915ddfc294658f9b517eed9912b) --- .../circular_loading_download_widget.dart | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 lib/widgets/file_widget/circular_loading_download_widget.dart diff --git a/lib/widgets/file_widget/circular_loading_download_widget.dart b/lib/widgets/file_widget/circular_loading_download_widget.dart new file mode 100644 index 0000000000..d2298a1a33 --- /dev/null +++ b/lib/widgets/file_widget/circular_loading_download_widget.dart @@ -0,0 +1,61 @@ +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:flutter/material.dart'; + +class CircularLoadingDownloadWidget extends StatefulWidget { + const CircularLoadingDownloadWidget({ + super.key, + required this.downloadProgress, + this.style = const MessageFileTileStyle(), + }); + + final double? downloadProgress; + + final MessageFileTileStyle style; + + @override + State createState() => + _CircularLoadingDownloadWidgetState(); +} + +class _CircularLoadingDownloadWidgetState + extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { + AnimationController? _controller; + Animation? _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + + _animation = CurvedAnimation( + parent: _controller!, + curve: Curves.linear, + ); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return RotationTransition( + turns: _animation!, + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.surface, + strokeWidth: widget.style.strokeWidthLoading, + value: widget.downloadProgress, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} From 85b36966b0be0890b816d3468f33c9ec361c08ab Mon Sep 17 00:00:00 2001 From: --global Date: Fri, 29 Mar 2024 17:31:16 +0700 Subject: [PATCH 084/183] improve: change the fileTile UI (cherry picked from commit c921472a3df997cccb07faf5be18a7b5251e3029) --- .../circular_loading_download_widget.dart | 4 +- .../download_file_tile_widget.dart | 125 ++++++++---------- .../file_widget/message_file_tile_style.dart | 12 +- 3 files changed, 65 insertions(+), 76 deletions(-) diff --git a/lib/widgets/file_widget/circular_loading_download_widget.dart b/lib/widgets/file_widget/circular_loading_download_widget.dart index d2298a1a33..2ada728268 100644 --- a/lib/widgets/file_widget/circular_loading_download_widget.dart +++ b/lib/widgets/file_widget/circular_loading_download_widget.dart @@ -23,11 +23,13 @@ class _CircularLoadingDownloadWidgetState AnimationController? _controller; Animation? _animation; + static const animationDurationSeconds = 2; + @override void initState() { super.initState(); _controller = AnimationController( - duration: const Duration(seconds: 2), + duration: const Duration(seconds: animationDurationSeconds), vsync: this, )..repeat(reverse: true); diff --git a/lib/widgets/file_widget/download_file_tile_widget.dart b/lib/widgets/file_widget/download_file_tile_widget.dart index f57748e62a..973cd77578 100644 --- a/lib/widgets/file_widget/download_file_tile_widget.dart +++ b/lib/widgets/file_widget/download_file_tile_widget.dart @@ -1,11 +1,12 @@ import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/file_widget/circular_loading_download_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:flutter/material.dart'; -class DownloadFileTileWidget extends StatefulWidget { +class DownloadFileTileWidget extends StatelessWidget { const DownloadFileTileWidget({ super.key, this.style = const MessageFileTileStyle(), @@ -27,51 +28,21 @@ class DownloadFileTileWidget extends StatefulWidget { final ValueNotifier downloadFileStateNotifier; final VoidCallback? onCancelDownload; - @override - State createState() => _DownloadFileTileWidgetState(); -} - -class _DownloadFileTileWidgetState extends State - with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { - AnimationController? _controller; - Animation? _animation; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(seconds: 2), - vsync: this, - )..repeat(reverse: true); - - _animation = CurvedAnimation( - parent: _controller!, - curve: Curves.linear, - ); - } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - super.build(context); return Container( - padding: widget.style.paddingFileTileAll, + padding: style.paddingFileTileAll, decoration: ShapeDecoration( - color: widget.style.backgroundColor, + color: style.backgroundColor, shape: RoundedRectangleBorder( - borderRadius: widget.style.borderRadius, + borderRadius: style.borderRadius, ), ), child: Row( - crossAxisAlignment: widget.style.crossAxisAlignment, + crossAxisAlignment: style.crossAxisAlignment, children: [ ValueListenableBuilder( - valueListenable: widget.downloadFileStateNotifier, + valueListenable: downloadFileStateNotifier, builder: (context, downloadFileState, child) { double? downloadProgress; if (downloadFileState is DownloadingPresentationState) { @@ -85,38 +56,50 @@ class _DownloadFileTileWidgetState extends State } else if (downloadFileState is NotDownloadPresentationState) { downloadProgress = 0; } - if (downloadProgress == 0) { - return Padding( - padding: widget.style.paddingDownloadFileIcon, - child: Icon( - Icons.file_download_rounded, - size: widget.style.iconSize, - ), - ); - } return Stack( alignment: Alignment.center, children: [ - RotationTransition( - turns: _animation!, - child: CircularProgressIndicator( - strokeWidth: widget.style.strokeWidthLoading, - value: downloadProgress, + Container( + margin: style.marginDownloadIcon, + width: style.iconSize, + height: style.iconSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, ), ), - IconButton( - onPressed: - PlatformInfos.isWeb ? null : widget.onCancelDownload, - icon: Icon( - Icons.close, - size: widget.style.cancelButtonSize, + if (downloadProgress != 0) + SizedBox( + width: style.circularProgressLoadingSize, + height: style.circularProgressLoadingSize, + child: CircularLoadingDownloadWidget( + style: style, + downloadProgress: downloadProgress, + ), + ), + InkWell( + onTap: PlatformInfos.isWeb ? null : onCancelDownload, + child: Container( + width: style.downloadIconSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Icon( + downloadProgress == 0 + ? Icons.arrow_downward + : Icons.close, + key: ValueKey(downloadProgress), + color: Theme.of(context).colorScheme.surface, + size: style.downloadIconSize, + ), ), ), ], ); }, ), - widget.style.paddingRightIcon, + style.paddingRightIcon, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -124,35 +107,34 @@ class _DownloadFileTileWidgetState extends State children: [ const SizedBox(height: 4.0), FileNameText( - filename: widget.filename, - highlightText: widget.highlightText, - style: widget.style, + filename: filename, + highlightText: highlightText, + style: style, ), Row( children: [ - if (widget.sizeString != null) + if (sizeString != null) TextInformationOfFile( - value: widget.sizeString!, - style: widget.style, - downloadFileStateNotifier: - widget.downloadFileStateNotifier, + value: sizeString!, + style: style, + downloadFileStateNotifier: downloadFileStateNotifier, ), TextInformationOfFile( value: " · ", - style: widget.style, + style: style, ), Flexible( child: TextInformationOfFile( - value: widget.mimeType.getFileType( + value: mimeType.getFileType( context, - fileType: widget.fileType, + fileType: fileType, ), - style: widget.style, + style: style, ), ), ], ), - widget.style.paddingBottomText, + style.paddingBottomText, ], ), ), @@ -160,7 +142,4 @@ class _DownloadFileTileWidgetState extends State ), ); } - - @override - bool get wantKeepAlive => true; } diff --git a/lib/widgets/file_widget/message_file_tile_style.dart b/lib/widgets/file_widget/message_file_tile_style.dart index 1f747b2587..65f261af33 100644 --- a/lib/widgets/file_widget/message_file_tile_style.dart +++ b/lib/widgets/file_widget/message_file_tile_style.dart @@ -6,7 +6,7 @@ class MessageFileTileStyle extends FileTileWidgetStyle { const MessageFileTileStyle(); @override - double get iconSize => 36; + double get iconSize => 40; @override EdgeInsets get paddingIcon => const EdgeInsets.only(right: 4); @@ -38,5 +38,13 @@ class MessageFileTileStyle extends FileTileWidgetStyle { double get strokeWidthLoading => 2; - double get cancelButtonSize => 32; + double get cancelButtonSize => 24; + + double get iconSizeDownload => 24; + + double get circularProgressLoadingSize => 32; + + double get downloadIconSize => 28; + + EdgeInsets get marginDownloadIcon => const EdgeInsets.all(4); } From 311db65b4438807c2e5e72e23ba0a8aa008c5908 Mon Sep 17 00:00:00 2001 From: --global Date: Fri, 29 Mar 2024 20:17:39 +0700 Subject: [PATCH 085/183] hot-fix: missing if else in file message (cherry picked from commit d5e144dc17ac94f6e0194e24515ca7c02402d4b6) --- lib/pages/chat/events/message_content.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index bd01ed84c0..84b67e4eea 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -100,7 +100,7 @@ class MessageContent extends StatelessWidget return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (PlatformInfos.isWeb) ...[ + if (!PlatformInfos.isWeb) ...[ MessageDownloadContent( event, ), @@ -121,9 +121,15 @@ class MessageContent extends StatelessWidget return Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - MessageDownloadContent( - event, - ), + if (!PlatformInfos.isWeb) ...[ + MessageDownloadContent( + event, + ), + ] else ...[ + MessageDownloadContentWeb( + event, + ), + ], Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, child: endOfBubbleWidget, From 0729a58f637fd2ceca40ab3c328e906bc405ff69 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 1 Apr 2024 13:31:33 +0700 Subject: [PATCH 086/183] hot-fix: remove redundancy filename extension when downloading file (cherry picked from commit c148b869f83baa44a491d973b3962e4c0ae28c93) --- lib/utils/matrix_sdk_extensions/matrix_file_extension.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 64fed89d69..9f429344fd 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -2,13 +2,11 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; -import 'package:mime/mime.dart'; import 'package:share_plus/share_plus.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/size_string.dart'; @@ -51,8 +49,6 @@ extension MatrixFileExtension on MatrixFile { final directory = await FileSaver.instance.saveFile( name: name, bytes: bytes, - ext: extensionFromMime(mimeType), - mimeType: mimeType.toMimeTypeEnum(), ); return '$directory/$name'; } catch (e) { From 7b8e9b032014eb5b0718f2d15e4cc4e56095e9ca Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 15:54:46 +0700 Subject: [PATCH 087/183] TW-1578: add Search screen to the GoRouter (cherry picked from commit e59824eaa0618fb8f50d1545be271ac0a1ec896f) --- lib/config/go_routes/go_router.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 4d77f4d177..33ca89772c 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/pages/error_page/error_page.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pages/login/on_auth_redirect.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; +import 'package:fluffychat/pages/search/search.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settings_app_language.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; @@ -230,7 +231,8 @@ abstract class AppRoutes { ], redirect: loggedOutRedirect, ), - if (FirstColumnInnerRoutes.instance.goRouteAvailableInFirstColumn()) + if (FirstColumnInnerRoutes.instance + .goRouteAvailableInFirstColumn()) ...[ GoRoute( path: 'newprivatechat', pageBuilder: (context, state) { @@ -270,6 +272,16 @@ abstract class AppRoutes { ), ], ), + GoRoute( + path: 'search', + pageBuilder: (_, __) { + return const CupertinoPage( + child: Search(), + ); + }, + redirect: loggedOutRedirect, + ), + ], GoRoute( path: 'newgroup', pageBuilder: (context, state) => defaultPageBuilder( From a3e0f2265157bfbbde33197714eb97e2cb94c034 Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 15:55:34 +0700 Subject: [PATCH 088/183] TW-1578: enable canPop so in ios can swipe to back (cherry picked from commit bdb8a5656d8946105b93a1c034e6cd5f5e2e099f) --- lib/pages/search/search_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/search/search_view.dart b/lib/pages/search/search_view.dart index 014a790a96..e74b88a5db 100644 --- a/lib/pages/search/search_view.dart +++ b/lib/pages/search/search_view.dart @@ -28,7 +28,7 @@ class SearchView extends StatelessWidget { child: _buildAppBarSearch(context), ), body: PopScope( - canPop: false, + canPop: PlatformInfos.isIOS ? true : false, onPopInvoked: (didPop) async { if (PlatformInfos.isAndroid) { searchController.goToRoomsShellBranch(); From f89275290fd8bf4cea8f69c3358418bbe0036fc1 Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 15:56:47 +0700 Subject: [PATCH 089/183] TW-1578: change animation when open server search with swipe to back gesture (cherry picked from commit 1215c025ae65562d0feaf3ec1b81d4ab5707f271) --- lib/config/first_column_inner_routes.dart | 4 +- lib/config/go_routes/go_router.dart | 10 ----- lib/pages/chat_list/chat_list.dart | 11 +++-- lib/pages/chat_list/chat_list_header.dart | 45 ++++++++++++++++--- lib/pages/chat_list/chat_list_view.dart | 6 +-- lib/pages/search/search_text_field.dart | 53 +++++++++++++++++++++++ lib/pages/search/search_view.dart | 30 ++----------- lib/pages/search/search_view_style.dart | 2 + lib/widgets/swipe_to_dismiss_wrap.dart | 38 ++++++++++++++++ 9 files changed, 148 insertions(+), 51 deletions(-) create mode 100644 lib/pages/search/search_text_field.dart create mode 100644 lib/widgets/swipe_to_dismiss_wrap.dart diff --git a/lib/config/first_column_inner_routes.dart b/lib/config/first_column_inner_routes.dart index 401b7fe320..7da75e08b8 100644 --- a/lib/config/first_column_inner_routes.dart +++ b/lib/config/first_column_inner_routes.dart @@ -4,7 +4,7 @@ import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; import 'package:fluffychat/pages/search/search.dart'; import 'package:fluffychat/presentation/model/presentation_contact.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; class FirstColumnInnerRoutes { static final _firstColumnInnerRoutes = FirstColumnInnerRoutes._(); @@ -52,7 +52,7 @@ class FirstColumnInnerRoutes { } static PageRoute _defaultPageRoute(Widget widget) { - return MaterialPageRoute( + return CupertinoPageRoute( builder: (context) { return widget; }, diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 33ca89772c..d585d5dd32 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -15,7 +15,6 @@ import 'package:fluffychat/pages/error_page/error_page.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pages/login/on_auth_redirect.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; -import 'package:fluffychat/pages/search/search.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settings_app_language.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; @@ -272,15 +271,6 @@ abstract class AppRoutes { ), ], ), - GoRoute( - path: 'search', - pageBuilder: (_, __) { - return const CupertinoPage( - child: Search(), - ); - }, - redirect: loggedOutRedirect, - ), ], GoRoute( path: 'newgroup', diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index fc7f696734..5d4cf4cad8 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -48,8 +48,6 @@ class ChatList extends StatefulWidget { final Widget? bottomNavigationBar; - final VoidCallback? onOpenSearchPage; - final VoidCallback? onOpenSettings; final AbsAppAdaptiveScaffoldBodyArgs? adaptiveScaffoldBodyArgs; @@ -58,7 +56,6 @@ class ChatList extends StatefulWidget { Key? key, required this.activeRoomIdNotifier, this.bottomNavigationBar, - this.onOpenSearchPage, this.onOpenSettings, this.adaptiveScaffoldBodyArgs, }) : super(key: key); @@ -819,6 +816,12 @@ class ChatListController extends State super.initState(); } + void onOpenSearchPageInMultipleColumns() { + if (!FirstColumnInnerRoutes.instance.goRouteAvailableInFirstColumn()) { + context.pushInner('innernavigator/search'); + } + } + @override void dispose() { scrollController.removeListener(_onScroll); @@ -831,7 +834,7 @@ class ChatListController extends State return ChatListView( controller: this, bottomNavigationBar: widget.bottomNavigationBar, - onOpenSearchPage: widget.onOpenSearchPage, + onOpenSearchPageInMultipleColumns: onOpenSearchPageInMultipleColumns, onTapBottomNavigation: _onTapBottomNavigation, ); } diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 001541bd2f..e141149380 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -1,16 +1,20 @@ +import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header_style.dart'; +import 'package:fluffychat/pages/search/search.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/swipe_to_dismiss_wrap.dart'; import 'package:fluffychat/widgets/twake_components/twake_header.dart'; import 'package:flutter/material.dart'; class ChatListHeader extends StatelessWidget { final ChatListController controller; - final VoidCallback? onOpenSearchPage; + final VoidCallback? onOpenSearchPageInMultipleColumns; const ChatListHeader({ Key? key, required this.controller, - this.onOpenSearchPage, + this.onOpenSearchPageInMultipleColumns, }) : super(key: key); @override @@ -24,20 +28,51 @@ class ChatListHeader extends StatelessWidget { Container( height: ChatListHeaderStyle.searchBarContainerHeight, padding: ChatListHeaderStyle.searchInputPadding, - child: _normalModeWidgets(context), + child: PlatformInfos.isWeb + ? _normalModeWidgetWeb(context) + : _normalModeWidgetsMobile(context), ), ], ); } - Widget _normalModeWidgets(BuildContext context) { + Widget _normalModeWidgetsMobile(BuildContext context) { + return Row( + children: [ + Expanded( + child: OpenContainer( + openBuilder: (context, _) { + return const SwipeToDismissWrap( + child: Search(), + ); + }, + closedBuilder: (context, action) => TextField( + textInputAction: TextInputAction.search, + enabled: false, + decoration: ChatListHeaderStyle.searchInputDecoration(context), + ), + closedElevation: 0, + transitionDuration: const Duration(milliseconds: 500), + transitionType: ContainerTransitionType.fade, + closedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + ChatListHeaderStyle.searchRadiusBorder, + ), + ), + ), + ), + ], + ); + } + + Widget _normalModeWidgetWeb(BuildContext context) { return Row( children: [ Expanded( child: InkWell( borderRadius: BorderRadius.circular(ChatListHeaderStyle.searchRadiusBorder), - onTap: onOpenSearchPage, + onTap: onOpenSearchPageInMultipleColumns, child: TextField( textInputAction: TextInputAction.search, enabled: false, diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 5fd0bfd90d..e460edaa6d 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -17,7 +17,7 @@ import 'package:matrix/matrix.dart'; class ChatListView extends StatelessWidget { final ChatListController controller; final Widget? bottomNavigationBar; - final VoidCallback? onOpenSearchPage; + final VoidCallback? onOpenSearchPageInMultipleColumns; final ChatListBottomNavigatorBarIcon onTapBottomNavigation; final responsiveUtils = getIt.get(); @@ -26,7 +26,7 @@ class ChatListView extends StatelessWidget { Key? key, required this.controller, this.bottomNavigationBar, - this.onOpenSearchPage, + this.onOpenSearchPageInMultipleColumns, required this.onTapBottomNavigation, }) : super(key: key); @@ -44,7 +44,7 @@ class ChatListView extends StatelessWidget { appBar: PreferredSize( preferredSize: ChatListViewStyle.preferredSizeAppBar(context), child: ChatListHeader( - onOpenSearchPage: onOpenSearchPage, + onOpenSearchPageInMultipleColumns: onOpenSearchPageInMultipleColumns, controller: controller, ), ), diff --git a/lib/pages/search/search_text_field.dart b/lib/pages/search/search_text_field.dart new file mode 100644 index 0000000000..02e2c61e33 --- /dev/null +++ b/lib/pages/search/search_text_field.dart @@ -0,0 +1,53 @@ +import 'package:fluffychat/pages/search/search_view_style.dart'; +import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:fluffychat/pages/dialer/pip/dismiss_keyboard.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class SearchTextField extends StatelessWidget { + final TextEditingController textEditingController; + + const SearchTextField({ + super.key, + required this.textEditingController, + }); + + @override + Widget build(BuildContext context) { + return Material( + borderRadius: BorderRadius.circular(16.0), + child: TextField( + onTapOutside: (event) { + dismissKeyboard(context); + }, + controller: textEditingController, + textInputAction: TextInputAction.search, + enabled: true, + autofocus: true, + decoration: InputDecoration( + filled: true, + contentPadding: SearchViewStyle.contentPaddingAppBar, + fillColor: Theme.of(context).colorScheme.surface, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: SearchViewStyle.borderRadiusTextField, + ), + hintText: L10n.of(context)!.search, + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: Icon( + Icons.search_outlined, + size: SearchViewStyle.searchIconSize, + color: Theme.of(context).colorScheme.onSurface, + ), + suffixIcon: TwakeIconButton( + tooltip: L10n.of(context)!.close, + icon: Icons.close, + onTap: () { + textEditingController.clear(); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/search/search_view.dart b/lib/pages/search/search_view.dart index e74b88a5db..92d307a996 100644 --- a/lib/pages/search/search_view.dart +++ b/lib/pages/search/search_view.dart @@ -1,8 +1,8 @@ import 'package:fluffychat/domain/app_state/search/pre_search_state.dart'; -import 'package:fluffychat/pages/dialer/pip/dismiss_keyboard.dart'; import 'package:fluffychat/pages/search/recent_contacts_banner_widget.dart'; import 'package:fluffychat/pages/search/recent_item_widget.dart'; import 'package:fluffychat/pages/search/search.dart'; +import 'package:fluffychat/pages/search/search_text_field.dart'; import 'package:fluffychat/pages/search/search_view_style.dart'; import 'package:fluffychat/pages/search/server_search_view.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_empty_search.dart'; @@ -184,32 +184,8 @@ class SearchView extends StatelessWidget { ), const SizedBox(width: 4.0), Expanded( - child: TextField( - onTapOutside: (event) { - dismissKeyboard(context); - }, - controller: searchController.textEditingController, - textInputAction: TextInputAction.search, - enabled: true, - autofocus: true, - decoration: InputDecoration( - filled: true, - contentPadding: SearchViewStyle.contentPaddingAppBar, - fillColor: Theme.of(context).colorScheme.surface, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: SearchViewStyle.borderRadiusTextField, - ), - hintText: L10n.of(context)!.search, - floatingLabelBehavior: FloatingLabelBehavior.never, - suffixIcon: TwakeIconButton( - tooltip: L10n.of(context)!.close, - icon: Icons.close, - onTap: () { - searchController.textEditingController.clear(); - }, - ), - ), + child: SearchTextField( + textEditingController: searchController.textEditingController, ), ), ], diff --git a/lib/pages/search/search_view_style.dart b/lib/pages/search/search_view_style.dart index cfcd34b3bb..7f6eb9c700 100644 --- a/lib/pages/search/search_view_style.dart +++ b/lib/pages/search/search_view_style.dart @@ -31,4 +31,6 @@ class SearchViewStyle { Theme.of(context).textTheme.labelLarge?.copyWith( color: LinagoraRefColors.material().neutral[40], ); + + static const double searchIconSize = 24.0; } diff --git a/lib/widgets/swipe_to_dismiss_wrap.dart b/lib/widgets/swipe_to_dismiss_wrap.dart new file mode 100644 index 0000000000..2cc0c8af43 --- /dev/null +++ b/lib/widgets/swipe_to_dismiss_wrap.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class SwipeToDismissWrap extends StatefulWidget { + final Widget child; + + const SwipeToDismissWrap({super.key, required this.child}); + + @override + State createState() => _SwipeToDismissWrapState(); +} + +class _SwipeToDismissWrapState extends State { + bool _swipeInProgress = false; + double _startPosX = 0; + + static const double _swipeStartAreaWidth = 60; + static const double _swipeMinLength = 50; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onHorizontalDragStart: (details) { + if (details.localPosition.dx < _swipeStartAreaWidth) { + _swipeInProgress = true; + _startPosX = details.localPosition.dx; + } + }, + onHorizontalDragUpdate: (details) { + if (_swipeInProgress && + details.localPosition.dx > _startPosX + _swipeMinLength) { + Navigator.of(context).pop(); + } + }, + onHorizontalDragEnd: (_) => _swipeInProgress = false, + child: widget.child, + ); + } +} From 5be826083f622eeb7e0e26549588a8e44bd19b9d Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 27 Mar 2024 15:58:01 +0700 Subject: [PATCH 090/183] TW-1578: clean the unused params and variables (cherry picked from commit 629c2ab0492302a9eb970488330e1aacb19ea8d1) --- lib/pages/search/search.dart | 5 +---- .../app_adaptive_scaffold_body.dart | 12 ------------ .../app_adaptive_scaffold_body_view.dart | 17 ----------------- .../enum/adaptive_destinations_enum.dart | 4 +--- 4 files changed, 2 insertions(+), 36 deletions(-) diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index f9d03d3eb7..8124157bad 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/domain/usecase/search/pre_search_recent_contacts_inte import 'package:fluffychat/pages/search/search_contacts_and_chats_controller.dart'; import 'package:fluffychat/pages/search/search_view.dart'; import 'package:fluffychat/pages/search/server_search_controller.dart'; -import 'package:fluffychat/presentation/mixins/comparable_presentation_search_mixin.dart'; import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; @@ -32,10 +31,8 @@ class Search extends StatefulWidget { State createState() => SearchController(); } -class SearchController extends State - with ComparablePresentationSearchMixin { +class SearchController extends State { static const int limitPrefetchedRecentChats = 3; - static const int limitSearchingPrefetchedRecentContacts = 30; static const int limitPrefetchedRecentContacts = 5; static const _prefixLengthHighlight = 20; diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index 90b149efcf..57d154fd6b 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -11,7 +11,6 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -typedef OnOpenSearchPage = void Function(); typedef OnCloseSearchPage = void Function(); typedef OnClientSelectedSetting = void Function( Object object, @@ -95,15 +94,6 @@ class AppAdaptiveScaffoldBodyController extends State { _jumpToPageByIndex(); } - void _onOpenSearchPage() { - pageController.jumpToPage(AdaptiveDestinationEnum.search.index); - } - - void _onCloseSearchPage() { - activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; - _jumpToPageByIndex(); - } - void _jumpToPageByIndex() { pageController.jumpToPage(activeNavigationBarNotifier.value.index); } @@ -163,8 +153,6 @@ class AppAdaptiveScaffoldBodyController extends State { activeRoomIdNotifier: activeRoomIdNotifier, activeNavigationBarNotifier: activeNavigationBarNotifier, pageController: pageController, - onOpenSearchPage: _onOpenSearchPage, - onCloseSearchPage: _onCloseSearchPage, onDestinationSelected: onDestinationSelected, onClientSelected: clientSelected, onPopInvoked: _onPopInvoked, diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart index b1030db584..772c5d3b5c 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart @@ -2,7 +2,6 @@ import 'package:fluffychat/config/first_column_inner_routes.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/contacts_tab/contacts_tab.dart'; -import 'package:fluffychat/pages/search/search.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart'; @@ -19,8 +18,6 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart' class AppAdaptiveScaffoldBodyView extends StatelessWidget { final List destinations; final ValueNotifier activeNavigationBarNotifier; - final OnOpenSearchPage onOpenSearchPage; - final OnCloseSearchPage onCloseSearchPage; final OnDestinationSelected onDestinationSelected; final OnClientSelectedSetting onClientSelected; final PageController pageController; @@ -43,8 +40,6 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { required this.activeRoomIdNotifier, required this.pageController, required this.activeNavigationBarNotifier, - required this.onOpenSearchPage, - required this.onCloseSearchPage, required this.onDestinationSelected, required this.onClientSelected, required this.destinations, @@ -123,8 +118,6 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { activeNavigationBarNotifier: activeNavigationBarNotifier, pageController: pageController, - onOpenSearchPage: onOpenSearchPage, - onCloseSearchPage: onCloseSearchPage, onDestinationSelected: onDestinationSelected, onClientSelected: onClientSelected, @@ -153,8 +146,6 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { activeRoomIdNotifier: activeRoomIdNotifier, activeNavigationBarNotifier: activeNavigationBarNotifier, pageController: pageController, - onOpenSearchPage: onOpenSearchPage, - onCloseSearchPage: onCloseSearchPage, onDestinationSelected: onDestinationSelected, onClientSelected: onClientSelected, destinations: destinations, @@ -183,8 +174,6 @@ class _ColumnPageView extends StatelessWidget { final List destinations; final ValueNotifier activeNavigationBarNotifier; final PageController pageController; - final OnOpenSearchPage onOpenSearchPage; - final OnCloseSearchPage onCloseSearchPage; final OnDestinationSelected onDestinationSelected; final OnClientSelectedSetting onClientSelected; final ValueKey bottomNavigationKey; @@ -196,8 +185,6 @@ class _ColumnPageView extends StatelessWidget { required this.activeNavigationBarNotifier, required this.activeRoomIdNotifier, required this.pageController, - required this.onOpenSearchPage, - required this.onCloseSearchPage, required this.onDestinationSelected, required this.onClientSelected, required this.destinations, @@ -223,7 +210,6 @@ class _ColumnPageView extends StatelessWidget { navigatorBarType: AdaptiveDestinationEnum.rooms, navigatorBarWidget: _bottomNavigationBarBuilder(context), ), - onOpenSearchPage: onOpenSearchPage, activeRoomIdNotifier: activeRoomIdNotifier, onOpenSettings: onOpenSettings, adaptiveScaffoldBodyArgs: adaptiveScaffoldBodyArgs, @@ -234,9 +220,6 @@ class _ColumnPageView extends StatelessWidget { bottomNavigationBar: _bottomNavigationBarBuilder(context), ), ), - Search( - onCloseSearchPage: onCloseSearchPage, - ), ], ); } diff --git a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart index 0a4113d6f6..7a93b27376 100644 --- a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart +++ b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart @@ -7,8 +7,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; enum AdaptiveDestinationEnum { contacts, rooms, - settings, - search; + settings; NavigationDestination getNavigationDestination(BuildContext context) { switch (this) { @@ -19,7 +18,6 @@ enum AdaptiveDestinationEnum { ), label: L10n.of(context)!.contacts, ); - case AdaptiveDestinationEnum.search: case AdaptiveDestinationEnum.rooms: return NavigationDestination( icon: UnreadRoomsBadge( From d240959da05393598ae9230419899c51566bcfc2 Mon Sep 17 00:00:00 2001 From: Quang Huy Nguyen Date: Wed, 3 Apr 2024 10:15:17 +0700 Subject: [PATCH 091/183] TW-1656: Fix user can't mark as read / unread some chat conversation (#1667) (cherry picked from commit 66bffaeba7acccc87e03e3276c91ee699dd6940e) --- lib/pages/chat_list/chat_list.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 5d4cf4cad8..cbb0720a55 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -273,13 +273,19 @@ class ChatListController extends State Future toggleUnreadSelections() async { await TwakeDialog.showFutureLoadingDialogFullScreen( future: () async { - final markUnread = anySelectedRoomNotMarkedUnread; + final markUnreadAction = anySelectedRoomNotMarkedUnread; for (final conversation in conversationSelectionNotifier.value) { final room = activeClient.getRoomById(conversation.roomId)!; - if (room.markedUnread == markUnread) continue; - await activeClient - .getRoomById(conversation.roomId)! - .markUnread(markUnread); + if (room.markedUnread == markUnreadAction) { + if (room.isUnread) { + await room.setReadMarker( + room.lastEvent!.eventId, + mRead: room.lastEvent!.eventId, + ); + } + } + + await room.markUnread(markUnreadAction); } }, ); From 9e692631201bf6e8479a8e0c50e209c84cc8ee85 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 1 Apr 2024 15:44:48 +0700 Subject: [PATCH 092/183] Hotfix: Can't get file on web (cherry picked from commit af52c264db55dcb45f5f83e6683f7ecb0777144b) --- .../platform_file/platform_file_extension.dart | 12 +++++++++++- lib/pages/chat_draft/draft_chat.dart | 7 +------ lib/presentation/mixins/send_files_mixin.dart | 11 ++--------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/domain/model/extensions/platform_file/platform_file_extension.dart b/lib/domain/model/extensions/platform_file/platform_file_extension.dart index 1be8141af4..74da8aa8f9 100644 --- a/lib/domain/model/extensions/platform_file/platform_file_extension.dart +++ b/lib/domain/model/extensions/platform_file/platform_file_extension.dart @@ -2,7 +2,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:matrix/matrix.dart'; extension PlatformFileListExtension on PlatformFile { - MatrixFile toMatrixFile({ + MatrixFile toMatrixFileOnMobile({ required String temporaryDirectoryPath, }) { return MatrixFile.fromMimeType( @@ -14,6 +14,16 @@ extension PlatformFileListExtension on PlatformFile { ); } + MatrixFile toMatrixFileOnWeb() { + return MatrixFile.fromMimeType( + bytes: bytes, + name: name, + filePath: '', + readStream: readStream, + sizeInBytes: size, + ); + } + FileInfo toFileInfo({ required String temporaryDirectoryPath, }) { diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index 78701a0a5c..e977175043 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -32,7 +32,6 @@ import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; import 'package:linagora_design_flutter/images_picker/images_picker.dart' hide ImagePicker; import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; typedef OnRoomCreatedSuccess = FutureOr Function(Room room)?; @@ -329,13 +328,9 @@ class DraftChatController extends State ); if (result == null || result.files.isEmpty) return; - final temporaryDirectory = await getTemporaryDirectory(); - final matrixFilesList = result.files .map( - (file) => file - .toMatrixFile(temporaryDirectoryPath: temporaryDirectory.path) - .detectFileType, + (file) => file.toMatrixFileOnWeb().detectFileType, ) .toList(); diff --git a/lib/presentation/mixins/send_files_mixin.dart b/lib/presentation/mixins/send_files_mixin.dart index 39ee901e00..1b7fa60c87 100644 --- a/lib/presentation/mixins/send_files_mixin.dart +++ b/lib/presentation/mixins/send_files_mixin.dart @@ -46,7 +46,7 @@ mixin SendFilesMixin { fileInfos ??= result?.files .map( (xFile) => FileInfo.fromMatrixFile( - xFile.toMatrixFile( + xFile.toMatrixFileOnMobile( temporaryDirectoryPath: temporaryDirectory.path, ), ), @@ -65,14 +65,7 @@ mixin SendFilesMixin { withReadStream: true, ); if (result == null || result.files.isEmpty) return []; - final temporaryDirectory = await getTemporaryDirectory(); - return result.files - .map( - (file) => file.toMatrixFile( - temporaryDirectoryPath: temporaryDirectory.path, - ), - ) - .toList(); + return result.files.map((file) => file.toMatrixFileOnWeb()).toList(); } void onPickerTypeClick({ From 898ebbb82d6570beaa1da477fd2477c7d0afe62d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 2 Apr 2024 09:03:17 +0700 Subject: [PATCH 093/183] TW-1662: Upgrade version for `wechat_camera_picker` lib (cherry picked from commit 705e22baec72dac9c76b445210e9fd7492335093) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5733999cf4..7c82ca0008 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -149,7 +149,7 @@ dependencies: flutter_inappwebview: ^5.8.0 tuple: ^2.0.2 lottie: ^2.3.2 - wechat_camera_picker: ^4.0.2 + wechat_camera_picker: 4.2.1 open_file: ^3.3.2 mime: ^1.0.4 async: ^2.11.0 From 4f19b166fab899442ba4f3767c9e8b77e1bd69e3 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 2 Apr 2024 09:04:14 +0700 Subject: [PATCH 094/183] TW-1662: Fix can't send a video or picture by using camera (cherry picked from commit 14bd4699d9f86b2bab3b1793700a87735579d753) --- lib/presentation/mixins/media_picker_mixin.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/presentation/mixins/media_picker_mixin.dart b/lib/presentation/mixins/media_picker_mixin.dart index 4519113b60..07243b2205 100644 --- a/lib/presentation/mixins/media_picker_mixin.dart +++ b/lib/presentation/mixins/media_picker_mixin.dart @@ -253,7 +253,11 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { ], ), cameraWidget: UseCameraWidget( - onPressed: () => _onPressedCamera(context, imagePickerController), + onPressed: () => _onPressedCamera( + context, + imagePickerController, + onCameraPicked, + ), backgroundImage: const AssetImage("assets/verification.png"), ), ); @@ -262,6 +266,7 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { void _onPressedCamera( BuildContext context, ImagePickerGridController imagePickerController, + OnCameraPicked? onCameraPicked, ) async { final currentPermissionMicro = await getCurrentMicroPermission(); final currentPermissionCamera = await getCurrentCameraPermission(); @@ -270,9 +275,7 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { _pickFromCameraAction( context: context, imagePickerGridController: imagePickerController, - onCameraPicked: (assetEntity) { - Navigator.of(context).pop(); - }, + onCameraPicked: onCameraPicked, ); } else { goToSettings( @@ -290,6 +293,9 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { }) async { final assetEntity = await pickMediaFromCameraAction(context: context, onlyImage: onlyImage); + Logs().d( + "MediaPickerMixin::_pickFromCameraAction(): assetEntity - $assetEntity", + ); if (assetEntity != null) { imagePickerGridController.pickAssetFromCamera(assetEntity); From 97b36f67b9c2c9cd99b57041d7636870fb14459e Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 2 Apr 2024 14:29:01 +0700 Subject: [PATCH 095/183] TW-1614: Update border for search result on chat search (cherry picked from commit 529b48c0b9529828511268fded8a1ad812fa7539) --- lib/pages/chat_search/chat_search_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index b7cd1fc1b3..61ea3c69fb 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -217,7 +217,7 @@ class _SearchItem extends StatelessWidget { height: ChatSearchStyle.itemHeight, decoration: BoxDecoration( border: Border( - top: BorderSide( + bottom: BorderSide( color: LinagoraRefColors.material().tertiary[60] ?? Colors.black, width: 1, From 06185556254d993d3959046bef4d0a8981a0f080 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 3 Apr 2024 13:57:34 +0700 Subject: [PATCH 096/183] TW-1497: Filter chat is yet accepted invitation (cherry picked from commit 237df2a7e9c89695c787ff3e7844fe65e7bdb8dc) --- lib/pages/forward/forward.dart | 2 +- lib/pages/share/share.dart | 2 +- lib/presentation/enum/chat_list/chat_list_enum.dart | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/pages/forward/forward.dart b/lib/pages/forward/forward.dart index cb6a5fe257..da3b8afa55 100644 --- a/lib/pages/forward/forward.dart +++ b/lib/pages/forward/forward.dart @@ -75,7 +75,7 @@ class ForwardController extends State with SearchRecentChat { selectedRoomIdNotifier.value = id; } - final ActiveFilter _activeFilterAllChats = ActiveFilter.allChats; + final ActiveFilter _activeFilterAllChats = ActiveFilter.acceptedChats; List get filteredRoomsForAll => Matrix.of(context).client.filteredRoomsForAll(_activeFilterAllChats); diff --git a/lib/pages/share/share.dart b/lib/pages/share/share.dart index 8a102b0038..c70c655f8f 100644 --- a/lib/pages/share/share.dart +++ b/lib/pages/share/share.dart @@ -125,7 +125,7 @@ class ShareController extends State } } - final ActiveFilter _activeFilterAllChats = ActiveFilter.allChats; + final ActiveFilter _activeFilterAllChats = ActiveFilter.acceptedChats; List get filteredRoomsForAll => Matrix.of(context).client.filteredRoomsForAll(_activeFilterAllChats); diff --git a/lib/presentation/enum/chat_list/chat_list_enum.dart b/lib/presentation/enum/chat_list/chat_list_enum.dart index b2bbb73ae3..870738f756 100644 --- a/lib/presentation/enum/chat_list/chat_list_enum.dart +++ b/lib/presentation/enum/chat_list/chat_list_enum.dart @@ -21,6 +21,7 @@ enum PopupMenuAction { enum ActiveFilter { allChats, + acceptedChats, groups, messages, spaces; @@ -37,6 +38,9 @@ enum ActiveFilter { !room.isSpace && room.isDirectChat && !room.isStoryRoom; case ActiveFilter.spaces: return (r) => r.isSpace; + case ActiveFilter.acceptedChats: + return (room) => + !room.isSpace && !room.isStoryRoom && !room.isInvitation; } } } From 2dfbdd230613cc5f834ab8c95bcb373d2a02a7fc Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 3 Apr 2024 17:28:13 +0700 Subject: [PATCH 097/183] TW-1523: Fix chat setting is not align with arrow (cherry picked from commit 96027ce8996389a6a8e121b1a3692a31e3065263) --- .../settings_dashboard/settings/settings_item_builder.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pages/settings_dashboard/settings/settings_item_builder.dart b/lib/pages/settings_dashboard/settings/settings_item_builder.dart index bd40de5ae7..b86fbb4d3f 100644 --- a/lib/pages/settings_dashboard/settings/settings_item_builder.dart +++ b/lib/pages/settings_dashboard/settings/settings_item_builder.dart @@ -48,6 +48,9 @@ class SettingsItemBuilder extends StatelessWidget { ), Expanded( child: Row( + crossAxisAlignment: subtitle.isEmpty + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, children: [ Expanded( child: Column( From 312b6f0964a44faa7f888b0d1ddb4d4aa0c4a326 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 4 Apr 2024 13:52:19 +0700 Subject: [PATCH 098/183] TW-1678: Add cache for Client error responses and Server error responses (cherry picked from commit 9f6d438c6da026911cb70cce6b557a7e5459dc20) --- lib/data/network/dio_cache_option.dart | 3 +- lib/data/network/status_error_code.dart | 44 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 lib/data/network/status_error_code.dart diff --git a/lib/data/network/dio_cache_option.dart b/lib/data/network/dio_cache_option.dart index 2fa3efe992..19d0ec7988 100644 --- a/lib/data/network/dio_cache_option.dart +++ b/lib/data/network/dio_cache_option.dart @@ -1,5 +1,6 @@ import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; +import 'package:fluffychat/data/network/status_error_code.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:matrix/matrix.dart'; @@ -47,7 +48,7 @@ class DioCacheOption { return CacheOptions( store: getIt.get(), policy: CachePolicy.forceCache, - hitCacheOnErrorExcept: [404], + hitCacheOnErrorExcept: HttpResponseStatusCode.errorCodes, keyBuilder: (request) { Logs().d( 'DioCacheOption::getMemCacheOptions() Request URI - ${request.uri}', diff --git a/lib/data/network/status_error_code.dart b/lib/data/network/status_error_code.dart new file mode 100644 index 0000000000..24bd22607f --- /dev/null +++ b/lib/data/network/status_error_code.dart @@ -0,0 +1,44 @@ +class HttpResponseStatusCode { + static const errorCodes = [ + 400, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 411, + 412, + 413, + 414, + 415, + 416, + 417, + 418, + 421, + 422, + 423, + 424, + 425, + 426, + 428, + 429, + 431, + 451, + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 510, + 511, + ]; +} From d62c2de15ff7130ec37dea0967dbbfbd5cec46ec Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 4 Apr 2024 13:54:52 +0700 Subject: [PATCH 099/183] TW-1678: Cache widget for preview link (cherry picked from commit bea755e8e8577d4387cb33bdb4b5d40f578f52d9) --- .../twake_preview_link/twake_link_preview.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart index fed7bc03a3..790dc9f095 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart @@ -38,7 +38,7 @@ class TwakeLinkPreview extends StatefulWidget { } class TwakeLinkPreviewController extends State - with GetPreviewUrlMixin { + with GetPreviewUrlMixin, AutomaticKeepAliveClientMixin { String? get firstValidUrl => widget.localizedBody.getFirstValidUrl(); Uri get uri => Uri.parse(firstValidUrl ?? ''); @@ -60,6 +60,7 @@ class TwakeLinkPreviewController extends State @override Widget build(BuildContext context) { + super.build(context); return TwakeLinkView( key: twakeLinkViewKey, firstValidUrl: firstValidUrl, @@ -181,4 +182,7 @@ class TwakeLinkPreviewController extends State ), ); } + + @override + bool get wantKeepAlive => true; } From 67a9c88ac4ee834d3c3e9a81bb61ce4ebb488c6a Mon Sep 17 00:00:00 2001 From: Nguyen Thai Date: Fri, 5 Apr 2024 16:24:02 +0700 Subject: [PATCH 100/183] Updated fastlane Updated macOS pods (cherry picked from commit f03779ad5d6d77bbde2356661e667f14f0e1c531) --- android/Gemfile.lock | 119 ++++++++++++------------- ios/Gemfile.lock | 84 ++++++++--------- macos/Gemfile.lock | 115 ++++++++++++------------ macos/Podfile.lock | 112 ++++++++++++++++------- macos/Runner.xcodeproj/project.pbxproj | 58 +++++++++++- 5 files changed, 293 insertions(+), 195 deletions(-) diff --git a/android/Gemfile.lock b/android/Gemfile.lock index 6fa396f3a5..99b01e8bf2 100644 --- a/android/Gemfile.lock +++ b/android/Gemfile.lock @@ -1,42 +1,44 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - addressable (2.8.4) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.766.0) - aws-sdk-core (3.173.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.908.0) + aws-sdk-core (3.191.6) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.64.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.122.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.146.1) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.2) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) declarative (0.0.20) - digest-crc (0.6.4) + digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.99.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -65,15 +67,15 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.212.2) + fastimage (2.3.1) + fastlane (2.220.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -85,30 +87,32 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) + multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.42.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.0) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -116,31 +120,29 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.5.2) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) @@ -149,31 +151,32 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.6.3) - jwt (2.7.0) - memoist (0.16.2) + json (2.7.2) + jwt (2.8.1) + base64 mini_magick (4.12.0) - mini_mime (1.1.2) + mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.0.0) + multipart-post (2.4.0) nanaimo (0.3.0) naturally (2.2.1) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.4.0) os (1.1.4) - plist (3.7.0) - public_suffix (5.0.1) - rake (13.0.6) + plist (3.7.1) + public_suffix (5.0.5) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.6) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.17.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -182,21 +185,17 @@ GEM CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (1.8.0) - webrick (1.8.1) + unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.22.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -215,4 +214,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 2.4.10 + 2.5.3 diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index e7325bf198..99b01e8bf2 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -1,29 +1,32 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.864.0) - aws-sdk-core (3.190.0) + aws-partitions (1.908.0) + aws-sdk-core (3.191.6) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.74.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.141.0) - aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-s3 (1.146.1) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -32,10 +35,10 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.105.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -64,15 +67,15 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.217.0) + fastimage (2.3.1) + fastlane (2.220.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -84,6 +87,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -92,10 +96,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -104,11 +108,11 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.53.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -116,24 +120,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) + google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.7.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.0.1) - faraday (>= 1.0, < 3.a) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -148,19 +151,21 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.1) - jwt (2.7.1) + json (2.7.2) + jwt (2.8.1) + base64 mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.0) nanaimo (0.3.0) naturally (2.2.1) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.4.0) os (1.1.4) - plist (3.7.0) - public_suffix (5.0.4) - rake (13.1.0) + plist (3.7.1) + public_suffix (5.0.5) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -170,8 +175,8 @@ GEM rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.18.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -184,14 +189,13 @@ GEM unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) unicode-display_width (2.5.0) - webrick (1.8.1) word_wrap (1.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -210,4 +214,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 2.4.10 + 2.5.3 diff --git a/macos/Gemfile.lock b/macos/Gemfile.lock index 2d7be56577..99b01e8bf2 100644 --- a/macos/Gemfile.lock +++ b/macos/Gemfile.lock @@ -1,29 +1,32 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - addressable (2.8.4) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.784.0) - aws-sdk-core (3.177.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.908.0) + aws-sdk-core (3.191.6) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.70.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.128.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-s3 (1.146.1) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -32,11 +35,10 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.100.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -65,15 +67,15 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.213.0) + fastimage (2.3.1) + fastlane (2.220.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -85,30 +87,32 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.45.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.0) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -116,31 +120,29 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.6.0) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) - memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) @@ -149,31 +151,32 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.6.3) - jwt (2.7.1) - memoist (0.16.2) + json (2.7.2) + jwt (2.8.1) + base64 mini_magick (4.12.0) - mini_mime (1.1.2) + mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.0) nanaimo (0.3.0) naturally (2.2.1) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.4.0) os (1.1.4) - plist (3.7.0) - public_suffix (5.0.1) - rake (13.0.6) + plist (3.7.1) + public_suffix (5.0.5) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.5) + rexml (3.2.6) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.17.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -182,21 +185,17 @@ GEM CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (1.8.0) - webrick (1.8.1) + unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.22.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -215,4 +214,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 2.4.10 + 2.5.3 diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7884c43cd6..674b8d59d8 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -14,6 +14,8 @@ PODS: - FlutterMacOS - emoji_picker_flutter (0.0.1): - FlutterMacOS + - file_saver (0.0.1): + - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS - Firebase/CoreOnly (9.6.0): @@ -39,37 +41,47 @@ PODS: - FlutterMacOS - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - - flutter_web_auth (0.5.0): + - flutter_web_auth_2 (3.0.0): - FlutterMacOS - flutter_webrtc (0.9.36): - FlutterMacOS - WebRTC-SDK (= 114.5735.02) - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - geolocator_apple (1.2.0): - - FlutterMacOS - - GoogleDataTransport (9.2.5): + - FMDB (2.7.10): + - FMDB/standard (= 2.7.10) + - FMDB/standard (2.7.10) + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Environment (7.11.5): + - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.13.0): - GoogleUtilities/Environment - - "GoogleUtilities/NSData+zlib (7.11.5)" + - GoogleUtilities/Privacy + - "GoogleUtilities/NSData+zlib (7.13.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.0) + - irondash_engine_context (0.0.1): + - FlutterMacOS - just_audio (0.0.1): - FlutterMacOS - macos_ui (0.1.0): - FlutterMacOS - macos_window_utils (1.0.0): - FlutterMacOS - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) + - media_kit_libs_macos_video (1.0.4): + - FlutterMacOS + - media_kit_native_event_loop (1.0.0): + - FlutterMacOS + - media_kit_video (0.0.1): + - FlutterMacOS + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -78,10 +90,12 @@ PODS: - photo_manager (2.0.0): - Flutter - FlutterMacOS - - PromisesObjC (2.3.1) - - ReachabilitySwift (5.0.0) + - PromisesObjC (2.4.0) + - ReachabilitySwift (5.2.1) - record_macos (0.2.0): - FlutterMacOS + - screen_brightness_macos (0.1.0): + - FlutterMacOS - share_plus_macos (0.0.1): - FlutterMacOS - shared_preferences_macos (0.0.1): @@ -89,6 +103,8 @@ PODS: - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) + - super_native_extensions (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - video_compress (0.3.0): @@ -98,6 +114,8 @@ PODS: - wakelock_plus (0.0.1): - FlutterMacOS - WebRTC-SDK (114.5735.02) + - window_to_front (0.0.1): + - FlutterMacOS DEPENDENCIES: - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) @@ -107,29 +125,36 @@ DEPENDENCIES: - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) - emoji_picker_flutter (from `Flutter/ephemeral/.symlinks/plugins/emoji_picker_flutter/macos`) + - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - - flutter_web_auth (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth/macos`) + - flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`) + - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`) - macos_ui (from `Flutter/ephemeral/.symlinks/plugins/macos_ui/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) + - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) + - media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`) + - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - photo_manager (from `Flutter/ephemeral/.symlinks/plugins/photo_manager/macos`) - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) + - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + - window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`) SPEC REPOS: trunk: @@ -160,6 +185,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos emoji_picker_flutter: :path: Flutter/ephemeral/.symlinks/plugins/emoji_picker_flutter/macos + file_saver: + :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos file_selector_macos: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos firebase_core: @@ -170,20 +197,26 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_secure_storage_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos - flutter_web_auth: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_web_auth/macos + flutter_web_auth_2: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos flutter_webrtc: :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos FlutterMacOS: :path: Flutter/ephemeral - geolocator_apple: - :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos + irondash_engine_context: + :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos just_audio: :path: Flutter/ephemeral/.symlinks/plugins/just_audio/macos macos_ui: :path: Flutter/ephemeral/.symlinks/plugins/macos_ui/macos macos_window_utils: :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos + media_kit_libs_macos_video: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos + media_kit_native_event_loop: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos + media_kit_video: + :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: @@ -192,12 +225,16 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/photo_manager/macos record_macos: :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos + screen_brightness_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos share_plus_macos: :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos shared_preferences_macos: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + super_native_extensions: + :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos video_compress: @@ -206,6 +243,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos wakelock_plus: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + window_to_front: + :path: Flutter/ephemeral/.symlinks/plugins/window_to_front/macos SPEC CHECKSUMS: audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 @@ -215,6 +254,7 @@ SPEC CHECKSUMS: device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f emoji_picker_flutter: 533634326b1c5de9a181ba14b9758e6dfe967a20 + file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 Firebase: 5ae8b7cf8efce559a653aef0ad95bab3f427c351 firebase_core: 970bc7db019f0985976324d90cdc370527c31461 @@ -224,32 +264,38 @@ SPEC CHECKSUMS: flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: 75c8cadfdba05ca007c0fa4ea0c16e5cf85e521b - flutter_web_auth: f129850adcc025e7136109a53a00aac93ec62076 + flutter_web_auth_2: 2e1dc2d2139973e4723c5286ce247dd590390d70 flutter_webrtc: b7de006ffa89334a52103ea577b6596c437e6b9e FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - geolocator_apple: 821be05bbdb1b49500e029ebcbf2d6acf2dfb966 - GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + FMDB: eae540775bf7d0c87a5af926ae37af69effe5a19 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 macos_ui: 6229a8922cd97bafb7d9636c8eb8dfb0744183ca macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 + media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 + media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 record_macos: 937889e0f2a7a12b6fc14e97a3678e5a18943de6 + screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 shared_preferences_macos: 8b221d457159a85f478c0b9d2f19aeae9feff475 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 video_compress: aebf9865ccccab1f4038be7aa72af525ef2c10d7 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 WebRTC-SDK: dd913fd31cfbf1d43b9a22d83f4c6354c960c623 + window_to_front: 4cdc24ddd8461ad1a55fa06286d6a79d8b29e8d8 PODFILE CHECKSUM: 9b8d08a513b178c33212d1b54cc9e3cba756d95b -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index ecc3a71fb1..4ef387c6e7 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -275,27 +275,52 @@ "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/dynamic_color/dynamic_color.framework", "${BUILT_PRODUCTS_DIR}/emoji_picker_flutter/emoji_picker_flutter.framework", + "${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework", "${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_macos/flutter_secure_storage_macos.framework", - "${BUILT_PRODUCTS_DIR}/flutter_web_auth/flutter_web_auth.framework", + "${BUILT_PRODUCTS_DIR}/flutter_web_auth_2/flutter_web_auth_2.framework", "${BUILT_PRODUCTS_DIR}/flutter_webrtc/flutter_webrtc.framework", - "${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework", + "${BUILT_PRODUCTS_DIR}/irondash_engine_context/irondash_engine_context.framework", "${BUILT_PRODUCTS_DIR}/just_audio/just_audio.framework", "${BUILT_PRODUCTS_DIR}/macos_ui/macos_ui.framework", + "${BUILT_PRODUCTS_DIR}/macos_window_utils/macos_window_utils.framework", + "${BUILT_PRODUCTS_DIR}/media_kit_libs_macos_video/media_kit_libs_macos_video.framework", + "${BUILT_PRODUCTS_DIR}/media_kit_native_event_loop/media_kit_native_event_loop.framework", + "${BUILT_PRODUCTS_DIR}/media_kit_video/media_kit_video.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", "${BUILT_PRODUCTS_DIR}/record_macos/record_macos.framework", + "${BUILT_PRODUCTS_DIR}/screen_brightness_macos/screen_brightness_macos.framework", "${BUILT_PRODUCTS_DIR}/share_plus_macos/share_plus_macos.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences_macos/shared_preferences_macos.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", + "${BUILT_PRODUCTS_DIR}/super_native_extensions/super_native_extensions.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework", "${BUILT_PRODUCTS_DIR}/video_compress/video_compress.framework", "${BUILT_PRODUCTS_DIR}/wakelock_macos/wakelock_macos.framework", + "${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework", + "${BUILT_PRODUCTS_DIR}/window_to_front/window_to_front.framework", "${PODS_XCFRAMEWORKS_BUILD_DIR}/WebRTC-SDK/WebRTC.framework/WebRTC", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Ass.framework/Ass", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Avcodec.framework/Avcodec", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Avfilter.framework/Avfilter", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Avformat.framework/Avformat", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Avutil.framework/Avutil", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Crypto.framework/Crypto", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Freetype.framework/Freetype", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Fribidi.framework/Fribidi", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Harfbuzz.framework/Harfbuzz", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Mpv.framework/Mpv", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Ssl.framework/Ssl", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Swresample.framework/Swresample", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Swscale.framework/Swscale", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Tls.framework/Tls", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Uchardet.framework/Uchardet", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Xml2.framework/Xml2", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( @@ -314,27 +339,52 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dynamic_color.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/emoji_picker_flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_macos.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_auth.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_auth_2.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_webrtc.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/irondash_engine_context.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/just_audio.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/macos_ui.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/macos_window_utils.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_libs_macos_video.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_native_event_loop.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_video.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/record_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/screen_brightness_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/super_native_extensions.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_compress.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/window_to_front.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ass.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avcodec.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avfilter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avformat.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avutil.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Crypto.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Freetype.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Fribidi.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Harfbuzz.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mpv.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swresample.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swscale.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Tls.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Uchardet.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Xml2.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; From c394f76ed766f9f34c58910fbe6abbe044090b0f Mon Sep 17 00:00:00 2001 From: Nguyen Thai Date: Fri, 5 Apr 2024 18:19:30 +0700 Subject: [PATCH 101/183] Bumped macOS dependencies (cherry picked from commit a342227aefa26eeefe2322da5347725f379df0a1) --- macos/Podfile.lock | 44 +++++++++++++++++--------- macos/Runner.xcodeproj/project.pbxproj | 24 +++++++++----- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 674b8d59d8..a8672602d9 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - appkit_ui_element_colors (1.0.0): + - FlutterMacOS - audio_session (0.0.1): - FlutterMacOS - connectivity_plus (0.0.1): @@ -37,6 +39,8 @@ PODS: - "GoogleUtilities/NSData+zlib (~> 7.7)" - flutter_app_badger (1.3.0): - FlutterMacOS + - flutter_image_compress_macos (1.0.0): + - FlutterMacOS - flutter_local_notifications (0.0.1): - FlutterMacOS - flutter_secure_storage_macos (6.1.1): @@ -45,11 +49,8 @@ PODS: - FlutterMacOS - flutter_webrtc (0.9.36): - FlutterMacOS - - WebRTC-SDK (= 114.5735.02) + - WebRTC-SDK (= 114.5735.08) - FlutterMacOS (1.0.0) - - FMDB (2.7.10): - - FMDB/standard (= 2.7.10) - - FMDB/standard (2.7.10) - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) @@ -100,24 +101,28 @@ PODS: - FlutterMacOS - shared_preferences_macos (0.0.1): - FlutterMacOS - - sqflite (0.0.2): + - sqflite (0.0.3): + - Flutter - FlutterMacOS - - FMDB (>= 2.7.5) - super_native_extensions (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - video_compress (0.3.0): - FlutterMacOS + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS - wakelock_macos (0.0.1): - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS - - WebRTC-SDK (114.5735.02) + - WebRTC-SDK (114.5735.08) - window_to_front (0.0.1): - FlutterMacOS DEPENDENCIES: + - appkit_ui_element_colors (from `Flutter/ephemeral/.symlinks/plugins/appkit_ui_element_colors/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) @@ -129,6 +134,7 @@ DEPENDENCIES: - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`) + - flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`) @@ -148,10 +154,11 @@ DEPENDENCIES: - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) + - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - window_to_front (from `Flutter/ephemeral/.symlinks/plugins/window_to_front/macos`) @@ -162,7 +169,6 @@ SPEC REPOS: - FirebaseCore - FirebaseCoreDiagnostics - FirebaseCoreInternal - - FMDB - GoogleDataTransport - GoogleUtilities - nanopb @@ -171,6 +177,8 @@ SPEC REPOS: - WebRTC-SDK EXTERNAL SOURCES: + appkit_ui_element_colors: + :path: Flutter/ephemeral/.symlinks/plugins/appkit_ui_element_colors/macos audio_session: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos connectivity_plus: @@ -193,6 +201,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos flutter_app_badger: :path: Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos + flutter_image_compress_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_secure_storage_macos: @@ -232,13 +242,15 @@ EXTERNAL SOURCES: shared_preferences_macos: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin super_native_extensions: :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos video_compress: :path: Flutter/ephemeral/.symlinks/plugins/video_compress/macos + video_player_avfoundation: + :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin wakelock_macos: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos wakelock_plus: @@ -247,6 +259,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_to_front/macos SPEC CHECKSUMS: + appkit_ui_element_colors: 39bb2d80be3f19b152ccf4c70d5bbe6cba43d74a audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 @@ -262,12 +275,12 @@ SPEC CHECKSUMS: FirebaseCoreDiagnostics: 99a495094b10a57eeb3ae8efa1665700ad0bdaa6 FirebaseCoreInternal: bca76517fe1ed381e989f5e7d8abb0da8d85bed3 flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730 + flutter_image_compress_macos: c26c3c13ea0f28ae6dea4e139b3292e7729f99f1 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: 75c8cadfdba05ca007c0fa4ea0c16e5cf85e521b flutter_web_auth_2: 2e1dc2d2139973e4723c5286ce247dd590390d70 - flutter_webrtc: b7de006ffa89334a52103ea577b6596c437e6b9e + flutter_webrtc: cf7dc44d26cbb5c5f1ae5f583dab545871f287f9 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: eae540775bf7d0c87a5af926ae37af69effe5a19 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 @@ -279,7 +292,7 @@ SPEC CHECKSUMS: media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 @@ -287,13 +300,14 @@ SPEC CHECKSUMS: screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 shared_preferences_macos: 8b221d457159a85f478c0b9d2f19aeae9feff475 - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 video_compress: aebf9865ccccab1f4038be7aa72af525ef2c10d7 + video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 - WebRTC-SDK: dd913fd31cfbf1d43b9a22d83f4c6354c960c623 + WebRTC-SDK: c24d2a6c9f571f2ed42297cb8ffba9557093142b window_to_front: 4cdc24ddd8461ad1a55fa06286d6a79d8b29e8d8 PODFILE CHECKSUM: 9b8d08a513b178c33212d1b54cc9e3cba756d95b diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 4ef387c6e7..6408dba728 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -260,7 +260,6 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", "${BUILT_PRODUCTS_DIR}/FirebaseCoreDiagnostics/FirebaseCoreDiagnostics.framework", "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", @@ -268,6 +267,7 @@ "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", + "${BUILT_PRODUCTS_DIR}/appkit_ui_element_colors/appkit_ui_element_colors.framework", "${BUILT_PRODUCTS_DIR}/audio_session/audio_session.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/desktop_drop/desktop_drop.framework", @@ -278,6 +278,7 @@ "${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework", "${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", + "${BUILT_PRODUCTS_DIR}/flutter_image_compress_macos/flutter_image_compress_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_macos/flutter_secure_storage_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_web_auth_2/flutter_web_auth_2.framework", @@ -301,6 +302,7 @@ "${BUILT_PRODUCTS_DIR}/super_native_extensions/super_native_extensions.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework", "${BUILT_PRODUCTS_DIR}/video_compress/video_compress.framework", + "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework", "${BUILT_PRODUCTS_DIR}/wakelock_macos/wakelock_macos.framework", "${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework", "${BUILT_PRODUCTS_DIR}/window_to_front/window_to_front.framework", @@ -310,21 +312,22 @@ "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Avfilter.framework/Avfilter", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Avformat.framework/Avformat", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Avutil.framework/Avutil", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Crypto.framework/Crypto", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Dav1d.framework/Dav1d", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Freetype.framework/Freetype", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Fribidi.framework/Fribidi", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Harfbuzz.framework/Harfbuzz", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Mbedcrypto.framework/Mbedcrypto", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Mbedtls.framework/Mbedtls", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Mbedx509.framework/Mbedx509", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Mpv.framework/Mpv", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Ssl.framework/Ssl", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Png16.framework/Png16", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Swresample.framework/Swresample", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Swscale.framework/Swscale", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Tls.framework/Tls", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Uchardet.framework/Uchardet", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_macos_video/Xml2.framework/Xml2", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreDiagnostics.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", @@ -332,6 +335,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/appkit_ui_element_colors.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audio_session.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/desktop_drop.framework", @@ -342,6 +346,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_auth_2.framework", @@ -365,6 +370,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/super_native_extensions.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_compress.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/window_to_front.framework", @@ -374,15 +380,17 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avfilter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avformat.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avutil.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Crypto.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Dav1d.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Freetype.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Fribidi.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Harfbuzz.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mbedcrypto.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mbedtls.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mbedx509.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mpv.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Png16.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swresample.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swscale.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Tls.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Uchardet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Xml2.framework", ); From a7ff6ed25cafad27c12c17cc763fd790a1189181 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 5 Apr 2024 16:23:41 +0700 Subject: [PATCH 102/183] Update background for forward screen (cherry picked from commit 4845bfa5ee8b2ae47dd47ff92521a8949cd17e6a) --- lib/pages/forward/recent_chat_list.dart | 59 ++++++++++++------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/lib/pages/forward/recent_chat_list.dart b/lib/pages/forward/recent_chat_list.dart index 796ea4db69..fe40c5d90c 100644 --- a/lib/pages/forward/recent_chat_list.dart +++ b/lib/pages/forward/recent_chat_list.dart @@ -38,41 +38,38 @@ class RecentChatList extends StatelessWidget { itemCount: rooms.length, itemBuilder: (BuildContext context, int index) { final room = rooms[index]; - return Material( + return InkWell( borderRadius: RecentChatListStyle.borderRadiusItem, - child: InkWell( - borderRadius: RecentChatListStyle.borderRadiusItem, - onTap: () => onSelectedChat(room.id), - child: Padding( - padding: RecentChatListStyle.paddingVerticalBetweenItem, - child: Row( - children: [ - Radio( - groupValue: room.id, - value: selectedChat, - onChanged: (value) => onSelectedChat(room.id), + onTap: () => onSelectedChat(room.id), + child: Padding( + padding: RecentChatListStyle.paddingVerticalBetweenItem, + child: Row( + children: [ + Radio( + groupValue: room.id, + value: selectedChat, + onChanged: (value) => onSelectedChat(room.id), + ), + Avatar( + mxContent: room.avatar, + name: room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)!), ), - Avatar( - mxContent: room.avatar, - name: room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + onTap: null, + ), + Expanded( + child: Padding( + padding: + RecentChatListStyle.paddingHorizontalBetweenItem, + child: Column( + children: [ + ChatListItemTitle(room: room), + ChatListItemSubtitle(room: room), + ], ), - onTap: null, ), - Expanded( - child: Padding( - padding: - RecentChatListStyle.paddingHorizontalBetweenItem, - child: Column( - children: [ - ChatListItemTitle(room: room), - ChatListItemSubtitle(room: room), - ], - ), - ), - ), - ], - ), + ), + ], ), ), ); From e77faccec6ab22e8122a07bbe97fc4105a8bb163 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 18 Mar 2024 16:43:30 +0700 Subject: [PATCH 103/183] TW-1573: Add a save file option to app bar actions inside chat (cherry picked from commit 122c9d788d1dee3cb7568767aac4375efa09eb04) --- assets/l10n/intl_en.arb | 4 +- lib/pages/chat/chat.dart | 57 ++++++++++++++++++++++- lib/pages/chat/chat_actions.dart | 80 ++++++++++++++++++++++++++------ lib/pages/chat/chat_view.dart | 51 ++------------------ 4 files changed, 129 insertions(+), 63 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index b528cb9e42..5e7056f871 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3014,5 +3014,7 @@ "showInChat": "Show in chat", "phone": "Phone", "viewProfile": "View profile", - "profileInfo": "Profile info" + "profileInfo": "Profile info", + "saveToDownloads": "Save to Downloads", + "saveToGallery": "Save to Gallery" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 1770181e92..d8a7305eb5 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:fluffychat/pages/chat/chat_actions.dart'; +import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; import 'package:fluffychat/presentation/extensions/event_update_extension.dart'; import 'package:fluffychat/presentation/mixins/handle_clipboard_action_mixin.dart'; import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; @@ -103,7 +104,8 @@ class ChatController extends State GoToDraftChatMixin, PasteImageMixin, HandleClipboardActionMixin, - TwakeContextMenuMixin { + TwakeContextMenuMixin, + MessageContentMixin { final NetworkConnectionService networkConnectionService = getIt.get(); @@ -1770,6 +1772,59 @@ class ChatController extends State } } + List> appBarActionsBuilder() { + final listAction = [ + if (PlatformInfos.isAndroid) ...[ + ChatAppBarActions.saveToGallery, + ChatAppBarActions.saveToDownload, + ], + ChatAppBarActions.info, + ChatAppBarActions.report, + ]; + return listAction + .map( + (action) => PopupMenuItem( + value: action, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + action.getIcon(), + color: action.getColorIcon(context), + ), + Padding( + padding: action.getPaddingTitle(), + child: Text(action.getTitle(context)), + ), + ], + ), + ), + ) + .toList(); + } + + void onSelectedAppBarActions(ChatAppBarActions action) { + switch (action) { + case ChatAppBarActions.saveToGallery: + break; + case ChatAppBarActions.saveToDownload: + break; + case ChatAppBarActions.info: + actionWithClearSelections( + () => showEventInfo( + context, + selectedEvents.single, + ), + ); + break; + case ChatAppBarActions.report: + actionWithClearSelections( + reportEventAction, + ); + break; + } + } + @override void initState() { _initializePinnedEvents(); diff --git a/lib/pages/chat/chat_actions.dart b/lib/pages/chat/chat_actions.dart index 6165a4f296..7b7db74dc0 100644 --- a/lib/pages/chat/chat_actions.dart +++ b/lib/pages/chat/chat_actions.dart @@ -1,22 +1,14 @@ import 'package:fluffychat/pages/chat/chat_actions_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; -enum PickerType { gallery, documents, location, contact } +enum PickerType { + gallery, + documents, + location, + contact; -enum ChatScrollState { - scrolling, - startScroll, - endScroll; - - bool get isScrolling => this == ChatScrollState.scrolling; - - bool get isStartScroll => this == ChatScrollState.startScroll; - - bool get isEndScroll => this == ChatScrollState.endScroll; -} - -extension PickerTypeExtension on PickerType { String getTitle(BuildContext context) { switch (this) { case PickerType.gallery: @@ -69,3 +61,63 @@ extension PickerTypeExtension on PickerType { } } } + +enum ChatScrollState { + scrolling, + startScroll, + endScroll; + + bool get isScrolling => this == ChatScrollState.scrolling; + + bool get isStartScroll => this == ChatScrollState.startScroll; + + bool get isEndScroll => this == ChatScrollState.endScroll; +} + +enum ChatAppBarActions { + info, + report, + saveToDownload, + saveToGallery; + + String getTitle(BuildContext context) { + switch (this) { + case ChatAppBarActions.info: + return L10n.of(context)!.messageInfo; + case ChatAppBarActions.report: + return L10n.of(context)!.reportMessage; + case ChatAppBarActions.saveToDownload: + return L10n.of(context)!.saveToDownloads; + case ChatAppBarActions.saveToGallery: + return L10n.of(context)!.saveToGallery; + } + } + + IconData getIcon() { + switch (this) { + case ChatAppBarActions.info: + return Icons.info_outlined; + case ChatAppBarActions.report: + return Icons.shield_outlined; + case ChatAppBarActions.saveToDownload: + return Icons.download_outlined; + case ChatAppBarActions.saveToGallery: + return Icons.save_outlined; + } + } + + Color getColorIcon(BuildContext context) { + switch (this) { + case ChatAppBarActions.info: + case ChatAppBarActions.saveToDownload: + case ChatAppBarActions.saveToGallery: + return Theme.of(context).colorScheme.onSurface; + case ChatAppBarActions.report: + return LinagoraSysColors.material().errorDark; + } + } + + EdgeInsets getPaddingTitle() { + return const EdgeInsets.only(left: 20); + } +} diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index ff471138e9..a933534014 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_invitation_body.dart'; import 'package:fluffychat/pages/chat/chat_view_body.dart'; @@ -15,8 +16,6 @@ import 'package:linagora_design_flutter/colors/linagora_state_layer.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; -enum _EventContextAction { info, report } - class ChatView extends StatelessWidget with MessageContentMixin { final ChatController controller; @@ -65,51 +64,9 @@ class ChatView extends StatelessWidget with MessageContentMixin { imageSize: ChatViewStyle.appBarIconSize, ), if (controller.selectedEvents.length == 1) - PopupMenuButton<_EventContextAction>( - onSelected: (action) { - switch (action) { - case _EventContextAction.info: - controller.actionWithClearSelections( - () => showEventInfo( - context, - controller.selectedEvents.single, - ), - ); - break; - case _EventContextAction.report: - controller.actionWithClearSelections( - controller.reportEventAction, - ); - break; - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: _EventContextAction.info, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.info_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.messageInfo), - ], - ), - ), - PopupMenuItem( - value: _EventContextAction.report, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.shield_outlined, - color: Colors.red, - ), - const SizedBox(width: 12), - Text(L10n.of(context)!.reportMessage), - ], - ), - ), - ], + PopupMenuButton( + onSelected: controller.onSelectedAppBarActions, + itemBuilder: (context) => controller.appBarActionsBuilder(), ), ], ); From dec6033864fca8326387caba0bdc3e78bf96b207 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 2 Apr 2024 17:03:14 +0700 Subject: [PATCH 104/183] TW-1573: Implement save file to downloads folder (cherry picked from commit 3f5f45e057cfc8a9e57a8c5f790b6d34adbc01e0) --- assets/l10n/intl_en.arb | 16 +- lib/pages/chat/chat.dart | 77 +++++++- .../chat/events/message_download_content.dart | 21 ++- ..._file_to_twake_downloads_folder_mixin.dart | 175 ++++++++++++++++++ lib/utils/dialog/downloading_file_dialog.dart | 127 +++++++++++++ .../dialog/downloading_file_dialog_style.dart | 21 +++ .../exception/downloading_exception.dart | 17 ++ .../save_to_downloads_exception.dart | 10 + .../download_manager/download_manager.dart | 7 +- lib/utils/permission_dialog.dart | 4 +- lib/utils/permission_service.dart | 5 + lib/utils/storage_directory_utils.dart | 40 ++++ pubspec.lock | 8 + pubspec.yaml | 1 + test/files/get_available_file_path_test.dart | 174 +++++++++++++++++ 15 files changed, 686 insertions(+), 17 deletions(-) create mode 100644 lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart create mode 100644 lib/utils/dialog/downloading_file_dialog.dart create mode 100644 lib/utils/dialog/downloading_file_dialog_style.dart create mode 100644 lib/utils/exception/downloading_exception.dart create mode 100644 lib/utils/exception/save_to_downloads_exception.dart create mode 100644 test/files/get_available_file_path_test.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 5e7056f871..84aad83953 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3016,5 +3016,19 @@ "viewProfile": "View profile", "profileInfo": "Profile info", "saveToDownloads": "Save to Downloads", - "saveToGallery": "Save to Gallery" + "saveToGallery": "Save to Gallery", + "fileSavedToDownloads": "File saved to Downloads", + "saveFileToDownloadsError": "Failed to save file to Downloads", + "explainPermissionToDownloadFiles": "To continue, please allow {appName} to access storage permission. This permission is essential for saving file to Downloads folder.", + "@explainPermissionToDownloadFiles": { + "placeholders": { + "appName": { + "type": "String", + "placeholders": { + "count": {} + } + } + } + }, + "downloading": "Downloading" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index d8a7305eb5..0377ecde17 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,17 +1,23 @@ import 'dart:async'; import 'dart:io'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; import 'package:fluffychat/presentation/extensions/event_update_extension.dart'; import 'package:fluffychat/presentation/mixins/handle_clipboard_action_mixin.dart'; import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; +import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; import 'package:fluffychat/presentation/model/chat/view_event_list_ui_state.dart'; import 'package:fluffychat/utils/extension/basic_event_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; +import 'package:fluffychat/utils/permission_dialog.dart'; +import 'package:fluffychat/utils/permission_service.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:fluffychat/utils/extension/global_key_extension.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:universal_html/html.dart' as html; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -105,7 +111,8 @@ class ChatController extends State PasteImageMixin, HandleClipboardActionMixin, TwakeContextMenuMixin, - MessageContentMixin { + MessageContentMixin, + SaveFileToTwakeAndroidDownloadsFolderMixin { final NetworkConnectionService networkConnectionService = getIt.get(); @@ -1775,8 +1782,9 @@ class ChatController extends State List> appBarActionsBuilder() { final listAction = [ if (PlatformInfos.isAndroid) ...[ - ChatAppBarActions.saveToGallery, - ChatAppBarActions.saveToDownload, + if (selectedEvents + .every((event) => event.hasAttachment && !event.isVideoOrImage)) + ChatAppBarActions.saveToDownload, ], ChatAppBarActions.info, ChatAppBarActions.report, @@ -1805,9 +1813,10 @@ class ChatController extends State void onSelectedAppBarActions(ChatAppBarActions action) { switch (action) { - case ChatAppBarActions.saveToGallery: - break; case ChatAppBarActions.saveToDownload: + actionWithClearSelections( + () => saveSelectedEventToDownloadAndroid(), + ); break; case ChatAppBarActions.info: actionWithClearSelections( @@ -1822,7 +1831,65 @@ class ChatController extends State reportEventAction, ); break; + default: + break; + } + } + + void saveSelectedEventToDownloadAndroid() async { + if (selectedEvents.length != 1) { + return; + } + final downloadEvent = selectedEvents.first; + if (await PermissionHandlerService() + .isUserHaveToRequestStoragePermissionAndroid()) { + final permission = await Permission.storage.request(); + + if (permission.isPermanentlyDenied) { + showDialog( + useRootNavigator: false, + context: context, + builder: (_) { + return PermissionDialog( + icon: const Icon(Icons.storage_rounded), + permission: Permission.storage, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToDownloadFiles( + AppConfig.applicationName, + ), + ), + onAcceptButton: () => + PermissionHandlerService().goToSettingsForPermissionActions(), + ); + }, + ); + } + + if (!permission.isGranted) { + Logs().i( + 'Chat::saveSelectedEventToDownloadAndroid():: Permission Denied', + ); + return; + } } + final downloadManager = getIt.get(); + final downloadingStreamSubscription = + downloadManager.getDownloadStateStream( + downloadEvent.eventId, + ); + if (downloadingStreamSubscription == null) { + await handleSaveToDownloadsForFileNotInDownloading( + downloadEvent, + context: context, + ); + return; + } + + handleSaveToDownloadForDownloadingFile( + downloadingStreamSubscription: downloadingStreamSubscription, + event: downloadEvent, + context: context, + ); } @override diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index cc531b5fd5..29a00425cd 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -115,6 +115,18 @@ class _MessageDownloadContentState extends State ); } + void onDownloadFileTap() async { + await checkFileInDownloadsInApp(); + if (downloadFileStateNotifier.value is DownloadedPresentationState) { + return; + } + downloadFileStateNotifier.value = const DownloadingPresentationState(); + downloadManager.download( + event: widget.event, + ); + _trySetupDownloadingStreamSubcription(); + } + @override void dispose() { streamSubscription?.cancel(); @@ -165,14 +177,7 @@ class _MessageDownloadContentState extends State } return InkWell( - onTap: () { - downloadFileStateNotifier.value = - const DownloadingPresentationState(); - downloadManager.download( - event: widget.event, - ); - _trySetupDownloadingStreamSubcription(); - }, + onTap: onDownloadFileTap, child: DownloadFileTileWidget( mimeType: widget.event.mimeType, fileType: filetype, diff --git a/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart b/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart new file mode 100644 index 0000000000..ef66360b25 --- /dev/null +++ b/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart @@ -0,0 +1,175 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/utils/dialog/downloading_file_dialog.dart'; +import 'package:fluffychat/utils/exception/save_to_downloads_exception.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +mixin SaveFileToTwakeAndroidDownloadsFolderMixin { + void handleSaveToDownloadForDownloadingFile({ + required BuildContext context, + required Stream> downloadingStreamSubscription, + required Event event, + }) { + final downloadProgressNotifier = ValueNotifier(0); + StreamSubscription? streamSubscription; + streamSubscription = downloadingStreamSubscription.listen((downloadState) { + _onDownloadingFileStateChange( + event: event, + downloadState: downloadState, + context: context, + downloadProgressNotifier: downloadProgressNotifier, + streamSubscription: streamSubscription, + ); + }); + + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => DownloadingFileDialog( + parentContext: context, + eventId: event.eventId, + downloadProgressNotifier: downloadProgressNotifier, + ), + ); + } + + Future handleSaveToDownloadsForFileNotInDownloading( + Event event, { + required BuildContext context, + }) async { + final filePath = + await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: event.eventId, + fileName: event.filename, + ); + final file = File(filePath); + if (!await file.exists()) { + await _handleWhenFileHaveNotDownloaded(event, context); + return; + } + await handleSaveToDownloadsFolderWhenFileExisted(event, file, context); + } + + Future handleSaveToDownloadsFolderWhenFileExisted( + Event event, + File file, + BuildContext context, + ) async { + try { + final twakeFolder = await StorageDirectoryUtils.instance + .getTwakeDownloadsFolderInDevice(); + if (twakeFolder?.isNotEmpty != true) { + throw SaveToDownloadsException(error: 'Twake folder is empty'); + } + final twakeFilePath = + await StorageDirectoryUtils.instance.getAvailableFilePath( + '$twakeFolder/${event.filename}', + ); + + await File(twakeFilePath).create(recursive: true); + final copiedFile = await file.copy(twakeFilePath); + Logs().d( + 'Chat::saveSelectedEventToDownloadAndroid():: Copied file - ${copiedFile.path}', + ); + TwakeSnackBar.show(context, L10n.of(context)!.fileSavedToDownloads); + } catch (e) { + Logs().e( + 'Chat::saveSelectedEventToDownloadAndroid():: Error - $e', + ); + TwakeSnackBar.show(context, L10n.of(context)!.saveFileToDownloadsError); + } + } + + Future _handleWhenFileHaveNotDownloaded( + Event event, + BuildContext context, + ) async { + Logs().d( + 'Chat::saveSelectedEventToDownloadAndroid():: File not exists', + ); + final downloadStreamController = + StreamController>(); + final cancelDownloadToken = CancelToken(); + StreamSubscription? streamSubcription; + final downloadProgressNotifier = ValueNotifier(0); + streamSubcription = downloadStreamController.stream.listen((downloadState) { + _onDownloadingFileStateChange( + event: event, + downloadState: downloadState, + context: context, + downloadProgressNotifier: downloadProgressNotifier, + streamSubscription: streamSubcription, + ); + }); + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => DownloadingFileDialog( + parentContext: context, + eventId: event.eventId, + downloadProgressNotifier: downloadProgressNotifier, + cancelDownloadToken: cancelDownloadToken, + ), + ); + await event.getFileInfo( + downloadStreamController: downloadStreamController, + cancelToken: cancelDownloadToken, + ); + } + + void _onDownloadingFileStateChange({ + required Event event, + required Either downloadState, + required BuildContext context, + required ValueNotifier downloadProgressNotifier, + required StreamSubscription? streamSubscription, + }) { + downloadState.fold( + (left) { + Logs().e( + 'Chat::saveSelectedEventToDownloadAndroid():: Downloading - $left', + ); + _clear( + downloadProgressNotifier: downloadProgressNotifier, + streamSubscription: streamSubscription, + ); + }, + (right) async { + if (right is DownloadingFileState) { + if (right.total == 0) return null; + downloadProgressNotifier.value = right.receive / right.total; + } else if (right is DownloadNativeFileSuccessState) { + _clear( + downloadProgressNotifier: downloadProgressNotifier, + streamSubscription: streamSubscription, + ); + Navigator.of(context, rootNavigator: true).pop(); + await handleSaveToDownloadsForFileNotInDownloading( + event, + context: context, + ); + } + }, + ); + } + + void _clear({ + ValueNotifier? downloadProgressNotifier, + StreamSubscription? streamSubscription, + }) { + downloadProgressNotifier?.dispose(); + streamSubscription?.cancel(); + } +} diff --git a/lib/utils/dialog/downloading_file_dialog.dart b/lib/utils/dialog/downloading_file_dialog.dart new file mode 100644 index 0000000000..ae3e44d34a --- /dev/null +++ b/lib/utils/dialog/downloading_file_dialog.dart @@ -0,0 +1,127 @@ +import 'package:dio/dio.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/dialog/downloading_file_dialog_style.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class DownloadingFileDialog extends StatelessWidget { + const DownloadingFileDialog({ + super.key, + required this.parentContext, + required this.eventId, + required this.downloadProgressNotifier, + this.cancelDownloadToken, + }); + + final BuildContext parentContext; + + final String eventId; + + final ValueNotifier downloadProgressNotifier; + + final CancelToken? cancelDownloadToken; + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: Container( + width: DownloadingFileDialogStyle.dialogWidth, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular( + DownloadingFileDialogStyle.borderRadiusDialog, + ), + ), + padding: DownloadingFileDialogStyle.paddingDialog, + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.of(parentContext)!.downloading, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontSize: DownloadingFileDialogStyle.titleFontSize, + ), + ), + const SizedBox(height: 16.0), + ValueListenableBuilder( + valueListenable: downloadProgressNotifier, + builder: (context, downloadProgress, child) { + final downloadPercentage = (downloadProgress ?? 0) * 100; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + DownloadingFileDialogStyle.borderRadiusLoading, + ), + child: LinearProgressIndicator( + backgroundColor: DownloadingFileDialogStyle + .backgroundColorLoading, + value: downloadProgress, + ), + ), + const SizedBox(height: 8.0), + Text( + '${downloadPercentage.toStringAsFixed(0)}%', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: DownloadingFileDialogStyle.fontSize, + ), + ), + ], + ); + }, + ), + const SizedBox( + height: 24, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular( + DownloadingFileDialogStyle.borderRadiusDialog, + ), + onTap: () => onCloseTap(context), + child: Padding( + padding: DownloadingFileDialogStyle.paddingButton, + child: Text( + L10n.of(parentContext)!.cancel, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: DownloadingFileDialogStyle.fontSize, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } + + void onCloseTap(BuildContext context) { + if (cancelDownloadToken != null) { + cancelDownloadToken!.cancel(); + } else { + final downloadManager = getIt.get(); + downloadManager.cancelDownload(eventId); + } + Navigator.of(context, rootNavigator: true).pop(); + } +} diff --git a/lib/utils/dialog/downloading_file_dialog_style.dart b/lib/utils/dialog/downloading_file_dialog_style.dart new file mode 100644 index 0000000000..871765281a --- /dev/null +++ b/lib/utils/dialog/downloading_file_dialog_style.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class DownloadingFileDialogStyle { + static const double dialogWidth = 300; + + static const double dialogHeight = 220; + + static const double borderRadiusDialog = 24.0; + + static const double titleFontSize = 20; + + static const double fontSize = 16.0; + + static const double borderRadiusLoading = 8.0; + + static const Color backgroundColorLoading = Colors.white; + + static const EdgeInsetsGeometry paddingDialog = EdgeInsets.all(24.0); + + static const EdgeInsetsGeometry paddingButton = EdgeInsets.all(8.0); +} diff --git a/lib/utils/exception/downloading_exception.dart b/lib/utils/exception/downloading_exception.dart new file mode 100644 index 0000000000..b5717e6b8d --- /dev/null +++ b/lib/utils/exception/downloading_exception.dart @@ -0,0 +1,17 @@ +class DownloadingException implements Exception { + final dynamic error; + + DownloadingException({ + this.error, + }); + + @override + String toString() => error; +} + +class CancelDownloadingException extends DownloadingException { + CancelDownloadingException() + : super( + error: 'User cancel downloading file', + ); +} diff --git a/lib/utils/exception/save_to_downloads_exception.dart b/lib/utils/exception/save_to_downloads_exception.dart new file mode 100644 index 0000000000..d2f71b093b --- /dev/null +++ b/lib/utils/exception/save_to_downloads_exception.dart @@ -0,0 +1,10 @@ +class SaveToDownloadsException implements Exception { + final dynamic error; + + SaveToDownloadsException({ + this.error, + }); + + @override + String toString() => error; +} diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart index 76f577bcb5..3f302d4884 100644 --- a/lib/utils/manager/download_manager/download_manager.dart +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -5,6 +5,7 @@ import 'package:dio/dio.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/exception/downloading_exception.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_info.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/manager/download_manager/downloading_worker_queue.dart'; @@ -32,8 +33,10 @@ class DownloadManager { try { cancelToken.cancel(); _eventIdMapDownloadFileInfo[eventId]?.downloadStateStreamController.add( - const Right( - DownloadFileInitial(), + Left( + DownloadFileFailureState( + exception: CancelDownloadingException(), + ), ), ); } catch (e) { diff --git a/lib/utils/permission_dialog.dart b/lib/utils/permission_dialog.dart index 68a355d8cd..51a3a8511f 100644 --- a/lib/utils/permission_dialog.dart +++ b/lib/utils/permission_dialog.dart @@ -84,7 +84,9 @@ class _PermissionDialogState extends State if (widget.onAcceptButton != null) { widget.onAcceptButton!.call(); } else { - await widget.permission.request(); + await widget.permission.request().then( + (value) => Navigator.of(context).pop(), + ); } }, ), diff --git a/lib/utils/permission_service.dart b/lib/utils/permission_service.dart index f10177b90f..6771cc5183 100644 --- a/lib/utils/permission_service.dart +++ b/lib/utils/permission_service.dart @@ -127,6 +127,11 @@ class PermissionHandlerService { } } + Future isUserHaveToRequestStoragePermissionAndroid() async { + return await _getCurrentAndroidVersion() <= 29 && + !(await Permission.storage.isGranted); + } + void goToSettingsForPermissionActions() { openAppSettings(); } diff --git a/lib/utils/storage_directory_utils.dart b/lib/utils/storage_directory_utils.dart index 4282442da7..b39983e787 100644 --- a/lib/utils/storage_directory_utils.dart +++ b/lib/utils/storage_directory_utils.dart @@ -1,4 +1,10 @@ +import 'dart:io'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/exception/save_to_downloads_exception.dart'; +import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:external_path/external_path.dart'; class StorageDirectoryUtils { StorageDirectoryUtils._(); @@ -34,4 +40,38 @@ class StorageDirectoryUtils { await StorageDirectoryUtils.instance.getDownloadFolderInApp(); return '$downloadInAppFolder/$eventId/$fileName'; } + + Future getTwakeDownloadsFolderInDevice() async { + try { + final downloadPath = await ExternalPath.getExternalStoragePublicDirectory( + ExternalPath.DIRECTORY_DOWNLOADS, + ); + if (downloadPath.isNotEmpty == true) { + return '$downloadPath/${AppConfig.applicationName}'; + } + throw SaveToDownloadsException(error: 'Download path is empty'); + } catch (e) { + Logs().e('StorageDirectoryUtils:: getDownloadFolderDevice: $e'); + } + return null; + } + + Future getAvailableFilePath(String filePath) async { + String availableFilePath = filePath; + final positionOfDot = filePath.lastIndexOf('.'); + String extension = ''; + String fileName = ''; + int counter = 1; + if (positionOfDot != -1) { + extension = filePath.substring(positionOfDot); + fileName = filePath.substring(0, positionOfDot); + } else { + fileName = filePath; + } + while (await File(availableFilePath).exists()) { + availableFilePath = '$fileName ($counter)$extension'; + counter++; + } + return availableFilePath; + } } diff --git a/pubspec.lock b/pubspec.lock index df5136747f..7b04240a69 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -562,6 +562,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + external_path: + dependency: "direct main" + description: + name: external_path + sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4" + url: "https://pub.dev" + source: hosted + version: "1.0.3" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7c82ca0008..affc3e3af1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -173,6 +173,7 @@ dependencies: flutter_slidable: ^3.0.1 skeletonizer: 1.1.0 flutter_portal: 1.1.4 + external_path: 1.0.3 dev_dependencies: build_runner: ^2.3.3 diff --git a/test/files/get_available_file_path_test.dart b/test/files/get_available_file_path_test.dart new file mode 100644 index 0000000000..1849028f4b --- /dev/null +++ b/test/files/get_available_file_path_test.dart @@ -0,0 +1,174 @@ +import 'dart:io'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future createMockFile(String filePath) async { + final file = File('$folderTestName/$filePath'); + await file.create(recursive: true); + return file; +} + +const folderTestName = 'generated'; + +void main() async { + if (PlatformInfos.isWeb) { + return; + } + test("""WHEN exist only one file in the folder, + THEN the function should create another file with format fileName (1) + """, () async { + final file = await createMockFile('file1.pdf'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + await file.delete(); + expect(fileAvailable, '$folderTestName/file1 (1).pdf'); + }); + + test("""WHEN exist only two files in the folder, + THEN the function should create another file with format fileName (2) + """, () async { + final file = await createMockFile('file1.pdf'); + final file1 = await createMockFile('file1 (1).pdf'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + file1.delete(), + ]); + expect(fileAvailable, '$folderTestName/file1 (2).pdf'); + }); + + test("""WHEN exist only two files in the folder, + THEN the function should create another file with format fileName (7) + """, () async { + final file = await createMockFile('file1.pdf'); + final file1 = await createMockFile('file1 (1).pdf'); + final file2 = await createMockFile('file1 (2).pdf'); + final file3 = await createMockFile('file1 (3).pdf'); + final file4 = await createMockFile('file1 (4).pdf'); + final file5 = await createMockFile('file1 (5).pdf'); + final file6 = await createMockFile('file1 (6).pdf'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + file1.delete(), + file2.delete(), + file3.delete(), + file4.delete(), + file5.delete(), + file6.delete(), + ]); + expect(fileAvailable, '$folderTestName/file1 (7).pdf'); + }); + + test("""WHEN exist a file with name already have counter in the folder, + THEN the function should create another file with format fileName (6) (1) + """, () async { + final file = await createMockFile('file1 (6).pdf'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + ]); + expect(fileAvailable, '$folderTestName/file1 (6) (1).pdf'); + }); + + test("""WHEN exist a file that contains dot, + THEN the function should create another file with format fileName (1) + """, () async { + final file = await createMockFile('my.document.v1.pdf'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + ]); + expect(fileAvailable, '$folderTestName/my.document.v1 (1).pdf'); + }); + + test("""WHEN exist a file that contains no dot, + THEN the function should create another file with format fileName (1) + """, () async { + final file = await createMockFile('text'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + ]); + expect(fileAvailable, '$folderTestName/text (1)'); + }); + + test("""WHEN exist a file that start with a dot, + THEN the function should create another file with format fileName (1) + """, () async { + final file = await createMockFile('.DS_Store'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + ]); + expect(fileAvailable, '$folderTestName/ (1).DS_Store'); + }); + + test("""WHEN exist a file that have extension contains multiple dots, + THEN the function should create another file with format fileName (1) + """, () async { + final file = await createMockFile('filename.tar.gz'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + ]); + expect(fileAvailable, '$folderTestName/filename.tar (1).gz'); + }); + + test( + """WHEN exist multiple files which have the same name and different counter, + AND file name exists in the folder are file, file (1) and file (3), + THEN the function should create another file with format fileName (2) + """, () async { + final file = await createMockFile('file.pdf'); + final file1 = await createMockFile('file (1).pdf'); + final file3 = await createMockFile('file (3).pdf'); + final fileAvailable = + await StorageDirectoryUtils.instance.getAvailableFilePath( + file.path, + ); + + await Future.wait([ + file.delete(), + file1.delete(), + file3.delete(), + ]); + expect(fileAvailable, '$folderTestName/file (2).pdf'); + }); + + final testDirectory = Directory(folderTestName); + if (await testDirectory.exists()) { + await testDirectory.delete(); + } +} From 070ed3381ad28939c628815ca97bad6c6558b33e Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 3 Apr 2024 16:45:14 +0700 Subject: [PATCH 105/183] TW-1613: Searching result should be empty when user remove keywords (cherry picked from commit 5db69bf393b9e7d0889990344f55095c4b1d5263) --- lib/pages/search/server_search_controller.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pages/search/server_search_controller.dart b/lib/pages/search/server_search_controller.dart index 69a41a3d39..6543178f92 100644 --- a/lib/pages/search/server_search_controller.dart +++ b/lib/pages/search/server_search_controller.dart @@ -73,6 +73,14 @@ class ServerSearchController with SearchDebouncerMixin { searchResult.fold( (failure) => resetNextBatch(), (success) { + if (!searchTermIsNotEmpty) { + return; + } + + if (success is ServerSearchInitial) { + searchResultsNotifier.value = PresentationServerSideInitial(); + } + if (success is ServerSearchChatSuccess) { updateNextBatch(success.nextBatch); if (success.results?.isEmpty == true) { From dea4511327c3878ec54f89ca58d4191cba4884d7 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 4 Apr 2024 14:46:04 +0700 Subject: [PATCH 106/183] hot-fix: PopScope make the app throw at runtime in android (cherry picked from commit 37ec2c76215ba579d2bade89e46d201628860c7f) --- lib/pages/search/search.dart | 7 -- lib/pages/search/search_view.dart | 166 ++++++++++++++---------------- 2 files changed, 79 insertions(+), 94 deletions(-) diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 8124157bad..3e3728a468 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pages/search/server_search_controller.dart'; import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/scroll_controller_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; @@ -152,12 +151,6 @@ class SearchController extends State { context.go('/rooms/${roomIdResult.result!}'); } - void goToRoomsShellBranch() { - textEditingController.clear(); - widget.onCloseSearchPage?.call(); - context.popInnerAll(); - } - @override void initState() { searchContactAndRecentChatController = diff --git a/lib/pages/search/search_view.dart b/lib/pages/search/search_view.dart index 92d307a996..a0f056e755 100644 --- a/lib/pages/search/search_view.dart +++ b/lib/pages/search/search_view.dart @@ -7,11 +7,11 @@ import 'package:fluffychat/pages/search/search_view_style.dart'; import 'package:fluffychat/pages/search/server_search_view.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_empty_search.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_search.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:fluffychat/widgets/twake_components/twake_loading/center_loading_indicator.dart'; import 'package:flutter/material.dart' hide SearchController; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class SearchView extends StatelessWidget { @@ -27,97 +27,89 @@ class SearchView extends StatelessWidget { preferredSize: Size.fromHeight(SearchViewStyle.toolbarHeightSearch), child: _buildAppBarSearch(context), ), - body: PopScope( - canPop: PlatformInfos.isIOS ? true : false, - onPopInvoked: (didPop) async { - if (PlatformInfos.isAndroid) { - searchController.goToRoomsShellBranch(); - } - }, - child: CustomScrollView( - physics: const ClampingScrollPhysics(), - controller: searchController.scrollController, - slivers: [ - ValueListenableBuilder( - valueListenable: searchController.preSearchRecentContactsNotifier, - builder: (context, value, emptyChild) => - value.fold((failure) => emptyChild!, (success) { - switch (success.runtimeType) { - case PreSearchRecentContactsSuccess: - final data = success as PreSearchRecentContactsSuccess; - return ValueListenableBuilder( - valueListenable: searchController.textEditingController, - builder: (context, textEditingValue, child) { - if (textEditingValue.text.isNotEmpty) { - return emptyChild!; - } - return SliverAppBar( - flexibleSpace: FlexibleSpaceBar( - title: PreSearchRecentContactsContainer( - searchController: searchController, - contactsList: data.users, - ), - titlePadding: - const EdgeInsetsDirectional.only(start: 0.0), + body: CustomScrollView( + physics: const ClampingScrollPhysics(), + controller: searchController.scrollController, + slivers: [ + ValueListenableBuilder( + valueListenable: searchController.preSearchRecentContactsNotifier, + builder: (context, value, emptyChild) => + value.fold((failure) => emptyChild!, (success) { + switch (success.runtimeType) { + case PreSearchRecentContactsSuccess: + final data = success as PreSearchRecentContactsSuccess; + return ValueListenableBuilder( + valueListenable: searchController.textEditingController, + builder: (context, textEditingValue, child) { + if (textEditingValue.text.isNotEmpty) { + return emptyChild!; + } + return SliverAppBar( + flexibleSpace: FlexibleSpaceBar( + title: PreSearchRecentContactsContainer( + searchController: searchController, + contactsList: data.users, ), - toolbarHeight: 112, - backgroundColor: Colors.transparent, - automaticallyImplyLeading: false, - ); - }, - ); - default: - return emptyChild!; - } - }), - child: const SliverToBoxAdapter(), - ), - _RecentChatAndContactsHeader(searchController: searchController), - _recentChatsWidget(), - ValueListenableBuilder( - valueListenable: - searchController.serverSearchController.searchResultsNotifier, - builder: ((context, searchResults, child) { - if (searchResults is PresentationServerSideEmptySearch) { - if (searchController.searchContactAndRecentChatController! - .recentAndContactsNotifier.value.isNotEmpty) { - return child!; - } - return _SearchHeader( - header: L10n.of(context)!.messages, - searchController: searchController, - needShowMore: false, + titlePadding: + const EdgeInsetsDirectional.only(start: 0.0), + ), + toolbarHeight: 112, + backgroundColor: Colors.transparent, + automaticallyImplyLeading: false, + ); + }, ); + default: + return emptyChild!; + } + }), + child: const SliverToBoxAdapter(), + ), + _RecentChatAndContactsHeader(searchController: searchController), + _recentChatsWidget(), + ValueListenableBuilder( + valueListenable: + searchController.serverSearchController.searchResultsNotifier, + builder: ((context, searchResults, child) { + if (searchResults is PresentationServerSideEmptySearch) { + if (searchController.searchContactAndRecentChatController! + .recentAndContactsNotifier.value.isNotEmpty) { + return child!; } + return _SearchHeader( + header: L10n.of(context)!.messages, + searchController: searchController, + needShowMore: false, + ); + } - if (searchResults is PresentationServerSideSearch) { - if (searchResults.searchResults.isEmpty) { - return child!; - } - return _SearchHeader( - header: L10n.of(context)!.messages, - searchController: searchController, - needShowMore: false, - ); + if (searchResults is PresentationServerSideSearch) { + if (searchResults.searchResults.isEmpty) { + return child!; } - return child!; - }), - child: _EmptySliverBox(), - ), - ServerSearchMessagesList(searchController: searchController), - ValueListenableBuilder( - valueListenable: - searchController.serverSearchController.isLoadingMoreNotifier, - builder: (context, isLoadingMore, _) { - return SliverToBoxAdapter( - child: isLoadingMore - ? const CenterLoadingIndicator() - : const SizedBox(), + return _SearchHeader( + header: L10n.of(context)!.messages, + searchController: searchController, + needShowMore: false, ); - }, - ), - ], - ), + } + return child!; + }), + child: _EmptySliverBox(), + ), + ServerSearchMessagesList(searchController: searchController), + ValueListenableBuilder( + valueListenable: + searchController.serverSearchController.isLoadingMoreNotifier, + builder: (context, isLoadingMore, _) { + return SliverToBoxAdapter( + child: isLoadingMore + ? const CenterLoadingIndicator() + : const SizedBox(), + ); + }, + ), + ], ), ); } @@ -179,7 +171,7 @@ class SearchView extends StatelessWidget { TwakeIconButton( tooltip: L10n.of(context)!.back, icon: Icons.arrow_back, - onTap: () => searchController.goToRoomsShellBranch(), + onTap: () => context.pop(), paddingAll: SearchViewStyle.paddingBackButton, ), const SizedBox(width: 4.0), From 847e6274806354aeded66c30624c514f027d01ad Mon Sep 17 00:00:00 2001 From: --global Date: Fri, 5 Apr 2024 14:38:53 +0700 Subject: [PATCH 107/183] fixup! hot-fix: PopScope make the app throw at runtime in android (cherry picked from commit d47dc87ccb93132dd024f181cb3e1bdf5f8217ec) --- lib/pages/search/search_view.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/search/search_view.dart b/lib/pages/search/search_view.dart index a0f056e755..1e747f7bd3 100644 --- a/lib/pages/search/search_view.dart +++ b/lib/pages/search/search_view.dart @@ -11,7 +11,6 @@ import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:fluffychat/widgets/twake_components/twake_loading/center_loading_indicator.dart'; import 'package:flutter/material.dart' hide SearchController; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class SearchView extends StatelessWidget { @@ -171,7 +170,7 @@ class SearchView extends StatelessWidget { TwakeIconButton( tooltip: L10n.of(context)!.back, icon: Icons.arrow_back, - onTap: () => context.pop(), + onTap: () => Navigator.of(context).pop(), paddingAll: SearchViewStyle.paddingBackButton, ), const SizedBox(width: 4.0), From b7c2e4b5582656029b3b89f56789ebf00e9ab5f4 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 5 Apr 2024 16:11:50 +0700 Subject: [PATCH 108/183] TW-1578: Apply download attachments on web using dio (cherry picked from commit 7729d103a57c2759b2c96979b756b47c88fc9887) --- .../{ => media}/download_file_response.dart | 0 lib/data/network/media/media_api.dart | 28 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) rename lib/data/model/{ => media}/download_file_response.dart (100%) diff --git a/lib/data/model/download_file_response.dart b/lib/data/model/media/download_file_response.dart similarity index 100% rename from lib/data/model/download_file_response.dart rename to lib/data/model/media/download_file_response.dart diff --git a/lib/data/network/media/media_api.dart b/lib/data/network/media/media_api.dart index ac67fef7ee..33b8279017 100644 --- a/lib/data/network/media/media_api.dart +++ b/lib/data/network/media/media_api.dart @@ -1,7 +1,8 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:dio/dio.dart'; -import 'package:fluffychat/data/model/download_file_response.dart'; +import 'package:fluffychat/data/model/media/download_file_response.dart'; import 'package:fluffychat/data/model/media/upload_file_json.dart'; import 'package:fluffychat/data/model/media/url_preview_response.dart'; import 'package:fluffychat/data/network/dio_client.dart'; @@ -72,6 +73,31 @@ class MediaAPI { ); } + Future downloadAttachmentWeb({ + required Uri uri, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) async { + final uint8List = await _client + .get( + uri.path, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + options: Options( + responseType: ResponseType.bytes, + ), + ) + .onError((error, stackTrace) { + if (error is DioException && error.type == DioExceptionType.cancel) { + throw CancelRequestException(); + } else { + throw Exception(error); + } + }); + + return uint8List; + } + Future getUrlPreview({ required Uri uri, int? preferredPreviewTime, From 60998d097741b28d69b9a37ebd2ddac87445061a Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 5 Apr 2024 16:13:28 +0700 Subject: [PATCH 109/183] TW-1578: Refactor download attachments on web (cherry picked from commit 542908274f3ee218a408808f1056d07006227714) --- .../download_manager/download_manager.dart | 38 ++-- .../download_file_extension.dart | 188 ++++++++++++++++++ 2 files changed, 210 insertions(+), 16 deletions(-) diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart index 3f302d4884..2530bf2728 100644 --- a/lib/utils/manager/download_manager/download_manager.dart +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -137,17 +137,22 @@ class DownloadManager { required StreamController> streamController, required CancelToken cancelToken, }) { - if (!PlatformInfos.isWeb) { - _addTaskToWorkerQueueNative( - event, - getThumbnail, - streamController, - cancelToken, + if (PlatformInfos.isWeb) { + _addTaskToWorkerQueueWeb( + event: event, + streamController: streamController, + getThumbnail: getThumbnail, + cancelToken: cancelToken, ); return; } - _addTaskToWorkerQueueWeb(event, streamController); + _addTaskToWorkerQueueNative( + event, + getThumbnail, + streamController, + cancelToken, + ); } void _addTaskToWorkerQueueNative( @@ -180,20 +185,21 @@ class DownloadManager { ); } - void _addTaskToWorkerQueueWeb( - Event event, - StreamController> streamController, - ) { + void _addTaskToWorkerQueueWeb({ + required Event event, + required StreamController> streamController, + getThumbnail = false, + required CancelToken cancelToken, + }) { workingQueue.addTask( Task( id: event.eventId, runnable: () async { try { - final matrixFile = await event.downloadAndDecryptAttachment(); - streamController.add( - Right( - DownloadMatrixFileSuccessState(matrixFile: matrixFile), - ), + await event.downloadAttachmentWeb( + getThumbnail: getThumbnail, + downloadStreamController: streamController, + cancelToken: cancelToken, ); } catch (e) { Logs().e('DownloadManager::download(): $e'); diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index ae96fb11a6..7f21682314 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; @@ -286,6 +287,193 @@ extension DownloadFileExtension on Event { ); } + Future downloadAttachmentWeb({ + getThumbnail = false, + required StreamController> + downloadStreamController, + CancelToken? cancelToken, + }) async { + if (!canContainAttachment()) { + throw ( + "downloadAttachmentWeb: This event has the type '$type' and so it can't contain an attachment.", + ); + } + + if (isSending()) { + final localFile = room.sendingFilePlaceholders[eventId]; + if (localFile != null) return localFile; + } + + final mxcUrl = getAttachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail); + if (mxcUrl == null) { + throw "downloadAttachmentWeb: This event hasn't any attachment or thumbnail."; + } + + final isFileEncrypted = + getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted; + if (isEncryptionDisabled(isFileEncrypted)) { + throw ( + 'downloadAttachmentWeb: Encryption is not enabled in your Client.', + ); + } + + final storeable = isFileStoreable(getThumbnail: getThumbnail); + + Uint8List? uint8list; + + if (storeable) { + uint8list = await room.client.database?.getFile(mxcUrl); + } + + if (uint8list != null) { + return MatrixFile( + bytes: await _decryptAttachmentWeb(uint8list: uint8list), + name: body, + ); + } + + return await _handleDownloadAttachmentWeb( + mxcUrl: mxcUrl, + downloadStreamController: downloadStreamController, + getThumbnail: getThumbnail, + cancelToken: cancelToken, + storeable: storeable, + ); + } + + Future _handleDownloadAttachmentWeb({ + required Uri mxcUrl, + required StreamController> + downloadStreamController, + bool getThumbnail = false, + CancelToken? cancelToken, + bool storeable = true, + }) async { + try { + final database = room.client.database; + final mediaAPI = getIt(); + final downloadLink = mxcUrl.getDownloadLink(room.client); + final uint8List = await mediaAPI.downloadAttachmentWeb( + uri: downloadLink, + onReceiveProgress: (receive, total) { + downloadStreamController.add( + Right( + DownloadingFileState( + receive: receive, + total: total, + ), + ), + ); + }, + cancelToken: cancelToken, + ); + if (database != null && + storeable && + uint8List.lengthInBytes < database.maxFileSize) { + await database.storeFile( + mxcUrl, + uint8List, + DateTime.now().millisecondsSinceEpoch, + ); + } + + await _handleDownloadAttachmentWebSuccess( + uint8List, + downloadStreamController, + ); + return MatrixFile(name: body); + } catch (e) { + if (e is CancelRequestException) { + Logs().i("_handleDownloadAttachmentWeb: user cancel the download"); + } + Logs().e("_handleDownloadAttachmentWeb: $e"); + } + return null; + } + + Future _handleDownloadAttachmentWebSuccess( + Uint8List uint8list, + StreamController> streamController, + ) async { + if (isAttachmentEncrypted) { + await _handleDecryptedAttachmentWeb( + streamController: streamController, + uint8list: uint8list, + ); + } else { + streamController.add( + Right( + DownloadMatrixFileSuccessState( + matrixFile: MatrixFile(bytes: uint8list, name: body), + ), + ), + ); + } + return; + } + + Future _handleDecryptedAttachmentWeb({ + required StreamController> streamController, + required Uint8List uint8list, + bool getThumbnail = false, + }) async { + streamController.add( + const Right( + DecryptingFileState(), + ), + ); + try { + final decryptedFile = await _decryptAttachmentWeb( + uint8list: uint8list, + getThumbnail: getThumbnail, + ); + if (decryptedFile == null) { + throw Exception( + '_handleDownloadAttachmentWeb:: decryptedFile is null', + ); + } + streamController.add( + Right( + DownloadMatrixFileSuccessState( + matrixFile: MatrixFile(bytes: decryptedFile, name: body), + ), + ), + ); + } catch (e) { + Logs().e( + '_handleDownloadAttachmentWeb:: $e', + ); + streamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); + } + } + + Future _decryptAttachmentWeb({ + required Uint8List uint8list, + bool getThumbnail = false, + }) async { + final fileMap = getThumbnail ? infoMap['thumbnail_file'] : content['file']; + if (!fileMap['key']['key_ops'].contains('decrypt')) { + throw ("Missing 'decrypt' in 'key_ops'."); + } + final encryptedFile = EncryptedFile( + data: uint8list, + iv: fileMap['iv'], + k: fileMap['key']['k'], + sha256: fileMap['hashes']['sha256'], + ); + final decryptAttachment = + await room.client.nativeImplementations.decryptFile(encryptedFile); + if (decryptAttachment == null) { + throw ('Unable to decrypt file'); + } + + return decryptAttachment; + } + Future getMediaFileInfo({ getThumbnail = false, ProgressCallback? progressCallback, From 18ce2806c66382668ad869fb609107c0e31fa88d Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 5 Apr 2024 16:14:02 +0700 Subject: [PATCH 110/183] TW-1578: Enable cancel token when download attachments on web (cherry picked from commit 93deff64dfcfe91a7a8710df5e2aee31ba4db984) --- lib/data/network/media/media_api.dart | 2 +- .../download_file_web_exception.dart | 10 + .../download_manager/download_manager.dart | 6 +- .../download_file_extension.dart | 188 ---------------- .../download_file_web_extension.dart | 204 ++++++++++++++++++ .../download_file_tile_widget.dart | 3 +- 6 files changed, 218 insertions(+), 195 deletions(-) create mode 100644 lib/utils/exception/download_file_web_exception.dart create mode 100644 lib/utils/matrix_sdk_extensions/download_file_web_extension.dart diff --git a/lib/data/network/media/media_api.dart b/lib/data/network/media/media_api.dart index 33b8279017..66fe80d0ed 100644 --- a/lib/data/network/media/media_api.dart +++ b/lib/data/network/media/media_api.dart @@ -73,7 +73,7 @@ class MediaAPI { ); } - Future downloadAttachmentWeb({ + Future downloadFileWeb({ required Uri uri, CancelToken? cancelToken, ProgressCallback? onReceiveProgress, diff --git a/lib/utils/exception/download_file_web_exception.dart b/lib/utils/exception/download_file_web_exception.dart new file mode 100644 index 0000000000..849ab0859f --- /dev/null +++ b/lib/utils/exception/download_file_web_exception.dart @@ -0,0 +1,10 @@ +class DownloadFileWebException implements Exception { + final dynamic error; + + DownloadFileWebException({ + this.error, + }); + + @override + String toString() => error; +} diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart index 2530bf2728..26205d2c5e 100644 --- a/lib/utils/manager/download_manager/download_manager.dart +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/utils/manager/download_manager/download_file_info.dar import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/manager/download_manager/downloading_worker_queue.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_web_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/task_queue/task.dart'; import 'package:matrix/matrix.dart'; @@ -141,7 +142,6 @@ class DownloadManager { _addTaskToWorkerQueueWeb( event: event, streamController: streamController, - getThumbnail: getThumbnail, cancelToken: cancelToken, ); return; @@ -188,7 +188,6 @@ class DownloadManager { void _addTaskToWorkerQueueWeb({ required Event event, required StreamController> streamController, - getThumbnail = false, required CancelToken cancelToken, }) { workingQueue.addTask( @@ -196,8 +195,7 @@ class DownloadManager { id: event.eventId, runnable: () async { try { - await event.downloadAttachmentWeb( - getThumbnail: getThumbnail, + await event.downloadFileWeb( downloadStreamController: streamController, cancelToken: cancelToken, ); diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 7f21682314..ae96fb11a6 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; @@ -287,193 +286,6 @@ extension DownloadFileExtension on Event { ); } - Future downloadAttachmentWeb({ - getThumbnail = false, - required StreamController> - downloadStreamController, - CancelToken? cancelToken, - }) async { - if (!canContainAttachment()) { - throw ( - "downloadAttachmentWeb: This event has the type '$type' and so it can't contain an attachment.", - ); - } - - if (isSending()) { - final localFile = room.sendingFilePlaceholders[eventId]; - if (localFile != null) return localFile; - } - - final mxcUrl = getAttachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail); - if (mxcUrl == null) { - throw "downloadAttachmentWeb: This event hasn't any attachment or thumbnail."; - } - - final isFileEncrypted = - getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted; - if (isEncryptionDisabled(isFileEncrypted)) { - throw ( - 'downloadAttachmentWeb: Encryption is not enabled in your Client.', - ); - } - - final storeable = isFileStoreable(getThumbnail: getThumbnail); - - Uint8List? uint8list; - - if (storeable) { - uint8list = await room.client.database?.getFile(mxcUrl); - } - - if (uint8list != null) { - return MatrixFile( - bytes: await _decryptAttachmentWeb(uint8list: uint8list), - name: body, - ); - } - - return await _handleDownloadAttachmentWeb( - mxcUrl: mxcUrl, - downloadStreamController: downloadStreamController, - getThumbnail: getThumbnail, - cancelToken: cancelToken, - storeable: storeable, - ); - } - - Future _handleDownloadAttachmentWeb({ - required Uri mxcUrl, - required StreamController> - downloadStreamController, - bool getThumbnail = false, - CancelToken? cancelToken, - bool storeable = true, - }) async { - try { - final database = room.client.database; - final mediaAPI = getIt(); - final downloadLink = mxcUrl.getDownloadLink(room.client); - final uint8List = await mediaAPI.downloadAttachmentWeb( - uri: downloadLink, - onReceiveProgress: (receive, total) { - downloadStreamController.add( - Right( - DownloadingFileState( - receive: receive, - total: total, - ), - ), - ); - }, - cancelToken: cancelToken, - ); - if (database != null && - storeable && - uint8List.lengthInBytes < database.maxFileSize) { - await database.storeFile( - mxcUrl, - uint8List, - DateTime.now().millisecondsSinceEpoch, - ); - } - - await _handleDownloadAttachmentWebSuccess( - uint8List, - downloadStreamController, - ); - return MatrixFile(name: body); - } catch (e) { - if (e is CancelRequestException) { - Logs().i("_handleDownloadAttachmentWeb: user cancel the download"); - } - Logs().e("_handleDownloadAttachmentWeb: $e"); - } - return null; - } - - Future _handleDownloadAttachmentWebSuccess( - Uint8List uint8list, - StreamController> streamController, - ) async { - if (isAttachmentEncrypted) { - await _handleDecryptedAttachmentWeb( - streamController: streamController, - uint8list: uint8list, - ); - } else { - streamController.add( - Right( - DownloadMatrixFileSuccessState( - matrixFile: MatrixFile(bytes: uint8list, name: body), - ), - ), - ); - } - return; - } - - Future _handleDecryptedAttachmentWeb({ - required StreamController> streamController, - required Uint8List uint8list, - bool getThumbnail = false, - }) async { - streamController.add( - const Right( - DecryptingFileState(), - ), - ); - try { - final decryptedFile = await _decryptAttachmentWeb( - uint8list: uint8list, - getThumbnail: getThumbnail, - ); - if (decryptedFile == null) { - throw Exception( - '_handleDownloadAttachmentWeb:: decryptedFile is null', - ); - } - streamController.add( - Right( - DownloadMatrixFileSuccessState( - matrixFile: MatrixFile(bytes: decryptedFile, name: body), - ), - ), - ); - } catch (e) { - Logs().e( - '_handleDownloadAttachmentWeb:: $e', - ); - streamController.add( - Left( - DownloadFileFailureState(exception: e), - ), - ); - } - } - - Future _decryptAttachmentWeb({ - required Uint8List uint8list, - bool getThumbnail = false, - }) async { - final fileMap = getThumbnail ? infoMap['thumbnail_file'] : content['file']; - if (!fileMap['key']['key_ops'].contains('decrypt')) { - throw ("Missing 'decrypt' in 'key_ops'."); - } - final encryptedFile = EncryptedFile( - data: uint8list, - iv: fileMap['iv'], - k: fileMap['key']['k'], - sha256: fileMap['hashes']['sha256'], - ); - final decryptAttachment = - await room.client.nativeImplementations.decryptFile(encryptedFile); - if (decryptAttachment == null) { - throw ('Unable to decrypt file'); - } - - return decryptAttachment; - } - Future getMediaFileInfo({ getThumbnail = false, ProgressCallback? progressCallback, diff --git a/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart new file mode 100644 index 0000000000..6e5de2574a --- /dev/null +++ b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart @@ -0,0 +1,204 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/data/network/media/cancel_exception.dart'; +import 'package:fluffychat/data/network/media/media_api.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/exception/download_file_web_exception.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:matrix/matrix.dart'; + +extension DownloadFileWebExtension on Event { + Future downloadFileWeb({ + required StreamController> + downloadStreamController, + CancelToken? cancelToken, + }) async { + if (!canContainAttachment()) { + throw DownloadFileWebException( + error: + "downloadFileWeb: This event has the type '$type' and so it can't contain an attachment.", + ); + } + + if (isSending()) { + final localFile = room.sendingFilePlaceholders[eventId]; + if (localFile != null) return localFile; + } + + final mxcUrl = getAttachmentOrThumbnailMxcUrl(); + if (mxcUrl == null) { + throw DownloadFileWebException( + error: + "downloadFileWeb: This event hasn't any attachment or thumbnail.", + ); + } + + final isFileEncrypted = isAttachmentEncrypted; + if (isEncryptionDisabled(isFileEncrypted)) { + throw DownloadFileWebException( + error: 'downloadFileWeb: Encryption is not enabled in your Client.', + ); + } + + final storeable = isFileStoreable(); + + Uint8List? uint8list; + + if (storeable) { + uint8list = await room.client.database?.getFile(mxcUrl); + } + + if (uint8list != null) { + return MatrixFile( + bytes: await _decryptAttachmentWeb(uint8list: uint8list), + name: body, + ); + } + + return await _handleDownloadFileWeb( + mxcUrl: mxcUrl, + downloadStreamController: downloadStreamController, + cancelToken: cancelToken, + storeable: storeable, + ); + } + + Future _handleDownloadFileWeb({ + required Uri mxcUrl, + required StreamController> + downloadStreamController, + CancelToken? cancelToken, + bool storeable = true, + }) async { + try { + final database = room.client.database; + final mediaAPI = getIt(); + final downloadLink = mxcUrl.getDownloadLink(room.client); + final uint8List = await mediaAPI.downloadFileWeb( + uri: downloadLink, + onReceiveProgress: (receive, total) { + downloadStreamController.add( + Right( + DownloadingFileState( + receive: receive, + total: total, + ), + ), + ); + }, + cancelToken: cancelToken, + ); + if (database != null && + storeable && + uint8List.lengthInBytes < database.maxFileSize) { + await database.storeFile( + mxcUrl, + uint8List, + DateTime.now().millisecondsSinceEpoch, + ); + } + + await _handleDownloadFileWebSuccess( + uint8List, + downloadStreamController, + ); + return MatrixFile(name: body); + } catch (e) { + if (e is CancelRequestException) { + Logs().i("_handleDownloadFileWeb: user cancel the download"); + } + Logs().e("_handleDownloadFileWeb: $e"); + } + return null; + } + + Future _handleDownloadFileWebSuccess( + Uint8List uint8list, + StreamController> streamController, + ) async { + if (isAttachmentEncrypted) { + await _handleDecryptedFileWeb( + streamController: streamController, + uint8list: uint8list, + ); + } else { + streamController.add( + Right( + DownloadMatrixFileSuccessState( + matrixFile: MatrixFile(bytes: uint8list, name: body), + ), + ), + ); + } + return; + } + + Future _handleDecryptedFileWeb({ + required StreamController> streamController, + required Uint8List uint8list, + }) async { + streamController.add( + const Right( + DecryptingFileState(), + ), + ); + try { + final decryptedFile = await _decryptAttachmentWeb( + uint8list: uint8list, + ); + if (decryptedFile == null) { + throw DownloadFileWebException( + error: '_handleDecryptedFileWeb:: decryptedFile is null', + ); + } + streamController.add( + Right( + DownloadMatrixFileSuccessState( + matrixFile: MatrixFile(bytes: decryptedFile, name: body), + ), + ), + ); + } catch (e) { + Logs().e( + '_handleDecryptedFileWeb:: $e', + ); + streamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); + } + } + + Future _decryptAttachmentWeb({ + required Uint8List uint8list, + }) async { + final dynamic fileMap = content['file']; + if (!fileMap['key']['key_ops'].contains('decrypt')) { + throw DownloadFileWebException( + error: "_decryptAttachmentWeb: Missing 'decrypt' in 'key_ops'.", + ); + } + final encryptedFile = EncryptedFile( + data: uint8list, + iv: fileMap['iv'], + k: fileMap['key']['k'], + sha256: fileMap['hashes']['sha256'], + ); + final decryptAttachment = + await room.client.nativeImplementations.decryptFile(encryptedFile); + if (decryptAttachment == null) { + throw DownloadFileWebException( + error: '_decryptAttachmentWeb: Unable to decrypt file', + ); + } + + return decryptAttachment; + } +} diff --git a/lib/widgets/file_widget/download_file_tile_widget.dart b/lib/widgets/file_widget/download_file_tile_widget.dart index 973cd77578..c74469fb6f 100644 --- a/lib/widgets/file_widget/download_file_tile_widget.dart +++ b/lib/widgets/file_widget/download_file_tile_widget.dart @@ -1,6 +1,5 @@ import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/file_widget/circular_loading_download_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; @@ -78,7 +77,7 @@ class DownloadFileTileWidget extends StatelessWidget { ), ), InkWell( - onTap: PlatformInfos.isWeb ? null : onCancelDownload, + onTap: onCancelDownload, child: Container( width: style.downloadIconSize, decoration: BoxDecoration( From e430c9e8ab56c6a46456887a3f6719be78219adb Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA <31937920+Te-Z@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:59:41 +0200 Subject: [PATCH 111/183] TW-1675: status of user is not correct (#1684) (cherry picked from commit 2a3e3462ac14993b54ff16150b2cd74b5325068e) --- docs/adr/0021-listen-to-presence-status.md | 64 ++++++++++++++++++++++ lib/pages/chat/chat_view.dart | 3 +- lib/utils/room_status_extension.dart | 2 +- pubspec.lock | 2 +- 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 docs/adr/0021-listen-to-presence-status.md diff --git a/docs/adr/0021-listen-to-presence-status.md b/docs/adr/0021-listen-to-presence-status.md new file mode 100644 index 0000000000..ca5c2da4aa --- /dev/null +++ b/docs/adr/0021-listen-to-presence-status.md @@ -0,0 +1,64 @@ +# 21. Listen to presence status + +Date: 2024-04-08 + +## Status + +Accepted + +## Context + +The status of presence of a user on a direct chat was not stable. By this we mean that it changes multiple times in a small timeline (few seconds). +This was because the UI was listening to `onPresenceChanged`'s stream which is updated in a for loop where there can be multiple items. So if there was 6 items in this loop, the status was updated 6 times. + +```dart + /// Callback will be called on presence updates. + final CachedStreamController onPresenceChanged = + CachedStreamController(); + + for (final newPresence in sync.presence ?? []) { + final cachedPresence = CachedPresence.fromMatrixEvent(newPresence); + presences[newPresence.senderId] = cachedPresence; + // ignore: deprecated_member_use_from_same_package + onPresence.add(newPresence); + onPresenceChanged.add(cachedPresence); + } +``` + +## Decision + +To avoid this problem we created a new stream which purpose is to get the status of presence the closest of current time: `onlatestPresenceChanged` . That's the one who should be listened by the UI. + +Here `lastActivePresence` is updated for each items in `sync.presence` list if it is `null` or if the current item's timestamp is after the one in `lastActivePresence`. Then when the loop is over and we are sure to have the right value, we can update `onLatestPresenceChange` with the right value and this way update the UI. + +```dart + /// Callback will be called on presence updates. + final CachedStreamController onPresenceChanged = + CachedStreamController(); + + /// Callback will be called on presence update and return latest value. + final CachedStreamController onlatestPresenceChanged = + CachedStreamController(); + + CachedPresence? lastActivePresence; + + for (final newPresence in sync.presence ?? []) { + final cachedPresence = CachedPresence.fromMatrixEvent(newPresence); + presences[newPresence.senderId] = cachedPresence; + // ignore: deprecated_member_use_from_same_package + onPresence.add(newPresence); + onPresenceChanged.add(cachedPresence); + + if (lastActivePresence == null || + (cachedPresence.lastActiveTimestamp != null && + lastActivePresence.lastActiveTimestamp != null && + cachedPresence.lastActiveTimestamp! + .isAfter(lastActivePresence.lastActiveTimestamp!))) { + lastActivePresence = cachedPresence; + } + } + + if (lastActivePresence != null) { + onlatestPresenceChanged.add(lastActivePresence); + } +``` \ No newline at end of file diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index a933534014..8828d91855 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -127,7 +127,8 @@ class ChatView extends StatelessWidget with MessageContentMixin { sendController: controller.sendController, connectivityResultStream: controller .networkConnectionService - .getStreamInstance(), + .connectivity + .onConnectivityChanged, actions: _appBarActions(context), onPushDetails: controller.onPushDetails, roomName: controller.roomName, diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart index cc4229d164..c0eb53c25e 100644 --- a/lib/utils/room_status_extension.dart +++ b/lib/utils/room_status_extension.dart @@ -12,7 +12,7 @@ extension RoomStatusExtension on Room { client.presences[directChatMatrixID]; Stream get directChatPresenceStream => - client.onPresenceChanged.stream; + client.onlatestPresenceChanged.stream; String getLocalizedStatus(BuildContext context, {CachedPresence? presence}) { if (isDirectChat) { diff --git a/pubspec.lock b/pubspec.lock index 7b04240a69..8a8529f2ef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1607,7 +1607,7 @@ packages: description: path: "." ref: "twake-supported-0.22.6" - resolved-ref: "8f821c1cab2506d13c2266cc8759ffd2b2818770" + resolved-ref: "699db764273c113e9d508a1f2d148067aabc8101" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" From d9e85ac059f1a9f782cc80e5009ebe073e68779a Mon Sep 17 00:00:00 2001 From: Nguyen Thai <39090621+tk-nguyen@users.noreply.github.com> Date: Fri, 12 Apr 2024 06:56:45 +0700 Subject: [PATCH 112/183] Bumped action versions (#1694) (cherry picked from commit 0483b5da6e62dc277a1908a735f2626d75d91681) --- .github/workflows/build.yaml | 14 +++++++------- .github/workflows/gh-pages.yaml | 10 +++++----- .github/workflows/image.yaml | 24 ++++++++++++------------ .github/workflows/release.yaml | 23 ++++++++++++----------- .github/workflows/tests.yaml | 14 +++++++------- 5 files changed, 43 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a0b5f4e74d..b3154eb5e4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -23,7 +23,7 @@ jobs: fail-fast: false steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -40,13 +40,13 @@ jobs: bundler-cache: true working-directory: ${{ matrix.os }} - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} - name: Setup Java if: matrix.os == 'android' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" # See 'Supported distributions' for available options java-version: "11" @@ -78,7 +78,7 @@ jobs: working-directory: ${{ matrix.os }} - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: twake-on-matrix-dev-mobile path: | @@ -100,7 +100,7 @@ jobs: fail-fast: false steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -110,7 +110,7 @@ jobs: cache: true cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} @@ -140,7 +140,7 @@ jobs: shell: bash - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: twake-on-matrix-dev-${{ matrix.os }} path: dist/ diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index 18b5c0c8c3..752a9acc41 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -34,7 +34,7 @@ jobs: cache: true cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} @@ -68,7 +68,7 @@ jobs: echo "URL=https://$GITHUB_REPOSITORY_OWNER.github.io/${GITHUB_REPOSITORY##*/}/$FOLDER" >> $GITHUB_OUTPUT - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: build/web @@ -76,7 +76,7 @@ jobs: destination_dir: "${{ github.event.pull_request.number }}" - name: Find deployment comment - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@v3 id: fc with: comment-author: "github-actions[bot]" @@ -84,7 +84,7 @@ jobs: body-includes: "This PR has been deployed to" - name: Create or update deployment comment - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index d9764b4511..5e4c8555f4 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -24,15 +24,15 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} - name: Docker metadata id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | ${{ github.repository_owner }}/twake-web @@ -41,20 +41,20 @@ jobs: type=ref,event=branch - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true platforms: "linux/amd64,linux/arm64" @@ -78,15 +78,15 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} - name: Docker metadata id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | ${{ github.repository_owner }}/twake-web @@ -96,20 +96,20 @@ jobs: type=raw,value=release - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true platforms: "linux/amd64,linux/arm64" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 308278086a..87059d1728 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -36,7 +36,7 @@ jobs: cache: true cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} @@ -49,7 +49,7 @@ jobs: - name: Setup Java if: matrix.os == 'android' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" # See 'Supported distributions' for available options java-version: "11" @@ -94,7 +94,7 @@ jobs: if: matrix.os == 'android' run: ./scripts/release-android-apk.sh - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: matrix.os == 'android' with: name: twake-release @@ -117,7 +117,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -127,7 +127,7 @@ jobs: cache: true cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} @@ -175,9 +175,9 @@ jobs: esac shell: bash - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: twake-release + name: twake-release-${{ matrix.os }} path: twake-${{ github.ref_name }}* release_github: @@ -191,9 +191,10 @@ jobs: contents: write steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: twake-release + pattern: twake-release* + merge-multiple: true - name: Calculate checksums id: shasum @@ -205,7 +206,7 @@ jobs: echo "$EOF" >> $GITHUB_OUTPUT - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: body: | See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGELOG.md) for full changelogs. diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b4dd40f596..a830f2f320 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup flutter uses: subosito/flutter-action@v2 @@ -28,7 +28,7 @@ jobs: cache: true cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup flutter uses: subosito/flutter-action@v2 @@ -50,7 +50,7 @@ jobs: cache: true cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" - - uses: webfactory/ssh-agent@v0.8.0 + - uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_KEY }} @@ -74,7 +74,7 @@ jobs: # fail-fast: false # steps: # - name: Checkout repo - # uses: actions/checkout@v3 + # uses: actions/checkout@v4 # - name: Setup flutter # uses: subosito/flutter-action@v2 @@ -84,7 +84,7 @@ jobs: # cache: true # - name: Setup Java - # uses: actions/setup-java@v3 + # uses: actions/setup-java@v4 # with: # distribution: "temurin" # java-version: "11" @@ -150,7 +150,7 @@ jobs: # timeout-minutes: 30 # - name: Upload video - # uses: actions/upload-artifact@v3 + # uses: actions/upload-artifact@v4 # with: # name: integration-test-recording # path: video-${{ matrix.flavor }}.mp4 From 5002dee03ff09061a14c51ce29f06487cdf09839 Mon Sep 17 00:00:00 2001 From: Terence Zafindratafa Date: Fri, 5 Apr 2024 09:51:47 +0200 Subject: [PATCH 113/183] fix: popscope on android caused bug on route (cherry picked from commit 4c4c7f76c478ace273100c083b6a7f6ff9ed52fc) --- lib/pages/forward/forward_view.dart | 57 ++++++++++++----------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/lib/pages/forward/forward_view.dart b/lib/pages/forward/forward_view.dart index 27bb6bfbe3..deaed1fd26 100644 --- a/lib/pages/forward/forward_view.dart +++ b/lib/pages/forward/forward_view.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/pages/forward/forward.dart'; import 'package:fluffychat/pages/forward/recent_chat_list.dart'; import 'package:fluffychat/pages/forward/recent_chat_title.dart'; import 'package:fluffychat/pages/forward/forward_view_style.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/twake_components/twake_fab.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/resource/image_paths.dart'; @@ -35,38 +34,30 @@ class ForwardView extends StatelessWidget { textEditingController: controller.searchTextEditingController, ), ), - body: PopScope( - canPop: false, - onPopInvoked: (didPop) async { - if (PlatformInfos.isAndroid) { - controller.popScreen(); - } - }, - child: SingleChildScrollView( - padding: - const EdgeInsetsDirectional.all(ForwardViewStyle.paddingBody), - child: Column( - children: [ - const RecentChatsTitle(), - ValueListenableBuilder>( - valueListenable: controller.recentlyChatsNotifier, - builder: (context, rooms, child) { - if (rooms.isNotEmpty) { - return RecentChatList( - rooms: rooms, - selectedChatNotifier: controller.selectedRoomIdNotifier, - onSelectedChat: (roomId) => - controller.onToggleSelectChat(roomId), - recentChatScrollController: - controller.recentChatScrollController, - ); - } - - return const SizedBox.shrink(); - }, - ), - ], - ), + body: SingleChildScrollView( + padding: + const EdgeInsetsDirectional.all(ForwardViewStyle.paddingBody), + child: Column( + children: [ + const RecentChatsTitle(), + ValueListenableBuilder>( + valueListenable: controller.recentlyChatsNotifier, + builder: (context, rooms, child) { + if (rooms.isNotEmpty) { + return RecentChatList( + rooms: rooms, + selectedChatNotifier: controller.selectedRoomIdNotifier, + onSelectedChat: (roomId) => + controller.onToggleSelectChat(roomId), + recentChatScrollController: + controller.recentChatScrollController, + ); + } + + return const SizedBox.shrink(); + }, + ), + ], ), ), floatingActionButton: ForwardButton( From b5ccad3400382c18ad69aa122ef2920d3c6293b9 Mon Sep 17 00:00:00 2001 From: Terence Zafindratafa Date: Fri, 5 Apr 2024 10:02:00 +0200 Subject: [PATCH 114/183] TW-1621: improve forward screen to dialog (cherry picked from commit c4fd88ae5ae4eb42b780aaf74e039cd4eb0f314e) --- lib/pages/forward/forward_view.dart | 5 +- lib/pages/forward/recent_chat_list.dart | 61 ++++++++++--------- .../image_viewer/media_viewer_app_bar.dart | 2 +- .../media_viewer_app_bar_web.dart | 2 +- .../media_viewer_app_bar_mixin.dart | 41 +++++++++++-- .../media_viewer_app_bar_mixin_style.dart | 4 ++ 6 files changed, 76 insertions(+), 39 deletions(-) rename lib/presentation/mixins/{ => media_viewer_app_bar_mixin}/media_viewer_app_bar_mixin.dart (69%) create mode 100644 lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart diff --git a/lib/pages/forward/forward_view.dart b/lib/pages/forward/forward_view.dart index deaed1fd26..32f62eb70b 100644 --- a/lib/pages/forward/forward_view.dart +++ b/lib/pages/forward/forward_view.dart @@ -35,8 +35,7 @@ class ForwardView extends StatelessWidget { ), ), body: SingleChildScrollView( - padding: - const EdgeInsetsDirectional.all(ForwardViewStyle.paddingBody), + padding: const EdgeInsetsDirectional.all(ForwardViewStyle.paddingBody), child: Column( children: [ const RecentChatsTitle(), @@ -53,7 +52,7 @@ class ForwardView extends StatelessWidget { controller.recentChatScrollController, ); } - + return const SizedBox.shrink(); }, ), diff --git a/lib/pages/forward/recent_chat_list.dart b/lib/pages/forward/recent_chat_list.dart index fe40c5d90c..d92c3486b4 100644 --- a/lib/pages/forward/recent_chat_list.dart +++ b/lib/pages/forward/recent_chat_list.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/pages/forward/recent_chat_list_style.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -38,38 +39,42 @@ class RecentChatList extends StatelessWidget { itemCount: rooms.length, itemBuilder: (BuildContext context, int index) { final room = rooms[index]; - return InkWell( + return Material( borderRadius: RecentChatListStyle.borderRadiusItem, - onTap: () => onSelectedChat(room.id), - child: Padding( - padding: RecentChatListStyle.paddingVerticalBetweenItem, - child: Row( - children: [ - Radio( - groupValue: room.id, - value: selectedChat, - onChanged: (value) => onSelectedChat(room.id), - ), - Avatar( - mxContent: room.avatar, - name: room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), + color: LinagoraRefColors.material().primary[100], + child: InkWell( + borderRadius: RecentChatListStyle.borderRadiusItem, + onTap: () => onSelectedChat(room.id), + child: Padding( + padding: RecentChatListStyle.paddingVerticalBetweenItem, + child: Row( + children: [ + Radio( + groupValue: room.id, + value: selectedChat, + onChanged: (value) => onSelectedChat(room.id), ), - onTap: null, - ), - Expanded( - child: Padding( - padding: - RecentChatListStyle.paddingHorizontalBetweenItem, - child: Column( - children: [ - ChatListItemTitle(room: room), - ChatListItemSubtitle(room: room), - ], + Avatar( + mxContent: room.avatar, + name: room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)!), ), + onTap: null, ), - ), - ], + Expanded( + child: Padding( + padding: + RecentChatListStyle.paddingHorizontalBetweenItem, + child: Column( + children: [ + ChatListItemTitle(room: room), + ChatListItemSubtitle(room: room), + ], + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/pages/image_viewer/media_viewer_app_bar.dart b/lib/pages/image_viewer/media_viewer_app_bar.dart index 1a58e95f9c..1c5802b1d7 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_view.dart'; -import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; +import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/image_viewer/media_viewer_app_bar_web.dart b/lib/pages/image_viewer/media_viewer_app_bar_web.dart index f8db6c5be6..bfe4e216b5 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_web.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_web.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; +import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/resource/image_paths.dart'; diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart similarity index 69% rename from lib/presentation/mixins/media_viewer_app_bar_mixin.dart rename to lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart index 0d734a4e77..b6455da0b1 100644 --- a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart +++ b/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart @@ -1,12 +1,14 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/forward/forward.dart'; import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; +import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart'; import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -29,17 +31,44 @@ mixin MediaViewerAppBarMixin { Event? event, ) async { Matrix.of(context).shareContent = event?.content; - final result = await showDialog( - context: context, - useSafeArea: false, - useRootNavigator: false, - builder: (c) => const Forward(), - ); + final responsive = getIt.get(); + + final result = responsive.isMobile(context) + ? await _showForwardMobileDialog(context) + : await _showForwardWebDialog(context); + if (result is PopResultFromForward) { Navigator.of(context).pop(); } } + Future _showForwardMobileDialog( + BuildContext context, + ) async => + await showDialog( + context: context, + useSafeArea: false, + useRootNavigator: false, + builder: (c) => const Forward(), + ); + + Future _showForwardWebDialog( + BuildContext context, + ) async => + await showDialog( + context: context, + useRootNavigator: false, + builder: (c) => AlertDialog( + backgroundColor: LinagoraRefColors.material().primary[100], + surfaceTintColor: LinagoraRefColors.material().primary[100], + content: const SizedBox( + width: MediaViewerAppBarMixinStyle.fixedForwardActionDialogWidth, + height: MediaViewerAppBarMixinStyle.fixedForwardActionDialogHeight, + child: Forward(), + ), + ), + ); + void showInChat( BuildContext context, Event? event, diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart new file mode 100644 index 0000000000..c25bfcf73c --- /dev/null +++ b/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart @@ -0,0 +1,4 @@ +class MediaViewerAppBarMixinStyle { + static const double fixedForwardActionDialogWidth = 448; + static const double fixedForwardActionDialogHeight = 648; +} From b97a5db602c67aec8c9918150f03930cf75680a7 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Mon, 8 Apr 2024 10:20:44 +0200 Subject: [PATCH 115/183] TW-1621: handle forward to click (cherry picked from commit f5aecf41363efc319456d3cb607d846b7ebb7599) --- lib/pages/forward/forward.dart | 12 ++++++++++++ lib/pages/forward/forward_view.dart | 17 ++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/pages/forward/forward.dart b/lib/pages/forward/forward.dart index da3b8afa55..7ea84e6319 100644 --- a/lib/pages/forward/forward.dart +++ b/lib/pages/forward/forward.dart @@ -11,8 +11,10 @@ import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/presentation/mixins/search_recent_chat_mixin.dart'; import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; @@ -47,6 +49,9 @@ class ForwardController extends State with SearchRecentChat { final ValueNotifier selectedRoomIdNotifier = ValueNotifier(''); + final KeyboardVisibilityController keyboardVisibilityController = + KeyboardVisibilityController(); + @override void initState() { super.initState(); @@ -58,6 +63,13 @@ class ForwardController extends State with SearchRecentChat { ); recentlyChatsNotifier.value = filteredRoomsForAll; }); + if (PlatformInfos.isMobile) { + keyboardVisibilityController.onChange.listen((visible) { + if (!visible) { + isSearchBarShowNotifier.value = false; + } + }); + } } @override diff --git a/lib/pages/forward/forward_view.dart b/lib/pages/forward/forward_view.dart index 32f62eb70b..8a82bd9400 100644 --- a/lib/pages/forward/forward_view.dart +++ b/lib/pages/forward/forward_view.dart @@ -203,13 +203,16 @@ class _ForwardAppBar extends StatelessWidget { ), ); } else { - return Text( - L10n.of(context)!.forwardTo, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - letterSpacing: - ChatAppBarTitleStyle.letterSpacingRoomName, - ), + return GestureDetector( + onTap: () => isSearchBarShowNotifier.value = true, + child: Text( + L10n.of(context)!.forwardTo, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + letterSpacing: + ChatAppBarTitleStyle.letterSpacingRoomName, + ), + ), ); } }, From 3025bcec3a56d33930c82dafe3a4a457752ba6ad Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Mon, 8 Apr 2024 12:52:44 +0200 Subject: [PATCH 116/183] TW-1621: searchable app bar implemented in forward screen (cherry picked from commit a0c154d9cc8849ac3b832d24ead0de3032b25848) --- lib/pages/forward/forward.dart | 33 +- lib/pages/forward/forward_view.dart | 297 +++++++++--------- lib/pages/forward/forward_view_style.dart | 31 +- lib/pages/forward/forward_web_view.dart | 27 ++ lib/pages/forward/forward_web_view_style.dart | 5 + .../image_viewer/media_viewer_app_bar.dart | 2 +- .../media_viewer_app_bar_web.dart | 2 +- .../media_viewer_app_bar_mixin.dart | 39 ++- .../media_viewer_app_bar_mixin_style.dart | 4 - lib/widgets/app_bars/searchable_app_bar.dart | 22 +- 10 files changed, 278 insertions(+), 184 deletions(-) create mode 100644 lib/pages/forward/forward_web_view.dart create mode 100644 lib/pages/forward/forward_web_view_style.dart rename lib/presentation/mixins/{media_viewer_app_bar_mixin => }/media_viewer_app_bar_mixin.dart (76%) delete mode 100644 lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart diff --git a/lib/pages/forward/forward.dart b/lib/pages/forward/forward.dart index 7ea84e6319..bd41acdd0a 100644 --- a/lib/pages/forward/forward.dart +++ b/lib/pages/forward/forward.dart @@ -9,12 +9,11 @@ import 'package:fluffychat/pages/chat/send_file_dialog/send_file_dialog.dart'; import 'package:fluffychat/pages/forward/forward_view.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; +import 'package:fluffychat/presentation/mixins/contacts_view_controller_mixin.dart'; import 'package:fluffychat/presentation/mixins/search_recent_chat_mixin.dart'; import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; @@ -22,20 +21,24 @@ import 'package:fluffychat/widgets/matrix.dart'; class Forward extends StatefulWidget { final String? sendFromRoomId; + final bool? isFullScreen; - const Forward({Key? key, this.sendFromRoomId}) : super(key: key); + const Forward({ + Key? key, + this.sendFromRoomId, + this.isFullScreen = true, + }) : super(key: key); @override ForwardController createState() => ForwardController(); } -class ForwardController extends State with SearchRecentChat { +class ForwardController extends State + with SearchRecentChat, ContactsViewControllerMixin { final _forwardMessageInteractor = getIt.get(); final forwardMessageNotifier = ValueNotifier?>(null); - final isSearchBarShowNotifier = ValueNotifier(false); - StreamSubscription? forwardMessageInteractorStreamSubscription; List? rooms; @@ -44,13 +47,18 @@ class ForwardController extends State with SearchRecentChat { String? get roomId => widget.sendFromRoomId; + bool get isFullScreen => widget.isFullScreen == true; + final AutoScrollController recentChatScrollController = AutoScrollController(); final ValueNotifier selectedRoomIdNotifier = ValueNotifier(''); - final KeyboardVisibilityController keyboardVisibilityController = - KeyboardVisibilityController(); + @override + void closeSearchBar() { + searchTextEditingController.clear(); + super.closeSearchBar(); + } @override void initState() { @@ -63,19 +71,12 @@ class ForwardController extends State with SearchRecentChat { ); recentlyChatsNotifier.value = filteredRoomsForAll; }); - if (PlatformInfos.isMobile) { - keyboardVisibilityController.onChange.listen((visible) { - if (!visible) { - isSearchBarShowNotifier.value = false; - } - }); - } } @override void dispose() { forwardMessageNotifier.dispose(); - isSearchBarShowNotifier.dispose(); + disposeContactsMixin(); recentChatScrollController.dispose(); forwardMessageInteractorStreamSubscription?.cancel(); disposeSearchRecentChat(); diff --git a/lib/pages/forward/forward_view.dart b/lib/pages/forward/forward_view.dart index 8a82bd9400..23da22b3fc 100644 --- a/lib/pages/forward/forward_view.dart +++ b/lib/pages/forward/forward_view.dart @@ -2,19 +2,20 @@ import 'package:dartz/dartz.dart' hide State; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/forward/forward_message_state.dart'; -import 'package:fluffychat/pages/chat/chat_app_bar_title_style.dart'; import 'package:fluffychat/pages/forward/forward.dart'; import 'package:fluffychat/pages/forward/recent_chat_list.dart'; import 'package:fluffychat/pages/forward/recent_chat_title.dart'; import 'package:fluffychat/pages/forward/forward_view_style.dart'; +import 'package:fluffychat/widgets/app_bars/searchable_app_bar.dart'; import 'package:fluffychat/widgets/twake_components/twake_fab.dart'; +import 'package:fluffychat/widgets/twake_components/twake_text_button.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/resource/image_paths.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:linagora_design_flutter/colors/linagora_state_layer.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; class ForwardView extends StatelessWidget { @@ -26,51 +27,169 @@ class ForwardView extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: PreferredSize( - preferredSize: - Size.fromHeight(ForwardViewStyle.preferredAppBarSize(context)), - child: _ForwardAppBar( - isSearchBarShowNotifier: controller.isSearchBarShowNotifier, - sendFromRoomId: controller.sendFromRoomId, + preferredSize: controller.isFullScreen + ? ForwardViewStyle.preferredSize(context) + : ForwardViewStyle.maxPreferredSize(context), + child: SearchableAppBar( + toolbarHeight: ForwardViewStyle.maxToolbarHeight(context), + focusNode: controller.searchFocusNode, + title: L10n.of(context)!.forwardTo, + searchModeNotifier: controller.isSearchModeNotifier, + hintText: L10n.of(context)!.searchContacts, textEditingController: controller.searchTextEditingController, + openSearchBar: controller.openSearchBar, + closeSearchBar: controller.closeSearchBar, + isFullScreen: controller.isFullScreen, ), ), - body: SingleChildScrollView( - padding: const EdgeInsetsDirectional.all(ForwardViewStyle.paddingBody), - child: Column( - children: [ - const RecentChatsTitle(), - ValueListenableBuilder>( - valueListenable: controller.recentlyChatsNotifier, - builder: (context, rooms, child) { - if (rooms.isNotEmpty) { - return RecentChatList( - rooms: rooms, - selectedChatNotifier: controller.selectedRoomIdNotifier, - onSelectedChat: (roomId) => - controller.onToggleSelectChat(roomId), - recentChatScrollController: - controller.recentChatScrollController, - ); - } + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: + const EdgeInsetsDirectional.all(ForwardViewStyle.paddingBody), + child: Column( + children: [ + const RecentChatsTitle(), + ValueListenableBuilder>( + valueListenable: controller.recentlyChatsNotifier, + builder: (context, rooms, child) { + if (rooms.isNotEmpty) { + return RecentChatList( + rooms: rooms, + selectedChatNotifier: + controller.selectedRoomIdNotifier, + onSelectedChat: (roomId) => + controller.onToggleSelectChat(roomId), + recentChatScrollController: + controller.recentChatScrollController, + ); + } - return const SizedBox.shrink(); - }, + return const SizedBox.shrink(); + }, + ), + ], + ), ), - ], - ), + ), + if (!controller.isFullScreen) + _WebActionsButton( + selectedChatNotifier: controller.selectedRoomIdNotifier, + forwardMessageNotifier: controller.forwardMessageNotifier, + forwardAction: controller.forwardAction, + ), + ], ), - floatingActionButton: ForwardButton( - forwardAction: controller.forwardAction, - selectedChatNotifier: controller.selectedRoomIdNotifier, - forwardMessageNotifier: controller.forwardMessageNotifier, + floatingActionButton: controller.isFullScreen + ? _ForwardButton( + forwardAction: controller.forwardAction, + selectedChatNotifier: controller.selectedRoomIdNotifier, + forwardMessageNotifier: controller.forwardMessageNotifier, + ) + : null, + ); + } +} + +class _WebActionsButton extends StatelessWidget { + final ValueNotifier selectedChatNotifier; + + final ValueNotifier?> forwardMessageNotifier; + + final void Function() forwardAction; + + const _WebActionsButton({ + required this.selectedChatNotifier, + required this.forwardMessageNotifier, + required this.forwardAction, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: ForwardViewStyle.webActionsButtonPadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TwakeTextButton( + onTap: () => context.pop(), + message: L10n.of(context)!.cancel, + borderHover: ForwardViewStyle.webActionsButtonBorder, + margin: ForwardViewStyle.webActionsButtonMargin, + buttonDecoration: BoxDecoration( + borderRadius: BorderRadius.circular( + ForwardViewStyle.webActionsButtonBorder, + ), + ), + styleMessage: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraSysColors.material().primary, + ), + ), + const SizedBox(width: 8.0), + ValueListenableBuilder( + valueListenable: selectedChatNotifier, + builder: ((context, selectedChat, child) { + return ValueListenableBuilder?>( + valueListenable: forwardMessageNotifier, + builder: (context, forwardMessageState, child) { + if (forwardMessageState == null) { + return child!; + } else { + return forwardMessageState.fold((failure) => child!, + (success) { + if (success is ForwardMessageLoading) { + return const SizedBox( + height: ForwardViewStyle.bottomBarHeight, + child: Align( + alignment: Alignment.centerRight, + child: TwakeFloatingActionButton( + customIcon: + SizedBox(child: CircularProgressIndicator()), + ), + ), + ); + } else { + return const SizedBox(); + } + }); + } + }, + child: TwakeTextButton( + onTap: forwardAction, + message: L10n.of(context)!.add, + margin: ForwardViewStyle.webActionsButtonMargin, + borderHover: ForwardViewStyle.webActionsButtonBorder, + buttonDecoration: BoxDecoration( + color: selectedChat.isNotEmpty + ? LinagoraSysColors.material().primary + : LinagoraStateLayer( + LinagoraSysColors.material().onSurface, + ).opacityLayer2, + borderRadius: BorderRadius.circular( + ForwardViewStyle.webActionsButtonBorder, + ), + ), + styleMessage: + Theme.of(context).textTheme.labelLarge?.copyWith( + color: selectedChat.isNotEmpty + ? LinagoraSysColors.material().onPrimary + : LinagoraSysColors.material() + .inverseSurface + .withOpacity(0.6), + ), + ), + ); + }), + ), + ], ), ); } } -class ForwardButton extends StatelessWidget { - const ForwardButton({ - super.key, +class _ForwardButton extends StatelessWidget { + const _ForwardButton({ required this.selectedChatNotifier, required this.forwardMessageNotifier, required this.forwardAction, @@ -133,107 +252,3 @@ class ForwardButton extends StatelessWidget { ); } } - -class _ForwardAppBar extends StatelessWidget { - const _ForwardAppBar({ - required this.isSearchBarShowNotifier, - this.sendFromRoomId, - required this.textEditingController, - }); - - final String? sendFromRoomId; - - final ValueNotifier isSearchBarShowNotifier; - - final TextEditingController textEditingController; - - @override - Widget build(BuildContext context) { - return AppBar( - toolbarHeight: ForwardViewStyle.preferredAppBarSize(context), - surfaceTintColor: Colors.transparent, - leadingWidth: double.infinity, - leading: Row( - children: [ - TwakeIconButton( - tooltip: L10n.of(context)!.back, - icon: Icons.arrow_back, - onTap: () { - Matrix.of(context).shareContent = null; - if (sendFromRoomId != null) { - context.go('/rooms/$sendFromRoomId'); - } else { - context.pop(); - } - }, - paddingAll: 8.0, - margin: const EdgeInsets.symmetric(vertical: 12.0), - ), - const SizedBox(width: 8.0), - Expanded( - child: ValueListenableBuilder( - valueListenable: isSearchBarShowNotifier, - builder: (context, isSearchBarShow, child) { - if (isSearchBarShow) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: TextField( - autofocus: true, - controller: textEditingController, - maxLines: 1, - buildCounter: ( - BuildContext context, { - required int currentLength, - required int? maxLength, - required bool isFocused, - }) => - const SizedBox.shrink(), - maxLength: 200, - cursorHeight: 26, - scrollPadding: const EdgeInsets.all(0), - decoration: InputDecoration( - isCollapsed: true, - hintStyle: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: LinagoraRefColors.material().neutral[60], - ), - ), - ), - ); - } else { - return GestureDetector( - onTap: () => isSearchBarShowNotifier.value = true, - child: Text( - L10n.of(context)!.forwardTo, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - letterSpacing: - ChatAppBarTitleStyle.letterSpacingRoomName, - ), - ), - ); - } - }, - ), - ), - ], - ), - actions: [ - TwakeIconButton( - icon: Icons.search, - onTap: () => isSearchBarShowNotifier.value = true, - tooltip: L10n.of(context)!.search, - ), - ], - bottom: PreferredSize( - preferredSize: const Size(double.infinity, 4), - child: Container( - color: Theme.of(context).colorScheme.surfaceTint.withOpacity(0.08), - height: 1, - ), - ), - ); - } -} diff --git a/lib/pages/forward/forward_view_style.dart b/lib/pages/forward/forward_view_style.dart index 5cd085693b..b0c50db712 100644 --- a/lib/pages/forward/forward_view_style.dart +++ b/lib/pages/forward/forward_view_style.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/cupertino.dart'; @@ -5,12 +6,38 @@ import 'package:flutter/cupertino.dart'; class ForwardViewStyle { static ResponsiveUtils responsive = getIt.get(); - static double preferredAppBarSize(BuildContext context) => - responsive.isMobile(context) ? 64 : 80; + static Size preferredSize(BuildContext context) => Size.fromHeight( + AppConfig.toolbarHeight(context), + ); + + static Size maxPreferredSize(BuildContext context) => Size.fromHeight( + maxToolbarHeight(context), + ); + + static double maxToolbarHeight(BuildContext context) => + responsive.isMobile(context) ? 48 : 136; static const double paddingBody = 8.0; static const double bottomBarHeight = 60.0; static const double iconSendSize = 56.0; + + static EdgeInsetsDirectional webActionsButtonPadding = + const EdgeInsetsDirectional.only( + top: 24, + bottom: 16, + start: 16, + end: 16, + ); + + static const double webActionsButtonPaddingAll = 10.0; + + static const double webActionsButtonBorder = 100.0; + + static EdgeInsetsDirectional webActionsButtonMargin = + const EdgeInsetsDirectional.symmetric( + vertical: 10.0, + horizontal: 24.0, + ); } diff --git a/lib/pages/forward/forward_web_view.dart b/lib/pages/forward/forward_web_view.dart new file mode 100644 index 0000000000..34fdb7380e --- /dev/null +++ b/lib/pages/forward/forward_web_view.dart @@ -0,0 +1,27 @@ +import 'package:fluffychat/pages/forward/forward.dart'; +import 'package:fluffychat/pages/forward/forward_web_view_style.dart'; +import 'package:flutter/material.dart'; + +class ForwardWebView extends StatelessWidget { + const ForwardWebView({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: ForwardWebViewStyle.dialogWidth, + height: ForwardWebViewStyle.dialogHeight, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all( + Radius.circular(ForwardWebViewStyle.dialogBorderRadius), + ), + ), + clipBehavior: Clip.antiAlias, + child: const Forward( + isFullScreen: false, + ), + ), + ); + } +} diff --git a/lib/pages/forward/forward_web_view_style.dart b/lib/pages/forward/forward_web_view_style.dart new file mode 100644 index 0000000000..b074fe63ef --- /dev/null +++ b/lib/pages/forward/forward_web_view_style.dart @@ -0,0 +1,5 @@ +class ForwardWebViewStyle { + static const double dialogHeight = 638; + static const double dialogWidth = 448; + static const double dialogBorderRadius = 16.0; +} diff --git a/lib/pages/image_viewer/media_viewer_app_bar.dart b/lib/pages/image_viewer/media_viewer_app_bar.dart index 1c5802b1d7..1a58e95f9c 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_view.dart'; -import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart'; +import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/image_viewer/media_viewer_app_bar_web.dart b/lib/pages/image_viewer/media_viewer_app_bar_web.dart index bfe4e216b5..f8db6c5be6 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_web.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_web.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart'; +import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/resource/image_paths.dart'; diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart similarity index 76% rename from lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart rename to lib/presentation/mixins/media_viewer_app_bar_mixin.dart index b6455da0b1..60d8f02dbd 100644 --- a/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin.dart +++ b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart @@ -1,14 +1,14 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/forward/forward.dart'; +import 'package:fluffychat/pages/forward/forward_web_view.dart'; import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; -import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart'; import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -52,21 +52,38 @@ mixin MediaViewerAppBarMixin { builder: (c) => const Forward(), ); + final forwardSelectionMobileAndTabletKey = + const Key('ForwardSelectionMobileAndTabletKey'); + + final forwardSelectionWebAndDesktopKey = + const Key('ForwardSelectionWebAndDesktopKey'); + Future _showForwardWebDialog( BuildContext context, ) async => await showDialog( context: context, + barrierDismissible: false, + useSafeArea: false, useRootNavigator: false, - builder: (c) => AlertDialog( - backgroundColor: LinagoraRefColors.material().primary[100], - surfaceTintColor: LinagoraRefColors.material().primary[100], - content: const SizedBox( - width: MediaViewerAppBarMixinStyle.fixedForwardActionDialogWidth, - height: MediaViewerAppBarMixinStyle.fixedForwardActionDialogHeight, - child: Forward(), - ), - ), + builder: (context) { + return SlotLayout( + config: { + const WidthPlatformBreakpoint( + begin: ResponsiveUtils.minTabletWidth, + ): SlotLayout.from( + key: forwardSelectionWebAndDesktopKey, + builder: (_) => const ForwardWebView(), + ), + const WidthPlatformBreakpoint( + end: ResponsiveUtils.minTabletWidth, + ): SlotLayout.from( + key: forwardSelectionMobileAndTabletKey, + builder: (_) => const Forward(), + ), + }, + ); + }, ); void showInChat( diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart deleted file mode 100644 index c25bfcf73c..0000000000 --- a/lib/presentation/mixins/media_viewer_app_bar_mixin/media_viewer_app_bar_mixin_style.dart +++ /dev/null @@ -1,4 +0,0 @@ -class MediaViewerAppBarMixinStyle { - static const double fixedForwardActionDialogWidth = 448; - static const double fixedForwardActionDialogHeight = 648; -} diff --git a/lib/widgets/app_bars/searchable_app_bar.dart b/lib/widgets/app_bars/searchable_app_bar.dart index 4e0a43b910..b4a33c2dd6 100644 --- a/lib/widgets/app_bars/searchable_app_bar.dart +++ b/lib/widgets/app_bars/searchable_app_bar.dart @@ -88,12 +88,18 @@ class SearchableAppBar extends StatelessWidget { child: _textFieldBuilder(context), ); } - return Text( - title, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), + return GestureDetector( + onTap: isFullScreen ? openSearchBar : null, + child: Text( + title, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), ); }, ), @@ -101,8 +107,8 @@ class SearchableAppBar extends StatelessWidget { if (isFullScreen) ...[ ValueListenableBuilder( valueListenable: searchModeNotifier, - builder: (context, searchModeNotifier, child) { - if (searchModeNotifier) { + builder: (context, isSearchModeEnabled, child) { + if (isSearchModeEnabled) { return TwakeIconButton( onTap: closeSearchBar, tooltip: L10n.of(context)!.close, From f2e536e5c8275b8a1ef6ae20bf882e0bfaff6739 Mon Sep 17 00:00:00 2001 From: Terence Zafindratafa Date: Tue, 9 Apr 2024 15:55:32 +0200 Subject: [PATCH 117/183] TW-1621: for non fullscreen app bar close button disappear when needed (cherry picked from commit c6a82dc868a688237a4156aef5be9c0cc9e8f8df) --- lib/pages/forward/forward_view.dart | 124 +++++++++--------- lib/widgets/app_bars/searchable_app_bar.dart | 36 +++-- .../app_bars/searchable_app_bar_style.dart | 9 ++ 3 files changed, 96 insertions(+), 73 deletions(-) diff --git a/lib/pages/forward/forward_view.dart b/lib/pages/forward/forward_view.dart index 23da22b3fc..0746ba5f86 100644 --- a/lib/pages/forward/forward_view.dart +++ b/lib/pages/forward/forward_view.dart @@ -30,16 +30,22 @@ class ForwardView extends StatelessWidget { preferredSize: controller.isFullScreen ? ForwardViewStyle.preferredSize(context) : ForwardViewStyle.maxPreferredSize(context), - child: SearchableAppBar( - toolbarHeight: ForwardViewStyle.maxToolbarHeight(context), - focusNode: controller.searchFocusNode, - title: L10n.of(context)!.forwardTo, - searchModeNotifier: controller.isSearchModeNotifier, - hintText: L10n.of(context)!.searchContacts, - textEditingController: controller.searchTextEditingController, - openSearchBar: controller.openSearchBar, - closeSearchBar: controller.closeSearchBar, - isFullScreen: controller.isFullScreen, + child: ValueListenableBuilder?>( + valueListenable: controller.forwardMessageNotifier, + builder: (context, forwardMessageState, child) { + return SearchableAppBar( + toolbarHeight: ForwardViewStyle.maxToolbarHeight(context), + focusNode: controller.searchFocusNode, + title: L10n.of(context)!.forwardTo, + searchModeNotifier: controller.isSearchModeNotifier, + hintText: L10n.of(context)!.searchContacts, + textEditingController: controller.searchTextEditingController, + openSearchBar: controller.openSearchBar, + closeSearchBar: controller.closeSearchBar, + isFullScreen: controller.isFullScreen, + displayBackButton: forwardMessageState == null, + ); + }, ), ), body: Column( @@ -109,53 +115,53 @@ class _WebActionsButton extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: ForwardViewStyle.webActionsButtonPadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TwakeTextButton( - onTap: () => context.pop(), - message: L10n.of(context)!.cancel, - borderHover: ForwardViewStyle.webActionsButtonBorder, - margin: ForwardViewStyle.webActionsButtonMargin, - buttonDecoration: BoxDecoration( - borderRadius: BorderRadius.circular( - ForwardViewStyle.webActionsButtonBorder, - ), - ), - styleMessage: Theme.of(context).textTheme.labelLarge?.copyWith( - color: LinagoraSysColors.material().primary, - ), - ), - const SizedBox(width: 8.0), - ValueListenableBuilder( - valueListenable: selectedChatNotifier, - builder: ((context, selectedChat, child) { - return ValueListenableBuilder?>( - valueListenable: forwardMessageNotifier, - builder: (context, forwardMessageState, child) { - if (forwardMessageState == null) { - return child!; + child: ValueListenableBuilder( + valueListenable: selectedChatNotifier, + builder: ((context, selectedChat, child) { + return ValueListenableBuilder?>( + valueListenable: forwardMessageNotifier, + builder: (context, forwardMessageState, child) { + if (forwardMessageState == null) { + return child!; + } else { + return forwardMessageState.fold((failure) => child!, (success) { + if (success is ForwardMessageLoading) { + return const SizedBox( + height: ForwardViewStyle.bottomBarHeight, + child: Align( + alignment: Alignment.centerRight, + child: TwakeFloatingActionButton( + customIcon: + SizedBox(child: CircularProgressIndicator()), + ), + ), + ); } else { - return forwardMessageState.fold((failure) => child!, - (success) { - if (success is ForwardMessageLoading) { - return const SizedBox( - height: ForwardViewStyle.bottomBarHeight, - child: Align( - alignment: Alignment.centerRight, - child: TwakeFloatingActionButton( - customIcon: - SizedBox(child: CircularProgressIndicator()), - ), - ), - ); - } else { - return const SizedBox(); - } - }); + return const SizedBox(); } - }, - child: TwakeTextButton( + }); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TwakeTextButton( + onTap: () => context.pop(), + message: L10n.of(context)!.cancel, + borderHover: ForwardViewStyle.webActionsButtonBorder, + margin: ForwardViewStyle.webActionsButtonMargin, + buttonDecoration: BoxDecoration( + borderRadius: BorderRadius.circular( + ForwardViewStyle.webActionsButtonBorder, + ), + ), + styleMessage: + Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraSysColors.material().primary, + ), + ), + const SizedBox(width: 8.0), + TwakeTextButton( onTap: forwardAction, message: L10n.of(context)!.add, margin: ForwardViewStyle.webActionsButtonMargin, @@ -179,10 +185,10 @@ class _WebActionsButton extends StatelessWidget { .withOpacity(0.6), ), ), - ); - }), - ), - ], + ], + ), + ); + }), ), ); } diff --git a/lib/widgets/app_bars/searchable_app_bar.dart b/lib/widgets/app_bars/searchable_app_bar.dart index b4a33c2dd6..5f64d4e6cb 100644 --- a/lib/widgets/app_bars/searchable_app_bar.dart +++ b/lib/widgets/app_bars/searchable_app_bar.dart @@ -9,6 +9,7 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class SearchableAppBar extends StatelessWidget { final ValueNotifier searchModeNotifier; + final bool displayBackButton; final FocusNode focusNode; final String title; final String? hintText; @@ -29,6 +30,7 @@ class SearchableAppBar extends StatelessWidget { required this.closeSearchBar, this.toolbarHeight, this.isFullScreen = true, + this.displayBackButton = true, }); @override @@ -113,11 +115,9 @@ class SearchableAppBar extends StatelessWidget { onTap: closeSearchBar, tooltip: L10n.of(context)!.close, icon: Icons.close, - paddingAll: 10.0, - margin: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 6.0, - ), + paddingAll: + SearchableAppBarStyle.closeButtonPaddingAll, + margin: SearchableAppBarStyle.closeButtonMargin, ); } return TwakeIconButton( @@ -130,16 +130,24 @@ class SearchableAppBar extends StatelessWidget { }, ), ] else ...[ - TwakeIconButton( - onTap: () => context.pop(), - tooltip: L10n.of(context)!.close, - icon: Icons.close, - paddingAll: 10.0, - margin: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 6.0, + if (displayBackButton) + TwakeIconButton( + onTap: () => context.pop(), + tooltip: L10n.of(context)!.close, + icon: Icons.close, + paddingAll: SearchableAppBarStyle.closeButtonPaddingAll, + margin: SearchableAppBarStyle.closeButtonMargin, + ) + else + Container( + width: SearchableAppBarStyle.closeButtonPlaceholderWidth, + height: SearchableAppBarStyle.closeButtonPlaceholderWidth, + padding: const EdgeInsets.all( + SearchableAppBarStyle.closeButtonPaddingAll, + ), + margin: SearchableAppBarStyle.closeButtonMargin, + child: const SizedBox.shrink(), ), - ), ], ], ), diff --git a/lib/widgets/app_bars/searchable_app_bar_style.dart b/lib/widgets/app_bars/searchable_app_bar_style.dart index 71a2c9a561..7983e128e7 100644 --- a/lib/widgets/app_bars/searchable_app_bar_style.dart +++ b/lib/widgets/app_bars/searchable_app_bar_style.dart @@ -35,4 +35,13 @@ class SearchableAppBarStyle { static const int textFieldMaxLines = 1; static const double appBarBorderRadius = 16.0; + + static const double closeButtonPaddingAll = 10.0; + + static const EdgeInsets closeButtonMargin = EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 6.0, + ); + + static const double closeButtonPlaceholderWidth = 44.0; } From 1547874616651a1e6133aa026e4b109ca5032583 Mon Sep 17 00:00:00 2001 From: Terence Zafindratafa Date: Tue, 9 Apr 2024 16:04:12 +0200 Subject: [PATCH 118/183] TW-1621: widget test added for searchable app bar (cherry picked from commit 4a1f2ab2717747dfe430d90c4873f4886e1b8f65) --- lib/pages/forward/forward_view.dart | 2 +- .../app_bars/searchable_app_bar_test.dart | 157 ++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 test/widget/app_bars/searchable_app_bar_test.dart diff --git a/lib/pages/forward/forward_view.dart b/lib/pages/forward/forward_view.dart index 0746ba5f86..89f11cc23a 100644 --- a/lib/pages/forward/forward_view.dart +++ b/lib/pages/forward/forward_view.dart @@ -31,7 +31,7 @@ class ForwardView extends StatelessWidget { ? ForwardViewStyle.preferredSize(context) : ForwardViewStyle.maxPreferredSize(context), child: ValueListenableBuilder?>( - valueListenable: controller.forwardMessageNotifier, + valueListenable: controller.forwardMessageNotifier, builder: (context, forwardMessageState, child) { return SearchableAppBar( toolbarHeight: ForwardViewStyle.maxToolbarHeight(context), diff --git a/test/widget/app_bars/searchable_app_bar_test.dart b/test/widget/app_bars/searchable_app_bar_test.dart new file mode 100644 index 0000000000..bfc6ef108e --- /dev/null +++ b/test/widget/app_bars/searchable_app_bar_test.dart @@ -0,0 +1,157 @@ +import 'package:fluffychat/pages/forward/forward_view_style.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/widgets/app_bars/searchable_app_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:get_it/get_it.dart'; + +final getIt = GetIt.instance; + +void main() { + final searchModeNotifier = ValueNotifier(false); + + Widget makeTestableAppBar({ + bool isFullScreen = true, + bool displayBackButton = true, + bool isSearchModeEnabled = false, + }) { + searchModeNotifier.value = isSearchModeEnabled; + getIt.registerSingleton(ResponsiveUtils()); + + return MaterialApp( + locale: const Locale('en'), + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, + home: Builder( + builder: (context) { + return Scaffold( + appBar: PreferredSize( + preferredSize: ForwardViewStyle.maxPreferredSize(context), + child: SearchableAppBar( + searchModeNotifier: searchModeNotifier, + title: "Title", + hintText: "Hint", + focusNode: FocusNode(), + textEditingController: TextEditingController(), + openSearchBar: () => searchModeNotifier.value = true, + closeSearchBar: () => searchModeNotifier.value = false, + isFullScreen: isFullScreen, + displayBackButton: displayBackButton, + ), + ), + body: const Center(child: CircularProgressIndicator()), + ); + }, + ), + ); + } + + setUp(() { + getIt.reset(); + }); + + group("fullscreen = false", () { + testWidgets("Display title and search button", (widgetTester) async { + await widgetTester.pumpWidget( + makeTestableAppBar(isFullScreen: false), + ); + + expect(find.text("Title"), findsOneWidget); + expect(find.text("Hint"), findsOneWidget); + expect(find.byIcon(Icons.search_outlined), findsOneWidget); + expect(find.byIcon(Icons.close), findsOneWidget); + expect(find.byIcon(Icons.arrow_back), findsNothing); + }); + + testWidgets("Still one textfield when clicking on title", + (widgetTester) async { + await widgetTester.pumpWidget( + makeTestableAppBar(isFullScreen: false), + ); + + await widgetTester.tap(find.text("Title"), warnIfMissed: false); + await widgetTester.pump(); + + expect(find.byType(TextField), findsOneWidget); + expect(find.byIcon(Icons.close), findsOneWidget); + expect(find.byIcon(Icons.search_outlined), findsOneWidget); + }); + + testWidgets("Display back button remove back button", (widgetTester) async { + await widgetTester.pumpWidget( + makeTestableAppBar( + isFullScreen: false, + displayBackButton: false, + ), + ); + + expect(find.byType(TextField), findsOneWidget); + expect(find.byIcon(Icons.close), findsNothing); + expect(find.byIcon(Icons.search_outlined), findsOneWidget); + }); + }); + + group("fullscreen = true", () { + testWidgets("Display title and search button (searchMode disabled)", + (widgetTester) async { + await widgetTester.pumpWidget( + makeTestableAppBar(), + ); + + expect(find.text("Title"), findsOneWidget); + expect(find.text("Hint"), findsNothing); + expect(find.byIcon(Icons.search_outlined), findsNothing); + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + expect(find.byIcon(Icons.close), findsNothing); + }); + + testWidgets("Still one textfield when clicking on title", + (widgetTester) async { + await widgetTester.pumpWidget( + makeTestableAppBar(), + ); + + await widgetTester.tap(find.byIcon(Icons.search)); + await widgetTester.pump(); + + expect(find.text("Title"), findsNothing); + expect(find.text("Hint"), findsOneWidget); + expect(find.byType(TextField), findsOneWidget); + expect(find.byIcon(Icons.search_outlined), findsNothing); + expect(find.byIcon(Icons.close), findsOneWidget); + }); + + testWidgets("Display title and search button when turning search mode off", + (widgetTester) async { + await widgetTester.pumpWidget( + makeTestableAppBar( + isSearchModeEnabled: true, + ), + ); + + await widgetTester.tap(find.byIcon(Icons.close)); + await widgetTester.pump(); + + expect(find.text("Title"), findsOneWidget); + expect(find.text("Hint"), findsNothing); + expect(find.byIcon(Icons.search_outlined), findsNothing); + expect(find.byIcon(Icons.close), findsNothing); + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + }); + + testWidgets("change displayBackButton does nothing", (widgetTester) async { + await widgetTester.pumpWidget( + makeTestableAppBar( + displayBackButton: false, + ), + ); + + expect(find.text("Title"), findsOneWidget); + expect(find.text("Hint"), findsNothing); + expect(find.byIcon(Icons.search_outlined), findsNothing); + expect(find.byIcon(Icons.arrow_back), findsOneWidget); + expect(find.byIcon(Icons.close), findsNothing); + }); + }); +} From 95bbcbb6788cb8fc1d56abd2c02574cf9854b365 Mon Sep 17 00:00:00 2001 From: Terence Zafindratafa Date: Mon, 8 Apr 2024 15:46:11 +0200 Subject: [PATCH 119/183] TW-1592: go to setting profile when clicking to own contact (cherry picked from commit a075a9606d7e85c5ca37e2a18e8567c5980f20cd) --- lib/pages/search/recent_item_widget.dart | 37 ++++++++++++++++++------ lib/pages/search/search.dart | 8 +++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/pages/search/recent_item_widget.dart b/lib/pages/search/recent_item_widget.dart index e534414a1f..7ee8eca2c2 100644 --- a/lib/pages/search/recent_item_widget.dart +++ b/lib/pages/search/recent_item_widget.dart @@ -3,8 +3,10 @@ import 'package:fluffychat/pages/search/recent_item_widget_style.dart'; import 'package:fluffychat/presentation/extensions/room_summary_extension.dart'; import 'package:fluffychat/presentation/extensions/search/presentation_search_extensions.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/highlight_text.dart'; +import 'package:fluffychat/widgets/twake_components/twake_chip.dart'; import 'package:flutter/material.dart' hide SearchController; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; @@ -253,16 +255,33 @@ class _ContactInformation extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SearchHighlightText( - text: contactPresentationSearch.displayName ?? "", - style: Theme.of(context).textTheme.titleMedium?.merge( - TextStyle( - overflow: TextOverflow.ellipsis, - letterSpacing: 0.15, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + Row( + children: [ + Expanded( + child: _SearchHighlightText( + text: contactPresentationSearch.displayName ?? "", + style: Theme.of(context).textTheme.titleMedium?.merge( + TextStyle( + overflow: TextOverflow.ellipsis, + letterSpacing: 0.15, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + searchWord: searchKeyword, ), - searchWord: searchKeyword, + ), + if (contactPresentationSearch.matrixId != null && + contactPresentationSearch.matrixId! + .isCurrentMatrixId(context)) ...[ + const SizedBox(width: 8.0), + TwakeChip( + text: L10n.of(context)!.owner, + textColor: Theme.of(context).colorScheme.primary, + ), + ], + ], ), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 3e3728a468..6f19927b6f 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -91,12 +91,20 @@ class SearchController extends State { void onSearchItemTap(PresentationSearch presentationSearch) async { if (presentationSearch is ContactPresentationSearch) { + if (presentationSearch.matrixId?.isCurrentMatrixId(context) == true) { + goToSettingsProfile(); + return; + } onContactTap(presentationSearch); } else if (presentationSearch is RecentChatPresentationSearch) { onRecentChatTap(presentationSearch); } } + void goToSettingsProfile() async { + context.go('/rooms/profile'); + } + void onContactTap(ContactPresentationSearch contactPresentationSearch) { final roomId = Matrix.of(context) .client From 6b2d700e1b5d5f3a48f343753af94b3406641015 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Tue, 9 Apr 2024 09:04:41 +0200 Subject: [PATCH 120/183] TW-1592: go to profile when click on own contact tile (cherry picked from commit 93637b18b40e2d84e0c78af7c109f5fe09461926) --- lib/pages/contacts_tab/contacts_tab.dart | 9 +++++++++ lib/pages/search/search.dart | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/pages/contacts_tab/contacts_tab.dart b/lib/pages/contacts_tab/contacts_tab.dart index 34903ce3a7..7dc5b3bd5d 100644 --- a/lib/pages/contacts_tab/contacts_tab.dart +++ b/lib/pages/contacts_tab/contacts_tab.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/presentation/mixins/contacts_view_controller_mixin.da import 'package:fluffychat/presentation/model/presentation_contact.dart'; import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; @@ -43,6 +44,10 @@ class ContactsTabController extends State required String path, required PresentationContact contact, }) { + if (contact.matrixId?.isCurrentMatrixId(context) == true) { + goToSettingsProfile(); + return; + } final roomId = Matrix.of(context).client.getDirectChatFromUserId(contact.matrixId!); if (roomId == null) { @@ -56,6 +61,10 @@ class ContactsTabController extends State } } + void goToSettingsProfile() { + context.go('/rooms/profile'); + } + void goToDraftChat({ required BuildContext context, required String path, diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 6f19927b6f..7eedc6978a 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -101,7 +101,7 @@ class SearchController extends State { } } - void goToSettingsProfile() async { + void goToSettingsProfile() { context.go('/rooms/profile'); } From b4b6ad3fece0d82f8110cc1d90dd3369e3ffb063 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 15 Apr 2024 11:24:22 +0700 Subject: [PATCH 121/183] Bump version to v2.5.0 (cherry picked from commit de36792e5e08a1b6ba4971e30acb5ae309c7b6cf) --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2f8ee524..91e4792bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +## [2.5.0+2330] - 2024-04-15 + +### Fixed +- #1435 Remove Big dot in message notification +- #1453 Error handling update profile failed +- #1565 Icon for avi, wmv +- unpin icon in context menu +- #1124 Profile Info view for Direct Chat (avatar) +- #1564 improve video player +- #1453 video player for web +- unpin icon for app bar +- Pin bottom sheet in mobile +- #1567 Prevent show hover, actions in sending or error message +- #1543 Fix cannot attach file +- #1380 updat AppLifeCycle for Twake Web +- #1544 Prevent create direct multiple times +- remove colorFilter of TwakeIconButton +- #1581 preview unknown file +- Highlight tag with link in message bubble +- Handle multiple download for web +- #1456 change profile info UI in member list of Group info +- #1589 Search with link content +- #1578 swipe to back in search screen (mobile) +- #1656 fix user can not mark as read/unread some chat +- Fix can not get fix on web +- #1662 Fix can not send video/picture by camera +- #1497 Filter chat in forward list +- #1523 Chat setting align in view +- #1678 Prevent blink blink in preview link +- Save to files (mobile) +- #1675 Online status is not stable +- #1621 Improve forward screen (mobile/web) + +### Added +- Integrate registration (mobile/web) +- Multiple accounts support +- Improve Download with queue +- Cancel downloading + ## [2.4.21+2330] - 2024-03-18 ### Fixed diff --git a/pubspec.yaml b/pubspec.yaml index affc3e3af1..1ce5882f15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fluffychat description: The open digital workplace. publish_to: none -version: 2.4.22+2330 +version: 2.5.0+2330 environment: sdk: ">=3.1.3 <4.0.0" From 4db6bc32f12eae8af99455fd5c6c715554489cc6 Mon Sep 17 00:00:00 2001 From: Nguyen Thai Date: Mon, 15 Apr 2024 17:42:44 +0700 Subject: [PATCH 122/183] Use unique naming for artifact (cherry picked from commit e7fa36465580e28880fa1f9f27304eedee691094) --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b3154eb5e4..2d1614306b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -80,7 +80,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: twake-on-matrix-dev-mobile + name: twake-on-matrix-dev-${{ matrix.os }} path: | twake-on-matrix-debug.apk Runner.ipa From 42c6f25ce0ff316956f89f24cc274f7dc6c8c0f1 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 16 Apr 2024 11:47:56 +0700 Subject: [PATCH 123/183] Hot fix setup config json on web (cherry picked from commit 19e27fcc6f1339b7b52ef2adbc732e275075f384) --- assets/l10n/intl_en.arb | 3 +- lib/config/app_config.dart | 7 + .../auto_homeserver_picker.dart | 33 +++- .../auto_homeserver_picker_state.dart | 26 ++++ .../auto_homeserver_picker_view.dart | 145 +++++++++++++----- .../mixins/init_config_mixin.dart | 41 +++++ lib/widgets/matrix.dart | 27 +--- 7 files changed, 208 insertions(+), 74 deletions(-) create mode 100644 lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart create mode 100644 lib/presentation/mixins/init_config_mixin.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 84aad83953..e50e507e6a 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3030,5 +3030,6 @@ } } }, - "downloading": "Downloading" + "downloading": "Downloading", + "configurationNotFound": "The configuration data not found" } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index bde39e45ac..6e63dd2e64 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:fluffychat/di/global/get_it_initializer.dart'; @@ -9,6 +10,12 @@ import 'package:matrix/matrix.dart'; abstract class AppConfig { static ResponsiveUtils responsive = getIt.get(); + static Completer initConfigCompleter = Completer(); + + static int retryCompleterCount = 0; + + static bool get hasReachedMaxRetries => retryCompleterCount == 3; + static String _applicationName = 'Twake Chat'; static String get applicationName => _applicationName; diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart index 840b377f5b..70001269b1 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart @@ -1,8 +1,9 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart'; import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart'; import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/presentation/mixins/init_config_mixin.dart'; import 'package:fluffychat/utils/exception/check_homeserver_exception.dart'; -import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -21,12 +22,14 @@ class AutoHomeserverPicker extends StatefulWidget { } class AutoHomeserverPickerController extends State - with ConnectPageMixin { + with ConnectPageMixin, InitConfigMixin { static const Duration autoHomeserverPickerTimeout = Duration(seconds: 30); static const _saasPlatform = 'saas'; - final showButtonRetryNotifier = ValueNotifier(false); + final autoHomeserverPickerUIState = ValueNotifier( + AutoHomeServerPickerInitialState(), + ); MatrixState get matrix => Matrix.of(context); @@ -91,7 +94,7 @@ class AutoHomeserverPickerController extends State context.push('/connect'); } } catch (e) { - showButtonRetryNotifier.toggle(); + autoHomeserverPickerUIState.value = AutoHomeServerPickerFailureState(); Logs().d( "AutoHomeserverPickerController: _autoCheckHomeserver: Error: $e", ); @@ -99,7 +102,7 @@ class AutoHomeserverPickerController extends State } void retryCheckHomeserver() { - showButtonRetryNotifier.toggle(); + autoHomeserverPickerUIState.value = AutoHomeServerPickerLoadingState(); if (_isSaasPlatform) { _autoConnectSaas(); } else { @@ -145,8 +148,18 @@ class AutoHomeserverPickerController extends State } } - void _setupAutoHomeserverPicker() { + Future _setupAutoHomeserverPicker() async { + autoHomeserverPickerUIState.value = AutoHomeServerPickerLoadingState(); if (widget.loggedOut == null) { + final isConfigured = await AppConfig.initConfigCompleter.future; + if (!isConfigured) { + if (!AppConfig.hasReachedMaxRetries) { + return _setupAutoHomeserverPicker(); + } else { + autoHomeserverPickerUIState.value = + AutoHomeServerPickerFailureState(); + } + } Logs().d( "AutoHomeserverPickerController: _initializeAutoHomeserverPicker: PlatForm ${AppConfig.platform}", ); @@ -157,7 +170,7 @@ class AutoHomeserverPickerController extends State } } else { if (widget.loggedOut == true) { - showButtonRetryNotifier.toggle(); + autoHomeserverPickerUIState.value = AutoHomeServerPickerInitialState(); } } } @@ -168,6 +181,12 @@ class AutoHomeserverPickerController extends State super.initState(); } + @override + void dispose() { + super.dispose(); + autoHomeserverPickerUIState.dispose(); + } + @override Widget build(BuildContext context) { return AutoHomeserverPickerView( diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart new file mode 100644 index 0000000000..fd717a6afb --- /dev/null +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart @@ -0,0 +1,26 @@ +import 'package:equatable/equatable.dart'; + +abstract class AutoHomeServerPickerState with EquatableMixin { + @override + List get props => []; +} + +class AutoHomeServerPickerInitialState extends AutoHomeServerPickerState { + @override + List get props => []; +} + +class AutoHomeServerPickerLoadingState extends AutoHomeServerPickerState { + @override + List get props => []; +} + +class AutoHomeServerPickerSuccessState extends AutoHomeServerPickerState { + @override + List get props => []; +} + +class AutoHomeServerPickerFailureState extends AutoHomeServerPickerState { + @override + List get props => []; +} diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart index 6dcfea9c10..c9bba9d0a9 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; +import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart'; import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_view_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:flutter/material.dart'; @@ -8,6 +9,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class AutoHomeserverPickerView extends StatelessWidget { final AutoHomeserverPickerController controller; + const AutoHomeserverPickerView({super.key, required this.controller}); @override @@ -47,10 +49,12 @@ class AutoHomeserverPickerView extends StatelessWidget { ), const SizedBox(height: 32), ValueListenableBuilder( - valueListenable: controller.showButtonRetryNotifier, - builder: (context, isShowRetry, child) { - if (isShowRetry) return child!; - return const CircularProgressIndicator.adaptive(); + valueListenable: controller.autoHomeserverPickerUIState, + builder: (context, state, child) { + if (state is AutoHomeServerPickerLoadingState) { + return const CircularProgressIndicator.adaptive(); + } + return child!; }, child: const SizedBox(), ), @@ -59,48 +63,107 @@ class AutoHomeserverPickerView extends StatelessWidget { Positioned( bottom: 0, child: ValueListenableBuilder( - valueListenable: controller.showButtonRetryNotifier, - builder: (context, isShowRetry, child) { - if (isShowRetry) return child!; - return const SizedBox(); - }, - child: Padding( - padding: AutoHomeserverPickerViewStyle.buttonPadding, - child: InkWell( - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - onTap: controller.retryCheckHomeserver, - child: Container( - height: AutoHomeserverPickerViewStyle.buttonHeight, - padding: const EdgeInsets.symmetric( - horizontal: 40, - ), - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: LinagoraSysColors.material().primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - AutoHomeserverPickerViewStyle.buttonRadius, + valueListenable: controller.autoHomeserverPickerUIState, + builder: (context, state, child) { + if (state is AutoHomeServerPickerFailureState) { + return Padding( + padding: AutoHomeserverPickerViewStyle.buttonPadding, + child: Column( + children: [ + InkWell( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + onTap: controller.retryCheckHomeserver, + child: Container( + height: + AutoHomeserverPickerViewStyle.buttonHeight, + padding: const EdgeInsets.symmetric( + horizontal: 40, + ), + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: LinagoraSysColors.material().primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AutoHomeserverPickerViewStyle.buttonRadius, + ), + ), + ), + child: Center( + child: Text( + L10n.of(context)!.startMessaging, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: LinagoraSysColors.material() + .onPrimary, + ), + ), + ), + ), ), - ), + const SizedBox(height: 16), + Text( + L10n.of(context)!.configurationNotFound, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + color: LinagoraSysColors.material().error, + ), + ), + ], ), - child: Center( - child: Text( - L10n.of(context)!.startMessaging, - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: LinagoraSysColors.material().onPrimary, + ); + } + + if (state is AutoHomeServerPickerInitialState) { + return Padding( + padding: AutoHomeserverPickerViewStyle.buttonPadding, + child: InkWell( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + onTap: controller.retryCheckHomeserver, + child: Container( + height: AutoHomeserverPickerViewStyle.buttonHeight, + padding: const EdgeInsets.symmetric( + horizontal: 40, + ), + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: LinagoraSysColors.material().primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AutoHomeserverPickerViewStyle.buttonRadius, ), + ), + ), + child: Center( + child: Text( + L10n.of(context)!.startMessaging, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: + LinagoraSysColors.material().onPrimary, + ), + ), + ), ), ), - ), - ), - ), + ); + } + return child!; + }, + child: const SizedBox(), ), ), ], diff --git a/lib/presentation/mixins/init_config_mixin.dart b/lib/presentation/mixins/init_config_mixin.dart new file mode 100644 index 0000000000..02ad41fef2 --- /dev/null +++ b/lib/presentation/mixins/init_config_mixin.dart @@ -0,0 +1,41 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +import 'package:matrix/matrix.dart'; + +mixin InitConfigMixin { + Future initConfigWeb() async { + try { + final configJsonString = + utf8.decode((await http.get(Uri.parse('config.json'))).bodyBytes); + final configJson = json.decode(configJsonString); + AppConfig.loadFromJson(configJson); + Logs().d('[ConfigLoader] $configJson'); + AppConfig.initConfigCompleter.complete(true); + } on FormatException catch (_) { + _retryInitConfigWeb(); + Logs().v('[ConfigLoader] config.json not found'); + } catch (e) { + _retryInitConfigWeb(); + Logs().v('[ConfigLoader] config.json not found', e); + } + } + + void _retryInitConfigWeb() { + if (!AppConfig.hasReachedMaxRetries) { + AppConfig.retryCompleterCount++; + initConfigWeb(); + } else { + AppConfig.initConfigCompleter.complete(false); + } + } + + Future initConfigMobile() async { + try { + AppConfig.loadEnvironment(); + } catch (e) { + Logs().e('[ConfigLoader] Config mobile error', e); + } + } +} diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index be2e502699..e4d7bccbde 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; +import 'package:fluffychat/presentation/mixins/init_config_mixin.dart'; import 'package:universal_html/html.dart' as html hide File; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -36,7 +36,6 @@ import 'package:flutter_app_lock/flutter_app_lock.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; @@ -75,7 +74,7 @@ class Matrix extends StatefulWidget { } class MatrixState extends State - with WidgetsBindingObserver, ReceiveSharingIntentMixin { + with WidgetsBindingObserver, ReceiveSharingIntentMixin, InitConfigMixin { final tomConfigurationRepository = getIt.get(); int _activeClient = -1; @@ -307,28 +306,6 @@ class MatrixState extends State }); } - Future initConfigWeb() async { - try { - final configJsonString = - utf8.decode((await http.get(Uri.parse('config.json'))).bodyBytes); - final configJson = json.decode(configJsonString); - AppConfig.loadFromJson(configJson); - Logs().d('[ConfigLoader] $configJson'); - } on FormatException catch (_) { - Logs().v('[ConfigLoader] config.json not found'); - } catch (e) { - Logs().v('[ConfigLoader] config.json not found', e); - } - } - - Future initConfigMobile() async { - try { - AppConfig.loadEnvironment(); - } catch (e) { - Logs().v('[ConfigLoader] config.json not found', e); - } - } - void _registerSubs(String name) async { final currentClient = getClientByName(name); if (currentClient == null) { From 89c8c7380ba561e54967d19860dbfd30e97e3982 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Fri, 19 Apr 2024 12:00:18 +0700 Subject: [PATCH 124/183] Bump version to v2.5.1 - Public platform config (cherry picked from commit 050bd85249d02b78e4843128d205077d0ac5a403) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1ce5882f15..89dc14299e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fluffychat description: The open digital workplace. publish_to: none -version: 2.5.0+2330 +version: 2.5.1+2330 environment: sdk: ">=3.1.3 <4.0.0" From 5e5daaf070f822d8b2fca4686cda312f541efed9 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 9 Apr 2024 15:01:39 +0700 Subject: [PATCH 125/183] TW-1573: refactor decrypted path to StorageDirectoryUtils (cherry picked from commit fe3055e89effcddd102f403d35c1d84fef04267d) --- .../download_file_extension.dart | 19 ++++++++++++------- lib/utils/storage_directory_utils.dart | 6 ++++++ lib/widgets/mxc_image.dart | 3 +-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index ae96fb11a6..2ebe5f01e2 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -139,14 +139,18 @@ extension DownloadFileExtension on Event { final decryptedFile = await event.decryptFile( fileInfo, event.getAttachmentOrThumbnailMxcUrl()!, - '${savePath}decrypted', + StorageDirectoryUtils.instance.getDecryptedFilePath(savePath: savePath), ); if (decryptedFile == null) { throw Exception( 'DownloadManager::download(): decryptedFile is null', ); } - final saveFile = File('${savePath}decrypted').copySync(savePath); + final saveFile = File( + StorageDirectoryUtils.instance.getDecryptedFilePath( + savePath: savePath, + ), + ).copySync(savePath); streamController.add( Right( DownloadNativeFileSuccessState( @@ -311,12 +315,13 @@ extension DownloadFileExtension on Event { throw ('getFileInfo: Encryption is not enabled in your Client.'); } - final tempDirectory = - await StorageDirectoryUtils.instance.getFileStoreDirectory(); String? decryptedPath; if (isFileEncrypted) { - decryptedPath = - '$tempDirectory/${Uri.encodeComponent(mxcUrl.toString())}-decrypted'; + decryptedPath = StorageDirectoryUtils.instance.getDecryptedFilePath( + savePath: await StorageDirectoryUtils.instance.getMediaFilePath( + mxcUrl: mxcUrl, + ), + ); final decryptedFile = File(decryptedPath); if (await File(decryptedPath).exists()) { @@ -335,7 +340,7 @@ extension DownloadFileExtension on Event { final fileInfo = await downloadOrRetrieveAttachmentForMedia( mxcUrl, - '$tempDirectory/${Uri.encodeComponent(mxcUrl.toString())}', + await StorageDirectoryUtils.instance.getMediaFilePath(mxcUrl: mxcUrl), progressCallback: progressCallback, getThumbnail: getThumbnail, cancelToken: cancelToken, diff --git a/lib/utils/storage_directory_utils.dart b/lib/utils/storage_directory_utils.dart index b39983e787..80b99b80c6 100644 --- a/lib/utils/storage_directory_utils.dart +++ b/lib/utils/storage_directory_utils.dart @@ -74,4 +74,10 @@ class StorageDirectoryUtils { } return availableFilePath; } + + String getDecryptedFilePath({ + required String savePath, + }) { + return '${savePath}decrypted'; + } } diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 477e59e509..799a29bfac 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -72,8 +72,7 @@ class MxcImage extends StatefulWidget { State createState() => _MxcImageState(); } -class _MxcImageState extends State - with SingleTickerProviderStateMixin { +class _MxcImageState extends State { static const String placeholderKey = 'placeholder'; static final Map _imageDataCache = {}; ImageData? _imageDataNoCache; From 10b53a6d57aea57494eb26596b38789520d6ca2e Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 9 Apr 2024 15:06:12 +0700 Subject: [PATCH 126/183] TW-1573: save image and videos to gallery in android (cherry picked from commit 85ca78bdd4cf78260974dcdf280be7082180e452) --- assets/l10n/intl_en.arb | 15 ++- lib/pages/chat/chat.dart | 93 ++++++++++++++++--- .../save_media_to_gallery_android_mixin.dart | 55 +++++++++++ .../exception/save_to_gallery_exception.dart | 10 ++ .../storage_permission_exception.dart | 5 + lib/utils/storage_directory_utils.dart | 7 ++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- pubspec.lock | 8 ++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 lib/presentation/mixins/save_media_to_gallery_android_mixin.dart create mode 100644 lib/utils/exception/save_to_gallery_exception.dart create mode 100644 lib/utils/exception/storage_permission_exception.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index e50e507e6a..da50ec484a 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3031,5 +3031,18 @@ } }, "downloading": "Downloading", - "configurationNotFound": "The configuration data not found" + "configurationNotFound": "The configuration data not found", + "fileSavedToGallery": "File saved to Gallery", + "saveFileToGalleryError": "Failed to save file to Gallery", + "explainPermissionToGallery": "To continue, please allow {appName} to access photo permission. This permission is essential for saving file to gallery.", + "@explainPermissionToGallery": { + "placeholders": { + "appName": { + "type": "String", + "placeholders": { + "count": {} + } + } + } + } } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 0377ecde17..e815945330 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -6,11 +6,14 @@ import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; import 'package:fluffychat/presentation/extensions/event_update_extension.dart'; import 'package:fluffychat/presentation/mixins/handle_clipboard_action_mixin.dart'; import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; +import 'package:fluffychat/presentation/mixins/save_media_to_gallery_android_mixin.dart'; import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; import 'package:fluffychat/presentation/model/chat/view_event_list_ui_state.dart'; +import 'package:fluffychat/utils/exception/storage_permission_exception.dart'; import 'package:fluffychat/utils/extension/basic_event_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/permission_dialog.dart'; import 'package:fluffychat/utils/permission_service.dart'; @@ -112,7 +115,8 @@ class ChatController extends State HandleClipboardActionMixin, TwakeContextMenuMixin, MessageContentMixin, - SaveFileToTwakeAndroidDownloadsFolderMixin { + SaveFileToTwakeAndroidDownloadsFolderMixin, + SaveMediaToGalleryAndroidMixin { final NetworkConnectionService networkConnectionService = getIt.get(); @@ -1869,27 +1873,86 @@ class ChatController extends State Logs().i( 'Chat::saveSelectedEventToDownloadAndroid():: Permission Denied', ); - return; + throw StoragePermissionException("Don't have permission to save file"); } } - final downloadManager = getIt.get(); - final downloadingStreamSubscription = - downloadManager.getDownloadStateStream( - downloadEvent.eventId, - ); - if (downloadingStreamSubscription == null) { - await handleSaveToDownloadsForFileNotInDownloading( - downloadEvent, + } + + void saveSelectedEventToDownloadAndroid() async { + if (selectedEvents.length != 1) { + return; + } + final downloadEvent = selectedEvents.first; + try { + await handleAndroidStoragePermission(); + + final downloadManager = getIt.get(); + final downloadingStreamSubscription = + downloadManager.getDownloadStateStream( + downloadEvent.eventId, + ); + if (downloadingStreamSubscription == null) { + await handleSaveToDownloadsForFileNotInDownloading( + downloadEvent, + context: context, + ); + return; + } + + handleSaveToDownloadForDownloadingFile( + downloadingStreamSubscription: downloadingStreamSubscription, + event: downloadEvent, context: context, ); + } catch (e) { + Logs().e('Chat::saveSelectedEventToDownloadAndroid(): $e'); + if (e is! StoragePermissionException) { + TwakeSnackBar.show( + context, + L10n.of(context)!.saveFileToDownloadsError, + ); + } + } + } + Future saveSelectedEventToGallery() async { + if (selectedEvents.length != 1) { return; } + final downloadEvent = selectedEvents.first; - handleSaveToDownloadForDownloadingFile( - downloadingStreamSubscription: downloadingStreamSubscription, - event: downloadEvent, - context: context, - ); + try { + if (PlatformInfos.isAndroid) { + await handleAndroidStoragePermission(); + } else { + await handlePhotoPermissionIOS(); + } + final mxcFile = await getMediaFile(downloadEvent); + if (await mxcFile.exists() && + await mxcFile.length() == downloadEvent.getFileSize()) { + final fileInDownloadsInApp = await getFileInDownloadsInAppFolder( + mxcFile: mxcFile, + downloadEvent: downloadEvent, + ); + if (downloadEvent.messageType == MessageTypes.Image) { + await saveImageToGallery(file: fileInDownloadsInApp); + } else if (downloadEvent.messageType == MessageTypes.Video) { + await saveVideoToGallery(file: fileInDownloadsInApp); + } + + TwakeSnackBar.show( + context, + L10n.of(context)!.fileSavedToGallery, + ); + } + } catch (e) { + Logs().e('Chat::saveSelectedEventToGallery(): $e'); + if (e is! StoragePermissionException) { + TwakeSnackBar.show( + context, + L10n.of(context)!.saveFileToDownloadsError, + ); + } + } } @override diff --git a/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart b/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart new file mode 100644 index 0000000000..d90d94523f --- /dev/null +++ b/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart @@ -0,0 +1,55 @@ +import 'dart:io'; + +import 'package:fluffychat/utils/exception/save_to_gallery_exception.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:matrix/matrix.dart'; +import 'package:gal/gal.dart'; + +mixin SaveMediaToGalleryAndroidMixin { + Future getFileInDownloadsInAppFolder({ + required File mxcFile, + required Event downloadEvent, + }) async { + final fileNameInAppDownloads = + await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: downloadEvent.eventId, + fileName: downloadEvent.filename, + ); + final file = File(fileNameInAppDownloads); + + if (!await file.exists() || await file.length() != await mxcFile.length()) { + await file.create(recursive: true); + await mxcFile.copy(fileNameInAppDownloads); + } + return file; + } + + Future getMediaFile(Event event) async { + if (event.attachmentMxcUrl == null) { + throw SaveToGalleryException( + error: 'File not found', + ); + } + final filePath = await StorageDirectoryUtils.instance + .getMediaFilePath(mxcUrl: event.attachmentMxcUrl!); + return File(filePath); + } + + Future saveImageToGallery({ + required File file, + }) async { + Logs().i('Chat::saveImageToGallery():: file path: ${file.path}'); + await Gal.putImage( + file.path, + ); + } + + Future saveVideoToGallery({ + required File file, + }) async { + await Gal.putVideo( + file.path, + ); + } +} diff --git a/lib/utils/exception/save_to_gallery_exception.dart b/lib/utils/exception/save_to_gallery_exception.dart new file mode 100644 index 0000000000..4adc76dec6 --- /dev/null +++ b/lib/utils/exception/save_to_gallery_exception.dart @@ -0,0 +1,10 @@ +class SaveToGalleryException implements Exception { + final dynamic error; + + SaveToGalleryException({ + this.error, + }); + + @override + String toString() => error; +} diff --git a/lib/utils/exception/storage_permission_exception.dart b/lib/utils/exception/storage_permission_exception.dart new file mode 100644 index 0000000000..b25b8ff190 --- /dev/null +++ b/lib/utils/exception/storage_permission_exception.dart @@ -0,0 +1,5 @@ +class StoragePermissionException implements Exception { + final dynamic error; + + StoragePermissionException(this.error); +} diff --git a/lib/utils/storage_directory_utils.dart b/lib/utils/storage_directory_utils.dart index 80b99b80c6..8a4dcaf70e 100644 --- a/lib/utils/storage_directory_utils.dart +++ b/lib/utils/storage_directory_utils.dart @@ -75,6 +75,13 @@ class StorageDirectoryUtils { return availableFilePath; } + Future getMediaFilePath({ + required Uri mxcUrl, + }) async { + final temporaryDirectory = await getTemporaryDirectory(); + return '${temporaryDirectory.path}/${Uri.encodeComponent(mxcUrl.toString())}'; + } + String getDecryptedFilePath({ required String savePath, }) { diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f2af6f2ea8..0a989c2082 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -22,6 +22,7 @@ import flutter_local_notifications import flutter_secure_storage_macos import flutter_web_auth_2 import flutter_webrtc +import gal import irondash_engine_context import just_audio import macos_ui @@ -62,6 +63,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) + GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 6408dba728..cb30842cf6 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -203,7 +203,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d222620202..2a5b3764b2 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ #include #include +#include #include #include #include @@ -41,6 +42,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); + GalPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GalPluginCApi")); IrondashEngineContextPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index da48dac76a..f0f48fefd0 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_saver file_selector_windows flutter_webrtc + gal irondash_engine_context media_kit_libs_windows_video media_kit_video From 6bf97334bc7992c06946d855594b59947c788216 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 9 Apr 2024 15:06:43 +0700 Subject: [PATCH 127/183] TW-1573: save image and videos to gallery ios (cherry picked from commit d4835bb436a8e73f904dd3f24ba3f758fa0c629b) --- lib/pages/chat/chat.dart | 42 +++++++++++++++++++++++++++---- lib/utils/permission_service.dart | 4 +++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index e815945330..43fc1c5f33 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1790,6 +1790,8 @@ class ChatController extends State .every((event) => event.hasAttachment && !event.isVideoOrImage)) ChatAppBarActions.saveToDownload, ], + if (selectedEvents.every((event) => event.isVideoOrImage)) + ChatAppBarActions.saveToGallery, ChatAppBarActions.info, ChatAppBarActions.report, ]; @@ -1822,6 +1824,11 @@ class ChatController extends State () => saveSelectedEventToDownloadAndroid(), ); break; + case ChatAppBarActions.saveToGallery: + actionWithClearSelections( + () => saveSelectedEventToGallery(), + ); + break; case ChatAppBarActions.info: actionWithClearSelections( () => showEventInfo( @@ -1840,11 +1847,7 @@ class ChatController extends State } } - void saveSelectedEventToDownloadAndroid() async { - if (selectedEvents.length != 1) { - return; - } - final downloadEvent = selectedEvents.first; + Future handleAndroidStoragePermission() async { if (await PermissionHandlerService() .isUserHaveToRequestStoragePermissionAndroid()) { final permission = await Permission.storage.request(); @@ -1914,6 +1917,35 @@ class ChatController extends State } } } + + Future handlePhotoPermissionIOS() async { + final permissionHandlerService = PermissionHandlerService(); + final permissionStatus = + await permissionHandlerService.requestPhotoAddOnlyPermissionIOS(); + if (permissionStatus.isPermanentlyDenied) { + showDialog( + useRootNavigator: false, + context: context, + builder: (_) { + return PermissionDialog( + icon: const Icon(Icons.photo), + permission: Permission.photos, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToGallery( + AppConfig.applicationName, + ), + ), + onAcceptButton: () => + permissionHandlerService.goToSettingsForPermissionActions(), + ); + }, + ); + } + if (!permissionStatus.isGranted) { + throw StoragePermissionException('Permission denied'); + } + } + Future saveSelectedEventToGallery() async { if (selectedEvents.length != 1) { return; diff --git a/lib/utils/permission_service.dart b/lib/utils/permission_service.dart index 6771cc5183..795dd2c6d7 100644 --- a/lib/utils/permission_service.dart +++ b/lib/utils/permission_service.dart @@ -132,6 +132,10 @@ class PermissionHandlerService { !(await Permission.storage.isGranted); } + Future requestPhotoAddOnlyPermissionIOS() async { + return await Permission.photosAddOnly.request(); + } + void goToSettingsForPermissionActions() { openAppSettings(); } From 514901b19dd464db59ad87a824cb76063a7e1638 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Apr 2024 09:32:23 +0700 Subject: [PATCH 128/183] TW-1573: rename class and function to correct name (cherry picked from commit 0fdbac75668c4e82d4cd390f6d6c1a14edb48503) --- lib/data/network/media/media_api.dart | 1 + .../download_file_for_preview_interactor.dart | 1 - .../chat/events/message_download_content.dart | 2 +- lib/pages/image_viewer/image_viewer_view.dart | 2 +- .../media_viewer_app_bar_view.dart | 2 +- .../mixins/handle_video_download_mixin.dart | 2 +- lib/utils/dialog/downloading_file_dialog.dart | 12 +----- .../download_file_web_extension.dart | 7 ++-- .../event_extension.dart | 4 -- lib/utils/storage_directory_utils.dart | 37 +++++++------------ macos/Runner.xcodeproj/project.pbxproj | 8 ++-- test/files/get_available_file_path_test.dart | 18 ++++----- 12 files changed, 38 insertions(+), 58 deletions(-) diff --git a/lib/data/network/media/media_api.dart b/lib/data/network/media/media_api.dart index 66fe80d0ed..d74ab3d8f3 100644 --- a/lib/data/network/media/media_api.dart +++ b/lib/data/network/media/media_api.dart @@ -55,6 +55,7 @@ class MediaAPI { if (error is DioException && error.type == DioExceptionType.cancel) { throw CancelRequestException(); } else { + Logs().i('downloadFileInfo error: $error'); throw Exception(error); } }); diff --git a/lib/domain/usecase/download_file_for_preview_interactor.dart b/lib/domain/usecase/download_file_for_preview_interactor.dart index 64d9d93845..fe893d4b0f 100644 --- a/lib/domain/usecase/download_file_for_preview_interactor.dart +++ b/lib/domain/usecase/download_file_for_preview_interactor.dart @@ -8,7 +8,6 @@ import 'package:fluffychat/domain/app_state/preview_file/download_file_for_previ import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_loading.dart'; import 'package:fluffychat/domain/app_state/preview_file/download_file_for_preview_success.dart'; import 'package:fluffychat/domain/model/download_file/download_file_for_preview_response.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:matrix/matrix.dart'; import 'package:mime/mime.dart'; diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 29a00425cd..63eb4481d5 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -71,7 +71,7 @@ class _MessageDownloadContentState extends State Future checkFileInDownloadsInApp() async { final filePath = - await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: widget.event.eventId, fileName: widget.event.filename, ); diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 28e06aa4fa..a43373d498 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -110,7 +110,7 @@ class _ImageWidget extends StatelessWidget { ); } else { return FutureBuilder( - future: event.getMediaFileInfo( + future: event.getFileInfo( getThumbnail: false, ), builder: (context, snapshot) { diff --git a/lib/pages/image_viewer/media_viewer_app_bar_view.dart b/lib/pages/image_viewer/media_viewer_app_bar_view.dart index a9ca319bc0..64c68be47b 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_view.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_view.dart @@ -92,7 +92,7 @@ class MediaViewerAppbarView extends StatelessWidget { menuChildren: [ ContextMenuItemImageViewer( icon: Icons.file_download_outlined, - title: L10n.of(context)!.saveFile, + title: L10n.of(context)!.saveToGallery, onTap: () => controller.saveFileAction( context, controller.widget.event, diff --git a/lib/presentation/mixins/handle_video_download_mixin.dart b/lib/presentation/mixins/handle_video_download_mixin.dart index bf329b0838..b131de7ec9 100644 --- a/lib/presentation/mixins/handle_video_download_mixin.dart +++ b/lib/presentation/mixins/handle_video_download_mixin.dart @@ -32,7 +32,7 @@ mixin HandleVideoDownloadMixin { } return url; } else { - final videoFile = await event.getMediaFileInfo( + final videoFile = await event.getFileInfo( progressCallback: progressCallback, cancelToken: cancelToken, ); diff --git a/lib/utils/dialog/downloading_file_dialog.dart b/lib/utils/dialog/downloading_file_dialog.dart index ae3e44d34a..e8b61489ea 100644 --- a/lib/utils/dialog/downloading_file_dialog.dart +++ b/lib/utils/dialog/downloading_file_dialog.dart @@ -1,4 +1,3 @@ -import 'package:dio/dio.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/dialog/downloading_file_dialog_style.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; @@ -11,7 +10,6 @@ class DownloadingFileDialog extends StatelessWidget { required this.parentContext, required this.eventId, required this.downloadProgressNotifier, - this.cancelDownloadToken, }); final BuildContext parentContext; @@ -20,8 +18,6 @@ class DownloadingFileDialog extends StatelessWidget { final ValueNotifier downloadProgressNotifier; - final CancelToken? cancelDownloadToken; - @override Widget build(BuildContext context) { return PopScope( @@ -116,12 +112,8 @@ class DownloadingFileDialog extends StatelessWidget { } void onCloseTap(BuildContext context) { - if (cancelDownloadToken != null) { - cancelDownloadToken!.cancel(); - } else { - final downloadManager = getIt.get(); - downloadManager.cancelDownload(eventId); - } + final downloadManager = getIt.get(); + downloadManager.cancelDownload(eventId); Navigator.of(context, rootNavigator: true).pop(); } } diff --git a/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart index 6e5de2574a..88a31f72d3 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart @@ -51,7 +51,7 @@ extension DownloadFileWebExtension on Event { Uint8List? uint8list; if (storeable) { - uint8list = await room.client.database?.getFile(mxcUrl); + uint8list = await room.client.database?.getFile(eventId, filename); } if (uint8list != null) { @@ -97,8 +97,9 @@ extension DownloadFileWebExtension on Event { if (database != null && storeable && uint8List.lengthInBytes < database.maxFileSize) { - await database.storeFile( - mxcUrl, + await database.storeEventFile( + eventId, + filename, uint8List, DateTime.now().millisecondsSinceEpoch, ); diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index dcb94db075..066286d3ff 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -38,10 +38,6 @@ extension LocalizedBody on Event { return filename.ellipsizeFileName; } - String get filename { - return (content.tryGet('filename') ?? body); - } - String? get blurHash { return infoMap['xyz.amorgan.blurhash'] is String ? infoMap['xyz.amorgan.blurhash'] diff --git a/lib/utils/storage_directory_utils.dart b/lib/utils/storage_directory_utils.dart index 8a4dcaf70e..a45902f8f5 100644 --- a/lib/utils/storage_directory_utils.dart +++ b/lib/utils/storage_directory_utils.dart @@ -6,14 +6,12 @@ import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:external_path/external_path.dart'; -class StorageDirectoryUtils { - StorageDirectoryUtils._(); +class StorageDirectoryManager { + StorageDirectoryManager._(); - static final StorageDirectoryUtils _instance = StorageDirectoryUtils._(); + static final StorageDirectoryManager _instance = StorageDirectoryManager._(); - static StorageDirectoryUtils get instance => _instance; - - static String? _tempDirectoryPath; + static StorageDirectoryManager get instance => _instance; Future getFileStoreDirectory() async { try { @@ -27,18 +25,13 @@ class StorageDirectoryUtils { } } - Future getDownloadFolderInApp() async { - _tempDirectoryPath ??= (await getTemporaryDirectory()).path; - return '$_tempDirectoryPath/Downloads'; - } - Future getFilePathInAppDownloads({ required String eventId, required String fileName, }) async { - final downloadInAppFolder = - await StorageDirectoryUtils.instance.getDownloadFolderInApp(); - return '$downloadInAppFolder/$eventId/$fileName'; + final fileStoreDirectory = + await StorageDirectoryManager.instance.getFileStoreDirectory(); + return '$fileStoreDirectory/$eventId/$fileName'; } Future getTwakeDownloadsFolderInDevice() async { @@ -75,16 +68,12 @@ class StorageDirectoryUtils { return availableFilePath; } - Future getMediaFilePath({ - required Uri mxcUrl, + Future getDecryptedFilePath({ + required String eventId, + required String fileName, }) async { - final temporaryDirectory = await getTemporaryDirectory(); - return '${temporaryDirectory.path}/${Uri.encodeComponent(mxcUrl.toString())}'; - } - - String getDecryptedFilePath({ - required String savePath, - }) { - return '${savePath}decrypted'; + final fileStoreDirectory = + await StorageDirectoryManager.instance.getFileStoreDirectory(); + return '$fileStoreDirectory/$eventId/decrypted-$fileName'; } } diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index cb30842cf6..fca4e314a0 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -283,6 +283,7 @@ "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_macos/flutter_secure_storage_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_web_auth_2/flutter_web_auth_2.framework", "${BUILT_PRODUCTS_DIR}/flutter_webrtc/flutter_webrtc.framework", + "${BUILT_PRODUCTS_DIR}/gal/gal.framework", "${BUILT_PRODUCTS_DIR}/irondash_engine_context/irondash_engine_context.framework", "${BUILT_PRODUCTS_DIR}/just_audio/just_audio.framework", "${BUILT_PRODUCTS_DIR}/macos_ui/macos_ui.framework", @@ -351,6 +352,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_auth_2.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_webrtc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/gal.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/irondash_engine_context.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/just_audio.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/macos_ui.framework", @@ -533,7 +535,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.2; + MACOSX_DEPLOYMENT_TARGET = 14.2.99; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -626,7 +628,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.2; + MACOSX_DEPLOYMENT_TARGET = 14.2.99; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -673,7 +675,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.2; + MACOSX_DEPLOYMENT_TARGET = 14.2.99; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/test/files/get_available_file_path_test.dart b/test/files/get_available_file_path_test.dart index 1849028f4b..0b4f153eba 100644 --- a/test/files/get_available_file_path_test.dart +++ b/test/files/get_available_file_path_test.dart @@ -20,7 +20,7 @@ void main() async { """, () async { final file = await createMockFile('file1.pdf'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); await file.delete(); @@ -33,7 +33,7 @@ void main() async { final file = await createMockFile('file1.pdf'); final file1 = await createMockFile('file1 (1).pdf'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); @@ -55,7 +55,7 @@ void main() async { final file5 = await createMockFile('file1 (5).pdf'); final file6 = await createMockFile('file1 (6).pdf'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); @@ -76,7 +76,7 @@ void main() async { """, () async { final file = await createMockFile('file1 (6).pdf'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); @@ -91,7 +91,7 @@ void main() async { """, () async { final file = await createMockFile('my.document.v1.pdf'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); @@ -106,7 +106,7 @@ void main() async { """, () async { final file = await createMockFile('text'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); @@ -121,7 +121,7 @@ void main() async { """, () async { final file = await createMockFile('.DS_Store'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); @@ -136,7 +136,7 @@ void main() async { """, () async { final file = await createMockFile('filename.tar.gz'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); @@ -155,7 +155,7 @@ void main() async { final file1 = await createMockFile('file (1).pdf'); final file3 = await createMockFile('file (3).pdf'); final fileAvailable = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( file.path, ); From 613141da682bb6fc20478fd612147aa6506b6eec Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Apr 2024 09:33:45 +0700 Subject: [PATCH 129/183] TW-1573: add priority to worker queue (cherry picked from commit ea164db5f17fdd1f20615432011ce803ba7c81f9) --- .../manager/download_manager/download_manager.dart | 13 +++++++++++-- lib/utils/task_queue/worker_queue.dart | 8 ++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/utils/manager/download_manager/download_manager.dart b/lib/utils/manager/download_manager/download_manager.dart index 26205d2c5e..a283163d0a 100644 --- a/lib/utils/manager/download_manager/download_manager.dart +++ b/lib/utils/manager/download_manager/download_manager.dart @@ -97,6 +97,7 @@ class DownloadManager { Future download({ required Event event, bool getThumbnail = false, + bool isFirstPriority = false, }) async { _initDownloadFileInfo(event); final streamController = _eventIdMapDownloadFileInfo[event.eventId] @@ -129,6 +130,7 @@ class DownloadManager { getThumbnail: getThumbnail, streamController: streamController, cancelToken: cancelToken, + isFirstPriority: isFirstPriority, ); } @@ -137,12 +139,14 @@ class DownloadManager { bool getThumbnail = false, required StreamController> streamController, required CancelToken cancelToken, + bool isFirstPriority = false, }) { if (PlatformInfos.isWeb) { _addTaskToWorkerQueueWeb( event: event, streamController: streamController, cancelToken: cancelToken, + isFirstPriority: isFirstPriority, ); return; } @@ -152,6 +156,7 @@ class DownloadManager { getThumbnail, streamController, cancelToken, + isFirstPriority: isFirstPriority, ); } @@ -159,8 +164,9 @@ class DownloadManager { Event event, bool getThumbnail, StreamController> streamController, - CancelToken cancelToken, - ) { + CancelToken cancelToken, { + bool isFirstPriority = false, + }) { workingQueue.addTask( Task( id: event.eventId, @@ -182,6 +188,7 @@ class DownloadManager { }, onTaskCompleted: () => clear(event.eventId), ), + isFirstPriority: isFirstPriority, ); } @@ -189,6 +196,7 @@ class DownloadManager { required Event event, required StreamController> streamController, required CancelToken cancelToken, + bool isFirstPriority = false, }) { workingQueue.addTask( Task( @@ -210,6 +218,7 @@ class DownloadManager { }, onTaskCompleted: () => clear(event.eventId), ), + isFirstPriority: isFirstPriority, ); } } diff --git a/lib/utils/task_queue/worker_queue.dart b/lib/utils/task_queue/worker_queue.dart index f58766370d..ecf88543fd 100644 --- a/lib/utils/task_queue/worker_queue.dart +++ b/lib/utils/task_queue/worker_queue.dart @@ -14,8 +14,12 @@ abstract class WorkerQueue { Queue get queue => _queue; - Future addTask(Task task) { - _queue.add(task); + Future addTask(Task task, {bool isFirstPriority = false}) { + if (isFirstPriority) { + _queue.addFirst(task); + } else { + _queue.add(task); + } Logs().i( 'WorkerQueue<$workerName>::addTask(): QUEUE_LENGTH: ${_queue.length}', ); From f062677c7d03f0dd58dead4129a8e8a69fa921ca Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Apr 2024 09:35:16 +0700 Subject: [PATCH 130/183] TW-1573: implement getFileEntity and storeEntity in database (cherry picked from commit ba9b6d03b4fc7b8a216ac360ba15f70deaaff9fb) --- .../flutter_hive_collections_database.dart | 41 ++++++++++++------- lib/widgets/mxc_image.dart | 20 ++++++--- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart index 6e485e40ca..536bf2492f 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -131,36 +131,47 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { bool get supportsFileStoring => !kIsWeb; @override - Future getFile(Uri mxcUri) async { + Future getFile(String eventId, String fileName) async { if (!supportsFileStoring) return null; - final tempDirectory = - await StorageDirectoryUtils.instance.getFileStoreDirectory(); - final file = - File('$tempDirectory/${Uri.encodeComponent(mxcUri.toString())}'); + final file = File( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: fileName, + ), + ); if (await file.exists() == false) return null; final bytes = await file.readAsBytes(); return bytes; } @override - Future storeFile(Uri mxcUri, Uint8List bytes, int time) async { + Future storeEventFile( + String eventId, + String fileName, + Uint8List bytes, + int time, + ) async { if (!supportsFileStoring) return null; - final tempDirectory = - await StorageDirectoryUtils.instance.getFileStoreDirectory(); - final file = - File('$tempDirectory/${Uri.encodeComponent(mxcUri.toString())}'); + final file = File( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: fileName, + ), + ); if (await file.exists()) return; await file.writeAsBytes(bytes); return; } @override - Future getFileEntity(Uri mxcUri) async { + Future getFileEntity(String eventId, String fileName) async { if (!supportsFileStoring) return null; - final tempDirectory = - await StorageDirectoryUtils.instance.getFileStoreDirectory(); - final file = - File('$tempDirectory/${Uri.encodeComponent(mxcUri.toString())}'); + final file = File( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: fileName, + ), + ); if (await file.exists() == false) return null; return file; } diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 799a29bfac..c0ede95b7a 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -133,10 +133,11 @@ class _MxcImageState extends State { ) : uri.getDownloadLink(client); - final storeKey = widget.isThumbnail ? httpUri : uri; - - if (_isCached == null) { - final cachedData = await client.database?.getFile(storeKey); + if (_isCached == null && widget.event != null) { + final cachedData = await client.database?.getFile( + event!.eventId, + widget.isThumbnail ? event.thumbnailFilename : event.filename, + ); if (cachedData != null) { if (!mounted) return; setState(() { @@ -161,12 +162,19 @@ class _MxcImageState extends State { setState(() { _imageData = remoteData; }); - await client.database?.storeFile(storeKey, remoteData, 0); + if (widget.event != null) { + await client.database?.storeEventFile( + widget.event!.eventId, + event!.filename, + remoteData, + 0, + ); + } } if (event != null) { if (!PlatformInfos.isWeb) { - final fileInfo = await event.getMediaFileInfo( + final fileInfo = await event.getFileInfo( getThumbnail: widget.isThumbnail, ); if (fileInfo != null && fileInfo.filePath.isNotEmpty) { From 6167d79cb812e441971cc3b64c7fd8c34b66ae6a Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Apr 2024 09:36:21 +0700 Subject: [PATCH 131/183] TW-1573: disable media app bar when downloading video (cherry picked from commit 6c0f402ef53a38cca42665283deaf6e0b3a515f5) --- .../chat/events/download_video_widget.dart | 187 +++++++++--------- 1 file changed, 91 insertions(+), 96 deletions(-) diff --git a/lib/pages/chat/events/download_video_widget.dart b/lib/pages/chat/events/download_video_widget.dart index 2b8dfc0195..22d3121236 100644 --- a/lib/pages/chat/events/download_video_widget.dart +++ b/lib/pages/chat/events/download_video_widget.dart @@ -6,7 +6,6 @@ import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_web.dart'; import 'package:fluffychat/presentation/mixins/handle_video_download_mixin.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; -import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; @@ -34,7 +33,7 @@ class _DownloadVideoWidgetState extends State final downloadProgressNotifier = ValueNotifier(0.0); final cancelToken = CancelToken(); - final ValueNotifier showAppbarPreview = ValueNotifier(true); + final ValueNotifier showAppbarPreview = ValueNotifier(false); @override void initState() { @@ -89,108 +88,104 @@ class _DownloadVideoWidgetState extends State Widget build(BuildContext context) { return Material( color: Colors.black, - child: GestureDetector( - onTap: () => showAppbarPreview.toggle(), - child: Stack( - alignment: Alignment.topLeft, - children: [ - Stack( - alignment: Alignment.center, - children: [ - MxcImage( - event: widget.event, - fit: BoxFit.cover, - ), - ValueListenableBuilder( - valueListenable: _downloadStateNotifier, - builder: (context, downloadState, child) { - switch (downloadState) { - case DownloadVideoState.loading: - return InkWell( - onTap: downloadState == DownloadVideoState.loading - ? null - : _downloadAction, - child: Center( - child: Stack( - children: [ - const CenterVideoButton( - icon: Icons.play_arrow, - ), - SizedBox( - width: - MessageContentStyle.videoCenterButtonSize, - height: - MessageContentStyle.videoCenterButtonSize, - child: ValueListenableBuilder( - valueListenable: downloadProgressNotifier, - builder: (context, progress, child) { - return CircularProgressIndicator( - strokeWidth: 2, - color: LinagoraRefColors.material() - .primary[100], - value: PlatformInfos.isWeb - ? null - : progress, - ); - }, - ), + child: Stack( + alignment: Alignment.topLeft, + children: [ + Stack( + alignment: Alignment.center, + children: [ + MxcImage( + event: widget.event, + fit: BoxFit.cover, + ), + ValueListenableBuilder( + valueListenable: _downloadStateNotifier, + builder: (context, downloadState, child) { + switch (downloadState) { + case DownloadVideoState.loading: + return InkWell( + onTap: downloadState == DownloadVideoState.loading + ? null + : _downloadAction, + child: Center( + child: Stack( + children: [ + const CenterVideoButton( + icon: Icons.play_arrow, + ), + SizedBox( + width: + MessageContentStyle.videoCenterButtonSize, + height: + MessageContentStyle.videoCenterButtonSize, + child: ValueListenableBuilder( + valueListenable: downloadProgressNotifier, + builder: (context, progress, child) { + return CircularProgressIndicator( + strokeWidth: 2, + color: LinagoraRefColors.material() + .primary[100], + value: + PlatformInfos.isWeb ? null : progress, + ); + }, ), - ], - ), + ), + ], ), - ); - case DownloadVideoState.initial: - return InkWell( - onTap: _downloadAction, - child: const Center( - child: CenterVideoButton( - icon: Icons.play_arrow, - ), + ), + ); + case DownloadVideoState.initial: + return InkWell( + onTap: _downloadAction, + child: const Center( + child: CenterVideoButton( + icon: Icons.play_arrow, ), - ); - case DownloadVideoState.done: - return InkWell( - onTap: () { - if (path != null) { - playVideoAction( - context, - path!, - event: widget.event, - ); - } - }, - child: const Center( - child: CenterVideoButton( - icon: Icons.play_arrow, - ), + ), + ); + case DownloadVideoState.done: + return InkWell( + onTap: () { + if (path != null) { + playVideoAction( + context, + path!, + event: widget.event, + ); + } + }, + child: const Center( + child: CenterVideoButton( + icon: Icons.play_arrow, ), - ); - case DownloadVideoState.failed: - return InkWell( - onTap: _downloadAction, - child: const Center( - child: CenterVideoButton( - icon: Icons.error, - ), + ), + ); + case DownloadVideoState.failed: + return InkWell( + onTap: _downloadAction, + child: const Center( + child: CenterVideoButton( + icon: Icons.error, ), - ); - } - }, - ), - ], - ), - if (PlatformInfos.isMobile) ...[ - MediaViewerAppBar( - showAppbarPreviewNotifier: showAppbarPreview, - event: widget.event, - ), - ] else ...[ - MediaViewerAppBarWeb( - event: widget.event, + ), + ); + } + }, ), ], + ), + if (PlatformInfos.isMobile) ...[ + MediaViewerAppBar( + showAppbarPreviewNotifier: showAppbarPreview, + event: widget.event, + ), + ] else ...[ + MediaViewerAppBarWeb( + event: widget.event, + ), ], - ), + ], ), ); } From 5bfb5e0c53f457e20a9a6e098755f96ded2a75e4 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Apr 2024 09:37:17 +0700 Subject: [PATCH 132/183] TW-1573: apply save to gallery to other place contains media (cherry picked from commit a57bbd7efe2e9ce7bb308d070ad30d717833b355) --- lib/pages/chat/chat.dart | 161 +------------ lib/pages/chat/events/message_content.dart | 2 +- .../chat/events/message_download_content.dart | 2 +- .../image_viewer/media_viewer_app_bar.dart | 7 +- .../media_viewer_app_bar_web.dart | 8 +- .../extensions/send_file_extension.dart | 23 +- .../mixins/media_viewer_app_bar_mixin.dart | 12 +- ..._file_to_twake_downloads_folder_mixin.dart | 152 ++++++++++--- .../save_media_to_gallery_android_mixin.dart | 138 ++++++++++-- .../storage_directory_manager.dart} | 0 .../download_file_extension.dart | 212 ++++++------------ .../flutter_hive_collections_database.dart | 2 +- macos/Podfile.lock | 7 + pubspec.lock | 2 +- test/files/get_available_file_path_test.dart | 2 +- 15 files changed, 367 insertions(+), 363 deletions(-) rename lib/utils/{storage_directory_utils.dart => manager/storage_directory_manager.dart} (100%) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 43fc1c5f33..747f711416 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; import 'package:fluffychat/presentation/extensions/event_update_extension.dart'; @@ -9,18 +8,12 @@ import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; import 'package:fluffychat/presentation/mixins/save_media_to_gallery_android_mixin.dart'; import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; import 'package:fluffychat/presentation/model/chat/view_event_list_ui_state.dart'; -import 'package:fluffychat/utils/exception/storage_permission_exception.dart'; import 'package:fluffychat/utils/extension/basic_event_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; -import 'package:fluffychat/utils/permission_dialog.dart'; -import 'package:fluffychat/utils/permission_service.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:fluffychat/utils/extension/global_key_extension.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:universal_html/html.dart' as html; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -1786,11 +1779,12 @@ class ChatController extends State List> appBarActionsBuilder() { final listAction = [ if (PlatformInfos.isAndroid) ...[ - if (selectedEvents - .every((event) => event.hasAttachment && !event.isVideoOrImage)) + if (selectedEvents.length == 1 && + selectedEvents.first.hasAttachment && + !selectedEvents.first.isVideoOrImage) ChatAppBarActions.saveToDownload, ], - if (selectedEvents.every((event) => event.isVideoOrImage)) + if (selectedEvents.length == 1 && selectedEvents.first.isVideoOrImage) ChatAppBarActions.saveToGallery, ChatAppBarActions.info, ChatAppBarActions.report, @@ -1821,12 +1815,15 @@ class ChatController extends State switch (action) { case ChatAppBarActions.saveToDownload: actionWithClearSelections( - () => saveSelectedEventToDownloadAndroid(), + () => saveSelectedEventToDownloadAndroid( + context, + selectedEvents.first, + ), ); break; case ChatAppBarActions.saveToGallery: actionWithClearSelections( - () => saveSelectedEventToGallery(), + () => saveSelectedEventToGallery(context, selectedEvents.first), ); break; case ChatAppBarActions.info: @@ -1847,146 +1844,6 @@ class ChatController extends State } } - Future handleAndroidStoragePermission() async { - if (await PermissionHandlerService() - .isUserHaveToRequestStoragePermissionAndroid()) { - final permission = await Permission.storage.request(); - - if (permission.isPermanentlyDenied) { - showDialog( - useRootNavigator: false, - context: context, - builder: (_) { - return PermissionDialog( - icon: const Icon(Icons.storage_rounded), - permission: Permission.storage, - explainTextRequestPermission: Text( - L10n.of(context)!.explainPermissionToDownloadFiles( - AppConfig.applicationName, - ), - ), - onAcceptButton: () => - PermissionHandlerService().goToSettingsForPermissionActions(), - ); - }, - ); - } - - if (!permission.isGranted) { - Logs().i( - 'Chat::saveSelectedEventToDownloadAndroid():: Permission Denied', - ); - throw StoragePermissionException("Don't have permission to save file"); - } - } - } - - void saveSelectedEventToDownloadAndroid() async { - if (selectedEvents.length != 1) { - return; - } - final downloadEvent = selectedEvents.first; - try { - await handleAndroidStoragePermission(); - - final downloadManager = getIt.get(); - final downloadingStreamSubscription = - downloadManager.getDownloadStateStream( - downloadEvent.eventId, - ); - if (downloadingStreamSubscription == null) { - await handleSaveToDownloadsForFileNotInDownloading( - downloadEvent, - context: context, - ); - return; - } - - handleSaveToDownloadForDownloadingFile( - downloadingStreamSubscription: downloadingStreamSubscription, - event: downloadEvent, - context: context, - ); - } catch (e) { - Logs().e('Chat::saveSelectedEventToDownloadAndroid(): $e'); - if (e is! StoragePermissionException) { - TwakeSnackBar.show( - context, - L10n.of(context)!.saveFileToDownloadsError, - ); - } - } - } - - Future handlePhotoPermissionIOS() async { - final permissionHandlerService = PermissionHandlerService(); - final permissionStatus = - await permissionHandlerService.requestPhotoAddOnlyPermissionIOS(); - if (permissionStatus.isPermanentlyDenied) { - showDialog( - useRootNavigator: false, - context: context, - builder: (_) { - return PermissionDialog( - icon: const Icon(Icons.photo), - permission: Permission.photos, - explainTextRequestPermission: Text( - L10n.of(context)!.explainPermissionToGallery( - AppConfig.applicationName, - ), - ), - onAcceptButton: () => - permissionHandlerService.goToSettingsForPermissionActions(), - ); - }, - ); - } - if (!permissionStatus.isGranted) { - throw StoragePermissionException('Permission denied'); - } - } - - Future saveSelectedEventToGallery() async { - if (selectedEvents.length != 1) { - return; - } - final downloadEvent = selectedEvents.first; - - try { - if (PlatformInfos.isAndroid) { - await handleAndroidStoragePermission(); - } else { - await handlePhotoPermissionIOS(); - } - final mxcFile = await getMediaFile(downloadEvent); - if (await mxcFile.exists() && - await mxcFile.length() == downloadEvent.getFileSize()) { - final fileInDownloadsInApp = await getFileInDownloadsInAppFolder( - mxcFile: mxcFile, - downloadEvent: downloadEvent, - ); - if (downloadEvent.messageType == MessageTypes.Image) { - await saveImageToGallery(file: fileInDownloadsInApp); - } else if (downloadEvent.messageType == MessageTypes.Video) { - await saveVideoToGallery(file: fileInDownloadsInApp); - } - - TwakeSnackBar.show( - context, - L10n.of(context)!.fileSavedToGallery, - ); - } - } catch (e) { - Logs().e('Chat::saveSelectedEventToGallery(): $e'); - if (e is! StoragePermissionException) { - TwakeSnackBar.show( - context, - L10n.of(context)!.saveFileToDownloadsError, - ); - } - } - } - @override void initState() { _initializePinnedEvents(); diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 84b67e4eea..a3d2f169c1 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -295,7 +295,7 @@ class _MessageImageBuilder extends StatelessWidget { onTapSelectMode: onTapSelectMode, onTapPreview: onTapPreview, animated: true, - thumbnailOnly: false, + thumbnailOnly: true, ); } diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 63eb4481d5..e3288f1523 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/utils/manager/download_manager/download_file_state.da import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; diff --git a/lib/pages/image_viewer/media_viewer_app_bar.dart b/lib/pages/image_viewer/media_viewer_app_bar.dart index 1a58e95f9c..a54b767670 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar.dart @@ -1,6 +1,8 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar_view.dart'; import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; +import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; +import 'package:fluffychat/presentation/mixins/save_media_to_gallery_android_mixin.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -24,7 +26,10 @@ class MediaViewerAppBar extends StatefulWidget { } class MediaViewerAppBarController extends State - with MediaViewerAppBarMixin { + with + SaveFileToTwakeAndroidDownloadsFolderMixin, + SaveMediaToGalleryAndroidMixin, + MediaViewerAppBarMixin { ValueNotifier? showAppbarPreview; @override diff --git a/lib/pages/image_viewer/media_viewer_app_bar_web.dart b/lib/pages/image_viewer/media_viewer_app_bar_web.dart index f8db6c5be6..02ae3585b7 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_web.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_web.dart @@ -1,4 +1,6 @@ import 'package:fluffychat/presentation/mixins/media_viewer_app_bar_mixin.dart'; +import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; +import 'package:fluffychat/presentation/mixins/save_media_to_gallery_android_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/resource/image_paths.dart'; @@ -10,7 +12,11 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; -class MediaViewerAppBarWeb extends StatelessWidget with MediaViewerAppBarMixin { +class MediaViewerAppBarWeb extends StatelessWidget + with + SaveFileToTwakeAndroidDownloadsFolderMixin, + SaveMediaToGalleryAndroidMixin, + MediaViewerAppBarMixin { final Event? event; MediaViewerAppBarWeb({super.key, this.event}); diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index 221fbb8a18..1f0aed96a3 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/presentation/extensions/image_extension.dart'; import 'package:fluffychat/presentation/fake_sending_file_info.dart'; import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:flutter/widgets.dart'; import 'package:image/image.dart' as img; import 'package:blurhash_dart/blurhash_dart.dart'; @@ -308,11 +308,21 @@ extension SendFileExtension on Room { required String fileName, }) async { try { - final filePathInAppDownloads = - await StorageDirectoryUtils.instance.getFilePathInAppDownloads( - eventId: eventId, - fileName: fileName, - ); + String? filePathInAppDownloads; + if (isRoomEncrypted()) { + filePathInAppDownloads = + await StorageDirectoryManager.instance.getDecryptedFilePath( + eventId: eventId, + fileName: fileName, + ); + } else { + filePathInAppDownloads = + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: fileName, + ); + } + final sendingFilePath = sendingFilePlaceholders[sendingEventId]?.filePath; final file = File(filePathInAppDownloads); if (await file.exists() || sendingFilePath == null) { @@ -323,7 +333,6 @@ extension SendFileExtension on Room { Logs().d('File copied in app downloads folder', filePathInAppDownloads); } catch (e) { Logs().e('Error while copying file in app downloads folder', e); - rethrow; } } diff --git a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart index 60d8f02dbd..9bd069b01f 100644 --- a/lib/presentation/mixins/media_viewer_app_bar_mixin.dart +++ b/lib/presentation/mixins/media_viewer_app_bar_mixin.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/forward/forward.dart'; import 'package:fluffychat/pages/forward/forward_web_view.dart'; import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; +import 'package:fluffychat/presentation/mixins/save_media_to_gallery_android_mixin.dart'; import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -12,7 +13,7 @@ import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -mixin MediaViewerAppBarMixin { +mixin MediaViewerAppBarMixin on SaveMediaToGalleryAndroidMixin { final MenuController menuController = MenuController(); final responsiveUtils = getIt.get(); @@ -149,8 +150,15 @@ mixin MediaViewerAppBarMixin { void saveFileAction( BuildContext context, Event? event, - ) => + ) { + if (PlatformInfos.isWeb) { event?.saveFile(context); + } else { + if (event != null) { + saveSelectedEventToGallery(context, event); + } + } + } void shareFileAction( BuildContext context, diff --git a/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart b/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart index ef66360b25..e2b5cd7299 100644 --- a/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart +++ b/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart @@ -2,21 +2,61 @@ import 'dart:async'; import 'dart:io'; import 'package:dartz/dartz.dart'; -import 'package:dio/dio.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/dialog/downloading_file_dialog.dart'; import 'package:fluffychat/utils/exception/save_to_downloads_exception.dart'; +import 'package:fluffychat/utils/exception/storage_permission_exception.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/permission_dialog.dart'; +import 'package:fluffychat/utils/permission_service.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:permission_handler/permission_handler.dart'; mixin SaveFileToTwakeAndroidDownloadsFolderMixin { + void saveSelectedEventToDownloadAndroid( + BuildContext context, + Event downloadEvent, + ) async { + try { + await handleAndroidStoragePermission(context); + + final downloadManager = getIt.get(); + final downloadingStreamSubscription = + downloadManager.getDownloadStateStream( + downloadEvent.eventId, + ); + if (downloadingStreamSubscription == null) { + await handleSaveToDownloadsForFileNotInDownloading( + downloadEvent, + context: context, + ); + return; + } + + handleSaveToDownloadForDownloadingFile( + downloadingStreamSubscription: downloadingStreamSubscription, + event: downloadEvent, + context: context, + ); + } catch (e) { + Logs().e('Chat::saveSelectedEventToDownloadAndroid(): $e'); + if (e is! StoragePermissionException) { + TwakeSnackBar.show( + context, + L10n.of(context)!.saveFileToDownloadsError, + ); + } + } + } + void handleSaveToDownloadForDownloadingFile({ required BuildContext context, required Stream> downloadingStreamSubscription, @@ -24,15 +64,18 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { }) { final downloadProgressNotifier = ValueNotifier(0); StreamSubscription? streamSubscription; - streamSubscription = downloadingStreamSubscription.listen((downloadState) { - _onDownloadingFileStateChange( - event: event, - downloadState: downloadState, - context: context, - downloadProgressNotifier: downloadProgressNotifier, - streamSubscription: streamSubscription, - ); - }); + streamSubscription = downloadingStreamSubscription.listen( + (downloadState) { + _onDownloadingFileStateChange( + event: event, + downloadState: downloadState, + context: context, + downloadProgressNotifier: downloadProgressNotifier, + streamSubscription: streamSubscription, + ); + }, + cancelOnError: true, + ); showDialog( context: context, @@ -50,13 +93,22 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { required BuildContext context, }) async { final filePath = - await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: event.eventId, fileName: event.filename, ); final file = File(filePath); if (!await file.exists()) { - await _handleWhenFileHaveNotDownloaded(event, context); + await handleWhenFileHaveNotDownloaded( + event, + context, + handleDownloadFileDone: (event, context) async { + return await handleSaveToDownloadsForFileNotInDownloading( + event, + context: context, + ); + }, + ); return; } await handleSaveToDownloadsFolderWhenFileExisted(event, file, context); @@ -68,13 +120,13 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { BuildContext context, ) async { try { - final twakeFolder = await StorageDirectoryUtils.instance + final twakeFolder = await StorageDirectoryManager.instance .getTwakeDownloadsFolderInDevice(); if (twakeFolder?.isNotEmpty != true) { throw SaveToDownloadsException(error: 'Twake folder is empty'); } final twakeFilePath = - await StorageDirectoryUtils.instance.getAvailableFilePath( + await StorageDirectoryManager.instance.getAvailableFilePath( '$twakeFolder/${event.filename}', ); @@ -92,25 +144,33 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { } } - Future _handleWhenFileHaveNotDownloaded( + Future handleWhenFileHaveNotDownloaded( Event event, - BuildContext context, - ) async { + BuildContext context, { + Future Function(Event event, BuildContext context)? + handleDownloadFileDone, + }) async { Logs().d( 'Chat::saveSelectedEventToDownloadAndroid():: File not exists', ); + final downloadManager = getIt.get(); + await downloadManager.download( + event: event, + isFirstPriority: true, + ); + final downloadStreamController = - StreamController>(); - final cancelDownloadToken = CancelToken(); + downloadManager.getDownloadStateStream(event.eventId); StreamSubscription? streamSubcription; final downloadProgressNotifier = ValueNotifier(0); - streamSubcription = downloadStreamController.stream.listen((downloadState) { + streamSubcription = downloadStreamController?.listen((downloadState) { _onDownloadingFileStateChange( event: event, downloadState: downloadState, context: context, downloadProgressNotifier: downloadProgressNotifier, streamSubscription: streamSubcription, + handleDownloadFileDone: handleDownloadFileDone, ); }); showDialog( @@ -120,13 +180,8 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { parentContext: context, eventId: event.eventId, downloadProgressNotifier: downloadProgressNotifier, - cancelDownloadToken: cancelDownloadToken, ), ); - await event.getFileInfo( - downloadStreamController: downloadStreamController, - cancelToken: cancelDownloadToken, - ); } void _onDownloadingFileStateChange({ @@ -135,6 +190,8 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { required BuildContext context, required ValueNotifier downloadProgressNotifier, required StreamSubscription? streamSubscription, + Future Function(Event event, BuildContext context)? + handleDownloadFileDone, }) { downloadState.fold( (left) { @@ -156,10 +213,7 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { streamSubscription: streamSubscription, ); Navigator.of(context, rootNavigator: true).pop(); - await handleSaveToDownloadsForFileNotInDownloading( - event, - context: context, - ); + await handleDownloadFileDone?.call(event, context); } }, ); @@ -172,4 +226,38 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { downloadProgressNotifier?.dispose(); streamSubscription?.cancel(); } + + Future handleAndroidStoragePermission(BuildContext context) async { + if (await PermissionHandlerService() + .isUserHaveToRequestStoragePermissionAndroid()) { + final permission = await Permission.storage.request(); + + if (permission.isPermanentlyDenied) { + showDialog( + useRootNavigator: false, + context: context, + builder: (_) { + return PermissionDialog( + icon: const Icon(Icons.storage_rounded), + permission: Permission.storage, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToDownloadFiles( + AppConfig.applicationName, + ), + ), + onAcceptButton: () => + PermissionHandlerService().goToSettingsForPermissionActions(), + ); + }, + ); + } + + if (!permission.isGranted) { + Logs().i( + 'Chat::saveSelectedEventToDownloadAndroid():: Permission Denied', + ); + throw StoragePermissionException("Don't have permission to save file"); + } + } + } } diff --git a/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart b/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart index d90d94523f..cee82bc2e5 100644 --- a/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart +++ b/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart @@ -1,38 +1,89 @@ import 'dart:io'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/presentation/extensions/send_file_extension.dart'; +import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; import 'package:fluffychat/utils/exception/save_to_gallery_exception.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/exception/storage_permission_exception.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/permission_dialog.dart'; +import 'package:fluffychat/utils/permission_service.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:gal/gal.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:permission_handler/permission_handler.dart'; -mixin SaveMediaToGalleryAndroidMixin { - Future getFileInDownloadsInAppFolder({ - required File mxcFile, - required Event downloadEvent, - }) async { - final fileNameInAppDownloads = - await StorageDirectoryUtils.instance.getFilePathInAppDownloads( - eventId: downloadEvent.eventId, - fileName: downloadEvent.filename, - ); - final file = File(fileNameInAppDownloads); - - if (!await file.exists() || await file.length() != await mxcFile.length()) { - await file.create(recursive: true); - await mxcFile.copy(fileNameInAppDownloads); +mixin SaveMediaToGalleryAndroidMixin + on SaveFileToTwakeAndroidDownloadsFolderMixin { + Future saveSelectedEventToGallery( + BuildContext context, + Event downloadEvent, + ) async { + try { + if (PlatformInfos.isAndroid) { + await handleAndroidStoragePermission(context); + } else if (PlatformInfos.isIOS) { + await handlePhotoPermissionIOS(context); + } + final fileInDownloadsInApp = await getCachedMediaFile(downloadEvent); + if (!await fileInDownloadsInApp.exists() || + await fileInDownloadsInApp.length() != downloadEvent.getFileSize()) { + await handleWhenFileHaveNotDownloaded( + downloadEvent, + context, + handleDownloadFileDone: (event, context) async { + await saveMediaToGallery( + context: context, + messageType: event.messageType, + fileInDownloadsInApp: fileInDownloadsInApp, + ); + }, + ); + return; + } + await saveMediaToGallery( + context: context, + messageType: downloadEvent.messageType, + fileInDownloadsInApp: fileInDownloadsInApp, + ); + } catch (e) { + Logs().e('Chat::saveSelectedEventToGallery(): $e'); + if (e is! StoragePermissionException) { + TwakeSnackBar.show( + context, + L10n.of(context)!.saveFileToDownloadsError, + ); + } } - return file; } - Future getMediaFile(Event event) async { + Future getCachedMediaFile(Event event) async { if (event.attachmentMxcUrl == null) { throw SaveToGalleryException( error: 'File not found', ); } - final filePath = await StorageDirectoryUtils.instance - .getMediaFilePath(mxcUrl: event.attachmentMxcUrl!); + if (event.room.isRoomEncrypted()) { + final filePath = + await StorageDirectoryManager.instance.getDecryptedFilePath( + eventId: event.eventId, + fileName: event.filename, + ); + final file = File(filePath); + if (await file.exists()) { + return file; + } + } + + final filePath = + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: event.eventId, + fileName: event.filename, + ); return File(filePath); } @@ -52,4 +103,49 @@ mixin SaveMediaToGalleryAndroidMixin { file.path, ); } + + Future saveMediaToGallery({ + required BuildContext context, + required File fileInDownloadsInApp, + required String messageType, + }) async { + if (messageType == MessageTypes.Image) { + await saveImageToGallery(file: fileInDownloadsInApp); + } else if (messageType == MessageTypes.Video) { + await saveVideoToGallery(file: fileInDownloadsInApp); + } + + TwakeSnackBar.show( + context, + L10n.of(context)!.fileSavedToGallery, + ); + } + + Future handlePhotoPermissionIOS(BuildContext context) async { + final permissionHandlerService = PermissionHandlerService(); + final permissionStatus = + await permissionHandlerService.requestPhotoAddOnlyPermissionIOS(); + if (permissionStatus.isPermanentlyDenied) { + showDialog( + useRootNavigator: false, + context: context, + builder: (_) { + return PermissionDialog( + icon: const Icon(Icons.photo), + permission: Permission.photos, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToGallery( + AppConfig.applicationName, + ), + ), + onAcceptButton: () => + permissionHandlerService.goToSettingsForPermissionActions(), + ); + }, + ); + } + if (!permissionStatus.isGranted) { + throw StoragePermissionException('Permission denied'); + } + } } diff --git a/lib/utils/storage_directory_utils.dart b/lib/utils/manager/storage_directory_manager.dart similarity index 100% rename from lib/utils/storage_directory_utils.dart rename to lib/utils/manager/storage_directory_manager.dart diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 2ebe5f01e2..06f23afc24 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -9,8 +9,7 @@ import 'package:fluffychat/data/network/media/cancel_exception.dart'; import 'package:fluffychat/data/network/media/media_api.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:matrix/matrix.dart'; extension DownloadFileExtension on Event { @@ -37,16 +36,22 @@ extension DownloadFileExtension on Event { Future downloadOrRetrieveAttachment( Uri mxcUrl, String savePath, { - required StreamController> + required StreamController>? downloadStreamController, + ProgressCallback? progressCallback, bool getThumbnail = false, CancelToken? cancelToken, + required String filename, }) async { - final database = room.client.database; - final attachment = await database?.getFileEntity(mxcUrl); + final attachment = File( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( + eventId: eventId, + fileName: filename, + ), + ); final downloadLink = mxcUrl.getDownloadLink(room.client); - if (attachment != null) { + if (await attachment.exists()) { if (await attachment.length() == getFileSize(getThumbnail: getThumbnail)) { return FileInfo( @@ -64,7 +69,8 @@ extension DownloadFileExtension on Event { uriPath: downloadLink, savePath: savePath, onReceiveProgress: (receive, total) { - downloadStreamController.add( + progressCallback?.call(receive, total); + downloadStreamController?.add( Right( DownloadingFileState( receive: receive, @@ -82,10 +88,12 @@ extension DownloadFileExtension on Event { content.tryGet('size') ?? await File(savePath).length(), ); await _handleDownloadFileDone( - this, - fileInfo, - downloadStreamController, - savePath, + mxcUrl: mxcUrl, + fileInfo: fileInfo, + savePath: savePath, + filename: filename, + streamController: downloadStreamController, + getThumbnail: getThumbnail, ); return fileInfo; } @@ -99,21 +107,25 @@ extension DownloadFileExtension on Event { return null; } - Future _handleDownloadFileDone( - Event event, - FileInfo fileInfo, - StreamController> streamController, - String savePath, - ) async { - if (event.isAttachmentEncrypted) { + Future _handleDownloadFileDone({ + required Uri mxcUrl, + required String savePath, + required String filename, + required FileInfo fileInfo, + getThumbnail = false, + StreamController>? streamController, + }) async { + if (isAttachmentEncrypted) { await _handleEncryptedFileEvent( - streamController, - event, - fileInfo, - savePath, + mxcUrl: mxcUrl, + streamController: streamController, + fileInfo: fileInfo, + savePath: savePath, + filename: filename, + getThumbnail: getThumbnail, ); } else { - streamController.add( + streamController?.add( Right( DownloadNativeFileSuccessState( filePath: fileInfo.filePath, @@ -124,22 +136,28 @@ extension DownloadFileExtension on Event { return; } - Future _handleEncryptedFileEvent( - StreamController> streamController, - Event event, - FileInfo fileInfo, - String savePath, - ) async { - streamController.add( + Future _handleEncryptedFileEvent({ + required Uri mxcUrl, + required FileInfo fileInfo, + required String savePath, + required String filename, + bool getThumbnail = false, + StreamController>? streamController, + }) async { + streamController?.add( const Right( DecryptingFileState(), ), ); try { - final decryptedFile = await event.decryptFile( + final decryptedFile = await decryptFile( fileInfo, - event.getAttachmentOrThumbnailMxcUrl()!, - StorageDirectoryUtils.instance.getDecryptedFilePath(savePath: savePath), + mxcUrl, + await StorageDirectoryManager.instance.getDecryptedFilePath( + eventId: eventId, + fileName: filename, + ), + getThumbnail: getThumbnail, ); if (decryptedFile == null) { throw Exception( @@ -147,11 +165,12 @@ extension DownloadFileExtension on Event { ); } final saveFile = File( - StorageDirectoryUtils.instance.getDecryptedFilePath( - savePath: savePath, + await StorageDirectoryManager.instance.getDecryptedFilePath( + eventId: eventId, + fileName: filename, ), ).copySync(savePath); - streamController.add( + streamController?.add( Right( DownloadNativeFileSuccessState( filePath: saveFile.path, @@ -162,7 +181,7 @@ extension DownloadFileExtension on Event { Logs().e( 'DownloadManager::_handleEncryptedFileEvent(): $e', ); - streamController.add( + streamController?.add( Left( DownloadFileFailureState(exception: e), ), @@ -170,54 +189,6 @@ extension DownloadFileExtension on Event { } } - Future downloadOrRetrieveAttachmentForMedia( - Uri mxcUrl, - String savePath, { - ProgressCallback? progressCallback, - CancelToken? cancelToken, - bool getThumbnail = false, - }) async { - final database = room.client.database; - final attachment = await database?.getFileEntity(mxcUrl); - - final mediaApi = getIt.get(); - final downloadLink = mxcUrl.getDownloadLink(room.client); - - if (attachment != null) { - if (await attachment.length() == - getFileSize(getThumbnail: getThumbnail)) { - return FileInfo( - filename, - attachment.path, - getFileSize(getThumbnail: getThumbnail), - ); - } else { - await attachment.delete(); - } - } - try { - final downloadResponse = await mediaApi.downloadFileInfo( - uriPath: downloadLink, - savePath: savePath, - cancelToken: cancelToken, - onReceiveProgress: progressCallback, - ); - if (downloadResponse.statusCode == 200) { - return FileInfo( - filename, - savePath, - content.tryGet('size') ?? await File(savePath).length(), - ); - } - throw ('getFileInfo: Download file $filename failed'); - } catch (e) { - if (e is CancelRequestException) { - Logs().i("downloadOrRetrieveAttachment: user cancel the download"); - } - } - return null; - } - // Decrypt the file if it's encrypted. Future decryptFile( FileInfo? fileInfo, @@ -254,44 +225,7 @@ extension DownloadFileExtension on Event { Future getFileInfo({ getThumbnail = false, - required StreamController> - downloadStreamController, - CancelToken? cancelToken, - }) async { - if (!canContainAttachment()) { - throw ("getFileInfo: This event has the type '$type' and so it can't contain an attachment."); - } - - if (isSending()) { - final localFile = room.sendingFilePlaceholders[eventId]; - if (localFile != null) return FileInfo.fromMatrixFile(localFile); - } - - final mxcUrl = getAttachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail); - if (mxcUrl == null) { - throw "getFileInfo: This event hasn't any attachment or thumbnail."; - } - - final isFileEncrypted = - getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted; - if (isEncryptionDisabled(isFileEncrypted)) { - throw ('getFileInfo: Encryption is not enabled in your Client.'); - } - - return downloadOrRetrieveAttachment( - mxcUrl, - await StorageDirectoryUtils.instance.getFilePathInAppDownloads( - eventId: eventId, - fileName: filename, - ), - downloadStreamController: downloadStreamController, - getThumbnail: getThumbnail, - cancelToken: cancelToken, - ); - } - - Future getMediaFileInfo({ - getThumbnail = false, + StreamController>? downloadStreamController, ProgressCallback? progressCallback, CancelToken? cancelToken, }) async { @@ -303,6 +237,7 @@ extension DownloadFileExtension on Event { final localFile = room.sendingFilePlaceholders[eventId]; if (localFile != null) return FileInfo.fromMatrixFile(localFile); } + getThumbnail = getThumbnail && hasThumbnail; final mxcUrl = getAttachmentOrThumbnailMxcUrl(getThumbnail: getThumbnail); if (mxcUrl == null) { @@ -315,12 +250,13 @@ extension DownloadFileExtension on Event { throw ('getFileInfo: Encryption is not enabled in your Client.'); } + final filename = getThumbnail ? thumbnailFilename : this.filename; String? decryptedPath; if (isFileEncrypted) { - decryptedPath = StorageDirectoryUtils.instance.getDecryptedFilePath( - savePath: await StorageDirectoryUtils.instance.getMediaFilePath( - mxcUrl: mxcUrl, - ), + decryptedPath = + await StorageDirectoryManager.instance.getDecryptedFilePath( + eventId: eventId, + fileName: filename, ); final decryptedFile = File(decryptedPath); @@ -328,7 +264,7 @@ extension DownloadFileExtension on Event { final decryptedFileLength = await decryptedFile.length(); if (decryptedFileLength == getFileSize(getThumbnail: getThumbnail)) { return FileInfo( - body, + filename, decryptedPath, getFileSize(getThumbnail: getThumbnail), ); @@ -338,23 +274,15 @@ extension DownloadFileExtension on Event { } } - final fileInfo = await downloadOrRetrieveAttachmentForMedia( + return downloadOrRetrieveAttachment( mxcUrl, - await StorageDirectoryUtils.instance.getMediaFilePath(mxcUrl: mxcUrl), - progressCallback: progressCallback, + await StorageDirectoryManager.instance + .getFilePathInAppDownloads(eventId: eventId, fileName: filename), + downloadStreamController: downloadStreamController, getThumbnail: getThumbnail, + progressCallback: progressCallback, cancelToken: cancelToken, + filename: filename, ); - - if (isFileEncrypted && fileInfo != null && decryptedPath != null) { - return await decryptFile( - fileInfo, - mxcUrl, - decryptedPath, - getThumbnail: getThumbnail, - ); - } - - return fileInfo; } } diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart index 536bf2492f..f19c6df01c 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -13,7 +13,7 @@ import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { FlutterHiveCollectionsDatabase( diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a8672602d9..34d9e2f1c9 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -51,6 +51,9 @@ PODS: - FlutterMacOS - WebRTC-SDK (= 114.5735.08) - FlutterMacOS (1.0.0) + - gal (1.0.0): + - Flutter + - FlutterMacOS - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) @@ -140,6 +143,7 @@ DEPENDENCIES: - flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`) - macos_ui (from `Flutter/ephemeral/.symlinks/plugins/macos_ui/macos`) @@ -213,6 +217,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos FlutterMacOS: :path: Flutter/ephemeral + gal: + :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin irondash_engine_context: :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos just_audio: @@ -281,6 +287,7 @@ SPEC CHECKSUMS: flutter_web_auth_2: 2e1dc2d2139973e4723c5286ce247dd590390d70 flutter_webrtc: cf7dc44d26cbb5c5f1ae5f583dab545871f287f9 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 diff --git a/pubspec.lock b/pubspec.lock index 715e21fc64..bf8ca963a2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1615,7 +1615,7 @@ packages: description: path: "." ref: "twake-supported-0.22.6" - resolved-ref: "699db764273c113e9d508a1f2d148067aabc8101" + resolved-ref: "14fe4d3144e1d45561b8e65713b5e1fc35651af4" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" diff --git a/test/files/get_available_file_path_test.dart b/test/files/get_available_file_path_test.dart index 0b4f153eba..a6a38a3c1c 100644 --- a/test/files/get_available_file_path_test.dart +++ b/test/files/get_available_file_path_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:flutter_test/flutter_test.dart'; Future createMockFile(String filePath) async { From 23ea6c218873e5d0f659b56526d4f5a227af6f4a Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 16 Apr 2024 12:00:59 +0700 Subject: [PATCH 133/183] TW-1573: add DownloadFileInterceptor to remove duplicate request (cherry picked from commit 2feb6d11a29620c71421c645c386b87ea49f9788) --- .../dio_duplicate_request_exception.dart | 9 +++++ .../download_file_interceptor.dart | 39 +++++++++++++++++++ lib/data/network/media/media_api.dart | 4 ++ lib/di/global/get_it_initializer.dart | 5 +++ lib/di/global/network_di.dart | 5 +++ .../download_file_extension.dart | 8 +++- 6 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 lib/data/network/exception/dio_duplicate_request_exception.dart create mode 100644 lib/data/network/interceptor/download_file_interceptor.dart diff --git a/lib/data/network/exception/dio_duplicate_request_exception.dart b/lib/data/network/exception/dio_duplicate_request_exception.dart new file mode 100644 index 0000000000..91f224e51f --- /dev/null +++ b/lib/data/network/exception/dio_duplicate_request_exception.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; + +class DioDuplicateRequestException extends DioException { + DioDuplicateRequestException() + : super( + message: 'Download already in progress', + requestOptions: RequestOptions(), + ); +} diff --git a/lib/data/network/interceptor/download_file_interceptor.dart b/lib/data/network/interceptor/download_file_interceptor.dart new file mode 100644 index 0000000000..46d1042cc5 --- /dev/null +++ b/lib/data/network/interceptor/download_file_interceptor.dart @@ -0,0 +1,39 @@ +import 'package:dio/dio.dart'; +import 'package:fluffychat/data/network/exception/dio_duplicate_request_exception.dart'; +import 'package:matrix/matrix.dart'; + +class DownloadFileInterceptor extends InterceptorsWrapper { + DownloadFileInterceptor(); + + final _currentDownloads = {}; + + static const downloadPath = '_matrix/media/v3/download/'; + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + Logs().d('DownloadFileInterceptor::onRequest: ${options.path}'); + if (options.path.contains(downloadPath) && + _currentDownloads.contains(options.path)) { + handler.reject( + DioDuplicateRequestException(), + ); + return; + } + _currentDownloads.add(options.path); + super.onRequest(options, handler); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + _currentDownloads + .removeWhere((request) => request == err.requestOptions.path); + super.onError(err, handler); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + _currentDownloads + .removeWhere((request) => request == response.requestOptions.path); + super.onResponse(response, handler); + } +} diff --git a/lib/data/network/media/media_api.dart b/lib/data/network/media/media_api.dart index d74ab3d8f3..0d3ba6987b 100644 --- a/lib/data/network/media/media_api.dart +++ b/lib/data/network/media/media_api.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/data/model/media/download_file_response.dart'; import 'package:fluffychat/data/model/media/upload_file_json.dart'; import 'package:fluffychat/data/model/media/url_preview_response.dart'; import 'package:fluffychat/data/network/dio_client.dart'; +import 'package:fluffychat/data/network/exception/dio_duplicate_request_exception.dart'; import 'package:fluffychat/data/network/homeserver_endpoint.dart'; import 'package:fluffychat/data/network/media/cancel_exception.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; @@ -54,6 +55,9 @@ class MediaAPI { .onError((error, stackTrace) { if (error is DioException && error.type == DioExceptionType.cancel) { throw CancelRequestException(); + } else if (error is DioDuplicateRequestException) { + Logs().i('downloadFileInfo error: $error'); + throw DioDuplicateRequestException(); } else { Logs().i('downloadFileInfo error: $error'); throw Exception(error); diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 64ea8bd0ba..c87518f648 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -68,6 +68,7 @@ import 'package:fluffychat/domain/usecase/recovery/save_recovery_words_interacto import 'package:fluffychat/domain/usecase/room/chat_get_pinned_events_interactor.dart'; import 'package:fluffychat/domain/usecase/room/chat_room_search_interactor.dart'; import 'package:fluffychat/domain/usecase/room/create_new_group_chat_interactor.dart'; +import 'package:fluffychat/domain/usecase/room/download_media_file_interactor.dart'; import 'package:fluffychat/domain/usecase/room/timeline_search_event_interactor.dart'; import 'package:fluffychat/domain/usecase/room/update_group_chat_interactor.dart'; import 'package:fluffychat/domain/usecase/room/update_pinned_messages_interactor.dart'; @@ -345,6 +346,10 @@ class GetItInitializer { getIt.get(), ), ); + + getIt.registerSingleton( + DownloadMediaFileInteractor(), + ); } void _bindingControllers() { diff --git a/lib/di/global/network_di.dart b/lib/di/global/network_di.dart index 28613edfb7..368cfca0f9 100644 --- a/lib/di/global/network_di.dart +++ b/lib/di/global/network_di.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/data/network/dio_client.dart'; import 'package:fluffychat/data/network/homeserver_endpoint.dart'; import 'package:fluffychat/data/network/identity_endpoint.dart'; import 'package:fluffychat/data/network/interceptor/authorization_interceptor.dart'; +import 'package:fluffychat/data/network/interceptor/download_file_interceptor.dart'; import 'package:fluffychat/data/network/interceptor/matrix_dio_cache_interceptor.dart'; import 'package:fluffychat/data/network/interceptor/dynamic_url_interceptor.dart'; import 'package:fluffychat/di/base_di.dart'; @@ -87,6 +88,9 @@ class NetworkDI extends BaseDI { ), instanceName: memCacheDioInterceptorName, ); + get.registerSingleton( + DownloadFileInterceptor(), + ); } void _bindDio(GetIt get) { @@ -149,6 +153,7 @@ class NetworkDI extends BaseDI { ), ); dio.interceptors.add(get.get()); + dio.interceptors.add(get.get()); if (kDebugMode) { dio.interceptors .add(LogInterceptor(requestBody: true, responseBody: true)); diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 06f23afc24..00a58b8791 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -5,6 +5,7 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/data/network/exception/dio_duplicate_request_exception.dart'; import 'package:fluffychat/data/network/media/cancel_exception.dart'; import 'package:fluffychat/data/network/media/media_api.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; @@ -81,7 +82,7 @@ extension DownloadFileExtension on Event { }, cancelToken: cancelToken, ); - if (downloadResponse.statusCode == 200) { + if (downloadResponse.statusCode == 200 && await File(savePath).exists()) { final fileInfo = FileInfo( filename, savePath, @@ -101,8 +102,11 @@ extension DownloadFileExtension on Event { } catch (e) { if (e is CancelRequestException) { Logs().i("downloadOrRetrieveAttachment: user cancel the download"); + } else if (e is DioDuplicateRequestException) { + Logs().i("downloadOrRetrieveAttachment: duplicate request"); + } else { + Logs().e("downloadOrRetrieveAttachment: $e"); } - Logs().e("downloadOrRetrieveAttachment: $e"); } return null; } From fcfdf9ad12a7c586d054d019241b800cdd37fec8 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 16 Apr 2024 12:02:27 +0700 Subject: [PATCH 134/183] TW-1573: handle duplicate download request in image_viewer (cherry picked from commit ead1892e762ac85466177e75b9dcf7aa732f9eb7) --- .../download/download_file_state.dart | 20 ++++++++++ .../room/download_media_file_interactor.dart | 31 +++++++++++++++ lib/pages/image_viewer/image_viewer.dart | 38 +++++++++++++++++++ lib/pages/image_viewer/image_viewer_view.dart | 34 ++++++++++------- 4 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 lib/domain/app_state/download/download_file_state.dart create mode 100644 lib/domain/usecase/room/download_media_file_interactor.dart diff --git a/lib/domain/app_state/download/download_file_state.dart b/lib/domain/app_state/download/download_file_state.dart new file mode 100644 index 0000000000..46fc67ef4e --- /dev/null +++ b/lib/domain/app_state/download/download_file_state.dart @@ -0,0 +1,20 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; + +class DownloadMediaFileFailure extends Failure { + final dynamic exception; + + const DownloadMediaFileFailure({required this.exception}); + + @override + List get props => [exception]; +} + +class DownloadMediaFileSuccess extends Success { + const DownloadMediaFileSuccess({required this.filePath}); + + final String filePath; + + @override + List get props => [filePath]; +} diff --git a/lib/domain/usecase/room/download_media_file_interactor.dart b/lib/domain/usecase/room/download_media_file_interactor.dart new file mode 100644 index 0000000000..fd35813fc6 --- /dev/null +++ b/lib/domain/usecase/room/download_media_file_interactor.dart @@ -0,0 +1,31 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/domain/app_state/download/download_file_state.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:matrix/matrix.dart'; + +class DownloadMediaFileInteractor { + DownloadMediaFileInteractor(); + + Stream> execute({ + required Event event, + bool getThumbnail = false, + }) async* { + try { + final fileInfo = await event.getFileInfo( + getThumbnail: getThumbnail, + ); + if (fileInfo == null) { + yield const Left( + DownloadMediaFileFailure(exception: 'FileInfo is null'), + ); + return; + } + yield Right(DownloadMediaFileSuccess(filePath: fileInfo.filePath)); + } catch (error) { + Logs().e('DownloadMediaFileInteractor: execute(): $error'); + yield Left(DownloadMediaFileFailure(exception: error)); + } + } +} diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 946318b3d8..eaa40b8a17 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -1,5 +1,9 @@ +import 'dart:async'; import 'dart:typed_data'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/download/download_file_state.dart'; +import 'package:fluffychat/domain/usecase/room/download_media_file_interactor.dart'; import 'package:fluffychat/pages/forward/forward.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer_view.dart'; import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; @@ -32,13 +36,47 @@ class ImageViewerController extends State { final ValueNotifier showAppbarPreview = ValueNotifier(true); + String? filePath; + + final downloadMediaFileInteractor = getIt.get(); + + StreamSubscription? streamSubcription; + @override void initState() { super.initState(); + if (!PlatformInfos.isWeb && widget.event != null) { + handleDownloadFile(widget.event!); + } + } + + Future handleDownloadFile(Event event) async { + try { + streamSubcription = + downloadMediaFileInteractor.execute(event: event).listen((state) { + state.fold( + (failure) { + if (failure is DownloadMediaFileFailure) { + Logs().e('Error downloading file', failure.exception); + } + }, + (success) { + if (success is DownloadMediaFileSuccess) { + setState(() { + filePath = success.filePath; + }); + } + }, + ); + }); + } catch (e) { + Logs().e('Error downloading file', e); + } } @override void dispose() { + streamSubcription?.cancel(); transformationController.dispose(); showAppbarPreview.dispose(); super.dispose(); diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index a43373d498..58742ba159 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -4,8 +4,8 @@ import 'dart:typed_data'; import 'package:fluffychat/pages/image_viewer/image_viewer_style.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -33,7 +33,10 @@ class ImageViewerView extends StatelessWidget { filterQuality: FilterQuality.none, ); } else if (controller.widget.event != null) { - imageWidget = _ImageWidget(event: controller.widget.event!); + imageWidget = _ImageWidget( + event: controller.widget.event!, + controller: controller, + ); } else if (imageData != null) { imageWidget = Image.memory( imageData!, @@ -90,9 +93,11 @@ class ImageViewerView extends StatelessWidget { } class _ImageWidget extends StatelessWidget { + final ImageViewerController controller; + final Event event; - const _ImageWidget({required this.event}); + const _ImageWidget({required this.event, required this.controller}); @override Widget build(BuildContext context) { @@ -109,17 +114,18 @@ class _ImageWidget extends StatelessWidget { }, ); } else { - return FutureBuilder( - future: event.getFileInfo( - getThumbnail: false, - ), - builder: (context, snapshot) { - if (snapshot.data == null || snapshot.data!.fileName.isEmpty) { - return const CircularProgressIndicator(); - } - return Image.file(File(snapshot.data!.filePath)); - }, - ); + if (controller.filePath != null) { + return Image.file( + File(controller.filePath!), + fit: BoxFit.contain, + filterQuality: FilterQuality.none, + ); + } else { + return const CupertinoActivityIndicator( + animating: true, + color: Colors.white, + ); + } } } } From 86ddb835e8df3632a893c2a5c4c7f9bbe554c30c Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 16 Apr 2024 17:29:00 +0700 Subject: [PATCH 135/183] TW-1573: add test for downloadFileInterceptor (cherry picked from commit 7ca849d025a42d2d6194c9ce0046a2666320f619) --- .gitignore | 1 + .../dio_duplicate_download_exception.dart | 14 + .../dio_duplicate_request_exception.dart | 9 - .../download_file_interceptor.dart | 17 +- lib/data/network/media/media_api.dart | 8 +- .../download_file_extension.dart | 9 +- macos/Runner.xcodeproj/project.pbxproj | 6 +- .../download_file_interceptor_test.dart | 297 ++++++++++++++++++ 8 files changed, 339 insertions(+), 22 deletions(-) create mode 100644 lib/data/network/exception/dio_duplicate_download_exception.dart delete mode 100644 lib/data/network/exception/dio_duplicate_request_exception.dart create mode 100644 test/interceptor/download_file_interceptor_test.dart diff --git a/.gitignore b/.gitignore index 1f93da407f..ffacbe6231 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ ios/Podfile.lock /macos/out .vs olm +test/interceptor/*.mocks.dart diff --git a/lib/data/network/exception/dio_duplicate_download_exception.dart b/lib/data/network/exception/dio_duplicate_download_exception.dart new file mode 100644 index 0000000000..b9ad49d8d2 --- /dev/null +++ b/lib/data/network/exception/dio_duplicate_download_exception.dart @@ -0,0 +1,14 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; + +class DioDuplicateDownloadException extends DioException with EquatableMixin { + DioDuplicateDownloadException({required RequestOptions requestOptions}) + : super( + message: 'Download already in progress', + requestOptions: requestOptions, + type: DioExceptionType.unknown, + ); + + @override + List get props => [message, requestOptions, type]; +} diff --git a/lib/data/network/exception/dio_duplicate_request_exception.dart b/lib/data/network/exception/dio_duplicate_request_exception.dart deleted file mode 100644 index 91f224e51f..0000000000 --- a/lib/data/network/exception/dio_duplicate_request_exception.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:dio/dio.dart'; - -class DioDuplicateRequestException extends DioException { - DioDuplicateRequestException() - : super( - message: 'Download already in progress', - requestOptions: RequestOptions(), - ); -} diff --git a/lib/data/network/interceptor/download_file_interceptor.dart b/lib/data/network/interceptor/download_file_interceptor.dart index 46d1042cc5..026e29ea59 100644 --- a/lib/data/network/interceptor/download_file_interceptor.dart +++ b/lib/data/network/interceptor/download_file_interceptor.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:fluffychat/data/network/exception/dio_duplicate_request_exception.dart'; +import 'package:fluffychat/data/network/exception/dio_duplicate_download_exception.dart'; import 'package:matrix/matrix.dart'; class DownloadFileInterceptor extends InterceptorsWrapper { @@ -9,13 +9,18 @@ class DownloadFileInterceptor extends InterceptorsWrapper { static const downloadPath = '_matrix/media/v3/download/'; + Set get currentDownloads => _currentDownloads; + @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { Logs().d('DownloadFileInterceptor::onRequest: ${options.path}'); - if (options.path.contains(downloadPath) && - _currentDownloads.contains(options.path)) { + if (!options.path.contains(downloadPath)) { + super.onRequest(options, handler); + return; + } + if (isAlreadyDownloading(options.path)) { handler.reject( - DioDuplicateRequestException(), + DioDuplicateDownloadException(requestOptions: options), ); return; } @@ -23,6 +28,10 @@ class DownloadFileInterceptor extends InterceptorsWrapper { super.onRequest(options, handler); } + bool isAlreadyDownloading(String path) { + return path.contains(downloadPath) && _currentDownloads.contains(path); + } + @override void onError(DioException err, ErrorInterceptorHandler handler) { _currentDownloads diff --git a/lib/data/network/media/media_api.dart b/lib/data/network/media/media_api.dart index 0d3ba6987b..77acd34047 100644 --- a/lib/data/network/media/media_api.dart +++ b/lib/data/network/media/media_api.dart @@ -6,7 +6,7 @@ import 'package:fluffychat/data/model/media/download_file_response.dart'; import 'package:fluffychat/data/model/media/upload_file_json.dart'; import 'package:fluffychat/data/model/media/url_preview_response.dart'; import 'package:fluffychat/data/network/dio_client.dart'; -import 'package:fluffychat/data/network/exception/dio_duplicate_request_exception.dart'; +import 'package:fluffychat/data/network/exception/dio_duplicate_download_exception.dart'; import 'package:fluffychat/data/network/homeserver_endpoint.dart'; import 'package:fluffychat/data/network/media/cancel_exception.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; @@ -55,9 +55,11 @@ class MediaAPI { .onError((error, stackTrace) { if (error is DioException && error.type == DioExceptionType.cancel) { throw CancelRequestException(); - } else if (error is DioDuplicateRequestException) { + } else if (error is DioDuplicateDownloadException) { Logs().i('downloadFileInfo error: $error'); - throw DioDuplicateRequestException(); + throw DioDuplicateDownloadException( + requestOptions: error.requestOptions, + ); } else { Logs().i('downloadFileInfo error: $error'); throw Exception(error); diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 00a58b8791..abde208ef6 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -5,7 +5,7 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/data/network/exception/dio_duplicate_request_exception.dart'; +import 'package:fluffychat/data/network/exception/dio_duplicate_download_exception.dart'; import 'package:fluffychat/data/network/media/cancel_exception.dart'; import 'package:fluffychat/data/network/media/media_api.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; @@ -82,7 +82,10 @@ extension DownloadFileExtension on Event { }, cancelToken: cancelToken, ); - if (downloadResponse.statusCode == 200 && await File(savePath).exists()) { + if (downloadResponse.statusCode == 200 && + await File(savePath).exists() && + await File(savePath).length() == + getFileSize(getThumbnail: getThumbnail)) { final fileInfo = FileInfo( filename, savePath, @@ -102,7 +105,7 @@ extension DownloadFileExtension on Event { } catch (e) { if (e is CancelRequestException) { Logs().i("downloadOrRetrieveAttachment: user cancel the download"); - } else if (e is DioDuplicateRequestException) { + } else if (e is DioDuplicateDownloadException) { Logs().i("downloadOrRetrieveAttachment: duplicate request"); } else { Logs().e("downloadOrRetrieveAttachment: $e"); diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index fca4e314a0..24ba24f44e 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -535,7 +535,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 14.2.99; + MACOSX_DEPLOYMENT_TARGET = 12.2; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -628,7 +628,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 14.2.99; + MACOSX_DEPLOYMENT_TARGET = 12.2; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -675,7 +675,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 14.2.99; + MACOSX_DEPLOYMENT_TARGET = 12.2; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/test/interceptor/download_file_interceptor_test.dart b/test/interceptor/download_file_interceptor_test.dart new file mode 100644 index 0000000000..854aff092b --- /dev/null +++ b/test/interceptor/download_file_interceptor_test.dart @@ -0,0 +1,297 @@ +import 'package:dio/dio.dart'; +import 'package:fluffychat/data/network/exception/dio_duplicate_download_exception.dart'; +import 'package:fluffychat/data/network/interceptor/download_file_interceptor.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:matrix/matrix.dart'; + +import 'download_file_interceptor_test.mocks.dart'; + +@GenerateMocks( + [ + Dio, + Logs, + RequestInterceptorHandler, + ErrorInterceptorHandler, + ResponseInterceptorHandler, + ], +) +void main() { + group('DownloadFileInterceptor', () { + late DownloadFileInterceptor interceptor; + + setUp(() { + interceptor = DownloadFileInterceptor(); + }); + + const downloadPath = + 'https://matrix.linagora.com/_matrix/media/v3/download/'; + const uploadPath = 'https://matrix.linagora.com/_matrix/media/v3/upload/'; + + test("""WHEN there are no request match the download path, + THEN interceptor should not handle the request, the next interceptor will handle it""", + () async { + final options = RequestOptions( + path: '${uploadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + + verifyNever(mockHandler.reject(any)); + verify(mockHandler.next(options)); + + expect( + interceptor.currentDownloads.length, + 0, + ); + }); + + test("""WHEN there are one item in currentDownloads set, + THEN another request comes with different path, + THEN interceptor should put both requests in currentDownloads set""", + () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final options1 = RequestOptions( + path: '${downloadPath}linagora.com/ADeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + interceptor.onRequest(options1, mockHandler); + + verifyNever(mockHandler.reject(any)); + verify(mockHandler.next(options)); + verify(mockHandler.next(options1)); + expect( + interceptor.currentDownloads.length, + 2, + ); + }); + + test("""WHEN there are two item in currentDownloads set, + THEN another request comes with different path, + THEN interceptor should have 3 requests in currentDownloads set""", + () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final options1 = RequestOptions( + path: '${downloadPath}linagora.com/ADeVrZRwVXanSwyMMoxuKVmE', + ); + final option2 = RequestOptions( + path: '${downloadPath}linagora.com/MDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + interceptor.onRequest(options1, mockHandler); + interceptor.onRequest(option2, mockHandler); + + verifyNever(mockHandler.reject(any)); + verify(mockHandler.next(options)); + verify(mockHandler.next(options1)); + verify(mockHandler.next(option2)); + expect( + interceptor.currentDownloads.length, + 3, + ); + }); + + test("""WHEN there are two item in currentDownloads set, + THEN another request comes with upload path, + THEN interceptor should have 2 requests in currentDownloads set""", + () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final options1 = RequestOptions( + path: '${downloadPath}linagora.com/ADeVrZRwVXanSwyMMoxuKVmE', + ); + final option2 = RequestOptions( + path: '${uploadPath}linagora.com/MDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + interceptor.onRequest(options1, mockHandler); + interceptor.onRequest(option2, mockHandler); + + verifyNever(mockHandler.reject(any)); + verify(mockHandler.next(options)); + verify(mockHandler.next(options1)); + verify(mockHandler.next(option2)); + expect( + interceptor.currentDownloads.length, + 2, + ); + }); + + test("""WHEN there are two item in currentDownloads set, + THEN another request comes with different path, + THEN interceptor should have 3 requests in currentDownloads set""", + () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final options1 = RequestOptions( + path: '${downloadPath}linagora.com/ADeVrZRwVXanSwyMMoxuKVmE', + ); + final option2 = RequestOptions( + path: '${downloadPath}linagora.com/MDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + interceptor.onRequest(options1, mockHandler); + interceptor.onRequest(option2, mockHandler); + + verifyNever(mockHandler.reject(any)); + verify(mockHandler.next(options)); + verify(mockHandler.next(options1)); + verify(mockHandler.next(option2)); + expect( + interceptor.currentDownloads.length, + 3, + ); + }); + + test("""WHEN there are two item in currentDownloads set, + THEN another request comes with same path in currentDownloads set, + THEN interceptor should have 2 requests in currentDownloads set""", + () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final options1 = RequestOptions( + path: '${downloadPath}linagora.com/ADeVrZRwVXanSwyMMoxuKVmE', + ); + final option2 = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + interceptor.onRequest(options1, mockHandler); + + verify(mockHandler.next(options)); + verify(mockHandler.next(options1)); + try { + interceptor.onRequest(option2, mockHandler); + } on DioDuplicateDownloadException { + // Expected exception thrown + } catch (e) { + expect(e, isA()); + } + expect( + interceptor.currentDownloads.length, + 2, + ); + }); + + test("""WHEN there are another request with same duplicate request path, + THEN rejects duplicate download requests with exception""", + () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + + interceptor.onRequest(options, mockHandler); + + verify( + mockHandler + .reject(DioDuplicateDownloadException(requestOptions: options)), + ); + expect( + interceptor.currentDownloads.length, + 1, + ); + }); + + test("""WHEN add a duplicate request path: + THEN throws DioDuplicateRequestException for duplicate downloads""", + () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + + try { + interceptor.onRequest(options, mockHandler); + } on DioDuplicateDownloadException { + // Expected exception thrown + } catch (e) { + expect(e, isA()); + } + expect( + interceptor.currentDownloads.length, + 1, + ); + }); + + test("""WHEN allows new multiple download requests + THEN allows all new multiple download requests""", () async { + final options = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final options1 = RequestOptions( + path: '${downloadPath}linagora.com/aDeVrRRwVXanSwyMMoxuKVmE', + ); + final mockHandler = MockRequestInterceptorHandler(); + + interceptor.onRequest(options, mockHandler); + interceptor.onRequest(options1, mockHandler); + + verifyNever(mockHandler.reject(any)); + expect(interceptor.currentDownloads.contains(options.path), true); + expect(interceptor.currentDownloads.contains(options1.path), true); + expect(interceptor.currentDownloads.length, 2); + }); + + test("""WHEN there is an error in downloading + THEN removes element in currentDownloads on error""", () async { + final requestOptions = RequestOptions( + path: '${downloadPath}linagora.com/dDeVrZRwVXanSwyMMoxuKVmE', + ); + final mockErr = DioException(requestOptions: requestOptions); + final mockHandler = MockErrorInterceptorHandler(); + + interceptor.onRequest(requestOptions, MockRequestInterceptorHandler()); + + interceptor.onError(mockErr, mockHandler); + + expect(interceptor.currentDownloads.contains(requestOptions.path), false); + expect( + interceptor.currentDownloads.length, + 0, + ); + }); + + test("""WHEN there are successfully download requests + THEN removes download in currentDownloads on successful response""", + () async { + final requestOptions = RequestOptions(path: '${downloadPath}success.mp3'); + final mockResponse = Response(requestOptions: requestOptions); + final mockHandler = MockResponseInterceptorHandler(); + + // Simulate download in progress + interceptor.onRequest(requestOptions, MockRequestInterceptorHandler()); + + interceptor.onResponse(mockResponse, mockHandler); + + expect(interceptor.currentDownloads.contains(requestOptions.path), false); + expect( + interceptor.currentDownloads.length, + 0, + ); + }); + }); +} From f4970bd133ec55515336c7c7f5bf041842a207e0 Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 17 Apr 2024 12:16:32 +0700 Subject: [PATCH 136/183] TW-1573: fix snackbar under the video player (cherry picked from commit e3a8630ca7392c94f1c173e3e8f2b1cfdab77e64) --- .../image_viewer/media_viewer_app_bar_view.dart | 13 +++++++++---- lib/widgets/video_player.dart | 12 +++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/pages/image_viewer/media_viewer_app_bar_view.dart b/lib/pages/image_viewer/media_viewer_app_bar_view.dart index 64c68be47b..530efd018d 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_view.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_view.dart @@ -93,10 +93,15 @@ class MediaViewerAppbarView extends StatelessWidget { ContextMenuItemImageViewer( icon: Icons.file_download_outlined, title: L10n.of(context)!.saveToGallery, - onTap: () => controller.saveFileAction( - context, - controller.widget.event, - ), + onTap: () { + controller.toggleShowMoreActions( + controller.menuController, + ); + controller.saveFileAction( + context, + controller.widget.event, + ); + }, ), ContextMenuItemImageViewer( title: L10n.of(context)!.showInChat, diff --git a/lib/widgets/video_player.dart b/lib/widgets/video_player.dart index 2233a7cfbd..23623fe562 100644 --- a/lib/widgets/video_player.dart +++ b/lib/widgets/video_player.dart @@ -35,11 +35,13 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return Video( - fill: Colors.black, - pauseUponEnteringBackgroundMode: true, - resumeUponEnteringForegroundMode: true, - controller: videoController, + return Scaffold( + body: Video( + fill: Colors.black, + pauseUponEnteringBackgroundMode: true, + resumeUponEnteringForegroundMode: true, + controller: videoController, + ), ); } } From 0bfb3eb38b7ecc1d4fb2b3001cce9c3984e96626 Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 17 Apr 2024 14:53:35 +0700 Subject: [PATCH 137/183] TW-1573: change name saveToGallery to saveFiles in web (cherry picked from commit 6602af11b85e699a88cb421c9d24637e20ddb693) --- lib/pages/image_viewer/media_viewer_app_bar_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/image_viewer/media_viewer_app_bar_view.dart b/lib/pages/image_viewer/media_viewer_app_bar_view.dart index 530efd018d..b8a724bc37 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar_view.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar_view.dart @@ -92,7 +92,9 @@ class MediaViewerAppbarView extends StatelessWidget { menuChildren: [ ContextMenuItemImageViewer( icon: Icons.file_download_outlined, - title: L10n.of(context)!.saveToGallery, + title: PlatformInfos.isWeb + ? L10n.of(context)!.saveFile + : L10n.of(context)!.saveToGallery, onTap: () { controller.toggleShowMoreActions( controller.menuController, From 937edd923e4c445e734ec67cb15e91274a079b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=B9i=20Trung=20Hi=E1=BA=BFu?= Date: Mon, 22 Apr 2024 05:14:54 +0700 Subject: [PATCH 138/183] TW-1461: Update app language setting subtitles (#1703) (cherry picked from commit 47c153fa5f74a0b3136cdebd17a28bfd510a9bd8) --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_fr.arb | 2 +- assets/l10n/intl_ru.arb | 2 +- assets/l10n/intl_vi.arb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index da50ec484a..fe76a0e7a1 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2843,7 +2843,7 @@ "@settingsNotificationAndSoundsSubtitle": {}, "settingsChatFoldersSubtitle": "Create and manage folders for different groups of chats and quickly switch between them.", "@settingsChatFoldersSubtitle": {}, - "settingsAppLanguageSubtitle": "English (phone’s language).", + "settingsAppLanguageSubtitle": "Choose in which language you want to use Twake Chat.", "@settingsAppLanguageSubtitle": {}, "settingsDevicesSubtitle": "Control your sign in and sign out on any device.", "@settingsDevicesSubtitle": {}, diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index 017130b09f..ab65dd7510 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -2789,7 +2789,7 @@ "@matrixId": {}, "roomCreationFailed": "La création d'un salon a échoué", "@roomCreationFailed": {}, - "settingsAppLanguageSubtitle": "Choisissez dans quelle langue vous souhaitez utiliser Twake.", + "settingsAppLanguageSubtitle": "Choisissez dans quelle langue vous souhaitez utiliser Twake Chat.", "@settingsAppLanguageSubtitle": {}, "settingsNotificationAndSoundsSubtitle": "Personnalisez la façon dont vous recevez les notifications de Twake Chat, telles que l'aperçu des messages, les sons, l'heure,...", "@settingsNotificationAndSoundsSubtitle": {}, diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index bf8a9f2ace..68e98d9b70 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -2802,7 +2802,7 @@ "@matrixId": {}, "roomCreationFailed": "Не удалось создать комнату", "@roomCreationFailed": {}, - "settingsAppLanguageSubtitle": "Английский (язык телефона).", + "settingsAppLanguageSubtitle": "Выберите, на каком языке вы хотите использовать Twake Chat.", "@settingsAppLanguageSubtitle": {}, "settingsNotificationAndSoundsSubtitle": "Настройте уведомления от Twake – предварительный просмотр сообщений, звук, время и т. д.", "@settingsNotificationAndSoundsSubtitle": {}, diff --git a/assets/l10n/intl_vi.arb b/assets/l10n/intl_vi.arb index 678c8d54b2..5df95bc95e 100644 --- a/assets/l10n/intl_vi.arb +++ b/assets/l10n/intl_vi.arb @@ -2801,7 +2801,7 @@ "@matrixId": {}, "roomCreationFailed": "Tạo hội thoại lỗi", "@roomCreationFailed": {}, - "settingsAppLanguageSubtitle": "English", + "settingsAppLanguageSubtitle": "Chọn ngôn ngữ bạn muốn sử dụng Twake Chat.", "@settingsAppLanguageSubtitle": {}, "settingsNotificationAndSoundsSubtitle": "Tùy chỉnh cách bạn nhận thông báo từ Twake như xem trước tin nhắn, âm thanh, thời gian,...", "@settingsNotificationAndSoundsSubtitle": {}, From 075575abcb74cac0b9e5ac142afd4572b3c30d22 Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 1 Apr 2024 23:13:32 +0700 Subject: [PATCH 139/183] TW-1666: Fix filename not resize (cherry picked from commit b8b70406e85dc2682985ba2027fffa3ad6a57387) --- lib/widgets/file_widget/file_tile_widget.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/widgets/file_widget/file_tile_widget.dart b/lib/widgets/file_widget/file_tile_widget.dart index e086beffc7..32ad5ede78 100644 --- a/lib/widgets/file_widget/file_tile_widget.dart +++ b/lib/widgets/file_widget/file_tile_widget.dart @@ -122,15 +122,15 @@ class FileNameText extends StatelessWidget { @override Widget build(BuildContext context) { - return RichText( - maxLines: 2, - text: TextSpan( + return Text.rich( + TextSpan( children: filename.buildHighlightTextSpans( highlightText ?? '', style: style.textStyle(context), highlightStyle: style.highlightTextStyle(context), ), ), + maxLines: 2, overflow: TextOverflow.ellipsis, ); } From ba2742f899ac405e1d3402830def301696c562bb Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 2 Apr 2024 22:55:52 +0700 Subject: [PATCH 140/183] TW-1666: Make `MessageTime` changes size follow system font size (cherry picked from commit 94f11e4212521e14b8e222a249f7323b0591ac14) --- lib/pages/chat/events/message_time.dart | 110 +++++++++++++----------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/lib/pages/chat/events/message_time.dart b/lib/pages/chat/events/message_time.dart index 736dc1f9ba..e6347fe97a 100644 --- a/lib/pages/chat/events/message_time.dart +++ b/lib/pages/chat/events/message_time.dart @@ -29,61 +29,69 @@ class MessageTime extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: timelineOverlayMessage - ? const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ) - : null, - decoration: timelineOverlayMessage - ? BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: LinagoraStateLayer(Colors.black).opacityLayer3, - ) - : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, + return Text.rich( + TextSpan( children: [ - if (event.isPinned) ...[ - TwakeIconButton( - tooltip: L10n.of(context)!.pin, - icon: Icons.push_pin_outlined, - size: MessageStyle.pushpinIconSize, - paddingAll: MessageStyle.paddingAllPushpin, - margin: EdgeInsets.zero, - iconColor: timelineOverlayMessage - ? Colors.white - : LinagoraRefColors.material().neutral[50], - ), - const SizedBox(width: 4.0), - ], - Text( - DateFormat("HH:mm").format(event.originServerTs), - style: Theme.of(context).textTheme.bodySmall?.merge( - TextStyle( - color: timelineOverlayMessage - ? Colors.white - : LinagoraRefColors.material().tertiary[30], - letterSpacing: 0.4, + WidgetSpan( + child: Container( + padding: timelineOverlayMessage + ? const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ) + : null, + decoration: timelineOverlayMessage + ? BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: LinagoraStateLayer(Colors.black).opacityLayer3, + ) + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (event.isPinned) ...[ + TwakeIconButton( + tooltip: L10n.of(context)!.pin, + icon: Icons.push_pin_outlined, + size: MessageStyle.pushpinIconSize, + paddingAll: MessageStyle.paddingAllPushpin, + margin: EdgeInsets.zero, + iconColor: timelineOverlayMessage + ? Colors.white + : LinagoraRefColors.material().neutral[50], + ), + const SizedBox(width: 4.0), + ], + Text( + DateFormat("HH:mm").format(event.originServerTs), + style: Theme.of(context).textTheme.bodySmall?.merge( + TextStyle( + color: timelineOverlayMessage + ? Colors.white + : LinagoraRefColors.material().tertiary[30], + letterSpacing: 0.4, + ), + ), ), - ), - ), - if (ownMessage) ...[ - SizedBox(width: MessageTimeStyle.paddingTimeAndIcon), - SeenByRow( - timelineOverlayMessage: timelineOverlayMessage, - participants: timeline.room.getParticipants(), - getSeenByUsers: room.getSeenByUsers( - timeline, - eventId: event.eventId, + if (ownMessage) ...[ + SizedBox(width: MessageTimeStyle.paddingTimeAndIcon), + SeenByRow( + timelineOverlayMessage: timelineOverlayMessage, + participants: timeline.room.getParticipants(), + getSeenByUsers: room.getSeenByUsers( + timeline, + eventId: event.eventId, + ), + eventStatus: event.status, + event: event, + ), + ], + ], ), - eventStatus: event.status, - event: event, ), - ], + ), ], ), ); From 3a98513abf9fe5eecb3e60b43568480e6e742b72 Mon Sep 17 00:00:00 2001 From: hieubt Date: Sun, 7 Apr 2024 19:01:36 +0700 Subject: [PATCH 141/183] TW-1666: Fix timeline scale too much (cherry picked from commit 42cfc27fb53b35e1c0d401165f7add6f31e3d47a) --- ...essage_content_with_timestamp_builder.dart | 19 +-- lib/pages/chat/events/message_content.dart | 12 +- lib/pages/chat/events/message_time.dart | 111 ++++++++---------- 3 files changed, 74 insertions(+), 68 deletions(-) diff --git a/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart b/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart index 7ef476984f..7a6584fe2e 100644 --- a/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart +++ b/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart @@ -150,13 +150,18 @@ class MessageContentWithTimestampBuilder extends StatelessWidget { child: Padding( padding: MessageStyle.paddingMessageTime, - child: MessageTime( - timelineOverlayMessage: - event.timelineOverlayMessage, - room: event.room, - event: event, - ownMessage: event.isOwnMessage, - timeline: timeline, + child: Text.rich( + WidgetSpan( + child: MessageTime( + timelineOverlayMessage: event + .timelineOverlayMessage, + room: event.room, + event: event, + ownMessage: + event.isOwnMessage, + timeline: timeline, + ), + ), ), ), ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index a3d2f169c1..5e846e763b 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -111,7 +111,11 @@ class MessageContent extends StatelessWidget ], Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, - child: endOfBubbleWidget, + child: Text.rich( + WidgetSpan( + child: endOfBubbleWidget, + ), + ), ), ], ); @@ -132,7 +136,11 @@ class MessageContent extends StatelessWidget ], Padding( padding: MessageContentStyle.endOfBubbleWidgetPadding, - child: endOfBubbleWidget, + child: Text.rich( + WidgetSpan( + child: endOfBubbleWidget, + ), + ), ), ], ); diff --git a/lib/pages/chat/events/message_time.dart b/lib/pages/chat/events/message_time.dart index e6347fe97a..0a22c54d34 100644 --- a/lib/pages/chat/events/message_time.dart +++ b/lib/pages/chat/events/message_time.dart @@ -29,69 +29,62 @@ class MessageTime extends StatelessWidget { @override Widget build(BuildContext context) { - return Text.rich( - TextSpan( + return Container( + padding: timelineOverlayMessage + ? const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ) + : null, + decoration: timelineOverlayMessage + ? BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: LinagoraStateLayer(Colors.black).opacityLayer3, + ) + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - WidgetSpan( - child: Container( - padding: timelineOverlayMessage - ? const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, - ) - : null, - decoration: timelineOverlayMessage - ? BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: LinagoraStateLayer(Colors.black).opacityLayer3, - ) - : null, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (event.isPinned) ...[ - TwakeIconButton( - tooltip: L10n.of(context)!.pin, - icon: Icons.push_pin_outlined, - size: MessageStyle.pushpinIconSize, - paddingAll: MessageStyle.paddingAllPushpin, - margin: EdgeInsets.zero, - iconColor: timelineOverlayMessage - ? Colors.white - : LinagoraRefColors.material().neutral[50], - ), - const SizedBox(width: 4.0), - ], - Text( - DateFormat("HH:mm").format(event.originServerTs), - style: Theme.of(context).textTheme.bodySmall?.merge( - TextStyle( - color: timelineOverlayMessage - ? Colors.white - : LinagoraRefColors.material().tertiary[30], - letterSpacing: 0.4, - ), - ), + if (event.isPinned) ...[ + TwakeIconButton( + tooltip: L10n.of(context)!.pin, + icon: Icons.push_pin_outlined, + size: MessageStyle.pushpinIconSize, + paddingAll: MessageStyle.paddingAllPushpin, + margin: EdgeInsets.zero, + iconColor: timelineOverlayMessage + ? Colors.white + : LinagoraRefColors.material().neutral[50], + ), + const SizedBox(width: 4.0), + ], + Text( + DateFormat("HH:mm").format(event.originServerTs), + textScaler: const TextScaler.linear(1.0), + style: Theme.of(context).textTheme.bodySmall?.merge( + TextStyle( + color: timelineOverlayMessage + ? Colors.white + : LinagoraRefColors.material().tertiary[30], + letterSpacing: 0.4, ), - if (ownMessage) ...[ - SizedBox(width: MessageTimeStyle.paddingTimeAndIcon), - SeenByRow( - timelineOverlayMessage: timelineOverlayMessage, - participants: timeline.room.getParticipants(), - getSeenByUsers: room.getSeenByUsers( - timeline, - eventId: event.eventId, - ), - eventStatus: event.status, - event: event, - ), - ], - ], + ), + ), + if (ownMessage) ...[ + SizedBox(width: MessageTimeStyle.paddingTimeAndIcon), + SeenByRow( + timelineOverlayMessage: timelineOverlayMessage, + participants: timeline.room.getParticipants(), + getSeenByUsers: room.getSeenByUsers( + timeline, + eventId: event.eventId, ), + eventStatus: event.status, + event: event, ), - ), + ], ], ), ); From 33b71b5f02c1e41a4f2348e191b008f9408ee452 Mon Sep 17 00:00:00 2001 From: hieubt Date: Sun, 7 Apr 2024 19:02:24 +0700 Subject: [PATCH 142/183] TW-1666: Update calculating message bubble width (cherry picked from commit 9c2ac992648f74802eac4fe422ee788cd967ea9d) --- .../message/message_content_builder.dart | 148 ++++++++++++++++-- 1 file changed, 132 insertions(+), 16 deletions(-) diff --git a/lib/pages/chat/events/message/message_content_builder.dart b/lib/pages/chat/events/message/message_content_builder.dart index db245c9d5e..e2068d24b4 100644 --- a/lib/pages/chat/events/message/message_content_builder.dart +++ b/lib/pages/chat/events/message/message_content_builder.dart @@ -1,12 +1,15 @@ import 'package:fluffychat/pages/chat/events/message/display_name_widget.dart'; import 'package:fluffychat/pages/chat/events/message/message_style.dart'; import 'package:fluffychat/pages/chat/events/message_time.dart'; +import 'package:fluffychat/pages/chat/events/message_time_style.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/pages/chat/events/message/reply_content_widget.dart'; import 'package:fluffychat/pages/chat/events/message_content.dart'; -import 'package:matrix/matrix.dart'; +import 'package:intl/intl.dart' hide TextDirection; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:matrix/matrix.dart' hide Visibility; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -101,12 +104,17 @@ class MessageContentBuilder extends StatelessWidget { right: 8, bottom: 4.0, child: SelectionContainer.disabled( - child: MessageTime( - timelineOverlayMessage: event.timelineOverlayMessage, - event: event, - ownMessage: event.isOwnMessage, - timeline: timeline, - room: event.room, + child: Text.rich( + WidgetSpan( + child: MessageTime( + timelineOverlayMessage: + event.timelineOverlayMessage, + event: event, + ownMessage: event.isOwnMessage, + timeline: timeline, + room: event.room, + ), + ), ), ), ), @@ -158,28 +166,37 @@ class MessageContentBuilder extends StatelessWidget { MessageTypes.Video, }.contains(event.messageType); - if (ownMessage || hideDisplayName || isNotSupportCalcSize) { + if (isNotSupportCalcSize || event.text.isEmpty) { return null; } - final sizeWidthDisplayName = _getSizeDisplayName( + final sizeWidthDisplayName = _paintDisplayName( context, maxWidth, ).width; - final sizeWidthMessageText = _getSizeMessageText( + final totalMessageWidth = _getWidthMessageAndTime( context, maxWidth, - ).width; + ); + + if (ownMessage || hideDisplayName) { + return totalMessageWidth; + } - return max(sizeWidthDisplayName, sizeWidthMessageText); + const rightSpaceDisplayName = 16.0; + + final totalDisplayNameWidth = sizeWidthDisplayName + rightSpaceDisplayName; + + return max(totalDisplayNameWidth, totalMessageWidth); } - TextPainter _getSizeDisplayName( + TextPainter _paintDisplayName( BuildContext context, double maxWidth, ) { return TextPainter( + textScaler: MediaQuery.of(context).textScaler, text: TextSpan( text: event.senderFromMemoryOrFallback .calcDisplayname() @@ -197,23 +214,122 @@ class MessageContentBuilder extends StatelessWidget { )..layout(minWidth: 0, maxWidth: maxWidth); } - TextPainter _getSizeMessageText( + TextPainter _paintMessageText( BuildContext context, double maxWidth, ) { return TextPainter( + textScaler: MediaQuery.of(context).textScaler, text: TextSpan( text: event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)!), hideReply: true, + plaintextBody: true, ), - style: Theme.of(context).textTheme.labelMedium?.copyWith( + style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of( context, - ).colorScheme.primary, + ).colorScheme.onSurface, + ), + ), + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: maxWidth); + } + + double _getWidthMessageTime( + BuildContext context, + double maxWidth, + ) { + final painTimeText = TextPainter( + textScaler: MediaQuery.of(context).textScaler, + text: TextSpan( + text: DateFormat("HH:mm").format(event.originServerTs), + style: Theme.of(context).textTheme.bodySmall?.merge( + TextStyle( + color: event.timelineOverlayMessage + ? Colors.white + : LinagoraRefColors.material().tertiary[30], + letterSpacing: 0.4, + ), ), ), textDirection: TextDirection.ltr, )..layout(minWidth: 0, maxWidth: maxWidth); + + const pushpinIconSize = MessageStyle.pushpinIconSize; + const paddingAllPushpin = MessageStyle.paddingAllPushpin; + const paddingToTimeSpacing = 4.0; + final seenByRowIconSize = MessageTimeStyle.seenByRowIconSize; + final paddingTimeAndIcon = MessageTimeStyle.paddingTimeAndIcon; + + double totalWidth = painTimeText.width; + + if (event.isPinned) { + totalWidth += paddingTimeAndIcon + + pushpinIconSize + + paddingAllPushpin + + paddingToTimeSpacing; + } + + if (event.isOwnMessage) { + totalWidth += paddingTimeAndIcon + seenByRowIconSize; + } + + final scaledWidth = MediaQuery.textScalerOf(context).scale(totalWidth); + + return scaledWidth; + } + + double _getWidthMessageAndTime( + BuildContext context, + double maxWidth, + ) { + const spaceMessageAndTime = 4.0; + const paddingTimeStamp = 12.0; + const paddingMessage = 16.0; + + final paintedMessageText = _paintMessageText( + context, + maxWidth, + ); + + final sizeMessageTime = _getWidthMessageTime( + context, + maxWidth, + ); + + final messageTimeAndPaddingWidth = + sizeMessageTime + spaceMessageAndTime + paddingTimeStamp; + + final messageTextWidth = paintedMessageText.width; + + final lastLineWidth = paintedMessageText + .getBoxesForSelection( + TextSelection( + baseOffset: 0, + extentOffset: event + .calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + plaintextBody: true, + ) + .length, + ), + ) + .last + .right; + + final lastLineToRightBoundarySpace = messageTextWidth - lastLineWidth; + + double totalMessageWidth; + + if (lastLineToRightBoundarySpace > messageTimeAndPaddingWidth) { + totalMessageWidth = messageTextWidth + paddingMessage; + } else { + totalMessageWidth = + lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; + } + + return totalMessageWidth; } } From c2ae57555e78b732a6d61fb1f03f086dbf54204c Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 9 Apr 2024 12:19:53 +0700 Subject: [PATCH 143/183] TW-1666: Remove fake timeline in message (cherry picked from commit 61a2d73ca92b0552e9b101db1454cc5eb8cb9976) --- lib/pages/chat/events/formatted_text_widget.dart | 9 --------- lib/pages/chat/events/message_content.dart | 1 - lib/pages/chat/events/message_content_style.dart | 1 - lib/widgets/clean_rich_text.dart | 10 ++++++---- .../twake_preview_link/twake_link_preview.dart | 8 -------- 5 files changed, 6 insertions(+), 23 deletions(-) diff --git a/lib/pages/chat/events/formatted_text_widget.dart b/lib/pages/chat/events/formatted_text_widget.dart index 8adba6f5af..3e1b1fc84d 100644 --- a/lib/pages/chat/events/formatted_text_widget.dart +++ b/lib/pages/chat/events/formatted_text_widget.dart @@ -5,14 +5,12 @@ import 'package:matrix/matrix.dart' hide Visibility; class FormattedTextWidget extends StatelessWidget { final Event event; final double fontSize; - final Widget endOfBubbleWidget; final TextStyle? linkStyle; const FormattedTextWidget({ super.key, required this.event, required this.fontSize, - required this.endOfBubbleWidget, this.linkStyle, }); @@ -32,13 +30,6 @@ class FormattedTextWidget extends StatelessWidget { linkStyle: linkStyle, room: event.room, emoteSize: bigEmotes ? fontSize * 3 : fontSize * 1.5, - bottomWidgetSpan: Visibility( - visible: false, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: endOfBubbleWidget, - ), ); } } diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 5e846e763b..806b29189c 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -160,7 +160,6 @@ class MessageContent extends StatelessWidget linkStyle: MessageContentStyle.linkStyleMessageContent(context), fontSize: fontSize, - endOfBubbleWidget: endOfBubbleWidget, ), ); } diff --git a/lib/pages/chat/events/message_content_style.dart b/lib/pages/chat/events/message_content_style.dart index 6bf34d64d3..0d1549208f 100644 --- a/lib/pages/chat/events/message_content_style.dart +++ b/lib/pages/chat/events/message_content_style.dart @@ -73,7 +73,6 @@ class MessageContentStyle { static const EdgeInsets emojiPadding = EdgeInsets.only( left: 8.0, - right: 8.0, ); static TextStyle? linkStyleMessageContent(BuildContext context) => diff --git a/lib/widgets/clean_rich_text.dart b/lib/widgets/clean_rich_text.dart index c6e8e89072..23c5a76da0 100644 --- a/lib/widgets/clean_rich_text.dart +++ b/lib/widgets/clean_rich_text.dart @@ -3,7 +3,7 @@ import 'package:matrix_link_text/link_text.dart'; class TwakeCleanRichText extends StatelessWidget { final String text; - final Widget childWidget; + final Widget? childWidget; final TextStyle? textStyle; final TextStyle? linkStyle; final TextAlign? textAlign; @@ -14,7 +14,7 @@ class TwakeCleanRichText extends StatelessWidget { const TwakeCleanRichText({ Key? key, required this.text, - required this.childWidget, + this.childWidget, this.textStyle, this.linkStyle, this.textAlign = TextAlign.start, @@ -36,8 +36,10 @@ class TwakeCleanRichText extends StatelessWidget { themeData: Theme.of(context), textSpanBuilder: textSpanBuilder, ), - const WidgetSpan(child: SizedBox(width: 4)), - WidgetSpan(child: childWidget), + if (childWidget != null) ...[ + const WidgetSpan(child: SizedBox(width: 4)), + WidgetSpan(child: childWidget!), + ], ], ), textAlign: textAlign, diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart index 790dc9f095..62318032af 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart @@ -69,17 +69,9 @@ class TwakeLinkPreviewController extends State event: widget.event, linkStyle: widget.linkStyle, fontSize: widget.fontSize, - endOfBubbleWidget: widget.endOfBubbleWidget, ) : TwakeCleanRichText( text: widget.localizedBody, - childWidget: Visibility( - visible: false, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: widget.endOfBubbleWidget, - ), textStyle: widget.richTextStyle, linkStyle: widget.linkStyle, textAlign: TextAlign.start, From bd3b98a65cf09242cf9e5ab2c7e41c94d6830786 Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 9 Apr 2024 12:20:39 +0700 Subject: [PATCH 144/183] TW-1666: Fix unordered list wrong font size (cherry picked from commit 98de4b5212fe857c3e17ac5a85eb71e40f32332d) --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index bf8ca963a2..7a337039ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -965,8 +965,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "9a1027b074c530f6deec4a5593a7a62f8fd70f45" + ref: time-overlap + resolved-ref: "74da044c1e1b62b0c2b0eee6a3320ca1de1d5439" url: "https://github.com/linagora/flutter_matrix_html.git" source: git version: "1.2.0" diff --git a/pubspec.yaml b/pubspec.yaml index 32970c7ffe..9cf91479fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: flutter_matrix_html: git: url: https://github.com/linagora/flutter_matrix_html.git - ref: master + ref: time-overlap contacts_service: git: From 226acab5b93bfce0d109ce88076e5d25606808d2 Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 9 Apr 2024 12:21:06 +0700 Subject: [PATCH 145/183] TW-1666: Update calculating message bubble (cherry picked from commit 583acc32e9227bedb6ee5d1b0c310044c7b62907) --- .../message/message_content_builder.dart | 119 ++++++++++++++---- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/lib/pages/chat/events/message/message_content_builder.dart b/lib/pages/chat/events/message/message_content_builder.dart index e2068d24b4..f4084dfd86 100644 --- a/lib/pages/chat/events/message/message_content_builder.dart +++ b/lib/pages/chat/events/message/message_content_builder.dart @@ -43,19 +43,22 @@ class MessageContentBuilder extends StatelessWidget { MessageTypes.File, MessageTypes.Audio, }.contains(event.messageType); + final sizeMessageBubble = _getSizeMessageBubbleWidth( + context, + maxWidth: availableBubbleContraints.maxWidth, + ownMessage: event.isOwnMessage, + hideDisplayName: event.hideDisplayName( + nextEvent, + ), + ); + final stepWidth = sizeMessageBubble?.keys.first; + final isNeedAddNewLine = sizeMessageBubble?.values.first ?? false; return Padding( padding: EdgeInsets.only( bottom: noPadding || event.timelineOverlayMessage ? 0 : 8, ), child: IntrinsicWidth( - stepWidth: _getSizeMessageBubbleWidth( - context, - maxWidth: availableBubbleContraints.maxWidth, - ownMessage: event.isOwnMessage, - hideDisplayName: event.hideDisplayName( - nextEvent, - ), - ), + stepWidth: stepWidth, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -120,6 +123,26 @@ class MessageContentBuilder extends StatelessWidget { ), ], ), + if (isNeedAddNewLine) + Visibility( + visible: false, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: SelectionContainer.disabled( + child: Text.rich( + WidgetSpan( + child: MessageTime( + timelineOverlayMessage: event.timelineOverlayMessage, + event: event, + ownMessage: event.isOwnMessage, + timeline: timeline, + room: event.room, + ), + ), + ), + ), + ), if (event.hasAggregatedEvents( timeline, RelationshipTypes.edit, @@ -154,7 +177,7 @@ class MessageContentBuilder extends StatelessWidget { ); } - double? _getSizeMessageBubbleWidth( + Map? _getSizeMessageBubbleWidth( BuildContext context, { required double maxWidth, bool ownMessage = false, @@ -181,14 +204,24 @@ class MessageContentBuilder extends StatelessWidget { ); if (ownMessage || hideDisplayName) { - return totalMessageWidth; + final Map result = { + totalMessageWidth.keys.first: totalMessageWidth.values.first, + }; + return result; } const rightSpaceDisplayName = 16.0; final totalDisplayNameWidth = sizeWidthDisplayName + rightSpaceDisplayName; - return max(totalDisplayNameWidth, totalMessageWidth); + final totalWidth = + max(totalDisplayNameWidth, totalMessageWidth.keys.first); + + final Map result = { + totalWidth: totalMessageWidth.values.first, + }; + + return result; } TextPainter _paintDisplayName( @@ -275,17 +308,14 @@ class MessageContentBuilder extends StatelessWidget { totalWidth += paddingTimeAndIcon + seenByRowIconSize; } - final scaledWidth = MediaQuery.textScalerOf(context).scale(totalWidth); - - return scaledWidth; + return totalWidth; } - double _getWidthMessageAndTime( + Map _getWidthMessageAndTime( BuildContext context, double maxWidth, ) { const spaceMessageAndTime = 4.0; - const paddingTimeStamp = 12.0; const paddingMessage = 16.0; final paintedMessageText = _paintMessageText( @@ -298,11 +328,14 @@ class MessageContentBuilder extends StatelessWidget { maxWidth, ); - final messageTimeAndPaddingWidth = - sizeMessageTime + spaceMessageAndTime + paddingTimeStamp; + final messageTimeAndPaddingWidth = sizeMessageTime + spaceMessageAndTime; final messageTextWidth = paintedMessageText.width; + double totalMessageWidth = messageTextWidth + paddingMessage; + + bool isNeedAddNewLine = false; + final lastLineWidth = paintedMessageText .getBoxesForSelection( TextSelection( @@ -319,17 +352,49 @@ class MessageContentBuilder extends StatelessWidget { .last .right; - final lastLineToRightBoundarySpace = messageTextWidth - lastLineWidth; - - double totalMessageWidth; - - if (lastLineToRightBoundarySpace > messageTimeAndPaddingWidth) { - totalMessageWidth = messageTextWidth + paddingMessage; + if (lastLineWidth >= maxWidth) { + isNeedAddNewLine = true; + totalMessageWidth = maxWidth; } else { - totalMessageWidth = - lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; + if (lastLineWidth < messageTextWidth) { + final lastLineToRightBoundarySpace = messageTextWidth - lastLineWidth; + if (lastLineToRightBoundarySpace >= messageTimeAndPaddingWidth) { + final messageAndTimeWidth = messageTextWidth + paddingMessage; + if (messageAndTimeWidth >= maxWidth) { + if (lastLineWidth + messageTimeAndPaddingWidth >= maxWidth) { + isNeedAddNewLine = true; + totalMessageWidth = maxWidth; + } else { + totalMessageWidth = lastLineWidth + messageTimeAndPaddingWidth; + } + } else { + totalMessageWidth = messageAndTimeWidth; + } + } else { + final lastLineWidthTimeWidth = + lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; + if (lastLineWidthTimeWidth >= maxWidth) { + isNeedAddNewLine = true; + totalMessageWidth = maxWidth; + } else { + totalMessageWidth = lastLineWidthTimeWidth; + } + } + } else { + final lastLineWithTimeWidth = + lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; + if (lastLineWithTimeWidth >= maxWidth) { + isNeedAddNewLine = true; + totalMessageWidth = maxWidth; + } else { + totalMessageWidth = lastLineWithTimeWidth; + } + } } - return totalMessageWidth; + final Map result = { + totalMessageWidth: isNeedAddNewLine, + }; + return result; } } From 81367ebb0e10c889016f969a428de23ec9636685 Mon Sep 17 00:00:00 2001 From: hieubt Date: Thu, 11 Apr 2024 12:04:20 +0700 Subject: [PATCH 146/183] TW-1666: Handle case tag name contains code (cherry picked from commit 33f3fa45b74851176d9eea6f1d3db0f108ff8171) --- .../message/message_content_builder.dart | 148 ++++++++++++------ lib/utils/string_extension.dart | 17 ++ .../twake_link_preview.dart | 4 +- test/utils/string_extension_test.dart | 86 ++++++++++ 4 files changed, 208 insertions(+), 47 deletions(-) diff --git a/lib/pages/chat/events/message/message_content_builder.dart b/lib/pages/chat/events/message/message_content_builder.dart index f4084dfd86..6ba0e95f81 100644 --- a/lib/pages/chat/events/message/message_content_builder.dart +++ b/lib/pages/chat/events/message/message_content_builder.dart @@ -58,7 +58,8 @@ class MessageContentBuilder extends StatelessWidget { bottom: noPadding || event.timelineOverlayMessage ? 0 : 8, ), child: IntrinsicWidth( - stepWidth: stepWidth, + stepWidth: + _isContainsTagName() || _isContainsCodeTag() ? null : stepWidth, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -123,7 +124,9 @@ class MessageContentBuilder extends StatelessWidget { ), ], ), - if (isNeedAddNewLine) + if (isNeedAddNewLine || + _isContainsTagName() || + _isContainsCodeTag()) Visibility( visible: false, maintainSize: true, @@ -177,7 +180,7 @@ class MessageContentBuilder extends StatelessWidget { ); } - Map? _getSizeMessageBubbleWidth( + Map? _getSizeMessageBubbleWidth( BuildContext context, { required double maxWidth, bool ownMessage = false, @@ -251,6 +254,8 @@ class MessageContentBuilder extends StatelessWidget { BuildContext context, double maxWidth, ) { + const double leftMessagePadding = 8.0; + final double messageMaxWidth = maxWidth - leftMessagePadding; return TextPainter( textScaler: MediaQuery.of(context).textScaler, text: TextSpan( @@ -266,7 +271,7 @@ class MessageContentBuilder extends StatelessWidget { ), ), textDirection: TextDirection.ltr, - )..layout(minWidth: 0, maxWidth: maxWidth); + )..layout(minWidth: 0, maxWidth: messageMaxWidth); } double _getWidthMessageTime( @@ -336,60 +341,58 @@ class MessageContentBuilder extends StatelessWidget { bool isNeedAddNewLine = false; - final lastLineWidth = paintedMessageText - .getBoxesForSelection( - TextSelection( - baseOffset: 0, - extentOffset: event - .calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - plaintextBody: true, - ) - .length, - ), - ) - .last - .right; - - if (lastLineWidth >= maxWidth) { - isNeedAddNewLine = true; - totalMessageWidth = maxWidth; - } else { - if (lastLineWidth < messageTextWidth) { - final lastLineToRightBoundarySpace = messageTextWidth - lastLineWidth; - if (lastLineToRightBoundarySpace >= messageTimeAndPaddingWidth) { - final messageAndTimeWidth = messageTextWidth + paddingMessage; - if (messageAndTimeWidth >= maxWidth) { - if (lastLineWidth + messageTimeAndPaddingWidth >= maxWidth) { - isNeedAddNewLine = true; - totalMessageWidth = maxWidth; - } else { - totalMessageWidth = lastLineWidth + messageTimeAndPaddingWidth; - } - } else { - totalMessageWidth = messageAndTimeWidth; - } + final TextRange lastLineRange = paintedMessageText.getLineBoundary( + paintedMessageText.getPositionForOffset( + Offset( + paintedMessageText.size.width, + paintedMessageText.size.height, + ), + ), + ); + final List lastLineBoxes = paintedMessageText.getBoxesForSelection( + TextSelection( + baseOffset: lastLineRange.start, + extentOffset: lastLineRange.end, + ), + ); + + final lastLineWidth = lastLineBoxes.last.right; + + if (lastLineWidth < messageTextWidth) { + final lastLineToRightBoundarySpace = messageTextWidth - lastLineWidth; + if (lastLineToRightBoundarySpace >= messageTimeAndPaddingWidth) { + final messageWithTimeWidth = messageTextWidth + paddingMessage; + if (messageWithTimeWidth < maxWidth) { + totalMessageWidth = messageWithTimeWidth; } else { - final lastLineWidthTimeWidth = + final lastLineWithTimeWidth = lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; - if (lastLineWidthTimeWidth >= maxWidth) { + if (lastLineWithTimeWidth < maxWidth) { + totalMessageWidth = lastLineWithTimeWidth; + } else { isNeedAddNewLine = true; totalMessageWidth = maxWidth; - } else { - totalMessageWidth = lastLineWidthTimeWidth; } } } else { final lastLineWithTimeWidth = lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; - if (lastLineWithTimeWidth >= maxWidth) { + if (lastLineWithTimeWidth < maxWidth) { + totalMessageWidth = lastLineWithTimeWidth; + } else { isNeedAddNewLine = true; totalMessageWidth = maxWidth; - } else { - totalMessageWidth = lastLineWithTimeWidth; } } + } else { + final lastLineWithTimeWidth = + lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; + if (lastLineWithTimeWidth < maxWidth) { + totalMessageWidth = lastLineWithTimeWidth; + } else { + isNeedAddNewLine = true; + totalMessageWidth = maxWidth; + } } final Map result = { @@ -397,4 +400,59 @@ class MessageContentBuilder extends StatelessWidget { }; return result; } + + bool _isContainsTagName() { + const matrixToScheme = "https://matrix.to/#/"; + const matrixScheme = "matrix:"; + final formattedText = event.formattedText; + + if (formattedText.isNotEmpty && formattedText.isContainsATag()) { + final List listHrefs = formattedText.extractAllHrefs(); + for (final href in listHrefs) { + final hrefLower = href.toLowerCase(); + if (hrefLower.startsWith(matrixToScheme) || + hrefLower.startsWith(matrixScheme)) { + var isPill = true; + var identifier = href; + if (hrefLower.startsWith(matrixToScheme)) { + final urlPart = + href.substring(matrixToScheme.length).split('?').first; + try { + identifier = Uri.decodeComponent(urlPart); + } catch (_) { + identifier = urlPart; + } + isPill = + RegExp(r'^[@#!+][^:]+:[^\/]+$').firstMatch(identifier) != null; + } else { + final match = RegExp(r'^matrix:(r|roomid|u)\/([^\/]+)$') + .firstMatch(hrefLower.split('?').first.split('#').first); + isPill = match != null && match.group(2) != null; + if (isPill) { + final sigil = { + 'r': '#', + 'roomid': '!', + 'u': '@', + }[match.group(1)]; + if (sigil == null) { + isPill = false; + } else { + identifier = sigil + match.group(2)!; + } + } + } + if (isPill) { + return true; + } + } + } + } + return false; + } + + bool _isContainsCodeTag() { + final formattedText = event.formattedText; + final codeTagRegex = RegExp(r']*>.*<\/code>'); + return codeTagRegex.hasMatch(formattedText); + } } diff --git a/lib/utils/string_extension.dart b/lib/utils/string_extension.dart index b1a413d731..c688a6d20e 100644 --- a/lib/utils/string_extension.dart +++ b/lib/utils/string_extension.dart @@ -356,4 +356,21 @@ extension StringCasingExtension on String { return replacedText; } + + bool isContainsATag() { + final aTagRegex = RegExp(r']*>([^<]+)'); + return aTagRegex.hasMatch(this); + } + + List extractAllHrefs() { + final regex = RegExp(r']*href="([^"]*)"[^>]*>[^<]*'); + final matches = regex.allMatches(this); + return matches.map((match) => match.group(1)!).toList(); + } + + String? extractInnerText() { + final regex = RegExp(r']*>([^<]*)'); + final match = regex.firstMatch(this); + return match?.group(1); + } } diff --git a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart index 62318032af..fd157dc504 100644 --- a/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart +++ b/lib/widgets/twake_components/twake_preview_link/twake_link_preview.dart @@ -3,7 +3,6 @@ import 'package:fluffychat/pages/chat/events/formatted_text_widget.dart'; import 'package:fluffychat/presentation/extensions/media/url_preview_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/utils/url_launcher.dart'; -import 'package:fluffychat/widgets/clean_rich_text.dart'; import 'package:fluffychat/widgets/mixins/get_preview_url_mixin.dart'; import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_preview_item.dart'; import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_preview_item_style.dart'; @@ -11,6 +10,7 @@ import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_lin import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart' hide Visibility; +import 'package:matrix_link_text/link_text.dart'; import 'package:skeletonizer/skeletonizer.dart'; class TwakeLinkPreview extends StatefulWidget { @@ -70,7 +70,7 @@ class TwakeLinkPreviewController extends State linkStyle: widget.linkStyle, fontSize: widget.fontSize, ) - : TwakeCleanRichText( + : LinkText( text: widget.localizedBody, textStyle: widget.richTextStyle, linkStyle: widget.linkStyle, diff --git a/test/utils/string_extension_test.dart b/test/utils/string_extension_test.dart index e83c472578..d37b6f8164 100644 --- a/test/utils/string_extension_test.dart +++ b/test/utils/string_extension_test.dart @@ -291,4 +291,90 @@ void main() { }); } }); + + group('[isContainsATag] TEST\n', () { + test('[isContainsATag] detects tag', () { + expect( + 'Hello world!'.isContainsATag(), + isTrue, + ); + expect('Hello world!'.isContainsATag(), isFalse); + }); + + test('[isContainsATag] detects tag with attributes', () { + expect( + 'Link' + .isContainsATag(), + isTrue, + ); + }); + + test('[isContainsATag] does not detect other tags', () { + expect('

Hello world!

'.isContainsATag(), isFalse); + }); + }); + + group('[extractAllHrefs] TEST\n', () { + test('extractAllHrefs extracts all hrefs from tags', () { + expect( + 'Link Another Link' + .extractAllHrefs(), + equals(['https://example.com', 'https://another.com']), + ); + }); + + test('extractAllHrefs returns empty list when no tags', () { + expect('Hello world!'.extractAllHrefs(), isEmpty); + }); + + test('extractAllHrefs ignores tags without href', () { + expect( + 'Link Another Link' + .extractAllHrefs(), + equals(['https://example.com']), + ); + }); + + test( + 'extractAllHrefs extracts hrefs from tags with multiple attributes', + () { + expect( + 'Link Another Link' + .extractAllHrefs(), + equals(['https://example.com', 'https://another.com']), + ); + }); + + test('extractAllHrefs extracts href when only one tag', () { + expect( + 'Link'.extractAllHrefs(), + equals(['https://example.com']), + ); + }); + }); + + group('[extractInnerText] TEST\n', () { + test( + 'GIVEN an a tag\n' + 'CONTAINS innerText\n' + 'THEN return innerText\n', () { + expect( + 'Link'.extractInnerText(), + equals('Link'), + ); + }); + + test( + 'GIVEN string without a tag\n' + 'THEN return null\n', () { + expect('Hello world!'.extractInnerText(), isNull); + }); + + test( + 'GIVEN an a tag\n' + 'NOT CONTAINS innerText\n' + 'THEN return an empty string\n', () { + expect(''.extractInnerText(), isEmpty); + }); + }); } From 9d44b1c9dff92951deed122e16dd0adae785cc3d Mon Sep 17 00:00:00 2001 From: hieubt Date: Thu, 11 Apr 2024 19:34:09 +0700 Subject: [PATCH 147/183] TW-1666: Normalize code (cherry picked from commit b94b5722d9c011c09b5d98bebbd6f3bd752b28ed) --- .../message/message_content_builder.dart | 304 +---------------- .../message_content_builder_mixin.dart | 307 ++++++++++++++++++ .../chat/events/message/message_metrics.dart | 17 + 3 files changed, 336 insertions(+), 292 deletions(-) create mode 100644 lib/pages/chat/events/message/message_content_builder_mixin.dart create mode 100644 lib/presentation/model/chat/events/message/message_metrics.dart diff --git a/lib/pages/chat/events/message/message_content_builder.dart b/lib/pages/chat/events/message/message_content_builder.dart index 6ba0e95f81..31bb5b63d9 100644 --- a/lib/pages/chat/events/message/message_content_builder.dart +++ b/lib/pages/chat/events/message/message_content_builder.dart @@ -1,21 +1,15 @@ -import 'package:fluffychat/pages/chat/events/message/display_name_widget.dart'; +import 'package:fluffychat/pages/chat/events/message/message_content_builder_mixin.dart'; import 'package:fluffychat/pages/chat/events/message/message_style.dart'; import 'package:fluffychat/pages/chat/events/message_time.dart'; -import 'package:fluffychat/pages/chat/events/message_time_style.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/pages/chat/events/message/reply_content_widget.dart'; import 'package:fluffychat/pages/chat/events/message_content.dart'; -import 'package:intl/intl.dart' hide TextDirection; -import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:matrix/matrix.dart' hide Visibility; -import 'package:fluffychat/utils/string_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'dart:math'; -class MessageContentBuilder extends StatelessWidget { +class MessageContentBuilder extends StatelessWidget + with MessageContentBuilderMixin { final Event event; final Timeline timeline; final BoxConstraints availableBubbleContraints; @@ -43,23 +37,25 @@ class MessageContentBuilder extends StatelessWidget { MessageTypes.File, MessageTypes.Audio, }.contains(event.messageType); - final sizeMessageBubble = _getSizeMessageBubbleWidth( + final sizeMessageBubble = getSizeMessageBubbleWidth( context, + event: event, maxWidth: availableBubbleContraints.maxWidth, ownMessage: event.isOwnMessage, hideDisplayName: event.hideDisplayName( nextEvent, ), ); - final stepWidth = sizeMessageBubble?.keys.first; - final isNeedAddNewLine = sizeMessageBubble?.values.first ?? false; + final stepWidth = sizeMessageBubble?.totalMessageWidth; + final isNeedAddNewLine = sizeMessageBubble?.isNeedAddNewLine ?? false; return Padding( padding: EdgeInsets.only( bottom: noPadding || event.timelineOverlayMessage ? 0 : 8, ), child: IntrinsicWidth( - stepWidth: - _isContainsTagName() || _isContainsCodeTag() ? null : stepWidth, + stepWidth: isContainsTagName(event) || isContainsSpecialHTMLTag(event) + ? null + : stepWidth, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -125,8 +121,8 @@ class MessageContentBuilder extends StatelessWidget { ], ), if (isNeedAddNewLine || - _isContainsTagName() || - _isContainsCodeTag()) + isContainsTagName(event) || + isContainsSpecialHTMLTag(event)) Visibility( visible: false, maintainSize: true, @@ -179,280 +175,4 @@ class MessageContentBuilder extends StatelessWidget { ), ); } - - Map? _getSizeMessageBubbleWidth( - BuildContext context, { - required double maxWidth, - bool ownMessage = false, - bool hideDisplayName = false, - }) { - final isNotSupportCalcSize = { - MessageTypes.File, - MessageTypes.Image, - MessageTypes.Video, - }.contains(event.messageType); - - if (isNotSupportCalcSize || event.text.isEmpty) { - return null; - } - - final sizeWidthDisplayName = _paintDisplayName( - context, - maxWidth, - ).width; - - final totalMessageWidth = _getWidthMessageAndTime( - context, - maxWidth, - ); - - if (ownMessage || hideDisplayName) { - final Map result = { - totalMessageWidth.keys.first: totalMessageWidth.values.first, - }; - return result; - } - - const rightSpaceDisplayName = 16.0; - - final totalDisplayNameWidth = sizeWidthDisplayName + rightSpaceDisplayName; - - final totalWidth = - max(totalDisplayNameWidth, totalMessageWidth.keys.first); - - final Map result = { - totalWidth: totalMessageWidth.values.first, - }; - - return result; - } - - TextPainter _paintDisplayName( - BuildContext context, - double maxWidth, - ) { - return TextPainter( - textScaler: MediaQuery.of(context).textScaler, - text: TextSpan( - text: event.senderFromMemoryOrFallback - .calcDisplayname() - .shortenDisplayName( - maxCharacters: DisplayNameWidget.maxCharactersDisplayNameBubble, - ), - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.primary, - ), - ), - maxLines: 2, - textDirection: TextDirection.ltr, - )..layout(minWidth: 0, maxWidth: maxWidth); - } - - TextPainter _paintMessageText( - BuildContext context, - double maxWidth, - ) { - const double leftMessagePadding = 8.0; - final double messageMaxWidth = maxWidth - leftMessagePadding; - return TextPainter( - textScaler: MediaQuery.of(context).textScaler, - text: TextSpan( - text: event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - plaintextBody: true, - ), - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface, - ), - ), - textDirection: TextDirection.ltr, - )..layout(minWidth: 0, maxWidth: messageMaxWidth); - } - - double _getWidthMessageTime( - BuildContext context, - double maxWidth, - ) { - final painTimeText = TextPainter( - textScaler: MediaQuery.of(context).textScaler, - text: TextSpan( - text: DateFormat("HH:mm").format(event.originServerTs), - style: Theme.of(context).textTheme.bodySmall?.merge( - TextStyle( - color: event.timelineOverlayMessage - ? Colors.white - : LinagoraRefColors.material().tertiary[30], - letterSpacing: 0.4, - ), - ), - ), - textDirection: TextDirection.ltr, - )..layout(minWidth: 0, maxWidth: maxWidth); - - const pushpinIconSize = MessageStyle.pushpinIconSize; - const paddingAllPushpin = MessageStyle.paddingAllPushpin; - const paddingToTimeSpacing = 4.0; - final seenByRowIconSize = MessageTimeStyle.seenByRowIconSize; - final paddingTimeAndIcon = MessageTimeStyle.paddingTimeAndIcon; - - double totalWidth = painTimeText.width; - - if (event.isPinned) { - totalWidth += paddingTimeAndIcon + - pushpinIconSize + - paddingAllPushpin + - paddingToTimeSpacing; - } - - if (event.isOwnMessage) { - totalWidth += paddingTimeAndIcon + seenByRowIconSize; - } - - return totalWidth; - } - - Map _getWidthMessageAndTime( - BuildContext context, - double maxWidth, - ) { - const spaceMessageAndTime = 4.0; - const paddingMessage = 16.0; - - final paintedMessageText = _paintMessageText( - context, - maxWidth, - ); - - final sizeMessageTime = _getWidthMessageTime( - context, - maxWidth, - ); - - final messageTimeAndPaddingWidth = sizeMessageTime + spaceMessageAndTime; - - final messageTextWidth = paintedMessageText.width; - - double totalMessageWidth = messageTextWidth + paddingMessage; - - bool isNeedAddNewLine = false; - - final TextRange lastLineRange = paintedMessageText.getLineBoundary( - paintedMessageText.getPositionForOffset( - Offset( - paintedMessageText.size.width, - paintedMessageText.size.height, - ), - ), - ); - final List lastLineBoxes = paintedMessageText.getBoxesForSelection( - TextSelection( - baseOffset: lastLineRange.start, - extentOffset: lastLineRange.end, - ), - ); - - final lastLineWidth = lastLineBoxes.last.right; - - if (lastLineWidth < messageTextWidth) { - final lastLineToRightBoundarySpace = messageTextWidth - lastLineWidth; - if (lastLineToRightBoundarySpace >= messageTimeAndPaddingWidth) { - final messageWithTimeWidth = messageTextWidth + paddingMessage; - if (messageWithTimeWidth < maxWidth) { - totalMessageWidth = messageWithTimeWidth; - } else { - final lastLineWithTimeWidth = - lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; - if (lastLineWithTimeWidth < maxWidth) { - totalMessageWidth = lastLineWithTimeWidth; - } else { - isNeedAddNewLine = true; - totalMessageWidth = maxWidth; - } - } - } else { - final lastLineWithTimeWidth = - lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; - if (lastLineWithTimeWidth < maxWidth) { - totalMessageWidth = lastLineWithTimeWidth; - } else { - isNeedAddNewLine = true; - totalMessageWidth = maxWidth; - } - } - } else { - final lastLineWithTimeWidth = - lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; - if (lastLineWithTimeWidth < maxWidth) { - totalMessageWidth = lastLineWithTimeWidth; - } else { - isNeedAddNewLine = true; - totalMessageWidth = maxWidth; - } - } - - final Map result = { - totalMessageWidth: isNeedAddNewLine, - }; - return result; - } - - bool _isContainsTagName() { - const matrixToScheme = "https://matrix.to/#/"; - const matrixScheme = "matrix:"; - final formattedText = event.formattedText; - - if (formattedText.isNotEmpty && formattedText.isContainsATag()) { - final List listHrefs = formattedText.extractAllHrefs(); - for (final href in listHrefs) { - final hrefLower = href.toLowerCase(); - if (hrefLower.startsWith(matrixToScheme) || - hrefLower.startsWith(matrixScheme)) { - var isPill = true; - var identifier = href; - if (hrefLower.startsWith(matrixToScheme)) { - final urlPart = - href.substring(matrixToScheme.length).split('?').first; - try { - identifier = Uri.decodeComponent(urlPart); - } catch (_) { - identifier = urlPart; - } - isPill = - RegExp(r'^[@#!+][^:]+:[^\/]+$').firstMatch(identifier) != null; - } else { - final match = RegExp(r'^matrix:(r|roomid|u)\/([^\/]+)$') - .firstMatch(hrefLower.split('?').first.split('#').first); - isPill = match != null && match.group(2) != null; - if (isPill) { - final sigil = { - 'r': '#', - 'roomid': '!', - 'u': '@', - }[match.group(1)]; - if (sigil == null) { - isPill = false; - } else { - identifier = sigil + match.group(2)!; - } - } - } - if (isPill) { - return true; - } - } - } - } - return false; - } - - bool _isContainsCodeTag() { - final formattedText = event.formattedText; - final codeTagRegex = RegExp(r']*>.*<\/code>'); - return codeTagRegex.hasMatch(formattedText); - } } diff --git a/lib/pages/chat/events/message/message_content_builder_mixin.dart b/lib/pages/chat/events/message/message_content_builder_mixin.dart new file mode 100644 index 0000000000..0db4a87ff2 --- /dev/null +++ b/lib/pages/chat/events/message/message_content_builder_mixin.dart @@ -0,0 +1,307 @@ +import 'dart:math'; +import 'package:fluffychat/pages/chat/events/message/message_style.dart'; +import 'package:fluffychat/pages/chat/events/message_time_style.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/string_extension.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/pages/chat/events/message/display_name_widget.dart'; +import 'package:fluffychat/presentation/model/chat/events/message/message_metrics.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:flutter/material.dart' hide Visibility; +import 'package:intl/intl.dart' hide TextDirection; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:matrix/matrix.dart'; + +mixin MessageContentBuilderMixin { + MessageMetrics? getSizeMessageBubbleWidth( + BuildContext context, { + required Event event, + required double maxWidth, + bool ownMessage = false, + bool hideDisplayName = false, + }) { + final isNotSupportCalcSize = { + MessageTypes.File, + MessageTypes.Image, + MessageTypes.Video, + }.contains(event.messageType); + + if (isNotSupportCalcSize || event.text.isEmpty) { + return null; + } + + final sizeWidthDisplayName = _paintDisplayName( + context, + event, + maxWidth, + ).width; + + final messageMetrics = _getMessageMetrics( + context, + event, + maxWidth, + ); + + if (ownMessage || hideDisplayName) { + return MessageMetrics( + totalMessageWidth: messageMetrics.totalMessageWidth, + isNeedAddNewLine: messageMetrics.isNeedAddNewLine, + ); + } + + const rightSpaceDisplayName = 16.0; + + final totalDisplayNameWidth = sizeWidthDisplayName + rightSpaceDisplayName; + + final totalWidth = max( + totalDisplayNameWidth, + messageMetrics.totalMessageWidth, + ); + + return MessageMetrics( + totalMessageWidth: totalWidth, + isNeedAddNewLine: messageMetrics.isNeedAddNewLine, + ); + } + + TextPainter _paintDisplayName( + BuildContext context, + Event event, + double maxWidth, + ) { + return TextPainter( + textScaler: MediaQuery.of(context).textScaler, + text: TextSpan( + text: event.senderFromMemoryOrFallback + .calcDisplayname() + .shortenDisplayName( + maxCharacters: DisplayNameWidget.maxCharactersDisplayNameBubble, + ), + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of( + context, + ).colorScheme.primary, + ), + ), + maxLines: 2, + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: maxWidth); + } + + TextPainter _paintMessageText( + BuildContext context, + Event event, + double maxWidth, + ) { + const double leftMessagePadding = 8.0; + final double messageMaxWidth = maxWidth - leftMessagePadding; + return TextPainter( + textScaler: MediaQuery.of(context).textScaler, + text: TextSpan( + text: event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + plaintextBody: true, + ), + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface, + ), + ), + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: messageMaxWidth); + } + + double _getWidthMessageTime( + BuildContext context, + Event event, + double maxWidth, + ) { + final painTimeText = TextPainter( + textScaler: MediaQuery.of(context).textScaler, + text: TextSpan( + text: DateFormat("HH:mm").format(event.originServerTs), + style: Theme.of(context).textTheme.bodySmall?.merge( + TextStyle( + color: event.timelineOverlayMessage + ? Colors.white + : LinagoraRefColors.material().tertiary[30], + letterSpacing: 0.4, + ), + ), + ), + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: maxWidth); + + const pushpinIconSize = MessageStyle.pushpinIconSize; + const paddingAllPushpin = MessageStyle.paddingAllPushpin; + const paddingToTimeSpacing = 4.0; + final seenByRowIconSize = MessageTimeStyle.seenByRowIconSize; + final paddingTimeAndIcon = MessageTimeStyle.paddingTimeAndIcon; + + double totalWidth = painTimeText.width; + + if (event.isPinned) { + totalWidth += paddingTimeAndIcon + + pushpinIconSize + + paddingAllPushpin + + paddingToTimeSpacing; + } + + if (event.isOwnMessage) { + totalWidth += paddingTimeAndIcon + seenByRowIconSize; + } + + return totalWidth; + } + + MessageMetrics _getMessageMetrics( + BuildContext context, + Event event, + double maxWidth, + ) { + const spaceMessageAndTime = 4.0; + const paddingMessage = 16.0; + + final paintedMessageText = _paintMessageText( + context, + event, + maxWidth, + ); + final sizeMessageTime = _getWidthMessageTime( + context, + event, + maxWidth, + ); + final messageTimeAndPaddingWidth = sizeMessageTime + spaceMessageAndTime; + final messageTextWidth = paintedMessageText.width; + final TextRange lastLineRange = paintedMessageText.getLineBoundary( + paintedMessageText.getPositionForOffset( + Offset( + paintedMessageText.size.width, + paintedMessageText.size.height, + ), + ), + ); + final List lastLineBoxes = paintedMessageText.getBoxesForSelection( + TextSelection( + baseOffset: lastLineRange.start, + extentOffset: lastLineRange.end, + ), + ); + final lastLineWidth = lastLineBoxes.last.right; + + double totalMessageWidth = messageTextWidth + paddingMessage; + + if (lastLineWidth < messageTextWidth && + messageTextWidth - lastLineWidth >= messageTimeAndPaddingWidth && + messageTextWidth + paddingMessage < maxWidth) { + totalMessageWidth = messageTextWidth + paddingMessage; + } else { + totalMessageWidth = _calculateTotalMessageWidth( + lastLineWidth, + messageTimeAndPaddingWidth, + paddingMessage, + maxWidth, + ); + } + + final isNeedAddNewLine = _checkNeedAddNewLine(totalMessageWidth, maxWidth); + final metrics = MessageMetrics( + totalMessageWidth: totalMessageWidth, + isNeedAddNewLine: isNeedAddNewLine, + ); + + return metrics; + } + + double _calculateTotalMessageWidth( + double lastLineWidth, + double messageTimeAndPaddingWidth, + double paddingMessage, + double maxWidth, + ) { + final lastLineWithTimeWidth = + lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; + + if (lastLineWithTimeWidth < maxWidth) { + return lastLineWithTimeWidth; + } else { + return maxWidth; + } + } + + bool _checkNeedAddNewLine(double totalMessageWidth, double maxWidth) { + return totalMessageWidth == maxWidth; + } + + bool isContainsTagName(Event event) { + const matrixToScheme = "https://matrix.to/#/"; + const matrixScheme = "matrix:"; + + final formattedText = event.formattedText; + + if (formattedText.isEmpty || !formattedText.isContainsATag()) { + return false; + } + + final List listHrefs = formattedText.extractAllHrefs(); + + for (final href in listHrefs) { + final hrefLower = href.toLowerCase(); + + if (!hrefLower.startsWith(matrixToScheme) && + !hrefLower.startsWith(matrixScheme)) continue; + + var isPill = true; + + if (hrefLower.startsWith(matrixToScheme)) { + isPill = _handleMatrixToSchemeTagName(href, matrixToScheme); + } else { + isPill = _handleMatrixSchemeTagName(hrefLower); + } + + if (isPill) { + return true; + } + } + + return false; + } + + bool _handleMatrixToSchemeTagName(String href, String matrixToScheme) { + final urlPart = href.substring(matrixToScheme.length).split('?').first; + var identifier = ''; + try { + identifier = Uri.decodeComponent(urlPart); + } catch (_) { + identifier = urlPart; + } + return RegExp(r'^[@#!+][^:]+:[^\/]+$').firstMatch(identifier) != null; + } + + bool _handleMatrixSchemeTagName(String hrefLower) { + final match = RegExp(r'^matrix:(r|roomid|u)\/([^\/]+)$') + .firstMatch(hrefLower.split('?').first.split('#').first); + if (match == null || match.group(2) == null) { + return false; + } + + final sigil = { + 'r': '#', + 'roomid': '!', + 'u': '@', + }[match.group(1)]; + + return sigil != null; + } + + bool isContainsSpecialHTMLTag(Event event) { + final formattedText = event.formattedText; + final specialTagRegex = RegExp( + r'<(b|strong|tt|h[1-6]|code)[^>]*>.*<\/(b|strong|tt|h[1-6]|code)>', + ); + return specialTagRegex.hasMatch(formattedText); + } +} diff --git a/lib/presentation/model/chat/events/message/message_metrics.dart b/lib/presentation/model/chat/events/message/message_metrics.dart new file mode 100644 index 0000000000..66db4c3a10 --- /dev/null +++ b/lib/presentation/model/chat/events/message/message_metrics.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; + +class MessageMetrics extends Equatable { + final double totalMessageWidth; + final bool isNeedAddNewLine; + + const MessageMetrics({ + required this.totalMessageWidth, + required this.isNeedAddNewLine, + }); + + @override + List get props => [ + totalMessageWidth, + isNeedAddNewLine, + ]; +} From f39f419220ad2ad299b8881e64b6f614e1f395ad Mon Sep 17 00:00:00 2001 From: hieubt Date: Fri, 12 Apr 2024 11:14:06 +0700 Subject: [PATCH 148/183] TW-1666: Support others special tag (cherry picked from commit ebe686d44667c5987152676e3e8a4c6970ad3c74) --- .../message/message_content_builder_mixin.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/events/message/message_content_builder_mixin.dart b/lib/pages/chat/events/message/message_content_builder_mixin.dart index 0db4a87ff2..56237aebe7 100644 --- a/lib/pages/chat/events/message/message_content_builder_mixin.dart +++ b/lib/pages/chat/events/message/message_content_builder_mixin.dart @@ -299,8 +299,22 @@ mixin MessageContentBuilderMixin { bool isContainsSpecialHTMLTag(Event event) { final formattedText = event.formattedText; + final specialTags = [ + 'b', + 'strong', + 'tt', + 'h[1-6]', + 'code', + 'pre', + 'blockquote', + 'i', + 'em', + ]; + final specialTagsPattern = specialTags.join('|'); final specialTagRegex = RegExp( - r'<(b|strong|tt|h[1-6]|code)[^>]*>.*<\/(b|strong|tt|h[1-6]|code)>', + '<($specialTagsPattern)[^>]*>.*|]*>', + multiLine: true, + dotAll: true, ); return specialTagRegex.hasMatch(formattedText); } From 808fce2568f429d767b8b777acf6c1c4d0dc2ad9 Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 16 Apr 2024 12:01:37 +0700 Subject: [PATCH 149/183] TW-1666: Write unit test (cherry picked from commit 4aa893093bf6b3a6d064217efdda828fa0c7bf91) --- .../message_content_builder_mixin.dart | 5 +- .../message_content_builder_mixin_test.dart | 1024 +++++++++++++++++ 2 files changed, 1028 insertions(+), 1 deletion(-) create mode 100644 test/pages/chat/events/message/message_content_builder_mixin_test.dart diff --git a/lib/pages/chat/events/message/message_content_builder_mixin.dart b/lib/pages/chat/events/message/message_content_builder_mixin.dart index 56237aebe7..c808dcb854 100644 --- a/lib/pages/chat/events/message/message_content_builder_mixin.dart +++ b/lib/pages/chat/events/message/message_content_builder_mixin.dart @@ -193,11 +193,13 @@ mixin MessageContentBuilderMixin { final lastLineWidth = lastLineBoxes.last.right; double totalMessageWidth = messageTextWidth + paddingMessage; + bool isNeedAddNewLine; if (lastLineWidth < messageTextWidth && messageTextWidth - lastLineWidth >= messageTimeAndPaddingWidth && messageTextWidth + paddingMessage < maxWidth) { totalMessageWidth = messageTextWidth + paddingMessage; + isNeedAddNewLine = false; } else { totalMessageWidth = _calculateTotalMessageWidth( lastLineWidth, @@ -205,9 +207,10 @@ mixin MessageContentBuilderMixin { paddingMessage, maxWidth, ); + + isNeedAddNewLine = _checkNeedAddNewLine(totalMessageWidth, maxWidth); } - final isNeedAddNewLine = _checkNeedAddNewLine(totalMessageWidth, maxWidth); final metrics = MessageMetrics( totalMessageWidth: totalMessageWidth, isNeedAddNewLine: isNeedAddNewLine, diff --git a/test/pages/chat/events/message/message_content_builder_mixin_test.dart b/test/pages/chat/events/message/message_content_builder_mixin_test.dart new file mode 100644 index 0000000000..6f5eba3251 --- /dev/null +++ b/test/pages/chat/events/message/message_content_builder_mixin_test.dart @@ -0,0 +1,1024 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'package:fluffychat/config/localizations/localization_service.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/message/message_content_builder_mixin.dart'; +import 'package:fluffychat/presentation/model/chat/events/message/message_metrics.dart'; +import 'package:fluffychat/utils/custom_scroll_behaviour.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/widgets/theme_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:matrix/matrix.dart'; +import 'package:matrix_api_lite/fake_matrix_api.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class MockUpMessageContentBuilder with MessageContentBuilderMixin {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late MockUpMessageContentBuilder mockUpMessageContentBuilder; + setUpAll(() { + mockUpMessageContentBuilder = MockUpMessageContentBuilder(); + final getIt = GetIt.instance; + getIt.registerSingleton(ResponsiveUtils()); + }); + + final client = Client('client', httpClient: FakeMatrixApi()); + final room = Room(id: '!room:example.abc', client: client); + final fileEvent = Event( + content: { + 'body': 'something-important.doc', + 'filename': 'something-important.doc', + 'info': {'mimetype': 'application/msword', 'size': 46144}, + 'msgtype': 'm.file', + 'url': 'mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe', + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1894270481925), + room: room, + ); + final imageEvent = Event( + content: { + 'body': 'filename.jpg', + 'info': {'h': 398, 'mimetype': 'image/jpeg', 'size': 31037, 'w': 394}, + 'msgtype': 'm.image', + 'url': 'mxc://example.org/JWEIFJgwEIhweiWJE', + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + final videoEvent = Event( + content: { + 'body': 'Gangnam Style', + 'info': { + 'duration': 2140786, + 'h': 320, + 'mimetype': 'video/mp4', + 'size': 1563685, + 'thumbnail_info': { + 'h': 300, + 'mimetype': 'image/jpeg', + 'size': 46144, + 'w': 300, + }, + 'thumbnail_url': 'mxc://example.org/FHyPlCeYUSFFxlgbQYZmoEoe', + 'w': 480, + }, + 'msgtype': 'm.video', + 'url': 'mxc://example.org/a526eYUSFFxlgbQYZmo442', + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + final emptyTextEvent = Event( + content: { + 'body': '', + 'msgtype': 'm.text', + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + + group( + '[MessageContentBuilderMixin] TEST\n', + () { + // In unit testing, TextScaler(1.5) simulates medium web font size. + const textScaler = TextScaler.linear(1.5); + + Future runTest( + WidgetTester tester, { + required Event event, + required double maxWidth, + MessageMetrics? expectedMetrics, + bool ownMessage = false, + bool hideDisplayName = false, + }) async { + MessageMetrics? getSizeForEmptyTextEvent; + + await tester.pumpWidget( + ThemeBuilder( + builder: (context, themeMode, primaryColor) => MaterialApp( + locale: const Locale('en'), + scrollBehavior: CustomScrollBehavior(), + localizationsDelegates: const [ + LocaleNamesLocalizationsDelegate(), + L10n.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: LocalizationService.supportedLocales, + theme: TwakeThemes.buildTheme( + context, + Brightness.light, + primaryColor, + ), + builder: (context, child) => MediaQuery( + data: const MediaQueryData(textScaler: textScaler), + child: child!, + ), + home: Scaffold( + body: Builder( + builder: (context) { + getSizeForEmptyTextEvent = + mockUpMessageContentBuilder.getSizeMessageBubbleWidth( + context, + event: event, + maxWidth: maxWidth, + ownMessage: ownMessage, + hideDisplayName: hideDisplayName, + ); + return const SizedBox(); + }, + ), + ), + ), + ), + ); + await tester.pump(); + + if (expectedMetrics != null) { + expect(getSizeForEmptyTextEvent, isNotNull); + expect( + getSizeForEmptyTextEvent, + isA(), + ); + expect( + getSizeForEmptyTextEvent!.totalMessageWidth, + equals(expectedMetrics.totalMessageWidth), + ); + expect( + getSizeForEmptyTextEvent!.isNeedAddNewLine, + equals(expectedMetrics.isNeedAddNewLine), + ); + } else { + expect(getSizeForEmptyTextEvent, isNull); + } + } + + group( + '[getSizeMessageBubbleWidth] TEST\n' + 'GIVEN platform is Web\n' + 'THEN maxWidth for message is 504.0\n', + () { + const messageMaxWidthWeb = 504.0; + group('GIVEN message type is not supported for calculate\n', () { + testWidgets( + 'GIVEN message type is file\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: fileEvent, + maxWidth: messageMaxWidthWeb, + ); + }, + ); + testWidgets( + 'GIVEN message type is image\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: imageEvent, + maxWidth: messageMaxWidthWeb, + ); + }, + ); + testWidgets( + 'GIVEN message type is video\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: videoEvent, + maxWidth: messageMaxWidthWeb, + ); + }, + ); + }); + + testWidgets( + 'GIVEN message text is empty\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: emptyTextEvent, + maxWidth: messageMaxWidthWeb, + ); + }, + ); + + group( + 'GIVEN ownMessage is true\n' + 'OR hideDisplayName is true\n' + 'THEN return message width\n', + () { + testWidgets( + 'GIVEN message body has multiple lines\n' + 'AND last line of message has enough space for timeline\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is false\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "sioaldhgowehg wegh welg\n- Pourquoi pas?\n- Non problem\n- Mais je ne comprend pas\n- d'arrcord\n- ádgnaslg\n- ádasd\n- asdg ag\n- asegw sioaldhg\n- helfnwlgweg", + "format": "org.matrix.custom.html", + "formatted_body": + "sioaldhgowehg wegh welg
- Pourquoi pas?
- Non problem
- Mais je ne comprend pas
- d'arrcord
- ádgnaslg
- ádasd
- asdg ag
- asegw sioaldhg
- helfnwlgweg", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + + const expectedMetrics = MessageMetrics( + totalMessageWidth: 421.97491455078125, + isNeedAddNewLine: false, + ); + + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthWeb, + expectedMetrics: expectedMetrics, + ownMessage: true, + ); + }, + ); + testWidgets( + 'GIVEN message body has multiple lines\n' + 'AND last line don\'t have enough space for time line\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is true\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "#2730 Fallback value for Always read receipt settings is false -> Done\n\n#2628 Disable view PDF file in mobile -> Done\n\n#2726 Remove logo in printed email -> Done\n\n#2737 View PDF in js to support download with name -> Done", + "format": "org.matrix.custom.html", + "formatted_body": + "

#2730 Fallback value for Always read receipt settings is false -> Done


#2628 Disable view PDF file in mobile -> Done


#2726 Remove logo in printed email -> Done


#2737 View PDF in js to support download with name -> Done

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + unsigned: { + "age": 26236536, + "com.famedly.famedlysdk.message_sending_status": 2, + }, + ); + const expectedMetrics = MessageMetrics( + totalMessageWidth: messageMaxWidthWeb, + isNeedAddNewLine: true, + ); + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthWeb, + expectedMetrics: expectedMetrics, + ownMessage: true, + ); + }, + ); + }, + ); + + group( + 'GIVEN ownMessage is false\n' + 'AND hideDisplayName is false\n', + () { + testWidgets( + 'GIVEN width of display name is wider than message body\n' + 'THEN return message bubble width is width of display name\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": "Hello", + "format": "org.matrix.custom.html", + "formatted_body": "

Hello

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@exampleabcdh:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + const expectedMetrics = MessageMetrics( + totalMessageWidth: 238.0, + isNeedAddNewLine: false, + ); + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthWeb, + expectedMetrics: expectedMetrics, + ownMessage: false, + hideDisplayName: false, + ); + }, + ); + + testWidgets( + 'GIVEN width of display name is smaller than message body\n' + 'AND message body has multiple lines\n' + 'AND last line of message has enough space for timeline\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is false\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "sioaldhgowehg wegh welg\n- Pourquoi pas?\n- Non problem\n- Mais je ne comprend pas\n- d'arrcord\n- ádgnaslg\n- ádasd\n- asdg ag\n- asegw sioaldhg\n- helfnwlgweg", + "format": "org.matrix.custom.html", + "formatted_body": + "sioaldhgowehg wegh welg
- Pourquoi pas?
- Non problem
- Mais je ne comprend pas
- d'arrcord
- ádgnaslg
- ádasd
- asdg ag
- asegw sioaldhg
- helfnwlgweg", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + const expectedMetrics = MessageMetrics( + totalMessageWidth: 421.97491455078125, + isNeedAddNewLine: false, + ); + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthWeb, + expectedMetrics: expectedMetrics, + ownMessage: false, + hideDisplayName: false, + ); + }, + ); + + testWidgets( + 'GIVEN width of display name is smaller than message body\n' + 'AND message body has multiple lines\n' + 'AND last line don\'t have enough space for time line\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is true\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "#2730 Fallback value for Always read receipt settings is false -> Done\n\n#2628 Disable view PDF file in mobile -> Done\n\n#2726 Remove logo in printed email -> Done\n\n#2737 View PDF in js to support download with name -> Done", + "format": "org.matrix.custom.html", + "formatted_body": + "

#2730 Fallback value for Always read receipt settings is false -> Done


#2628 Disable view PDF file in mobile -> Done


#2726 Remove logo in printed email -> Done


#2737 View PDF in js to support download with name -> Done

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + const expectedMetrics = MessageMetrics( + totalMessageWidth: messageMaxWidthWeb, + isNeedAddNewLine: true, + ); + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthWeb, + expectedMetrics: expectedMetrics, + ownMessage: false, + hideDisplayName: false, + ); + }, + ); + }, + ); + }, + ); + + group( + '[getSizeMessageBubbleWidth] TEST\n' + 'GIVEN platform is Mobile\n' + 'THEN maxWidth for message is 360.0\n', + () { + const messageMaxWidthMobile = 412.0; + group('GIVEN message type is not supported for calculate\n', () { + testWidgets( + 'GIVEN message type is file\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: fileEvent, + maxWidth: messageMaxWidthMobile, + ); + }, + ); + testWidgets( + 'GIVEN message type is image\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: imageEvent, + maxWidth: messageMaxWidthMobile, + ); + }, + ); + testWidgets( + 'GIVEN message type is video\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: videoEvent, + maxWidth: messageMaxWidthMobile, + ); + }, + ); + }); + + testWidgets( + 'GIVEN message text is empty\n' + 'THEN return null\n', + (WidgetTester tester) async { + await runTest( + tester, + event: emptyTextEvent, + maxWidth: messageMaxWidthMobile, + ); + }, + ); + + group( + 'GIVEN ownMessage is true\n' + 'OR hideDisplayName is true\n' + 'THEN return message width\n', + () { + testWidgets( + 'GIVEN message body has multiple lines\n' + 'AND last line of message has enough space for timeline\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is false\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "#2730 Fallback value for Always read receipt settings is false -> Done\n\n#2628 Disable view PDF file in mobile -> Done\n\n#2726 Remove logo in printed email -> Done\n\n#2737 View PDF in js to support download with name -> Done", + "format": "org.matrix.custom.html", + "formatted_body": + "

#2730 Fallback value for Always read receipt settings is false -> Done


#2628 Disable view PDF file in mobile -> Done


#2726 Remove logo in printed email -> Done


#2737 View PDF in js to support download with name -> Done

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + + const expectedMetrics = MessageMetrics( + totalMessageWidth: 398.12469482421875, + isNeedAddNewLine: false, + ); + + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthMobile, + expectedMetrics: expectedMetrics, + ownMessage: true, + ); + }, + ); + testWidgets( + 'GIVEN message body has multiple lines\n' + 'AND last line don\'t have enough space for time line\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is true\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "- Copy/Drop text from LibreOffice files to composer\n- Download PDF file from Chrome viewer\n- Download attachment for mobile\n- Small improvement for Printing email", + "format": "org.matrix.custom.html", + "formatted_body": + "- Copy/Drop text from LibreOffice files to composer
- Download PDF file from Chrome viewer
- Download attachment for mobile
- Small improvement for Printing email", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + const expectedMetrics = MessageMetrics( + totalMessageWidth: messageMaxWidthMobile, + isNeedAddNewLine: true, + ); + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthMobile, + expectedMetrics: expectedMetrics, + ownMessage: true, + ); + }, + ); + }, + ); + + group( + 'GIVEN ownMessage is false\n' + 'AND hideDisplayName is false\n', + () { + testWidgets( + 'GIVEN width of display name is wider than message body\n' + 'THEN return message bubble width is width of display name\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": "Hello", + "format": "org.matrix.custom.html", + "formatted_body": "

Hello

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@exampleabcdh:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + const displayNameWithPaddingWidth = 238.0; + const expectedMetrics = MessageMetrics( + totalMessageWidth: displayNameWithPaddingWidth, + isNeedAddNewLine: false, + ); + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthMobile, + expectedMetrics: expectedMetrics, + ownMessage: false, + hideDisplayName: false, + ); + }, + ); + + testWidgets( + 'GIVEN width of display name is smaller than message body\n' + 'AND message body has multiple lines\n' + 'AND last line of message has enough space for timeline\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is false\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "#2730 Fallback value for Always read receipt settings is false -> Done\n\n#2628 Disable view PDF file in mobile -> Done\n\n#2726 Remove logo in printed email -> Done\n\n#2737 View PDF in js to support download with name -> Done", + "format": "org.matrix.custom.html", + "formatted_body": + "

#2730 Fallback value for Always read receipt settings is false -> Done


#2628 Disable view PDF file in mobile -> Done


#2726 Remove logo in printed email -> Done


#2737 View PDF in js to support download with name -> Done

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + + const expectedMetrics = MessageMetrics( + totalMessageWidth: 398.12469482421875, + isNeedAddNewLine: false, + ); + + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthMobile, + expectedMetrics: expectedMetrics, + ownMessage: true, + ); + }, + ); + + testWidgets( + 'GIVEN width of display name is smaller than message body\n' + 'AND message body has multiple lines\n' + 'AND last line don\'t have enough space for time line\n' + 'THEN return total message width is width of longest line\n' + 'AND isNeedAddNewLine is true\n', + (WidgetTester tester) async { + final eventToTest = Event( + content: { + "msgtype": "m.text", + "body": + "- Copy/Drop text from LibreOffice files to composer\n- Download PDF file from Chrome viewer\n- Download attachment for mobile\n- Small improvement for Printing email", + "format": "org.matrix.custom.html", + "formatted_body": + "- Copy/Drop text from LibreOffice files to composer
- Download PDF file from Chrome viewer
- Download attachment for mobile
- Small improvement for Printing email", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + const expectedMetrics = MessageMetrics( + totalMessageWidth: messageMaxWidthMobile, + isNeedAddNewLine: true, + ); + await runTest( + tester, + event: eventToTest, + maxWidth: messageMaxWidthMobile, + expectedMetrics: expectedMetrics, + ownMessage: true, + ); + }, + ); + }, + ); + }, + ); + + group('[isContainsTagName] TEST\n', () { + test( + 'GIVEN body of event contains tag name\n' + 'THEN return true\n', + () { + final listEventWidthTagName = [ + Event( + content: { + "msgtype": "m.text", + "body": "!jaweog:example.com", + "format": "org.matrix.custom.html", + "formatted_body": + "!jaweog:example.com", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "@alice:example.com", + "format": "org.matrix.custom.html", + "formatted_body": + "@alice:example.com", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "#lihs:example.com", + "format": "org.matrix.custom.html", + "formatted_body": + "#lihs:example.com", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "@[Alice]", + "format": "org.matrix.custom.html", + "formatted_body": + "@[Alice]", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + ]; + for (final event in listEventWidthTagName) { + final isContainsTagName = + mockUpMessageContentBuilder.isContainsTagName(event); + expect(isContainsTagName, isTrue); + } + }, + ); + test( + 'GIVEN body of event does not contain tag name\n' + 'THEN return false\n', () { + final normalEvent = Event( + content: { + 'body': 'Hello', + 'msgtype': 'm.text', + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + final isContainsTagName = + mockUpMessageContentBuilder.isContainsTagName(normalEvent); + expect(isContainsTagName, isFalse); + }); + }); + + group('[isContainsSpecialHTMLTag] test\n', () { + test( + 'GIVEN event contains special HTML tags\n' + 'THEN return true\n', + () { + final listEventWithSpecialTag = [ + Event( + content: { + "msgtype": "m.text", + "body": "*example*", + "format": "org.matrix.custom.html", + "formatted_body": "example", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "# Header 1", + "format": "org.matrix.custom.html", + "formatted_body": "

Header 1

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "## Header 2", + "format": "org.matrix.custom.html", + "formatted_body": "

Header 2

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "### Header 3", + "format": "org.matrix.custom.html", + "formatted_body": "

Header 3

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "#### Header 4", + "format": "org.matrix.custom.html", + "formatted_body": "

Header 4

", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "##### Header 5", + "format": "org.matrix.custom.html", + "formatted_body": "
Header 5
", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "###### Header 6", + "format": "org.matrix.custom.html", + "formatted_body": "
Header 6
", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "```example```", + "format": "org.matrix.custom.html", + "formatted_body": "
example
", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "> example", + "format": "org.matrix.custom.html", + "formatted_body": "
example
", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "bold text", + "format": "org.matrix.custom.html", + "formatted_body": "bold text", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "strong text", + "format": "org.matrix.custom.html", + "formatted_body": "strong text", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "teletype text", + "format": "org.matrix.custom.html", + "formatted_body": "teletype text", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "```\nif () then () else ()\n```", + "format": "org.matrix.custom.html", + "formatted_body": + "
if () then () else ()\n
", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "*Italic text*", + "format": "org.matrix.custom.html", + "formatted_body": "Italic text", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + Event( + content: { + "msgtype": "m.text", + "body": "___", + "format": "org.matrix.custom.html", + "formatted_body": "
", + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ), + ]; + for (final event in listEventWithSpecialTag) { + final isContainsSpecialHTMLTag = + mockUpMessageContentBuilder.isContainsSpecialHTMLTag(event); + expect(isContainsSpecialHTMLTag, isTrue); + } + }, + ); + test( + 'GIVEN event does not contain special HTML tags\n' + 'THEN return false\n', + () { + final normalEvent = Event( + content: { + 'body': 'Hello', + 'msgtype': 'm.text', + }, + type: 'm.room.message', + eventId: '\$143273582443PhrSn:example.org', + senderId: '@example:example.org', + originServerTs: + DateTime.fromMillisecondsSinceEpoch(1432735824653), + room: room, + ); + final isContainsSpecialHTMLTag = mockUpMessageContentBuilder + .isContainsSpecialHTMLTag(normalEvent); + expect(isContainsSpecialHTMLTag, isFalse); + }, + ); + }); + }, + ); +} From babf47f0ca78010f47fe759101724639744abf5e Mon Sep 17 00:00:00 2001 From: hieubt Date: Tue, 16 Apr 2024 12:01:48 +0700 Subject: [PATCH 150/183] TW-1666: Write ADR (cherry picked from commit 91d60de61a1d201db0bd8956d90ebc855fcb482f) --- docs/adr/0021--chat-message-bubbles-width.md | 152 ++++++++++++++++++ lib/widgets/clean_rich_text.dart | 10 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- .../message_content_builder_mixin_test.dart | 4 +- 5 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 docs/adr/0021--chat-message-bubbles-width.md diff --git a/docs/adr/0021--chat-message-bubbles-width.md b/docs/adr/0021--chat-message-bubbles-width.md new file mode 100644 index 0000000000..fec8593d33 --- /dev/null +++ b/docs/adr/0021--chat-message-bubbles-width.md @@ -0,0 +1,152 @@ +# 21. Chat message bubbles width + +Date: 2024-04-16 + +## Status + +Accepted + +## Issue: +[#1666](https://github.com/linagora/twake-on-matrix/issues/1666) + +## Context + +* We introduced a non-visible timeline at the end of the message body within the `TextSpan` to create a blank space for the visible timeline. However, we did not adjust the non-visible timeline to match the scale of the visible one, leading to an overlap with the text. +* By incorporating a `Widget` at the end of a `TextSpan`, the size of the last line of the message body decreases when the system font size is altered. +* In the process of rendering message bubble, we only computed the width of the plain message body and compared it with the width of display name, using the maximum value for bubble step width. This approach is insufficient as it only considers plain text. + +## Decision + +* Eliminate the non-visible timeline that follows the message body. +* Re-compute the message body's width by taking into account both the plain text, padding and timeline: + + * Calculating width of plain text: (reduce `maxWidth` by left padding of message `8.0`) + ```dart + const double leftMessagePadding = 8.0; + final double messageMaxWidth = maxWidth - leftMessagePadding; + return TextPainter( + textScaler: MediaQuery.of(context).textScaler, + text: TextSpan( + text: event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + plaintextBody: true, + ), + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface, + ), + ), + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: messageMaxWidth); + ``` + * Calculating the width of the visible timeline, taking into account the pin and seen icon: + ```dart + final painTimeText = TextPainter( + textScaler: MediaQuery.of(context).textScaler, + text: TextSpan( + text: DateFormat("HH:mm").format(event.originServerTs), + style: Theme.of(context).textTheme.bodySmall?.merge( + TextStyle( + color: event.timelineOverlayMessage + ? Colors.white + : LinagoraRefColors.material().tertiary[30], + letterSpacing: 0.4, + ), + ), + ), + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: maxWidth); + + const pushpinIconSize = MessageStyle.pushpinIconSize; + const paddingAllPushpin = MessageStyle.paddingAllPushpin; + const paddingToTimeSpacing = 4.0; + final seenByRowIconSize = MessageTimeStyle.seenByRowIconSize; + final paddingTimeAndIcon = MessageTimeStyle.paddingTimeAndIcon; + + double totalWidth = painTimeText.width; + + if (event.isPinned) { + totalWidth += paddingTimeAndIcon + + pushpinIconSize + + paddingAllPushpin + + paddingToTimeSpacing; + } + + if (event.isOwnMessage) { + totalWidth += paddingTimeAndIcon + seenByRowIconSize; + } + + return totalWidth; + ``` + + * To calculate the total message body width: + * Use the following logic: + * If message body contains tag name then create a new line for the timeline. + * If message body contains special HTML tags: [`b`, `strong`, `tt`, `h[1-6]`, `code`, `pre`, `blockquote`, `i`, `em`] then create new line for the timeline + * If the last line has sufficient space for the timeline, then the total width equals the sum of the message width and padding. + * Otherwise, add the width of the timeline to the last line width: + * If the sum of the last line width and timeline width is less than the maximum width, then the total width equals this sum. + * If not, set the total width to the maximum width and create a new line for the timeline. + * Calculating last line width + ```dart + final TextRange lastLineRange = paintedMessageText.getLineBoundary( + paintedMessageText.getPositionForOffset( + Offset( + paintedMessageText.size.width, + paintedMessageText.size.height, + ), + ), + ); + final List lastLineBoxes = paintedMessageText.getBoxesForSelection( + TextSelection( + baseOffset: lastLineRange.start, + extentOffset: lastLineRange.end, + ), + ); + final lastLineWidth = lastLineBoxes.last.right; + ``` + * Calculating total width: + ```dart + if (lastLineWidth < messageTextWidth && + messageTextWidth - lastLineWidth >= messageTimeAndPaddingWidth && + messageTextWidth + paddingMessage < maxWidth) { + totalMessageWidth = messageTextWidth + paddingMessage; + isNeedAddNewLine = false; + } else { + totalMessageWidth = _calculateTotalMessageWidth( + lastLineWidth, + messageTimeAndPaddingWidth, + paddingMessage, + maxWidth, + ); + + isNeedAddNewLine = _checkNeedAddNewLine(totalMessageWidth, maxWidth); + } + ``` + ```dart + double _calculateTotalMessageWidth( + double lastLineWidth, + double messageTimeAndPaddingWidth, + double paddingMessage, + double maxWidth, + ) { + final lastLineWithTimeWidth = + lastLineWidth + messageTimeAndPaddingWidth + paddingMessage; + + if (lastLineWithTimeWidth < maxWidth) { + return lastLineWithTimeWidth; + } else { + return maxWidth; + } + } + + bool _checkNeedAddNewLine(double totalMessageWidth, double maxWidth) { + return totalMessageWidth == maxWidth; + } + ``` + +## Consequences + +* Timeline will not overlap the message body \ No newline at end of file diff --git a/lib/widgets/clean_rich_text.dart b/lib/widgets/clean_rich_text.dart index 23c5a76da0..c6e8e89072 100644 --- a/lib/widgets/clean_rich_text.dart +++ b/lib/widgets/clean_rich_text.dart @@ -3,7 +3,7 @@ import 'package:matrix_link_text/link_text.dart'; class TwakeCleanRichText extends StatelessWidget { final String text; - final Widget? childWidget; + final Widget childWidget; final TextStyle? textStyle; final TextStyle? linkStyle; final TextAlign? textAlign; @@ -14,7 +14,7 @@ class TwakeCleanRichText extends StatelessWidget { const TwakeCleanRichText({ Key? key, required this.text, - this.childWidget, + required this.childWidget, this.textStyle, this.linkStyle, this.textAlign = TextAlign.start, @@ -36,10 +36,8 @@ class TwakeCleanRichText extends StatelessWidget { themeData: Theme.of(context), textSpanBuilder: textSpanBuilder, ), - if (childWidget != null) ...[ - const WidgetSpan(child: SizedBox(width: 4)), - WidgetSpan(child: childWidget!), - ], + const WidgetSpan(child: SizedBox(width: 4)), + WidgetSpan(child: childWidget), ], ), textAlign: textAlign, diff --git a/pubspec.lock b/pubspec.lock index 7a337039ba..6d22a13ddc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -965,8 +965,8 @@ packages: dependency: "direct main" description: path: "." - ref: time-overlap - resolved-ref: "74da044c1e1b62b0c2b0eee6a3320ca1de1d5439" + ref: master + resolved-ref: c4e9f7ab8e093e7e23be3a5f15d901781305ffa7 url: "https://github.com/linagora/flutter_matrix_html.git" source: git version: "1.2.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9cf91479fc..32970c7ffe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: flutter_matrix_html: git: url: https://github.com/linagora/flutter_matrix_html.git - ref: time-overlap + ref: master contacts_service: git: diff --git a/test/pages/chat/events/message/message_content_builder_mixin_test.dart b/test/pages/chat/events/message/message_content_builder_mixin_test.dart index 6f5eba3251..adc2fdcf03 100644 --- a/test/pages/chat/events/message/message_content_builder_mixin_test.dart +++ b/test/pages/chat/events/message/message_content_builder_mixin_test.dart @@ -97,7 +97,7 @@ void main() { group( '[MessageContentBuilderMixin] TEST\n', () { - // In unit testing, TextScaler(1.5) simulates medium web font size. + // In unit testing, TextScaler(1.5) simulates medium font size. const textScaler = TextScaler.linear(1.5); Future runTest( @@ -429,7 +429,7 @@ void main() { group( '[getSizeMessageBubbleWidth] TEST\n' 'GIVEN platform is Mobile\n' - 'THEN maxWidth for message is 360.0\n', + 'THEN maxWidth for message is 412.0\n', () { const messageMaxWidthMobile = 412.0; group('GIVEN message type is not supported for calculate\n', () { From 070148759bd84398d30167414b4d7ac09cb4b5fb Mon Sep 17 00:00:00 2001 From: --global Date: Fri, 19 Apr 2024 18:09:29 +0700 Subject: [PATCH 151/183] TW-1693: delele the encrypted file after downloading it (cherry picked from commit 1f532a8014a418a908ee6cec001ec4d8e4f19004) --- .../download_file_extension.dart | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index abde208ef6..0521192c01 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -193,6 +193,26 @@ extension DownloadFileExtension on Event { DownloadFileFailureState(exception: e), ), ); + } finally { + await _clearEncryptedFile( + eventId: eventId, + filename: filename, + ); + } + } + + Future _clearEncryptedFile({ + required String eventId, + required String filename, + }) async { + try { + final encryptedFilePath = await StorageDirectoryManager.instance + .getFilePathInAppDownloads(eventId: eventId, fileName: filename); + await File(encryptedFilePath).delete(); + } catch (e) { + Logs().e( + '_clearEncryptedFile(): $e', + ); } } From 8e6717d7b093f60f23d656a955e32b3179a907e2 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 22 Apr 2024 23:59:08 +0700 Subject: [PATCH 152/183] TW-1719: Fix can't tag name on mobile TW-1719: Fix can't tag name on mobile (cherry picked from commit 090e0fded07a54485d88e2b7e392bbe60e1b55c7) --- lib/pages/chat/input_bar/input_bar.dart | 36 ++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/pages/chat/input_bar/input_bar.dart b/lib/pages/chat/input_bar/input_bar.dart index 013eec9678..a46f3f5eaa 100644 --- a/lib/pages/chat/input_bar/input_bar.dart +++ b/lib/pages/chat/input_bar/input_bar.dart @@ -365,6 +365,23 @@ class InputBar extends StatelessWidget with PasteImageMixin { } } + void _handleSuggestionsCallbackWeb(List> suggestions) { + if (suggestions.isNotEmpty) { + suggestionsController?.open(); + } else { + suggestionsController?.close(); + if (PlatformInfos.isWeb || showEmojiPickerNotifier?.value == false) { + typeAheadFocusNode?.requestFocus(); + } + } + } + + void _handleSuggestionsCallbackMobile() { + if (showEmojiPickerNotifier?.value == false) { + typeAheadFocusNode?.requestFocus(); + } + } + @override Widget build(BuildContext context) { return InputBarShortcuts( @@ -400,6 +417,7 @@ class InputBar extends StatelessWidget with PasteImageMixin { decoration: decoration, focusNode: focusNode, onChanged: (text) { + suggestionsController?.open(); if (onChanged != null) { onChanged!(text); } @@ -426,14 +444,12 @@ class InputBar extends StatelessWidget with PasteImageMixin { suggestionsCallback: (text) { if (room!.isDirectChat) return []; final suggestions = getSuggestions(text); - if (suggestions.isNotEmpty) { - suggestionsController?.open(); - } else { - suggestionsController?.close(); - if (PlatformInfos.isWeb || - showEmojiPickerNotifier?.value == false) { - typeAheadFocusNode?.requestFocus(); - } + if (PlatformInfos.isMobile) { + _handleSuggestionsCallbackMobile(); + } + + if (PlatformInfos.isWeb) { + _handleSuggestionsCallbackWeb(suggestions); } focusSuggestionController.suggestions = suggestions; return suggestions; @@ -447,8 +463,8 @@ class InputBar extends StatelessWidget with PasteImageMixin { const SizedBox.shrink(), loadingBuilder: (BuildContext context) => const SizedBox.shrink(), // fix loading briefly flickering a dark box - emptyBuilder: (BuildContext context) => const SizedBox - .shrink(), // fix loading briefly showing no suggestions + emptyBuilder: (BuildContext context) => const SizedBox.shrink(), + // fix loading briefly showing no suggestions listBuilder: (context, widgets) => FocusSuggestionList( items: widgets, scrollController: suggestionScrollController, From 566d18efbf93a28315b3ddba33e7b3d791d78405 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA <31937920+Te-Z@users.noreply.github.com> Date: Thu, 25 Apr 2024 05:43:14 +0200 Subject: [PATCH 153/183] TW-1702: DownloadErrorPresentationState added handle errors (#1714) (cherry picked from commit cb0bb9695ffb2a17a27c5cf7b6818d75bf2c64d2) --- .../chat/events/message_download_content.dart | 14 ++++++++++-- .../events/message_download_content_web.dart | 16 +++++++++++--- .../downloading_state_presentation_model.dart | 9 ++++++++ .../download_file_extension.dart | 5 +++++ .../download_file_web_extension.dart | 5 +++++ .../download_file_tile_widget.dart | 22 ++++++++++++++----- .../file_widget/message_file_tile_style.dart | 8 +++++++ 7 files changed, 68 insertions(+), 11 deletions(-) diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index e3288f1523..ecc6a1a9fa 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/exception/downloading_exception.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; @@ -95,7 +96,14 @@ class _MessageDownloadContentState extends State event.fold( (failure) { Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); - downloadFileStateNotifier.value = const NotDownloadPresentationState(); + if (failure is DownloadFileFailureState && + failure.exception is CancelDownloadingException) { + downloadFileStateNotifier.value = + const NotDownloadPresentationState(); + } else { + downloadFileStateNotifier.value = + DownloadErrorPresentationState(error: failure); + } streamSubscription?.cancel(); }, (success) { @@ -159,7 +167,8 @@ class _MessageDownloadContentState extends State style: const MessageFileTileStyle(), ), ); - } else if (state is DownloadingPresentationState) { + } else if (state is DownloadingPresentationState || + state is DownloadErrorPresentationState) { return DownloadFileTileWidget( mimeType: widget.event.mimeType, fileType: filetype, @@ -173,6 +182,7 @@ class _MessageDownloadContentState extends State const NotDownloadPresentationState(); downloadManager.cancelDownload(widget.event.eventId); }, + hasError: state is DownloadErrorPresentationState, ); } diff --git a/lib/pages/chat/events/message_download_content_web.dart b/lib/pages/chat/events/message_download_content_web.dart index fcfd4e8101..6b1fecda7b 100644 --- a/lib/pages/chat/events/message_download_content_web.dart +++ b/lib/pages/chat/events/message_download_content_web.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/exception/downloading_exception.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -59,8 +60,15 @@ class _MessageDownloadContentWebState extends State void setupDownloadingProcess(Either event) { event.fold( (failure) { - Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); - downloadFileStateNotifier.value = const NotDownloadPresentationState(); + Logs().e('MessageDownloadContentWeb::onDownloadingProcess(): $failure'); + if (failure is DownloadFileFailureState && + failure.exception is CancelDownloadingException) { + downloadFileStateNotifier.value = + const NotDownloadPresentationState(); + } else { + downloadFileStateNotifier.value = + DownloadErrorPresentationState(error: failure); + } }, (success) { if (success is DownloadingFileState) { @@ -111,7 +119,8 @@ class _MessageDownloadContentWebState extends State return ValueListenableBuilder( valueListenable: downloadFileStateNotifier, builder: (context, DownloadPresentationState state, child) { - if (state is DownloadingPresentationState) { + if (state is DownloadingPresentationState || + state is DownloadErrorPresentationState) { return DownloadFileTileWidget( mimeType: widget.event.mimeType, fileType: filetype, @@ -125,6 +134,7 @@ class _MessageDownloadContentWebState extends State const NotDownloadPresentationState(); downloadManager.cancelDownload(widget.event.eventId); }, + hasError: state is DownloadErrorPresentationState, ); } else if (state is FileWebDownloadedPresentationState) { return InkWell( diff --git a/lib/presentation/model/chat/downloading_state_presentation_model.dart b/lib/presentation/model/chat/downloading_state_presentation_model.dart index 4086a76175..ca48a125a8 100644 --- a/lib/presentation/model/chat/downloading_state_presentation_model.dart +++ b/lib/presentation/model/chat/downloading_state_presentation_model.dart @@ -44,3 +44,12 @@ class DownloadingPresentationState extends DownloadPresentationState { @override List get props => [receive, total]; } + +class DownloadErrorPresentationState extends DownloadPresentationState { + final dynamic error; + + const DownloadErrorPresentationState({required this.error}); + + @override + List get props => [error]; +} diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 0521192c01..2ebde64094 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -109,6 +109,11 @@ extension DownloadFileExtension on Event { Logs().i("downloadOrRetrieveAttachment: duplicate request"); } else { Logs().e("downloadOrRetrieveAttachment: $e"); + downloadStreamController?.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); } } return null; diff --git a/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart index 88a31f72d3..242d6aafcf 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart @@ -115,6 +115,11 @@ extension DownloadFileWebExtension on Event { Logs().i("_handleDownloadFileWeb: user cancel the download"); } Logs().e("_handleDownloadFileWeb: $e"); + downloadStreamController.add( + Left( + DownloadFileFailureState(exception: e), + ), + ); } return null; } diff --git a/lib/widgets/file_widget/download_file_tile_widget.dart b/lib/widgets/file_widget/download_file_tile_widget.dart index c74469fb6f..8d24575b95 100644 --- a/lib/widgets/file_widget/download_file_tile_widget.dart +++ b/lib/widgets/file_widget/download_file_tile_widget.dart @@ -16,6 +16,7 @@ class DownloadFileTileWidget extends StatelessWidget { this.sizeString, required this.downloadFileStateNotifier, this.onCancelDownload, + this.hasError = false, }); final TwakeMimeType mimeType; @@ -26,6 +27,7 @@ class DownloadFileTileWidget extends StatelessWidget { final String? fileType; final ValueNotifier downloadFileStateNotifier; final VoidCallback? onCancelDownload; + final bool hasError; @override Widget build(BuildContext context) { @@ -63,11 +65,14 @@ class DownloadFileTileWidget extends StatelessWidget { width: style.iconSize, height: style.iconSize, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, + color: style.iconBackgroundColor( + hasError: hasError, + context: context, + ), shape: BoxShape.circle, ), ), - if (downloadProgress != 0) + if (downloadProgress != 0 && !hasError) SizedBox( width: style.circularProgressLoadingSize, height: style.circularProgressLoadingSize, @@ -81,13 +86,18 @@ class DownloadFileTileWidget extends StatelessWidget { child: Container( width: style.downloadIconSize, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, + color: style.iconBackgroundColor( + hasError: hasError, + context: context, + ), shape: BoxShape.circle, ), child: Icon( - downloadProgress == 0 - ? Icons.arrow_downward - : Icons.close, + hasError + ? Icons.error_outline + : downloadProgress == 0 + ? Icons.arrow_downward + : Icons.close, key: ValueKey(downloadProgress), color: Theme.of(context).colorScheme.surface, size: style.downloadIconSize, diff --git a/lib/widgets/file_widget/message_file_tile_style.dart b/lib/widgets/file_widget/message_file_tile_style.dart index 65f261af33..64b4c68149 100644 --- a/lib/widgets/file_widget/message_file_tile_style.dart +++ b/lib/widgets/file_widget/message_file_tile_style.dart @@ -47,4 +47,12 @@ class MessageFileTileStyle extends FileTileWidgetStyle { double get downloadIconSize => 28; EdgeInsets get marginDownloadIcon => const EdgeInsets.all(4); + + Color iconBackgroundColor({ + required bool hasError, + required BuildContext context, + }) => + hasError + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary; } From 677ecc36b6d6fca057c9efe36504028eecebf1cc Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 25 Apr 2024 01:19:04 +0700 Subject: [PATCH 154/183] TW-1727: Update login with `matrix.org` homeserver (cherry picked from commit e6517a3b4ebbc7450194d8df08353c9341d7a1ea) --- .../auto_homeserver_picker.dart | 2 +- lib/pages/connect/connect_page.dart | 81 +----- lib/pages/connect/connect_page_view.dart | 247 ++++-------------- .../connect/connect_page_view_style.dart | 7 + .../homeserver_picker/homeserver_picker.dart | 6 +- .../homeserver_picker_view.dart | 3 +- .../settings_dashboard/settings/settings.dart | 2 +- lib/pages/twake_welcome/twake_welcome.dart | 2 +- .../mixins}/connect_page_mixin.dart | 0 9 files changed, 70 insertions(+), 280 deletions(-) create mode 100644 lib/pages/connect/connect_page_view_style.dart rename lib/{pages/connect => presentation/mixins}/connect_page_mixin.dart (100%) diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart index 70001269b1..690f91314e 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart @@ -1,7 +1,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart'; import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart'; -import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/presentation/mixins/init_config_mixin.dart'; import 'package:fluffychat/utils/exception/check_homeserver_exception.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/pages/connect/connect_page.dart b/lib/pages/connect/connect_page.dart index eb0813a584..915371ee71 100644 --- a/lib/pages/connect/connect_page.dart +++ b/lib/pages/connect/connect_page.dart @@ -1,14 +1,7 @@ -import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:flutter/material.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/connect/connect_page_view.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ConnectPage extends StatefulWidget { @@ -20,78 +13,6 @@ class ConnectPage extends StatefulWidget { class ConnectPageController extends State with ConnectPageMixin { final TextEditingController usernameController = TextEditingController(); - String? signupError; - bool loading = false; - - void pickAvatar() async { - final source = !PlatformInfos.isMobile - ? ImageSource.gallery - : await showModalActionSheet( - context: context, - title: L10n.of(context)!.changeYourAvatar, - actions: [ - SheetAction( - key: ImageSource.camera, - label: L10n.of(context)!.openCamera, - isDefaultAction: true, - icon: Icons.camera_alt_outlined, - ), - SheetAction( - key: ImageSource.gallery, - label: L10n.of(context)!.openGallery, - icon: Icons.photo_outlined, - ), - ], - ); - if (source == null) return; - final picked = await ImagePicker().pickImage( - source: source, - imageQuality: 50, - maxWidth: 512, - maxHeight: 512, - ); - setState(() { - Matrix.of(context).loginAvatar = picked; - }); - } - - void signUp() async { - usernameController.text = usernameController.text.trim(); - final localpart = - usernameController.text.toLowerCase().replaceAll(' ', '_'); - if (localpart.isEmpty) { - setState(() { - signupError = L10n.of(context)!.pleaseChooseAUsername; - }); - return; - } - - setState(() { - signupError = null; - loading = true; - }); - - try { - try { - await Matrix.of(context).getLoginClient().register(username: localpart); - } on MatrixException catch (e) { - if (!e.requireAdditionalAuthentication) rethrow; - } - setState(() { - loading = false; - }); - Matrix.of(context).loginUsername = usernameController.text; - context.push('/signup'); - } catch (e, s) { - Logs().d('Sign up failed', e, s); - setState(() { - signupError = e.toLocalizedString(context); - loading = false; - }); - } - } - - void login() => context.push('/login'); Map? rawLoginTypes; diff --git a/lib/pages/connect/connect_page_view.dart b/lib/pages/connect/connect_page_view.dart index d020202269..6d1c8f8322 100644 --- a/lib/pages/connect/connect_page_view.dart +++ b/lib/pages/connect/connect_page_view.dart @@ -1,7 +1,8 @@ -import 'dart:typed_data'; +import 'package:fluffychat/pages/connect/connect_page_view_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/connect/connect_page.dart'; @@ -11,219 +12,75 @@ import 'sso_button.dart'; class ConnectPageView extends StatelessWidget { final ConnectPageController controller; + const ConnectPageView(this.controller, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final avatar = Matrix.of(context).loginAvatar; final identityProviders = controller.identityProviders(rawLoginTypes: controller.rawLoginTypes); return LoginScaffold( appBar: AppBar( - leading: controller.loading ? null : const BackButton(), - automaticallyImplyLeading: !controller.loading, + leading: const BackButton(), centerTitle: true, title: Text( Matrix.of(context).getLoginClient().homeserver?.host ?? '', ), ), - body: ListView( - key: const Key('ConnectPageListView'), - children: [ - if (Matrix.of(context).loginRegistrationSupported ?? false) ...[ - Padding( - padding: const EdgeInsets.all(12.0), - child: Center( - child: Stack( - children: [ - Material( - borderRadius: BorderRadius.circular(64), - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 10, - color: Colors.transparent, - shadowColor: Theme.of(context) - .colorScheme - .onBackground - .withAlpha(64), - clipBehavior: Clip.hardEdge, - child: CircleAvatar( - radius: 64, - backgroundColor: Colors.white, - child: avatar == null - ? const Icon( - Icons.person, - color: Colors.black, - size: 64, - ) - : FutureBuilder( - future: avatar.readAsBytes(), - builder: (context, snapshot) { - final bytes = snapshot.data; - if (bytes == null) { - return const CircularProgressIndicator - .adaptive(); - } - return Image.memory( - bytes, - fit: BoxFit.cover, - width: 128, - height: 128, - ); - }, - ), + body: Center( + child: identityProviders == null + ? CircularProgressIndicator.adaptive( + backgroundColor: + LinagoraSysColors.material().onTertiaryContainer, + ) + : identityProviders.length == 1 + ? Container( + width: double.infinity, + padding: ConnectPageViewStyle.padding, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onPrimaryContainer, ), - ), - Positioned( - bottom: 0, - right: 0, - child: FloatingActionButton( - mini: true, - onPressed: controller.pickAvatar, - backgroundColor: Colors.white, - foregroundColor: Colors.black, - child: const Icon(Icons.camera_alt_outlined), + icon: identityProviders.single.icon == null + ? const Icon( + Icons.web_outlined, + size: 16, + ) + : Image.network( + Uri.parse(identityProviders.single.icon!) + .getDownloadLink( + Matrix.of(context).getLoginClient(), + ) + .toString(), + width: ConnectPageViewStyle.iconSize, + height: ConnectPageViewStyle.iconSize, + ), + onPressed: () => controller.ssoLoginAction( + context: context, + id: identityProviders.single.id!, + ), + label: Text( + identityProviders.single.name ?? + identityProviders.single.brand ?? + L10n.of(context)!.loginWithOneClick, ), ), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - controller: controller.usernameController, - onSubmitted: (_) => controller.signUp(), - decoration: InputDecoration( - prefixIcon: const Icon(Icons.account_box_outlined), - hintText: L10n.of(context)!.chooseAUsername, - errorText: controller.signupError, - errorStyle: const TextStyle(color: Colors.orange), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: Hero( - tag: 'loginButton', - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Theme.of(context).colorScheme.onPrimary, - ), - onPressed: controller.loading ? () {} : controller.signUp, - icon: const Icon(Icons.person_add_outlined), - label: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context)!.signUp), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - Expanded( - child: Divider( - thickness: 1, - color: Theme.of(context).dividerColor, - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - L10n.of(context)!.or, - style: const TextStyle(fontSize: 18), - ), - ), - Expanded( - child: Divider( - thickness: 1, - color: Theme.of(context).dividerColor, - ), - ), - ], - ), - ), - ], - if (controller.supportsSso(context)) - identityProviders == null - ? const SizedBox( - height: 74, - child: Center(child: CircularProgressIndicator.adaptive()), ) - : Center( - child: identityProviders.length == 1 - ? Container( - width: double.infinity, - padding: const EdgeInsets.all(12.0), - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context) - .colorScheme - .primaryContainer, - foregroundColor: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - icon: identityProviders.single.icon == null - ? const Icon( - Icons.web_outlined, - size: 16, - ) - : Image.network( - Uri.parse(identityProviders.single.icon!) - .getDownloadLink( - Matrix.of(context).getLoginClient(), - ) - .toString(), - width: 32, - height: 32, - ), - onPressed: () => controller.ssoLoginAction( - context: context, - id: identityProviders.single.id!, - ), - label: Text( - identityProviders.single.name ?? - identityProviders.single.brand ?? - L10n.of(context)!.loginWithOneClick, - ), - ), - ) - : Wrap( - children: [ - for (final identityProvider in identityProviders) - SsoButton( - onPressed: () => controller.ssoLoginAction( - context: context, - id: identityProvider.id!, - ), - identityProvider: identityProvider, - ), - ].toList(), + : Wrap( + children: [ + for (final identityProvider in identityProviders) + SsoButton( + onPressed: () => controller.ssoLoginAction( + context: context, + id: identityProvider.id!, ), + identityProvider: identityProvider, + ), + ].toList(), ), - if (controller.supportsLogin(context)) - Padding( - padding: const EdgeInsets.all(12.0), - child: Hero( - tag: 'signinButton', - child: ElevatedButton.icon( - icon: const Icon(Icons.login_outlined), - style: ElevatedButton.styleFrom( - backgroundColor: - Theme.of(context).colorScheme.primaryContainer, - foregroundColor: - Theme.of(context).colorScheme.onPrimaryContainer, - ), - onPressed: controller.loading ? () {} : controller.login, - label: Text(L10n.of(context)!.login), - ), - ), - ), - ], ), ); } diff --git a/lib/pages/connect/connect_page_view_style.dart b/lib/pages/connect/connect_page_view_style.dart new file mode 100644 index 0000000000..98661c6b7e --- /dev/null +++ b/lib/pages/connect/connect_page_view_style.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class ConnectPageViewStyle { + static const EdgeInsets padding = EdgeInsets.all(12.0); + + static const double iconSize = 32; +} diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 801f4879d1..0da2919616 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_state.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; @@ -195,6 +195,8 @@ class HomeserverPickerController extends State } else { state = HomeserverState.otherLoginMethod; context.push('/connect'); + FocusManager.instance.primaryFocus?.unfocus(); + setState(() {}); } } catch (e) { state = HomeserverState.wrongServerName; @@ -227,6 +229,8 @@ class HomeserverPickerController extends State @override void dispose() { homeserverFocusNode.removeListener(_updateFocus); + homeserverFocusNode.dispose(); + homeserverController.dispose(); super.dispose(); } diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 8e8ee27398..f1975b0861 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -147,7 +147,8 @@ class HomeserverTextField extends StatelessWidget { builder: (context, value, focusNode) { return TextField( onEditingComplete: () => controller.loginButtonPressed(), - autofocus: controller.state != HomeserverState.ssoLoginServer, + autofocus: controller.state != HomeserverState.ssoLoginServer || + controller.state != HomeserverState.otherLoginMethod, focusNode: controller.homeserverFocusNode, autocorrect: false, enabled: true, diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 8a9fa0b53b..8658b3253b 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -5,7 +5,7 @@ import 'package:fluffychat/data/hive/hive_collection_tom_database.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/event/twake_inapp_event_types.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; -import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; diff --git a/lib/pages/twake_welcome/twake_welcome.dart b/lib/pages/twake_welcome/twake_welcome.dart index dfc9774091..a091123c57 100644 --- a/lib/pages/twake_welcome/twake_welcome.dart +++ b/lib/pages/twake_welcome/twake_welcome.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:equatable/equatable.dart'; -import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/connect/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart similarity index 100% rename from lib/pages/connect/connect_page_mixin.dart rename to lib/presentation/mixins/connect_page_mixin.dart From ad94aaf46eeedf3eeb0451d790f9b6e30cf9284e Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Tue, 16 Apr 2024 12:06:37 +0200 Subject: [PATCH 155/183] TW-1584: web done (cherry picked from commit be483dbd204290650487642823d48ce0491527cf) --- lib/pages/chat_details/chat_details.dart | 25 +++++ .../files/chat_details_file_row.dart | 98 +++++++++++++++++ .../files/chat_details_file_row_style.dart | 12 ++ .../files/chat_details_files_item.dart | 103 ++++++++++++++++++ .../files/chat_details_files_item_view.dart | 77 +++++++++++++ .../files/chat_details_files_page.dart | 40 +++++++ .../files/chat_details_files_style.dart | 10 ++ .../files/chat_details_files_tile_style.dart | 10 ++ .../chat_profile_info_shared.dart | 28 +++++ .../event_extension.dart | 2 + 10 files changed, 405 insertions(+) create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index bea5654231..9d3a2c4a69 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/domain/model/room/room_extension.dart'; import 'package:fluffychat/pages/chat_details/chat_details_edit.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_members_page.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_page_enum.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart'; import 'package:fluffychat/pages/chat_details/chat_details_view_style.dart'; @@ -59,6 +60,8 @@ class ChatDetailsController extends State static const _linksFetchLimit = 20; + static const _filesFetchLimit = 20; + final invitationSelectionMobileAndTabletKey = const Key('InvitationSelectionMobileAndTabletKey'); @@ -75,6 +78,7 @@ class ChatDetailsController extends State ChatDetailsPage.members, ChatDetailsPage.media, ChatDetailsPage.links, + ChatDetailsPage.files, ]; final responsive = getIt.get(); @@ -87,6 +91,7 @@ class ChatDetailsController extends State SameTypeEventsBuilderController? mediaListController; SameTypeEventsBuilderController? linksListController; + SameTypeEventsBuilderController? filesListController; Room? room; @@ -125,6 +130,11 @@ class ChatDetailsController extends State searchFunc: (event) => event.isContainsLink, limit: _linksFetchLimit, ); + filesListController = SameTypeEventsBuilderController( + getTimeline: getTimeline, + searchFunc: (event) => event.isAFile, + limit: _filesFetchLimit, + ); WidgetsBinding.instance.addPostFrameCallback((_) { nestedScrollViewState.currentState?.innerController.addListener( _listenerInnerController, @@ -143,6 +153,7 @@ class ChatDetailsController extends State muteNotifier.dispose(); mediaListController?.dispose(); linksListController?.dispose(); + filesListController?.dispose(); nestedScrollViewState.currentState?.innerController.dispose(); super.dispose(); } @@ -159,6 +170,9 @@ class ChatDetailsController extends State case ChatDetailsPage.links: linksListController?.loadMore(); break; + case ChatDetailsPage.files: + filesListController?.loadMore(); + break; default: break; } @@ -168,6 +182,7 @@ class ChatDetailsController extends State void _refreshDataInTabviewInit() { linksListController?.refresh(); mediaListController?.refresh(); + filesListController?.refresh(); } void requestMoreMembersAction() async { @@ -267,6 +282,16 @@ class ChatDetailsController extends State controller: linksListController!, ), ); + case ChatDetailsPage.files: + return ChatDetailsPageModel( + page: page, + child: filesListController == null + ? const SizedBox() + : ChatDetailsFilesPage( + key: const PageStorageKey('Files'), + controller: filesListController!, + ), + ); default: return ChatDetailsPageModel( page: page, diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart new file mode 100644 index 0000000000..5f90fd1bc5 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart @@ -0,0 +1,98 @@ +import 'dart:typed_data'; + +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart'; +import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ChatDetailsFileTileRow extends StatelessWidget { + const ChatDetailsFileTileRow({ + super.key, + required this.mimeType, + required this.filename, + required this.sentDate, + this.style = const FileTileWidgetStyle(), + this.imageBytes, + this.fileTileIcon, + this.fileType, + this.highlightText, + this.sizeString, + }); + + final FileTileWidgetStyle style; + final Uint8List? imageBytes; + final String? fileTileIcon; + final TwakeMimeType? mimeType; + final String? fileType; + final String filename; + final String? highlightText; + final String? sizeString; + final DateTime sentDate; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: style.crossAxisAlignment, + children: [ + if (imageBytes != null) + Padding( + padding: style.imagePadding, + child: ClipRRect( + borderRadius: style.borderRadius, + child: Image.memory( + imageBytes!, + width: style.imageSize, + height: style.imageSize, + fit: BoxFit.cover, + ), + ), + ) + else + SvgPicture.asset( + fileTileIcon ?? mimeType.getIcon(fileType: fileType), + width: style.iconSize, + height: style.iconSize, + ), + style.paddingRightIcon, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: ChatDetailsFileRow.textTopMargin), + FileNameText( + filename: filename, + highlightText: highlightText, + style: style, + ), + Row( + children: [ + if (sizeString != null) ...[ + Text( + sizeString!, + style: ChatDetailsFileRow.textInformationStyle(context), + ), + Text( + " - ", + style: ChatDetailsFileRow.textInformationStyle(context), + ), + ], + Flexible( + child: Text( + sentDate.localizedTime(context), + style: ChatDetailsFileRow.textInformationStyle(context), + ), + ), + ], + ), + style.paddingBottomText, + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart new file mode 100644 index 0000000000..601a7609d4 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ChatDetailsFileRow { + static TextStyle textInformationStyle(BuildContext context) { + return Theme.of(context).textTheme.bodySmall!.copyWith( + color: LinagoraSysColors.material().tertiary, + ); + } + + static const double textTopMargin = 4.0; +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart new file mode 100644 index 0000000000..afae9e21d6 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class ChatDetailsFileItem extends StatefulWidget { + const ChatDetailsFileItem({super.key, required this.event}); + + final Event event; + + @override + State createState() => ChatDetailsFileItemState(); +} + +class ChatDetailsFileItemState extends State + with HandleDownloadAndPreviewFileMixin { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + StreamSubscription>? streamSubscription; + + Event get event => widget.event; + + @override + void initState() { + super.initState(); + trySetupDownloadingStreamSubcription(); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } + + void trySetupDownloadingStreamSubcription() { + streamSubscription = downloadManager + .getDownloadStateStream(widget.event.eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess(Either event) { + event.fold( + (failure) { + Logs().e('ChatDetailsFileItem::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadMatrixFileSuccessState) { + _handleDownloadMatrixFileSuccessState(success); + } + }, + ); + } + + void _handleDownloadMatrixFileSuccessState( + DownloadMatrixFileSuccessState success, + ) { + streamSubscription?.cancel(); + if (mounted) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + handlePreviewWeb(event: widget.event, context: context); + return; + } + + if (TwakeApp.routerKey.currentContext != null) { + handlePreviewWeb( + event: widget.event, + context: TwakeApp.routerKey.currentContext!, + ); + } + } + + @override + void dispose() { + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChatDetailsFilesView(controller: this); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart new file mode 100644 index 0000000000..30ac72e20c --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart @@ -0,0 +1,77 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class ChatDetailsFilesView extends StatelessWidget { + const ChatDetailsFilesView({ + super.key, + required this.controller, + }); + + final ChatDetailsFileItemState controller; + + @override + Widget build(BuildContext context) { + final filename = controller.event.filename; + final filetype = controller.event.fileType; + final sizeString = controller.event.sizeString; + + return ValueListenableBuilder( + valueListenable: controller.downloadFileStateNotifier, + builder: (context, DownloadPresentationState state, child) { + if (state is DownloadingPresentationState) { + return DownloadFileTileWidget( + mimeType: controller.event.mimeType, + fileType: filetype, + filename: filename, + sizeString: sizeString, + style: const MessageFileTileStyle(), + downloadFileStateNotifier: controller.downloadFileStateNotifier, + onCancelDownload: () { + controller.downloadFileStateNotifier.value = + const NotDownloadPresentationState(); + controller.downloadManager + .cancelDownload(controller.event.eventId); + }, + ); + } + + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: () { + if (state is FileWebDownloadedPresentationState) { + controller.handlePreviewWeb( + event: controller.event, + context: context, + ); + } else { + controller.downloadFileStateNotifier.value = + const DownloadingPresentationState(); + controller.downloadManager.download( + event: controller.event, + ); + controller.trySetupDownloadingStreamSubcription(); + } + }, + child: Padding( + padding: ChatDetailsFileTileStyle().paddingFileTileAll, + child: ChatDetailsFileTileRow( + mimeType: controller.event.mimeType, + fileType: filetype, + filename: filename, + sizeString: sizeString, + style: ChatDetailsFileTileStyle(), + sentDate: controller.event.originServerTs, + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart new file mode 100644 index 0000000000..0f3c13da32 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart @@ -0,0 +1,40 @@ +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart'; +import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_builder.dart'; +import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_controller.dart'; +import 'package:flutter/material.dart'; + +class ChatDetailsFilesPage extends StatelessWidget { + final SameTypeEventsBuilderController controller; + + const ChatDetailsFilesPage({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return SameTypeEventsBuilder( + controller: controller, + builder: (context, eventsState, _) { + final events = eventsState + .getSuccessOrNull() + ?.events ?? + []; + return SliverList.separated( + itemCount: events.length, + itemBuilder: (context, index) { + return ChatDetailsFileItem(event: events[index]); + }, + separatorBuilder: (context, index) => Container( + height: ChatDetailsFilesStyle.dividerHeight, + margin: ChatDetailsFilesStyle.dividerMargin, + color: ChatDetailsFilesStyle.dividerColor(context), + ), + ); + }, + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart new file mode 100644 index 0000000000..0646c96fac --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class ChatDetailsFilesStyle { + static const EdgeInsets dividerMargin = EdgeInsets.only(left: 64); + + static const double dividerHeight = 1; + + static Color dividerColor(BuildContext context) => + Theme.of(context).dividerColor; +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart new file mode 100644 index 0000000000..f124a627f6 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart @@ -0,0 +1,10 @@ +import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; +import 'package:flutter/material.dart'; + +class ChatDetailsFileTileStyle extends FileTileWidgetStyle { + @override + Color get backgroundColor => Colors.transparent; + + @override + BorderRadiusGeometry get borderRadius => BorderRadius.circular(8); +} diff --git a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart index 6cc4fa04cc..d609c635ea 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_page_enum.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart'; @@ -36,10 +37,14 @@ class ChatProfileInfoSharedController extends State static const _linksFetchLimit = 20; + static const _filesFetchLimit = 20; + SameTypeEventsBuilderController? mediaListController; SameTypeEventsBuilderController? linksListController; + SameTypeEventsBuilderController? filesListController; + TabController? tabController; Timeline? _timeline; @@ -49,6 +54,7 @@ class ChatProfileInfoSharedController extends State final List profileSharedPageView = [ ChatDetailsPage.media, ChatDetailsPage.links, + ChatDetailsPage.files, ]; Future getTimeline() async { @@ -87,6 +93,18 @@ class ChatProfileInfoSharedController extends State controller: linksListController!, ), ); + case ChatDetailsPage.files: + return ChatDetailsPageModel( + page: page, + child: filesListController == null + ? const SizedBox() + : ChatDetailsFilesPage( + key: const PageStorageKey( + 'ChatProfileInfoSharedFiles', + ), + controller: filesListController!, + ), + ); default: return ChatDetailsPageModel( page: page, @@ -119,6 +137,9 @@ class ChatProfileInfoSharedController extends State case ChatDetailsPage.links: linksListController?.loadMore(); break; + case ChatDetailsPage.files: + filesListController?.loadMore(); + break; default: break; } @@ -128,6 +149,7 @@ class ChatProfileInfoSharedController extends State void _refreshDataInTabviewInit() { linksListController?.refresh(); mediaListController?.refresh(); + filesListController?.refresh(); } @override @@ -146,6 +168,11 @@ class ChatProfileInfoSharedController extends State searchFunc: (event) => event.isContainsLink, limit: _linksFetchLimit, ); + filesListController = SameTypeEventsBuilderController( + getTimeline: getTimeline, + searchFunc: (event) => event.isAFile, + limit: _filesFetchLimit, + ); WidgetsBinding.instance.addPostFrameCallback((_) { nestedScrollViewState.currentState?.innerController.addListener( _listenerInnerController, @@ -160,6 +187,7 @@ class ChatProfileInfoSharedController extends State tabController?.dispose(); mediaListController?.dispose(); linksListController?.dispose(); + filesListController?.dispose(); super.dispose(); } diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 066286d3ff..12c790d3ce 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -75,6 +75,8 @@ extension LocalizedBody on Event { bool get isContainsLink => firstValidUrl != null; + bool get isAFile => messageType == MessageTypes.File; + void shareFile(BuildContext context) async { final matrixFile = await getFile(context); From 54e090ceb28b3b56d502217f7b1cfc43d9f51324 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Tue, 16 Apr 2024 13:15:41 +0200 Subject: [PATCH 156/183] TW-1584: download on mobile ok (cherry picked from commit b73c37a928220abec48bddf23c1a0efe0f092ae2) --- .../chat_details_files_item.dart | 37 ++++++ .../chat_details_files_item_style.dart} | 0 .../chat_details_files_item_view.dart | 21 ++-- .../chat_details_files_item_view_web.dart | 77 ++++++++++++ .../chat_details_files_item_web.dart} | 12 +- .../files/chat_details_files_page.dart | 9 +- .../chat_details_file_row.dart | 13 +- .../chat_details_file_row_style.dart | 2 +- .../mixins/download_file_on_mobile_mixin.dart | 114 ++++++++++++++++++ 9 files changed, 258 insertions(+), 27 deletions(-) create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart rename lib/pages/chat_details/chat_details_page_view/files/{chat_details_files_tile_style.dart => chat_details_files_item/chat_details_files_item_style.dart} (100%) rename lib/pages/chat_details/chat_details_page_view/files/{ => chat_details_files_item}/chat_details_files_item_view.dart (80%) create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart rename lib/pages/chat_details/chat_details_page_view/files/{chat_details_files_item.dart => chat_details_files_item_web/chat_details_files_item_web.dart} (87%) rename lib/pages/chat_details/chat_details_page_view/files/{ => chat_details_files_row}/chat_details_file_row.dart (85%) rename lib/pages/chat_details/chat_details_page_view/files/{ => chat_details_files_row}/chat_details_file_row_style.dart (91%) create mode 100644 lib/widgets/mixins/download_file_on_mobile_mixin.dart diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart new file mode 100644 index 0000000000..bfde0ae76e --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart @@ -0,0 +1,37 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart'; +import 'package:fluffychat/widgets/mixins/download_file_on_mobile_mixin.dart'; +import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class ChatDetailsFileItem extends StatefulWidget { + const ChatDetailsFileItem({super.key, required this.event}); + + final Event event; + + @override + State createState() => ChatDetailsFileItemState(); +} + +class ChatDetailsFileItemState extends State + with HandleDownloadAndPreviewFileMixin, DownloadFileOnMobileMixin { + Event get event => widget.event; + + @override + void initState() { + super.initState(); + checkDownloadFileState(event: event); + } + + @override + void dispose() { + streamSubscription?.cancel(); + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChatDetailsFilesView(controller: this); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart similarity index 100% rename from lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart rename to lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart similarity index 80% rename from lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart rename to lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart index 30ac72e20c..b14c54b661 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart @@ -1,6 +1,6 @@ -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_tile_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; @@ -45,18 +45,13 @@ class ChatDetailsFilesView extends StatelessWidget { return InkWell( hoverColor: LinagoraSysColors.material().surfaceVariant, onTap: () { - if (state is FileWebDownloadedPresentationState) { - controller.handlePreviewWeb( - event: controller.event, - context: context, + if (state is DownloadedPresentationState) { + controller.handleDownloadFileForPreviewSuccess( + filePath: state.filePath, + mimeType: controller.event.mimeType, ); } else { - controller.downloadFileStateNotifier.value = - const DownloadingPresentationState(); - controller.downloadManager.download( - event: controller.event, - ); - controller.trySetupDownloadingStreamSubcription(); + controller.onDownloadFileTap(event: controller.event); } }, child: Padding( diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart new file mode 100644 index 0000000000..fb6f72185b --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart @@ -0,0 +1,77 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class ChatDetailsFilesViewWeb extends StatelessWidget { + const ChatDetailsFilesViewWeb({ + super.key, + required this.controller, + }); + + final ChatDetailsFileItemWebState controller; + + @override + Widget build(BuildContext context) { + final filename = controller.event.filename; + final filetype = controller.event.fileType; + final sizeString = controller.event.sizeString; + + return ValueListenableBuilder( + valueListenable: controller.downloadFileStateNotifier, + builder: (context, DownloadPresentationState state, child) { + if (state is DownloadingPresentationState) { + return DownloadFileTileWidget( + mimeType: controller.event.mimeType, + fileType: filetype, + filename: filename, + sizeString: sizeString, + style: const MessageFileTileStyle(), + downloadFileStateNotifier: controller.downloadFileStateNotifier, + onCancelDownload: () { + controller.downloadFileStateNotifier.value = + const NotDownloadPresentationState(); + controller.downloadManager + .cancelDownload(controller.event.eventId); + }, + ); + } + + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: () { + if (state is FileWebDownloadedPresentationState) { + controller.handlePreviewWeb( + event: controller.event, + context: context, + ); + } else { + controller.downloadFileStateNotifier.value = + const DownloadingPresentationState(); + controller.downloadManager.download( + event: controller.event, + ); + controller.trySetupDownloadingStreamSubcription(); + } + }, + child: Padding( + padding: ChatDetailsFileTileStyle().paddingFileTileAll, + child: ChatDetailsFileTileRow( + mimeType: controller.event.mimeType, + fileType: filetype, + filename: filename, + sizeString: sizeString, + style: ChatDetailsFileTileStyle(), + sentDate: controller.event.originServerTs, + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart similarity index 87% rename from lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart rename to lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart index afae9e21d6..f76e86f5ab 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart @@ -4,7 +4,7 @@ import 'package:dartz/dartz.dart' hide State, OpenFile; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_view.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; @@ -13,16 +13,16 @@ import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -class ChatDetailsFileItem extends StatefulWidget { - const ChatDetailsFileItem({super.key, required this.event}); +class ChatDetailsFileItemWeb extends StatefulWidget { + const ChatDetailsFileItemWeb({super.key, required this.event}); final Event event; @override - State createState() => ChatDetailsFileItemState(); + State createState() => ChatDetailsFileItemWebState(); } -class ChatDetailsFileItemState extends State +class ChatDetailsFileItemWebState extends State with HandleDownloadAndPreviewFileMixin { final downloadManager = getIt.get(); @@ -98,6 +98,6 @@ class ChatDetailsFileItemState extends State @override Widget build(BuildContext context) { - return ChatDetailsFilesView(controller: this); + return ChatDetailsFilesViewWeb(controller: this); } } diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart index 0f3c13da32..6f319e9d8f 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart @@ -1,9 +1,11 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart'; import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_builder.dart'; import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_controller.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; class ChatDetailsFilesPage extends StatelessWidget { @@ -26,7 +28,10 @@ class ChatDetailsFilesPage extends StatelessWidget { return SliverList.separated( itemCount: events.length, itemBuilder: (context, index) { - return ChatDetailsFileItem(event: events[index]); + if (!PlatformInfos.isWeb) { + return ChatDetailsFileItem(event: events[index]); + } + return ChatDetailsFileItemWeb(event: events[index]); }, separatorBuilder: (context, index) => Container( height: ChatDetailsFilesStyle.dividerHeight, diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart similarity index 85% rename from lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart rename to lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart index 5f90fd1bc5..173ddac4b3 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart @@ -1,6 +1,6 @@ import 'dart:typed_data'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; @@ -62,7 +62,7 @@ class ChatDetailsFileTileRow extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - const SizedBox(height: ChatDetailsFileRow.textTopMargin), + const SizedBox(height: ChatDetailsFileRowStyle.textTopMargin), FileNameText( filename: filename, highlightText: highlightText, @@ -73,17 +73,20 @@ class ChatDetailsFileTileRow extends StatelessWidget { if (sizeString != null) ...[ Text( sizeString!, - style: ChatDetailsFileRow.textInformationStyle(context), + style: + ChatDetailsFileRowStyle.textInformationStyle(context), ), Text( " - ", - style: ChatDetailsFileRow.textInformationStyle(context), + style: + ChatDetailsFileRowStyle.textInformationStyle(context), ), ], Flexible( child: Text( sentDate.localizedTime(context), - style: ChatDetailsFileRow.textInformationStyle(context), + style: + ChatDetailsFileRowStyle.textInformationStyle(context), ), ), ], diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart similarity index 91% rename from lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart rename to lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart index 601a7609d4..5f421fb522 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_file_row_style.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; -class ChatDetailsFileRow { +class ChatDetailsFileRowStyle { static TextStyle textInformationStyle(BuildContext context) { return Theme.of(context).textTheme.bodySmall!.copyWith( color: LinagoraSysColors.material().tertiary, diff --git a/lib/widgets/mixins/download_file_on_mobile_mixin.dart b/lib/widgets/mixins/download_file_on_mobile_mixin.dart new file mode 100644 index 0000000000..5be940adb1 --- /dev/null +++ b/lib/widgets/mixins/download_file_on_mobile_mixin.dart @@ -0,0 +1,114 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +mixin DownloadFileOnMobileMixin { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + StreamSubscription>? streamSubscription; + + void checkDownloadFileState({ + required Event event, + }) async { + checkFileExistInMemory(event: event); + await checkFileInDownloadsInApp( + event: event, + ); + + _trySetupDownloadingStreamSubcription(event.eventId); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } + + void checkFileExistInMemory({ + required Event event, + }) { + final filePathInMem = event.getFilePathFromMem(); + if (filePathInMem?.isNotEmpty == true) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePathInMem!, + ); + return; + } + } + + Future checkFileInDownloadsInApp({ + required Event event, + }) async { + final filePath = + await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: event.eventId, + fileName: event.filename, + ); + final file = File(filePath); + if (await file.exists() && await file.length() == event.getFileSize()) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePath, + ); + return; + } + } + + void _trySetupDownloadingStreamSubcription(String eventId) { + streamSubscription = downloadManager + .getDownloadStateStream(eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess(Either event) { + event.fold( + (failure) { + Logs().e('setupDownloadingProcess::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + streamSubscription?.cancel(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadNativeFileSuccessState) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: success.filePath, + ); + } + }, + ); + } + + void onDownloadFileTap({ + required Event event, + }) async { + await checkFileInDownloadsInApp( + event: event, + ); + if (downloadFileStateNotifier.value is DownloadedPresentationState) { + return; + } + downloadFileStateNotifier.value = const DownloadingPresentationState(); + downloadManager.download( + event: event, + ); + _trySetupDownloadingStreamSubcription(event.eventId); + } +} From 10657fdf156baa63e4de2470814e10ee422d822d Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Tue, 16 Apr 2024 13:19:57 +0200 Subject: [PATCH 157/183] TW-1584: message_download_content logic moved to mixin (cherry picked from commit e8f98c6679e5e3f5d586f0b1f26a58408b40733e) --- .../chat/events/message_download_content.dart | 127 +------------- .../chat_details_files_item.dart | 18 +- .../chat_details_files_item_view.dart | 4 +- .../chat_details_file_row.dart | 121 ++++--------- .../file_widget/base_file_tile_widget.dart | 116 ++++++++++++ .../download_file_tile_widget.dart | 7 +- .../downloading_file_tile_widget.dart | 145 +++++++++++++++ lib/widgets/file_widget/file_tile_widget.dart | 165 ++++-------------- .../mixins/download_file_on_mobile_mixin.dart | 65 +++---- 9 files changed, 379 insertions(+), 389 deletions(-) create mode 100644 lib/widgets/file_widget/base_file_tile_widget.dart create mode 100644 lib/widgets/file_widget/downloading_file_tile_widget.dart diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index ecc6a1a9fa..817a0bb072 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,20 +1,10 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:dartz/dartz.dart' hide State, OpenFile; -import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; -import 'package:fluffychat/utils/exception/downloading_exception.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/downloading_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:fluffychat/widgets/mixins/download_file_on_mobile_mixin.dart'; import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -35,112 +25,11 @@ class MessageDownloadContent extends StatefulWidget { } class _MessageDownloadContentState extends State - with HandleDownloadAndPreviewFileMixin { - final downloadManager = getIt.get(); - - final downloadFileStateNotifier = ValueNotifier( - const NotDownloadPresentationState(), - ); - - StreamSubscription>? streamSubscription; - + with + HandleDownloadAndPreviewFileMixin, + DownloadFileOnMobileMixin { @override - void initState() { - super.initState(); - checkDownloadFileState(); - } - - void checkDownloadFileState() async { - checkFileExistInMemory(); - await checkFileInDownloadsInApp(); - - _trySetupDownloadingStreamSubcription(); - if (streamSubscription != null) { - downloadFileStateNotifier.value = const DownloadingPresentationState(); - } - } - - void checkFileExistInMemory() { - final filePathInMem = widget.event.getFilePathFromMem(); - if (filePathInMem?.isNotEmpty == true) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: filePathInMem!, - ); - return; - } - } - - Future checkFileInDownloadsInApp() async { - final filePath = - await StorageDirectoryManager.instance.getFilePathInAppDownloads( - eventId: widget.event.eventId, - fileName: widget.event.filename, - ); - final file = File(filePath); - if (await file.exists() && - await file.length() == widget.event.getFileSize()) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: filePath, - ); - return; - } - } - - void _trySetupDownloadingStreamSubcription() { - streamSubscription = downloadManager - .getDownloadStateStream(widget.event.eventId) - ?.listen(setupDownloadingProcess); - } - - void setupDownloadingProcess(Either event) { - event.fold( - (failure) { - Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); - if (failure is DownloadFileFailureState && - failure.exception is CancelDownloadingException) { - downloadFileStateNotifier.value = - const NotDownloadPresentationState(); - } else { - downloadFileStateNotifier.value = - DownloadErrorPresentationState(error: failure); - } - streamSubscription?.cancel(); - }, - (success) { - if (success is DownloadingFileState) { - if (success.total != 0) { - downloadFileStateNotifier.value = DownloadingPresentationState( - receive: success.receive, - total: success.total, - ); - } - } else if (success is DownloadNativeFileSuccessState) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: success.filePath, - ); - } - }, - ); - } - - void onDownloadFileTap() async { - await checkFileInDownloadsInApp(); - if (downloadFileStateNotifier.value is DownloadedPresentationState) { - return; - } - downloadFileStateNotifier.value = const DownloadingPresentationState(); - downloadManager.download( - event: widget.event, - ); - _trySetupDownloadingStreamSubcription(); - } - - @override - void dispose() { - streamSubscription?.cancel(); - downloadFileStateNotifier.dispose(); - super.dispose(); - } + Event get event => widget.event; @override Widget build(BuildContext context) { @@ -187,8 +76,8 @@ class _MessageDownloadContentState extends State } return InkWell( - onTap: onDownloadFileTap, - child: DownloadFileTileWidget( + onTap: () => onDownloadFileTap(), + child: DownloadingFileTileWidget( mimeType: widget.event.mimeType, fileType: filetype, filename: filename, diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart index bfde0ae76e..eb2567be00 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart @@ -10,25 +10,13 @@ class ChatDetailsFileItem extends StatefulWidget { final Event event; @override - State createState() => ChatDetailsFileItemState(); + State createState() => ChatDetailsFileItemController(); } -class ChatDetailsFileItemState extends State +class ChatDetailsFileItemController extends State with HandleDownloadAndPreviewFileMixin, DownloadFileOnMobileMixin { - Event get event => widget.event; - @override - void initState() { - super.initState(); - checkDownloadFileState(event: event); - } - - @override - void dispose() { - streamSubscription?.cancel(); - downloadFileStateNotifier.dispose(); - super.dispose(); - } + Event get event => widget.event; @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart index b14c54b661..ec918488e2 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart @@ -14,7 +14,7 @@ class ChatDetailsFilesView extends StatelessWidget { required this.controller, }); - final ChatDetailsFileItemState controller; + final ChatDetailsFileItemController controller; @override Widget build(BuildContext context) { @@ -51,7 +51,7 @@ class ChatDetailsFilesView extends StatelessWidget { mimeType: controller.event.mimeType, ); } else { - controller.onDownloadFileTap(event: controller.event); + controller.onDownloadFileTap(); } }, child: Padding( diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart index 173ddac4b3..f469817b30 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart @@ -1,101 +1,40 @@ -import 'dart:typed_data'; - import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/utils/extension/mime_type_extension.dart'; -import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; -import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; +import 'package:fluffychat/widgets/file_widget/base_file_tile_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -class ChatDetailsFileTileRow extends StatelessWidget { - const ChatDetailsFileTileRow({ +class ChatDetailsFileTileRow extends BaseFileTileWidget { + ChatDetailsFileTileRow({ super.key, - required this.mimeType, - required this.filename, - required this.sentDate, - this.style = const FileTileWidgetStyle(), - this.imageBytes, - this.fileTileIcon, - this.fileType, - this.highlightText, - this.sizeString, - }); - - final FileTileWidgetStyle style; - final Uint8List? imageBytes; - final String? fileTileIcon; - final TwakeMimeType? mimeType; - final String? fileType; - final String filename; - final String? highlightText; - final String? sizeString; - final DateTime sentDate; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: style.crossAxisAlignment, - children: [ - if (imageBytes != null) - Padding( - padding: style.imagePadding, - child: ClipRRect( - borderRadius: style.borderRadius, - child: Image.memory( - imageBytes!, - width: style.imageSize, - height: style.imageSize, - fit: BoxFit.cover, - ), - ), - ) - else - SvgPicture.asset( - fileTileIcon ?? mimeType.getIcon(fileType: fileType), - width: style.iconSize, - height: style.iconSize, - ), - style.paddingRightIcon, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + required super.mimeType, + required super.filename, + required DateTime sentDate, + super.style, + super.imageBytes, + super.fileTileIcon, + super.fileType, + super.highlightText, + super.sizeString, + }) : super( + subTitle: (context) => Row( children: [ - const SizedBox(height: ChatDetailsFileRowStyle.textTopMargin), - FileNameText( - filename: filename, - highlightText: highlightText, - style: style, - ), - Row( - children: [ - if (sizeString != null) ...[ - Text( - sizeString!, - style: - ChatDetailsFileRowStyle.textInformationStyle(context), - ), - Text( - " - ", - style: - ChatDetailsFileRowStyle.textInformationStyle(context), - ), - ], - Flexible( - child: Text( - sentDate.localizedTime(context), - style: - ChatDetailsFileRowStyle.textInformationStyle(context), - ), - ), - ], + if (sizeString != null) ...[ + Text( + sizeString, + style: ChatDetailsFileRowStyle.textInformationStyle(context), + ), + Text( + " - ", + style: ChatDetailsFileRowStyle.textInformationStyle(context), + ), + ], + Flexible( + child: Text( + sentDate.localizedTime(context), + style: ChatDetailsFileRowStyle.textInformationStyle(context), + ), ), - style.paddingBottomText, ], ), - ), - ], - ); - } + ); } diff --git a/lib/widgets/file_widget/base_file_tile_widget.dart b/lib/widgets/file_widget/base_file_tile_widget.dart new file mode 100644 index 0000000000..d74393e9e8 --- /dev/null +++ b/lib/widgets/file_widget/base_file_tile_widget.dart @@ -0,0 +1,116 @@ +import 'dart:typed_data'; + +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/utils/string_extension.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class BaseFileTileWidget extends StatelessWidget { + const BaseFileTileWidget({ + super.key, + required this.mimeType, + required this.filename, + this.fileType, + this.highlightText, + this.sizeString, + this.backgroundColor, + this.fileTileIcon, + this.imageBytes, + this.style = const FileTileWidgetStyle(), + required this.subTitle, + }); + + final TwakeMimeType mimeType; + final String filename; + final String? highlightText; + final String? sizeString; + final Color? backgroundColor; + final String? fileType; + final Uint8List? imageBytes; + final String? fileTileIcon; + final FileTileWidgetStyle style; + final Widget Function(BuildContext) subTitle; + + @override + Widget build(BuildContext context) { + return Container( + padding: style.paddingFileTileAll, + decoration: ShapeDecoration( + color: backgroundColor ?? style.backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: style.borderRadius, + ), + ), + child: Row( + crossAxisAlignment: style.crossAxisAlignment, + children: [ + if (imageBytes != null) + Padding( + padding: style.imagePadding, + child: ClipRRect( + borderRadius: style.borderRadius, + child: Image.memory( + imageBytes!, + width: style.imageSize, + height: style.imageSize, + fit: BoxFit.cover, + ), + ), + ), + if (imageBytes == null) + SvgPicture.asset( + fileTileIcon ?? mimeType.getIcon(fileType: fileType), + width: style.iconSize, + height: style.iconSize, + ), + style.paddingRightIcon, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 4.0), + FileNameText( + filename: filename, + highlightText: highlightText, + style: style, + ), + subTitle(context), + style.paddingBottomText, + ], + ), + ), + ], + ), + ); + } +} + +class FileNameText extends StatelessWidget { + const FileNameText({ + super.key, + required this.filename, + this.highlightText, + this.style = const FileTileWidgetStyle(), + }); + + final String filename; + final String? highlightText; + final FileTileWidgetStyle style; + + @override + Widget build(BuildContext context) { + return RichText( + maxLines: 2, + text: TextSpan( + children: filename.buildHighlightTextSpans( + highlightText ?? '', + style: style.textStyle(context), + highlightStyle: style.highlightTextStyle(context), + ), + ), + overflow: TextOverflow.ellipsis, + ); + } +} diff --git a/lib/widgets/file_widget/download_file_tile_widget.dart b/lib/widgets/file_widget/download_file_tile_widget.dart index 8d24575b95..a1f60c338d 100644 --- a/lib/widgets/file_widget/download_file_tile_widget.dart +++ b/lib/widgets/file_widget/download_file_tile_widget.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/widgets/file_widget/base_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/circular_loading_download_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; @@ -125,12 +126,12 @@ class DownloadFileTileWidget extends StatelessWidget { if (sizeString != null) TextInformationOfFile( value: sizeString!, - style: style, + style: style.textInformationStyle(context), downloadFileStateNotifier: downloadFileStateNotifier, ), TextInformationOfFile( value: " · ", - style: style, + style: style.textInformationStyle(context), ), Flexible( child: TextInformationOfFile( @@ -138,7 +139,7 @@ class DownloadFileTileWidget extends StatelessWidget { context, fileType: fileType, ), - style: style, + style: style.textInformationStyle(context), ), ), ], diff --git a/lib/widgets/file_widget/downloading_file_tile_widget.dart b/lib/widgets/file_widget/downloading_file_tile_widget.dart new file mode 100644 index 0000000000..9b4fb7a0ef --- /dev/null +++ b/lib/widgets/file_widget/downloading_file_tile_widget.dart @@ -0,0 +1,145 @@ +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/widgets/file_widget/base_file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/circular_loading_download_widget.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:flutter/material.dart'; + +class DownloadingFileTileWidget extends StatelessWidget { + const DownloadingFileTileWidget({ + super.key, + this.style = const MessageFileTileStyle(), + required this.mimeType, + required this.filename, + this.highlightText, + this.fileType, + this.sizeString, + required this.downloadFileStateNotifier, + this.onCancelDownload, + }); + + final TwakeMimeType mimeType; + final String filename; + final MessageFileTileStyle style; + final String? highlightText; + final String? sizeString; + final String? fileType; + final ValueNotifier downloadFileStateNotifier; + final VoidCallback? onCancelDownload; + + @override + Widget build(BuildContext context) { + return Container( + padding: style.paddingFileTileAll, + decoration: ShapeDecoration( + color: style.backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: style.borderRadius, + ), + ), + child: Row( + crossAxisAlignment: style.crossAxisAlignment, + children: [ + ValueListenableBuilder( + valueListenable: downloadFileStateNotifier, + builder: (context, downloadFileState, child) { + double? downloadProgress; + if (downloadFileState is DownloadingPresentationState) { + if (downloadFileState.total == null || + downloadFileState.receive == null) { + downloadProgress = null; + } else { + downloadProgress = + downloadFileState.receive! / downloadFileState.total!; + } + } else if (downloadFileState is NotDownloadPresentationState) { + downloadProgress = 0; + } + return Stack( + alignment: Alignment.center, + children: [ + Container( + margin: style.marginDownloadIcon, + width: style.iconSize, + height: style.iconSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + ), + if (downloadProgress != 0) + SizedBox( + width: style.circularProgressLoadingSize, + height: style.circularProgressLoadingSize, + child: CircularLoadingDownloadWidget( + style: style, + downloadProgress: downloadProgress, + ), + ), + InkWell( + onTap: onCancelDownload, + child: Container( + width: style.downloadIconSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Icon( + downloadProgress == 0 + ? Icons.arrow_downward + : Icons.close, + key: ValueKey(downloadProgress), + color: Theme.of(context).colorScheme.surface, + size: style.downloadIconSize, + ), + ), + ), + ], + ); + }, + ), + style.paddingRightIcon, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 4.0), + FileNameText( + filename: filename, + highlightText: highlightText, + style: style, + ), + Row( + children: [ + if (sizeString != null) + TextInformationOfFile( + value: sizeString!, + style: style.textInformationStyle(context), + downloadFileStateNotifier: downloadFileStateNotifier, + ), + TextInformationOfFile( + value: " · ", + style: style.textInformationStyle(context), + ), + Flexible( + child: TextInformationOfFile( + value: mimeType.getFileType( + context, + fileType: fileType, + ), + style: style.textInformationStyle(context), + ), + ), + ], + ), + style.paddingBottomText, + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/file_widget/file_tile_widget.dart b/lib/widgets/file_widget/file_tile_widget.dart index 32ad5ede78..d735406348 100644 --- a/lib/widgets/file_widget/file_tile_widget.dart +++ b/lib/widgets/file_widget/file_tile_widget.dart @@ -1,151 +1,58 @@ -import 'dart:typed_data'; - import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/int_extension.dart'; -import 'package:fluffychat/utils/string_extension.dart'; +import 'package:fluffychat/widgets/file_widget/base_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -class FileTileWidget extends StatelessWidget { - const FileTileWidget({ +class FileTileWidget extends BaseFileTileWidget { + FileTileWidget({ super.key, - required this.mimeType, - required this.filename, - this.fileType, - this.highlightText, - this.sizeString, - this.backgroundColor, - this.fileTileIcon, - this.imageBytes, - this.style = const FileTileWidgetStyle(), - }); - - final TwakeMimeType mimeType; - final String filename; - final String? highlightText; - final String? sizeString; - final Color? backgroundColor; - final String? fileType; - final Uint8List? imageBytes; - final String? fileTileIcon; - final FileTileWidgetStyle style; - - @override - Widget build(BuildContext context) { - return Container( - padding: style.paddingFileTileAll, - decoration: ShapeDecoration( - color: backgroundColor ?? style.backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: style.borderRadius, - ), - ), - child: Row( - crossAxisAlignment: style.crossAxisAlignment, - children: [ - if (imageBytes != null) - Padding( - padding: style.imagePadding, - child: ClipRRect( - borderRadius: style.borderRadius, - child: Image.memory( - imageBytes!, - width: style.imageSize, - height: style.imageSize, - fit: BoxFit.cover, + required super.mimeType, + required super.filename, + super.fileType, + super.highlightText, + super.sizeString, + super.backgroundColor, + super.fileTileIcon, + super.imageBytes, + super.style = const FileTileWidgetStyle(), + }) : super( + subTitle: (context) => Row( + children: [ + if (sizeString != null) + TextInformationOfFile( + value: sizeString, + style: style.textInformationStyle(context), ), + TextInformationOfFile( + value: " · ", + style: style.textInformationStyle(context), ), - ), - if (imageBytes == null) - SvgPicture.asset( - fileTileIcon ?? mimeType.getIcon(fileType: fileType), - width: style.iconSize, - height: style.iconSize, - ), - style.paddingRightIcon, - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - const SizedBox(height: 4.0), - FileNameText( - filename: filename, - highlightText: highlightText, - style: style, - ), - Row( - children: [ - if (sizeString != null) - TextInformationOfFile( - value: sizeString!, - style: style, - ), - TextInformationOfFile( - value: " · ", - style: style, - ), - Flexible( - child: TextInformationOfFile( - value: mimeType.getFileType( - context, - fileType: fileType, - ), - style: style, - ), - ), - ], + Flexible( + child: TextInformationOfFile( + value: mimeType.getFileType( + context, + fileType: fileType, + ), + style: style.textInformationStyle(context), ), - style.paddingBottomText, - ], - ), + ), + ], ), - ], - ), - ); - } -} - -class FileNameText extends StatelessWidget { - const FileNameText({ - super.key, - required this.filename, - this.highlightText, - this.style = const FileTileWidgetStyle(), - }); - - final String filename; - final String? highlightText; - final FileTileWidgetStyle style; - - @override - Widget build(BuildContext context) { - return Text.rich( - TextSpan( - children: filename.buildHighlightTextSpans( - highlightText ?? '', - style: style.textStyle(context), - highlightStyle: style.highlightTextStyle(context), - ), - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ); - } + ); } class TextInformationOfFile extends StatelessWidget { final String value; - final FileTileWidgetStyle style; + final TextStyle? style; final ValueNotifier? downloadFileStateNotifier; const TextInformationOfFile({ super.key, required this.value, this.downloadFileStateNotifier, - this.style = const FileTileWidgetStyle(), + this.style, }); @override @@ -162,7 +69,7 @@ class TextInformationOfFile extends StatelessWidget { downloadFileState.total! >= IntExtension.oneKB) { return Text( '${downloadFileState.receive!.bytesToMB(placeDecimal: 1)} MB / ', - style: style.textInformationStyle(context), + style: style, maxLines: 1, overflow: TextOverflow.ellipsis, ); @@ -172,7 +79,7 @@ class TextInformationOfFile extends StatelessWidget { ), Text( value, - style: style.textInformationStyle(context), + style: style, maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/widgets/mixins/download_file_on_mobile_mixin.dart b/lib/widgets/mixins/download_file_on_mobile_mixin.dart index 5be940adb1..95a4adbf2c 100644 --- a/lib/widgets/mixins/download_file_on_mobile_mixin.dart +++ b/lib/widgets/mixins/download_file_on_mobile_mixin.dart @@ -7,14 +7,14 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -mixin DownloadFileOnMobileMixin { +mixin DownloadFileOnMobileMixin on State { final downloadManager = getIt.get(); final downloadFileStateNotifier = ValueNotifier( @@ -23,37 +23,45 @@ mixin DownloadFileOnMobileMixin { StreamSubscription>? streamSubscription; - void checkDownloadFileState({ - required Event event, - }) async { - checkFileExistInMemory(event: event); - await checkFileInDownloadsInApp( - event: event, - ); + Event get event; + + @override + void initState() { + super.initState(); + checkDownloadFileState(); + } + + @override + void dispose() { + streamSubscription?.cancel(); + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + void checkDownloadFileState() async { + checkFileExistInMemory(); + await checkFileInDownloadsInApp(); - _trySetupDownloadingStreamSubcription(event.eventId); + _trySetupDownloadingStreamSubcription(); if (streamSubscription != null) { downloadFileStateNotifier.value = const DownloadingPresentationState(); } } - void checkFileExistInMemory({ - required Event event, - }) { + bool checkFileExistInMemory() { final filePathInMem = event.getFilePathFromMem(); if (filePathInMem?.isNotEmpty == true) { downloadFileStateNotifier.value = DownloadedPresentationState( filePath: filePathInMem!, ); - return; + return true; } + return false; } - Future checkFileInDownloadsInApp({ - required Event event, - }) async { + Future checkFileInDownloadsInApp() async { final filePath = - await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: event.eventId, fileName: event.filename, ); @@ -66,16 +74,17 @@ mixin DownloadFileOnMobileMixin { } } - void _trySetupDownloadingStreamSubcription(String eventId) { + void _trySetupDownloadingStreamSubcription() { streamSubscription = downloadManager - .getDownloadStateStream(eventId) + .getDownloadStateStream(event.eventId) ?.listen(setupDownloadingProcess); } - void setupDownloadingProcess(Either event) { - event.fold( + void setupDownloadingProcess(Either resultEvent) { + resultEvent.fold( (failure) { - Logs().e('setupDownloadingProcess::onDownloadingProcess(): $failure'); + Logs() + .e('$T::setupDownloadingProcess::onDownloadingProcess(): $failure'); downloadFileStateNotifier.value = const NotDownloadPresentationState(); streamSubscription?.cancel(); }, @@ -96,12 +105,8 @@ mixin DownloadFileOnMobileMixin { ); } - void onDownloadFileTap({ - required Event event, - }) async { - await checkFileInDownloadsInApp( - event: event, - ); + void onDownloadFileTap() async { + await checkFileInDownloadsInApp(); if (downloadFileStateNotifier.value is DownloadedPresentationState) { return; } @@ -109,6 +114,6 @@ mixin DownloadFileOnMobileMixin { downloadManager.download( event: event, ); - _trySetupDownloadingStreamSubcription(event.eventId); + _trySetupDownloadingStreamSubcription(); } } From 94a98e9eff860da108bdaddf445810a8d65735d4 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Wed, 17 Apr 2024 12:17:48 +0200 Subject: [PATCH 158/183] TW-1584: download file on web mixin created (cherry picked from commit f70e84f91106674c7ca00012a0d2a7c88197088c) --- .../chat/events/message_download_content.dart | 3 +- .../events/message_download_content_web.dart | 88 +------ .../chat_details_files_item.dart | 4 +- .../chat_details_files_item_style.dart | 7 + .../chat_details_files_item_view.dart | 43 ++-- .../chat_details_files_item_view_web.dart | 22 +- .../chat_details_files_item_web.dart | 79 +------ .../files/chat_details_files_page.dart | 43 ++-- .../chat_details_file_row.dart | 218 +++++++++++++++--- .../chat_details_file_row_style.dart | 12 - .../chat_details_file_row_web.dart | 84 +++++++ .../files/chat_details_files_style.dart | 10 - .../mixins/download_file_on_web_mixin.dart | 85 +++++++ 13 files changed, 431 insertions(+), 267 deletions(-) delete mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart delete mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart create mode 100644 lib/widgets/mixins/download_file_on_web_mixin.dart diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 817a0bb072..22ab151f1a 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,7 +1,6 @@ import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; -import 'package:fluffychat/widgets/file_widget/downloading_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:fluffychat/widgets/mixins/download_file_on_mobile_mixin.dart'; @@ -77,7 +76,7 @@ class _MessageDownloadContentState extends State return InkWell( onTap: () => onDownloadFileTap(), - child: DownloadingFileTileWidget( + child: DownloadFileTileWidget( mimeType: widget.event.mimeType, fileType: filetype, filename: filename, diff --git a/lib/pages/chat/events/message_download_content_web.dart b/lib/pages/chat/events/message_download_content_web.dart index 6b1fecda7b..dcbbad2b16 100644 --- a/lib/pages/chat/events/message_download_content_web.dart +++ b/lib/pages/chat/events/message_download_content_web.dart @@ -1,17 +1,11 @@ import 'dart:async'; -import 'package:dartz/dartz.dart' hide State, OpenFile; -import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; -import 'package:fluffychat/utils/exception/downloading_exception.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:fluffychat/widgets/mixins/download_file_on_web_mixin.dart'; import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/material.dart'; @@ -33,83 +27,17 @@ class MessageDownloadContentWeb extends StatefulWidget { } class _MessageDownloadContentWebState extends State - with HandleDownloadAndPreviewFileMixin { - final downloadManager = getIt.get(); - - final downloadFileStateNotifier = ValueNotifier( - const NotDownloadPresentationState(), - ); - - StreamSubscription>? streamSubscription; - + with + HandleDownloadAndPreviewFileMixin, + DownloadFileOnWebMixin { @override - void initState() { - super.initState(); - _trySetupDownloadingStreamSubcription(); - if (streamSubscription != null) { - downloadFileStateNotifier.value = const DownloadingPresentationState(); - } - } - - void _trySetupDownloadingStreamSubcription() { - streamSubscription = downloadManager - .getDownloadStateStream(widget.event.eventId) - ?.listen(setupDownloadingProcess); - } + Event get event => widget.event; - void setupDownloadingProcess(Either event) { - event.fold( - (failure) { - Logs().e('MessageDownloadContentWeb::onDownloadingProcess(): $failure'); - if (failure is DownloadFileFailureState && - failure.exception is CancelDownloadingException) { - downloadFileStateNotifier.value = - const NotDownloadPresentationState(); - } else { - downloadFileStateNotifier.value = - DownloadErrorPresentationState(error: failure); - } - }, - (success) { - if (success is DownloadingFileState) { - if (success.total != 0) { - downloadFileStateNotifier.value = DownloadingPresentationState( - receive: success.receive, - total: success.total, - ); - } - } else if (success is DownloadMatrixFileSuccessState) { - _handleDownloadMatrixFileSuccessState(success); - } - }, - ); - } - - void _handleDownloadMatrixFileSuccessState( - DownloadMatrixFileSuccessState success, - ) { - streamSubscription?.cancel(); - if (mounted) { - downloadFileStateNotifier.value = FileWebDownloadedPresentationState( - matrixFile: success.matrixFile, - ); - handlePreviewWeb(event: widget.event, context: context); - return; - } - - if (TwakeApp.routerKey.currentContext != null) { - handlePreviewWeb( + @override + Future get handlePreview => handlePreviewWeb( event: widget.event, context: TwakeApp.routerKey.currentContext!, ); - } - } - - @override - void dispose() { - downloadFileStateNotifier.dispose(); - super.dispose(); - } @override Widget build(BuildContext context) { @@ -162,7 +90,7 @@ class _MessageDownloadContentWebState extends State downloadManager.download( event: widget.event, ); - _trySetupDownloadingStreamSubcription(); + trySetupDownloadingStreamSubcription(); }, child: FileTileWidget( mimeType: widget.event.mimeType, diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart index eb2567be00..da0a8d8e24 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart @@ -14,7 +14,9 @@ class ChatDetailsFileItem extends StatefulWidget { } class ChatDetailsFileItemController extends State - with HandleDownloadAndPreviewFileMixin, DownloadFileOnMobileMixin { + with + HandleDownloadAndPreviewFileMixin, + DownloadFileOnMobileMixin { @override Event get event => widget.event; diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart index f124a627f6..a136f94caf 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart @@ -2,9 +2,16 @@ import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; import 'package:flutter/material.dart'; class ChatDetailsFileTileStyle extends FileTileWidgetStyle { + static const double textTopMargin = 4.0; + @override Color get backgroundColor => Colors.transparent; @override BorderRadiusGeometry get borderRadius => BorderRadius.circular(8); + + static const double dividerHeight = 1; + + static Color dividerColor(BuildContext context) => + Theme.of(context).dividerColor; } diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart index ec918488e2..33da8419e1 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart @@ -1,6 +1,5 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; @@ -40,31 +39,31 @@ class ChatDetailsFilesView extends StatelessWidget { .cancelDownload(controller.event.eventId); }, ); - } - - return InkWell( - hoverColor: LinagoraSysColors.material().surfaceVariant, - onTap: () { - if (state is DownloadedPresentationState) { + } else if (state is DownloadedPresentationState) { + return ChatDetailsDownloadedFileTileWidget( + onTap: () { controller.handleDownloadFileForPreviewSuccess( filePath: state.filePath, mimeType: controller.event.mimeType, ); - } else { - controller.onDownloadFileTap(); - } - }, - child: Padding( - padding: ChatDetailsFileTileStyle().paddingFileTileAll, - child: ChatDetailsFileTileRow( - mimeType: controller.event.mimeType, - fileType: filetype, - filename: filename, - sizeString: sizeString, - style: ChatDetailsFileTileStyle(), - sentDate: controller.event.originServerTs, - ), - ), + }, + trailingIcon: Icons.folder_outlined, + iconColor: LinagoraSysColors.material().primary, + filename: filename, + mimeType: controller.event.mimeType, + fileType: filetype, + ); + } + + return ChatDetailsDownloadFileTileWidget( + onTap: () => controller.onDownloadFileTap(), + mimeType: controller.event.mimeType, + fileType: filetype, + filename: filename, + sizeString: sizeString, + sentDate: controller.event.originServerTs, + trailingIcon: Icons.download_outlined, + iconColor: LinagoraSysColors.material().tertiary, ); }, ); diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart index fb6f72185b..0c743f57f3 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart @@ -1,6 +1,5 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; @@ -42,8 +41,7 @@ class ChatDetailsFilesViewWeb extends StatelessWidget { ); } - return InkWell( - hoverColor: LinagoraSysColors.material().surfaceVariant, + return ChatDetailsDownloadFileTileWidget( onTap: () { if (state is FileWebDownloadedPresentationState) { controller.handlePreviewWeb( @@ -59,17 +57,13 @@ class ChatDetailsFilesViewWeb extends StatelessWidget { controller.trySetupDownloadingStreamSubcription(); } }, - child: Padding( - padding: ChatDetailsFileTileStyle().paddingFileTileAll, - child: ChatDetailsFileTileRow( - mimeType: controller.event.mimeType, - fileType: filetype, - filename: filename, - sizeString: sizeString, - style: ChatDetailsFileTileStyle(), - sentDate: controller.event.originServerTs, - ), - ), + mimeType: controller.event.mimeType, + fileType: filetype, + filename: filename, + sizeString: sizeString, + sentDate: controller.event.originServerTs, + trailingIcon: Icons.download_outlined, + iconColor: LinagoraSysColors.material().tertiary, ); }, ); diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart index f76e86f5ab..a367255964 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart @@ -1,13 +1,7 @@ import 'dart:async'; -import 'package:dartz/dartz.dart' hide State, OpenFile; -import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart'; -import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/widgets/mixins/download_file_on_web_mixin.dart'; import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/material.dart'; @@ -23,78 +17,17 @@ class ChatDetailsFileItemWeb extends StatefulWidget { } class ChatDetailsFileItemWebState extends State - with HandleDownloadAndPreviewFileMixin { - final downloadManager = getIt.get(); - - final downloadFileStateNotifier = ValueNotifier( - const NotDownloadPresentationState(), - ); - - StreamSubscription>? streamSubscription; - + with + HandleDownloadAndPreviewFileMixin, + DownloadFileOnWebMixin { + @override Event get event => widget.event; @override - void initState() { - super.initState(); - trySetupDownloadingStreamSubcription(); - if (streamSubscription != null) { - downloadFileStateNotifier.value = const DownloadingPresentationState(); - } - } - - void trySetupDownloadingStreamSubcription() { - streamSubscription = downloadManager - .getDownloadStateStream(widget.event.eventId) - ?.listen(setupDownloadingProcess); - } - - void setupDownloadingProcess(Either event) { - event.fold( - (failure) { - Logs().e('ChatDetailsFileItem::onDownloadingProcess(): $failure'); - downloadFileStateNotifier.value = const NotDownloadPresentationState(); - }, - (success) { - if (success is DownloadingFileState) { - if (success.total != 0) { - downloadFileStateNotifier.value = DownloadingPresentationState( - receive: success.receive, - total: success.total, - ); - } - } else if (success is DownloadMatrixFileSuccessState) { - _handleDownloadMatrixFileSuccessState(success); - } - }, - ); - } - - void _handleDownloadMatrixFileSuccessState( - DownloadMatrixFileSuccessState success, - ) { - streamSubscription?.cancel(); - if (mounted) { - downloadFileStateNotifier.value = FileWebDownloadedPresentationState( - matrixFile: success.matrixFile, - ); - handlePreviewWeb(event: widget.event, context: context); - return; - } - - if (TwakeApp.routerKey.currentContext != null) { - handlePreviewWeb( + Future get handlePreview => handlePreviewWeb( event: widget.event, context: TwakeApp.routerKey.currentContext!, ); - } - } - - @override - void dispose() { - downloadFileStateNotifier.dispose(); - super.dispose(); - } @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart index 6f319e9d8f..33b7aa0013 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart @@ -2,7 +2,6 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart'; import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_builder.dart'; import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_controller.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -18,28 +17,26 @@ class ChatDetailsFilesPage extends StatelessWidget { @override Widget build(BuildContext context) { - return SameTypeEventsBuilder( - controller: controller, - builder: (context, eventsState, _) { - final events = eventsState - .getSuccessOrNull() - ?.events ?? - []; - return SliverList.separated( - itemCount: events.length, - itemBuilder: (context, index) { - if (!PlatformInfos.isWeb) { - return ChatDetailsFileItem(event: events[index]); - } - return ChatDetailsFileItemWeb(event: events[index]); - }, - separatorBuilder: (context, index) => Container( - height: ChatDetailsFilesStyle.dividerHeight, - margin: ChatDetailsFilesStyle.dividerMargin, - color: ChatDetailsFilesStyle.dividerColor(context), - ), - ); - }, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SameTypeEventsBuilder( + controller: controller, + builder: (context, eventsState, _) { + final events = eventsState + .getSuccessOrNull() + ?.events ?? + []; + return SliverList.builder( + itemCount: events.length, + itemBuilder: (context, index) { + if (!PlatformInfos.isWeb) { + return ChatDetailsFileItem(event: events[index]); + } + return ChatDetailsFileItemWeb(event: events[index]); + }, + ); + }, + ), ); } } diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart index f469817b30..77d51cf0dc 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart @@ -1,40 +1,198 @@ -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/widgets/file_widget/base_file_tile_widget.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; -class ChatDetailsFileTileRow extends BaseFileTileWidget { - ChatDetailsFileTileRow({ +class ChatDetailsDownloadFileTileWidget extends StatelessWidget { + const ChatDetailsDownloadFileTileWidget({ super.key, - required super.mimeType, - required super.filename, - required DateTime sentDate, - super.style, - super.imageBytes, - super.fileTileIcon, - super.fileType, - super.highlightText, - super.sizeString, - }) : super( - subTitle: (context) => Row( + required this.onTap, + required this.trailingIcon, + required this.iconColor, + required this.filename, + required this.mimeType, + required this.fileType, + required this.sizeString, + required this.sentDate, + }); + + final GestureTapCallback onTap; + final IconData trailingIcon; + final Color iconColor; + final String filename; + final String? mimeType; + final String? fileType; + final String? sizeString; + final DateTime sentDate; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SizedBox( + width: 8, + ), + SvgPicture.asset( + mimeType.getIcon(fileType: fileType), + width: ChatDetailsFileTileStyle().iconSize, + height: ChatDetailsFileTileStyle().iconSize, + ), + ChatDetailsFileTileStyle().paddingRightIcon, + Expanded( + child: Column( children: [ - if (sizeString != null) ...[ - Text( - sizeString, - style: ChatDetailsFileRowStyle.textInformationStyle(context), - ), - Text( - " - ", - style: ChatDetailsFileRowStyle.textInformationStyle(context), - ), - ], - Flexible( - child: Text( - sentDate.localizedTime(context), - style: ChatDetailsFileRowStyle.textInformationStyle(context), + ChatDetailsFileTileRow( + onTap: onTap, + trailingIcon: trailingIcon, + iconColor: iconColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + filename, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + if (sizeString != null) ...[ + Text( + sizeString!, + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + Text( + " - ", + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + ], + Flexible( + child: Text( + sentDate.localizedTime(context), + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + ), + ], + ), + ], ), ), ], ), - ); + ), + ], + ); + } +} + +class ChatDetailsDownloadedFileTileWidget extends StatelessWidget { + const ChatDetailsDownloadedFileTileWidget({ + super.key, + required this.onTap, + required this.trailingIcon, + required this.iconColor, + required this.filename, + required this.mimeType, + required this.fileType, + }); + + final GestureTapCallback onTap; + final IconData trailingIcon; + final Color iconColor; + final String filename; + final String? mimeType; + final String? fileType; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SizedBox( + width: 8, + ), + SvgPicture.asset( + mimeType.getIcon(fileType: fileType), + width: ChatDetailsFileTileStyle().iconSize, + height: ChatDetailsFileTileStyle().iconSize, + ), + ChatDetailsFileTileStyle().paddingRightIcon, + Expanded( + child: ChatDetailsFileTileRow( + onTap: onTap, + trailingIcon: trailingIcon, + iconColor: iconColor, + child: RichText( + maxLines: 3, + text: TextSpan( + text: filename, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: LinagoraSysColors.material().primary), + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } +} + +class ChatDetailsFileTileRow extends StatelessWidget { + const ChatDetailsFileTileRow({ + super.key, + required this.child, + required this.onTap, + required this.trailingIcon, + required this.iconColor, + }); + + final GestureTapCallback onTap; + final IconData trailingIcon; + final Color iconColor; + final Widget child; + + @override + Widget build(BuildContext context) { + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: Container( + padding: const EdgeInsets.only(right: 8.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ChatDetailsFileTileStyle.dividerHeight, + color: ChatDetailsFileTileStyle.dividerColor(context), + ), + ), + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: child, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8), + child: Icon( + trailingIcon, + color: iconColor, + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart deleted file mode 100644 index 5f421fb522..0000000000 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_style.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; - -class ChatDetailsFileRowStyle { - static TextStyle textInformationStyle(BuildContext context) { - return Theme.of(context).textTheme.bodySmall!.copyWith( - color: LinagoraSysColors.material().tertiary, - ); - } - - static const double textTopMargin = 4.0; -} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart new file mode 100644 index 0000000000..a59dbb13a3 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart @@ -0,0 +1,84 @@ +import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/widgets/file_widget/base_file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ChatDetailsFileTileRowWeb extends StatelessWidget { + const ChatDetailsFileTileRowWeb({ + super.key, + required this.mimeType, + required this.filename, + required this.sentDate, + required this.style, + required this.fileType, + required this.sizeString, + }); + + final TwakeMimeType mimeType; + final String filename; + final String? sizeString; + final String? fileType; + final FileTileWidgetStyle style; + final DateTime sentDate; + + @override + Widget build(BuildContext context) { + return Container( + padding: style.paddingFileTileAll, + decoration: ShapeDecoration( + color: style.backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: style.borderRadius, + ), + ), + child: Row( + crossAxisAlignment: style.crossAxisAlignment, + children: [ + SvgPicture.asset( + mimeType.getIcon(fileType: fileType), + width: style.iconSize, + height: style.iconSize, + ), + style.paddingRightIcon, + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + FileNameText( + filename: filename, + style: style, + ), + Row( + children: [ + if (sizeString != null) ...[ + Text( + sizeString!, + style: style.textInformationStyle( + context, + ), + ), + ], + ], + ), + style.paddingBottomText, + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Text( + sentDate.localizedTimeShort(context), + style: style.textInformationStyle(context), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart deleted file mode 100644 index 0646c96fac..0000000000 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_style.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class ChatDetailsFilesStyle { - static const EdgeInsets dividerMargin = EdgeInsets.only(left: 64); - - static const double dividerHeight = 1; - - static Color dividerColor(BuildContext context) => - Theme.of(context).dividerColor; -} diff --git a/lib/widgets/mixins/download_file_on_web_mixin.dart b/lib/widgets/mixins/download_file_on_web_mixin.dart new file mode 100644 index 0000000000..7416dcfe5f --- /dev/null +++ b/lib/widgets/mixins/download_file_on_web_mixin.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +mixin DownloadFileOnWebMixin on State { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + StreamSubscription>? streamSubscription; + + Event get event; + + Future get handlePreview; + + @override + void initState() { + super.initState(); + trySetupDownloadingStreamSubcription(); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } + + void trySetupDownloadingStreamSubcription() { + streamSubscription = downloadManager + .getDownloadStateStream(event.eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess(Either resultEvent) { + resultEvent.fold( + (failure) { + Logs().e('$T::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadMatrixFileSuccessState) { + _handleDownloadMatrixFileSuccessState(success); + } + }, + ); + } + + void _handleDownloadMatrixFileSuccessState( + DownloadMatrixFileSuccessState success, + ) { + streamSubscription?.cancel(); + if (mounted) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + handlePreview; + return; + } + + if (TwakeApp.routerKey.currentContext != null) { + handlePreview; + } + } + + @override + void dispose() { + downloadFileStateNotifier.dispose(); + super.dispose(); + } +} From 797d8295751451daf4f215dbdbe4bed1c766cd51 Mon Sep 17 00:00:00 2001 From: Terence ZAFINDRATAFA Date: Thu, 18 Apr 2024 20:55:57 +0200 Subject: [PATCH 159/183] TW-1584: DownloadFileTileWidget renamed DownloadingFileTileWidget (cherry picked from commit aa535e329d09aa9b0fe117d3194063bebb7b1373) --- .../chat/events/message_download_content.dart | 3 +- .../events/message_download_content_web.dart | 9 +- .../chat_details_files_item_style.dart | 37 ++++ .../chat_details_files_item_view.dart | 15 +- .../chat_details_files_item_view_web.dart | 24 +-- .../files/chat_details_files_page.dart | 3 +- .../files/chat_details_files_page_style.dart | 8 + .../chat_details_file_download_tile.dart | 86 ++++++++ .../chat_details_file_downloaded_tile.dart | 45 ++++ .../chat_details_file_downloading_tile.dart | 83 ++++++++ .../chat_details_file_row.dart | 198 ------------------ .../chat_details_file_row_body.dart | 48 +++++ ...chat_details_file_row_downloading_web.dart | 102 +++++++++ .../chat_details_file_row_web.dart | 113 +++++----- .../chat_details_row_downloading_wrapper.dart | 90 ++++++++ .../chat_details_row_wrapper.dart | 37 ++++ .../chat_details_file_downloading_tile.dart | 83 ++++++++ .../chat_details_file_row_web.dart | 98 +++++++++ .../mixins/download_file_on_mobile_mixin.dart | 20 +- .../mixins/download_file_on_web_mixin.dart | 28 ++- 20 files changed, 836 insertions(+), 294 deletions(-) create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page_style.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_download_tile.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloaded_tile.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloading_tile.dart delete mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_body.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_downloading_web.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_downloading_wrapper.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_downloading_tile.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_row_web.dart diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 22ab151f1a..817a0bb072 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,6 +1,7 @@ import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; +import 'package:fluffychat/widgets/file_widget/downloading_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:fluffychat/widgets/mixins/download_file_on_mobile_mixin.dart'; @@ -76,7 +77,7 @@ class _MessageDownloadContentState extends State return InkWell( onTap: () => onDownloadFileTap(), - child: DownloadFileTileWidget( + child: DownloadingFileTileWidget( mimeType: widget.event.mimeType, fileType: filetype, filename: filename, diff --git a/lib/pages/chat/events/message_download_content_web.dart b/lib/pages/chat/events/message_download_content_web.dart index dcbbad2b16..2d5803a250 100644 --- a/lib/pages/chat/events/message_download_content_web.dart +++ b/lib/pages/chat/events/message_download_content_web.dart @@ -84,14 +84,7 @@ class _MessageDownloadContentWebState extends State } return InkWell( - onTap: () { - downloadFileStateNotifier.value = - const DownloadingPresentationState(); - downloadManager.download( - event: widget.event, - ); - trySetupDownloadingStreamSubcription(); - }, + onTap: () => onDownloadFileTap(), child: FileTileWidget( mimeType: widget.event.mimeType, fileType: filetype, diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart index a136f94caf..e59da2b683 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart @@ -1,7 +1,10 @@ import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class ChatDetailsFileTileStyle extends FileTileWidgetStyle { + ChatDetailsFileTileStyle(); + static const double textTopMargin = 4.0; @override @@ -14,4 +17,38 @@ class ChatDetailsFileTileStyle extends FileTileWidgetStyle { static Color dividerColor(BuildContext context) => Theme.of(context).dividerColor; + + static const EdgeInsets bodyPadding = EdgeInsets.only(right: 8.0); + static const EdgeInsets bodyPaddingWeb = EdgeInsets.all(8.0); + static const EdgeInsets bodyChildPadding = + EdgeInsets.symmetric(vertical: 8.0); + static const EdgeInsets trailingPadding = EdgeInsets.only(left: 8); + + static TextStyle? downloadedFileTextStyle(BuildContext context) => + Theme.of(context).textTheme.titleMedium?.copyWith( + color: LinagoraSysColors.material().primary, + ); + static const int downloadedFilenameMaxLines = 3; + + static TextStyle? downloadFileTextStyle(BuildContext context) => + Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ); + static const int downloadFilenameMaxLines = 1; + + static const double wrapperLeftPadding = 8; + + static TextStyle? downloadSizeFileTextStyle(BuildContext context) => + Theme.of(context).textTheme.bodyMedium?.copyWith( + color: const FileTileWidgetStyle().fileInfoColor, + ); + + static double get tileHeight => 56; + + static double get downloadingTileBottomPlaceholder => 16; + static double get downloadingTileBottomPlaceholderWeb => 22; + + static double get downloadTileBottomPlaceholderWeb => 24; + + static double get downloadingTileInformationPadding => 2; } diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart index 33da8419e1..d9a0a9f446 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_view.dart @@ -1,9 +1,9 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_download_tile.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloaded_tile.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloading_tile.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; -import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; @@ -25,14 +25,13 @@ class ChatDetailsFilesView extends StatelessWidget { valueListenable: controller.downloadFileStateNotifier, builder: (context, DownloadPresentationState state, child) { if (state is DownloadingPresentationState) { - return DownloadFileTileWidget( + return ChatDetailsDownloadingFileTile( mimeType: controller.event.mimeType, fileType: filetype, filename: filename, sizeString: sizeString, - style: const MessageFileTileStyle(), downloadFileStateNotifier: controller.downloadFileStateNotifier, - onCancelDownload: () { + onTap: () { controller.downloadFileStateNotifier.value = const NotDownloadPresentationState(); controller.downloadManager @@ -40,7 +39,7 @@ class ChatDetailsFilesView extends StatelessWidget { }, ); } else if (state is DownloadedPresentationState) { - return ChatDetailsDownloadedFileTileWidget( + return ChatDetailsDownloadedFileTile( onTap: () { controller.handleDownloadFileForPreviewSuccess( filePath: state.filePath, @@ -55,7 +54,7 @@ class ChatDetailsFilesView extends StatelessWidget { ); } - return ChatDetailsDownloadFileTileWidget( + return ChatDetailsDownloadFileTile( onTap: () => controller.onDownloadFileTap(), mimeType: controller.event.mimeType, fileType: filetype, diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart index 0c743f57f3..f7f66b4d52 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart @@ -1,11 +1,9 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_downloading_web.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; -import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:flutter/material.dart'; -import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class ChatDetailsFilesViewWeb extends StatelessWidget { const ChatDetailsFilesViewWeb({ @@ -25,14 +23,14 @@ class ChatDetailsFilesViewWeb extends StatelessWidget { valueListenable: controller.downloadFileStateNotifier, builder: (context, DownloadPresentationState state, child) { if (state is DownloadingPresentationState) { - return DownloadFileTileWidget( + return ChatDetailsFileTileRowDownloadingWeb( mimeType: controller.event.mimeType, fileType: filetype, filename: filename, sizeString: sizeString, - style: const MessageFileTileStyle(), + sentDate: controller.event.originServerTs, downloadFileStateNotifier: controller.downloadFileStateNotifier, - onCancelDownload: () { + onTap: () { controller.downloadFileStateNotifier.value = const NotDownloadPresentationState(); controller.downloadManager @@ -41,7 +39,7 @@ class ChatDetailsFilesViewWeb extends StatelessWidget { ); } - return ChatDetailsDownloadFileTileWidget( + return ChatDetailsFileTileRowWeb( onTap: () { if (state is FileWebDownloadedPresentationState) { controller.handlePreviewWeb( @@ -49,12 +47,7 @@ class ChatDetailsFilesViewWeb extends StatelessWidget { context: context, ); } else { - controller.downloadFileStateNotifier.value = - const DownloadingPresentationState(); - controller.downloadManager.download( - event: controller.event, - ); - controller.trySetupDownloadingStreamSubcription(); + controller.onDownloadFileTap(); } }, mimeType: controller.event.mimeType, @@ -62,8 +55,7 @@ class ChatDetailsFilesViewWeb extends StatelessWidget { filename: filename, sizeString: sizeString, sentDate: controller.event.originServerTs, - trailingIcon: Icons.download_outlined, - iconColor: LinagoraSysColors.material().tertiary, + isDownloaded: state is FileWebDownloadedPresentationState, ); }, ); diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart index 33b7aa0013..af78b271ff 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_page_style.dart'; import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_builder.dart'; import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_controller.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -18,7 +19,7 @@ class ChatDetailsFilesPage extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: ChatDetailsFilesPageStyle.horizontalPadding, child: SameTypeEventsBuilder( controller: controller, builder: (context, eventsState, _) { diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page_style.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page_style.dart new file mode 100644 index 0000000000..6e2e79a495 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_page_style.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class ChatDetailsFilesPageStyle { + const ChatDetailsFilesPageStyle(); + + static EdgeInsets horizontalPadding = + const EdgeInsets.symmetric(horizontal: 8.0); +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_download_tile.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_download_tile.dart new file mode 100644 index 0000000000..fd352a62ca --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_download_tile.dart @@ -0,0 +1,86 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_body.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart'; +import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ChatDetailsDownloadFileTile extends StatelessWidget { + const ChatDetailsDownloadFileTile({ + super.key, + required this.onTap, + required this.trailingIcon, + required this.iconColor, + required this.filename, + required this.mimeType, + required this.fileType, + required this.sizeString, + required this.sentDate, + }); + + final GestureTapCallback onTap; + final IconData trailingIcon; + final Color iconColor; + final String filename; + final String? mimeType; + final String? fileType; + final String? sizeString; + final DateTime sentDate; + + @override + Widget build(BuildContext context) { + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: ChatDetailsFileRowWrapper( + mimeType: mimeType, + fileType: fileType, + child: Column( + children: [ + ChatDetailsFileRowBody( + trailingIcon: trailingIcon, + iconColor: iconColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + filename, + style: + ChatDetailsFileTileStyle.downloadFileTextStyle(context), + maxLines: ChatDetailsFileTileStyle.downloadFilenameMaxLines, + overflow: TextOverflow.ellipsis, + ), + Flexible( + child: Row( + children: [ + if (sizeString != null) ...[ + Text( + sizeString!, + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + Text( + " - ", + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + ], + Flexible( + child: Text( + sentDate.localizedTime(context), + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloaded_tile.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloaded_tile.dart new file mode 100644 index 0000000000..5c5bf94272 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloaded_tile.dart @@ -0,0 +1,45 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_body.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ChatDetailsDownloadedFileTile extends StatelessWidget { + const ChatDetailsDownloadedFileTile({ + super.key, + required this.onTap, + required this.trailingIcon, + required this.iconColor, + required this.filename, + required this.mimeType, + required this.fileType, + }); + + final GestureTapCallback onTap; + final IconData trailingIcon; + final Color iconColor; + final String filename; + final String? mimeType; + final String? fileType; + + @override + Widget build(BuildContext context) { + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: ChatDetailsFileRowWrapper( + mimeType: mimeType, + fileType: fileType, + child: ChatDetailsFileRowBody( + trailingIcon: trailingIcon, + iconColor: iconColor, + child: Text( + filename, + style: ChatDetailsFileTileStyle.downloadedFileTextStyle(context), + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloading_tile.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloading_tile.dart new file mode 100644 index 0000000000..e37f764eb9 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_downloading_tile.dart @@ -0,0 +1,83 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_downloading_wrapper.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ChatDetailsDownloadingFileTile extends StatelessWidget { + const ChatDetailsDownloadingFileTile({ + super.key, + required this.onTap, + required this.filename, + required this.mimeType, + required this.fileType, + required this.sizeString, + required this.downloadFileStateNotifier, + }); + + final GestureTapCallback onTap; + final String filename; + final String? mimeType; + final String? fileType; + final String? sizeString; + final ValueNotifier downloadFileStateNotifier; + + @override + Widget build(BuildContext context) { + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: ChatDetailsFileRowDownloadingWrapper( + mimeType: mimeType, + fileType: fileType, + downloadFileStateNotifier: downloadFileStateNotifier, + child: Container( + height: ChatDetailsFileTileStyle.tileHeight, + padding: ChatDetailsFileTileStyle.bodyPadding, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ChatDetailsFileTileStyle.dividerHeight, + color: ChatDetailsFileTileStyle.dividerColor(context), + ), + ), + ), + child: Padding( + padding: ChatDetailsFileTileStyle.bodyChildPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + maxLines: ChatDetailsFileTileStyle.downloadFilenameMaxLines, + text: TextSpan( + text: filename, + style: ChatDetailsFileTileStyle.downloadFileTextStyle( + context, + ), + ), + overflow: TextOverflow.ellipsis, + ), + if (sizeString != null) + Expanded( + child: TextInformationOfFile( + value: sizeString!, + style: ChatDetailsFileTileStyle().textInformationStyle( + context, + ), + downloadFileStateNotifier: downloadFileStateNotifier, + ), + ) + else + SizedBox( + height: ChatDetailsFileTileStyle + .downloadingTileBottomPlaceholder, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart deleted file mode 100644 index 77d51cf0dc..0000000000 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; -import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/utils/extension/mime_type_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; - -class ChatDetailsDownloadFileTileWidget extends StatelessWidget { - const ChatDetailsDownloadFileTileWidget({ - super.key, - required this.onTap, - required this.trailingIcon, - required this.iconColor, - required this.filename, - required this.mimeType, - required this.fileType, - required this.sizeString, - required this.sentDate, - }); - - final GestureTapCallback onTap; - final IconData trailingIcon; - final Color iconColor; - final String filename; - final String? mimeType; - final String? fileType; - final String? sizeString; - final DateTime sentDate; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const SizedBox( - width: 8, - ), - SvgPicture.asset( - mimeType.getIcon(fileType: fileType), - width: ChatDetailsFileTileStyle().iconSize, - height: ChatDetailsFileTileStyle().iconSize, - ), - ChatDetailsFileTileStyle().paddingRightIcon, - Expanded( - child: Column( - children: [ - ChatDetailsFileTileRow( - onTap: onTap, - trailingIcon: trailingIcon, - iconColor: iconColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - filename, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Row( - children: [ - if (sizeString != null) ...[ - Text( - sizeString!, - style: ChatDetailsFileTileStyle() - .textInformationStyle(context), - ), - Text( - " - ", - style: ChatDetailsFileTileStyle() - .textInformationStyle(context), - ), - ], - Flexible( - child: Text( - sentDate.localizedTime(context), - style: ChatDetailsFileTileStyle() - .textInformationStyle(context), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ], - ); - } -} - -class ChatDetailsDownloadedFileTileWidget extends StatelessWidget { - const ChatDetailsDownloadedFileTileWidget({ - super.key, - required this.onTap, - required this.trailingIcon, - required this.iconColor, - required this.filename, - required this.mimeType, - required this.fileType, - }); - - final GestureTapCallback onTap; - final IconData trailingIcon; - final Color iconColor; - final String filename; - final String? mimeType; - final String? fileType; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const SizedBox( - width: 8, - ), - SvgPicture.asset( - mimeType.getIcon(fileType: fileType), - width: ChatDetailsFileTileStyle().iconSize, - height: ChatDetailsFileTileStyle().iconSize, - ), - ChatDetailsFileTileStyle().paddingRightIcon, - Expanded( - child: ChatDetailsFileTileRow( - onTap: onTap, - trailingIcon: trailingIcon, - iconColor: iconColor, - child: RichText( - maxLines: 3, - text: TextSpan( - text: filename, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(color: LinagoraSysColors.material().primary), - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ); - } -} - -class ChatDetailsFileTileRow extends StatelessWidget { - const ChatDetailsFileTileRow({ - super.key, - required this.child, - required this.onTap, - required this.trailingIcon, - required this.iconColor, - }); - - final GestureTapCallback onTap; - final IconData trailingIcon; - final Color iconColor; - final Widget child; - - @override - Widget build(BuildContext context) { - return InkWell( - hoverColor: LinagoraSysColors.material().surfaceVariant, - onTap: onTap, - child: Container( - padding: const EdgeInsets.only(right: 8.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: ChatDetailsFileTileStyle.dividerHeight, - color: ChatDetailsFileTileStyle.dividerColor(context), - ), - ), - ), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: child, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 8), - child: Icon( - trailingIcon, - color: iconColor, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_body.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_body.dart new file mode 100644 index 0000000000..48003130cc --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_body.dart @@ -0,0 +1,48 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:flutter/material.dart'; + +class ChatDetailsFileRowBody extends StatelessWidget { + const ChatDetailsFileRowBody({ + super.key, + required this.child, + required this.trailingIcon, + required this.iconColor, + }); + + final IconData trailingIcon; + final Color iconColor; + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + padding: ChatDetailsFileTileStyle.bodyPadding, + height: ChatDetailsFileTileStyle.tileHeight, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ChatDetailsFileTileStyle.dividerHeight, + color: ChatDetailsFileTileStyle.dividerColor(context), + ), + ), + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: ChatDetailsFileTileStyle.bodyChildPadding, + child: child, + ), + ), + Padding( + padding: ChatDetailsFileTileStyle.trailingPadding, + child: Icon( + trailingIcon, + color: iconColor, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_downloading_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_downloading_web.dart new file mode 100644 index 0000000000..d7e45da4d1 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_downloading_web.dart @@ -0,0 +1,102 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_downloading_wrapper.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ChatDetailsFileTileRowDownloadingWeb extends StatelessWidget { + const ChatDetailsFileTileRowDownloadingWeb({ + super.key, + required this.mimeType, + required this.filename, + required this.sentDate, + required this.fileType, + required this.sizeString, + required this.onTap, + required this.downloadFileStateNotifier, + }); + + final GestureTapCallback onTap; + final TwakeMimeType mimeType; + final String filename; + final String? sizeString; + final String? fileType; + final DateTime sentDate; + final ValueNotifier downloadFileStateNotifier; + + @override + Widget build(BuildContext context) { + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: ChatDetailsFileRowDownloadingWrapper( + mimeType: mimeType, + fileType: fileType, + downloadFileStateNotifier: downloadFileStateNotifier, + child: Container( + padding: ChatDetailsFileTileStyle.bodyPaddingWeb, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ChatDetailsFileTileStyle.dividerHeight, + color: ChatDetailsFileTileStyle.dividerColor(context), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + filename, + style: ChatDetailsFileTileStyle.downloadFileTextStyle( + context, + ), + maxLines: + ChatDetailsFileTileStyle.downloadFilenameMaxLines, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: ChatDetailsFileTileStyle.trailingPadding, + child: Text( + sentDate.localizedTimeShort(context), + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + ), + ], + ), + SizedBox( + height: + ChatDetailsFileTileStyle.downloadingTileInformationPadding, + ), + if (sizeString != null) ...[ + TextInformationOfFile( + value: sizeString!, + style: ChatDetailsFileTileStyle.downloadSizeFileTextStyle( + context, + ), + downloadFileStateNotifier: downloadFileStateNotifier, + ), + SizedBox( + height: ChatDetailsFileTileStyle + .downloadingTileInformationPadding, + ), + ] else + SizedBox( + height: ChatDetailsFileTileStyle + .downloadingTileBottomPlaceholderWeb, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart index a59dbb13a3..21a7be1642 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_file_row_web.dart @@ -1,9 +1,10 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; -import 'package:fluffychat/widgets/file_widget/base_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class ChatDetailsFileTileRowWeb extends StatelessWidget { const ChatDetailsFileTileRowWeb({ @@ -11,73 +12,85 @@ class ChatDetailsFileTileRowWeb extends StatelessWidget { required this.mimeType, required this.filename, required this.sentDate, - required this.style, required this.fileType, required this.sizeString, + required this.onTap, + required this.isDownloaded, }); + final GestureTapCallback onTap; final TwakeMimeType mimeType; final String filename; final String? sizeString; final String? fileType; - final FileTileWidgetStyle style; final DateTime sentDate; + final bool isDownloaded; @override Widget build(BuildContext context) { - return Container( - padding: style.paddingFileTileAll, - decoration: ShapeDecoration( - color: style.backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: style.borderRadius, - ), - ), - child: Row( - crossAxisAlignment: style.crossAxisAlignment, - children: [ - SvgPicture.asset( - mimeType.getIcon(fileType: fileType), - width: style.iconSize, - height: style.iconSize, + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: ChatDetailsFileRowWrapper( + mimeType: mimeType, + fileType: fileType, + child: Container( + padding: ChatDetailsFileTileStyle.bodyPaddingWeb, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ChatDetailsFileTileStyle.dividerHeight, + color: ChatDetailsFileTileStyle.dividerColor(context), + ), + ), ), - style.paddingRightIcon, - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + child: Column( + children: [ + Row( children: [ - FileNameText( - filename: filename, - style: style, + Expanded( + child: Text( + filename, + style: ChatDetailsFileTileStyle.downloadFileTextStyle( + context, + ), + maxLines: + ChatDetailsFileTileStyle.downloadFilenameMaxLines, + overflow: TextOverflow.ellipsis, + ), ), - Row( - children: [ - if (sizeString != null) ...[ - Text( - sizeString!, - style: style.textInformationStyle( - context, - ), - ), - ], - ], + Padding( + padding: ChatDetailsFileTileStyle.trailingPadding, + child: Text( + sentDate.localizedTimeShort(context), + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), ), - style.paddingBottomText, ], ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Text( - sentDate.localizedTimeShort(context), - style: style.textInformationStyle(context), - ), + Row( + children: [ + if (sizeString != null) + Text( + sizeString!, + style: ChatDetailsFileTileStyle.downloadSizeFileTextStyle( + context, + ), + ), + if (!isDownloaded) ...[ + const Spacer(), + Icon( + Icons.download_outlined, + color: const FileTileWidgetStyle().fileInfoColor, + ), + ] else + const SizedBox(height: 24), + ], + ), + ], ), - ], + ), ), ); } diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_downloading_wrapper.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_downloading_wrapper.dart new file mode 100644 index 0000000000..955d963ec5 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_downloading_wrapper.dart @@ -0,0 +1,90 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/widgets/file_widget/circular_loading_download_widget.dart'; +import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; +import 'package:flutter/material.dart'; + +class ChatDetailsFileRowDownloadingWrapper extends StatelessWidget { + final Widget child; + final String? mimeType; + final String? fileType; + final ValueNotifier downloadFileStateNotifier; + + const ChatDetailsFileRowDownloadingWrapper({ + super.key, + required this.child, + required this.mimeType, + required this.fileType, + required this.downloadFileStateNotifier, + }); + + final style = const MessageFileTileStyle(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SizedBox( + width: ChatDetailsFileTileStyle.wrapperLeftPadding, + ), + ValueListenableBuilder( + valueListenable: downloadFileStateNotifier, + builder: (context, downloadFileState, child) { + double? downloadProgress; + if (downloadFileState is DownloadingPresentationState) { + if (downloadFileState.total == null || + downloadFileState.receive == null) { + downloadProgress = null; + } else { + downloadProgress = + downloadFileState.receive! / downloadFileState.total!; + } + } else if (downloadFileState is NotDownloadPresentationState) { + downloadProgress = 0; + } + return Stack( + alignment: Alignment.center, + children: [ + Container( + margin: style.marginDownloadIcon, + width: style.iconSize, + height: style.iconSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + ), + if (downloadProgress != 0) + SizedBox( + width: style.circularProgressLoadingSize, + height: style.circularProgressLoadingSize, + child: CircularLoadingDownloadWidget( + style: style, + downloadProgress: downloadProgress, + ), + ), + Container( + width: style.downloadIconSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Icon( + downloadProgress == 0 ? Icons.arrow_downward : Icons.close, + key: ValueKey(downloadProgress), + color: Theme.of(context).colorScheme.surface, + size: style.downloadIconSize, + ), + ), + ], + ); + }, + ), + ChatDetailsFileTileStyle().paddingRightIcon, + Expanded( + child: child, + ), + ], + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart new file mode 100644 index 0000000000..d31292b71e --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart @@ -0,0 +1,37 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:flutter/material.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ChatDetailsFileRowWrapper extends StatelessWidget { + final Widget child; + final String? mimeType; + final String? fileType; + + const ChatDetailsFileRowWrapper({ + super.key, + required this.child, + required this.mimeType, + required this.fileType, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SizedBox( + width: ChatDetailsFileTileStyle.wrapperLeftPadding, + ), + SvgPicture.asset( + mimeType.getIcon(fileType: fileType), + width: ChatDetailsFileTileStyle().iconSize, + height: ChatDetailsFileTileStyle().iconSize, + ), + ChatDetailsFileTileStyle().paddingRightIcon, + Expanded( + child: child, + ), + ], + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_downloading_tile.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_downloading_tile.dart new file mode 100644 index 0000000000..e37f764eb9 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_downloading_tile.dart @@ -0,0 +1,83 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_downloading_wrapper.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ChatDetailsDownloadingFileTile extends StatelessWidget { + const ChatDetailsDownloadingFileTile({ + super.key, + required this.onTap, + required this.filename, + required this.mimeType, + required this.fileType, + required this.sizeString, + required this.downloadFileStateNotifier, + }); + + final GestureTapCallback onTap; + final String filename; + final String? mimeType; + final String? fileType; + final String? sizeString; + final ValueNotifier downloadFileStateNotifier; + + @override + Widget build(BuildContext context) { + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: ChatDetailsFileRowDownloadingWrapper( + mimeType: mimeType, + fileType: fileType, + downloadFileStateNotifier: downloadFileStateNotifier, + child: Container( + height: ChatDetailsFileTileStyle.tileHeight, + padding: ChatDetailsFileTileStyle.bodyPadding, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ChatDetailsFileTileStyle.dividerHeight, + color: ChatDetailsFileTileStyle.dividerColor(context), + ), + ), + ), + child: Padding( + padding: ChatDetailsFileTileStyle.bodyChildPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + maxLines: ChatDetailsFileTileStyle.downloadFilenameMaxLines, + text: TextSpan( + text: filename, + style: ChatDetailsFileTileStyle.downloadFileTextStyle( + context, + ), + ), + overflow: TextOverflow.ellipsis, + ), + if (sizeString != null) + Expanded( + child: TextInformationOfFile( + value: sizeString!, + style: ChatDetailsFileTileStyle().textInformationStyle( + context, + ), + downloadFileStateNotifier: downloadFileStateNotifier, + ), + ) + else + SizedBox( + height: ChatDetailsFileTileStyle + .downloadingTileBottomPlaceholder, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_row_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_row_web.dart new file mode 100644 index 0000000000..1992d9e2d3 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_tile/chat_details_file_row_web.dart @@ -0,0 +1,98 @@ +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item/chat_details_files_item_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_row/chat_details_row_wrapper.dart'; +import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:fluffychat/widgets/file_widget/file_tile_widget_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; + +class ChatDetailsFileTileRowWeb extends StatelessWidget { + const ChatDetailsFileTileRowWeb({ + super.key, + required this.mimeType, + required this.filename, + required this.sentDate, + required this.fileType, + required this.sizeString, + required this.onTap, + required this.isDownloaded, + }); + + final GestureTapCallback onTap; + final TwakeMimeType mimeType; + final String filename; + final String? sizeString; + final String? fileType; + final DateTime sentDate; + final bool isDownloaded; + + @override + Widget build(BuildContext context) { + return InkWell( + hoverColor: LinagoraSysColors.material().surfaceVariant, + onTap: onTap, + child: ChatDetailsFileRowWrapper( + mimeType: mimeType, + fileType: fileType, + child: Container( + padding: ChatDetailsFileTileStyle.bodyPaddingWeb, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: ChatDetailsFileTileStyle.dividerHeight, + color: ChatDetailsFileTileStyle.dividerColor(context), + ), + ), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + filename, + style: ChatDetailsFileTileStyle.downloadFileTextStyle( + context, + ), + maxLines: + ChatDetailsFileTileStyle.downloadFilenameMaxLines, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: ChatDetailsFileTileStyle.trailingPadding, + child: Text( + sentDate.localizedTimeShort(context), + style: ChatDetailsFileTileStyle() + .textInformationStyle(context), + ), + ), + ], + ), + if (!isDownloaded) + Row( + children: [ + if (sizeString != null) + Text( + sizeString!, + style: + ChatDetailsFileTileStyle.downloadSizeFileTextStyle( + context, + ), + ), + const Spacer(), + Icon( + Icons.download_outlined, + color: const FileTileWidgetStyle().fileInfoColor, + ), + ], + ) + else + const SizedBox(height: 24), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/mixins/download_file_on_mobile_mixin.dart b/lib/widgets/mixins/download_file_on_mobile_mixin.dart index 95a4adbf2c..9ac60f12bc 100644 --- a/lib/widgets/mixins/download_file_on_mobile_mixin.dart +++ b/lib/widgets/mixins/download_file_on_mobile_mixin.dart @@ -74,11 +74,11 @@ mixin DownloadFileOnMobileMixin on State { } } - void _trySetupDownloadingStreamSubcription() { - streamSubscription = downloadManager - .getDownloadStateStream(event.eventId) - ?.listen(setupDownloadingProcess); - } + StreamSubscription>? + _trySetupDownloadingStreamSubcription() => + streamSubscription = downloadManager + .getDownloadStateStream(event.eventId) + ?.listen(setupDownloadingProcess); void setupDownloadingProcess(Either resultEvent) { resultEvent.fold( @@ -105,7 +105,7 @@ mixin DownloadFileOnMobileMixin on State { ); } - void onDownloadFileTap() async { + void _downloadFile() async { await checkFileInDownloadsInApp(); if (downloadFileStateNotifier.value is DownloadedPresentationState) { return; @@ -116,4 +116,12 @@ mixin DownloadFileOnMobileMixin on State { ); _trySetupDownloadingStreamSubcription(); } + + void onDownloadFileTap() async { + final streamSubscribtion = _trySetupDownloadingStreamSubcription(); + if (streamSubscribtion != null) { + return; + } + _downloadFile(); + } } diff --git a/lib/widgets/mixins/download_file_on_web_mixin.dart b/lib/widgets/mixins/download_file_on_web_mixin.dart index 7416dcfe5f..a58be0575c 100644 --- a/lib/widgets/mixins/download_file_on_web_mixin.dart +++ b/lib/widgets/mixins/download_file_on_web_mixin.dart @@ -27,17 +27,17 @@ mixin DownloadFileOnWebMixin on State { @override void initState() { super.initState(); - trySetupDownloadingStreamSubcription(); + _trySetupDownloadingStreamSubcription(); if (streamSubscription != null) { downloadFileStateNotifier.value = const DownloadingPresentationState(); } } - void trySetupDownloadingStreamSubcription() { - streamSubscription = downloadManager - .getDownloadStateStream(event.eventId) - ?.listen(setupDownloadingProcess); - } + StreamSubscription>? + _trySetupDownloadingStreamSubcription() => + streamSubscription = downloadManager + .getDownloadStateStream(event.eventId) + ?.listen(setupDownloadingProcess); void setupDownloadingProcess(Either resultEvent) { resultEvent.fold( @@ -77,6 +77,22 @@ mixin DownloadFileOnWebMixin on State { } } + void _downloadFile() async { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + downloadManager.download( + event: event, + ); + _trySetupDownloadingStreamSubcription(); + } + + void onDownloadFileTap() { + final streamSubscribtion = _trySetupDownloadingStreamSubcription(); + if (streamSubscribtion != null) { + return; + } + _downloadFile(); + } + @override void dispose() { downloadFileStateNotifier.dispose(); From 0913f5da2fc0f2eb4de8f7985994461175c3cf56 Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 22 Apr 2024 12:40:21 +0700 Subject: [PATCH 160/183] TW-1695: Add text (cherry picked from commit 888c0d6625b2db0fe19a3ec64cb8be69ea1502be) --- assets/l10n/intl_en.arb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index fe76a0e7a1..42b46a6a89 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3044,5 +3044,7 @@ } } } - } + }, + "dangerZone": "Danger zone", + "leaveGroupSubtitle": "This group will still remain after you left" } From 1b63616c834d4b0effe9e3abf96cd1ec58fe0e32 Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 22 Apr 2024 12:40:51 +0700 Subject: [PATCH 161/183] TW-1695: Add `leaveGroup` to chat actions (cherry picked from commit 95f67ab17bc70fa042d80580625a59add3ec9519) --- lib/pages/chat/chat_actions.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/chat_actions.dart b/lib/pages/chat/chat_actions.dart index 7b7db74dc0..bfb3acd6d2 100644 --- a/lib/pages/chat/chat_actions.dart +++ b/lib/pages/chat/chat_actions.dart @@ -78,7 +78,8 @@ enum ChatAppBarActions { info, report, saveToDownload, - saveToGallery; + saveToGallery, + leaveGroup; String getTitle(BuildContext context) { switch (this) { @@ -90,6 +91,8 @@ enum ChatAppBarActions { return L10n.of(context)!.saveToDownloads; case ChatAppBarActions.saveToGallery: return L10n.of(context)!.saveToGallery; + case ChatAppBarActions.leaveGroup: + return L10n.of(context)!.commandHint_leave; } } @@ -103,6 +106,8 @@ enum ChatAppBarActions { return Icons.download_outlined; case ChatAppBarActions.saveToGallery: return Icons.save_outlined; + case ChatAppBarActions.leaveGroup: + return Icons.logout_outlined; } } @@ -114,6 +119,8 @@ enum ChatAppBarActions { return Theme.of(context).colorScheme.onSurface; case ChatAppBarActions.report: return LinagoraSysColors.material().errorDark; + case ChatAppBarActions.leaveGroup: + return Theme.of(context).colorScheme.error; } } From dce05993a2e2abd986d4276c0b936d19096fcf38 Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 22 Apr 2024 12:41:22 +0700 Subject: [PATCH 162/183] TW-1695: Add option leave to chat app bar (cherry picked from commit 94795d958cca558deb2f08ae299e9104389bb640) --- lib/pages/chat/chat.dart | 30 ++++++++++++++++++------------ lib/pages/chat/chat_view.dart | 22 ++++++++++++++++------ 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 747f711416..400b826183 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1777,18 +1777,21 @@ class ChatController extends State } List> appBarActionsBuilder() { - final listAction = [ - if (PlatformInfos.isAndroid) ...[ - if (selectedEvents.length == 1 && - selectedEvents.first.hasAttachment && - !selectedEvents.first.isVideoOrImage) - ChatAppBarActions.saveToDownload, - ], - if (selectedEvents.length == 1 && selectedEvents.first.isVideoOrImage) - ChatAppBarActions.saveToGallery, - ChatAppBarActions.info, - ChatAppBarActions.report, - ]; + final listAction = selectMode + ? [ + if (PlatformInfos.isAndroid) ...[ + if (selectedEvents.length == 1 && + selectedEvents.first.hasAttachment && + !selectedEvents.first.isVideoOrImage) + ChatAppBarActions.saveToDownload, + ], + if (selectedEvents.length == 1 && + selectedEvents.first.isVideoOrImage) + ChatAppBarActions.saveToGallery, + ChatAppBarActions.info, + ChatAppBarActions.report, + ] + : [ChatAppBarActions.leaveGroup]; return listAction .map( (action) => PopupMenuItem( @@ -1839,6 +1842,9 @@ class ChatController extends State reportEventAction, ); break; + case ChatAppBarActions.leaveGroup: + leaveChat(); + break; default: break; } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 8828d91855..7a4642e307 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -141,12 +141,22 @@ class ChatView extends StatelessWidget with MessageContentMixin { if (!controller.selectMode) Padding( padding: ChatViewStyle.paddingTrailing(context), - child: IconButton( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - onPressed: controller.toggleSearch, - icon: const Icon(Icons.search), + child: Row( + children: [ + IconButton( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + onPressed: controller.toggleSearch, + icon: const Icon(Icons.search), + ), + if (!controller.room!.isDirectChat) + PopupMenuButton( + itemBuilder: (context) => + controller.appBarActionsBuilder(), + onSelected: controller.onSelectedAppBarActions, + ), + ], ), ), ], From a851cc9748da61f67031ea1fdd97018dfe39fc8d Mon Sep 17 00:00:00 2001 From: hieubt Date: Mon, 22 Apr 2024 12:41:38 +0700 Subject: [PATCH 163/183] TW-1695: Add option leave to chat edit (cherry picked from commit 20067d8f46c910dc4187de6e8a34708cc8d890f3) --- lib/pages/chat/chat.dart | 29 +++-- lib/pages/chat_details/chat_details_edit.dart | 26 +++++ .../chat_details_edit_option.dart | 107 ++++++++++++++++++ .../chat_details_edit_option_style.dart | 37 ++++++ .../chat_details/chat_details_edit_view.dart | 51 ++++++--- .../chat_details_edit_view_style.dart | 6 + lib/utils/exception/leave_room_exception.dart | 18 +++ 7 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 lib/pages/chat_details/chat_details_edit_option.dart create mode 100644 lib/pages/chat_details/chat_details_edit_option_style.dart create mode 100644 lib/utils/exception/leave_room_exception.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 400b826183..fb9aa50f72 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; import 'package:fluffychat/presentation/mixins/save_media_to_gallery_android_mixin.dart'; import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; import 'package:fluffychat/presentation/model/chat/view_event_list_ui_state.dart'; +import 'package:fluffychat/utils/exception/leave_room_exception.dart'; import 'package:fluffychat/utils/extension/basic_event_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; @@ -311,17 +312,27 @@ class ChatController extends State } Future leaveChat() async { - final room = this.room; - if (room == null) { - throw Exception( - 'Leave room button clicked while room is null. This should not be possible from the UI!', + try { + final room = this.room; + if (room == null) { + throw RoomNullException(); + } + + final success = await TwakeDialog.showFutureLoadingDialogFullScreen( + future: room.leave, + ); + + if (success.error != null) return; + context.go('/rooms'); + } on RoomNullException catch (e) { + Logs().e( + 'Chat::leaveChat() - RoomNullException - $e', + ); + } catch (e) { + Logs().e( + 'Chat::leaveChat() - error - $e', ); } - final success = await TwakeDialog.showFutureLoadingDialogFullScreen( - future: room.leave, - ); - if (success.error != null) return; - context.go('/rooms'); } EmojiPickerType emojiPickerType = EmojiPickerType.keyboard; diff --git a/lib/pages/chat_details/chat_details_edit.dart b/lib/pages/chat_details/chat_details_edit.dart index 54127a3669..0dbbd39187 100644 --- a/lib/pages/chat_details/chat_details_edit.dart +++ b/lib/pages/chat_details/chat_details_edit.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/pages/chat_details/chat_details_edit_view_style.dart' import 'package:fluffychat/presentation/mixins/common_media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/single_image_picker_mixin.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/exception/leave_room_exception.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -23,6 +24,7 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; import 'package:linagora_design_flutter/images_picker/images_picker_grid.dart'; import 'package:matrix/matrix.dart'; @@ -494,6 +496,30 @@ class ChatDetailsEditController extends State }); } + Future leaveChat() async { + try { + final currentRoom = room; + if (currentRoom == null) { + throw RoomNullException(); + } + + final success = await TwakeDialog.showFutureLoadingDialogFullScreen( + future: currentRoom.leave, + ); + + if (success.error != null) return; + context.go('/rooms'); + } on RoomNullException catch (e) { + Logs().e( + 'ChatDetailsEdit::leaveChat() - RoomNullException - $e', + ); + } catch (e) { + Logs().e( + 'ChatDetailsEdit::leaveChat() - error: $e', + ); + } + } + @override void initState() { room = Matrix.of(context).client.getRoomById(widget.roomId); diff --git a/lib/pages/chat_details/chat_details_edit_option.dart b/lib/pages/chat_details/chat_details_edit_option.dart new file mode 100644 index 0000000000..07752e0188 --- /dev/null +++ b/lib/pages/chat_details/chat_details_edit_option.dart @@ -0,0 +1,107 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_edit_option_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class ChatDetailsEditOption extends StatelessWidget { + final String title; + final String subtitle; + final IconData leading; + final VoidCallback onTap; + final double? leadingIconSize; + final double? trailingIconSize; + final Color? leadingIconColor; + final Color? titleColor; + final Color? subtitleColor; + + const ChatDetailsEditOption({ + super.key, + required this.title, + required this.subtitle, + required this.leading, + required this.onTap, + this.leadingIconSize, + this.trailingIconSize, + this.leadingIconColor, + this.titleColor, + this.subtitleColor, + }); + + @override + Widget build(BuildContext context) { + return Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + color: LinagoraSysColors.material().onPrimary, + child: InkWell( + onTap: onTap, + child: Padding( + padding: ChatDetailsEditOptionStyle.itemBuilderPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: ChatDetailsEditOptionStyle.leadingIconPadding, + child: Icon( + leading, + size: leadingIconSize ?? + ChatDetailsEditOptionStyle.defaultLeadingIconSize, + color: leadingIconColor ?? + ChatDetailsEditOptionStyle.defaultLeadingIconColor( + context, + ), + ), + ), + Expanded( + child: Row( + crossAxisAlignment: subtitle.isEmpty + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: ChatDetailsEditOptionStyle.titleTextStyle( + context, + titleColor, + ), + maxLines: ChatDetailsEditOptionStyle.titleMaxLines, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: ChatDetailsEditOptionStyle + .subtitleItemBuilderPadding, + child: Text( + subtitle, + style: + ChatDetailsEditOptionStyle.subtitleTextStyle( + context, + subtitleColor, + ), + maxLines: + ChatDetailsEditOptionStyle.subtitleMaxLines, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right_outlined, + size: trailingIconSize ?? + ChatDetailsEditOptionStyle.defaultTrailingIconSize, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_edit_option_style.dart b/lib/pages/chat_details/chat_details_edit_option_style.dart new file mode 100644 index 0000000000..a732545e17 --- /dev/null +++ b/lib/pages/chat_details/chat_details_edit_option_style.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; + +class ChatDetailsEditOptionStyle { + static const double defaultLeadingIconSize = 24.0; + static const double defaultTrailingIconSize = 24.0; + static const int titleMaxLines = 2; + static const int subtitleMaxLines = 3; + + static Color defaultLeadingIconColor(BuildContext context) { + return Theme.of(context).colorScheme.onSurfaceVariant; + } + + static const EdgeInsetsDirectional itemBuilderPadding = + EdgeInsetsDirectional.all(16.0); + static const EdgeInsetsDirectional leadingIconPadding = + EdgeInsetsDirectional.only(end: 8.0); + static const EdgeInsetsDirectional subtitleItemBuilderPadding = + EdgeInsetsDirectional.only(top: 4.0); + + static TextStyle? titleTextStyle(BuildContext context, Color? titleColor) { + return Theme.of(context).textTheme.titleMedium?.copyWith( + color: titleColor ?? Theme.of(context).colorScheme.onSurface, + fontSize: 16.0, + ); + } + + static TextStyle? subtitleTextStyle( + BuildContext context, + Color? subtitleColor, + ) { + return Theme.of(context).textTheme.bodySmall?.copyWith( + color: subtitleColor ?? LinagoraRefColors.material().neutral[40], + fontSize: 12.0, + ); + } +} diff --git a/lib/pages/chat_details/chat_details_edit_view.dart b/lib/pages/chat_details/chat_details_edit_view.dart index 4b233d8900..c0b147ca21 100644 --- a/lib/pages/chat_details/chat_details_edit_view.dart +++ b/lib/pages/chat_details/chat_details_edit_view.dart @@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/pages/chat_details/chat_details_edit.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_edit_option.dart'; import 'package:fluffychat/pages/chat_details/chat_details_edit_ui_state/upload_avatar_ui_state.dart'; import 'package:fluffychat/pages/chat_details/chat_details_edit_view_style.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -82,14 +83,14 @@ class ChatDetailsEditView extends StatelessWidget { ], ), ), - body: Padding( - padding: ChatDetailEditViewStyle.editAvatarPadding, - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - padding: EdgeInsets.zero, - child: Column( - children: [ - Center( + body: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.zero, + child: Column( + children: [ + Padding( + padding: ChatDetailEditViewStyle.editAvatarPadding, + child: Center( child: Stack( children: [ Padding( @@ -167,10 +168,13 @@ class ChatDetailsEditView extends StatelessWidget { ], ), ), - const SizedBox( - height: ChatDetailEditViewStyle.avatarAndTextFieldsGap, - ), - Column( + ), + const SizedBox( + height: ChatDetailEditViewStyle.avatarAndTextFieldsGap, + ), + Padding( + padding: ChatDetailEditViewStyle.editAvatarPadding, + child: Column( children: [ _GroupNameField(controller: controller), const SizedBox( @@ -179,8 +183,27 @@ class ChatDetailsEditView extends StatelessWidget { _DescriptionField(controller: controller), ], ), - ], - ), + ), + Container( + color: Theme.of(context).colorScheme.surfaceVariant, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(8.0), + child: Text( + L10n.of(context)!.dangerZone, + style: ChatDetailEditViewStyle.textChatDetailsEditCategoryStyle( + context, + ), + ), + ), + ChatDetailsEditOption( + title: L10n.of(context)!.commandHint_leave, + subtitle: L10n.of(context)!.leaveGroupSubtitle, + leading: Icons.logout_outlined, + titleColor: Theme.of(context).colorScheme.error, + leadingIconColor: Theme.of(context).colorScheme.error, + onTap: controller.leaveChat, + ), + ], ), ), ); diff --git a/lib/pages/chat_details/chat_details_edit_view_style.dart b/lib/pages/chat_details/chat_details_edit_view_style.dart index 3b745ef3d8..be06eee7df 100644 --- a/lib/pages/chat_details/chat_details_edit_view_style.dart +++ b/lib/pages/chat_details/chat_details_edit_view_style.dart @@ -55,6 +55,12 @@ class ChatDetailEditViewStyle { color: Colors.black, ); + static TextStyle? textChatDetailsEditCategoryStyle(BuildContext context) => + Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.tertiary, + fontSize: 14.0, + ); + static const double clearIconSize = 20.0; static const EdgeInsetsDirectional contentPadding = diff --git a/lib/utils/exception/leave_room_exception.dart b/lib/utils/exception/leave_room_exception.dart new file mode 100644 index 0000000000..8846789c03 --- /dev/null +++ b/lib/utils/exception/leave_room_exception.dart @@ -0,0 +1,18 @@ +class LeaveChatException implements Exception { + final dynamic error; + + LeaveChatException({ + this.error, + }); + + @override + String toString() => error; +} + +class RoomNullException extends LeaveChatException { + RoomNullException() + : super( + error: + 'Leave room button clicked while room is null. This should not be possible from the UI!', + ); +} From 805abcbb46dd05ee282d27bedfb0909a1cc81f19 Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 24 Apr 2024 00:46:05 +0700 Subject: [PATCH 164/183] TW-1695: Update twake context menu (cherry picked from commit 4ec8e59ada3e08820d8e7ada779feca571d3f66f) --- .../context_menu/context_menu_position.dart | 21 ++++ .../context_menu/twake_context_menu.dart | 105 ++++++++++++------ 2 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 lib/widgets/context_menu/context_menu_position.dart diff --git a/lib/widgets/context_menu/context_menu_position.dart b/lib/widgets/context_menu/context_menu_position.dart new file mode 100644 index 0000000000..f63a7a2305 --- /dev/null +++ b/lib/widgets/context_menu/context_menu_position.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; + +class ContextMenuPosition extends Equatable { + final Alignment alignment; + final double? left; + final double? top; + final double? right; + final double? bottom; + + const ContextMenuPosition({ + required this.alignment, + this.left, + this.top, + this.right, + this.bottom, + }); + + @override + List get props => [alignment, left, top, right, bottom]; +} diff --git a/lib/widgets/context_menu/twake_context_menu.dart b/lib/widgets/context_menu/twake_context_menu.dart index e1c4ecc300..38223f7b33 100644 --- a/lib/widgets/context_menu/twake_context_menu.dart +++ b/lib/widgets/context_menu/twake_context_menu.dart @@ -1,4 +1,6 @@ // reference to: https://pub.dev/packages/contextmenu +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_position.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_style.dart'; import 'package:flutter/material.dart'; import 'package:after_layout/after_layout.dart'; @@ -65,31 +67,7 @@ class TwakeContextMenuState extends State @override Widget build(BuildContext context) { final children = widget.builder(context); - - double height = 2 * - (widget.verticalPadding ?? - TwakeContextMenuStyle.defaultVerticalPadding); - - for (final element in _heights.values) { - height += element; - } - - final heightsNotAvailable = children.length - _heights.length; - height += heightsNotAvailable * _kMinTileHeight; - - if (height > MediaQuery.sizeOf(context).height) { - height = MediaQuery.sizeOf(context).height; - } - - final double positionLeft = widget.position.dx; - double positionTop = widget.position.dy; - double positionBottom = - MediaQuery.sizeOf(context).height - widget.position.dy - height; - - if (positionBottom < 0) { - positionTop += positionBottom; - positionBottom = 0; - } + final contextMenuPosition = _calculatePosition(children); return GestureDetector( onTap: () => closeContextMenu(), @@ -102,15 +80,16 @@ class TwakeContextMenuState extends State child: Stack( children: [ Positioned( - left: positionLeft, - top: positionTop, - bottom: positionBottom, + left: contextMenuPosition.left, + top: contextMenuPosition.top, + bottom: contextMenuPosition.bottom, + right: contextMenuPosition.right, child: AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.scale( scale: _animation.value, - alignment: Alignment.topLeft, + alignment: contextMenuPosition.alignment, child: Opacity( opacity: _animation.value, child: child, @@ -147,13 +126,19 @@ class TwakeContextMenuState extends State crossAxisAlignment: CrossAxisAlignment.start, children: children .map( - (e) => _GrowingWidget( - child: e, - onHeightChange: (height) { - setState(() { - _heights[ValueKey(e)] = height; - }); - }, + (e) => Listener( + onPointerDown: (_) => + PlatformInfos.isMobile + ? closeContextMenu() + : null, + child: _GrowingWidget( + child: e, + onHeightChange: (height) { + setState(() { + _heights[ValueKey(e)] = height; + }); + }, + ), ), ) .toList(), @@ -178,6 +163,54 @@ class TwakeContextMenuState extends State .reverse() .whenComplete(() => Navigator.of(context).pop()); } + + ContextMenuPosition _calculatePosition(List children) { + double height = 2 * + (widget.verticalPadding ?? + TwakeContextMenuStyle.defaultVerticalPadding); + for (final element in _heights.values) { + height += element; + } + + final heightsNotAvailable = children.length - _heights.length; + height += heightsNotAvailable * _kMinTileHeight; + + if (height > MediaQuery.sizeOf(context).height) { + height = MediaQuery.sizeOf(context).height; + } + + double positionTop = widget.position.dy - + MediaQueryData.fromView(View.of(context)).viewPadding.top; + double positionBottom = + MediaQuery.sizeOf(context).height - widget.position.dy - height; + + if (positionBottom < 0) { + positionTop += positionBottom; + positionBottom = 0; + } + + final double positionLeftTap = widget.position.dx; + final double screenWidth = MediaQuery.sizeOf(context).width; + final double availableRightSpace = screenWidth - positionLeftTap; + double? positionLeft; + double? positionRight; + Alignment alignment = Alignment.topLeft; + + if (availableRightSpace < TwakeContextMenuStyle.menuMaxWidth) { + positionRight = screenWidth - positionLeftTap; + alignment = Alignment.topRight; + } else { + positionLeft = positionLeftTap; + } + + return ContextMenuPosition( + alignment: alignment, + left: positionLeft, + top: positionTop, + right: positionRight, + bottom: positionBottom, + ); + } } class _GrowingWidget extends StatefulWidget { From 03c7efe5372fc2be308c92936c6d76ca99f99a36 Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 24 Apr 2024 00:46:41 +0700 Subject: [PATCH 165/183] TW-1695: Change app bar menu follow message context menu (cherry picked from commit eafe0be17a214f966bce1c064cc109bc4f0d8136) --- lib/pages/chat/chat.dart | 53 +++++++++++-------- lib/pages/chat/chat_view.dart | 27 +++++++--- lib/pages/chat_details/chat_details_edit.dart | 4 +- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index fb9aa50f72..5624fc0f80 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/utils/exception/leave_room_exception.dart'; import 'package:fluffychat/utils/extension/basic_event_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; +import 'package:fluffychat/widgets/mixins/popup_menu_widget_style.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:fluffychat/utils/extension/global_key_extension.dart'; @@ -318,11 +319,11 @@ class ChatController extends State throw RoomNullException(); } - final success = await TwakeDialog.showFutureLoadingDialogFullScreen( + final result = await TwakeDialog.showFutureLoadingDialogFullScreen( future: room.leave, ); - if (success.error != null) return; + if (result.error != null) return; context.go('/rooms'); } on RoomNullException catch (e) { Logs().e( @@ -1787,7 +1788,19 @@ class ChatController extends State } } - List> appBarActionsBuilder() { + void handleAppbarMenuAction( + BuildContext context, + TapDownDetails tapDownDetails, + ) { + final offset = tapDownDetails.globalPosition; + showTwakeContextMenu( + offset: offset, + context: context, + builder: (_) => _appbarMenuActionTile(context), + ); + } + + List _appbarMenuActionTile(BuildContext context) { final listAction = selectMode ? [ if (PlatformInfos.isAndroid) ...[ @@ -1803,26 +1816,20 @@ class ChatController extends State ChatAppBarActions.report, ] : [ChatAppBarActions.leaveGroup]; - return listAction - .map( - (action) => PopupMenuItem( - value: action, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - action.getIcon(), - color: action.getColorIcon(context), - ), - Padding( - padding: action.getPaddingTitle(), - child: Text(action.getTitle(context)), - ), - ], - ), - ), - ) - .toList(); + return listAction.map((action) { + return popupItemByTwakeAppRouter( + context, + action.getTitle(context), + iconAction: action.getIcon(), + colorIcon: action.getColorIcon(context), + styleName: action == ChatAppBarActions.leaveGroup + ? PopupMenuWidgetStyle.defaultItemTextStyle(context)?.copyWith( + color: action.getColorIcon(context), + ) + : null, + onCallbackAction: () => onSelectedAppBarActions(action), + ); + }).toList(); } void onSelectedAppBarActions(ChatAppBarActions action) { diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 7a4642e307..d2b0b17b91 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,6 +1,5 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_invitation_body.dart'; import 'package:fluffychat/pages/chat/chat_view_body.dart'; @@ -64,9 +63,14 @@ class ChatView extends StatelessWidget with MessageContentMixin { imageSize: ChatViewStyle.appBarIconSize, ), if (controller.selectedEvents.length == 1) - PopupMenuButton( - onSelected: controller.onSelectedAppBarActions, - itemBuilder: (context) => controller.appBarActionsBuilder(), + TwakeIconButton( + icon: Icons.more_vert, + tooltip: L10n.of(context)!.more, + onTapDown: (tapDownDetails) => controller.handleAppbarMenuAction( + context, + tapDownDetails, + ), + preferBelow: false, ), ], ); @@ -151,10 +155,17 @@ class ChatView extends StatelessWidget with MessageContentMixin { icon: const Icon(Icons.search), ), if (!controller.room!.isDirectChat) - PopupMenuButton( - itemBuilder: (context) => - controller.appBarActionsBuilder(), - onSelected: controller.onSelectedAppBarActions, + Builder( + builder: (context) => TwakeIconButton( + icon: Icons.more_vert, + tooltip: L10n.of(context)!.more, + onTapDown: (tapDownDetails) => + controller.handleAppbarMenuAction( + context, + tapDownDetails, + ), + preferBelow: false, + ), ), ], ), diff --git a/lib/pages/chat_details/chat_details_edit.dart b/lib/pages/chat_details/chat_details_edit.dart index 0dbbd39187..caaf7b9088 100644 --- a/lib/pages/chat_details/chat_details_edit.dart +++ b/lib/pages/chat_details/chat_details_edit.dart @@ -503,11 +503,11 @@ class ChatDetailsEditController extends State throw RoomNullException(); } - final success = await TwakeDialog.showFutureLoadingDialogFullScreen( + final result = await TwakeDialog.showFutureLoadingDialogFullScreen( future: currentRoom.leave, ); - if (success.error != null) return; + if (result.error != null) return; context.go('/rooms'); } on RoomNullException catch (e) { Logs().e( From 2cb69700f6f499c07500fcf1796f88b756e658dc Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 24 Apr 2024 21:41:59 +0700 Subject: [PATCH 166/183] TW-1695: Separate `leaveChat` function into a mixin (cherry picked from commit 223dcbdc897e7a29eff6635b2edba6a40d7d47d6) --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat/chat.dart | 83 +++++++++---------- lib/pages/chat/chat_view_body.dart | 5 +- lib/pages/chat_details/chat_details_edit.dart | 33 ++------ .../chat_details/chat_details_edit_view.dart | 2 +- lib/presentation/mixins/leave_chat_mixin.dart | 31 +++++++ 6 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 lib/presentation/mixins/leave_chat_mixin.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 42b46a6a89..babcc54973 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3046,5 +3046,6 @@ } }, "dangerZone": "Danger zone", - "leaveGroupSubtitle": "This group will still remain after you left" + "leaveGroupSubtitle": "This group will still remain after you left", + "leaveChatFailed": "Failed to leave the chat" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 5624fc0f80..bd5d9ad3f3 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -4,11 +4,11 @@ import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; import 'package:fluffychat/presentation/extensions/event_update_extension.dart'; import 'package:fluffychat/presentation/mixins/handle_clipboard_action_mixin.dart'; +import 'package:fluffychat/presentation/mixins/leave_chat_mixin.dart'; import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; import 'package:fluffychat/presentation/mixins/save_media_to_gallery_android_mixin.dart'; import 'package:fluffychat/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart'; import 'package:fluffychat/presentation/model/chat/view_event_list_ui_state.dart'; -import 'package:fluffychat/utils/exception/leave_room_exception.dart'; import 'package:fluffychat/utils/extension/basic_event_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; @@ -111,7 +111,8 @@ class ChatController extends State TwakeContextMenuMixin, MessageContentMixin, SaveFileToTwakeAndroidDownloadsFolderMixin, - SaveMediaToGalleryAndroidMixin { + SaveMediaToGalleryAndroidMixin, + LeaveChatMixin { final NetworkConnectionService networkConnectionService = getIt.get(); @@ -312,30 +313,6 @@ class ChatController extends State context.go('/rooms/$roomId'); } - Future leaveChat() async { - try { - final room = this.room; - if (room == null) { - throw RoomNullException(); - } - - final result = await TwakeDialog.showFutureLoadingDialogFullScreen( - future: room.leave, - ); - - if (result.error != null) return; - context.go('/rooms'); - } on RoomNullException catch (e) { - Logs().e( - 'Chat::leaveChat() - RoomNullException - $e', - ); - } catch (e) { - Logs().e( - 'Chat::leaveChat() - error - $e', - ); - } - } - EmojiPickerType emojiPickerType = EmojiPickerType.keyboard; Future requestHistory({ @@ -1535,7 +1512,7 @@ class ChatController extends State case DialogRejectInviteResult.cancel: return; case DialogRejectInviteResult.reject: - await leaveChat(); + await leaveChat(context, room); return; } } @@ -1796,26 +1773,42 @@ class ChatController extends State showTwakeContextMenu( offset: offset, context: context, - builder: (_) => _appbarMenuActionTile(context), + builder: (_) => + _appbarMenuActionTile(context, _getListActionAppBarMenu()), ); } - List _appbarMenuActionTile(BuildContext context) { - final listAction = selectMode - ? [ - if (PlatformInfos.isAndroid) ...[ - if (selectedEvents.length == 1 && - selectedEvents.first.hasAttachment && - !selectedEvents.first.isVideoOrImage) - ChatAppBarActions.saveToDownload, - ], - if (selectedEvents.length == 1 && - selectedEvents.first.isVideoOrImage) - ChatAppBarActions.saveToGallery, - ChatAppBarActions.info, - ChatAppBarActions.report, - ] - : [ChatAppBarActions.leaveGroup]; + List _getListActionAppBarMenu() { + return selectMode + ? _getListActionAppbarMenuSelectMode() + : _getListActionAppbarMenuNormal(); + } + + List _getListActionAppbarMenuSelectMode() { + return [ + if (PlatformInfos.isAndroid) ...[ + if (selectedEvents.length == 1 && + selectedEvents.first.hasAttachment && + !selectedEvents.first.isVideoOrImage) + ChatAppBarActions.saveToDownload, + ], + if (selectedEvents.length == 1 && selectedEvents.first.isVideoOrImage) + ChatAppBarActions.saveToGallery, + ChatAppBarActions.info, + ChatAppBarActions.report, + ]; + } + + List _getListActionAppbarMenuNormal() { + return [ + ChatAppBarActions.leaveGroup, + ]; + } + + List _appbarMenuActionTile( + BuildContext context, + List listAction, + ) { return listAction.map((action) { return popupItemByTwakeAppRouter( context, @@ -1861,7 +1854,7 @@ class ChatController extends State ); break; case ChatAppBarActions.leaveGroup: - leaveChat(); + leaveChat(context, room); break; default: break; diff --git a/lib/pages/chat/chat_view_body.dart b/lib/pages/chat/chat_view_body.dart index 287b1b9b22..6d281d0bcf 100644 --- a/lib/pages/chat/chat_view_body.dart +++ b/lib/pages/chat/chat_view_body.dart @@ -108,7 +108,10 @@ class ChatViewBody extends StatelessWidget with MessageContentMixin { icon: const Icon( Icons.archive_outlined, ), - onPressed: controller.leaveChat, + onPressed: () => controller.leaveChat( + context, + controller.room, + ), label: Text( L10n.of(context)!.leave, ), diff --git a/lib/pages/chat_details/chat_details_edit.dart b/lib/pages/chat_details/chat_details_edit.dart index caaf7b9088..ef44388ddb 100644 --- a/lib/pages/chat_details/chat_details_edit.dart +++ b/lib/pages/chat_details/chat_details_edit.dart @@ -14,9 +14,9 @@ import 'package:fluffychat/pages/chat_details/chat_details_edit_ui_state/upload_ import 'package:fluffychat/pages/chat_details/chat_details_edit_view.dart'; import 'package:fluffychat/pages/chat_details/chat_details_edit_view_style.dart'; import 'package:fluffychat/presentation/mixins/common_media_picker_mixin.dart'; +import 'package:fluffychat/presentation/mixins/leave_chat_mixin.dart'; import 'package:fluffychat/presentation/mixins/single_image_picker_mixin.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/exception/leave_room_exception.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -24,7 +24,6 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; import 'package:linagora_design_flutter/images_picker/images_picker_grid.dart'; import 'package:matrix/matrix.dart'; @@ -44,7 +43,11 @@ class ChatDetailsEdit extends StatefulWidget { } class ChatDetailsEditController extends State - with PopupMenuWidgetMixin, CommonMediaPickerMixin, SingleImagePickerMixin { + with + PopupMenuWidgetMixin, + CommonMediaPickerMixin, + SingleImagePickerMixin, + LeaveChatMixin { final updateGroupChatInteractor = getIt.get(); final uploadContentInteractor = getIt.get(); @@ -496,30 +499,6 @@ class ChatDetailsEditController extends State }); } - Future leaveChat() async { - try { - final currentRoom = room; - if (currentRoom == null) { - throw RoomNullException(); - } - - final result = await TwakeDialog.showFutureLoadingDialogFullScreen( - future: currentRoom.leave, - ); - - if (result.error != null) return; - context.go('/rooms'); - } on RoomNullException catch (e) { - Logs().e( - 'ChatDetailsEdit::leaveChat() - RoomNullException - $e', - ); - } catch (e) { - Logs().e( - 'ChatDetailsEdit::leaveChat() - error: $e', - ); - } - } - @override void initState() { room = Matrix.of(context).client.getRoomById(widget.roomId); diff --git a/lib/pages/chat_details/chat_details_edit_view.dart b/lib/pages/chat_details/chat_details_edit_view.dart index c0b147ca21..61572242d5 100644 --- a/lib/pages/chat_details/chat_details_edit_view.dart +++ b/lib/pages/chat_details/chat_details_edit_view.dart @@ -201,7 +201,7 @@ class ChatDetailsEditView extends StatelessWidget { leading: Icons.logout_outlined, titleColor: Theme.of(context).colorScheme.error, leadingIconColor: Theme.of(context).colorScheme.error, - onTap: controller.leaveChat, + onTap: () => controller.leaveChat(context, controller.room), ), ], ), diff --git a/lib/presentation/mixins/leave_chat_mixin.dart b/lib/presentation/mixins/leave_chat_mixin.dart new file mode 100644 index 0000000000..7df781d489 --- /dev/null +++ b/lib/presentation/mixins/leave_chat_mixin.dart @@ -0,0 +1,31 @@ +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/exception/leave_room_exception.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +mixin LeaveChatMixin { + Future leaveChat(BuildContext context, Room? room) async { + try { + if (room == null) { + throw RoomNullException(); + } + + final result = await TwakeDialog.showFutureLoadingDialogFullScreen( + future: room.leave, + ); + + if (result.error != null) return; + + context.go('/rooms'); + } on RoomNullException catch (e) { + Logs().e('LeaveChatMixin::leaveChat(): - RoomNullException - $e'); + TwakeSnackBar.show(context, L10n.of(context)!.leaveChatFailed); + } catch (e) { + Logs().e('LeaveChatMixin::leaveChat(): - error: $e'); + TwakeSnackBar.show(context, L10n.of(context)!.leaveChatFailed); + } + } +} From bb3e5d24932dab1f7e38a1c6028f4841832bff6b Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 22 Apr 2024 11:37:25 +0700 Subject: [PATCH 167/183] TW-1728: handle token not found for registration on web (cherry picked from commit 4d78475aa5bc706108b1f3fe014edb25249c200f) --- assets/l10n/intl_en.arb | 1 + .../auto_homeserver_picker.dart | 28 +++++++++---------- .../auto_homeserver_picker_state.dart | 16 +++++++---- .../auto_homeserver_picker_view.dart | 3 +- lib/pages/twake_welcome/twake_welcome.dart | 3 +- .../mixins/connect_page_mixin.dart | 16 +++++++---- .../exception/check_homeserver_exception.dart | 10 ------- lib/utils/exception/homeserver_exception.dart | 21 ++++++++++++++ 8 files changed, 60 insertions(+), 38 deletions(-) delete mode 100644 lib/utils/exception/check_homeserver_exception.dart create mode 100644 lib/utils/exception/homeserver_exception.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index babcc54973..6ce72f7b90 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3045,6 +3045,7 @@ } } }, + "tokenNotFound": "The login token not found", "dangerZone": "Danger zone", "leaveGroupSubtitle": "This group will still remain after you left", "leaveChatFailed": "Failed to leave the chat" diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart index 690f91314e..49463bdc2b 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart @@ -3,7 +3,7 @@ import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_s import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart'; import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/presentation/mixins/init_config_mixin.dart'; -import 'package:fluffychat/utils/exception/check_homeserver_exception.dart'; +import 'package:fluffychat/utils/exception/homeserver_exception.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -131,20 +131,18 @@ class AutoHomeserverPickerController extends State ); final identitiesProvider = identityProviders(rawLoginTypes: rawLoginTypes); if (identitiesProvider?.length == 1) { - registerPublicPlatformAction( - context: context, - id: identitiesProvider!.single.id!, - saasRegistrationErrorCallback: (object) { - Logs().e( - "AutoHomeserverPickerController: _saasAutoRegistration: Error - $object", - ); - }, - saasRegistrationTimeoutCallback: () { - Logs().e( - "AutoHomeserverPickerController: _saasAutoRegistration: Timeout", - ); - }, - ); + try { + await registerPublicPlatformAction( + context: context, + id: identitiesProvider!.single.id!, + ); + } on HomeserverTokenNotFoundException catch (e) { + autoHomeserverPickerUIState.value = AutoHomeServerPickerFailureState( + error: e.toString(), + ); + } catch (e) { + autoHomeserverPickerUIState.value = AutoHomeServerPickerFailureState(); + } } } diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart index fd717a6afb..2afc984a74 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_state.dart @@ -2,25 +2,31 @@ import 'package:equatable/equatable.dart'; abstract class AutoHomeServerPickerState with EquatableMixin { @override - List get props => []; + List get props => []; } class AutoHomeServerPickerInitialState extends AutoHomeServerPickerState { @override - List get props => []; + List get props => []; } class AutoHomeServerPickerLoadingState extends AutoHomeServerPickerState { @override - List get props => []; + List get props => []; } class AutoHomeServerPickerSuccessState extends AutoHomeServerPickerState { @override - List get props => []; + List get props => []; } class AutoHomeServerPickerFailureState extends AutoHomeServerPickerState { + final String? error; + + AutoHomeServerPickerFailureState({ + this.error, + }); + @override - List get props => []; + List get props => [error]; } diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart index c9bba9d0a9..ff77820a98 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart @@ -108,7 +108,8 @@ class AutoHomeserverPickerView extends StatelessWidget { ), const SizedBox(height: 16), Text( - L10n.of(context)!.configurationNotFound, + state.error ?? + L10n.of(context)!.configurationNotFound, style: Theme.of(context) .textTheme .titleMedium diff --git a/lib/pages/twake_welcome/twake_welcome.dart b/lib/pages/twake_welcome/twake_welcome.dart index a091123c57..e178511390 100644 --- a/lib/pages/twake_welcome/twake_welcome.dart +++ b/lib/pages/twake_welcome/twake_welcome.dart @@ -75,8 +75,9 @@ class TwakeWelcomeController extends State with ConnectPageMixin { intentFlags: ephemeralIntentFlags, ), ); + final token = Uri.parse(uri).queryParameters['loginToken']; Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); - handleTokenFromRegistrationSite(matrix: matrix, uri: uri); + handleTokenFromRegistrationSite(matrix: matrix, token: token); } void onClickCreateTwakeId() { diff --git a/lib/presentation/mixins/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart index f28b16ed9c..a1ccc35c10 100644 --- a/lib/presentation/mixins/connect_page_mixin.dart +++ b/lib/presentation/mixins/connect_page_mixin.dart @@ -4,11 +4,12 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; import 'package:fluffychat/pages/connect/connect_page.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/exception/check_homeserver_exception.dart'; +import 'package:fluffychat/utils/exception/homeserver_exception.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:matrix/matrix.dart'; @@ -169,8 +170,6 @@ mixin ConnectPageMixin { Future registerPublicPlatformAction({ required BuildContext context, required String id, - OnSAASRegistrationTimeoutCallback? saasRegistrationTimeoutCallback, - OnSAASRegistrationErrorCallback? saasRegistrationErrorCallback, }) async { final redirectUrl = _generateRedirectUrl( Matrix.of(context).client.homeserver.toString(), @@ -190,9 +189,15 @@ mixin ConnectPageMixin { ), ); Logs().d("ConnectPageMixin:_redirectRegistrationUrl: URI - $uri"); + final token = Uri.parse(uri).queryParameters['loginToken']; + if (token == null) { + throw HomeserverTokenNotFoundException( + error: L10n.of(context)!.tokenNotFound, + ); + } handleTokenFromRegistrationSite( matrix: Matrix.of(context), - uri: uri, + token: token, ); } @@ -239,9 +244,8 @@ mixin ConnectPageMixin { void handleTokenFromRegistrationSite({ required MatrixState matrix, - required String uri, + required String? token, }) async { - final token = Uri.parse(uri).queryParameters['loginToken']; Logs().d( "ConnectPageMixin: handleTokenFromRegistrationSite: token: $token", ); diff --git a/lib/utils/exception/check_homeserver_exception.dart b/lib/utils/exception/check_homeserver_exception.dart deleted file mode 100644 index 08e3a10e61..0000000000 --- a/lib/utils/exception/check_homeserver_exception.dart +++ /dev/null @@ -1,10 +0,0 @@ -class CheckHomeserverTimeoutException implements Exception { - final dynamic error; - - CheckHomeserverTimeoutException({ - this.error, - }); - - @override - String toString() => error; -} diff --git a/lib/utils/exception/homeserver_exception.dart b/lib/utils/exception/homeserver_exception.dart new file mode 100644 index 0000000000..a1ba282468 --- /dev/null +++ b/lib/utils/exception/homeserver_exception.dart @@ -0,0 +1,21 @@ +class CheckHomeserverTimeoutException implements Exception { + final dynamic error; + + CheckHomeserverTimeoutException({ + this.error, + }); + + @override + String toString() => error; +} + +class HomeserverTokenNotFoundException implements Exception { + final dynamic error; + + HomeserverTokenNotFoundException({ + this.error, + }); + + @override + String toString() => error; +} From 6a0f3f3fa211a481c7f585ae42a206474954cbbb Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 24 Apr 2024 16:03:41 +0700 Subject: [PATCH 168/183] TW-1728: Fix can't logout on web (cherry picked from commit 2cdf44c70887f9ad44b749b9727e283c3e16e328) --- lib/pages/login/on_auth_redirect.dart | 4 ++-- .../settings_dashboard/settings/settings.dart | 4 +++- lib/pages/twake_welcome/twake_welcome.dart | 3 +-- lib/presentation/mixins/connect_page_mixin.dart | 14 ++------------ 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/lib/pages/login/on_auth_redirect.dart b/lib/pages/login/on_auth_redirect.dart index 15503cb6b6..c5ac278701 100644 --- a/lib/pages/login/on_auth_redirect.dart +++ b/lib/pages/login/on_auth_redirect.dart @@ -1,8 +1,8 @@ import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/cupertino.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; import 'package:flutter/material.dart'; @@ -61,7 +61,7 @@ class _OnAuthRedirectState extends State { ); } catch (e) { Logs().e('tryLoggingUsingToken::error: $e'); - context.go('/home'); + TwakeApp.router.go('/home', extra: true); } } diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 8658b3253b..4a56c4edfd 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -87,8 +87,10 @@ class SettingsController extends State with ConnectPageMixin { return; } final matrix = Matrix.of(context); - if (matrix.twakeSupported == true) { + if (PlatformInfos.isMobile) { await tryLogoutSso(context); + } + if (matrix.twakeSupported == true) { final hiveCollectionToMDatabase = getIt.get(); await hiveCollectionToMDatabase.clear(); } diff --git a/lib/pages/twake_welcome/twake_welcome.dart b/lib/pages/twake_welcome/twake_welcome.dart index e178511390..a091123c57 100644 --- a/lib/pages/twake_welcome/twake_welcome.dart +++ b/lib/pages/twake_welcome/twake_welcome.dart @@ -75,9 +75,8 @@ class TwakeWelcomeController extends State with ConnectPageMixin { intentFlags: ephemeralIntentFlags, ), ); - final token = Uri.parse(uri).queryParameters['loginToken']; Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); - handleTokenFromRegistrationSite(matrix: matrix, token: token); + handleTokenFromRegistrationSite(matrix: matrix, uri: uri); } void onClickCreateTwakeId() { diff --git a/lib/presentation/mixins/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart index a1ccc35c10..25bbd74b7a 100644 --- a/lib/presentation/mixins/connect_page_mixin.dart +++ b/lib/presentation/mixins/connect_page_mixin.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:matrix/matrix.dart'; @@ -189,16 +188,6 @@ mixin ConnectPageMixin { ), ); Logs().d("ConnectPageMixin:_redirectRegistrationUrl: URI - $uri"); - final token = Uri.parse(uri).queryParameters['loginToken']; - if (token == null) { - throw HomeserverTokenNotFoundException( - error: L10n.of(context)!.tokenNotFound, - ); - } - handleTokenFromRegistrationSite( - matrix: Matrix.of(context), - token: token, - ); } String _generatePostLogoutRedirectUrl() { @@ -244,8 +233,9 @@ mixin ConnectPageMixin { void handleTokenFromRegistrationSite({ required MatrixState matrix, - required String? token, + required String uri, }) async { + final token = Uri.parse(uri).queryParameters['loginToken']; Logs().d( "ConnectPageMixin: handleTokenFromRegistrationSite: token: $token", ); From 1e5586545dfae47ac93617e52e175da56094a177 Mon Sep 17 00:00:00 2001 From: --global Date: Fri, 19 Apr 2024 12:49:28 +0700 Subject: [PATCH 169/183] TW-1685: refactor the code by using mixin and reuse EventVideoPlayer (cherry picked from commit 53b82918f150467e12e36405a5e439173f273946) --- lib/pages/chat/events/event_video_player.dart | 27 +++-- .../events/message_download_content_web.dart | 1 + .../media/chat_details_media_page.dart | 5 +- ...nload_file_from_queue_in_mobile_mixin.dart | 113 ++++++++++++++++++ ...download_file_from_queue_in_web_mixin.dart | 63 ++++++++++ 5 files changed, 199 insertions(+), 10 deletions(-) create mode 100644 lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart create mode 100644 lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart diff --git a/lib/pages/chat/events/event_video_player.dart b/lib/pages/chat/events/event_video_player.dart index 6e7efb7de3..aa17e214bc 100644 --- a/lib/pages/chat/events/event_video_player.dart +++ b/lib/pages/chat/events/event_video_player.dart @@ -39,10 +39,12 @@ class EventVideoPlayer extends StatelessWidget { /// Enable it if the thumbnail image is stretched, and you don't want to resize it final bool noResizeThumbnail; - final bool showPlayButton; - final VoidCallback? onPop; + final VoidCallback? onVideoTapped; + + final Widget centerWidget; + static final responsiveUtils = getIt.get(); const EventVideoPlayer( @@ -55,8 +57,9 @@ class EventVideoPlayer extends StatelessWidget { this.thumbnailCacheMap, this.thumbnailCacheKey, this.noResizeThumbnail = false, - this.showPlayButton = true, this.onPop, + this.onVideoTapped, + this.centerWidget = const CenterVideoButton(icon: Icons.play_arrow), }) : super(key: key); @override @@ -74,7 +77,13 @@ class EventVideoPlayer extends StatelessWidget { color: Colors.black, child: InkWell( mouseCursor: SystemMouseCursors.click, - onTap: () => _onTapVideo(context), + onTap: () { + if (onVideoTapped != null) { + onVideoTapped!.call(); + } else { + _onTapVideo(context); + } + }, child: SizedBox( width: MessageContentStyle.imageBubbleWidth(imageWidth), height: MessageContentStyle.videoBubbleHeight(imageHeight), @@ -97,10 +106,7 @@ class EventVideoPlayer extends StatelessWidget { isPreview: false, ), ), - if (showPlayButton) - const CenterVideoButton( - icon: Icons.play_arrow, - ), + centerWidget, if (showDuration) Positioned( bottom: ChatDetailsMediaStyle.durationPaddingAll(context), @@ -148,9 +154,12 @@ class EventVideoPlayer extends StatelessWidget { class CenterVideoButton extends StatelessWidget { final IconData icon; + final double? iconSize; + const CenterVideoButton({ super.key, required this.icon, + this.iconSize, }); @override @@ -166,7 +175,7 @@ class CenterVideoButton extends StatelessWidget { child: Icon( icon, color: LinagoraRefColors.material().primary[100], - size: MessageContentStyle.iconInsideVideoButtonSize, + size: iconSize ?? MessageContentStyle.iconInsideVideoButtonSize, ), ); } diff --git a/lib/pages/chat/events/message_download_content_web.dart b/lib/pages/chat/events/message_download_content_web.dart index 2d5803a250..155c65b6c1 100644 --- a/lib/pages/chat/events/message_download_content_web.dart +++ b/lib/pages/chat/events/message_download_content_web.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/widgets/file_widget/message_file_tile_style.dart'; import 'package:fluffychat/widgets/mixins/download_file_on_web_mixin.dart'; import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; import 'package:fluffychat/widgets/twake_app.dart'; + import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart index 71cfb0d67b..334fee3281 100644 --- a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart @@ -107,6 +107,9 @@ class _VideoItem extends StatelessWidget { @override Widget build(BuildContext context) { + final centerVideoWidget = !responsiveUtil.isDesktop(context) + ? const CenterVideoButton(icon: Icons.play_arrow) + : const SizedBox.shrink(); return EventVideoPlayer( event, rounded: false, @@ -115,7 +118,7 @@ class _VideoItem extends StatelessWidget { thumbnailCacheMap: thumbnailCacheMap, noResizeThumbnail: true, onPop: closeRightColumn, - showPlayButton: !responsiveUtil.isDesktop(context), + centerWidget: centerVideoWidget, ); } } diff --git a/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart b/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart new file mode 100644 index 0000000000..a7fa5426a0 --- /dev/null +++ b/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart @@ -0,0 +1,113 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +typedef OnDownloadedFileDone = void Function(String filePath); + +mixin HandleDownloadFileFromQueueInMobileMixin { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + void onDownloadedFileDone(String filePath) { + Logs().i( + 'HandleDownloadFileFromQueueInMobileMixin::onDownloadedFile(): $filePath', + ); + } + + StreamSubscription>? streamSubscription; + + void checkDownloadFileState(Event event) async { + checkFileExistInMemory(event); + await checkFileInDownloadsInApp(event); + + trySetupDownloadingStreamSubcription(event); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } + + void checkFileExistInMemory(Event event) { + final filePathInMem = event.getFilePathFromMem(); + if (filePathInMem?.isNotEmpty == true) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePathInMem!, + ); + return; + } + } + + Future checkFileInDownloadsInApp(Event event) async { + final filePath = + await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + eventId: event.eventId, + fileName: event.filename, + ); + final file = File(filePath); + if (await file.exists() && await file.length() == event.getFileSize()) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: filePath, + ); + return; + } + } + + void trySetupDownloadingStreamSubcription(Event event) { + streamSubscription = downloadManager + .getDownloadStateStream(event.eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess( + Either event, + ) { + event.fold( + (failure) { + Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + streamSubscription?.cancel(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadNativeFileSuccessState) { + downloadFileStateNotifier.value = DownloadedPresentationState( + filePath: success.filePath, + ); + onDownloadedFileDone.call(success.filePath); + } + }, + ); + } + + void onDownloadFileTapped(Event event) async { + await checkFileInDownloadsInApp(event); + if (downloadFileStateNotifier.value is DownloadedPresentationState) { + return; + } + downloadFileStateNotifier.value = const DownloadingPresentationState(); + downloadManager.download( + event: event, + ); + trySetupDownloadingStreamSubcription(event); + } +} diff --git a/lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart b/lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart new file mode 100644 index 0000000000..46d45e30b7 --- /dev/null +++ b/lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart' hide State, OpenFile; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:matrix/matrix.dart'; + +mixin HandleDownloadFileFromQueueInWebMixin { + final downloadManager = getIt.get(); + + final downloadFileStateNotifier = ValueNotifier( + const NotDownloadPresentationState(), + ); + + StreamSubscription>? streamSubscription; + + void handleDownloadMatrixFileSuccessDone({ + required DownloadMatrixFileSuccessState success, + }) { + Logs().i('MessageDownloadContent::handleDownloadMatrixFileSuccessDone()'); + } + + void trySetupDownloadingStreamSubcription(String eventId) { + streamSubscription = downloadManager + .getDownloadStateStream(eventId) + ?.listen(setupDownloadingProcess); + } + + void setupDownloadingProcess(Either event) { + event.fold( + (failure) { + Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); + downloadFileStateNotifier.value = const NotDownloadPresentationState(); + }, + (success) { + if (success is DownloadingFileState) { + if (success.total != 0) { + downloadFileStateNotifier.value = DownloadingPresentationState( + receive: success.receive, + total: success.total, + ); + } + } else if (success is DownloadMatrixFileSuccessState) { + handleDownloadMatrixFileSuccessDone(success: success); + } + }, + ); + } + + void onDownloadFileTapped(Event event) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + downloadManager.download( + event: event, + ); + trySetupDownloadingStreamSubcription(event.eventId); + } +} From d7cdcceb9b1732837f1624e8295e80d6c6c3b2cc Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 29 Apr 2024 09:45:17 +0700 Subject: [PATCH 170/183] TW-1685: change from bytes to stream when downloading files web (cherry picked from commit 92bbb6efea2b18074b6c4a9b2813bacb43741c95) --- lib/data/network/media/media_api.dart | 9 +++--- .../matrix_file_extension.dart | 32 +++---------------- lib/utils/stream_list_int_extension.dart | 31 ++++++++++++++++++ 3 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 lib/utils/stream_list_int_extension.dart diff --git a/lib/data/network/media/media_api.dart b/lib/data/network/media/media_api.dart index 77acd34047..3320653db7 100644 --- a/lib/data/network/media/media_api.dart +++ b/lib/data/network/media/media_api.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:fluffychat/data/model/media/download_file_response.dart'; @@ -80,18 +79,18 @@ class MediaAPI { ); } - Future downloadFileWeb({ + Future>> downloadFileWeb({ required Uri uri, CancelToken? cancelToken, ProgressCallback? onReceiveProgress, }) async { - final uint8List = await _client + final response = await _client .get( uri.path, onReceiveProgress: onReceiveProgress, cancelToken: cancelToken, options: Options( - responseType: ResponseType.bytes, + responseType: ResponseType.stream, ), ) .onError((error, stackTrace) { @@ -102,7 +101,7 @@ class MediaAPI { } }); - return uint8List; + return response.stream; } Future getUrlPreview({ diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 9f429344fd..1930cbd93c 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/utils/stream_list_int_extension.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; @@ -111,41 +112,16 @@ extension MatrixFileExtension on MatrixFile { if (bytes != null || readStream == null) { return this; } - return MatrixFile( - bytes: await _streamToUint8List(readStream!), + bytes: await streamToUint8List(readStream!), name: name, mimeType: mimeType, filePath: filePath, ).detectFileType; } - Future _streamToUint8List(Stream> stream) async { - var byteData = ByteData(0); - var length = 0; - - await for (final chunk in stream) { - final chunkLength = chunk.length; - final newLength = length + chunkLength; - - if (newLength > byteData.lengthInBytes) { - final newByteData = ByteData(newLength); - - for (var i = 0; i < length; i++) { - newByteData.setUint8(i, byteData.getUint8(i)); - } - - byteData = newByteData; - } - - for (var i = 0; i < chunkLength; i++) { - byteData.setUint8(length + i, chunk[i]); - } - - length = newLength; - } - - return Uint8List.view(byteData.buffer, 0, length); + Future streamToUint8List(Stream> stream) async { + return await stream.toUint8List(); } bool get isFileHaveThumbnail => diff --git a/lib/utils/stream_list_int_extension.dart b/lib/utils/stream_list_int_extension.dart new file mode 100644 index 0000000000..38fbe1f5e7 --- /dev/null +++ b/lib/utils/stream_list_int_extension.dart @@ -0,0 +1,31 @@ +import 'dart:typed_data'; + +extension StreamListIntExtension on Stream> { + Future toUint8List() async { + var byteData = ByteData(0); + var length = 0; + + await for (final chunk in this) { + final chunkLength = chunk.length; + final newLength = length + chunkLength; + + if (newLength > byteData.lengthInBytes) { + final newByteData = ByteData(newLength); + + for (var i = 0; i < length; i++) { + newByteData.setUint8(i, byteData.getUint8(i)); + } + + byteData = newByteData; + } + + for (var i = 0; i < chunkLength; i++) { + byteData.setUint8(length + i, chunk[i]); + } + + length = newLength; + } + + return Uint8List.view(byteData.buffer, 0, length); + } +} From 4dd2cdd12bca88f8b591c0273e16828c668d3f8b Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 29 Apr 2024 09:46:51 +0700 Subject: [PATCH 171/183] TW-1685: refactor UI and mixin to work in video media type (cherry picked from commit 145ff9cd6c27ad56d0fd0db29b7cd8957134e1d7) --- lib/pages/chat/events/event_video_player.dart | 34 ----- lib/pages/chat/events/message_content.dart | 14 +- .../chat/events/message_content_style.dart | 4 + .../events/message_download_content_web.dart | 27 +++- .../message_video_download_content.dart | 109 ++++++++++++++ .../message_video_download_content_web.dart | 134 ++++++++++++++++++ .../chat_details_files_item_web.dart | 9 +- .../media/chat_details_media_page.dart | 27 +++- ...nload_file_from_queue_in_mobile_mixin.dart | 4 +- .../download_file_web_extension.dart | 14 +- .../event_extension.dart | 9 +- .../mixins/download_file_on_web_mixin.dart | 9 +- ...andle_download_and_preview_file_mixin.dart | 18 ++- 13 files changed, 348 insertions(+), 64 deletions(-) create mode 100644 lib/pages/chat/events/message_video_download_content.dart create mode 100644 lib/pages/chat/events/message_video_download_content_web.dart diff --git a/lib/pages/chat/events/event_video_player.dart b/lib/pages/chat/events/event_video_player.dart index aa17e214bc..f87faf3dde 100644 --- a/lib/pages/chat/events/event_video_player.dart +++ b/lib/pages/chat/events/event_video_player.dart @@ -1,14 +1,7 @@ import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/pages/chat/events/download_video_widget.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart'; -import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; -import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/responsive/responsive_utils.dart'; -import 'package:fluffychat/widgets/hero_page_route.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; @@ -39,14 +32,10 @@ class EventVideoPlayer extends StatelessWidget { /// Enable it if the thumbnail image is stretched, and you don't want to resize it final bool noResizeThumbnail; - final VoidCallback? onPop; - final VoidCallback? onVideoTapped; final Widget centerWidget; - static final responsiveUtils = getIt.get(); - const EventVideoPlayer( this.event, { Key? key, @@ -57,7 +46,6 @@ class EventVideoPlayer extends StatelessWidget { this.thumbnailCacheMap, this.thumbnailCacheKey, this.noResizeThumbnail = false, - this.onPop, this.onVideoTapped, this.centerWidget = const CenterVideoButton(icon: Icons.play_arrow), }) : super(key: key); @@ -80,8 +68,6 @@ class EventVideoPlayer extends StatelessWidget { onTap: () { if (onVideoTapped != null) { onVideoTapped!.call(); - } else { - _onTapVideo(context); } }, child: SizedBox( @@ -129,26 +115,6 @@ class EventVideoPlayer extends StatelessWidget { ), ); } - - Future _onTapVideo(BuildContext context) async { - final result = await Navigator.of( - context, - rootNavigator: PlatformInfos.isWeb, - ).push( - HeroPageRoute( - builder: (context) { - return InteractiveViewerGallery( - itemBuilder: DownloadVideoWidget( - event: event, - ), - ); - }, - ), - ); - if (result == MediaViewerPopupResultEnum.closeRightColumnFlag) { - onPop?.call(); - } - } } class CenterVideoButton extends StatelessWidget { diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 806b29189c..9e367c14d8 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,10 +1,11 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/call_invite_content.dart'; import 'package:fluffychat/pages/chat/events/encrypted_content.dart'; -import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/pages/chat/events/message_download_content_web.dart'; import 'package:fluffychat/pages/chat/events/formatted_text_widget.dart'; +import 'package:fluffychat/pages/chat/events/message_video_download_content.dart'; +import 'package:fluffychat/pages/chat/events/message_video_download_content_web.dart'; import 'package:fluffychat/pages/chat/events/redacted_content.dart'; import 'package:fluffychat/pages/chat/events/sending_image_info_widget.dart'; import 'package:fluffychat/pages/chat/events/sending_video_widget.dart'; @@ -357,8 +358,15 @@ class _MessageVideoBuilder extends StatelessWidget { displayImageInfo: displayImageInfo, ); } - return EventVideoPlayer( - event, + if (PlatformInfos.isWeb) { + return MessageVideoDownloadContentWeb( + event: event, + width: displayImageInfo.size.width, + height: displayImageInfo.size.height, + ); + } + return MessageVideoDownloadContent( + event: event, width: displayImageInfo.size.width, height: displayImageInfo.size.height, ); diff --git a/lib/pages/chat/events/message_content_style.dart b/lib/pages/chat/events/message_content_style.dart index 0d1549208f..e12a46d20a 100644 --- a/lib/pages/chat/events/message_content_style.dart +++ b/lib/pages/chat/events/message_content_style.dart @@ -59,6 +59,10 @@ class MessageContentStyle { static const double iconInsideVideoButtonSize = 48; + static const double cancelButtonSize = 28; + + static const double downloadButtonSize = 32; + static const double strokeVideoWidth = 2; static const Color backgroundColorCenterButton = Colors.black38; diff --git a/lib/pages/chat/events/message_download_content_web.dart b/lib/pages/chat/events/message_download_content_web.dart index 155c65b6c1..56261c388a 100644 --- a/lib/pages/chat/events/message_download_content_web.dart +++ b/lib/pages/chat/events/message_download_content_web.dart @@ -1,6 +1,5 @@ -import 'dart:async'; - import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/file_widget/download_file_tile_widget.dart'; import 'package:fluffychat/widgets/file_widget/file_tile_widget.dart'; @@ -35,10 +34,31 @@ class _MessageDownloadContentWebState extends State Event get event => widget.event; @override - Future get handlePreview => handlePreviewWeb( + void handleDownloadMatrixFileSuccessDone({ + required DownloadMatrixFileSuccessState success, + }) { + streamSubscription?.cancel(); + if (mounted) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + downloadFileStateNotifier.dispose(); + handlePreviewWeb( + event: widget.event, + matrixFile: success.matrixFile, + context: context, + ); + return; + } + + if (TwakeApp.routerKey.currentContext != null) { + handlePreviewWeb( + matrixFile: success.matrixFile, event: widget.event, context: TwakeApp.routerKey.currentContext!, ); + } + } @override Widget build(BuildContext context) { @@ -69,6 +89,7 @@ class _MessageDownloadContentWebState extends State return InkWell( onTap: () { handlePreviewWeb( + matrixFile: state.matrixFile, event: widget.event, context: context, ); diff --git a/lib/pages/chat/events/message_video_download_content.dart b/lib/pages/chat/events/message_video_download_content.dart new file mode 100644 index 0000000000..cd6825f636 --- /dev/null +++ b/lib/pages/chat/events/message_video_download_content.dart @@ -0,0 +1,109 @@ +import 'package:fluffychat/pages/chat/events/event_video_player.dart'; +import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart'; +import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:matrix/matrix.dart'; + +class MessageVideoDownloadContent extends StatefulWidget { + const MessageVideoDownloadContent({ + super.key, + required this.event, + required this.width, + required this.height, + }); + + final Event event; + + final double width; + + final double height; + + @override + State createState() => _MessageVideoDownloadContentState(); +} + +class _MessageVideoDownloadContentState + extends State + with HandleDownloadFileFromQueueInMobileMixin, PlayVideoActionMixin { + @override + void initState() { + super.initState(); + checkDownloadFileState(widget.event); + } + + @override + void dispose() { + streamSubscription?.cancel(); + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + @override + void onDownloadedFileDone(String filePath) { + super.onDownloadedFileDone(filePath); + streamSubscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + return EventVideoPlayer( + widget.event, + width: widget.width, + height: widget.height, + centerWidget: ValueListenableBuilder( + valueListenable: downloadFileStateNotifier, + builder: (context, downloadState, child) { + if (downloadState is DownloadingPresentationState) { + double? progress; + if (downloadState.total != null && + downloadState.receive != null && + downloadState.receive != 0) { + progress = downloadState.receive! / downloadState.total!; + return Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: MessageContentStyle.videoCenterButtonSize, + height: MessageContentStyle.videoCenterButtonSize, + child: CircularProgressIndicator( + value: progress, + color: LinagoraRefColors.material().primary[100], + strokeWidth: MessageContentStyle.strokeVideoWidth, + ), + ), + const CenterVideoButton( + icon: Icons.close, + iconSize: MessageContentStyle.cancelButtonSize, + ), + ], + ); + } + } else if (downloadState is NotDownloadPresentationState) { + return const CenterVideoButton( + icon: Icons.arrow_downward, + iconSize: MessageContentStyle.downloadButtonSize, + ); + } + return const CenterVideoButton(icon: Icons.play_arrow); + }, + ), + onVideoTapped: () { + final downloadState = downloadFileStateNotifier.value; + if (downloadState is DownloadingPresentationState) { + downloadManager.cancelDownload(widget.event.eventId); + } else if (downloadState is NotDownloadPresentationState) { + onDownloadFileTapped(widget.event); + } else if (downloadState is DownloadedPresentationState) { + playVideoAction( + context, + downloadState.filePath, + isReplacement: false, + ); + } + }, + ); + } +} diff --git a/lib/pages/chat/events/message_video_download_content_web.dart b/lib/pages/chat/events/message_video_download_content_web.dart new file mode 100644 index 0000000000..669614a1bd --- /dev/null +++ b/lib/pages/chat/events/message_video_download_content_web.dart @@ -0,0 +1,134 @@ +import 'package:fluffychat/pages/chat/events/event_video_player.dart'; +import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart'; +import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; +import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/utils/extension/web_url_creation_extension.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:matrix/matrix.dart'; + +class MessageVideoDownloadContentWeb extends StatefulWidget { + const MessageVideoDownloadContentWeb({ + super.key, + required this.event, + required this.width, + required this.height, + }); + + final Event event; + + final double width; + + final double height; + + @override + State createState() => _MessageVideoDownloadContentWebState(); +} + +class _MessageVideoDownloadContentWebState + extends State + with HandleDownloadFileFromQueueInWebMixin, PlayVideoActionMixin { + @override + void initState() { + super.initState(); + trySetupDownloadingStreamSubcription(widget.event.eventId); + if (streamSubscription != null) { + downloadFileStateNotifier.value = const DownloadingPresentationState(); + } + } + + @override + void dispose() { + downloadFileStateNotifier.dispose(); + super.dispose(); + } + + @override + void handleDownloadMatrixFileSuccessDone({ + required DownloadMatrixFileSuccessState success, + }) { + if (mounted) { + downloadFileStateNotifier.value = FileWebDownloadedPresentationState( + matrixFile: success.matrixFile, + ); + downloadFileStateNotifier.dispose(); + } + + super.handleDownloadMatrixFileSuccessDone(success: success); + streamSubscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: downloadFileStateNotifier, + builder: ((context, downloadState, child) { + if (downloadState is DownloadingPresentationState) { + double? progress; + if (downloadState.total != null && + downloadState.receive != null && + downloadState.receive != 0) { + progress = downloadState.receive! / downloadState.total!; + } + return EventVideoPlayer( + widget.event, + width: widget.width, + height: widget.height, + onVideoTapped: () { + downloadManager.cancelDownload(widget.event.eventId); + }, + centerWidget: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: MessageContentStyle.videoCenterButtonSize, + height: MessageContentStyle.videoCenterButtonSize, + child: CircularProgressIndicator( + value: progress, + color: LinagoraRefColors.material().primary[100], + strokeWidth: MessageContentStyle.strokeVideoWidth, + ), + ), + const CenterVideoButton( + icon: Icons.close, + iconSize: MessageContentStyle.cancelButtonSize, + ), + ], + ), + ); + } else if (downloadState is NotDownloadPresentationState) { + return EventVideoPlayer( + widget.event, + width: widget.width, + height: widget.height, + onVideoTapped: () async { + onDownloadFileTapped(widget.event); + }, + centerWidget: const CenterVideoButton( + icon: Icons.arrow_downward, + iconSize: MessageContentStyle.downloadButtonSize, + ), + ); + } + return EventVideoPlayer( + widget.event, + width: widget.width, + height: widget.height, + onVideoTapped: () async { + if (downloadState is FileWebDownloadedPresentationState) { + playVideoAction( + context, + downloadState.matrixFile.bytes!.toWebUrl( + mimeType: downloadState.matrixFile.mimeType, + ), + isReplacement: false, + ); + } + }, + ); + }), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart index a367255964..21e936a804 100644 --- a/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart +++ b/lib/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_web.dart @@ -1,6 +1,5 @@ -import 'dart:async'; - import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_item_web/chat_details_files_item_view_web.dart'; +import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/widgets/mixins/download_file_on_web_mixin.dart'; import 'package:fluffychat/widgets/mixins/handle_download_and_preview_file_mixin.dart'; import 'package:fluffychat/widgets/twake_app.dart'; @@ -24,8 +23,12 @@ class ChatDetailsFileItemWebState extends State Event get event => widget.event; @override - Future get handlePreview => handlePreviewWeb( + void handleDownloadMatrixFileSuccessDone({ + required DownloadMatrixFileSuccessState success, + }) => + handlePreviewWeb( event: widget.event, + matrixFile: success.matrixFile, context: TwakeApp.routerKey.currentContext!, ); diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart index 334fee3281..1ca9f2278d 100644 --- a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart @@ -12,6 +12,11 @@ import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/pages/chat/events/download_video_widget.dart'; +import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; +import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/hero_page_route.dart'; class ChatDetailsMediaPage extends StatelessWidget { final SameTypeEventsBuilderController controller; @@ -117,8 +122,28 @@ class _VideoItem extends StatelessWidget { thumbnailCacheKey: event.eventId, thumbnailCacheMap: thumbnailCacheMap, noResizeThumbnail: true, - onPop: closeRightColumn, centerWidget: centerVideoWidget, + onVideoTapped: () => _onTapVideo(context), ); } + + Future _onTapVideo(BuildContext context) async { + final result = await Navigator.of( + context, + rootNavigator: PlatformInfos.isWeb, + ).push( + HeroPageRoute( + builder: (context) { + return InteractiveViewerGallery( + itemBuilder: DownloadVideoWidget( + event: event, + ), + ); + }, + ), + ); + if (result == MediaViewerPopupResultEnum.closeRightColumnFlag) { + closeRightColumn?.call(); + } + } } diff --git a/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart b/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart index a7fa5426a0..ee6d3f3e32 100644 --- a/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart +++ b/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart @@ -8,9 +8,9 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; +import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -53,7 +53,7 @@ mixin HandleDownloadFileFromQueueInMobileMixin { Future checkFileInDownloadsInApp(Event event) async { final filePath = - await StorageDirectoryUtils.instance.getFilePathInAppDownloads( + await StorageDirectoryManager.instance.getFilePathInAppDownloads( eventId: event.eventId, fileName: event.filename, ); diff --git a/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart index 242d6aafcf..6cf9173abf 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_web_extension.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/exception/download_file_web_exception.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/stream_list_int_extension.dart'; import 'package:matrix/matrix.dart'; extension DownloadFileWebExtension on Event { @@ -80,7 +81,7 @@ extension DownloadFileWebExtension on Event { final database = room.client.database; final mediaAPI = getIt(); final downloadLink = mxcUrl.getDownloadLink(room.client); - final uint8List = await mediaAPI.downloadFileWeb( + final stream = await mediaAPI.downloadFileWeb( uri: downloadLink, onReceiveProgress: (receive, total) { downloadStreamController.add( @@ -94,9 +95,10 @@ extension DownloadFileWebExtension on Event { }, cancelToken: cancelToken, ); + final uint8List = await stream.toUint8List(); if (database != null && storeable && - uint8List.lengthInBytes < database.maxFileSize) { + uint8List.length < database.maxFileSize) { await database.storeEventFile( eventId, filename, @@ -109,7 +111,7 @@ extension DownloadFileWebExtension on Event { uint8List, downloadStreamController, ); - return MatrixFile(name: body); + return MatrixFile(bytes: uint8List, name: body); } catch (e) { if (e is CancelRequestException) { Logs().i("_handleDownloadFileWeb: user cancel the download"); @@ -125,19 +127,19 @@ extension DownloadFileWebExtension on Event { } Future _handleDownloadFileWebSuccess( - Uint8List uint8list, + Uint8List uint8List, StreamController> streamController, ) async { if (isAttachmentEncrypted) { await _handleDecryptedFileWeb( streamController: streamController, - uint8list: uint8list, + uint8list: uint8List, ); } else { streamController.add( Right( DownloadMatrixFileSuccessState( - matrixFile: MatrixFile(bytes: uint8list, name: body), + matrixFile: MatrixFile(bytes: uint8List, name: body), ), ), ); diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 12c790d3ce..9c78247ab3 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -28,10 +28,13 @@ extension LocalizedBody on Event { future: downloadAndDecryptAttachment, ); - Future saveFile(BuildContext context) async { - final matrixFile = await getFile(context); + Future saveFile( + BuildContext context, { + MatrixFile? matrixFile, + }) async { + matrixFile ??= (await getFile(context)).result; - return await matrixFile.result?.downloadFile(context); + return await matrixFile?.downloadFile(context); } String get filenameEllipsized { diff --git a/lib/widgets/mixins/download_file_on_web_mixin.dart b/lib/widgets/mixins/download_file_on_web_mixin.dart index a58be0575c..9532d762da 100644 --- a/lib/widgets/mixins/download_file_on_web_mixin.dart +++ b/lib/widgets/mixins/download_file_on_web_mixin.dart @@ -22,7 +22,9 @@ mixin DownloadFileOnWebMixin on State { Event get event; - Future get handlePreview; + void handleDownloadMatrixFileSuccessDone({ + required DownloadMatrixFileSuccessState success, + }); @override void initState() { @@ -68,12 +70,13 @@ mixin DownloadFileOnWebMixin on State { downloadFileStateNotifier.value = FileWebDownloadedPresentationState( matrixFile: success.matrixFile, ); - handlePreview; + downloadFileStateNotifier.dispose(); + handleDownloadMatrixFileSuccessDone(success: success); return; } if (TwakeApp.routerKey.currentContext != null) { - handlePreview; + handleDownloadMatrixFileSuccessDone(success: success); } } diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index e116987eb2..6af3307dfc 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -107,6 +107,7 @@ mixin HandleDownloadAndPreviewFileMixin { } Future handlePreviewWeb({ + MatrixFile? matrixFile, required Event event, required BuildContext context, }) async { @@ -116,10 +117,10 @@ mixin HandleDownloadAndPreviewFileMixin { } if (event.mimeType.isPdfFile()) { - return await previewPdfWeb(context, event); + return await previewPdfWeb(context, event, matrixFile: matrixFile); } - await event.saveFile(context); + await event.saveFile(context, matrixFile: matrixFile); } void _handleDownloadFileForPreviewMobile({ @@ -212,15 +213,20 @@ mixin HandleDownloadAndPreviewFileMixin { ); } - Future previewPdfWeb(BuildContext context, Event event) async { - final pdf = await event.getFile(context); - if (pdf.result == null || event.sizeString != pdf.result?.sizeString) { + Future previewPdfWeb( + BuildContext context, + Event event, { + MatrixFile? matrixFile, + }) async { + matrixFile ??= (await event.getFile(context)).result; + + if (matrixFile == null || event.sizeString != matrixFile.sizeString) { TwakeSnackBar.show(context, L10n.of(context)!.errorGettingPdf); return; } - final blob = html.Blob([pdf.result!.bytes], 'application/pdf'); + final blob = html.Blob([matrixFile.bytes], 'application/pdf'); final url = html.Url.createObjectUrlFromBlob(blob); html.window.open(url, "_blank"); html.Url.revokeObjectUrl(url); From cbf461b8275fa67aa2d4d23e3c5f8cdff9e02458 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 29 Apr 2024 11:03:14 +0700 Subject: [PATCH 172/183] TW-1685: rebase to use one mixin only (cherry picked from commit fb241869f4b5d9d8b2413f9b678534dcd8d219cd) --- .../message_video_download_content.dart | 60 ++++------ .../message_video_download_content_web.dart | 23 +--- ...nload_file_from_queue_in_mobile_mixin.dart | 113 ------------------ ...download_file_from_queue_in_web_mixin.dart | 63 ---------- .../mixins/download_file_on_mobile_mixin.dart | 6 + 5 files changed, 37 insertions(+), 228 deletions(-) delete mode 100644 lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart delete mode 100644 lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart diff --git a/lib/pages/chat/events/message_video_download_content.dart b/lib/pages/chat/events/message_video_download_content.dart index cd6825f636..7a86b790d0 100644 --- a/lib/pages/chat/events/message_video_download_content.dart +++ b/lib/pages/chat/events/message_video_download_content.dart @@ -1,8 +1,8 @@ import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; -import 'package:fluffychat/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; +import 'package:fluffychat/widgets/mixins/download_file_on_mobile_mixin.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:matrix/matrix.dart'; @@ -27,19 +27,11 @@ class MessageVideoDownloadContent extends StatefulWidget { class _MessageVideoDownloadContentState extends State - with HandleDownloadFileFromQueueInMobileMixin, PlayVideoActionMixin { + with + DownloadFileOnMobileMixin, + PlayVideoActionMixin { @override - void initState() { - super.initState(); - checkDownloadFileState(widget.event); - } - - @override - void dispose() { - streamSubscription?.cancel(); - downloadFileStateNotifier.dispose(); - super.dispose(); - } + Event get event => widget.event; @override void onDownloadedFileDone(String filePath) { @@ -58,29 +50,27 @@ class _MessageVideoDownloadContentState builder: (context, downloadState, child) { if (downloadState is DownloadingPresentationState) { double? progress; - if (downloadState.total != null && - downloadState.receive != null && - downloadState.receive != 0) { + if (downloadState.total != null && downloadState.total! > 0) { progress = downloadState.receive! / downloadState.total!; - return Stack( - alignment: Alignment.center, - children: [ - SizedBox( - width: MessageContentStyle.videoCenterButtonSize, - height: MessageContentStyle.videoCenterButtonSize, - child: CircularProgressIndicator( - value: progress, - color: LinagoraRefColors.material().primary[100], - strokeWidth: MessageContentStyle.strokeVideoWidth, - ), - ), - const CenterVideoButton( - icon: Icons.close, - iconSize: MessageContentStyle.cancelButtonSize, - ), - ], - ); } + return Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: MessageContentStyle.videoCenterButtonSize, + height: MessageContentStyle.videoCenterButtonSize, + child: CircularProgressIndicator( + value: progress, + color: LinagoraRefColors.material().primary[100], + strokeWidth: MessageContentStyle.strokeVideoWidth, + ), + ), + const CenterVideoButton( + icon: Icons.close, + iconSize: MessageContentStyle.cancelButtonSize, + ), + ], + ); } else if (downloadState is NotDownloadPresentationState) { return const CenterVideoButton( icon: Icons.arrow_downward, @@ -95,7 +85,7 @@ class _MessageVideoDownloadContentState if (downloadState is DownloadingPresentationState) { downloadManager.cancelDownload(widget.event.eventId); } else if (downloadState is NotDownloadPresentationState) { - onDownloadFileTapped(widget.event); + onDownloadFileTap(); } else if (downloadState is DownloadedPresentationState) { playVideoAction( context, diff --git a/lib/pages/chat/events/message_video_download_content_web.dart b/lib/pages/chat/events/message_video_download_content_web.dart index 669614a1bd..bf1aca2757 100644 --- a/lib/pages/chat/events/message_video_download_content_web.dart +++ b/lib/pages/chat/events/message_video_download_content_web.dart @@ -1,10 +1,10 @@ import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; -import 'package:fluffychat/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; import 'package:fluffychat/utils/extension/web_url_creation_extension.dart'; import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; +import 'package:fluffychat/widgets/mixins/download_file_on_web_mixin.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:matrix/matrix.dart'; @@ -29,21 +29,11 @@ class MessageVideoDownloadContentWeb extends StatefulWidget { class _MessageVideoDownloadContentWebState extends State - with HandleDownloadFileFromQueueInWebMixin, PlayVideoActionMixin { + with + DownloadFileOnWebMixin, + PlayVideoActionMixin { @override - void initState() { - super.initState(); - trySetupDownloadingStreamSubcription(widget.event.eventId); - if (streamSubscription != null) { - downloadFileStateNotifier.value = const DownloadingPresentationState(); - } - } - - @override - void dispose() { - downloadFileStateNotifier.dispose(); - super.dispose(); - } + Event get event => widget.event; @override void handleDownloadMatrixFileSuccessDone({ @@ -56,7 +46,6 @@ class _MessageVideoDownloadContentWebState downloadFileStateNotifier.dispose(); } - super.handleDownloadMatrixFileSuccessDone(success: success); streamSubscription?.cancel(); } @@ -104,7 +93,7 @@ class _MessageVideoDownloadContentWebState width: widget.width, height: widget.height, onVideoTapped: () async { - onDownloadFileTapped(widget.event); + onDownloadFileTap(); }, centerWidget: const CenterVideoButton( icon: Icons.arrow_downward, diff --git a/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart b/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart deleted file mode 100644 index ee6d3f3e32..0000000000 --- a/lib/presentation/mixins/handle_download_file_from_queue_in_mobile_mixin.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:dartz/dartz.dart' hide State, OpenFile; -import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; -import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; - -typedef OnDownloadedFileDone = void Function(String filePath); - -mixin HandleDownloadFileFromQueueInMobileMixin { - final downloadManager = getIt.get(); - - final downloadFileStateNotifier = ValueNotifier( - const NotDownloadPresentationState(), - ); - - void onDownloadedFileDone(String filePath) { - Logs().i( - 'HandleDownloadFileFromQueueInMobileMixin::onDownloadedFile(): $filePath', - ); - } - - StreamSubscription>? streamSubscription; - - void checkDownloadFileState(Event event) async { - checkFileExistInMemory(event); - await checkFileInDownloadsInApp(event); - - trySetupDownloadingStreamSubcription(event); - if (streamSubscription != null) { - downloadFileStateNotifier.value = const DownloadingPresentationState(); - } - } - - void checkFileExistInMemory(Event event) { - final filePathInMem = event.getFilePathFromMem(); - if (filePathInMem?.isNotEmpty == true) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: filePathInMem!, - ); - return; - } - } - - Future checkFileInDownloadsInApp(Event event) async { - final filePath = - await StorageDirectoryManager.instance.getFilePathInAppDownloads( - eventId: event.eventId, - fileName: event.filename, - ); - final file = File(filePath); - if (await file.exists() && await file.length() == event.getFileSize()) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: filePath, - ); - return; - } - } - - void trySetupDownloadingStreamSubcription(Event event) { - streamSubscription = downloadManager - .getDownloadStateStream(event.eventId) - ?.listen(setupDownloadingProcess); - } - - void setupDownloadingProcess( - Either event, - ) { - event.fold( - (failure) { - Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); - downloadFileStateNotifier.value = const NotDownloadPresentationState(); - streamSubscription?.cancel(); - }, - (success) { - if (success is DownloadingFileState) { - if (success.total != 0) { - downloadFileStateNotifier.value = DownloadingPresentationState( - receive: success.receive, - total: success.total, - ); - } - } else if (success is DownloadNativeFileSuccessState) { - downloadFileStateNotifier.value = DownloadedPresentationState( - filePath: success.filePath, - ); - onDownloadedFileDone.call(success.filePath); - } - }, - ); - } - - void onDownloadFileTapped(Event event) async { - await checkFileInDownloadsInApp(event); - if (downloadFileStateNotifier.value is DownloadedPresentationState) { - return; - } - downloadFileStateNotifier.value = const DownloadingPresentationState(); - downloadManager.download( - event: event, - ); - trySetupDownloadingStreamSubcription(event); - } -} diff --git a/lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart b/lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart deleted file mode 100644 index 46d45e30b7..0000000000 --- a/lib/presentation/mixins/handle_download_file_from_queue_in_web_mixin.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:async'; - -import 'package:dartz/dartz.dart' hide State, OpenFile; -import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:dartz/dartz.dart'; -import 'package:fluffychat/presentation/model/chat/downloading_state_presentation_model.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_manager.dart'; -import 'package:flutter/material.dart'; -import 'package:fluffychat/utils/manager/download_manager/download_file_state.dart'; -import 'package:matrix/matrix.dart'; - -mixin HandleDownloadFileFromQueueInWebMixin { - final downloadManager = getIt.get(); - - final downloadFileStateNotifier = ValueNotifier( - const NotDownloadPresentationState(), - ); - - StreamSubscription>? streamSubscription; - - void handleDownloadMatrixFileSuccessDone({ - required DownloadMatrixFileSuccessState success, - }) { - Logs().i('MessageDownloadContent::handleDownloadMatrixFileSuccessDone()'); - } - - void trySetupDownloadingStreamSubcription(String eventId) { - streamSubscription = downloadManager - .getDownloadStateStream(eventId) - ?.listen(setupDownloadingProcess); - } - - void setupDownloadingProcess(Either event) { - event.fold( - (failure) { - Logs().e('MessageDownloadContent::onDownloadingProcess(): $failure'); - downloadFileStateNotifier.value = const NotDownloadPresentationState(); - }, - (success) { - if (success is DownloadingFileState) { - if (success.total != 0) { - downloadFileStateNotifier.value = DownloadingPresentationState( - receive: success.receive, - total: success.total, - ); - } - } else if (success is DownloadMatrixFileSuccessState) { - handleDownloadMatrixFileSuccessDone(success: success); - } - }, - ); - } - - void onDownloadFileTapped(Event event) { - downloadFileStateNotifier.value = const DownloadingPresentationState(); - downloadManager.download( - event: event, - ); - trySetupDownloadingStreamSubcription(event.eventId); - } -} diff --git a/lib/widgets/mixins/download_file_on_mobile_mixin.dart b/lib/widgets/mixins/download_file_on_mobile_mixin.dart index 9ac60f12bc..4a2a438835 100644 --- a/lib/widgets/mixins/download_file_on_mobile_mixin.dart +++ b/lib/widgets/mixins/download_file_on_mobile_mixin.dart @@ -25,6 +25,12 @@ mixin DownloadFileOnMobileMixin on State { Event get event; + void onDownloadedFileDone(String filePath) { + Logs().i( + 'HandleDownloadFileFromQueueInMobileMixin::onDownloadedFile(): $filePath', + ); + } + @override void initState() { super.initState(); From 2231b51659cd4f2ad2c3a3321562cda114d4f94a Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 29 Apr 2024 11:32:07 +0700 Subject: [PATCH 173/183] TW-1685: delete decrypted file because we already copied the decrypted file to encrypted file (cherry picked from commit 1109648419882b43626a5c300ac3f07f3d320159) --- .../matrix_sdk_extensions/download_file_extension.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 2ebde64094..1b8483cd54 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -199,21 +199,21 @@ extension DownloadFileExtension on Event { ), ); } finally { - await _clearEncryptedFile( + await _clearDecryptedFile( eventId: eventId, filename: filename, ); } } - Future _clearEncryptedFile({ + Future _clearDecryptedFile({ required String eventId, required String filename, }) async { try { - final encryptedFilePath = await StorageDirectoryManager.instance - .getFilePathInAppDownloads(eventId: eventId, fileName: filename); - await File(encryptedFilePath).delete(); + final decryptedFilePath = await StorageDirectoryManager.instance + .getDecryptedFilePath(eventId: eventId, fileName: filename); + await File(decryptedFilePath).delete(); } catch (e) { Logs().e( '_clearEncryptedFile(): $e', From bf73015026e226709c3116b8d438ca567f06613f Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 26 Apr 2024 10:54:13 +0700 Subject: [PATCH 174/183] TW-1711: Handle deep link from registration (cherry picked from commit 5beb8d63e35a992ec974cb1766bb44ee1ce52333) --- lib/pages/login/on_auth_redirect.dart | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/pages/login/on_auth_redirect.dart b/lib/pages/login/on_auth_redirect.dart index c5ac278701..b0d856b123 100644 --- a/lib/pages/login/on_auth_redirect.dart +++ b/lib/pages/login/on_auth_redirect.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -33,16 +34,33 @@ class _OnAuthRedirectState extends State { return queryParams[key]; } + static bool get homeserverIsConfigured => + AppConfig.homeserver != 'https://example.com/' || + AppConfig.homeserver.isNotEmpty; + Future tryLoggingUsingToken() async { try { + final isConfigured = await AppConfig.initConfigCompleter.future; + if (!isConfigured) { + if (!AppConfig.hasReachedMaxRetries) { + tryLoggingUsingToken(); + } else { + throw Exception( + 'tryLoggingUsingToken(): Config not found', + ); + } + } + final homeserver = AppConfig.homeserver; + if (!homeserverIsConfigured) { + throw Exception( + 'tryLoggingUsingToken(): Missing homeserver', + ); + } + final loginToken = getQueryParameter('loginToken'); - final homeserver = getQueryParameter('homeserver'); - if (loginToken == null || - loginToken.isEmpty || - homeserver == null || - homeserver.isEmpty) { + if (loginToken == null || loginToken.isEmpty) { throw Exception( - 'tryLoggingUsingToken(): Missing loginToken or homeserver', + 'tryLoggingUsingToken(): Missing loginToken', ); } Logs().i('tryLoggingUsingToken::loginToken: $loginToken'); From 27beabbca96b0074e42b55e93e72e23fa7f049f9 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 26 Apr 2024 13:37:37 +0700 Subject: [PATCH 175/183] TW-1711: Handle deep link in auto homeserver picker (cherry picked from commit 8b317b46d2b92f00bdb9f81272f603c055ea2893) --- .../auto_homeserver_picker.dart | 26 +++++++++++++++++ lib/pages/login/on_auth_redirect.dart | 28 ++++++------------- .../mixins/connect_page_mixin.dart | 14 ++++++++++ 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart index 49463bdc2b..5453516490 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart @@ -3,11 +3,14 @@ import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_s import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker_view.dart'; import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/presentation/mixins/init_config_mixin.dart'; +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/exception/homeserver_exception.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +import 'package:universal_html/html.dart' as html; class AutoHomeserverPicker extends StatefulWidget { final bool? loggedOut; @@ -161,6 +164,25 @@ class AutoHomeserverPickerController extends State Logs().d( "AutoHomeserverPickerController: _initializeAutoHomeserverPicker: PlatForm ${AppConfig.platform}", ); + final loginToken = getQueryParameter('loginToken'); + if (loginToken != null || loginToken?.isNotEmpty == true) { + Matrix.of(context).loginType = LoginType.mLoginToken; + Matrix.of(context).loginHomeserverSummary = + await Matrix.of(context).getLoginClient().checkHomeserver( + Uri.parse( + AppConfig.homeserver, + ), + ); + await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => Matrix.of(context).getLoginClient().login( + LoginType.mLoginToken, + token: loginToken, + initialDeviceDisplayName: PlatformInfos.clientName, + ), + ); + _resetLocationPath(); + return; + } if (_isSaasPlatform) { _autoConnectSaas(); } else { @@ -173,6 +195,10 @@ class AutoHomeserverPickerController extends State } } + void _resetLocationPath() { + html.window.history.replaceState({}, '', '/#/rooms'); + } + @override void initState() { _setupAutoHomeserverPicker(); diff --git a/lib/pages/login/on_auth_redirect.dart b/lib/pages/login/on_auth_redirect.dart index b0d856b123..eed6c4a390 100644 --- a/lib/pages/login/on_auth_redirect.dart +++ b/lib/pages/login/on_auth_redirect.dart @@ -1,12 +1,12 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/cupertino.dart'; -import 'package:matrix/matrix.dart'; -import 'package:universal_html/html.dart' as html; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class OnAuthRedirect extends StatefulWidget { const OnAuthRedirect({super.key}); @@ -15,35 +15,23 @@ class OnAuthRedirect extends StatefulWidget { State createState() => _OnAuthRedirectState(); } -class _OnAuthRedirectState extends State { +class _OnAuthRedirectState extends State with ConnectPageMixin { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - tryLoggingUsingToken(); + tryLoggingUsingToken(context: context); }); } - String? getQueryParameter(String key) { - final questionMarkIndex = html.window.location.href.indexOf('?'); - if (questionMarkIndex == -1) { - return null; - } - final queryParams = - Uri.parse(html.window.location.href, questionMarkIndex).queryParameters; - return queryParams[key]; - } - - static bool get homeserverIsConfigured => - AppConfig.homeserver != 'https://example.com/' || - AppConfig.homeserver.isNotEmpty; - - Future tryLoggingUsingToken() async { + Future tryLoggingUsingToken({ + required BuildContext context, + }) async { try { final isConfigured = await AppConfig.initConfigCompleter.future; if (!isConfigured) { if (!AppConfig.hasReachedMaxRetries) { - tryLoggingUsingToken(); + tryLoggingUsingToken(context: context); } else { throw Exception( 'tryLoggingUsingToken(): Config not found', diff --git a/lib/presentation/mixins/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart index 25bbd74b7a..a2d030c169 100644 --- a/lib/presentation/mixins/connect_page_mixin.dart +++ b/lib/presentation/mixins/connect_page_mixin.dart @@ -43,6 +43,20 @@ mixin ConnectPageMixin { bool supportsLogin(BuildContext context) => supportsFlow(context: context, flowType: 'm.login.password'); + String? getQueryParameter(String key) { + final questionMarkIndex = html.window.location.href.indexOf('?'); + if (questionMarkIndex == -1) { + return null; + } + final queryParams = + Uri.parse(html.window.location.href, questionMarkIndex).queryParameters; + return queryParams[key]; + } + + bool get homeserverIsConfigured => + AppConfig.homeserver != 'https://example.com/' || + AppConfig.homeserver.isNotEmpty; + String _getRedirectUrlScheme(String redirectUrl) { return Uri.parse(redirectUrl).scheme; } From 86c5588507fcb3cd332f4119f167b15c0d5a32aa Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 26 Apr 2024 15:32:19 +0700 Subject: [PATCH 176/183] TW-1711: Error handle with wrong login (cherry picked from commit e03b88d7a60316e43b5e480a09161e2ed5487430) --- assets/l10n/intl_en.arb | 3 ++- .../auto_homeserver_picker.dart | 21 ++++++++++++------- .../mixins/connect_page_mixin.dart | 8 +++++++ .../app_adaptive_scaffold_body.dart | 5 ++++- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 6ce72f7b90..892e27a26e 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3048,5 +3048,6 @@ "tokenNotFound": "The login token not found", "dangerZone": "Danger zone", "leaveGroupSubtitle": "This group will still remain after you left", - "leaveChatFailed": "Failed to leave the chat" + "leaveChatFailed": "Failed to leave the chat", + "invalidLoginToken": "Invalid login token" } diff --git a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart index 5453516490..05bd9c9843 100644 --- a/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart +++ b/lib/pages/auto_homeserver_picker/auto_homeserver_picker.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:universal_html/html.dart' as html; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class AutoHomeserverPicker extends StatefulWidget { final bool? loggedOut; @@ -162,7 +162,7 @@ class AutoHomeserverPickerController extends State } } Logs().d( - "AutoHomeserverPickerController: _initializeAutoHomeserverPicker: PlatForm ${AppConfig.platform}", + "AutoHomeserverPickerController::_setupAutoHomeserverPicker: PlatForm ${AppConfig.platform}", ); final loginToken = getQueryParameter('loginToken'); if (loginToken != null || loginToken?.isNotEmpty == true) { @@ -173,14 +173,23 @@ class AutoHomeserverPickerController extends State AppConfig.homeserver, ), ); - await TwakeDialog.showFutureLoadingDialogFullScreen( + final result = await TwakeDialog.showFutureLoadingDialogFullScreen( future: () => Matrix.of(context).getLoginClient().login( LoginType.mLoginToken, token: loginToken, initialDeviceDisplayName: PlatformInfos.clientName, ), ); - _resetLocationPath(); + if (result.error != null) { + autoHomeserverPickerUIState.value = AutoHomeServerPickerFailureState( + error: L10n.of(context)!.invalidLoginToken, + ); + resetLocationPathWithLoginToken( + route: 'home', + ); + } else { + resetLocationPathWithLoginToken(); + } return; } if (_isSaasPlatform) { @@ -195,10 +204,6 @@ class AutoHomeserverPickerController extends State } } - void _resetLocationPath() { - html.window.history.replaceState({}, '', '/#/rooms'); - } - @override void initState() { _setupAutoHomeserverPicker(); diff --git a/lib/presentation/mixins/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart index a2d030c169..9f5ec79a41 100644 --- a/lib/presentation/mixins/connect_page_mixin.dart +++ b/lib/presentation/mixins/connect_page_mixin.dart @@ -263,4 +263,12 @@ mixin ConnectPageMixin { ), ); } + + void resetLocationPathWithLoginToken({ + String? route, + }) { + final loginTokenExisted = getQueryParameter('loginToken') != null; + if (!loginTokenExisted) return; + html.window.history.replaceState({}, '', '/#/${route ?? 'rooms'}'); + } } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index 57d154fd6b..fb2cb4e065 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/config/first_column_inner_routes.dart'; import 'package:fluffychat/presentation/enum/settings/settings_action_enum.dart'; +import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; @@ -34,7 +35,8 @@ class AppAdaptiveScaffoldBody extends StatefulWidget { AppAdaptiveScaffoldBodyController(); } -class AppAdaptiveScaffoldBodyController extends State { +class AppAdaptiveScaffoldBodyController extends State + with ConnectPageMixin { final ValueNotifier activeNavigationBarNotifier = ValueNotifier(AdaptiveDestinationEnum.rooms); @@ -129,6 +131,7 @@ class AppAdaptiveScaffoldBodyController extends State { @override void initState() { activeRoomIdNotifier.value = widget.activeRoomId; + resetLocationPathWithLoginToken(); super.initState(); } From c05f01f56c3c4a4a877261c1942cc5cf28998bb9 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 2 May 2024 17:14:21 +0700 Subject: [PATCH 177/183] =?UTF-8?q?TW-1734:=20Change=20naming:=20`New=20ch?= =?UTF-8?q?at`=20=E2=86=92=20`New=20group=20chat`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit ade92c5a5c59582f4f5c75eb8ec43abe466c7cdb) --- lib/pages/chat_list/chat_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index e460edaa6d..6b04f3f108 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -94,7 +94,7 @@ class ChatListView extends StatelessWidget { MenuItemButton( leadingIcon: const Icon(Icons.group), onPressed: () => controller.goToNewGroupChat(context), - child: Text(L10n.of(context)!.newChat), + child: Text(L10n.of(context)!.newGroupChat), ), ], style: const MenuStyle( From 6af339786341b7b816a30ab7c0942a8d71bce832 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 2 May 2024 22:23:44 +0700 Subject: [PATCH 178/183] =?UTF-8?q?TW-1734:=20Make=20the=20=E2=80=9CEnter?= =?UTF-8?q?=20chat=20name=E2=80=9D=20clue=20grey=20in=20Create=20Group=20c?= =?UTF-8?q?hat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 7ad770bc7eee8f916e3a53c0e9ae4659e6bcbd06) --- lib/pages/new_group/new_group_chat_info_view.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/pages/new_group/new_group_chat_info_view.dart b/lib/pages/new_group/new_group_chat_info_view.dart index 389b6d1ead..ab750db249 100644 --- a/lib/pages/new_group/new_group_chat_info_view.dart +++ b/lib/pages/new_group/new_group_chat_info_view.dart @@ -220,14 +220,11 @@ class NewGroupChatInfoView extends StatelessWidget { ), labelText: L10n.of(context)!.widgetName, labelStyle: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 16, - letterSpacing: 0.4, color: Theme.of(context).colorScheme.onSurface, ), hintText: L10n.of(context)!.enterGroupName, hintStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( - letterSpacing: -0.15, - color: Theme.of(context).colorScheme.onSurface, + color: LinagoraRefColors.material().neutral[60], ), contentPadding: NewGroupChatInfoStyle.contentPadding, ), From 03a1d05ada39db2dddd32d38560c7d15339a55ae Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 2 May 2024 22:36:22 +0700 Subject: [PATCH 179/183] TW-1734: Update Security tab in settings (cherry picked from commit 480c959d126dc3291b97c1bc899938235c3ad40a) --- assets/l10n/intl_en.arb | 3 +- .../settings_security/settings_security.dart | 71 ++++--------------- .../settings_security_view.dart | 42 ++++++----- 3 files changed, 38 insertions(+), 78 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 892e27a26e..0892c58aa4 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3049,5 +3049,6 @@ "dangerZone": "Danger zone", "leaveGroupSubtitle": "This group will still remain after you left", "leaveChatFailed": "Failed to leave the chat", - "invalidLoginToken": "Invalid login token" + "invalidLoginToken": "Invalid login token", + "copiedPublicKeyToClipboard": "Copied public key to clipboard." } diff --git a/lib/pages/settings_dashboard/settings_security/settings_security.dart b/lib/pages/settings_dashboard/settings_security/settings_security.dart index 8d21f083e0..97f3e100b8 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security.dart @@ -1,13 +1,14 @@ import 'dart:convert'; -import 'dart:typed_data'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; +import 'package:fluffychat/utils/beautify_string_extension.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_app_lock/flutter_app_lock.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -99,64 +100,6 @@ class SettingsSecurityController extends State { } } - void deleteAccountAction() async { - if (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.warning, - message: L10n.of(context)!.deactivateAccountWarning, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - ) == - OkCancelResult.cancel) { - return; - } - final supposedMxid = Matrix.of(context).client.userID!; - final mxids = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.confirmMatrixId, - textFields: [ - DialogTextField( - validator: (text) => text == supposedMxid - ? null - : L10n.of(context)!.supposedMxid(supposedMxid), - ), - ], - okLabel: L10n.of(context)!.delete, - cancelLabel: L10n.of(context)!.cancel, - ); - if (mxids == null || mxids.length != 1 || mxids.single != supposedMxid) { - return; - } - final input = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.pleaseEnterYourPassword, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [ - const DialogTextField( - obscureText: true, - hintText: '******', - minLines: 1, - maxLines: 1, - ), - ], - ); - if (input == null) return; - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context).client.deactivateAccount( - auth: AuthenticationPassword( - password: input.single, - identifier: AuthenticationUserIdentifier( - user: Matrix.of(context).client.userID!, - ), - ), - ), - ); - } - void showBootstrapDialog(BuildContext context) async { await BootstrapDialog( client: Matrix.of(context).client, @@ -194,6 +137,16 @@ class SettingsSecurityController extends State { file.result?.downloadFile(context); } + void copyPublicKey() { + Clipboard.setData( + ClipboardData(text: Matrix.of(context).client.fingerprintKey.beautified), + ); + TwakeSnackBar.show( + context, + L10n.of(context)!.copiedPublicKeyToClipboard, + ); + } + @override Widget build(BuildContext context) => SettingsSecurityView(this); } diff --git a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart index c92ebbe776..473630eea6 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart @@ -10,6 +10,7 @@ import 'settings_security.dart'; class SettingsSecurityView extends StatelessWidget { final SettingsSecurityController controller; + const SettingsSecurityView(this.controller, {Key? key}) : super(key: key); @override @@ -69,27 +70,32 @@ class SettingsSecurityView extends StatelessWidget { style: const TextStyle(fontFamily: 'monospace'), ), leading: const Icon(Icons.vpn_key_outlined), + trailing: InkWell( + onTap: controller.copyPublicKey, + child: const Icon(Icons.content_copy), + ), ), }, const Divider(height: 1), - ListTile( - leading: const Icon(Icons.tap_and_play), - trailing: const Icon(Icons.chevron_right_outlined), - title: Text( - L10n.of(context)!.dehydrate, - style: const TextStyle(color: Colors.red), - ), - onTap: controller.dehydrateAction, - ), - ListTile( - leading: const Icon(Icons.delete_outlined), - trailing: const Icon(Icons.chevron_right_outlined), - title: Text( - L10n.of(context)!.deleteAccount, - style: const TextStyle(color: Colors.red), - ), - onTap: controller.deleteAccountAction, - ), + //TODO #1734: Remove dehydrate and delete account + // ListTile( + // leading: const Icon(Icons.tap_and_play), + // trailing: const Icon(Icons.chevron_right_outlined), + // title: Text( + // L10n.of(context)!.dehydrate, + // style: const TextStyle(color: Colors.red), + // ), + // onTap: controller.dehydrateAction, + // ), + // ListTile( + // leading: const Icon(Icons.delete_outlined), + // trailing: const Icon(Icons.chevron_right_outlined), + // title: Text( + // L10n.of(context)!.deleteAccount, + // style: const TextStyle(color: Colors.red), + // ), + // onTap: controller.deleteAccountAction, + // ), ], ), ), From 2b44ff9abf105f4da6a6044dd8383c4dff65fee3 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Thu, 2 May 2024 23:54:07 +0700 Subject: [PATCH 180/183] =?UTF-8?q?TW-1734:=20Add=20button=20=E2=80=9CRemo?= =?UTF-8?q?ve=20from=20group=E2=80=9D=20when=20open=20chat=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit cde1bc346b12337a36f27531dbe2a9d4e0e65acc) --- assets/l10n/intl_en.arb | 11 ++- .../participant_list_item.dart | 1 + .../profile_info_body/profile_info_body.dart | 99 ++++++++++++++++++- .../profile_info_body_view.dart | 32 +----- .../profile_info_body_view_style.dart | 7 +- .../profile_info/profile_info_body_enum.dart | 42 ++++++++ 6 files changed, 158 insertions(+), 34 deletions(-) create mode 100644 lib/presentation/enum/profile_info/profile_info_body_enum.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 0892c58aa4..9c8242dd2e 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3050,5 +3050,14 @@ "leaveGroupSubtitle": "This group will still remain after you left", "leaveChatFailed": "Failed to leave the chat", "invalidLoginToken": "Invalid login token", - "copiedPublicKeyToClipboard": "Copied public key to clipboard." + "copiedPublicKeyToClipboard": "Copied public key to clipboard.", + "removeFromGroup": "Remove from group", + "removeUser": "Remove User", + "removeReason": "Remove {user} from the group", + "@removeReason": { + "type": "text", + "placeholders": { + "user": {} + } + } } diff --git a/lib/pages/chat_details/participant_list_item/participant_list_item.dart b/lib/pages/chat_details/participant_list_item/participant_list_item.dart index 2408b6c8ff..20b751dc75 100644 --- a/lib/pages/chat_details/participant_list_item/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item/participant_list_item.dart @@ -236,6 +236,7 @@ class ParticipantListItem extends StatelessWidget { onNewChatOpen: () { dialogContext.pop(); }, + onUpdatedMembers: onUpdatedMembers, ), ], ), diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body.dart b/lib/pages/profile_info/profile_info_body/profile_info_body.dart index 518892ceff..d174b61aec 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:dartz/dartz.dart' hide State; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; @@ -7,12 +8,18 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; import 'package:fluffychat/domain/usecase/contacts/lookup_match_contact_interactor.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view.dart'; +import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view_style.dart'; +import 'package:fluffychat/presentation/enum/profile_info/profile_info_body_enum.dart'; import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; @@ -20,12 +27,15 @@ class ProfileInfoBody extends StatefulWidget { const ProfileInfoBody({ required this.user, this.onNewChatOpen, + this.onUpdatedMembers, Key? key, }) : super(key: key); final User? user; - final void Function()? onNewChatOpen; + final VoidCallback? onNewChatOpen; + + final VoidCallback? onUpdatedMembers; @override State createState() => ProfileInfoBodyController(); @@ -121,6 +131,93 @@ class ProfileInfoBodyController extends State { } } + void leaveFromChat() async { + if (user == null) return; + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.removeUser, + okLabel: L10n.of(context)!.remove, + cancelLabel: L10n.of(context)!.cancel, + message: L10n.of(context)!.removeReason( + user?.displayName ?? '', + ), + ) == + OkCancelResult.ok) { + final result = await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => user!.kick(), + ); + if (result.error != null) { + TwakeSnackBar.show( + context, + result.error!.message, + ); + return; + } + Navigator.of(context).pop(); + widget.onUpdatedMembers?.call(); + } + } + + List profileInfoActions() { + return [ + ProfileInfoActions.sendMessage, + ProfileInfoActions.removeFromGroup, + ]; + } + + void handleActions(ProfileInfoActions action) { + switch (action) { + case ProfileInfoActions.sendMessage: + openNewChat(); + break; + case ProfileInfoActions.removeFromGroup: + leaveFromChat(); + break; + default: + break; + } + } + + Widget buildProfileInfoActions(BuildContext context) { + return Column( + children: profileInfoActions().map((action) { + return Column( + children: [ + Divider( + thickness: ProfileInfoBodyViewStyle.bigDividerThickness, + color: LinagoraSysColors.material().surface, + ), + Padding( + padding: ProfileInfoBodyViewStyle.actionItemPadding, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + onPressed: () => handleActions(action), + icon: action.icon(), + label: Row( + children: [ + Expanded( + child: Text( + action.label(context), + style: action.textStyle(context), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + }).toList(), + ); + } + @override void initState() { lookupMatchContactAction(); diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart index 997283a56b..f5b55ce9fb 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart @@ -4,9 +4,6 @@ import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_con import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_header.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; - class ProfileInfoBodyView extends StatelessWidget { const ProfileInfoBodyView({ required this.controller, @@ -36,34 +33,9 @@ class ProfileInfoBodyView extends StatelessWidget { ), ), if (!controller.isOwnProfile) ...[ - Divider( - thickness: ProfileInfoBodyViewStyle.bigDividerThickness, - color: LinagoraSysColors.material().surface, - ), Padding( - padding: ProfileInfoBodyViewStyle.newChatButtonPadding, - child: Row( - children: [ - Expanded( - child: TextButton.icon( - onPressed: () => controller.openNewChat(), - icon: const Icon(Icons.chat_outlined), - label: L10n.of(context)?.newChat != null - ? Row( - children: [ - Expanded( - child: Text( - L10n.of(context)!.sendMessage, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ) - : const SizedBox.shrink(), - ), - ), - ], - ), + padding: ProfileInfoBodyViewStyle.actionsPadding, + child: controller.buildProfileInfoActions(context), ), ] else const SizedBox(height: 16), diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart index 0b7174ba58..21fb27530d 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view_style.dart @@ -14,12 +14,15 @@ class ProfileInfoBodyViewStyle { static const double bigDividerThickness = 4; - static const EdgeInsetsGeometry newChatButtonPadding = EdgeInsets.only( - bottom: 16.0, + static const EdgeInsetsGeometry actionItemPadding = EdgeInsets.only( left: 16.0, right: 16.0, ); + static const EdgeInsetsGeometry actionsPadding = EdgeInsets.only( + bottom: 16.0, + ); + static const EdgeInsetsGeometry copiableRowPadding = EdgeInsets.symmetric( horizontal: 16.0, vertical: 8, diff --git a/lib/presentation/enum/profile_info/profile_info_body_enum.dart b/lib/presentation/enum/profile_info/profile_info_body_enum.dart new file mode 100644 index 0000000000..6630e7ee6a --- /dev/null +++ b/lib/presentation/enum/profile_info/profile_info_body_enum.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +enum ProfileInfoActions { + sendMessage, + removeFromGroup; + + String label(BuildContext context) { + switch (this) { + case ProfileInfoActions.sendMessage: + return L10n.of(context)!.sendMessage; + case ProfileInfoActions.removeFromGroup: + return L10n.of(context)!.removeFromGroup; + } + } + + TextStyle textStyle(BuildContext context) { + switch (this) { + case ProfileInfoActions.sendMessage: + return Theme.of(context).textTheme.titleMedium!.copyWith( + color: LinagoraSysColors.material().primary, + ); + case ProfileInfoActions.removeFromGroup: + return Theme.of(context).textTheme.titleMedium!.copyWith( + color: LinagoraSysColors.material().error, + ); + } + } + + Icon icon() { + switch (this) { + case ProfileInfoActions.sendMessage: + return const Icon(Icons.chat_outlined); + case ProfileInfoActions.removeFromGroup: + return Icon( + Icons.logout_outlined, + color: LinagoraSysColors.material().error, + ); + } + } +} From 39722ebc5e9a5c615d16909c383ed81a4bffa204 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 3 May 2024 00:39:05 +0700 Subject: [PATCH 181/183] TW-1734 Update condition for remove from group (cherry picked from commit dd56f317051aba2c71909764c423773af5f0131b) --- lib/pages/profile_info/profile_info_body/profile_info_body.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body.dart b/lib/pages/profile_info/profile_info_body/profile_info_body.dart index d174b61aec..894b79c94d 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body.dart @@ -162,7 +162,7 @@ class ProfileInfoBodyController extends State { List profileInfoActions() { return [ ProfileInfoActions.sendMessage, - ProfileInfoActions.removeFromGroup, + if (user!.canKick) ProfileInfoActions.removeFromGroup, ]; } From 9fb7353b7ee4099573eb0c121044c9508cc0ee2b Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 3 May 2024 00:51:25 +0700 Subject: [PATCH 182/183] TW-1734: Update remove from group on mobile (cherry picked from commit fb4df718afbf8b1781c6e2b4e4ad27e26d0b70cf) --- .../participant_list_item.dart | 2 + .../profile_info_body/profile_info_body.dart | 52 +++++++++---------- lib/pages/profile_info/profile_info_page.dart | 7 ++- lib/pages/profile_info/profile_info_view.dart | 4 ++ .../settings_security/settings_security.dart | 2 +- lib/utils/dialog/warning_dialog.dart | 4 ++ lib/utils/warning_dialog.dart | 6 ++- 7 files changed, 48 insertions(+), 29 deletions(-) diff --git a/lib/pages/chat_details/participant_list_item/participant_list_item.dart b/lib/pages/chat_details/participant_list_item/participant_list_item.dart index 20b751dc75..82bc93f237 100644 --- a/lib/pages/chat_details/participant_list_item/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item/participant_list_item.dart @@ -112,6 +112,7 @@ class ParticipantListItem extends StatelessWidget { builder: (ctx) => ProfileInfoPage( roomId: member.room.id, userId: member.id, + onUpdatedMembers: onUpdatedMembers, ), ), ); @@ -126,6 +127,7 @@ class ParticipantListItem extends StatelessWidget { return ProfileInfoPage( roomId: member.room.id, userId: member.id, + onUpdatedMembers: onUpdatedMembers, onNewChatOpen: () { dialogContext.pop(); }, diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body.dart b/lib/pages/profile_info/profile_info_body/profile_info_body.dart index 894b79c94d..b9cad46666 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:dartz/dartz.dart' hide State; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; @@ -13,6 +12,7 @@ import 'package:fluffychat/presentation/enum/profile_info/profile_info_body_enum import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/dialog/warning_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -131,32 +131,32 @@ class ProfileInfoBodyController extends State { } } - void leaveFromChat() async { + Future removeFromGroupChat() async { if (user == null) return; - if (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.removeUser, - okLabel: L10n.of(context)!.remove, - cancelLabel: L10n.of(context)!.cancel, - message: L10n.of(context)!.removeReason( - user?.displayName ?? '', - ), - ) == - OkCancelResult.ok) { - final result = await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => user!.kick(), - ); - if (result.error != null) { - TwakeSnackBar.show( - context, - result.error!.message, + WarningDialog.showCancelable( + context, + message: L10n.of(context)!.removeReason( + user?.displayName ?? '', + ), + title: L10n.of(context)!.removeUser, + acceptText: L10n.of(context)!.remove, + cancelText: L10n.of(context)!.cancel, + acceptTextColor: LinagoraSysColors.material().error, + onAccept: () async { + WarningDialog.hideWarningDialog(context); + final result = await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => user!.kick(), ); - return; - } - Navigator.of(context).pop(); - widget.onUpdatedMembers?.call(); - } + if (result.error != null) { + TwakeSnackBar.show( + context, + result.error!.message, + ); + return; + } + widget.onUpdatedMembers?.call(); + }, + ); } List profileInfoActions() { @@ -172,7 +172,7 @@ class ProfileInfoBodyController extends State { openNewChat(); break; case ProfileInfoActions.removeFromGroup: - leaveFromChat(); + removeFromGroupChat(); break; default: break; diff --git a/lib/pages/profile_info/profile_info_page.dart b/lib/pages/profile_info/profile_info_page.dart index a803dc242d..e6a7aa1927 100644 --- a/lib/pages/profile_info/profile_info_page.dart +++ b/lib/pages/profile_info/profile_info_page.dart @@ -9,11 +9,13 @@ class ProfileInfoPage extends StatefulWidget { required this.roomId, required this.userId, this.onNewChatOpen, + this.onUpdatedMembers, }); final String roomId; final String userId; final void Function()? onNewChatOpen; + final VoidCallback? onUpdatedMembers; @override State createState() => ProfileInfoPageState(); @@ -25,5 +27,8 @@ class ProfileInfoPageState extends State { User? get user => room?.unsafeGetUserFromMemoryOrFallback(widget.userId); @override - Widget build(BuildContext context) => ProfileInfoView(this); + Widget build(BuildContext context) => ProfileInfoView( + this, + onUpdatedMembers: widget.onUpdatedMembers, + ); } diff --git a/lib/pages/profile_info/profile_info_view.dart b/lib/pages/profile_info/profile_info_view.dart index ef19fcde71..5fec66f637 100644 --- a/lib/pages/profile_info/profile_info_view.dart +++ b/lib/pages/profile_info/profile_info_view.dart @@ -11,10 +11,13 @@ class ProfileInfoView extends StatelessWidget { const ProfileInfoView( this.controller, { super.key, + this.onUpdatedMembers, }); final ProfileInfoPageState controller; + final VoidCallback? onUpdatedMembers; + @override Widget build(BuildContext context) { return Scaffold( @@ -58,6 +61,7 @@ class ProfileInfoView extends StatelessWidget { body: ProfileInfoBody( user: controller.user, onNewChatOpen: controller.widget.onNewChatOpen, + onUpdatedMembers: onUpdatedMembers, ), ); } diff --git a/lib/pages/settings_dashboard/settings_security/settings_security.dart b/lib/pages/settings_dashboard/settings_security/settings_security.dart index 97f3e100b8..e4a3830e56 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security.dart @@ -137,7 +137,7 @@ class SettingsSecurityController extends State { file.result?.downloadFile(context); } - void copyPublicKey() { + Future copyPublicKey() async { Clipboard.setData( ClipboardData(text: Matrix.of(context).client.fingerprintKey.beautified), ); diff --git a/lib/utils/dialog/warning_dialog.dart b/lib/utils/dialog/warning_dialog.dart index 8c3802c9c1..3778b1b3eb 100644 --- a/lib/utils/dialog/warning_dialog.dart +++ b/lib/utils/dialog/warning_dialog.dart @@ -27,8 +27,10 @@ class WarningDialog { String? title, String? message, String? acceptText, + Color? acceptTextColor, OnAcceptButton? onAccept, String? cancelText, + Color? cancelTextColor, OnAcceptButton? onCancel, }) { showDialog( @@ -40,6 +42,7 @@ class WarningDialog { actions: [ DialogAction( text: cancelText ?? L10n.of(context)!.cancel, + textColor: cancelTextColor, onPressed: () { Navigator.pop(context); onCancel?.call(); @@ -47,6 +50,7 @@ class WarningDialog { ), DialogAction( text: acceptText ?? L10n.of(context)!.ok, + textColor: acceptTextColor, onPressed: () { Navigator.pop(context); onAccept?.call(); diff --git a/lib/utils/warning_dialog.dart b/lib/utils/warning_dialog.dart index 5d7c06fc7c..2f0c1e7618 100644 --- a/lib/utils/warning_dialog.dart +++ b/lib/utils/warning_dialog.dart @@ -59,16 +59,19 @@ class WarningDialogWidget extends StatelessWidget { class DialogAction { final String text; + final Color? textColor; final VoidCallback? onPressed; DialogAction({ required this.text, this.onPressed, + this.textColor, }); } class _WarningTextButton extends StatelessWidget { final DialogAction action; + const _WarningTextButton({ required this.action, }); @@ -85,7 +88,8 @@ class _WarningTextButton extends StatelessWidget { child: Text( action.text, style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, + color: + action.textColor ?? Theme.of(context).colorScheme.primary, ), ), ), From b6cb0e0857768a62741001d54c710927520321e2 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 3 May 2024 11:52:38 +0700 Subject: [PATCH 183/183] Bump version to v2.5.2 (cherry picked from commit fe3618104a29fee334af38542b27baa68f538ae8) --- CHANGELOG.md | 22 ++++++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91e4792bcb..447a62bf6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [2.5.2+2330] - 2024-05-03 + +### Fixed + +- #1573 Save media to gallery +- #1461 Update app language setting subtitles +- #1666 Fix Filename does not resize and time overlapping +- #1693 Delete the encrypted file after downloading it +- #1719 Fix can't tag name on mobile +- #1702 DownloadErrorPresentationState added handle errors +- #1727 Update login with `matrix.org` homeserver +- #1728 Fix can't logout on registration site +- #1658 Fix small bugs +- #1711 Handle deep link from registration platform +- #1734 Fix bugs pre-prod + +### Added + +- #1584 Sent files in room +- #1695 Leave group + ## [2.5.0+2330] - 2024-04-15 ### Fixed @@ -2309,6 +2330,7 @@ interesting devices. If you have one, I would very like to see some screenshots This CHANGELOG.md was generated with [**Changelog for Dart**](https://pub.dartlang.org/packages/changelog) +[2.5.2+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.5.2 [2.4.20+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.4.20 [2.4.19+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.4.19 [2.4.18+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.4.18 diff --git a/pubspec.yaml b/pubspec.yaml index 32970c7ffe..49176b0ff7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fluffychat description: The open digital workplace. publish_to: none -version: 2.5.1+2330 +version: 2.5.2+2330 environment: sdk: ">=3.1.3 <4.0.0"