From 3549ee1ae18fc5801f8f227189e99b2adfd8bf11 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 28 Sep 2023 17:01:19 +0700 Subject: [PATCH 01/60] fix: blynk when scroll in search room --- lib/pages/chat/chat.dart | 1 + lib/pages/chat/chat_room_search_mixin.dart | 28 ++++++++++++++++++---- lib/pages/chat/chat_view.dart | 1 + 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 537f3b836..fe3e3fa96 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -269,6 +269,7 @@ class ChatController extends State return; } hideKeyboardChatScreen(); + hideSearchKeyboardIfNeeded(); setReadMarker(); if (!scrollController.hasClients) return; if (scrollController.position.pixels == diff --git a/lib/pages/chat/chat_room_search_mixin.dart b/lib/pages/chat/chat_room_search_mixin.dart index b1aafc8e2..d003041c2 100644 --- a/lib/pages/chat/chat_room_search_mixin.dart +++ b/lib/pages/chat/chat_room_search_mixin.dart @@ -1,9 +1,12 @@ +import 'dart:async'; + 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/chat_room_search_state.dart'; import 'package:fluffychat/domain/app_state/search/search_state.dart'; import 'package:fluffychat/domain/usecase/room/chat_room_search_interactor.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:dartz/dartz.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; @@ -16,6 +19,7 @@ mixin ChatRoomSearchMixin { final isSearchingNotifier = ValueNotifier(false); final searchTextController = TextEditingController(); + final searchFocusNode = FocusNode(); final searchStatus = ValueNotifier>(Right(SearchInitial())); final canGoUp = ValueNotifier(false); @@ -25,7 +29,7 @@ mixin ChatRoomSearchMixin { int _historyCount = 100; Timeline? Function()? _getTimeline; - void Function(int)? _scrollToIndex; + Future Function(int)? _scrollToIndex; static const _debouncerDuration = Duration(milliseconds: 300); final _debouncer = Debouncer( @@ -33,6 +37,8 @@ mixin ChatRoomSearchMixin { initialValue: '', ); + var _scrollingToIndexCount = 0; + void closeSearch() { isSearchingNotifier.value = false; clearSearch(); @@ -49,7 +55,7 @@ mixin ChatRoomSearchMixin { void initializeSearch({ required Timeline? Function()? getTimeline, - Function(int)? scrollToIndex, + Future Function(int)? scrollToIndex, required int historyCount, }) { _scrollToIndex = scrollToIndex; @@ -97,10 +103,17 @@ mixin ChatRoomSearchMixin { canGoUp.value = false; } - void _scrollToEvent(Either event) { + Future _scrollToEvent(Either event) async { final index = event.getSuccessOrNull()?.eventIndex; if (index != null) { - _scrollToIndex?.call(index); + _scrollingToIndexCount++; + try { + await _scrollToIndex?.call(index); + } catch (e) { + Logs().e('ChatRoomSearchMixin::_scrollToEvent $e'); + } finally { + _scrollingToIndexCount--; + } } } @@ -158,4 +171,11 @@ mixin ChatRoomSearchMixin { }, ); } + + void hideSearchKeyboardIfNeeded() { + if (_scrollingToIndexCount > 0 || !searchFocusNode.hasFocus || kIsWeb) { + return; + } + searchFocusNode.unfocus(); + } } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 5406b0b61..e84b126f2 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -149,6 +149,7 @@ class ChatView extends StatelessWidget { if (isSearching) { return TextField( controller: controller.searchTextController, + focusNode: controller.searchFocusNode, autofocus: true, onChanged: controller.onSearchChanged, decoration: InputDecoration( From ec48d68834dbacc680bcef88776cc7db2c2bec3a Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Sat, 30 Sep 2023 00:31:02 +0700 Subject: [PATCH 02/60] Fix UI chat wrong --- .../twake_components/twake_header.dart | 80 ++++++++++--------- .../twake_preview_link/twake_link_view.dart | 21 +++-- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index 29d9ead29..87dc49f19 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -37,47 +37,51 @@ class TwakeHeader extends StatelessWidget padding: TwakeHeaderStyle.padding, child: Row( children: [ - Expanded( - flex: TwakeHeaderStyle.flexActions, - child: Row( - children: [ - InkWell( - onTap: onClearSelection, - borderRadius: BorderRadius.circular( - TwakeHeaderStyle.closeIconSize, + if (!TwakeHeaderStyle.isDesktop(context)) + Expanded( + flex: TwakeHeaderStyle.flexActions, + child: Row( + children: [ + InkWell( + onTap: onClearSelection, + borderRadius: BorderRadius.circular( + TwakeHeaderStyle.closeIconSize, + ), + child: Icon( + Icons.close, + size: TwakeHeaderStyle.closeIconSize, + color: selectMode == SelectMode.select + ? Theme.of(context) + .colorScheme + .onSurfaceVariant + : Colors.transparent, + ), ), - child: Icon( - Icons.close, - size: TwakeHeaderStyle.closeIconSize, - color: selectMode == SelectMode.select - ? Theme.of(context).colorScheme.onSurfaceVariant - : Colors.transparent, + ValueListenableBuilder( + valueListenable: conversationSelectionNotifier, + builder: (context, conversationSelection, _) { + return Padding( + padding: + TwakeHeaderStyle.counterSelectionPadding, + child: Text( + conversationSelection.length.toString(), + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: selectMode == SelectMode.select + ? Theme.of(context) + .colorScheme + .onSurfaceVariant + : Colors.transparent, + ), + ), + ); + }, ), - ), - ValueListenableBuilder( - valueListenable: conversationSelectionNotifier, - builder: (context, conversationSelection, _) { - return Padding( - padding: TwakeHeaderStyle.counterSelectionPadding, - child: Text( - conversationSelection.length.toString(), - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: selectMode == SelectMode.select - ? Theme.of(context) - .colorScheme - .onSurfaceVariant - : Colors.transparent, - ), - ), - ); - }, - ), - ], + ], + ), ), - ), Expanded( flex: TwakeHeaderStyle.flexTitle, child: Align( 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 d473cf2b9..6ca6ff2d6 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 @@ -58,18 +58,15 @@ class TwakeLinkView extends StatelessWidget { } Widget _buildCleanRichText(BuildContext context) { - return Padding( - padding: TwakeLinkViewStyle.paddingCleanRichText, - child: TwakeCleanRichText( - text: text, - childWidget: childWidget, - textStyle: textStyle, - linkStyle: linkStyle, - textAlign: textAlign ?? TextAlign.start, - onLinkTap: onLinkTap, - maxLines: maxLines, - textSpanBuilder: textSpanBuilder, - ), + return TwakeCleanRichText( + text: text, + childWidget: childWidget, + textStyle: textStyle, + linkStyle: linkStyle, + textAlign: textAlign ?? TextAlign.start, + onLinkTap: onLinkTap, + maxLines: maxLines, + textSpanBuilder: textSpanBuilder, ); } } From 5ce1959016b3652950cf878f077f2408bdce92bc Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Sat, 30 Sep 2023 01:10:37 +0700 Subject: [PATCH 03/60] Update padding for chat list screen --- .../twake_components/twake_header.dart | 47 ++++++++++--------- .../twake_components/twake_header_style.dart | 11 +++-- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index 87dc49f19..c8bdaac44 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -33,13 +33,13 @@ class TwakeHeader extends StatelessWidget builder: (context, selectMode, _) { return Align( alignment: TwakeHeaderStyle.alignment(context), - child: Padding( - padding: TwakeHeaderStyle.padding, - child: Row( - children: [ - if (!TwakeHeaderStyle.isDesktop(context)) - Expanded( - flex: TwakeHeaderStyle.flexActions, + child: Row( + children: [ + if (!TwakeHeaderStyle.isDesktop(context)) + Expanded( + flex: TwakeHeaderStyle.flexActions, + child: Padding( + padding: TwakeHeaderStyle.leadingPadding, child: Row( children: [ InkWell( @@ -82,21 +82,24 @@ class TwakeHeader extends StatelessWidget ], ), ), - Expanded( - flex: TwakeHeaderStyle.flexTitle, - child: Align( - alignment: Alignment.center, - child: Text( - L10n.of(context)!.chats, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - ), + ), + Expanded( + flex: TwakeHeaderStyle.flexTitle, + child: Align( + alignment: Alignment.center, + child: Text( + L10n.of(context)!.chats, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), ), - if (!TwakeHeaderStyle.isDesktop(context)) - Expanded( - flex: TwakeHeaderStyle.flexActions, + ), + if (!TwakeHeaderStyle.isDesktop(context)) + Expanded( + flex: TwakeHeaderStyle.flexActions, + child: Padding( + padding: TwakeHeaderStyle.actionsPadding, child: Align( alignment: Alignment.centerRight, child: InkWell( @@ -122,8 +125,8 @@ class TwakeHeader extends StatelessWidget ), ), ), - ], - ), + ), + ], ), ); }, diff --git a/lib/widgets/twake_components/twake_header_style.dart b/lib/widgets/twake_components/twake_header_style.dart index aae4b6a5f..4cbf5419c 100644 --- a/lib/widgets/twake_components/twake_header_style.dart +++ b/lib/widgets/twake_components/twake_header_style.dart @@ -21,9 +21,14 @@ class TwakeHeaderStyle { : AlignmentDirectional.center; } - static const EdgeInsetsDirectional padding = EdgeInsetsDirectional.only( - end: 4, - start: 4, + static const EdgeInsetsDirectional actionsPadding = + EdgeInsetsDirectional.only( + end: 16, + ); + + static const EdgeInsetsDirectional leadingPadding = + EdgeInsetsDirectional.only( + start: 26, ); static const EdgeInsetsDirectional textButtonPadding = From 10ac262e617c98aa25394968880325952d4505b4 Mon Sep 17 00:00:00 2001 From: sherlock Date: Mon, 2 Oct 2023 10:34:59 +0700 Subject: [PATCH 04/60] hot-fix: chat list out of order --- .../extensions/client_extension.dart | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/presentation/extensions/client_extension.dart b/lib/presentation/extensions/client_extension.dart index b6c9b473f..89a599277 100644 --- a/lib/presentation/extensions/client_extension.dart +++ b/lib/presentation/extensions/client_extension.dart @@ -3,27 +3,21 @@ import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:matrix/matrix.dart'; extension ClientExtension on Client { - static const int _ascendingOrder = 1; + static const int newerChat = -1; - static const int _descendingOrder = -1; + static const int olderChat = 1; - int _sortListRomByTimeCreatedMessage(Room currentRoom, Room nextRoom) { - return nextRoom.timeCreated.compareTo(currentRoom.timeCreated); - } - - int _sortListRoomByPinMessage(Room currentRoom, Room nextRoom) { - if (nextRoom.isFavourite && !currentRoom.isFavourite) { - return _ascendingOrder; - } else { - return _descendingOrder; + int chatListItemComparator(Room room1, Room room2) { + if (room1.isFavourite ^ room2.isFavourite) { + return room1.isFavourite ? newerChat : olderChat; } + return room2.timeCreated.compareTo(room1.timeCreated); } List filteredRoomsForAll(ActiveFilter activeFilter) { return rooms .where(activeFilter.getRoomFilterByActiveFilter()) - .sorted(_sortListRomByTimeCreatedMessage) - .sorted(_sortListRoomByPinMessage) + .sorted(chatListItemComparator) .toList(); } } From bd943f669cefe6344aa8729d91734feb3aa783f6 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 2 Oct 2023 13:38:24 +0700 Subject: [PATCH 05/60] TW-715: Fix clear DB when logout in web --- lib/main.dart | 2 +- .../flutter_hive_collections_database.dart | 5 ++++- pubspec.lock | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 3f37b5235..f25be1051 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,9 +23,9 @@ void main() async { GoRouter.optionURLReflectsImperativeAPIs = true; Logs().nativeColors = !PlatformInfos.isIOS; final clients = await ClientManager.getClients(); - // Preload first client final firstClient = clients.firstOrNull; + firstClient?.isSupportDeleteCollections = !PlatformInfos.isWeb; await firstClient?.roomsLoading; await firstClient?.accountDataLoading; 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 13d96b134..ddcd97de2 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart' hide Key; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -74,7 +75,9 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { } catch (e, s) { Logs().w('Unable to open Hive. Delete database and storage key...', e, s); const FlutterSecureStorage().delete(key: cipherStorageKey); - await db.clear().catchError((_) {}); + await db + .clear(supportDeleteCollections: !PlatformInfos.isWeb) + .catchError((_) {}); await Hive.deleteFromDisk(); rethrow; } diff --git a/pubspec.lock b/pubspec.lock index 3096be91b..0378e1560 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1534,7 +1534,7 @@ packages: description: path: "." ref: "twake-supported-0.22.4" - resolved-ref: "5f756700d23b76ac94ad096488732e54e4cc4e8f" + resolved-ref: bbc843232affc4b04fb7dbd45a925ddde4a7e878 url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.4" From 8daaf1d3492eb9d70ee6b97c1a091d738a0108fa Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Thu, 28 Sep 2023 17:12:36 +0200 Subject: [PATCH 06/60] fix: last sender display name null safety --- lib/pages/chat_list/chat_list_item_mixin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat_list/chat_list_item_mixin.dart b/lib/pages/chat_list/chat_list_item_mixin.dart index 2077f77fb..c8606b53f 100644 --- a/lib/pages/chat_list/chat_list_item_mixin.dart +++ b/lib/pages/chat_list/chat_list_item_mixin.dart @@ -80,7 +80,7 @@ mixin ChatListItemMixin { builder: (context, snapshot) { if (snapshot.data == null) return const SizedBox.shrink(); return Text( - snapshot.data!.displayName!, + snapshot.data!.calcDisplayname(), overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, From 4597d0be401f9e6c43e321bf28bcb4ce3aae28ee Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Mon, 2 Oct 2023 16:08:08 +0200 Subject: [PATCH 07/60] fix: remove download file when no attachment --- lib/pages/chat/chat.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index fe3e3fa96..ea426efb5 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1426,7 +1426,8 @@ class ChatController extends State ChatContextMenuActions.copyMessage, ChatContextMenuActions.pinMessage, ChatContextMenuActions.forward, - if (PlatformInfos.isWeb) ChatContextMenuActions.downloadFile, + if (PlatformInfos.isWeb && event.hasAttachment) + ChatContextMenuActions.downloadFile, ]; return listAction.map((action) { return PopupMenuItem( From 484f9e50612892abf05fc54c0006c5c8644cfca6 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Tue, 3 Oct 2023 14:07:11 +0700 Subject: [PATCH 08/60] fix: Build iOS 17 --- ios/Podfile | 4 ++++ ios/Runner.xcodeproj/project.pbxproj | 2 +- macos/.gitignore | 1 + pubspec.lock | 36 ++++++++++++++-------------- pubspec.yaml | 4 ++-- web/index.html | 1 + 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index b3ea74368..95b266171 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -38,6 +38,10 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| + xcconfig_path = config.base_configuration_reference.real_path + xcconfig = File.read(xcconfig_path) + xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") + File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } config.build_settings['ENABLE_BITCODE'] = 'NO' # see https://github.com/flutter-webrtc/flutter-webrtc/issues/1054 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 00ae79446..38be348e9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -190,6 +190,7 @@ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 8C9CCA7C5C45651F90C7BFDD /* [CP] Check Pods Manifest.lock */, + C1005C4D261071B5002F4F32 /* Embed App Extensions */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -197,7 +198,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */, - C1005C4D261071B5002F4F32 /* Embed App Extensions */, ); buildRules = ( ); diff --git a/macos/.gitignore b/macos/.gitignore index d2fd37723..e5f06c673 100644 --- a/macos/.gitignore +++ b/macos/.gitignore @@ -4,3 +4,4 @@ # Xcode-related **/xcuserdata/ +**/DerivedData/ \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 0378e1560..60965d072 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -816,10 +816,10 @@ packages: dependency: "direct main" description: name: flutter_inappwebview - sha256: f73505c792cf083d5566e1a94002311be497d984b5607f25be36d685cf6361cf + sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 url: "https://pub.dev" source: hosted - version: "5.7.2+3" + version: "5.8.0" flutter_keyboard_visibility: dependency: "direct main" description: @@ -1270,66 +1270,66 @@ packages: dependency: "direct main" description: name: image_picker - sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c + sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" url: "https://pub.dev" source: hosted - version: "0.8.9" + version: "1.0.4" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" + sha256: "47da2161c2e9f8f8a9cbbd89d466d174333fbdd769aeed848912e0b16d9cb369" url: "https://pub.dev" source: hosted - version: "0.8.7+4" + version: "0.8.8" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.1" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b + sha256: c5538cacefacac733c724be7484377923b476216ad1ead35a0d2eadcdc0fc497 url: "https://pub.dev" source: hosted - version: "0.8.8" + version: "0.8.8+2" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 + sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.9.1" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" import_sorter: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 781ffea50..4fc6c98a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,7 @@ dependencies: hive_flutter: ^1.1.0 http: ^0.13.4 image: ^4.0.15 - image_picker: ^0.8.4+8 + image_picker: ^1.0.4 intl: any just_audio: ^0.9.30 just_audio_mpv: ^0.1.6 @@ -125,7 +125,7 @@ dependencies: fluttertoast: ^8.2.2 rxdart: ^0.27.7 photo_manager: ^2.7.1 - flutter_inappwebview: ^5.7.2+3 + flutter_inappwebview: ^5.8.0 tuple: ^2.0.2 lottie: ^2.3.2 wechat_camera_picker: ^3.8.0 diff --git a/web/index.html b/web/index.html index 6a4447b64..5e69da154 100644 --- a/web/index.html +++ b/web/index.html @@ -137,5 +137,6 @@ + From be4567589f8d1b5c5ccede0b5c0f290e45125332 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Wed, 4 Oct 2023 11:35:19 +0700 Subject: [PATCH 09/60] TW-726 Notification no sound in iOS --- ios/Runner/AppDelegate.swift | 6 ++++++ lib/utils/background_push.dart | 8 ++++++++ lib/widgets/matrix.dart | 1 + 3 files changed, 15 insertions(+) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 87ca5b522..5bdffc6d0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -33,6 +33,12 @@ let apnTokenKey = "apnToken" case "getInitialNoti": result(self?.initialNotiInfo) self?.initialNotiInfo = nil + case "clearAll": + UIApplication.shared.applicationIconBadgeNumber = 0 + let center = UNUserNotificationCenter.current() + center.removeAllDeliveredNotifications() + center.removeAllPendingNotificationRequests() + result(true) default: break } diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 4753b082a..4f41479ee 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -198,6 +198,8 @@ class BackgroundPush { "aps": { "mutable-content": 1, "content-available": 1, + "badge": 1, + "sound": "default", "alert": {"loc-key": "SINGLE_UNREAD", "loc-args": []} } } @@ -557,4 +559,10 @@ class BackgroundPush { return PushNotificationExtensions().error(); } } + + void clearAllNotifications() { + if (Platform.isIOS) { + apnChannel.invokeMethod('clearAll'); + } + } } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index b290853b8..963c9cbb3 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -594,6 +594,7 @@ class MatrixState extends State with WidgetsBindingObserver { client.backgroundSync = foreground; client.syncPresence = foreground ? null : PresenceType.unavailable; client.requestHistoryOnLimitedTimeline = !foreground; + backgroundPush?.clearAllNotifications(); } void initSettings() { From 4dc36b9133a043c26e6c97e3db3a3f9750398ddd Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:20:19 +0700 Subject: [PATCH 10/60] TW-692: Update UI when change page --- .../layouts/adaptive_layout/adaptive_scaffold.dart | 8 ++++++++ .../layouts/adaptive_layout/adaptive_scaffold_view.dart | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart index 6e758135d..d67ed0f01 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_vie import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; typedef OnOpenSearchPage = Function(); @@ -48,6 +49,7 @@ class AdaptiveScaffoldAppController extends State { ]; void onDestinationSelected(int index) { + _clearSettingsPage(); final destinationType = destinations[index]; activeNavigationBar.value = destinationType; pageController.jumpToPage(index); @@ -94,6 +96,12 @@ class AdaptiveScaffoldAppController extends State { pageController.jumpToPage(activeNavigationBar.value.index); } + void _clearSettingsPage() { + if (activeNavigationBar.value == AdaptiveDestinationEnum.settings) { + context.go('/rooms'); + } + } + MatrixState get matrix => Matrix.of(context); @override diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart index 2bd33a600..6232ff8c7 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart @@ -2,7 +2,7 @@ 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/settings.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'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold.dart'; From cfdb8d5d8adda7b25705a80b5f84f77fb6daef70 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:22:35 +0700 Subject: [PATCH 11/60] TW-692: Update folder settings --- .../settings_3pid/settings_3pid.dart | 0 .../settings_3pid/settings_3pid_view.dart | 2 +- .../settings_chat/settings_chat.dart | 0 .../settings_chat/settings_chat_view.dart | 0 .../settings_emotes/settings_emotes.dart | 2 +- .../settings_emotes/settings_emotes_view.dart | 2 +- .../settings_ignore_list/settings_ignore_list.dart | 2 +- .../settings_ignore_list/settings_ignore_list_view.dart | 2 +- .../settings_multiple_emotes/settings_multiple_emotes.dart | 0 .../settings_multiple_emotes_view.dart | 2 +- .../settings_notifications/settings_notifications.dart | 2 +- .../settings_notifications/settings_notifications_view.dart | 4 ++-- .../settings_security/settings_security.dart | 2 +- .../settings_security/settings_security_view.dart | 0 .../settings_stories/settings_stories.dart | 6 ++---- .../settings_stories/settings_stories_view.dart | 2 +- 16 files changed, 13 insertions(+), 15 deletions(-) rename lib/pages/{ => settings_dashboard}/settings_3pid/settings_3pid.dart (100%) rename lib/pages/{ => settings_dashboard}/settings_3pid/settings_3pid_view.dart (97%) rename lib/pages/{ => settings_dashboard}/settings_chat/settings_chat.dart (100%) rename lib/pages/{ => settings_dashboard}/settings_chat/settings_chat_view.dart (100%) rename lib/pages/{ => settings_dashboard}/settings_emotes/settings_emotes.dart (99%) rename lib/pages/{ => settings_dashboard}/settings_emotes/settings_emotes_view.dart (99%) rename lib/pages/{ => settings_dashboard}/settings_ignore_list/settings_ignore_list.dart (95%) rename lib/pages/{ => settings_dashboard}/settings_ignore_list/settings_ignore_list_view.dart (98%) rename lib/pages/{ => settings_dashboard}/settings_multiple_emotes/settings_multiple_emotes.dart (100%) rename lib/pages/{ => settings_dashboard}/settings_multiple_emotes/settings_multiple_emotes_view.dart (95%) rename lib/pages/{ => settings_dashboard}/settings_notifications/settings_notifications.dart (98%) rename lib/pages/{ => settings_dashboard}/settings_notifications/settings_notifications_view.dart (97%) rename lib/pages/{ => settings_dashboard}/settings_security/settings_security.dart (98%) rename lib/pages/{ => settings_dashboard}/settings_security/settings_security_view.dart (100%) rename lib/pages/{ => settings_dashboard}/settings_stories/settings_stories.dart (93%) rename lib/pages/{ => settings_dashboard}/settings_stories/settings_stories_view.dart (96%) diff --git a/lib/pages/settings_3pid/settings_3pid.dart b/lib/pages/settings_dashboard/settings_3pid/settings_3pid.dart similarity index 100% rename from lib/pages/settings_3pid/settings_3pid.dart rename to lib/pages/settings_dashboard/settings_3pid/settings_3pid.dart diff --git a/lib/pages/settings_3pid/settings_3pid_view.dart b/lib/pages/settings_dashboard/settings_3pid/settings_3pid_view.dart similarity index 97% rename from lib/pages/settings_3pid/settings_3pid_view.dart rename to lib/pages/settings_dashboard/settings_3pid/settings_3pid_view.dart index e4cc31667..b00c7541d 100644 --- a/lib/pages/settings_3pid/settings_3pid_view.dart +++ b/lib/pages/settings_dashboard/settings_3pid/settings_3pid_view.dart @@ -1,9 +1,9 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_3pid/settings_3pid.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/pages/settings_chat/settings_chat.dart b/lib/pages/settings_dashboard/settings_chat/settings_chat.dart similarity index 100% rename from lib/pages/settings_chat/settings_chat.dart rename to lib/pages/settings_dashboard/settings_chat/settings_chat.dart diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart similarity index 100% rename from lib/pages/settings_chat/settings_chat_view.dart rename to lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart diff --git a/lib/pages/settings_emotes/settings_emotes.dart b/lib/pages/settings_dashboard/settings_emotes/settings_emotes.dart similarity index 99% rename from lib/pages/settings_emotes/settings_emotes.dart rename to lib/pages/settings_dashboard/settings_emotes/settings_emotes.dart index c262eb234..9314af70f 100644 --- a/lib/pages/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_dashboard/settings_emotes/settings_emotes.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -8,7 +9,6 @@ import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/client_manager.dart'; -import '../../widgets/matrix.dart'; import 'settings_emotes_view.dart'; class EmotesSettings extends StatefulWidget { diff --git a/lib/pages/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart similarity index 99% rename from lib/pages/settings_emotes/settings_emotes_view.dart rename to lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart index 88d4cda1e..ec0ddc610 100644 --- a/lib/pages/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -7,7 +8,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; -import '../../widgets/matrix.dart'; import 'settings_emotes.dart'; class EmotesSettingsView extends StatelessWidget { diff --git a/lib/pages/settings_ignore_list/settings_ignore_list.dart b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart similarity index 95% rename from lib/pages/settings_ignore_list/settings_ignore_list.dart rename to lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart index cf0713aa9..d02d985bf 100644 --- a/lib/pages/settings_ignore_list/settings_ignore_list.dart +++ b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart @@ -1,8 +1,8 @@ +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; -import '../../widgets/matrix.dart'; import 'settings_ignore_list_view.dart'; class SettingsIgnoreList extends StatefulWidget { diff --git a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart similarity index 98% rename from lib/pages/settings_ignore_list/settings_ignore_list_view.dart rename to lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart index fb112ad2e..fc8fa9943 100644 --- a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart +++ b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -6,7 +7,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import '../../widgets/matrix.dart'; import 'settings_ignore_list.dart'; class SettingsIgnoreListView extends StatelessWidget { diff --git a/lib/pages/settings_multiple_emotes/settings_multiple_emotes.dart b/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart similarity index 100% rename from lib/pages/settings_multiple_emotes/settings_multiple_emotes.dart rename to lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart diff --git a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart b/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes_view.dart similarity index 95% rename from lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart rename to lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes_view.dart index 41c6a5326..666145aae 100644 --- a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart +++ b/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes_view.dart @@ -4,7 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pages/settings_multiple_emotes/settings_multiple_emotes.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart'; import 'package:fluffychat/widgets/matrix.dart'; class MultipleEmotesSettingsView extends StatelessWidget { diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_dashboard/settings_notifications/settings_notifications.dart similarity index 98% rename from lib/pages/settings_notifications/settings_notifications.dart rename to lib/pages/settings_dashboard/settings_notifications/settings_notifications.dart index 5f2d5ff5e..731d55c30 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_dashboard/settings_notifications/settings_notifications.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -6,7 +7,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; -import '../../widgets/matrix.dart'; import 'settings_notifications_view.dart'; class NotificationSettingsItem { diff --git a/lib/pages/settings_notifications/settings_notifications_view.dart b/lib/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart similarity index 97% rename from lib/pages/settings_notifications/settings_notifications_view.dart rename to lib/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart index 57160da4d..db7fba4a1 100644 --- a/lib/pages/settings_notifications/settings_notifications_view.dart +++ b/lib/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart @@ -1,3 +1,5 @@ +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -5,8 +7,6 @@ import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import '../../utils/localized_exception_extension.dart'; -import '../../widgets/matrix.dart'; import 'settings_notifications.dart'; class SettingsNotificationsView extends StatelessWidget { diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_dashboard/settings_security/settings_security.dart similarity index 98% rename from lib/pages/settings_security/settings_security.dart rename to lib/pages/settings_dashboard/settings_security/settings_security.dart index 52d0d01d3..4652d7f1a 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:flutter/material.dart'; @@ -14,7 +15,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; import 'settings_security_view.dart'; class SettingsSecurity extends StatefulWidget { diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart similarity index 100% rename from lib/pages/settings_security/settings_security_view.dart rename to lib/pages/settings_dashboard/settings_security/settings_security_view.dart diff --git a/lib/pages/settings_stories/settings_stories.dart b/lib/pages/settings_dashboard/settings_stories/settings_stories.dart similarity index 93% rename from lib/pages/settings_stories/settings_stories.dart rename to lib/pages/settings_dashboard/settings_stories/settings_stories.dart index 802c07d0c..7098a4841 100644 --- a/lib/pages/settings_stories/settings_stories.dart +++ b/lib/pages/settings_dashboard/settings_stories/settings_stories.dart @@ -1,11 +1,9 @@ +import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:flutter/material.dart'; - import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pages/settings_stories/settings_stories_view.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_stories/settings_stories_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import '../../utils/matrix_sdk_extensions/client_stories_extension.dart'; class SettingsStories extends StatefulWidget { const SettingsStories({Key? key}) : super(key: key); diff --git a/lib/pages/settings_stories/settings_stories_view.dart b/lib/pages/settings_dashboard/settings_stories/settings_stories_view.dart similarity index 96% rename from lib/pages/settings_stories/settings_stories_view.dart rename to lib/pages/settings_dashboard/settings_stories/settings_stories_view.dart index de34e04b6..0c48504a6 100644 --- a/lib/pages/settings_stories/settings_stories_view.dart +++ b/lib/pages/settings_dashboard/settings_stories/settings_stories_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pages/settings_stories/settings_stories.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_stories/settings_stories.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; From 8991919a289c1435c4ebdd4a236e979646f42921 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:23:20 +0700 Subject: [PATCH 12/60] TW-692: Fix overflow for display name --- lib/pages/chat_details/chat_details.dart | 2 +- lib/pages/chat_list/chat_list.dart | 2 +- lib/pages/chat_list/chat_list_item_mixin.dart | 38 ++++++++++--------- .../chat_list/chat_list_item_subtitle.dart | 2 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 3ad310824..197c84b0c 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/links/chat_ import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection_web.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; import 'package:fluffychat/presentation/extensions/room_summary_extension.dart'; import 'package:fluffychat/presentation/mixins/handle_video_download_mixin.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; @@ -28,7 +29,6 @@ import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; -import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index cb4776ca4..88efb224b 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -12,7 +12,7 @@ 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'; import 'package:fluffychat/pages/chat_list/receive_sharing_intent_mixin.dart'; -import 'package:fluffychat/pages/settings_security/settings_security.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/model/chat_list/chat_selection_actions.dart'; diff --git a/lib/pages/chat_list/chat_list_item_mixin.dart b/lib/pages/chat_list/chat_list_item_mixin.dart index c8606b53f..4034702ae 100644 --- a/lib/pages/chat_list/chat_list_item_mixin.dart +++ b/lib/pages/chat_list/chat_list_item_mixin.dart @@ -75,25 +75,27 @@ mixin ChatListItemMixin { return isGroup ? Row( children: [ - FutureBuilder( - future: room.lastEvent?.fetchSenderUser(), - builder: (context, snapshot) { - if (snapshot.data == null) return const SizedBox.shrink(); - return Text( - snapshot.data!.calcDisplayname(), - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: Theme.of(context).textTheme.labelLarge?.merge( - TextStyle( - overflow: TextOverflow.ellipsis, - color: unread - ? Theme.of(context).colorScheme.onSurface - : ChatListItemStyle.readMessageColor, + Expanded( + child: FutureBuilder( + future: room.lastEvent?.fetchSenderUser(), + builder: (context, snapshot) { + if (snapshot.data == null) return const SizedBox.shrink(); + return Text( + snapshot.data!.calcDisplayname(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: Theme.of(context).textTheme.labelLarge?.merge( + TextStyle( + overflow: TextOverflow.ellipsis, + color: unread + ? Theme.of(context).colorScheme.onSurface + : ChatListItemStyle.readMessageColor, + ), ), - ), - ); - }, + ); + }, + ), ), const Spacer() ], diff --git a/lib/pages/chat_list/chat_list_item_subtitle.dart b/lib/pages/chat_list/chat_list_item_subtitle.dart index 1804c2176..119995524 100644 --- a/lib/pages/chat_list/chat_list_item_subtitle.dart +++ b/lib/pages/chat_list/chat_list_item_subtitle.dart @@ -58,7 +58,7 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin { ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible( + Expanded( child: lastSenderWidget(room, isGroup, unread), ), const SizedBox(height: 2), From 94640d9b925f3224b53badf5fc9d9f62c63955b0 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:24:11 +0700 Subject: [PATCH 13/60] TW-692: Remove Settings screen from go_router --- lib/config/go_routes/go_router.dart | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 27767ed13..d1154a0d9 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -10,6 +10,8 @@ import 'package:fluffychat/pages/chat_draft/draft_chat.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/pages/error_page/error_page.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_dashboard_manager.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; import 'package:fluffychat/pages/story/story_page.dart'; import 'package:fluffychat/presentation/model/chat/chat_router_input_argument.dart'; @@ -25,15 +27,15 @@ import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart' import 'package:fluffychat/pages/login/login.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; -import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; -import 'package:fluffychat/pages/settings_chat/settings_chat.dart'; -import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart'; -import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart'; -import 'package:fluffychat/pages/settings_multiple_emotes/settings_multiple_emotes.dart'; -import 'package:fluffychat/pages/settings_notifications/settings_notifications.dart'; -import 'package:fluffychat/pages/settings_security/settings_security.dart'; -import 'package:fluffychat/pages/settings_stories/settings_stories.dart'; -import 'package:fluffychat/pages/settings_style/settings_style.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_3pid/settings_3pid.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_chat/settings_chat.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_emotes/settings_emotes.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_notifications/settings_notifications.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_security/settings_security.dart'; +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/log_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -243,6 +245,25 @@ abstract class AppRoutes { ); }, ), + GoRoute( + path: 'profile', + pageBuilder: (context, state) => defaultPageBuilder( + context, + SettingsProfile( + profile: state.extra as Profile?, + ), + ), + redirect: (context, state) { + final settingsDashboardManagerController = + SettingsDashboardManagerController(); + + if (!settingsDashboardManagerController.initialized) { + return '/rooms'; + } + + return Matrix.of(context).client.isLogged() ? null : '/home'; + }, + ), GoRoute( path: 'notifications', pageBuilder: (context, state) => defaultPageBuilder( From 14a2691aa52c00e2cc6241b1cf62a129eff5c189 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:24:58 +0700 Subject: [PATCH 14/60] TW-692: Create settings, settings profile enum and settings profile presentation --- .../enum/settings/settings_enum.dart | 78 +++++++++++++++++++ .../enum/settings/settings_profile_enum.dart | 74 ++++++++++++++++++ .../settings_profile_presentation.dart | 17 ++++ 3 files changed, 169 insertions(+) create mode 100644 lib/presentation/enum/settings/settings_enum.dart create mode 100644 lib/presentation/enum/settings/settings_profile_enum.dart create mode 100644 lib/presentation/model/settings/settings_profile_presentation.dart diff --git a/lib/presentation/enum/settings/settings_enum.dart b/lib/presentation/enum/settings/settings_enum.dart new file mode 100644 index 000000000..75546b7f0 --- /dev/null +++ b/lib/presentation/enum/settings/settings_enum.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum SettingEnum { + chatSettings, + privacyAndSecurity, + notificationAndSounds, + chatFolders, + appLanguage, + devices, + help, + logout; + + String titleSettings(BuildContext context) { + switch (this) { + case SettingEnum.chatSettings: + return L10n.of(context)!.chat; + case SettingEnum.privacyAndSecurity: + return L10n.of(context)!.privacyAndSecurity; + case SettingEnum.notificationAndSounds: + return L10n.of(context)!.notificationAndSounds; + case SettingEnum.chatFolders: + return L10n.of(context)!.chatFolders; + case SettingEnum.appLanguage: + return L10n.of(context)!.appLanguage; + case SettingEnum.devices: + return L10n.of(context)!.devices; + case SettingEnum.help: + return L10n.of(context)!.help; + case SettingEnum.logout: + return L10n.of(context)!.logout; + } + } + + String subtitleSettings(BuildContext context) { + switch (this) { + case SettingEnum.chatSettings: + return L10n.of(context)!.settingsChatSubtitle; + case SettingEnum.privacyAndSecurity: + return L10n.of(context)!.settingsPrivacyAndSecuritySubtitle; + case SettingEnum.notificationAndSounds: + return L10n.of(context)!.settingsNotificationAndSoundsSubtitle; + case SettingEnum.chatFolders: + return L10n.of(context)!.settingsChatFoldersSubtitle; + case SettingEnum.appLanguage: + return L10n.of(context)!.settingsAppLanguageSubtitle; + case SettingEnum.devices: + return L10n.of(context)!.settingsDevicesSubtitle; + case SettingEnum.help: + return L10n.of(context)!.settingsHelpSubtitle; + default: + return ''; + } + } + + IconData iconLeading() { + switch (this) { + case SettingEnum.chatSettings: + return Icons.chat_bubble_outline_outlined; + case SettingEnum.privacyAndSecurity: + return Icons.lock; + case SettingEnum.notificationAndSounds: + return Icons.notifications_none; + case SettingEnum.chatFolders: + return Icons.folder_outlined; + case SettingEnum.appLanguage: + return Icons.language; + case SettingEnum.devices: + return Icons.devices; + case SettingEnum.help: + return Icons.question_mark; + case SettingEnum.logout: + return Icons.logout_outlined; + } + } + + bool get isHideTrailingIcon => this == SettingEnum.logout; +} diff --git a/lib/presentation/enum/settings/settings_profile_enum.dart b/lib/presentation/enum/settings/settings_profile_enum.dart new file mode 100644 index 000000000..4068b15c1 --- /dev/null +++ b/lib/presentation/enum/settings/settings_profile_enum.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum SettingsProfileEnum { + displayName, + bio, + matrixId, + email, + company; + + String getTitle(BuildContext context) { + switch (this) { + case SettingsProfileEnum.displayName: + return L10n.of(context)!.displayName; + case SettingsProfileEnum.bio: + return L10n.of(context)!.bio; + case SettingsProfileEnum.matrixId: + return L10n.of(context)!.matrixId; + case SettingsProfileEnum.email: + return L10n.of(context)!.email; + case SettingsProfileEnum.company: + return L10n.of(context)!.company; + } + } + + IconData getLeadingIcon() { + switch (this) { + case SettingsProfileEnum.displayName: + case SettingsProfileEnum.bio: + return Icons.person_outline; + case SettingsProfileEnum.matrixId: + return Icons.language_outlined; + case SettingsProfileEnum.email: + return Icons.email_outlined; + case SettingsProfileEnum.company: + return Icons.apartment_outlined; + } + } + + IconData getTrailingIcon() { + switch (this) { + case SettingsProfileEnum.displayName: + case SettingsProfileEnum.bio: + case SettingsProfileEnum.company: + return Icons.edit_outlined; + case SettingsProfileEnum.matrixId: + case SettingsProfileEnum.email: + return Icons.content_copy; + } + } + + SettingsProfileType getSettingsProfileType() { + switch (this) { + case SettingsProfileEnum.displayName: + case SettingsProfileEnum.bio: + case SettingsProfileEnum.company: + return SettingsProfileType.edit; + case SettingsProfileEnum.matrixId: + case SettingsProfileEnum.email: + return SettingsProfileType.copy; + } + } +} + +enum SettingsProfileType { + edit, + copy, +} + +enum AvatarAction { + camera, + file, + remove, +} diff --git a/lib/presentation/model/settings/settings_profile_presentation.dart b/lib/presentation/model/settings/settings_profile_presentation.dart new file mode 100644 index 000000000..a401c1b52 --- /dev/null +++ b/lib/presentation/model/settings/settings_profile_presentation.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; + +class SettingsProfilePresentation extends Equatable { + final SettingsProfileType settingsProfileType; + + bool get isEditable => settingsProfileType == SettingsProfileType.edit; + + const SettingsProfilePresentation({ + this.settingsProfileType = SettingsProfileType.edit, + }); + + @override + List get props => [ + settingsProfileType, + ]; +} From 3da7dfde2f3eedad6b605be6a7d2c6929177f968 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:25:34 +0700 Subject: [PATCH 15/60] TW-692: Create SettingsDashboardManagerController for manage settings --- .../settings_dashboard_manager.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 lib/pages/settings_dashboard/settings_dashboard_manager.dart diff --git a/lib/pages/settings_dashboard/settings_dashboard_manager.dart b/lib/pages/settings_dashboard/settings_dashboard_manager.dart new file mode 100644 index 000000000..e5b7d7b3c --- /dev/null +++ b/lib/pages/settings_dashboard/settings_dashboard_manager.dart @@ -0,0 +1,50 @@ +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class SettingsDashboardManagerController { + SettingsDashboardManagerController._privateConstructor(); + + static SettingsDashboardManagerController get instance { + _instance.initProfileNotifier(); + return _instance; + } + + static final SettingsDashboardManagerController _instance = + SettingsDashboardManagerController._privateConstructor(); + + factory SettingsDashboardManagerController() { + return _instance; + } + + void getCurrentProfile(Client client) async { + final profile = await client.getProfileFromUserId( + client.userID!, + getFromRooms: false, + ); + Logs().v( + 'SettingsDashboardManagerController::_getCurrentProfile() - currentProfile: $profile', + ); + profileNotifier.value = profile; + } + + late ValueNotifier profileNotifier; + + bool initialized = false; + + void initProfileNotifier() { + initialized = true; + profileNotifier = ValueNotifier( + Profile(userId: ''), + ); + } + + String mxid(BuildContext context) => + Matrix.of(context).client.userID ?? L10n.of(context)!.user; + + String displayName(BuildContext context) => + profileNotifier.value.displayName ?? + mxid(context).localpart ?? + mxid(context); +} From 1afe18e859983a3a78d458e365e10f0b89fa18ee Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:26:01 +0700 Subject: [PATCH 16/60] TW-692: Add new text for intl --- assets/l10n/intl_en.arb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index d21055772..0abad73dd 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2734,5 +2734,25 @@ "unmuteThisMessage": "Unmute this message", "read": "Read", "unread": "Unread", - "unmute": "Unmute" + "unmute": "Unmute", + "privacyAndSecurity" : "Privacy & Security", + "notificationAndSounds": "Notification & Sounds", + "appLanguage": "App Language", + "chatFolders": "Chat Folders", + "settingsChatSubtitle" : "Appearance, themes, wallpaper, chat history.", + "settingsPrivacyAndSecuritySubtitle": "Block contacts, disappearing messages.", + "settingsNotificationAndSoundsSubtitle": "Custom how you get notifications from Twake such as previewing messages, sounds, time,...", + "settingsChatFoldersSubtitle": "Create and manage folders for different groups of chats and quickly switch between them.", + "settingsAppLanguageSubtitle": "English (phone’s language).", + "settingsDevicesSubtitle": "Control your sign in and sign out on any device.", + "settingsHelpSubtitle": "Help center, contact us, privacy policy.", + "displayName": "Display Name", + "bio": "Bio (optional)", + "matrixId": "Matrix ID", + "email": "Email", + "company": "Company", + "basicInfo": "BASIC INFO", + "editProfileDescriptions": "Update your profile with a new name, picture and a short introduction.", + "workIdentitiesInfo": "WORK IDENTITIES INFO", + "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name." } \ No newline at end of file From f955c23a40134239b1f2084319d839af9153a2e2 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:26:39 +0700 Subject: [PATCH 17/60] TW-692: Create `SettingsProfileItemBuilder` --- .../settings_profile_item.dart | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart new file mode 100644 index 000000000..d4352905b --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart @@ -0,0 +1,97 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; +import 'package:fluffychat/presentation/model/settings/settings_profile_presentation.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class SettingsProfileItemBuilder extends StatelessWidget { + final String title; + final SettingsProfilePresentation settingsProfilePresentation; + final SettingsProfileEnum settingsProfileEnum; + final FocusNode? focusNode; + final TextEditingController? textEditingController; + final IconData suffixIcon; + final IconData? leadingIcon; + final void Function(String, SettingsProfileEnum)? onChange; + + const SettingsProfileItemBuilder({ + super.key, + required this.settingsProfileEnum, + required this.title, + required this.settingsProfilePresentation, + this.focusNode, + this.textEditingController, + required this.suffixIcon, + this.leadingIcon, + this.onChange, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: SettingsProfileItemStyle.itemBuilderPadding, + child: Icon( + leadingIcon, + size: SettingsProfileItemStyle.iconSize, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Expanded( + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: LinagoraRefColors.material().neutral[40], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + TextField( + onChanged: (value) => + onChange!(value, settingsProfileEnum), + readOnly: !settingsProfilePresentation.isEditable, + autofocus: false, + focusNode: focusNode, + controller: textEditingController, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: settingsProfilePresentation.isEditable + ? () { + focusNode?.requestFocus(); + } + : () {}, + icon: Icon( + suffixIcon, + size: SettingsProfileItemStyle.iconSize, + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + hintText: textEditingController?.text, + ), + ), + Divider( + height: SettingsProfileItemStyle.dividerSize, + color: LinagoraStateLayer( + LinagoraSysColors.material().surfaceTint, + ).opacityLayer3, + ), + ], + ), + ), + ], + ), + ), + ], + ); + } +} From 94fac4fed0a4628cc502a208ec7349e4974f14fe Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:26:54 +0700 Subject: [PATCH 18/60] TW-692: Create `SettingsItemBuilder` --- .../settings/settings_item_builder.dart | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 lib/pages/settings_dashboard/settings/settings_item_builder.dart diff --git a/lib/pages/settings_dashboard/settings/settings_item_builder.dart b/lib/pages/settings_dashboard/settings/settings_item_builder.dart new file mode 100644 index 000000000..7830221ad --- /dev/null +++ b/lib/pages/settings_dashboard/settings/settings_item_builder.dart @@ -0,0 +1,92 @@ +import 'package:fluffychat/pages/settings_dashboard/settings/settings_view_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; + +class SettingsItemBuilder extends StatelessWidget { + final String title; + final String subtitle; + final IconData leading; + final VoidCallback onTap; + final bool isHideTrailingIcon; + + const SettingsItemBuilder({ + super.key, + required this.title, + required this.subtitle, + required this.leading, + required this.onTap, + this.isHideTrailingIcon = false, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: SettingsViewStyle.itemBuilderPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: SettingsViewStyle.leadingItemBuilderPadding, + child: Icon( + leading, + size: SettingsViewStyle.iconSize, + color: isHideTrailingIcon + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Expanded( + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + color: isHideTrailingIcon + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onSurface, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: SettingsViewStyle.subtitleItemBuilderPadding, + child: Text( + subtitle, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: + LinagoraRefColors.material().neutral[40], + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + if (!isHideTrailingIcon) + const Icon( + Icons.chevron_right_outlined, + size: SettingsViewStyle.iconSize, + ), + ], + ), + ), + ], + ), + ), + ); + } +} From bea3db7d5b0101ebb61aa8cac3c424d2d1a5ead0 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:27:12 +0700 Subject: [PATCH 19/60] TW-692: Create `SettingsProfileViewMobile` --- .../settings_profile_view_mobile.dart | 99 +++++++++++++++++++ .../settings_profile_view_mobile_style.dart | 18 ++++ 2 files changed, 117 insertions(+) create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart new file mode 100644 index 000000000..3556cb3db --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -0,0 +1,99 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/avatar/avatar_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:matrix/matrix.dart'; + +class SettingsProfileViewMobile extends StatelessWidget { + final ValueNotifier profileNotifier; + final String displayName; + final Widget settingsProfileOptions; + final VoidCallback onAvatarTap; + + const SettingsProfileViewMobile({ + super.key, + required this.profileNotifier, + required this.settingsProfileOptions, + required this.displayName, + required this.onAvatarTap, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: profileNotifier, + builder: (context, _, __) { + return Column( + children: [ + Divider( + height: SettingsProfileViewMobileStyle.dividerHeight, + color: LinagoraStateLayer( + LinagoraSysColors.material().surfaceTint, + ).opacityLayer3, + ), + Padding( + padding: SettingsProfileViewMobileStyle.padding, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox( + width: SettingsProfileViewMobileStyle.widthSize, + ), + Material( + elevation: + Theme.of(context).appBarTheme.scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context).appBarTheme.shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, + ), + ), + child: Avatar( + mxContent: profileNotifier.value.avatarUrl, + name: displayName, + size: SettingsProfileViewMobileStyle.avatarSize, + fontSize: + SettingsProfileViewMobileStyle.positionedRightSize, + ), + ), + Positioned( + bottom: SettingsProfileViewMobileStyle.positionedBottomSize, + right: SettingsProfileViewMobileStyle.positionedRightSize, + child: InkWell( + onTap: onAvatarTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewMobileStyle.avatarSize, + ), + border: Border.all( + color: Theme.of(context).colorScheme.onPrimary, + width: SettingsProfileViewMobileStyle + .iconEditBorderWidth, + ), + ), + padding: SettingsProfileViewMobileStyle.editIconPadding, + child: Icon( + Icons.edit, + size: SettingsProfileViewMobileStyle.iconEditSize, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ), + ], + ), + ), + settingsProfileOptions, + ], + ); + }, + ); + } +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart new file mode 100644 index 000000000..3ba1345b5 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class SettingsProfileViewMobileStyle { + static const double widthSize = 116; + static const double avatarSize = 96; + static const double avatarFontSize = 18 * 2.5; + static const double positionedBottomSize = 0; + static const double positionedRightSize = 0; + static const double iconEditBorderWidth = 4; + static const double iconEditSize = 24; + static const double dividerHeight = 2; + + static EdgeInsetsDirectional padding = + const EdgeInsetsDirectional.symmetric(vertical: 16.0); + + static EdgeInsetsDirectional editIconPadding = + const EdgeInsetsDirectional.all(8); +} From 29e0c958095c5c676d4858dd5c0873aee8f08ed1 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:27:30 +0700 Subject: [PATCH 20/60] TW-692: Create `SettingsProfileViewWeb` --- .../settings_profile_view_web.dart | 221 ++++++++++++++++++ .../settings_profile_view_web_style.dart | 32 +++ 2 files changed, 253 insertions(+) create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart new file mode 100644 index 000000000..b1c2d198a --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart @@ -0,0 +1,221 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/avatar/avatar_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class SettingsProfileViewWeb extends StatelessWidget { + final ValueNotifier profileNotifier; + final String displayName; + final Widget basicInfoWidget; + final Widget workIdentitiesInfoWidget; + final VoidCallback onAvatarTap; + + const SettingsProfileViewWeb({ + super.key, + required this.profileNotifier, + required this.displayName, + required this.basicInfoWidget, + required this.onAvatarTap, + required this.workIdentitiesInfoWidget, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: profileNotifier, + builder: (context, _, __) { + return Padding( + padding: SettingsProfileViewWebStyle.paddingBody, + child: Center( + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: SettingsProfileViewWebStyle.bodyWidth, + padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.radiusCircular, + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + SettingsProfileViewWebStyle.paddingBasicInfoTitle, + child: Text( + L10n.of(context)!.basicInfo, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: + Theme.of(context).colorScheme.onSurface, + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: SettingsProfileViewWebStyle + .paddingWidgetBasicInfo, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox( + width: + SettingsProfileViewWebStyle.widthSize, + ), + Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context) + .appBarTheme + .shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, + ), + ), + child: Avatar( + mxContent: + profileNotifier.value.avatarUrl, + name: displayName, + size: SettingsProfileViewWebStyle + .avatarSize, + fontSize: SettingsProfileViewWebStyle + .avatarFontSize, + ), + ), + Positioned( + bottom: SettingsProfileViewWebStyle + .positionedBottomSize, + right: SettingsProfileViewWebStyle + .positionedRightSize, + child: InkWell( + onTap: onAvatarTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle + .avatarSize, + ), + border: Border.all( + color: Theme.of(context) + .colorScheme + .onPrimary, + width: 4, + ), + ), + padding: SettingsProfileViewWebStyle + .paddingEditIcon, + child: Icon( + Icons.edit, + size: SettingsProfileViewWebStyle + .iconEditSize, + color: Theme.of(context) + .colorScheme + .onPrimary, + ), + ), + ), + ), + ], + ), + ), + Expanded( + child: basicInfoWidget, + ), + ], + ), + ], + ), + ), + Padding( + padding: SettingsProfileViewWebStyle + .paddingWidgetEditProfileInfo, + child: Text( + L10n.of(context)!.editProfileDescriptions, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), + ), + ), + Container( + width: SettingsProfileViewWebStyle.bodyWidth, + padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.radiusCircular, + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + SettingsProfileViewWebStyle.paddingBasicInfoTitle, + child: Text( + L10n.of(context)!.workIdentitiesInfo, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: + Theme.of(context).colorScheme.onSurface, + ), + ), + ), + Padding( + padding: SettingsProfileViewWebStyle + .paddingWorkIdentitiesInfoWidget, + child: workIdentitiesInfoWidget, + ) + ], + ), + ), + Padding( + padding: SettingsProfileViewWebStyle + .paddingWidgetEditProfileInfo, + child: Text( + L10n.of(context)!.editWorkIdentitiesDescriptions, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart new file mode 100644 index 000000000..a6a4680f2 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +class SettingsProfileViewWebStyle { + static const double bodyWidth = 640; + static const double widthSize = 116; + static const double avatarSize = 96; + static const double avatarFontSize = 18 * 2.5; + static const double positionedBottomSize = 0; + static const double positionedRightSize = 0; + static const double iconEditBorderWidth = 4; + static const double iconEditSize = 24; + static const double dividerHeight = 2; + static const double radiusCircular = 16; + + static const EdgeInsetsDirectional paddingBody = + EdgeInsetsDirectional.all(32); + + static const EdgeInsetsDirectional paddingWidgetBasicInfo = + EdgeInsetsDirectional.all(16); + + static const EdgeInsetsDirectional paddingBasicInfoTitle = + EdgeInsetsDirectional.only(bottom: 32); + + static const EdgeInsetsDirectional paddingEditIcon = + EdgeInsetsDirectional.all(8); + + static const EdgeInsetsDirectional paddingWidgetEditProfileInfo = + EdgeInsetsDirectional.symmetric(vertical: 16); + + static const EdgeInsetsDirectional paddingWorkIdentitiesInfoWidget = + EdgeInsetsDirectional.only(bottom: 16); +} From 495aaf03e15ffa199f235090d0aaf859d14a728e Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:28:34 +0700 Subject: [PATCH 21/60] TW-692: Update settings screen --- lib/pages/settings/settings.dart | 231 ------------------ lib/pages/settings/settings_view.dart | 197 --------------- .../settings_dashboard/settings/settings.dart | 172 +++++++++++++ .../settings/settings_view.dart | 155 ++++++++++++ .../settings/settings_view_style.dart | 22 ++ .../settings_style/settings_style_view.dart | 4 +- lib/pages/settings_style/settings_style.dart | 92 ------- 7 files changed, 351 insertions(+), 522 deletions(-) delete mode 100644 lib/pages/settings/settings.dart delete mode 100644 lib/pages/settings/settings_view.dart create mode 100644 lib/pages/settings_dashboard/settings/settings.dart create mode 100644 lib/pages/settings_dashboard/settings/settings_view.dart create mode 100644 lib/pages/settings_dashboard/settings/settings_view_style.dart rename lib/pages/{ => settings_dashboard}/settings_style/settings_style_view.dart (98%) delete mode 100644 lib/pages/settings_style/settings_style.dart diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart deleted file mode 100644 index 936ca2898..000000000 --- a/lib/pages/settings/settings.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/data/hive/hive_collection_tom_database.dart'; -import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/pages/connect/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:future_loading_dialog/future_loading_dialog.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/utils/platform_infos.dart'; -import '../../widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; -import 'settings_view.dart'; - -class Settings extends StatefulWidget { - final Widget? bottomNavigationBar; - - const Settings({ - super.key, - this.bottomNavigationBar, - }); - - @override - SettingsController createState() => SettingsController(); -} - -class SettingsController extends State with ConnectPageMixin { - Future? profileFuture; - bool profileUpdated = false; - - void updateProfile() => setState(() { - profileUpdated = true; - profileFuture = null; - }); - - void setDisplaynameAction() async { - final profile = await profileFuture; - final input = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.editDisplayname, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [ - DialogTextField( - initialText: profile?.displayName ?? - Matrix.of(context).client.userID!.localpart, - ) - ], - ); - if (input == null) return; - final matrix = Matrix.of(context); - final success = await showFutureLoadingDialog( - context: context, - future: () => - matrix.client.setDisplayName(matrix.client.userID!, input.single), - ); - if (success.error == null) { - updateProfile(); - } - } - - void logoutAction() async { - final noBackup = showChatBackupBanner == true; - if (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSureYouWantToLogout, - message: L10n.of(context)!.noBackupWarning, - isDestructiveAction: noBackup, - okLabel: L10n.of(context)!.logout, - cancelLabel: L10n.of(context)!.cancel, - ) == - OkCancelResult.cancel) { - return; - } - await tryLogoutSso(context); - final hiveCollectionToMDatabase = getIt.get(); - await hiveCollectionToMDatabase.clear(); - final matrix = Matrix.of(context); - await showFutureLoadingDialog( - context: context, - future: () => matrix.client.logout(), - ); - } - - void setAvatarAction() async { - final profile = await profileFuture; - final actions = [ - if (PlatformInfos.isMobile) - SheetAction( - key: AvatarAction.camera, - label: L10n.of(context)!.openCamera, - isDefaultAction: true, - icon: Icons.camera_alt_outlined, - ), - SheetAction( - key: AvatarAction.file, - label: L10n.of(context)!.openGallery, - icon: Icons.photo_outlined, - ), - if (profile?.avatarUrl != null) - SheetAction( - key: AvatarAction.remove, - label: L10n.of(context)!.removeYourAvatar, - isDestructiveAction: true, - icon: Icons.delete_outlined, - ), - ]; - final action = actions.length == 1 - ? actions.single.key - : await showModalActionSheet( - context: context, - title: L10n.of(context)!.changeYourAvatar, - actions: actions, - ); - if (action == null) return; - final matrix = Matrix.of(context); - if (action == AvatarAction.remove) { - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(null), - ); - if (success.error == null) { - updateProfile(); - } - return; - } - MatrixFile file; - if (PlatformInfos.isMobile) { - final result = await ImagePicker().pickImage( - source: action == AvatarAction.camera - ? ImageSource.camera - : ImageSource.gallery, - imageQuality: 50, - ); - if (result == null) return; - file = MatrixFile( - bytes: await result.readAsBytes(), - name: result.path, - ); - } else { - final result = await FilePicker.platform.pickFiles( - type: FileType.image, - withData: true, - ); - final pickedFile = result?.files.firstOrNull; - if (pickedFile == null) return; - file = MatrixFile( - bytes: pickedFile.bytes!, - name: pickedFile.name, - ); - } - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(file), - ); - if (success.error == null) { - updateProfile(); - } - } - - Client get client => Matrix.of(context).client; - - @override - void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { - profileFuture ??= client.getProfileFromUserId( - client.userID!, - cache: !profileUpdated, - getFromRooms: false, - ); - checkBootstrap(); - }); - super.initState(); - } - - void checkBootstrap() async { - if (!client.encryptionEnabled) return; - await client.accountDataLoading; - await client.userDeviceKeysLoading; - if (client.prevBatch == null) { - await client.onSync.stream.first; - } - final crossSigning = - await client.encryption?.crossSigning.isCached() ?? false; - final needsBootstrap = - await client.encryption?.keyManager.isCached() == false || - client.encryption?.crossSigning.enabled == false || - crossSigning == false; - final isUnknownSession = client.isUnknownSession; - setState(() { - showChatBackupBanner = needsBootstrap || isUnknownSession; - }); - } - - bool? crossSigningCached; - bool? showChatBackupBanner; - - void firstRunBootstrapAction([_]) async { - if (showChatBackupBanner != true) { - showOkAlertDialog( - context: context, - title: L10n.of(context)!.chatBackup, - message: L10n.of(context)!.onlineKeyBackupEnabled, - okLabel: L10n.of(context)!.close, - ); - return; - } - await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - checkBootstrap(); - } - - @override - Widget build(BuildContext context) { - return SettingsView( - this, - bottomNavigationBar: widget.bottomNavigationBar, - ); - } -} - -enum AvatarAction { camera, file, remove } diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart deleted file mode 100644 index 1874ac5ae..000000000 --- a/lib/pages/settings/settings_view.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/url_launcher.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; -import 'package:fluffychat/widgets/avatar/avatar_style.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -import 'settings.dart'; - -class SettingsView extends StatelessWidget { - final SettingsController controller; - final Widget? bottomNavigationBar; - - const SettingsView( - this.controller, { - super.key, - this.bottomNavigationBar, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(L10n.of(context)!.settings), - actions: [ - TextButton.icon( - onPressed: controller.logoutAction, - label: Text(L10n.of(context)!.logout), - icon: const Icon(Icons.logout_outlined), - ), - ], - ), - bottomNavigationBar: bottomNavigationBar, - body: ListTileTheme( - iconColor: Theme.of(context).colorScheme.onBackground, - child: ListView( - key: const Key('SettingsListViewContent'), - children: [ - FutureBuilder( - future: controller.profileFuture, - builder: (context, snapshot) { - final profile = snapshot.data; - final mxid = - Matrix.of(context).client.userID ?? L10n.of(context)!.user; - final displayname = - profile?.displayName ?? mxid.localpart ?? mxid; - return Row( - children: [ - Padding( - padding: const EdgeInsets.all(32.0), - child: Stack( - children: [ - Material( - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: - Theme.of(context).appBarTheme.shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize * 2.5, - ), - ), - child: Avatar( - mxContent: profile?.avatarUrl, - name: displayname, - size: AvatarStyle.defaultSize * 2.5, - fontSize: 18 * 2.5, - ), - ), - if (profile != null) - Positioned( - bottom: 0, - right: 0, - child: FloatingActionButton.small( - onPressed: controller.setAvatarAction, - heroTag: null, - child: const Icon(Icons.camera_alt_outlined), - ), - ), - ], - ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextButton.icon( - onPressed: controller.setDisplaynameAction, - icon: const Icon( - Icons.edit_outlined, - size: 16, - ), - style: TextButton.styleFrom( - foregroundColor: - Theme.of(context).colorScheme.onBackground, - ), - label: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - // style: const TextStyle(fontSize: 18), - ), - ), - TextButton.icon( - onPressed: () => FluffyShare.share(mxid, context), - icon: const Icon( - Icons.copy_outlined, - size: 14, - ), - style: TextButton.styleFrom( - foregroundColor: - Theme.of(context).colorScheme.secondary, - ), - label: Text( - mxid, - maxLines: 1, - overflow: TextOverflow.ellipsis, - // style: const TextStyle(fontSize: 12), - ), - ), - ], - ), - ), - ], - ); - }, - ), - const Divider(thickness: 1), - const Divider(thickness: 1), - // ListTile( - // leading: const Icon(Icons.format_paint_outlined), - // title: Text(L10n.of(context)!.changeTheme), - // onTap: () => context.go('/settings/style'), - // trailing: const Icon(Icons.chevron_right_outlined), - // ), - ListTile( - leading: const Icon(Icons.notifications_outlined), - title: Text(L10n.of(context)!.notifications), - onTap: () => context.go('/rooms/notifications'), - trailing: const Icon(Icons.chevron_right_outlined), - ), - ListTile( - leading: const Icon(Icons.devices_outlined), - title: Text(L10n.of(context)!.devices), - onTap: () => context.go('/rooms/devices'), - trailing: const Icon(Icons.chevron_right_outlined), - ), - ListTile( - leading: const Icon(Icons.chat_bubble_outline_outlined), - title: Text(L10n.of(context)!.chat), - onTap: () => context.go('/rooms/chat'), - trailing: const Icon(Icons.chevron_right_outlined), - ), - ListTile( - leading: const Icon(Icons.shield_outlined), - title: Text(L10n.of(context)!.security), - onTap: () => context.go('/rooms/security'), - trailing: const Icon(Icons.chevron_right_outlined), - ), - const Divider(thickness: 1), - ListTile( - leading: const Icon(Icons.help_outline_outlined), - title: Text(L10n.of(context)!.help), - onTap: () => UrlLauncher(context, AppConfig.supportUrl) - .openUrlInAppBrowser(), - trailing: const Icon(Icons.open_in_new_outlined), - ), - ListTile( - leading: const Icon(Icons.shield_sharp), - title: Text(L10n.of(context)!.privacy), - onTap: () => UrlLauncher(context, AppConfig.privacyUrl) - .openUrlInAppBrowser(), - trailing: const Icon(Icons.open_in_new_outlined), - ), - ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text(L10n.of(context)!.about), - onTap: () => PlatformInfos.showDialog(context), - trailing: const Icon(Icons.chevron_right_outlined), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart new file mode 100644 index 000000000..0ca1abafb --- /dev/null +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -0,0 +1,172 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/data/hive/hive_collection_tom_database.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; +import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_dashboard_manager.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'settings_view.dart'; + +class Settings extends StatefulWidget { + final Widget? bottomNavigationBar; + + const Settings({ + super.key, + this.bottomNavigationBar, + }); + + @override + SettingsController createState() => SettingsController(); +} + +class SettingsController extends State with ConnectPageMixin { + late SettingsDashboardManagerController settingsDashboardManagerController; + + List getListSettingItem() { + return [ + SettingEnum.chatSettings, + SettingEnum.privacyAndSecurity, + SettingEnum.notificationAndSounds, + SettingEnum.chatFolders, + SettingEnum.appLanguage, + SettingEnum.devices, + SettingEnum.help, + SettingEnum.logout, + ]; + } + + String get mxid => settingsDashboardManagerController.mxid(context); + + String get displayName => + settingsDashboardManagerController.displayName(context); + + void logoutAction() async { + final noBackup = showChatBackupBanner == true; + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSureYouWantToLogout, + message: L10n.of(context)!.noBackupWarning, + isDestructiveAction: noBackup, + okLabel: L10n.of(context)!.logout, + cancelLabel: L10n.of(context)!.cancel, + ) == + OkCancelResult.cancel) { + return; + } + await tryLogoutSso(context); + final hiveCollectionToMDatabase = getIt.get(); + await hiveCollectionToMDatabase.clear(); + final matrix = Matrix.of(context); + await showFutureLoadingDialog( + context: context, + future: () => matrix.client.logout(), + ); + } + + Client get client => Matrix.of(context).client; + + @override + void initState() { + settingsDashboardManagerController = + SettingsDashboardManagerController.instance; + settingsDashboardManagerController.getCurrentProfile(client); + WidgetsBinding.instance.addPostFrameCallback((_) { + checkBootstrap(); + }); + super.initState(); + } + + void checkBootstrap() async { + if (!client.encryptionEnabled) return; + await client.accountDataLoading; + await client.userDeviceKeysLoading; + if (client.prevBatch == null) { + await client.onSync.stream.first; + } + final crossSigning = + await client.encryption?.crossSigning.isCached() ?? false; + final needsBootstrap = + await client.encryption?.keyManager.isCached() == false || + client.encryption?.crossSigning.enabled == false || + crossSigning == false; + final isUnknownSession = client.isUnknownSession; + setState(() { + showChatBackupBanner = needsBootstrap || isUnknownSession; + }); + } + + bool? crossSigningCached; + bool? showChatBackupBanner; + + void firstRunBootstrapAction([_]) async { + if (showChatBackupBanner != true) { + showOkAlertDialog( + context: context, + title: L10n.of(context)!.chatBackup, + message: L10n.of(context)!.onlineKeyBackupEnabled, + okLabel: L10n.of(context)!.close, + ); + return; + } + await BootstrapDialog( + client: Matrix.of(context).client, + ).show(context); + checkBootstrap(); + } + + void goToSettingsProfile(Profile? profile) async { + context.push( + '/rooms/profile', + extra: profile, + ); + } + + void onClickToSettingsItem(SettingEnum settingEnum) { + switch (settingEnum) { + case SettingEnum.chatSettings: + context.go('/rooms/chat'); + break; + case SettingEnum.privacyAndSecurity: + context.go('/rooms/security'); + break; + case SettingEnum.notificationAndSounds: + context.go('/rooms/notifications'); + break; + case SettingEnum.chatFolders: + break; + case SettingEnum.appLanguage: + break; + case SettingEnum.devices: + context.go('/rooms/devices'); + break; + case SettingEnum.help: + UrlLauncher( + context, + AppConfig.supportUrl, + ).openUrlInAppBrowser(); + break; + case SettingEnum.logout: + logoutAction(); + break; + } + } + + @override + Widget build(BuildContext context) { + return SettingsView( + this, + bottomNavigationBar: widget.bottomNavigationBar, + ); + } +} diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart new file mode 100644 index 000000000..558382b70 --- /dev/null +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -0,0 +1,155 @@ +import 'package:fluffychat/pages/settings_dashboard/settings/settings_item_builder.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings/settings_view_style.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'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; + +import 'settings.dart'; + +class SettingsView extends StatelessWidget { + final SettingsController controller; + final Widget? bottomNavigationBar; + + const SettingsView( + this.controller, { + super.key, + this.bottomNavigationBar, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.settings, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + centerTitle: true, + ), + bottomNavigationBar: bottomNavigationBar, + body: ListTileTheme( + iconColor: Theme.of(context).colorScheme.onBackground, + child: ListView( + key: const Key('SettingsListViewContent'), + children: [ + Builder( + builder: (context) { + return ValueListenableBuilder( + valueListenable: controller + .settingsDashboardManagerController.profileNotifier, + builder: (context, profile, _) { + return Padding( + padding: SettingsViewStyle.bodySettingsScreenPadding, + child: InkWell( + onTap: () => controller.goToSettingsProfile(profile), + child: Row( + children: [ + Padding( + padding: SettingsViewStyle.avatarPadding, + child: Stack( + children: [ + Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context) + .appBarTheme + .shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, + ), + ), + child: Avatar( + mxContent: profile.avatarUrl, + name: controller.displayName, + size: AvatarStyle.defaultSize, + fontSize: 18 * 2.5, + ), + ), + ], + ), + ), + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + profile.displayName ?? + controller.displayName, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + controller.mxid, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: + LinagoraRefColors.material() + .neutral[40], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const Icon( + Icons.chevron_right_outlined, + size: SettingsViewStyle.iconSize, + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + const Divider(thickness: 1), + Column( + children: controller.getListSettingItem().map((item) { + return SettingsItemBuilder( + title: item.titleSettings(context), + subtitle: item.subtitleSettings(context), + leading: item.iconLeading(), + onTap: () => controller.onClickToSettingsItem(item), + isHideTrailingIcon: item.isHideTrailingIcon, + ); + }).toList(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_dashboard/settings/settings_view_style.dart b/lib/pages/settings_dashboard/settings/settings_view_style.dart new file mode 100644 index 000000000..a58c6d2db --- /dev/null +++ b/lib/pages/settings_dashboard/settings/settings_view_style.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class SettingsViewStyle { + static const double iconSize = 24.0; + + static EdgeInsetsDirectional itemBuilderPadding = + const EdgeInsetsDirectional.all(16.0); + + static EdgeInsetsDirectional leadingItemBuilderPadding = + const EdgeInsetsDirectional.only(end: 8); + + static EdgeInsetsDirectional subtitleItemBuilderPadding = + const EdgeInsetsDirectional.only(top: 4); + + static EdgeInsetsDirectional bodySettingsScreenPadding = + const EdgeInsetsDirectional.symmetric( + horizontal: 16, + ); + + static EdgeInsetsDirectional avatarPadding = + const EdgeInsetsDirectional.only(end: 8); +} diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_dashboard/settings_style/settings_style_view.dart similarity index 98% rename from lib/pages/settings_style/settings_style_view.dart rename to lib/pages/settings_dashboard/settings_style/settings_style_view.dart index e8a9c7d5d..18db7af58 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_dashboard/settings_style/settings_style_view.dart @@ -1,10 +1,10 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import '../../config/app_config.dart'; -import '../../widgets/matrix.dart'; import 'settings_style.dart'; class SettingsStyleView extends StatelessWidget { diff --git a/lib/pages/settings_style/settings_style.dart b/lib/pages/settings_style/settings_style.dart deleted file mode 100644 index 4f2bc6708..000000000 --- a/lib/pages/settings_style/settings_style.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/setting_keys.dart'; -import 'package:fluffychat/widgets/theme_builder.dart'; -import 'settings_style_view.dart'; - -class SettingsStyle extends StatefulWidget { - const SettingsStyle({Key? key}) : super(key: key); - - @override - SettingsStyleController createState() => SettingsStyleController(); -} - -class SettingsStyleController extends State { - void setWallpaperAction() async { - final picked = await FilePicker.platform.pickFiles( - type: FileType.image, - withData: false, - ); - final pickedFile = picked?.files.firstOrNull; - - if (pickedFile == null) return; - await Matrix.of(context) - .store - .setItem(SettingKeys.wallpaper, pickedFile.path); - setState(() {}); - } - - void deleteWallpaperAction() async { - Matrix.of(context).wallpaper = null; - await Matrix.of(context).store.deleteItem(SettingKeys.wallpaper); - setState(() {}); - } - - void setChatColor(Color? color) async { - if (color != null) { - AppConfig.colorSchemeSeed = color; - ThemeController.of(context).setPrimaryColor(color); - } - } - - ThemeMode get currentTheme => ThemeController.of(context).themeMode; - Color? get currentColor => ThemeController.of(context).primaryColor; - - static final List customColors = [ - AppConfig.chatColor, - Colors.blue.shade800, - Colors.green.shade800, - Colors.orange.shade700, - Colors.pink.shade700, - Colors.blueGrey.shade600, - null, - ]; - - void switchTheme(ThemeMode? newTheme) { - if (newTheme == null) return; - switch (newTheme) { - case ThemeMode.light: - ThemeController.of(context).setThemeMode(ThemeMode.light); - break; - case ThemeMode.dark: - ThemeController.of(context).setThemeMode(ThemeMode.dark); - break; - case ThemeMode.system: - ThemeController.of(context).setThemeMode(ThemeMode.system); - break; - } - setState(() {}); - } - - void changeFontSizeFactor(double d) { - setState(() => AppConfig.fontSizeFactor = d); - Matrix.of(context).store.setItem( - SettingKeys.fontSizeFactor, - AppConfig.fontSizeFactor.toString(), - ); - } - - void changeBubbleSizeFactor(double d) { - setState(() => AppConfig.bubbleSizeFactor = d); - Matrix.of(context).store.setItem( - SettingKeys.bubbleSizeFactor, - AppConfig.bubbleSizeFactor.toString(), - ); - } - - @override - Widget build(BuildContext context) => SettingsStyleView(this); -} From 65d07a1b8d150b0826a57b84860179a83a7d16be Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 16:28:55 +0700 Subject: [PATCH 22/60] TW-692: Update settings profile screen --- .../settings_profile/settings_profile.dart | 240 ++++++++++++++++++ .../settings_profile_item_style.dart | 9 + .../settings_profile_view.dart | 217 ++++++++++++++++ .../settings_style/settings_style.dart | 92 +++++++ 4 files changed, 558 insertions(+) create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart create mode 100644 lib/pages/settings_dashboard/settings_style/settings_style.dart diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart new file mode 100644 index 000000000..33fde5b81 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -0,0 +1,240 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_dashboard_manager.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; +import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class SettingsProfile extends StatefulWidget { + final Profile? profile; + + const SettingsProfile({ + super.key, + required this.profile, + }); + + @override + State createState() => SettingsProfileController(); +} + +class SettingsProfileController extends State { + final settingsDashboardManagerController = + SettingsDashboardManagerController(); + + final ValueNotifier isEditedProfileNotifier = ValueNotifier(false); + + Client get client => Matrix.of(context).client; + + String get mxid => settingsDashboardManagerController.mxid(context); + + String get displayName => + settingsDashboardManagerController.displayName(context); + + final TextEditingController displayNameEditingController = + TextEditingController(); + final TextEditingController matrixIdEditingController = + TextEditingController(); + + final FocusNode displayNameFocusNode = FocusNode( + debugLabel: 'displayNameFocusNode', + ); + + List get getListProfileMobile => + getListProfileBasicInfo + getListProfileWorkIdentitiesInfo; + + List getListProfileBasicInfo = [ + SettingsProfileEnum.displayName, + ]; + + List getListProfileWorkIdentitiesInfo = [ + SettingsProfileEnum.matrixId + ]; + + TextEditingController? getController( + SettingsProfileEnum settingsProfileEnum, + ) { + switch (settingsProfileEnum) { + case SettingsProfileEnum.displayName: + return displayNameEditingController; + case SettingsProfileEnum.matrixId: + return matrixIdEditingController; + default: + return null; + } + } + + FocusNode? getFocusNode(SettingsProfileEnum settingsProfileEnum) { + switch (settingsProfileEnum) { + case SettingsProfileEnum.displayName: + return displayNameFocusNode; + default: + return null; + } + } + + void setAvatarAction() async { + final actions = [ + if (PlatformInfos.isMobile) + SheetAction( + key: AvatarAction.camera, + label: L10n.of(context)!.openCamera, + isDefaultAction: true, + icon: Icons.camera_alt_outlined, + ), + SheetAction( + key: AvatarAction.file, + label: L10n.of(context)!.openGallery, + icon: Icons.photo_outlined, + ), + if (settingsDashboardManagerController.profileNotifier.value.avatarUrl != + null) + SheetAction( + key: AvatarAction.remove, + label: L10n.of(context)!.removeYourAvatar, + isDestructiveAction: true, + icon: Icons.delete_outlined, + ), + ]; + final action = actions.length == 1 + ? actions.single.key + : await showModalActionSheet( + context: context, + title: L10n.of(context)!.changeYourAvatar, + actions: actions, + ); + if (action == null) return; + final matrix = Matrix.of(context); + if (action == AvatarAction.remove) { + final success = await showFutureLoadingDialog( + context: context, + future: () => matrix.client.setAvatar(null), + ); + if (success.error == null) { + _getProfileFromUserId(isUpdated: true); + } + return; + } + MatrixFile file; + if (PlatformInfos.isMobile) { + final result = await ImagePicker().pickImage( + source: action == AvatarAction.camera + ? ImageSource.camera + : ImageSource.gallery, + imageQuality: 50, + ); + if (result == null) return; + file = MatrixFile( + bytes: await result.readAsBytes(), + name: result.path, + ); + } else { + final result = await FilePicker.platform.pickFiles( + type: FileType.image, + withData: true, + ); + final pickedFile = result?.files.firstOrNull; + if (pickedFile == null) return; + file = MatrixFile( + bytes: pickedFile.bytes!, + name: pickedFile.name, + ); + } + final success = await showFutureLoadingDialog( + context: context, + future: () => matrix.client.setAvatar(file), + ); + if (success.error == null) { + _getProfileFromUserId(isUpdated: true); + } + } + + void setDisplayNameAction() async { + if (displayNameFocusNode.hasFocus) { + displayNameFocusNode.unfocus(); + } + final matrix = Matrix.of(context); + final success = await showFutureLoadingDialog( + context: context, + future: () => matrix.client.setDisplayName( + matrix.client.userID!, + displayNameEditingController.text, + ), + ); + if (success.error == null) { + isEditedProfileNotifier.toggle(); + _getProfileFromUserId(isUpdated: true); + } + } + + void _getProfileFromUserId({ + isUpdated = false, + }) async { + final profile = await client.getProfileFromUserId( + client.userID!, + cache: !isUpdated, + getFromRooms: false, + ); + Logs().d( + 'SettingsProfile::_getProfileFromUserId() - avatarUrl: ${profile.avatarUrl} - displayName: ${profile.displayName} - userId: ${profile.userId}', + ); + settingsDashboardManagerController.profileNotifier.value = profile; + displayNameEditingController.text = displayName; + matrixIdEditingController.text = mxid; + } + + void handleTextEditOnChange(SettingsProfileEnum settingsProfileEnum) { + switch (settingsProfileEnum) { + case SettingsProfileEnum.displayName: + _listeningDisplayNameHasChange(); + break; + default: + break; + } + } + + void _listeningDisplayNameHasChange() { + isEditedProfileNotifier.value = + displayNameEditingController.text != displayName; + Logs().d( + 'SettingsProfile::_listeningDisplayNameHasChange() - ${isEditedProfileNotifier.value}', + ); + } + + void _initProfile() { + if (widget.profile == null) { + _getProfileFromUserId(); + return; + } + settingsDashboardManagerController.profileNotifier.value = widget.profile!; + displayNameEditingController.text = displayName; + matrixIdEditingController.text = mxid; + } + + @override + void initState() { + _initProfile(); + super.initState(); + } + + @override + void dispose() { + displayNameEditingController.dispose(); + matrixIdEditingController.dispose(); + displayNameFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SettingsProfileView( + controller: this, + ); + } +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart new file mode 100644 index 000000000..dc359e972 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class SettingsProfileItemStyle { + static const double iconSize = 24.0; + static const double dividerSize = 2.0; + + static const EdgeInsetsDirectional itemBuilderPadding = + EdgeInsetsDirectional.only(end: 8.0); +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart new file mode 100644 index 000000000..b85b66bdf --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -0,0 +1,217 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart'; +import 'package:fluffychat/presentation/model/settings/settings_profile_presentation.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +class SettingsProfileView extends StatelessWidget { + final SettingsProfileController controller; + + static const ValueKey settingsProfileViewMobileKey = + ValueKey('settingsProfileViewMobile'); + + static const ValueKey settingsProfileViewWebKey = + ValueKey('settingsProfileViewWeb'); + + const SettingsProfileView({ + super.key, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + final responsive = getIt.get(); + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon( + Icons.arrow_back, + size: 24, + ), + onPressed: () => context.pop(), + ), + title: Text( + L10n.of(context)!.profile, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + actions: [ + ValueListenableBuilder( + valueListenable: controller.isEditedProfileNotifier, + builder: (context, edited, _) { + if (!edited) return const SizedBox(); + return InkWell( + borderRadius: BorderRadius.circular( + 20, + ), + onTap: () => controller.setDisplayNameAction(), + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + vertical: 14, + horizontal: 12, + ), + child: Text( + L10n.of(context)!.done, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + }, + ), + ], + centerTitle: true, + ), + backgroundColor: responsive.isWebDesktop(context) + ? Theme.of(context).colorScheme.surface + : null, + body: SingleChildScrollView( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 16), + child: SlotLayout( + config: { + const WidthPlatformBreakpoint( + end: ResponsiveUtils.minDesktopWidth, + ): SlotLayout.from( + key: settingsProfileViewMobileKey, + builder: (_) { + return SettingsProfileViewMobile( + profileNotifier: controller + .settingsDashboardManagerController.profileNotifier, + displayName: controller.displayName, + onAvatarTap: () => controller.setAvatarAction(), + settingsProfileOptions: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return SettingsProfileItemBuilder( + settingsProfileEnum: + controller.getListProfileMobile[index], + title: controller.getListProfileMobile[index] + .getTitle(context), + settingsProfilePresentation: + SettingsProfilePresentation( + settingsProfileType: controller + .getListProfileMobile[index] + .getSettingsProfileType(), + ), + suffixIcon: controller.getListProfileMobile[index] + .getTrailingIcon(), + leadingIcon: controller.getListProfileMobile[index] + .getLeadingIcon(), + focusNode: controller.getFocusNode( + controller.getListProfileMobile[index], + ), + textEditingController: controller.getController( + controller.getListProfileMobile[index], + ), + onChange: (_, settingsProfileEnum) { + controller + .handleTextEditOnChange(settingsProfileEnum); + }, + ); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 16); + }, + itemCount: controller.getListProfileMobile.length, + ), + ); + }, + ), + const WidthPlatformBreakpoint( + begin: ResponsiveUtils.minDesktopWidth, + ): SlotLayout.from( + key: settingsProfileViewWebKey, + builder: (_) { + return SettingsProfileViewWeb( + profileNotifier: controller + .settingsDashboardManagerController.profileNotifier, + displayName: controller.displayName, + basicInfoWidget: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return SettingsProfileItemBuilder( + settingsProfileEnum: + controller.getListProfileBasicInfo[index], + title: controller.getListProfileBasicInfo[index] + .getTitle(context), + settingsProfilePresentation: + SettingsProfilePresentation( + settingsProfileType: controller + .getListProfileBasicInfo[index] + .getSettingsProfileType(), + ), + suffixIcon: controller.getListProfileBasicInfo[index] + .getTrailingIcon(), + focusNode: controller.getFocusNode( + controller.getListProfileBasicInfo[index], + ), + textEditingController: controller.getController( + controller.getListProfileBasicInfo[index], + ), + onChange: (_, settingsProfileEnum) { + controller + .handleTextEditOnChange(settingsProfileEnum); + }, + ); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 16); + }, + itemCount: controller.getListProfileBasicInfo.length, + ), + workIdentitiesInfoWidget: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return SettingsProfileItemBuilder( + settingsProfileEnum: + controller.getListProfileWorkIdentitiesInfo[index], + title: controller + .getListProfileWorkIdentitiesInfo[index] + .getTitle(context), + settingsProfilePresentation: + SettingsProfilePresentation( + settingsProfileType: controller + .getListProfileWorkIdentitiesInfo[index] + .getSettingsProfileType(), + ), + suffixIcon: controller + .getListProfileWorkIdentitiesInfo[index] + .getTrailingIcon(), + focusNode: controller.getFocusNode( + controller.getListProfileWorkIdentitiesInfo[index], + ), + textEditingController: controller.getController( + controller.getListProfileWorkIdentitiesInfo[index], + ), + onChange: (_, settingsProfileEnum) { + controller + .handleTextEditOnChange(settingsProfileEnum); + }, + ); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 16); + }, + itemCount: controller.getListProfileBasicInfo.length, + ), + onAvatarTap: () => controller.setAvatarAction(), + ); + }, + ), + }, + ), + ), + ); + } +} diff --git a/lib/pages/settings_dashboard/settings_style/settings_style.dart b/lib/pages/settings_dashboard/settings_style/settings_style.dart new file mode 100644 index 000000000..4f2bc6708 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_style/settings_style.dart @@ -0,0 +1,92 @@ +import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/widgets/theme_builder.dart'; +import 'settings_style_view.dart'; + +class SettingsStyle extends StatefulWidget { + const SettingsStyle({Key? key}) : super(key: key); + + @override + SettingsStyleController createState() => SettingsStyleController(); +} + +class SettingsStyleController extends State { + void setWallpaperAction() async { + final picked = await FilePicker.platform.pickFiles( + type: FileType.image, + withData: false, + ); + final pickedFile = picked?.files.firstOrNull; + + if (pickedFile == null) return; + await Matrix.of(context) + .store + .setItem(SettingKeys.wallpaper, pickedFile.path); + setState(() {}); + } + + void deleteWallpaperAction() async { + Matrix.of(context).wallpaper = null; + await Matrix.of(context).store.deleteItem(SettingKeys.wallpaper); + setState(() {}); + } + + void setChatColor(Color? color) async { + if (color != null) { + AppConfig.colorSchemeSeed = color; + ThemeController.of(context).setPrimaryColor(color); + } + } + + ThemeMode get currentTheme => ThemeController.of(context).themeMode; + Color? get currentColor => ThemeController.of(context).primaryColor; + + static final List customColors = [ + AppConfig.chatColor, + Colors.blue.shade800, + Colors.green.shade800, + Colors.orange.shade700, + Colors.pink.shade700, + Colors.blueGrey.shade600, + null, + ]; + + void switchTheme(ThemeMode? newTheme) { + if (newTheme == null) return; + switch (newTheme) { + case ThemeMode.light: + ThemeController.of(context).setThemeMode(ThemeMode.light); + break; + case ThemeMode.dark: + ThemeController.of(context).setThemeMode(ThemeMode.dark); + break; + case ThemeMode.system: + ThemeController.of(context).setThemeMode(ThemeMode.system); + break; + } + setState(() {}); + } + + void changeFontSizeFactor(double d) { + setState(() => AppConfig.fontSizeFactor = d); + Matrix.of(context).store.setItem( + SettingKeys.fontSizeFactor, + AppConfig.fontSizeFactor.toString(), + ); + } + + void changeBubbleSizeFactor(double d) { + setState(() => AppConfig.bubbleSizeFactor = d); + Matrix.of(context).store.setItem( + SettingKeys.bubbleSizeFactor, + AppConfig.bubbleSizeFactor.toString(), + ); + } + + @override + Widget build(BuildContext context) => SettingsStyleView(this); +} From 127f29a85d10c826825c7ccb19be52a3ddced322 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 29 Sep 2023 18:07:32 +0700 Subject: [PATCH 23/60] TW-692: Add SnackBar for copy and update highlight when select option in settings --- assets/l10n/intl_en.arb | 3 +- .../settings_dashboard/settings/settings.dart | 6 + .../settings/settings_item_builder.dart | 122 ++++++------ .../settings/settings_view.dart | 188 ++++++++++-------- .../settings/settings_view_style.dart | 4 +- .../settings_dashboard_manager.dart | 7 + .../settings_profile/settings_profile.dart | 24 +++ .../settings_profile_item.dart | 4 +- .../settings_profile_item_style.dart | 18 ++ .../settings_profile_view.dart | 8 +- .../settings_profile_view_mobile.dart | 8 +- .../settings_profile_view_web.dart | 5 +- .../enum/settings/settings_enum.dart | 5 + 13 files changed, 250 insertions(+), 152 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 0abad73dd..7916b90bd 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2754,5 +2754,6 @@ "basicInfo": "BASIC INFO", "editProfileDescriptions": "Update your profile with a new name, picture and a short introduction.", "workIdentitiesInfo": "WORK IDENTITIES INFO", - "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name." + "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name.", + "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard." } \ No newline at end of file diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 0ca1abafb..915f69bc2 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -126,6 +126,8 @@ class SettingsController extends State with ConnectPageMixin { } void goToSettingsProfile(Profile? profile) async { + settingsDashboardManagerController.optionsSelectNotifier.value = + SettingEnum.profile; context.push( '/rooms/profile', extra: profile, @@ -133,6 +135,8 @@ class SettingsController extends State with ConnectPageMixin { } void onClickToSettingsItem(SettingEnum settingEnum) { + settingsDashboardManagerController.optionsSelectNotifier.value = + settingEnum; switch (settingEnum) { case SettingEnum.chatSettings: context.go('/rooms/chat'); @@ -159,6 +163,8 @@ class SettingsController extends State with ConnectPageMixin { case SettingEnum.logout: logoutAction(); break; + default: + break; } } diff --git a/lib/pages/settings_dashboard/settings/settings_item_builder.dart b/lib/pages/settings_dashboard/settings/settings_item_builder.dart index 7830221ad..5247fec5b 100644 --- a/lib/pages/settings_dashboard/settings/settings_item_builder.dart +++ b/lib/pages/settings_dashboard/settings/settings_item_builder.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings_view_style.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; @@ -8,6 +9,7 @@ class SettingsItemBuilder extends StatelessWidget { final IconData leading; final VoidCallback onTap; final bool isHideTrailingIcon; + final bool isSelected; const SettingsItemBuilder({ super.key, @@ -16,75 +18,83 @@ class SettingsItemBuilder extends StatelessWidget { required this.leading, required this.onTap, this.isHideTrailingIcon = false, + this.isSelected = false, }); @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Padding( - padding: SettingsViewStyle.itemBuilderPadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: SettingsViewStyle.leadingItemBuilderPadding, - child: Icon( - leading, - size: SettingsViewStyle.iconSize, - color: isHideTrailingIcon - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.onSurfaceVariant, + return Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + color: + isSelected ? Theme.of(context).colorScheme.secondaryContainer : null, + child: InkWell( + onTap: onTap, + child: Padding( + padding: SettingsViewStyle.itemBuilderPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: SettingsViewStyle.leadingItemBuilderPadding, + child: Icon( + leading, + size: SettingsViewStyle.iconSize, + color: isHideTrailingIcon + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onSurfaceVariant, + ), ), - ), - Expanded( - child: Row( - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - color: isHideTrailingIcon - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.onSurface, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - Padding( - padding: SettingsViewStyle.subtitleItemBuilderPadding, - child: Text( - subtitle, + Expanded( + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, style: Theme.of(context) .textTheme - .bodySmall + .titleMedium ?.copyWith( - color: - LinagoraRefColors.material().neutral[40], + color: isHideTrailingIcon + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onSurface, ), - maxLines: 3, + maxLines: 2, overflow: TextOverflow.ellipsis, ), - ), - ], - ), - ), - if (!isHideTrailingIcon) - const Icon( - Icons.chevron_right_outlined, - size: SettingsViewStyle.iconSize, + Padding( + padding: + SettingsViewStyle.subtitleItemBuilderPadding, + child: Text( + subtitle, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: LinagoraRefColors.material() + .neutral[40], + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), - ], + if (!isHideTrailingIcon) + const Icon( + Icons.chevron_right_outlined, + size: SettingsViewStyle.iconSize, + ), + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart index 558382b70..94b55de0f 100644 --- a/lib/pages/settings_dashboard/settings/settings_view.dart +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -1,5 +1,7 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings_item_builder.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings_view_style.dart'; +import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; @@ -44,90 +46,105 @@ class SettingsView extends StatelessWidget { builder: (context, profile, _) { return Padding( padding: SettingsViewStyle.bodySettingsScreenPadding, - child: InkWell( - onTap: () => controller.goToSettingsProfile(profile), - child: Row( - children: [ - Padding( - padding: SettingsViewStyle.avatarPadding, - child: Stack( - children: [ - Material( - elevation: Theme.of(context) + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + color: controller.settingsDashboardManagerController + .optionsSelectNotifier.value == + SettingEnum.profile + ? Theme.of(context).colorScheme.secondaryContainer + : null, + child: InkWell( + onTap: () => controller.goToSettingsProfile(profile), + child: Padding( + padding: SettingsViewStyle.itemBuilderPadding, + child: Row( + children: [ + Padding( + padding: SettingsViewStyle.avatarPadding, + child: Stack( + children: [ + Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context) .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: Theme.of(context) - .appBarTheme - .shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize, + .shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: + Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, + ), + ), + child: Avatar( + mxContent: profile.avatarUrl, + name: controller.displayName, + size: AvatarStyle.defaultSize, + fontSize: + SettingsViewStyle.fontSizeAvatar, + ), ), - ), - child: Avatar( - mxContent: profile.avatarUrl, - name: controller.displayName, - size: AvatarStyle.defaultSize, - fontSize: 18 * 2.5, - ), + ], ), - ], - ), - ), - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - profile.displayName ?? - controller.displayName, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - controller.mxid, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - LinagoraRefColors.material() + ), + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + profile.displayName ?? + controller.displayName, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + controller.mxid, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: LinagoraRefColors + .material() .neutral[40], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), - ], - ), - ), - const Icon( - Icons.chevron_right_outlined, - size: SettingsViewStyle.iconSize, + ), + const Icon( + Icons.chevron_right_outlined, + size: SettingsViewStyle.iconSize, + ), + ], ), - ], - ), + ), + ], ), - ], + ), ), ), ); @@ -138,12 +155,17 @@ class SettingsView extends StatelessWidget { const Divider(thickness: 1), Column( children: controller.getListSettingItem().map((item) { - return SettingsItemBuilder( - title: item.titleSettings(context), - subtitle: item.subtitleSettings(context), - leading: item.iconLeading(), - onTap: () => controller.onClickToSettingsItem(item), - isHideTrailingIcon: item.isHideTrailingIcon, + return Padding( + padding: SettingsViewStyle.bodySettingsScreenPadding, + child: SettingsItemBuilder( + title: item.titleSettings(context), + subtitle: item.subtitleSettings(context), + leading: item.iconLeading(), + onTap: () => controller.onClickToSettingsItem(item), + isHideTrailingIcon: item.isHideTrailingIcon, + isSelected: controller.settingsDashboardManagerController + .optionSelected(item), + ), ); }).toList(), ), diff --git a/lib/pages/settings_dashboard/settings/settings_view_style.dart b/lib/pages/settings_dashboard/settings/settings_view_style.dart index a58c6d2db..d131430c7 100644 --- a/lib/pages/settings_dashboard/settings/settings_view_style.dart +++ b/lib/pages/settings_dashboard/settings/settings_view_style.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; class SettingsViewStyle { static const double iconSize = 24.0; + static const double fontSizeAvatar = 9 * 2.5; + static EdgeInsetsDirectional itemBuilderPadding = const EdgeInsetsDirectional.all(16.0); @@ -14,7 +16,7 @@ class SettingsViewStyle { static EdgeInsetsDirectional bodySettingsScreenPadding = const EdgeInsetsDirectional.symmetric( - horizontal: 16, + horizontal: 8, ); static EdgeInsetsDirectional avatarPadding = diff --git a/lib/pages/settings_dashboard/settings_dashboard_manager.dart b/lib/pages/settings_dashboard/settings_dashboard_manager.dart index e5b7d7b3c..14b79bc9f 100644 --- a/lib/pages/settings_dashboard/settings_dashboard_manager.dart +++ b/lib/pages/settings_dashboard/settings_dashboard_manager.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -30,6 +31,7 @@ class SettingsDashboardManagerController { } late ValueNotifier profileNotifier; + late ValueNotifier optionsSelectNotifier; bool initialized = false; @@ -38,6 +40,8 @@ class SettingsDashboardManagerController { profileNotifier = ValueNotifier( Profile(userId: ''), ); + + optionsSelectNotifier = ValueNotifier(null); } String mxid(BuildContext context) => @@ -47,4 +51,7 @@ class SettingsDashboardManagerController { profileNotifier.value.displayName ?? mxid(context).localpart ?? mxid(context); + + bool optionSelected(SettingEnum settingEnum) => + settingEnum == optionsSelectNotifier.value; } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 33fde5b81..56dc22c5b 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -1,12 +1,14 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_dashboard_manager.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; @@ -217,6 +219,28 @@ class SettingsProfileController extends State { matrixIdEditingController.text = mxid; } + void copyEventsAction(SettingsProfileEnum settingsProfileEnum) { + switch (settingsProfileEnum) { + case SettingsProfileEnum.matrixId: + Clipboard.setData(ClipboardData(text: mxid)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + width: SettingsProfileItemStyle.widthSnackBar(context), + padding: SettingsProfileItemStyle.snackBarPadding, + content: Text( + L10n.of(context)!.copiedMatrixIdToClipboard, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.background, + ), + ), + ), + ); + break; + default: + break; + } + } + @override void initState() { _initProfile(); diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart index d4352905b..3ca594a2c 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart @@ -13,6 +13,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { final IconData suffixIcon; final IconData? leadingIcon; final void Function(String, SettingsProfileEnum)? onChange; + final VoidCallback? onCopyAction; const SettingsProfileItemBuilder({ super.key, @@ -24,6 +25,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { required this.suffixIcon, this.leadingIcon, this.onChange, + this.onCopyAction, }); @override @@ -68,7 +70,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { ? () { focusNode?.requestFocus(); } - : () {}, + : onCopyAction, icon: Icon( suffixIcon, size: SettingsProfileItemStyle.iconSize, diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart index dc359e972..1c27526b3 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart @@ -1,9 +1,27 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; class SettingsProfileItemStyle { + static ResponsiveUtils responsiveUtils = getIt.get(); + static const double iconSize = 24.0; static const double dividerSize = 2.0; static const EdgeInsetsDirectional itemBuilderPadding = EdgeInsetsDirectional.only(end: 8.0); + + static const EdgeInsetsDirectional snackBarPadding = + EdgeInsetsDirectional.symmetric( + horizontal: 16, + vertical: 14, + ); + + static double? widthSnackBar(BuildContext context) { + if (responsiveUtils.isWebDesktop(context)) { + return 334; + } else { + return null; + } + } } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart index b85b66bdf..88f22f2ba 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -85,7 +85,6 @@ class SettingsProfileView extends StatelessWidget { return SettingsProfileViewMobile( profileNotifier: controller .settingsDashboardManagerController.profileNotifier, - displayName: controller.displayName, onAvatarTap: () => controller.setAvatarAction(), settingsProfileOptions: ListView.separated( shrinkWrap: true, @@ -116,6 +115,9 @@ class SettingsProfileView extends StatelessWidget { controller .handleTextEditOnChange(settingsProfileEnum); }, + onCopyAction: () => controller.copyEventsAction( + controller.getListProfileMobile[index], + ), ); }, separatorBuilder: (context, index) { @@ -134,7 +136,6 @@ class SettingsProfileView extends StatelessWidget { return SettingsProfileViewWeb( profileNotifier: controller .settingsDashboardManagerController.profileNotifier, - displayName: controller.displayName, basicInfoWidget: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -198,6 +199,9 @@ class SettingsProfileView extends StatelessWidget { controller .handleTextEditOnChange(settingsProfileEnum); }, + onCopyAction: () => controller.copyEventsAction( + controller.getListProfileWorkIdentitiesInfo[index], + ), ); }, separatorBuilder: (context, index) { diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart index 3556cb3db..e064d04dc 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -7,7 +7,6 @@ import 'package:matrix/matrix.dart'; class SettingsProfileViewMobile extends StatelessWidget { final ValueNotifier profileNotifier; - final String displayName; final Widget settingsProfileOptions; final VoidCallback onAvatarTap; @@ -15,7 +14,6 @@ class SettingsProfileViewMobile extends StatelessWidget { super.key, required this.profileNotifier, required this.settingsProfileOptions, - required this.displayName, required this.onAvatarTap, }); @@ -23,7 +21,8 @@ class SettingsProfileViewMobile extends StatelessWidget { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: profileNotifier, - builder: (context, _, __) { + builder: (context, profile, __) { + final displayName = profile.displayName ?? profile.userId; return Column( children: [ Divider( @@ -57,8 +56,7 @@ class SettingsProfileViewMobile extends StatelessWidget { mxContent: profileNotifier.value.avatarUrl, name: displayName, size: SettingsProfileViewMobileStyle.avatarSize, - fontSize: - SettingsProfileViewMobileStyle.positionedRightSize, + fontSize: SettingsProfileViewMobileStyle.avatarFontSize, ), ), Positioned( diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart index b1c2d198a..7551dce5e 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart @@ -8,7 +8,6 @@ import 'package:matrix/matrix.dart'; class SettingsProfileViewWeb extends StatelessWidget { final ValueNotifier profileNotifier; - final String displayName; final Widget basicInfoWidget; final Widget workIdentitiesInfoWidget; final VoidCallback onAvatarTap; @@ -16,7 +15,6 @@ class SettingsProfileViewWeb extends StatelessWidget { const SettingsProfileViewWeb({ super.key, required this.profileNotifier, - required this.displayName, required this.basicInfoWidget, required this.onAvatarTap, required this.workIdentitiesInfoWidget, @@ -26,7 +24,8 @@ class SettingsProfileViewWeb extends StatelessWidget { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: profileNotifier, - builder: (context, _, __) { + builder: (context, profile, __) { + final displayName = profile.displayName ?? profile.userId; return Padding( padding: SettingsProfileViewWebStyle.paddingBody, child: Center( diff --git a/lib/presentation/enum/settings/settings_enum.dart b/lib/presentation/enum/settings/settings_enum.dart index 75546b7f0..889993dc4 100644 --- a/lib/presentation/enum/settings/settings_enum.dart +++ b/lib/presentation/enum/settings/settings_enum.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; enum SettingEnum { + profile, chatSettings, privacyAndSecurity, notificationAndSounds, @@ -29,6 +30,8 @@ enum SettingEnum { return L10n.of(context)!.help; case SettingEnum.logout: return L10n.of(context)!.logout; + default: + return ''; } } @@ -71,6 +74,8 @@ enum SettingEnum { return Icons.question_mark; case SettingEnum.logout: return Icons.logout_outlined; + default: + return Icons.person_outline; } } From 6308787c84540aabfcf164d751d40ccde2c39165 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 3 Oct 2023 14:54:05 +0700 Subject: [PATCH 24/60] TW-692: Create `TwakeEventDispatcher`, `TwakeInappEventTypes` to use stream and handle when updated --- lib/event/twake_event_dispatcher.dart | 19 +++++++++++++++++++ lib/event/twake_inapp_event_types.dart | 3 +++ 2 files changed, 22 insertions(+) create mode 100644 lib/event/twake_event_dispatcher.dart create mode 100644 lib/event/twake_inapp_event_types.dart diff --git a/lib/event/twake_event_dispatcher.dart b/lib/event/twake_event_dispatcher.dart new file mode 100644 index 000000000..5d16a25d9 --- /dev/null +++ b/lib/event/twake_event_dispatcher.dart @@ -0,0 +1,19 @@ +import 'package:matrix/matrix.dart'; + +class TwakeEventDispatcher { + static final TwakeEventDispatcher _twakeEventDispatcher = + TwakeEventDispatcher._instance(); + + factory TwakeEventDispatcher() { + return _twakeEventDispatcher; + } + + TwakeEventDispatcher._instance(); + + void sendAccountDataEvent({ + required Client client, + required BasicEvent basicEvent, + }) { + client.onAccountData.add(basicEvent); + } +} diff --git a/lib/event/twake_inapp_event_types.dart b/lib/event/twake_inapp_event_types.dart new file mode 100644 index 000000000..c2a5ffc54 --- /dev/null +++ b/lib/event/twake_inapp_event_types.dart @@ -0,0 +1,3 @@ +class TwakeInappEventTypes { + static const String uploadAvatarEvent = 'app.twake.inapp.profile.avatar'; +} From e1e42bf1a126d1ee649c5141e4121681360bfc70 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 3 Oct 2023 14:54:38 +0700 Subject: [PATCH 25/60] TW-692: Create `FetchProfileMixin` to handle fetch profile --- .../mixins/fetch_profile_mixin.dart | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 lib/presentation/mixins/fetch_profile_mixin.dart diff --git a/lib/presentation/mixins/fetch_profile_mixin.dart b/lib/presentation/mixins/fetch_profile_mixin.dart new file mode 100644 index 000000000..dfecb56ca --- /dev/null +++ b/lib/presentation/mixins/fetch_profile_mixin.dart @@ -0,0 +1,35 @@ +import 'package:fluffychat/event/twake_inapp_event_types.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +mixin FetchProfileMixin { + final ValueNotifier profileNotifier = ValueNotifier( + Profile(userId: ''), + ); + + void getCurrentProfile( + Client client, { + isUpdated = false, + }) async { + final profile = await client.getProfileFromUserId( + client.userID!, + cache: !isUpdated, + getFromRooms: false, + ); + Logs().d( + 'FetchProfileMixin::_getCurrentProfile() - currentProfile: $profile', + ); + profileNotifier.value = profile; + } + + void handleOnAccountData(Client client) { + client.onAccountData.stream.listen((event) { + Logs().d( + 'FetchProfileMixin::onAccountData() - EventType: ${event.type} - EventContent: ${event.content}', + ); + if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { + profileNotifier.value = Profile.fromJson(event.content); + } + }); + } +} From fe29ab4ac90d42bf02032d2ed7f16cdabd465ebd Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 3 Oct 2023 14:55:09 +0700 Subject: [PATCH 26/60] TW-692: Handle sync profile for stream --- lib/config/app_config.dart | 1 + lib/config/go_routes/go_router.dart | 11 - lib/di/global/get_it_initializer.dart | 2 + .../settings_dashboard/settings/settings.dart | 52 ++--- .../settings/settings_view.dart | 202 +++++++++--------- .../settings_dashboard_manager.dart | 57 ----- .../settings_profile/settings_profile.dart | 198 ++++++++++------- .../settings_profile_view.dart | 6 +- .../extensions/client_extension.dart | 4 + .../adaptive_layout/adaptive_scaffold.dart | 26 +-- .../adaptive_scaffold_primary_navigation.dart | 20 +- .../adaptive_scaffold_view.dart | 6 +- 12 files changed, 270 insertions(+), 315 deletions(-) delete mode 100644 lib/pages/settings_dashboard/settings_dashboard_manager.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 35066235e..5d4e844f0 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -75,6 +75,7 @@ abstract class AppConfig { static const String defaultVideoBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; static const int thumbnailQuality = 70; static const int blurHashSize = 32; + static const int imageQuality = 50; static String? issueId; diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index d1154a0d9..4e3064088 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pages/chat_draft/draft_chat.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/pages/error_page/error_page.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; -import 'package:fluffychat/pages/settings_dashboard/settings_dashboard_manager.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/share/share.dart'; import 'package:fluffychat/pages/story/story_page.dart'; @@ -253,16 +252,6 @@ abstract class AppRoutes { profile: state.extra as Profile?, ), ), - redirect: (context, state) { - final settingsDashboardManagerController = - SettingsDashboardManagerController(); - - if (!settingsDashboardManagerController.initialized) { - return '/rooms'; - } - - return Matrix.of(context).client.isLogged() ? null : '/home'; - }, ), GoRoute( path: 'notifications', diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 8deeb4c4f..30c3ecf94 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -41,6 +41,7 @@ import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/domain/usecase/send_image_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; +import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:get_it/get_it.dart'; @@ -71,6 +72,7 @@ class GetItInitializer { HiveDI().bind(); NetworkConnectivityDI().bind(); getIt.registerSingleton(ResponsiveUtils()); + getIt.registerSingleton(TwakeEventDispatcher()); } void bindingQueue() { diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 915f69bc2..050e2c143 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -3,8 +3,9 @@ import 'package:fluffychat/data/hive/hive_collection_tom_database.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; -import 'package:fluffychat/pages/settings_dashboard/settings_dashboard_manager.dart'; import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; +import 'package:fluffychat/presentation/mixins/fetch_profile_mixin.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -29,26 +30,28 @@ class Settings extends StatefulWidget { SettingsController createState() => SettingsController(); } -class SettingsController extends State with ConnectPageMixin { - late SettingsDashboardManagerController settingsDashboardManagerController; - - List getListSettingItem() { - return [ - SettingEnum.chatSettings, - SettingEnum.privacyAndSecurity, - SettingEnum.notificationAndSounds, - SettingEnum.chatFolders, - SettingEnum.appLanguage, - SettingEnum.devices, - SettingEnum.help, - SettingEnum.logout, - ]; - } - - String get mxid => settingsDashboardManagerController.mxid(context); +class SettingsController extends State + with ConnectPageMixin, FetchProfileMixin { + final List getListSettingItem = [ + SettingEnum.chatSettings, + SettingEnum.privacyAndSecurity, + SettingEnum.notificationAndSounds, + SettingEnum.chatFolders, + SettingEnum.appLanguage, + SettingEnum.devices, + SettingEnum.help, + SettingEnum.logout, + ]; + + final ValueNotifier optionsSelectNotifier = ValueNotifier(null); String get displayName => - settingsDashboardManagerController.displayName(context); + profileNotifier.value.displayName ?? + client.mxid(context).localpart ?? + client.mxid(context); + + bool optionSelected(SettingEnum settingEnum) => + settingEnum == optionsSelectNotifier.value; void logoutAction() async { final noBackup = showChatBackupBanner == true; @@ -78,9 +81,8 @@ class SettingsController extends State with ConnectPageMixin { @override void initState() { - settingsDashboardManagerController = - SettingsDashboardManagerController.instance; - settingsDashboardManagerController.getCurrentProfile(client); + getCurrentProfile(client); + handleOnAccountData(client); WidgetsBinding.instance.addPostFrameCallback((_) { checkBootstrap(); }); @@ -126,8 +128,7 @@ class SettingsController extends State with ConnectPageMixin { } void goToSettingsProfile(Profile? profile) async { - settingsDashboardManagerController.optionsSelectNotifier.value = - SettingEnum.profile; + optionsSelectNotifier.value = SettingEnum.profile; context.push( '/rooms/profile', extra: profile, @@ -135,8 +136,7 @@ class SettingsController extends State with ConnectPageMixin { } void onClickToSettingsItem(SettingEnum settingEnum) { - settingsDashboardManagerController.optionsSelectNotifier.value = - settingEnum; + optionsSelectNotifier.value = settingEnum; switch (settingEnum) { case SettingEnum.chatSettings: context.go('/rooms/chat'); diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart index 94b55de0f..bd76e69b3 100644 --- a/lib/pages/settings_dashboard/settings/settings_view.dart +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings_item_builder.dart'; import 'package:fluffychat/pages/settings_dashboard/settings/settings_view_style.dart'; import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; @@ -38,123 +39,115 @@ class SettingsView extends StatelessWidget { child: ListView( key: const Key('SettingsListViewContent'), children: [ - Builder( - builder: (context) { - return ValueListenableBuilder( - valueListenable: controller - .settingsDashboardManagerController.profileNotifier, - builder: (context, profile, _) { - return Padding( - padding: SettingsViewStyle.bodySettingsScreenPadding, - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - color: controller.settingsDashboardManagerController - .optionsSelectNotifier.value == - SettingEnum.profile - ? Theme.of(context).colorScheme.secondaryContainer - : null, - child: InkWell( - onTap: () => controller.goToSettingsProfile(profile), - child: Padding( - padding: SettingsViewStyle.itemBuilderPadding, - child: Row( - children: [ - Padding( - padding: SettingsViewStyle.avatarPadding, - child: Stack( - children: [ - Material( - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: Theme.of(context) + ValueListenableBuilder( + valueListenable: controller.profileNotifier, + builder: (context, profile, _) { + return Padding( + padding: SettingsViewStyle.bodySettingsScreenPadding, + child: Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + color: controller.optionsSelectNotifier.value == + SettingEnum.profile + ? Theme.of(context).colorScheme.secondaryContainer + : null, + child: InkWell( + onTap: () => controller.goToSettingsProfile(profile), + child: Padding( + padding: SettingsViewStyle.itemBuilderPadding, + child: Row( + children: [ + Padding( + padding: SettingsViewStyle.avatarPadding, + child: Stack( + children: [ + Material( + elevation: Theme.of(context) .appBarTheme - .shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: - Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize, - ), - ), - child: Avatar( - mxContent: profile.avatarUrl, - name: controller.displayName, - size: AvatarStyle.defaultSize, - fontSize: - SettingsViewStyle.fontSizeAvatar, - ), + .scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context) + .appBarTheme + .shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, ), - ], + ), + child: Avatar( + mxContent: profile.avatarUrl, + name: controller.displayName, + size: AvatarStyle.defaultSize, + fontSize: + SettingsViewStyle.fontSizeAvatar, + ), ), - ), - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - profile.displayName ?? - controller.displayName, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - controller.mxid, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: LinagoraRefColors - .material() + ], + ), + ), + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + profile.displayName ?? + controller.displayName, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + controller.client.mxid(context), + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: + LinagoraRefColors.material() .neutral[40], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ), - const Icon( - Icons.chevron_right_outlined, - size: SettingsViewStyle.iconSize, - ), - ], + ], + ), ), - ), - ], + const Icon( + Icons.chevron_right_outlined, + size: SettingsViewStyle.iconSize, + ), + ], + ), ), - ), + ], ), ), - ); - }, + ), + ), ); }, ), const Divider(thickness: 1), Column( - children: controller.getListSettingItem().map((item) { + children: controller.getListSettingItem.map((item) { return Padding( padding: SettingsViewStyle.bodySettingsScreenPadding, child: SettingsItemBuilder( @@ -163,8 +156,7 @@ class SettingsView extends StatelessWidget { leading: item.iconLeading(), onTap: () => controller.onClickToSettingsItem(item), isHideTrailingIcon: item.isHideTrailingIcon, - isSelected: controller.settingsDashboardManagerController - .optionSelected(item), + isSelected: controller.optionSelected(item), ), ); }).toList(), diff --git a/lib/pages/settings_dashboard/settings_dashboard_manager.dart b/lib/pages/settings_dashboard/settings_dashboard_manager.dart deleted file mode 100644 index 14b79bc9f..000000000 --- a/lib/pages/settings_dashboard/settings_dashboard_manager.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -class SettingsDashboardManagerController { - SettingsDashboardManagerController._privateConstructor(); - - static SettingsDashboardManagerController get instance { - _instance.initProfileNotifier(); - return _instance; - } - - static final SettingsDashboardManagerController _instance = - SettingsDashboardManagerController._privateConstructor(); - - factory SettingsDashboardManagerController() { - return _instance; - } - - void getCurrentProfile(Client client) async { - final profile = await client.getProfileFromUserId( - client.userID!, - getFromRooms: false, - ); - Logs().v( - 'SettingsDashboardManagerController::_getCurrentProfile() - currentProfile: $profile', - ); - profileNotifier.value = profile; - } - - late ValueNotifier profileNotifier; - late ValueNotifier optionsSelectNotifier; - - bool initialized = false; - - void initProfileNotifier() { - initialized = true; - profileNotifier = ValueNotifier( - Profile(userId: ''), - ); - - optionsSelectNotifier = ValueNotifier(null); - } - - String mxid(BuildContext context) => - Matrix.of(context).client.userID ?? L10n.of(context)!.user; - - String displayName(BuildContext context) => - profileNotifier.value.displayName ?? - mxid(context).localpart ?? - mxid(context); - - bool optionSelected(SettingEnum settingEnum) => - settingEnum == optionsSelectNotifier.value; -} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 56dc22c5b..7dbbb2f66 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -1,9 +1,14 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/pages/settings_dashboard/settings_dashboard_manager.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/event/twake_event_dispatcher.dart'; +import 'package:fluffychat/event/twake_inapp_event_types.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; +import 'package:fluffychat/presentation/mixins/fetch_profile_mixin.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -26,18 +31,21 @@ class SettingsProfile extends StatefulWidget { State createState() => SettingsProfileController(); } -class SettingsProfileController extends State { - final settingsDashboardManagerController = - SettingsDashboardManagerController(); +class SettingsProfileController extends State + with FetchProfileMixin { + final TwakeEventDispatcher twakeEventDispatcher = + getIt.get(); final ValueNotifier isEditedProfileNotifier = ValueNotifier(false); Client get client => Matrix.of(context).client; - String get mxid => settingsDashboardManagerController.mxid(context); + MatrixState get matrix => Matrix.of(context); String get displayName => - settingsDashboardManagerController.displayName(context); + profileNotifier.value.displayName ?? + client.mxid(context).localpart ?? + client.mxid(context); final TextEditingController displayNameEditingController = TextEditingController(); @@ -51,14 +59,36 @@ class SettingsProfileController extends State { List get getListProfileMobile => getListProfileBasicInfo + getListProfileWorkIdentitiesInfo; - List getListProfileBasicInfo = [ + final List getListProfileBasicInfo = [ SettingsProfileEnum.displayName, ]; - List getListProfileWorkIdentitiesInfo = [ + final List getListProfileWorkIdentitiesInfo = [ SettingsProfileEnum.matrixId ]; + List> actions() => [ + if (PlatformInfos.isMobile) + SheetAction( + key: AvatarAction.camera, + label: L10n.of(context)!.openCamera, + isDefaultAction: true, + icon: Icons.camera_alt_outlined, + ), + SheetAction( + key: AvatarAction.file, + label: L10n.of(context)!.openGallery, + icon: Icons.photo_outlined, + ), + if (profileNotifier.value.avatarUrl != null) + SheetAction( + key: AvatarAction.remove, + label: L10n.of(context)!.removeYourAvatar, + isDestructiveAction: true, + icon: Icons.delete_outlined, + ), + ]; + TextEditingController? getController( SettingsProfileEnum settingsProfileEnum, ) { @@ -81,82 +111,79 @@ class SettingsProfileController extends State { } } - void setAvatarAction() async { - final actions = [ - if (PlatformInfos.isMobile) - SheetAction( - key: AvatarAction.camera, - label: L10n.of(context)!.openCamera, - isDefaultAction: true, - icon: Icons.camera_alt_outlined, - ), - SheetAction( - key: AvatarAction.file, - label: L10n.of(context)!.openGallery, - icon: Icons.photo_outlined, - ), - if (settingsDashboardManagerController.profileNotifier.value.avatarUrl != - null) - SheetAction( - key: AvatarAction.remove, - label: L10n.of(context)!.removeYourAvatar, - isDestructiveAction: true, - icon: Icons.delete_outlined, - ), - ]; - final action = actions.length == 1 - ? actions.single.key - : await showModalActionSheet( - context: context, - title: L10n.of(context)!.changeYourAvatar, - actions: actions, - ); - if (action == null) return; - final matrix = Matrix.of(context); - if (action == AvatarAction.remove) { - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(null), - ); - if (success.error == null) { - _getProfileFromUserId(isUpdated: true); - } - return; + void _handleRemoveAvatarAction() async { + final success = await showFutureLoadingDialog( + context: context, + future: () => matrix.client.setAvatar(null), + ); + if (success.error == null) { + getCurrentProfile(client, isUpdated: true); } + return; + } + + Future _handleGetAvatarInByte() async { + final result = await FilePicker.platform.pickFiles( + type: FileType.image, + withData: true, + ); + final pickedFile = result?.files.firstOrNull; + if (pickedFile == null || pickedFile.bytes == null) return null; + return MatrixFile( + bytes: pickedFile.bytes!, + name: pickedFile.name, + ); + } + + Future _handleGetAvatarInStream(AvatarAction action) async { + final result = await ImagePicker().pickImage( + source: action == AvatarAction.camera + ? ImageSource.camera + : ImageSource.gallery, + imageQuality: AppConfig.imageQuality, + ); + if (result == null) return null; + return MatrixFile( + bytes: await result.readAsBytes(), + name: result.path, + ); + } + + void _handleGetAvatarAction(AvatarAction action) async { MatrixFile file; if (PlatformInfos.isMobile) { - final result = await ImagePicker().pickImage( - source: action == AvatarAction.camera - ? ImageSource.camera - : ImageSource.gallery, - imageQuality: 50, - ); - if (result == null) return; - file = MatrixFile( - bytes: await result.readAsBytes(), - name: result.path, - ); + final matrixFile = await _handleGetAvatarInStream(action); + if (matrixFile == null) return; + file = matrixFile; } else { - final result = await FilePicker.platform.pickFiles( - type: FileType.image, - withData: true, - ); - final pickedFile = result?.files.firstOrNull; - if (pickedFile == null) return; - file = MatrixFile( - bytes: pickedFile.bytes!, - name: pickedFile.name, - ); + final matrixFile = await _handleGetAvatarInByte(); + if (matrixFile == null) return; + file = matrixFile; } final success = await showFutureLoadingDialog( context: context, future: () => matrix.client.setAvatar(file), ); if (success.error == null) { - _getProfileFromUserId(isUpdated: true); + getCurrentProfile(client, isUpdated: true); } } + void setAvatarAction() async { + final action = actions().length == 1 + ? actions().single.key + : await showModalActionSheet( + context: context, + title: L10n.of(context)!.changeYourAvatar, + actions: actions(), + ); + if (action == null) return; + if (action == AvatarAction.remove) { + _handleRemoveAvatarAction(); + } + _handleGetAvatarAction(action); + } + void setDisplayNameAction() async { if (displayNameFocusNode.hasFocus) { displayNameFocusNode.unfocus(); @@ -171,11 +198,13 @@ class SettingsProfileController extends State { ); if (success.error == null) { isEditedProfileNotifier.toggle(); - _getProfileFromUserId(isUpdated: true); + getCurrentProfile(client, isUpdated: true); } } - void _getProfileFromUserId({ + @override + void getCurrentProfile( + Client client, { isUpdated = false, }) async { final profile = await client.getProfileFromUserId( @@ -184,11 +213,18 @@ class SettingsProfileController extends State { getFromRooms: false, ); Logs().d( - 'SettingsProfile::_getProfileFromUserId() - avatarUrl: ${profile.avatarUrl} - displayName: ${profile.displayName} - userId: ${profile.userId}', + 'SettingsProfileController::_getCurrentProfile() - currentProfile: $profile', + ); + profileNotifier.value = profile; + twakeEventDispatcher.sendAccountDataEvent( + client: client, + basicEvent: BasicEvent( + type: TwakeInappEventTypes.uploadAvatarEvent, + content: profile.toJson(), + ), ); - settingsDashboardManagerController.profileNotifier.value = profile; displayNameEditingController.text = displayName; - matrixIdEditingController.text = mxid; + matrixIdEditingController.text = client.mxid(context); } void handleTextEditOnChange(SettingsProfileEnum settingsProfileEnum) { @@ -205,24 +241,24 @@ class SettingsProfileController extends State { isEditedProfileNotifier.value = displayNameEditingController.text != displayName; Logs().d( - 'SettingsProfile::_listeningDisplayNameHasChange() - ${isEditedProfileNotifier.value}', + 'SettingsProfileController::_listeningDisplayNameHasChange() - ${isEditedProfileNotifier.value}', ); } void _initProfile() { if (widget.profile == null) { - _getProfileFromUserId(); + getCurrentProfile(client); return; } - settingsDashboardManagerController.profileNotifier.value = widget.profile!; + profileNotifier.value = widget.profile!; displayNameEditingController.text = displayName; - matrixIdEditingController.text = mxid; + matrixIdEditingController.text = client.mxid(context); } void copyEventsAction(SettingsProfileEnum settingsProfileEnum) { switch (settingsProfileEnum) { case SettingsProfileEnum.matrixId: - Clipboard.setData(ClipboardData(text: mxid)); + Clipboard.setData(ClipboardData(text: client.mxid(context))); ScaffoldMessenger.of(context).showSnackBar( SnackBar( width: SettingsProfileItemStyle.widthSnackBar(context), diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart index 88f22f2ba..82134260f 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -83,8 +83,7 @@ class SettingsProfileView extends StatelessWidget { key: settingsProfileViewMobileKey, builder: (_) { return SettingsProfileViewMobile( - profileNotifier: controller - .settingsDashboardManagerController.profileNotifier, + profileNotifier: controller.profileNotifier, onAvatarTap: () => controller.setAvatarAction(), settingsProfileOptions: ListView.separated( shrinkWrap: true, @@ -134,8 +133,7 @@ class SettingsProfileView extends StatelessWidget { key: settingsProfileViewWebKey, builder: (_) { return SettingsProfileViewWeb( - profileNotifier: controller - .settingsDashboardManagerController.profileNotifier, + profileNotifier: controller.profileNotifier, basicInfoWidget: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), diff --git a/lib/presentation/extensions/client_extension.dart b/lib/presentation/extensions/client_extension.dart index 89a599277..0c813ebcf 100644 --- a/lib/presentation/extensions/client_extension.dart +++ b/lib/presentation/extensions/client_extension.dart @@ -1,5 +1,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; extension ClientExtension on Client { @@ -20,4 +22,6 @@ extension ClientExtension on Client { .sorted(chatListItemComparator) .toList(); } + + String mxid(BuildContext context) => userID ?? L10n.of(context)!.user; } diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart index d67ed0f01..10bc17cf6 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart @@ -1,11 +1,10 @@ -import 'package:async/async.dart'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; +import 'package:fluffychat/presentation/mixins/fetch_profile_mixin.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart'; import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; typedef OnOpenSearchPage = Function(); typedef OnCloseSearchPage = Function(); @@ -24,24 +23,14 @@ class AdaptiveScaffoldApp extends StatefulWidget { State createState() => AdaptiveScaffoldAppController(); } -class AdaptiveScaffoldAppController extends State { - late final profileMemoizers = >{}; - +class AdaptiveScaffoldAppController extends State + with FetchProfileMixin { final ValueNotifier activeNavigationBar = ValueNotifier(AdaptiveDestinationEnum.rooms); final PageController pageController = PageController(initialPage: 1, keepPage: true); - Future fetchOwnProfile() { - if (!profileMemoizers.containsKey(matrix.client)) { - profileMemoizers[matrix.client] = AsyncMemoizer(); - } - return profileMemoizers[matrix.client]!.runOnce(() async { - return await matrix.client.fetchOwnProfile(); - }); - } - List get destinations => [ AdaptiveDestinationEnum.contacts, AdaptiveDestinationEnum.rooms, @@ -104,13 +93,20 @@ class AdaptiveScaffoldAppController extends State { MatrixState get matrix => Matrix.of(context); + @override + void initState() { + getCurrentProfile(matrix.client); + handleOnAccountData(matrix.client); + super.initState(); + } + @override Widget build(BuildContext context) => AppScaffoldView( destinations: destinations, activeRoomId: widget.activeRoomId, activeNavigationBar: activeNavigationBar, pageController: pageController, - fetchOwnProfile: fetchOwnProfile(), + profileNotifier: profileNotifier, onOpenSearchPage: _onOpenSearchPage, onCloseSearchPage: _onCloseSearchPage, onDestinationSelected: onDestinationSelected, diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart index c78d20c0e..673fad4e9 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart @@ -8,7 +8,7 @@ import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class AdaptiveScaffoldPrimaryNavigation extends StatelessWidget { - final Future? myProfile; + final ValueNotifier profileNotifier; final List getNavigationRailDestinations; final int? selectedIndex; final Function(int)? onDestinationSelected; @@ -16,7 +16,7 @@ class AdaptiveScaffoldPrimaryNavigation extends StatelessWidget { const AdaptiveScaffoldPrimaryNavigation({ super.key, - this.myProfile, + required this.profileNotifier, required this.getNavigationRailDestinations, this.selectedIndex, this.onDestinationSelected, @@ -81,22 +81,16 @@ class AdaptiveScaffoldPrimaryNavigation extends StatelessWidget { .separatorLightColor, ), ), - FutureBuilder( - future: myProfile, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - + ValueListenableBuilder( + valueListenable: profileNotifier, + builder: (context, profile, _) { return PopupMenuButton( padding: EdgeInsets.zero, onSelected: onSelected, itemBuilder: _bundleMenuItems, child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? + mxContent: profile.avatarUrl, + name: profile.displayName ?? Matrix.of(context).client.userID!.localpart, size: AdaptiveScaffoldPrimaryNavigationStyle.avatarSize, fontSize: diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart index 6232ff8c7..5c60db5a1 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart @@ -16,7 +16,7 @@ class AppScaffoldView extends StatelessWidget { final List destinations; final ValueNotifier activeNavigationBar; final PageController pageController; - final Future fetchOwnProfile; + final ValueNotifier profileNotifier; final OnOpenSearchPage onOpenSearchPage; final OnCloseSearchPage onCloseSearchPage; final OnDestinationSelected onDestinationSelected; @@ -39,7 +39,7 @@ class AppScaffoldView extends StatelessWidget { this.activeRoomId, required this.activeNavigationBar, required this.pageController, - required this.fetchOwnProfile, + required this.profileNotifier, required this.onOpenSearchPage, required this.onCloseSearchPage, required this.onDestinationSelected, @@ -178,7 +178,7 @@ class AppScaffoldView extends StatelessWidget { final destinations = getNavigationDestinations(context); return AdaptiveScaffoldPrimaryNavigation( - myProfile: fetchOwnProfile, + profileNotifier: profileNotifier, selectedIndex: activeNavigationBar.value.index, getNavigationRailDestinations: destinations .map((_) => AdaptiveScaffold.toRailDestination(_)) From 1aefa10abbc82a0f889935de82927659aa9ff7ca Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Tue, 3 Oct 2023 15:17:16 +0700 Subject: [PATCH 27/60] TW-692: Add ADR for send/listen event --- ...09-Instructions-for-naming-twake-events.md | 73 +++++++++++ .../settings_dashboard/settings/settings.dart | 54 ++++++-- .../settings_profile/settings_profile.dart | 45 ++++--- .../settings_profile_view.dart | 2 + .../settings_profile_view_mobile.dart | 3 +- .../settings_profile_view_web.dart | 3 +- .../mixins/fetch_profile_mixin.dart | 35 ------ .../adaptive_layout/adaptive_scaffold.dart | 12 +- .../adaptive_scaffold_primary_navigation.dart | 119 +++++++++--------- ...tive_scaffold_primary_navigation_view.dart | 84 +++++++++++++ .../adaptive_scaffold_view.dart | 4 - 11 files changed, 295 insertions(+), 139 deletions(-) create mode 100644 docs/adr/0009-Instructions-for-naming-twake-events.md delete mode 100644 lib/presentation/mixins/fetch_profile_mixin.dart create mode 100644 lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart diff --git a/docs/adr/0009-Instructions-for-naming-twake-events.md b/docs/adr/0009-Instructions-for-naming-twake-events.md new file mode 100644 index 000000000..8efe8737c --- /dev/null +++ b/docs/adr/0009-Instructions-for-naming-twake-events.md @@ -0,0 +1,73 @@ +# 7. Instructions for naming twake events + +Date: 2023-10-03 + +## Status + +Accepted + +## Docs + +- [Spec](https://spec.matrix.org/v1.6/#events) + +## Context + +- All data exchanged through Matrix is represented as an “event”. In addition, we have local support events. +- We can use local events to handle logic and behavior by injecting events into a defined stream. + +## Decision + +How to name events +- Below are the naming rules for events. +- App.twake is the name of the application. +- Inapp is event use for local support. +- Profile is the name of the feature. +- Avatar is the name of the event. + ex: 'app.twake.inapp.profile.avatar' + +How to use events. +- Send event + +``` +class TwakeEventDispatcher { + static final TwakeEventDispatcher _twakeEventDispatcher = + TwakeEventDispatcher._instance(); + + factory TwakeEventDispatcher() { + return _twakeEventDispatcher; + } + + TwakeEventDispatcher._instance(); + + void sendAccountDataEvent({ + required Client client, + required BasicEvent basicEvent, + }) { + client.onAccountData.add(basicEvent); + } +} + +twakeEventDispatcher.sendAccountDataEvent( + client: client, + basicEvent: BasicEvent( + type: TwakeInappEventTypes.uploadAvatarEvent, + content: profile.toJson(), + ), + ); +``` +- Listen event and handle it + +``` +client.onAccountData.stream.listen((event) { + Logs().d( + 'FetchProfileMixin::onAccountData() - EventType: ${event.type} - EventContent: ${event.content}', + ); + if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { + profileNotifier.value = Profile.fromJson(event.content); + } + }); +``` + +## Consequences + +- If we do not specifically define the event, it will be duplicated with the homeserver event \ No newline at end of file diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 050e2c143..720b0c28c 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -1,11 +1,13 @@ +import 'dart:async'; + import 'package:fluffychat/config/app_config.dart'; 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/enum/settings/settings_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; -import 'package:fluffychat/presentation/mixins/fetch_profile_mixin.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -30,8 +32,13 @@ class Settings extends StatefulWidget { SettingsController createState() => SettingsController(); } -class SettingsController extends State - with ConnectPageMixin, FetchProfileMixin { +class SettingsController extends State with ConnectPageMixin { + final ValueNotifier profileNotifier = ValueNotifier( + Profile(userId: ''), + ); + + StreamSubscription? onAccountDataSubscription; + final List getListSettingItem = [ SettingEnum.chatSettings, SettingEnum.privacyAndSecurity, @@ -79,14 +86,15 @@ class SettingsController extends State Client get client => Matrix.of(context).client; - @override - void initState() { - getCurrentProfile(client); - handleOnAccountData(client); - WidgetsBinding.instance.addPostFrameCallback((_) { - checkBootstrap(); - }); - super.initState(); + void _getCurrentProfile(Client client) async { + final profile = await client.getProfileFromUserId( + client.userID!, + getFromRooms: false, + ); + Logs().d( + 'Settings::_getCurrentProfile() - currentProfile: $profile', + ); + profileNotifier.value = profile; } void checkBootstrap() async { @@ -168,6 +176,30 @@ class SettingsController extends State } } + void _handleOnAccountDataSubscription() { + onAccountDataSubscription = client.onAccountData.stream.listen((event) { + if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { + profileNotifier.value = Profile.fromJson(event.content); + } + }); + } + + @override + void initState() { + _getCurrentProfile(client); + _handleOnAccountDataSubscription(); + WidgetsBinding.instance.addPostFrameCallback((_) { + checkBootstrap(); + }); + super.initState(); + } + + @override + void dispose() { + onAccountDataSubscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SettingsView( diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 7dbbb2f66..dc0fa09e4 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -8,7 +10,6 @@ import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_pr import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; -import 'package:fluffychat/presentation/mixins/fetch_profile_mixin.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -31,8 +32,11 @@ class SettingsProfile extends StatefulWidget { State createState() => SettingsProfileController(); } -class SettingsProfileController extends State - with FetchProfileMixin { +class SettingsProfileController extends State { + final ValueNotifier profileNotifier = ValueNotifier( + Profile(userId: ''), + ); + final TwakeEventDispatcher twakeEventDispatcher = getIt.get(); @@ -117,7 +121,7 @@ class SettingsProfileController extends State future: () => matrix.client.setAvatar(null), ); if (success.error == null) { - getCurrentProfile(client, isUpdated: true); + _getCurrentProfile(client, isUpdated: true); } return; } @@ -165,7 +169,7 @@ class SettingsProfileController extends State future: () => matrix.client.setAvatar(file), ); if (success.error == null) { - getCurrentProfile(client, isUpdated: true); + _getCurrentProfile(client, isUpdated: true); } } @@ -184,6 +188,22 @@ class SettingsProfileController extends State _handleGetAvatarAction(action); } + void _handleSyncProfile() async { + Logs().d( + 'SettingsProfileController::_handleSyncProfile() - Syncing profile', + ); + twakeEventDispatcher.sendAccountDataEvent( + client: client, + basicEvent: BasicEvent( + type: TwakeInappEventTypes.uploadAvatarEvent, + content: profileNotifier.value.toJson(), + ), + ); + Logs().d( + 'SettingsProfileController::_handleSyncProfile() - Syncing success', + ); + } + void setDisplayNameAction() async { if (displayNameFocusNode.hasFocus) { displayNameFocusNode.unfocus(); @@ -198,12 +218,11 @@ class SettingsProfileController extends State ); if (success.error == null) { isEditedProfileNotifier.toggle(); - getCurrentProfile(client, isUpdated: true); + _getCurrentProfile(client, isUpdated: true); } } - @override - void getCurrentProfile( + void _getCurrentProfile( Client client, { isUpdated = false, }) async { @@ -216,15 +235,9 @@ class SettingsProfileController extends State 'SettingsProfileController::_getCurrentProfile() - currentProfile: $profile', ); profileNotifier.value = profile; - twakeEventDispatcher.sendAccountDataEvent( - client: client, - basicEvent: BasicEvent( - type: TwakeInappEventTypes.uploadAvatarEvent, - content: profile.toJson(), - ), - ); displayNameEditingController.text = displayName; matrixIdEditingController.text = client.mxid(context); + _handleSyncProfile(); } void handleTextEditOnChange(SettingsProfileEnum settingsProfileEnum) { @@ -247,7 +260,7 @@ class SettingsProfileController extends State void _initProfile() { if (widget.profile == null) { - getCurrentProfile(client); + _getCurrentProfile(client); return; } profileNotifier.value = widget.profile!; diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart index 82134260f..eda5b7388 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -84,6 +84,7 @@ class SettingsProfileView extends StatelessWidget { builder: (_) { return SettingsProfileViewMobile( profileNotifier: controller.profileNotifier, + displayName: controller.displayName, onAvatarTap: () => controller.setAvatarAction(), settingsProfileOptions: ListView.separated( shrinkWrap: true, @@ -134,6 +135,7 @@ class SettingsProfileView extends StatelessWidget { builder: (_) { return SettingsProfileViewWeb( profileNotifier: controller.profileNotifier, + displayName: controller.displayName, basicInfoWidget: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart index e064d04dc..44ae1e804 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -9,12 +9,14 @@ class SettingsProfileViewMobile extends StatelessWidget { final ValueNotifier profileNotifier; final Widget settingsProfileOptions; final VoidCallback onAvatarTap; + final String displayName; const SettingsProfileViewMobile({ super.key, required this.profileNotifier, required this.settingsProfileOptions, required this.onAvatarTap, + required this.displayName, }); @override @@ -22,7 +24,6 @@ class SettingsProfileViewMobile extends StatelessWidget { return ValueListenableBuilder( valueListenable: profileNotifier, builder: (context, profile, __) { - final displayName = profile.displayName ?? profile.userId; return Column( children: [ Divider( diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart index 7551dce5e..8af3562d0 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart @@ -11,6 +11,7 @@ class SettingsProfileViewWeb extends StatelessWidget { final Widget basicInfoWidget; final Widget workIdentitiesInfoWidget; final VoidCallback onAvatarTap; + final String displayName; const SettingsProfileViewWeb({ super.key, @@ -18,6 +19,7 @@ class SettingsProfileViewWeb extends StatelessWidget { required this.basicInfoWidget, required this.onAvatarTap, required this.workIdentitiesInfoWidget, + required this.displayName, }); @override @@ -25,7 +27,6 @@ class SettingsProfileViewWeb extends StatelessWidget { return ValueListenableBuilder( valueListenable: profileNotifier, builder: (context, profile, __) { - final displayName = profile.displayName ?? profile.userId; return Padding( padding: SettingsProfileViewWebStyle.paddingBody, child: Center( diff --git a/lib/presentation/mixins/fetch_profile_mixin.dart b/lib/presentation/mixins/fetch_profile_mixin.dart deleted file mode 100644 index dfecb56ca..000000000 --- a/lib/presentation/mixins/fetch_profile_mixin.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:fluffychat/event/twake_inapp_event_types.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; - -mixin FetchProfileMixin { - final ValueNotifier profileNotifier = ValueNotifier( - Profile(userId: ''), - ); - - void getCurrentProfile( - Client client, { - isUpdated = false, - }) async { - final profile = await client.getProfileFromUserId( - client.userID!, - cache: !isUpdated, - getFromRooms: false, - ); - Logs().d( - 'FetchProfileMixin::_getCurrentProfile() - currentProfile: $profile', - ); - profileNotifier.value = profile; - } - - void handleOnAccountData(Client client) { - client.onAccountData.stream.listen((event) { - Logs().d( - 'FetchProfileMixin::onAccountData() - EventType: ${event.type} - EventContent: ${event.content}', - ); - if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { - profileNotifier.value = Profile.fromJson(event.content); - } - }); - } -} diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart index 10bc17cf6..488ae0180 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart @@ -1,5 +1,4 @@ import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; -import 'package:fluffychat/presentation/mixins/fetch_profile_mixin.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart'; import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -23,8 +22,7 @@ class AdaptiveScaffoldApp extends StatefulWidget { State createState() => AdaptiveScaffoldAppController(); } -class AdaptiveScaffoldAppController extends State - with FetchProfileMixin { +class AdaptiveScaffoldAppController extends State { final ValueNotifier activeNavigationBar = ValueNotifier(AdaptiveDestinationEnum.rooms); @@ -93,20 +91,12 @@ class AdaptiveScaffoldAppController extends State MatrixState get matrix => Matrix.of(context); - @override - void initState() { - getCurrentProfile(matrix.client); - handleOnAccountData(matrix.client); - super.initState(); - } - @override Widget build(BuildContext context) => AppScaffoldView( destinations: destinations, activeRoomId: widget.activeRoomId, activeNavigationBar: activeNavigationBar, pageController: pageController, - profileNotifier: profileNotifier, onOpenSearchPage: _onOpenSearchPage, onCloseSearchPage: _onCloseSearchPage, onDestinationSelected: onDestinationSelected, diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart index 673fad4e9..3739c32ec 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart @@ -1,14 +1,14 @@ +import 'dart:async'; + +import 'package:fluffychat/event/twake_inapp_event_types.dart'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; -import 'package:fluffychat/pages/chat_list/client_chooser_button_style.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; -import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_style.dart'; +import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class AdaptiveScaffoldPrimaryNavigation extends StatelessWidget { - final ValueNotifier profileNotifier; +class AdaptiveScaffoldPrimaryNavigation extends StatefulWidget { final List getNavigationRailDestinations; final int? selectedIndex; final Function(int)? onDestinationSelected; @@ -16,13 +16,27 @@ class AdaptiveScaffoldPrimaryNavigation extends StatelessWidget { const AdaptiveScaffoldPrimaryNavigation({ super.key, - required this.profileNotifier, required this.getNavigationRailDestinations, this.selectedIndex, this.onDestinationSelected, this.onSelected, }); + @override + State createState() => + _AdaptiveScaffoldPrimaryNavigationState(); +} + +class _AdaptiveScaffoldPrimaryNavigationState + extends State { + final ValueNotifier profileNotifier = ValueNotifier( + Profile(userId: ''), + ); + + StreamSubscription? onAccountDataSubscription; + + Client get client => Matrix.of(context).client; + List> _bundleMenuItems(BuildContext context) { return >[ PopupMenuItem( @@ -48,62 +62,47 @@ class AdaptiveScaffoldPrimaryNavigation extends StatelessWidget { ]; } + void _getCurrentProfile(Client client) async { + final profile = await client.getProfileFromUserId( + client.userID!, + getFromRooms: false, + ); + Logs().d( + 'AdaptiveScaffoldPrimaryNavigation::_getCurrentProfile() - currentProfile: $profile', + ); + profileNotifier.value = profile; + } + + void _handleOnAccountDataSubscription() { + onAccountDataSubscription = client.onAccountData.stream.listen((event) { + if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { + profileNotifier.value = Profile.fromJson(event.content); + } + }); + } + + @override + void initState() { + _getCurrentProfile(client); + _handleOnAccountDataSubscription(); + super.initState(); + } + + @override + void dispose() { + onAccountDataSubscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Material( - color: Theme.of(context).colorScheme.surface, - child: Container( - margin: AdaptiveScaffoldPrimaryNavigationStyle.primaryNavigationMargin, - width: AdaptiveScaffoldPrimaryNavigationStyle.primaryNavigationWidth, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: NavigationRail( - selectedIndex: selectedIndex, - destinations: getNavigationRailDestinations, - onDestinationSelected: onDestinationSelected, - labelType: NavigationRailLabelType.all, - backgroundColor: Theme.of(context).colorScheme.surface, - ), - ), - Column( - children: [ - const Padding( - padding: - AdaptiveScaffoldPrimaryNavigationStyle.dividerPadding, - child: Divider( - height: AdaptiveScaffoldPrimaryNavigationStyle.dividerSize, - color: AdaptiveScaffoldPrimaryNavigationStyle - .separatorLightColor, - ), - ), - ValueListenableBuilder( - valueListenable: profileNotifier, - builder: (context, profile, _) { - return PopupMenuButton( - padding: EdgeInsets.zero, - onSelected: onSelected, - itemBuilder: _bundleMenuItems, - child: Avatar( - mxContent: profile.avatarUrl, - name: profile.displayName ?? - Matrix.of(context).client.userID!.localpart, - size: AdaptiveScaffoldPrimaryNavigationStyle.avatarSize, - fontSize: - ClientChooserButtonStyle.avatarFontSizeInAppBar, - ), - ); - }, - ), - ], - ) - ], - ), - ), + return AdaptiveScaffoldPrimaryNavigationView( + getNavigationRailDestinations: widget.getNavigationRailDestinations, + selectedIndex: widget.selectedIndex, + onDestinationSelected: widget.onDestinationSelected, + onSelected: widget.onSelected, + profileNotifier: profileNotifier, + itemBuilder: _bundleMenuItems, ); } } diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart new file mode 100644 index 000000000..fe8d06a9b --- /dev/null +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart @@ -0,0 +1,84 @@ +import 'package:fluffychat/pages/chat_list/client_chooser_button_style.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_style.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class AdaptiveScaffoldPrimaryNavigationView extends StatelessWidget { + final List getNavigationRailDestinations; + final int? selectedIndex; + final Function(int)? onDestinationSelected; + final Function(Object)? onSelected; + final ValueNotifier profileNotifier; + final List> Function(BuildContext) itemBuilder; + + const AdaptiveScaffoldPrimaryNavigationView({ + super.key, + required this.getNavigationRailDestinations, + this.selectedIndex, + this.onDestinationSelected, + this.onSelected, + required this.profileNotifier, + required this.itemBuilder, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colorScheme.surface, + child: Container( + margin: AdaptiveScaffoldPrimaryNavigationStyle.primaryNavigationMargin, + width: AdaptiveScaffoldPrimaryNavigationStyle.primaryNavigationWidth, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: NavigationRail( + selectedIndex: selectedIndex, + destinations: getNavigationRailDestinations, + onDestinationSelected: onDestinationSelected, + labelType: NavigationRailLabelType.all, + backgroundColor: Theme.of(context).colorScheme.surface, + ), + ), + Column( + children: [ + const Padding( + padding: + AdaptiveScaffoldPrimaryNavigationStyle.dividerPadding, + child: Divider( + height: AdaptiveScaffoldPrimaryNavigationStyle.dividerSize, + color: AdaptiveScaffoldPrimaryNavigationStyle + .separatorLightColor, + ), + ), + ValueListenableBuilder( + valueListenable: profileNotifier, + builder: (context, profile, _) { + return PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: onSelected, + itemBuilder: itemBuilder, + child: Avatar( + mxContent: profile.avatarUrl, + name: profile.displayName ?? + Matrix.of(context).client.userID!.localpart, + size: AdaptiveScaffoldPrimaryNavigationStyle.avatarSize, + fontSize: + ClientChooserButtonStyle.avatarFontSizeInAppBar, + ), + ); + }, + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart index 5c60db5a1..5fc9ccf55 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart @@ -10,13 +10,11 @@ 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'; -import 'package:matrix/matrix.dart'; class AppScaffoldView extends StatelessWidget { final List destinations; final ValueNotifier activeNavigationBar; final PageController pageController; - final ValueNotifier profileNotifier; final OnOpenSearchPage onOpenSearchPage; final OnCloseSearchPage onCloseSearchPage; final OnDestinationSelected onDestinationSelected; @@ -39,7 +37,6 @@ class AppScaffoldView extends StatelessWidget { this.activeRoomId, required this.activeNavigationBar, required this.pageController, - required this.profileNotifier, required this.onOpenSearchPage, required this.onCloseSearchPage, required this.onDestinationSelected, @@ -178,7 +175,6 @@ class AppScaffoldView extends StatelessWidget { final destinations = getNavigationDestinations(context); return AdaptiveScaffoldPrimaryNavigation( - profileNotifier: profileNotifier, selectedIndex: activeNavigationBar.value.index, getNavigationRailDestinations: destinations .map((_) => AdaptiveScaffold.toRailDestination(_)) From 696501277c706d937d87b9aad8d96e818dc8e8ba Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 4 Oct 2023 00:54:03 +0700 Subject: [PATCH 28/60] TW-692: Create `UploadProfileInteractor` --- lib/di/global/get_it_initializer.dart | 4 ++ .../settings/upload_profile_failure.dart | 10 +++++ .../settings/upload_profile_loading.dart | 8 ++++ .../settings/upload_profile_success.dart | 19 ++++++++ .../settings/upload_profile_interactor.dart | 44 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 lib/domain/app_state/settings/upload_profile_failure.dart create mode 100644 lib/domain/app_state/settings/upload_profile_loading.dart create mode 100644 lib/domain/app_state/settings/upload_profile_success.dart create mode 100644 lib/domain/usecase/settings/upload_profile_interactor.dart diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 30c3ecf94..78ccce98c 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -41,6 +41,7 @@ import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/domain/usecase/send_image_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; +import 'package:fluffychat/domain/usecase/settings/upload_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:get_it/get_it.dart'; @@ -177,5 +178,8 @@ class GetItInitializer { getIt.registerSingleton( TimelineSearchEventInteractor(), ); + getIt.registerSingleton( + UploadProfileInteractor(), + ); } } diff --git a/lib/domain/app_state/settings/upload_profile_failure.dart b/lib/domain/app_state/settings/upload_profile_failure.dart new file mode 100644 index 000000000..884b03e4c --- /dev/null +++ b/lib/domain/app_state/settings/upload_profile_failure.dart @@ -0,0 +1,10 @@ +import 'package:fluffychat/app_state/failure.dart'; + +class UploadProfileFailure extends Failure { + final dynamic exception; + + const UploadProfileFailure(this.exception) : super(); + + @override + List get props => [exception]; +} diff --git a/lib/domain/app_state/settings/upload_profile_loading.dart b/lib/domain/app_state/settings/upload_profile_loading.dart new file mode 100644 index 000000000..05eede919 --- /dev/null +++ b/lib/domain/app_state/settings/upload_profile_loading.dart @@ -0,0 +1,8 @@ +import 'package:fluffychat/app_state/success.dart'; + +class UploadProfileLoading extends Success { + const UploadProfileLoading(); + + @override + List get props => []; +} diff --git a/lib/domain/app_state/settings/upload_profile_success.dart b/lib/domain/app_state/settings/upload_profile_success.dart new file mode 100644 index 000000000..580f6ae63 --- /dev/null +++ b/lib/domain/app_state/settings/upload_profile_success.dart @@ -0,0 +1,19 @@ +import 'package:fluffychat/app_state/success.dart'; + +class UploadProfileInitial extends Success { + @override + List get props => []; +} + +class UploadProfileSuccess extends Success { + final Uri? avatar; + final String? displayName; + + const UploadProfileSuccess({ + this.avatar, + this.displayName, + }); + + @override + List get props => [avatar, displayName]; +} diff --git a/lib/domain/usecase/settings/upload_profile_interactor.dart b/lib/domain/usecase/settings/upload_profile_interactor.dart new file mode 100644 index 000000000..34b8f7597 --- /dev/null +++ b/lib/domain/usecase/settings/upload_profile_interactor.dart @@ -0,0 +1,44 @@ +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/settings/upload_profile_failure.dart'; +import 'package:fluffychat/domain/app_state/settings/upload_profile_loading.dart'; +import 'package:fluffychat/domain/app_state/settings/upload_profile_success.dart'; +import 'package:matrix/matrix.dart'; + +class UploadProfileInteractor { + Stream> execute({ + required Client client, + required String userId, + Uri? avatarUrl, + bool isUpdateDisPlayName = false, + String? displayName, + }) async* { + yield const Right(UploadProfileLoading()); + try { + Logs().d( + 'UploadAvatarInteractor::execute(): Uri - $avatarUrl - displayName - $displayName', + ); + if (avatarUrl != null) { + await client.setAvatarUrl( + userId, + avatarUrl, + ); + } + if (displayName != null) { + await client.setDisplayName(userId, displayName); + } + yield Right( + UploadProfileSuccess( + displayName: displayName, + avatar: avatarUrl, + ), + ); + } catch (e) { + Logs().d( + 'UploadAvatarInteractor::execute(): Exception - $e}', + ); + yield Left(UploadProfileFailure(e)); + } + } +} From d6e8085f3ee2e7da12b097ecfb266a448964da53 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Wed, 4 Oct 2023 02:23:01 +0700 Subject: [PATCH 29/60] TW-692: Implement `UploadProfile` in presentation layer --- assets/l10n/intl_en.arb | 7 +- lib/config/go_routes/go_router.dart | 4 +- lib/di/global/get_it_initializer.dart | 6 +- ...ilure.dart => update_profile_failure.dart} | 4 +- ...ading.dart => update_profile_loading.dart} | 4 +- .../settings/update_profile_success.dart | 21 + .../settings/upload_profile_success.dart | 19 - .../settings/update_profile_interactor.dart | 47 ++ .../settings/upload_profile_interactor.dart | 44 -- .../settings_dashboard/settings/settings.dart | 27 +- .../settings/settings_view.dart | 167 ++++--- .../settings_profile/settings_profile.dart | 417 ++++++++++++++---- .../settings_profile_item.dart | 55 ++- .../get_avatar_ui_state.dart | 25 ++ .../get_profile_ui_state.dart | 11 + .../settings_profile_ui_state.dart | 3 + .../settings_profile_view.dart | 28 +- .../settings_profile_view_mobile.dart | 188 +++++--- .../settings_profile_view_mobile_style.dart | 1 + .../settings_profile_view_style.dart | 17 + .../settings_profile_view_web.dart | 380 ++++++++-------- .../settings_profile_view_web_style.dart | 1 + lib/presentation/state/failure.dart | 10 + lib/presentation/state/success.dart | 16 + lib/utils/dialog/twake_loading_dialog.dart | 57 +++ .../adaptive_scaffold_primary_navigation.dart | 5 +- 26 files changed, 1019 insertions(+), 545 deletions(-) rename lib/domain/app_state/settings/{upload_profile_failure.dart => update_profile_failure.dart} (57%) rename lib/domain/app_state/settings/{upload_profile_loading.dart => update_profile_loading.dart} (56%) create mode 100644 lib/domain/app_state/settings/update_profile_success.dart delete mode 100644 lib/domain/app_state/settings/upload_profile_success.dart create mode 100644 lib/domain/usecase/settings/update_profile_interactor.dart delete mode 100644 lib/domain/usecase/settings/upload_profile_interactor.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart create mode 100644 lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart create mode 100644 lib/presentation/state/failure.dart create mode 100644 lib/presentation/state/success.dart create mode 100644 lib/utils/dialog/twake_loading_dialog.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 7916b90bd..4c38a9ffe 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1689,7 +1689,7 @@ "type": "text", "placeholders": {} }, - "search": "Search for people and channels", + "searchForPeopleAndChannels": "Search for people and channels", "@search": { "type": "text", "placeholders": {} @@ -2647,7 +2647,6 @@ "tapToAllowAccessToYourGallery": "Tap to allow access to your Gallery", "tapToAllowAccessToYourCamera": "You can enable camera access in the Settings app to make video calls in", "twake": "Twake", - "dismiss": "Dismiss", "permissionAccess": "Permission access", "allow": "Allow", "explainStoragePermission": "Twake need access to your storage to preview file", @@ -2669,7 +2668,6 @@ } }, "keyboard": "Keyboard", - "tapToAllowAccessToYourGallery": "Tap to allow access to your Gallery", "changeChatAvatar": "Change the Chat avatar", "roomAvatarMaxFileSize": "The avatar size is too large", "@roomAvatarMaxFileSize": {}, @@ -2755,5 +2753,6 @@ "editProfileDescriptions": "Update your profile with a new name, picture and a short introduction.", "workIdentitiesInfo": "WORK IDENTITIES INFO", "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name.", - "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard." + "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard.", + "changeProfilePhoto": "Change profile photo" } \ No newline at end of file diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 4e3064088..22955d5ef 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -248,9 +248,7 @@ abstract class AppRoutes { path: 'profile', pageBuilder: (context, state) => defaultPageBuilder( context, - SettingsProfile( - profile: state.extra as Profile?, - ), + const SettingsProfile(), ), ), GoRoute( diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 78ccce98c..cc728b586 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -41,7 +41,7 @@ import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/domain/usecase/send_image_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; -import 'package:fluffychat/domain/usecase/settings/upload_profile_interactor.dart'; +import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:get_it/get_it.dart'; @@ -178,8 +178,8 @@ class GetItInitializer { getIt.registerSingleton( TimelineSearchEventInteractor(), ); - getIt.registerSingleton( - UploadProfileInteractor(), + getIt.registerSingleton( + UpdateProfileInteractor(), ); } } diff --git a/lib/domain/app_state/settings/upload_profile_failure.dart b/lib/domain/app_state/settings/update_profile_failure.dart similarity index 57% rename from lib/domain/app_state/settings/upload_profile_failure.dart rename to lib/domain/app_state/settings/update_profile_failure.dart index 884b03e4c..53e8b56a4 100644 --- a/lib/domain/app_state/settings/upload_profile_failure.dart +++ b/lib/domain/app_state/settings/update_profile_failure.dart @@ -1,9 +1,9 @@ import 'package:fluffychat/app_state/failure.dart'; -class UploadProfileFailure extends Failure { +class UpdateProfileFailure extends Failure { final dynamic exception; - const UploadProfileFailure(this.exception) : super(); + const UpdateProfileFailure(this.exception) : super(); @override List get props => [exception]; diff --git a/lib/domain/app_state/settings/upload_profile_loading.dart b/lib/domain/app_state/settings/update_profile_loading.dart similarity index 56% rename from lib/domain/app_state/settings/upload_profile_loading.dart rename to lib/domain/app_state/settings/update_profile_loading.dart index 05eede919..badd69e07 100644 --- a/lib/domain/app_state/settings/upload_profile_loading.dart +++ b/lib/domain/app_state/settings/update_profile_loading.dart @@ -1,7 +1,7 @@ import 'package:fluffychat/app_state/success.dart'; -class UploadProfileLoading extends Success { - const UploadProfileLoading(); +class UpdateProfileLoading extends Success { + const UpdateProfileLoading(); @override List get props => []; diff --git a/lib/domain/app_state/settings/update_profile_success.dart b/lib/domain/app_state/settings/update_profile_success.dart new file mode 100644 index 000000000..eebecda06 --- /dev/null +++ b/lib/domain/app_state/settings/update_profile_success.dart @@ -0,0 +1,21 @@ +import 'package:fluffychat/app_state/success.dart'; + +class UpdateProfileInitial extends Success { + @override + List get props => []; +} + +class UpdateProfileSuccess extends Success { + final Uri? avatar; + final String? displayName; + final bool isDeleteAvatar; + + const UpdateProfileSuccess({ + this.avatar, + this.displayName, + this.isDeleteAvatar = false, + }); + + @override + List get props => [avatar, displayName, isDeleteAvatar]; +} diff --git a/lib/domain/app_state/settings/upload_profile_success.dart b/lib/domain/app_state/settings/upload_profile_success.dart deleted file mode 100644 index 580f6ae63..000000000 --- a/lib/domain/app_state/settings/upload_profile_success.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:fluffychat/app_state/success.dart'; - -class UploadProfileInitial extends Success { - @override - List get props => []; -} - -class UploadProfileSuccess extends Success { - final Uri? avatar; - final String? displayName; - - const UploadProfileSuccess({ - this.avatar, - this.displayName, - }); - - @override - List get props => [avatar, displayName]; -} diff --git a/lib/domain/usecase/settings/update_profile_interactor.dart b/lib/domain/usecase/settings/update_profile_interactor.dart new file mode 100644 index 000000000..ff4f7aa7d --- /dev/null +++ b/lib/domain/usecase/settings/update_profile_interactor.dart @@ -0,0 +1,47 @@ +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/settings/update_profile_failure.dart'; +import 'package:fluffychat/domain/app_state/settings/update_profile_loading.dart'; +import 'package:fluffychat/domain/app_state/settings/update_profile_success.dart'; +import 'package:matrix/matrix.dart'; + +class UpdateProfileInteractor { + Stream> execute({ + required Client client, + Uri? avatarUrl, + bool isDeleteAvatar = false, + String? displayName, + }) async* { + yield const Right(UpdateProfileLoading()); + try { + Logs().d( + 'UploadProfileInteractor::execute(): Uri - $avatarUrl - displayName - $displayName', + ); + if (avatarUrl != null || isDeleteAvatar) { + await client.setAvatarUrl( + client.userID!, + avatarUrl ?? Uri.parse(''), + ); + } + if (displayName != null) { + await client.setDisplayName( + client.userID!, + displayName, + ); + } + yield Right( + UpdateProfileSuccess( + displayName: displayName, + avatar: avatarUrl, + isDeleteAvatar: isDeleteAvatar, + ), + ); + } catch (e) { + Logs().d( + 'UploadAvatarInteractor::execute(): Exception - $e}', + ); + yield Left(UpdateProfileFailure(e)); + } + } +} diff --git a/lib/domain/usecase/settings/upload_profile_interactor.dart b/lib/domain/usecase/settings/upload_profile_interactor.dart deleted file mode 100644 index 34b8f7597..000000000 --- a/lib/domain/usecase/settings/upload_profile_interactor.dart +++ /dev/null @@ -1,44 +0,0 @@ -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/settings/upload_profile_failure.dart'; -import 'package:fluffychat/domain/app_state/settings/upload_profile_loading.dart'; -import 'package:fluffychat/domain/app_state/settings/upload_profile_success.dart'; -import 'package:matrix/matrix.dart'; - -class UploadProfileInteractor { - Stream> execute({ - required Client client, - required String userId, - Uri? avatarUrl, - bool isUpdateDisPlayName = false, - String? displayName, - }) async* { - yield const Right(UploadProfileLoading()); - try { - Logs().d( - 'UploadAvatarInteractor::execute(): Uri - $avatarUrl - displayName - $displayName', - ); - if (avatarUrl != null) { - await client.setAvatarUrl( - userId, - avatarUrl, - ); - } - if (displayName != null) { - await client.setDisplayName(userId, displayName); - } - yield Right( - UploadProfileSuccess( - displayName: displayName, - avatar: avatarUrl, - ), - ); - } catch (e) { - Logs().d( - 'UploadAvatarInteractor::execute(): Exception - $e}', - ); - yield Left(UploadProfileFailure(e)); - } - } -} diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 720b0c28c..17ae8a034 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -33,9 +33,8 @@ class Settings extends StatefulWidget { } class SettingsController extends State with ConnectPageMixin { - final ValueNotifier profileNotifier = ValueNotifier( - Profile(userId: ''), - ); + final ValueNotifier avatarUriNotifier = ValueNotifier(Uri()); + final ValueNotifier displayNameNotifier = ValueNotifier(''); StreamSubscription? onAccountDataSubscription; @@ -53,7 +52,7 @@ class SettingsController extends State with ConnectPageMixin { final ValueNotifier optionsSelectNotifier = ValueNotifier(null); String get displayName => - profileNotifier.value.displayName ?? + displayNameNotifier.value ?? client.mxid(context).localpart ?? client.mxid(context); @@ -94,7 +93,8 @@ class SettingsController extends State with ConnectPageMixin { Logs().d( 'Settings::_getCurrentProfile() - currentProfile: $profile', ); - profileNotifier.value = profile; + avatarUriNotifier.value = profile.avatarUrl; + displayNameNotifier.value = profile.displayName; } void checkBootstrap() async { @@ -135,12 +135,9 @@ class SettingsController extends State with ConnectPageMixin { checkBootstrap(); } - void goToSettingsProfile(Profile? profile) async { + void goToSettingsProfile() async { optionsSelectNotifier.value = SettingEnum.profile; - context.push( - '/rooms/profile', - extra: profile, - ); + context.go('/rooms/profile'); } void onClickToSettingsItem(SettingEnum settingEnum) { @@ -179,7 +176,13 @@ class SettingsController extends State with ConnectPageMixin { void _handleOnAccountDataSubscription() { onAccountDataSubscription = client.onAccountData.stream.listen((event) { if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { - profileNotifier.value = Profile.fromJson(event.content); + final newProfile = Profile.fromJson(event.content); + if (newProfile.avatarUrl != avatarUriNotifier.value) { + avatarUriNotifier.value = newProfile.avatarUrl; + } + if (newProfile.displayName != displayNameNotifier.value) { + displayNameNotifier.value = newProfile.displayName; + } } }); } @@ -197,6 +200,8 @@ class SettingsController extends State with ConnectPageMixin { @override void dispose() { onAccountDataSubscription?.cancel(); + avatarUriNotifier.dispose(); + displayNameNotifier.dispose(); super.dispose(); } diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart index bd76e69b3..668b3c7b0 100644 --- a/lib/pages/settings_dashboard/settings/settings_view.dart +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -39,70 +39,66 @@ class SettingsView extends StatelessWidget { child: ListView( key: const Key('SettingsListViewContent'), children: [ - ValueListenableBuilder( - valueListenable: controller.profileNotifier, - builder: (context, profile, _) { - return Padding( - padding: SettingsViewStyle.bodySettingsScreenPadding, - child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - color: controller.optionsSelectNotifier.value == - SettingEnum.profile - ? Theme.of(context).colorScheme.secondaryContainer - : null, - child: InkWell( - onTap: () => controller.goToSettingsProfile(profile), - child: Padding( - padding: SettingsViewStyle.itemBuilderPadding, - child: Row( - children: [ - Padding( + Padding( + padding: SettingsViewStyle.bodySettingsScreenPadding, + child: Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + color: controller.optionsSelectNotifier.value == + SettingEnum.profile + ? Theme.of(context).colorScheme.secondaryContainer + : null, + child: InkWell( + onTap: () => controller.goToSettingsProfile(), + child: Padding( + padding: SettingsViewStyle.itemBuilderPadding, + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: controller.avatarUriNotifier, + builder: (context, avatarUrl, __) { + return Padding( padding: SettingsViewStyle.avatarPadding, - child: Stack( - children: [ - Material( - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: Theme.of(context) + child: Material( + elevation: Theme.of(context) .appBarTheme - .shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize, - ), - ), - child: Avatar( - mxContent: profile.avatarUrl, - name: controller.displayName, - size: AvatarStyle.defaultSize, - fontSize: - SettingsViewStyle.fontSizeAvatar, - ), + .scrolledUnderElevation ?? + 4, + shadowColor: + Theme.of(context).appBarTheme.shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, ), - ], + ), + child: Avatar( + mxContent: avatarUrl, + name: controller.displayName, + size: AvatarStyle.defaultSize, + fontSize: SettingsViewStyle.fontSizeAvatar, + ), ), - ), - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - profile.displayName ?? - controller.displayName, + ); + }, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: + controller.displayNameNotifier, + builder: (context, displayName, _) { + return Text( + displayName ?? controller.displayName, style: Theme.of(context) .textTheme .titleLarge @@ -113,37 +109,36 @@ class SettingsView extends StatelessWidget { ), maxLines: 1, overflow: TextOverflow.ellipsis, - ), - Text( - controller.client.mxid(context), - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - LinagoraRefColors.material() - .neutral[40], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], + ); + }, ), - ), - const Icon( - Icons.chevron_right_outlined, - size: SettingsViewStyle.iconSize, - ), - ], + Text( + controller.client.mxid(context), + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: LinagoraRefColors.material() + .neutral[40], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const Icon( + Icons.chevron_right_outlined, + size: SettingsViewStyle.iconSize, ), - ), - ], + ], + ), ), - ), + ], ), ), - ); - }, + ), + ), ), const Divider(thickness: 1), Column( diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index dc0fa09e4..d05daeef8 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -1,53 +1,70 @@ -import 'dart:async'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:dartz/dartz.dart' hide State; import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/config/app_config.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/domain/app_state/room/upload_content_state.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'; +import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/event/twake_inapp_event_types.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.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_loading_dialog.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:image_picker/image_picker.dart'; +import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; class SettingsProfile extends StatefulWidget { - final Profile? profile; - const SettingsProfile({ super.key, - required this.profile, }); @override State createState() => SettingsProfileController(); } -class SettingsProfileController extends State { - final ValueNotifier profileNotifier = ValueNotifier( - Profile(userId: ''), - ); +class SettingsProfileController extends State + with CommonMediaPickerMixin, SingleImagePickerMixin { + final uploadProfileInteractor = getIt.get(); + final uploadContentInteractor = getIt.get(); + final uploadContentWebInteractor = + getIt.get(); + + Profile? currentProfile; + AssetEntity? assetEntity; + FilePickerResult? filePickerResult; final TwakeEventDispatcher twakeEventDispatcher = getIt.get(); final ValueNotifier isEditedProfileNotifier = ValueNotifier(false); + final ValueNotifier> settingsProfileUIState = + ValueNotifier>(Right(GetAvatarInitialUIState())); Client get client => Matrix.of(context).client; - MatrixState get matrix => Matrix.of(context); + bool get _hasEditedDisplayName => + displayNameEditingController.text != displayName; String get displayName => - profileNotifier.value.displayName ?? + currentProfile?.displayName ?? client.mxid(context).localpart ?? client.mxid(context); @@ -72,19 +89,14 @@ class SettingsProfileController extends State { ]; List> actions() => [ - if (PlatformInfos.isMobile) - SheetAction( - key: AvatarAction.camera, - label: L10n.of(context)!.openCamera, - isDefaultAction: true, - icon: Icons.camera_alt_outlined, - ), SheetAction( key: AvatarAction.file, - label: L10n.of(context)!.openGallery, - icon: Icons.photo_outlined, + label: L10n.of(context)!.changeProfilePhoto, + icon: Icons.add_a_photo_outlined, ), - if (profileNotifier.value.avatarUrl != null) + if (currentProfile?.avatarUrl != null || + assetEntity != null || + filePickerResult != null) SheetAction( key: AvatarAction.remove, label: L10n.of(context)!.removeYourAvatar, @@ -116,65 +128,95 @@ class SettingsProfileController extends State { } void _handleRemoveAvatarAction() async { - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(null), - ); - if (success.error == null) { - _getCurrentProfile(client, isUpdated: true); + if ((assetEntity != null || filePickerResult != null) && + currentProfile?.avatarUrl == null) { + _clearImageInLocal(); + return; } + TwakeLoadingDialog.showLoadingDialog(context); + final newProfile = Profile( + userId: client.userID!, + displayName: displayNameEditingController.text, + avatarUrl: null, + ); + settingsProfileUIState.value = + Right(GetProfileUIStateSuccess(newProfile)); + _uploadProfile(isDeleteAvatar: true); return; } - Future _handleGetAvatarInByte() async { + void _getImageOnWeb( + BuildContext context, + ) async { final result = await FilePicker.platform.pickFiles( type: FileType.image, - withData: true, ); - final pickedFile = result?.files.firstOrNull; - if (pickedFile == null || pickedFile.bytes == null) return null; - return MatrixFile( - bytes: pickedFile.bytes!, - name: pickedFile.name, + Logs().d( + 'SettingsProfile::_getImageOnWeb(): FilePickerResult - $result', ); + if (result == null || result.files.single.bytes == null) { + return; + } else { + if (!isEditedProfileNotifier.value) { + isEditedProfileNotifier.toggle(); + } + settingsProfileUIState.value = Right( + GetAvatarInBytesUIStateSuccess( + filePickerResult: result, + ), + ); + Logs().d( + 'SettingsProfile::_getImageOnWeb(): AvatarWebNotifier - $result', + ); + } } - Future _handleGetAvatarInStream(AvatarAction action) async { - final result = await ImagePicker().pickImage( - source: action == AvatarAction.camera - ? ImageSource.camera - : ImageSource.gallery, - imageQuality: AppConfig.imageQuality, - ); - if (result == null) return null; - return MatrixFile( - bytes: await result.readAsBytes(), - name: result.path, - ); + void _showImagesPickerAction() async { + if (PlatformInfos.isWeb) { + _getImageOnWeb(context); + return; + } + final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionCamera = await getCurrentCameraPermission(); + if (currentPermissionPhotos != null && currentPermissionCamera != null) { + final imagePickerController = createImagePickerController(); + showImagePickerBottomSheet( + context, + currentPermissionPhotos, + currentPermissionCamera, + imagePickerController, + ); + } } - void _handleGetAvatarAction(AvatarAction action) async { - MatrixFile file; - if (PlatformInfos.isMobile) { - final matrixFile = await _handleGetAvatarInStream(action); - if (matrixFile == null) return; - file = matrixFile; - } else { - final matrixFile = await _handleGetAvatarInByte(); - if (matrixFile == null) return; - file = matrixFile; - } - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setAvatar(file), + ImagePickerGridController createImagePickerController() { + final imagePickerController = ImagePickerGridController( + AssetCounter(imagePickerMode: ImagePickerMode.single), ); - if (success.error == null) { - _getCurrentProfile(client, isUpdated: true); - } + + imagePickerController.addListener(() { + final selectedAsset = imagePickerController.selectedAssets.firstOrNull; + if (selectedAsset?.asset.type == AssetType.image) { + if (!imagePickerController.pickFromCamera()) { + Navigator.pop(context); + } + settingsProfileUIState.value = Right( + GetAvatarInStreamUIStateSuccess( + assetEntity: selectedAsset?.asset, + ), + ); + if (!isEditedProfileNotifier.value) { + isEditedProfileNotifier.toggle(); + } + imagePickerController.removeAllSelectedItem(); + } + }); + + return imagePickerController; } void setAvatarAction() async { - final action = actions().length == 1 + final action = actions().isEmpty ? actions().single.key : await showModalActionSheet( context: context, @@ -184,11 +226,14 @@ class SettingsProfileController extends State { if (action == null) return; if (action == AvatarAction.remove) { _handleRemoveAvatarAction(); + return; } - _handleGetAvatarAction(action); + _showImagesPickerAction(); } - void _handleSyncProfile() async { + void _sendAccountDataEvent({ + required Profile profile, + }) async { Logs().d( 'SettingsProfileController::_handleSyncProfile() - Syncing profile', ); @@ -196,7 +241,7 @@ class SettingsProfileController extends State { client: client, basicEvent: BasicEvent( type: TwakeInappEventTypes.uploadAvatarEvent, - content: profileNotifier.value.toJson(), + content: profile.toJson(), ), ); Logs().d( @@ -204,24 +249,170 @@ class SettingsProfileController extends State { ); } - void setDisplayNameAction() async { - if (displayNameFocusNode.hasFocus) { - displayNameFocusNode.unfocus(); + void _setAvatarInStream() { + if (assetEntity != null) { + uploadContentInteractor + .execute( + matrixClient: client, + entity: assetEntity!, + ) + .listen( + (event) => _handleUploadAvatarOnData(context, event), + onDone: _handleUploadAvatarOnDone, + onError: _handleUploadAvatarOnError, + ); + } else { + _uploadProfile(displayName: displayNameEditingController.text); } - final matrix = Matrix.of(context); - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.setDisplayName( - matrix.client.userID!, - displayNameEditingController.text, - ), - ); - if (success.error == null) { - isEditedProfileNotifier.toggle(); - _getCurrentProfile(client, isUpdated: true); + } + + void _setAvatarInBytes() { + if (filePickerResult != null) { + uploadContentWebInteractor + .execute( + matrixClient: client, + filePickerResult: filePickerResult!, + ) + .listen( + (event) => _handleUploadAvatarOnData(context, event), + onDone: _handleUploadAvatarOnDone, + onError: _handleUploadAvatarOnError, + ); + } else { + _uploadProfile(displayName: displayNameEditingController.text); + } + } + + void onUploadProfileAction() { + displayNameFocusNode.unfocus(); + TwakeLoadingDialog.showLoadingDialog(context); + if (PlatformInfos.isMobile) { + _setAvatarInStream(); + } else { + _setAvatarInBytes(); } } + void _clearImageInLocal() { + if (assetEntity != null) { + assetEntity = null; + } + if (filePickerResult != null) { + filePickerResult = null; + } + } + + void _handleUploadAvatarOnDone() { + Logs().d( + 'SettingsProfile::_handleUploadAvatarOnDone() - done', + ); + } + + void _handleUploadAvatarOnError( + dynamic error, + StackTrace? stackTrace, + ) { + TwakeLoadingDialog.hideLoadingDialog(context); + Logs().e( + 'SettingsProfile::_handleUploadAvatarOnError() - error: $error | stackTrace: $stackTrace', + ); + } + + void _handleUploadAvatarOnData( + BuildContext context, + Either event, + ) { + Logs().d('SettingsProfile::_handleUploadAvatarOnData()'); + event.fold( + (failure) { + Logs().e( + 'SettingsProfile::_handleUploadAvatarOnData() - failure: $failure', + ); + }, + (success) { + Logs().d( + 'SettingsProfile::_handleUploadAvatarOnData() - success: $success', + ); + if (success is UploadContentSuccess) { + _uploadProfile( + avatarUr: success.uri, + displayName: _hasEditedDisplayName + ? displayNameEditingController.text + : null, + ); + } + }, + ); + } + + void _uploadProfile({ + Uri? avatarUr, + String? displayName, + bool isDeleteAvatar = false, + }) async { + uploadProfileInteractor + .execute( + client: client, + avatarUrl: avatarUr, + isDeleteAvatar: isDeleteAvatar, + displayName: displayName, + ) + .listen( + (event) => _handleUploadProfileOnData(context, event), + onDone: _handleUploadProfileOnDone, + onError: _handleUploadProfileOnError, + ); + } + + void _handleUploadProfileOnDone() { + Logs().d( + 'SettingsProfile::_handleUploadProfileOnDone() - done', + ); + } + + void _handleUploadProfileOnError( + dynamic error, + StackTrace? stackTrace, + ) { + TwakeLoadingDialog.hideLoadingDialog(context); + Logs().e( + 'SettingsProfile::_handleUploadProfileOnError() - error: $error | stackTrace: $stackTrace', + ); + } + + void _handleUploadProfileOnData( + BuildContext context, + Either event, + ) { + Logs().d('SettingsProfile::_handleUploadProfileOnData()'); + event.fold( + (failure) { + Logs().e( + 'SettingsProfile::_handleUploadProfileOnData() - failure: $failure', + ); + }, + (success) { + Logs().d( + 'SettingsProfile::_handleUploadProfileOnData() - success: $success', + ); + if (success is UpdateProfileSuccess) { + _clearImageInLocal(); + final newProfile = Profile( + userId: client.userID!, + displayName: success.displayName ?? displayName, + avatarUrl: success.avatar ?? currentProfile?.avatarUrl, + ); + _sendAccountDataEvent(profile: newProfile); + if (!success.isDeleteAvatar) { + isEditedProfileNotifier.toggle(); + } + _getCurrentProfile(client, isUpdated: true); + TwakeLoadingDialog.hideLoadingDialog(context); + } + }, + ); + } + void _getCurrentProfile( Client client, { isUpdated = false, @@ -234,10 +425,16 @@ class SettingsProfileController extends State { Logs().d( 'SettingsProfileController::_getCurrentProfile() - currentProfile: $profile', ); - profileNotifier.value = profile; + settingsProfileUIState.value = + Right(GetProfileUIStateSuccess(profile)); + Logs().d( + 'SettingsProfileController::_getCurrentProfile() - currentProfile: ${settingsProfileUIState.value}', + ); + if (profile.avatarUrl == null) { + _clearImageInLocal(); + } displayNameEditingController.text = displayName; matrixIdEditingController.text = client.mxid(context); - _handleSyncProfile(); } void handleTextEditOnChange(SettingsProfileEnum settingsProfileEnum) { @@ -251,6 +448,10 @@ class SettingsProfileController extends State { } void _listeningDisplayNameHasChange() { + if (displayNameEditingController.text.isEmpty) { + isEditedProfileNotifier.value = false; + return; + } isEditedProfileNotifier.value = displayNameEditingController.text != displayName; Logs().d( @@ -258,16 +459,6 @@ class SettingsProfileController extends State { ); } - void _initProfile() { - if (widget.profile == null) { - _getCurrentProfile(client); - return; - } - profileNotifier.value = widget.profile!; - displayNameEditingController.text = displayName; - matrixIdEditingController.text = client.mxid(context); - } - void copyEventsAction(SettingsProfileEnum settingsProfileEnum) { switch (settingsProfileEnum) { case SettingsProfileEnum.matrixId: @@ -290,9 +481,39 @@ class SettingsProfileController extends State { } } + void _handleViewState() { + settingsProfileUIState.addListener(() { + Logs().d( + "settingsProfileUIState()::_handleViewState(): ${settingsProfileUIState.value}", + ); + settingsProfileUIState.value.fold( + (failure) => null, + (success) { + switch (success.runtimeType) { + case GetAvatarInStreamUIStateSuccess: + final uiState = success as GetAvatarInStreamUIStateSuccess; + assetEntity = uiState.assetEntity; + break; + case GetAvatarInBytesUIStateSuccess: + final uiState = success as GetAvatarInBytesUIStateSuccess; + filePickerResult = uiState.filePickerResult; + break; + case GetProfileUIStateSuccess: + final uiState = success as GetProfileUIStateSuccess; + currentProfile = uiState.profile; + break; + default: + break; + } + }, + ); + }); + } + @override void initState() { - _initProfile(); + _handleViewState(); + _getCurrentProfile(client); super.initState(); } @@ -301,6 +522,8 @@ class SettingsProfileController extends State { displayNameEditingController.dispose(); matrixIdEditingController.dispose(); displayNameFocusNode.dispose(); + settingsProfileUIState.dispose(); + isEditedProfileNotifier.dispose(); super.dispose(); } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart index 3ca594a2c..a3054c9c0 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart @@ -1,3 +1,6 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; import 'package:fluffychat/presentation/model/settings/settings_profile_presentation.dart'; @@ -14,6 +17,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { final IconData? leadingIcon; final void Function(String, SettingsProfileEnum)? onChange; final VoidCallback? onCopyAction; + final ValueNotifier> settingsProfileUIState; const SettingsProfileItemBuilder({ super.key, @@ -26,6 +30,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { this.leadingIcon, this.onChange, this.onCopyAction, + required this.settingsProfileUIState, }); @override @@ -57,29 +62,35 @@ class SettingsProfileItemBuilder extends StatelessWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - TextField( - onChanged: (value) => - onChange!(value, settingsProfileEnum), - readOnly: !settingsProfilePresentation.isEditable, - autofocus: false, - focusNode: focusNode, - controller: textEditingController, - decoration: InputDecoration( - suffixIcon: IconButton( - onPressed: settingsProfilePresentation.isEditable - ? () { - focusNode?.requestFocus(); - } - : onCopyAction, - icon: Icon( - suffixIcon, - size: SettingsProfileItemStyle.iconSize, - color: - Theme.of(context).colorScheme.onSurfaceVariant, + ValueListenableBuilder( + valueListenable: settingsProfileUIState, + builder: (context, _, __) { + return TextField( + onChanged: (value) => + onChange!(value, settingsProfileEnum), + readOnly: !settingsProfilePresentation.isEditable, + autofocus: false, + focusNode: focusNode, + controller: textEditingController, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: settingsProfilePresentation.isEditable + ? () { + focusNode?.requestFocus(); + } + : onCopyAction, + icon: Icon( + suffixIcon, + size: SettingsProfileItemStyle.iconSize, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + hintText: textEditingController?.text, ), - ), - hintText: textEditingController?.text, - ), + ); + }, ), Divider( height: SettingsProfileItemStyle.dividerSize, diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart new file mode 100644 index 000000000..8dcaa68b5 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart @@ -0,0 +1,25 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; + +class GetAvatarInitialUIState extends SettingsProfileUIState {} + +class GetAvatarInStreamUIStateSuccess extends SettingsProfileUIState { + final AssetEntity? assetEntity; + + GetAvatarInStreamUIStateSuccess({ + this.assetEntity, + }); + + @override + List get props => [assetEntity]; +} + +class GetAvatarInBytesUIStateSuccess extends SettingsProfileUIState { + final FilePickerResult? filePickerResult; + + GetAvatarInBytesUIStateSuccess({this.filePickerResult}); + + @override + List get props => [filePickerResult]; +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart new file mode 100644 index 000000000..7e488049b --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart @@ -0,0 +1,11 @@ +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart'; +import 'package:matrix/matrix.dart'; + +class GetProfileUIStateSuccess extends SettingsProfileUIState { + final Profile profile; + + GetProfileUIStateSuccess(this.profile); + + @override + List get props => [profile]; +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart new file mode 100644 index 000000000..54f00f7e6 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/settings_profile_ui_state.dart @@ -0,0 +1,3 @@ +import 'package:fluffychat/presentation/state/success.dart'; + +abstract class SettingsProfileUIState extends UIState {} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart index eda5b7388..90df760cc 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart'; import 'package:fluffychat/presentation/model/settings/settings_profile_presentation.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; @@ -32,7 +33,7 @@ class SettingsProfileView extends StatelessWidget { leading: IconButton( icon: const Icon( Icons.arrow_back, - size: 24, + size: SettingsProfileViewStyle.sizeIcon, ), onPressed: () => context.pop(), ), @@ -49,14 +50,11 @@ class SettingsProfileView extends StatelessWidget { if (!edited) return const SizedBox(); return InkWell( borderRadius: BorderRadius.circular( - 20, + SettingsProfileViewStyle.borderRadius, ), - onTap: () => controller.setDisplayNameAction(), + onTap: () => controller.onUploadProfileAction(), child: Padding( - padding: const EdgeInsetsDirectional.symmetric( - vertical: 14, - horizontal: 12, - ), + padding: SettingsProfileViewStyle.paddingTextButton, child: Text( L10n.of(context)!.done, style: Theme.of(context).textTheme.labelLarge?.copyWith( @@ -74,7 +72,7 @@ class SettingsProfileView extends StatelessWidget { ? Theme.of(context).colorScheme.surface : null, body: SingleChildScrollView( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 16), + padding: SettingsProfileViewStyle.paddingBody, child: SlotLayout( config: { const WidthPlatformBreakpoint( @@ -83,8 +81,8 @@ class SettingsProfileView extends StatelessWidget { key: settingsProfileViewMobileKey, builder: (_) { return SettingsProfileViewMobile( - profileNotifier: controller.profileNotifier, - displayName: controller.displayName, + client: controller.client, + settingsProfileUIState: controller.settingsProfileUIState, onAvatarTap: () => controller.setAvatarAction(), settingsProfileOptions: ListView.separated( shrinkWrap: true, @@ -95,6 +93,8 @@ class SettingsProfileView extends StatelessWidget { controller.getListProfileMobile[index], title: controller.getListProfileMobile[index] .getTitle(context), + settingsProfileUIState: + controller.settingsProfileUIState, settingsProfilePresentation: SettingsProfilePresentation( settingsProfileType: controller @@ -134,13 +134,15 @@ class SettingsProfileView extends StatelessWidget { key: settingsProfileViewWebKey, builder: (_) { return SettingsProfileViewWeb( - profileNotifier: controller.profileNotifier, - displayName: controller.displayName, + settingsProfileUIState: controller.settingsProfileUIState, + client: controller.client, basicInfoWidget: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return SettingsProfileItemBuilder( + settingsProfileUIState: + controller.settingsProfileUIState, settingsProfileEnum: controller.getListProfileBasicInfo[index], title: controller.getListProfileBasicInfo[index] @@ -175,6 +177,8 @@ class SettingsProfileView extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return SettingsProfileItemBuilder( + settingsProfileUIState: + controller.settingsProfileUIState, settingsProfileEnum: controller.getListProfileWorkIdentitiesInfo[index], title: controller diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart index 44ae1e804..27fa0e98e 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -1,98 +1,152 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; class SettingsProfileViewMobile extends StatelessWidget { - final ValueNotifier profileNotifier; + final ValueNotifier> settingsProfileUIState; final Widget settingsProfileOptions; final VoidCallback onAvatarTap; - final String displayName; + final Client client; const SettingsProfileViewMobile({ super.key, - required this.profileNotifier, required this.settingsProfileOptions, required this.onAvatarTap, - required this.displayName, + required this.settingsProfileUIState, + required this.client, }); @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: profileNotifier, - builder: (context, profile, __) { - return Column( - children: [ - Divider( - height: SettingsProfileViewMobileStyle.dividerHeight, - color: LinagoraStateLayer( - LinagoraSysColors.material().surfaceTint, - ).opacityLayer3, - ), - Padding( - padding: SettingsProfileViewMobileStyle.padding, - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - const SizedBox( - width: SettingsProfileViewMobileStyle.widthSize, - ), - Material( - elevation: - Theme.of(context).appBarTheme.scrolledUnderElevation ?? + return Column( + children: [ + Divider( + height: SettingsProfileViewMobileStyle.dividerHeight, + color: LinagoraStateLayer( + LinagoraSysColors.material().surfaceTint, + ).opacityLayer3, + ), + Padding( + padding: SettingsProfileViewMobileStyle.padding, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox( + width: SettingsProfileViewMobileStyle.widthSize, + ), + ValueListenableBuilder( + valueListenable: settingsProfileUIState, + builder: (context, uiState, child) => uiState.fold( + (failure) => child!, + (success) { + if (success is GetAvatarInStreamUIStateSuccess) { + if (success.assetEntity == null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( + AvatarStyle.defaultSize, + ), + child: AssetEntityImage( + success.assetEntity!, + thumbnailSize: const ThumbnailSize( + SettingsProfileViewMobileStyle.thumbnailSize, + SettingsProfileViewMobileStyle.thumbnailSize, + ), + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress != null && + loadingProgress.cumulativeBytesLoaded != + loadingProgress.expectedTotalBytes) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + return child; + }, + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Icon(Icons.error_outline), + ); + }, + ), + ), + ); + } + if (success is GetProfileUIStateSuccess) { + final displayName = success.profile.displayName ?? + client.mxid(context).localpart ?? + client.mxid(context); + return Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? 4, - shadowColor: Theme.of(context).appBarTheme.shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize, - ), - ), - child: Avatar( - mxContent: profileNotifier.value.avatarUrl, - name: displayName, - size: SettingsProfileViewMobileStyle.avatarSize, - fontSize: SettingsProfileViewMobileStyle.avatarFontSize, - ), - ), - Positioned( - bottom: SettingsProfileViewMobileStyle.positionedBottomSize, - right: SettingsProfileViewMobileStyle.positionedRightSize, - child: InkWell( - onTap: onAvatarTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular( - SettingsProfileViewMobileStyle.avatarSize, + shadowColor: Theme.of(context).appBarTheme.shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).dividerColor, ), - border: Border.all( - color: Theme.of(context).colorScheme.onPrimary, - width: SettingsProfileViewMobileStyle - .iconEditBorderWidth, + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, ), ), - padding: SettingsProfileViewMobileStyle.editIconPadding, - child: Icon( - Icons.edit, - size: SettingsProfileViewMobileStyle.iconEditSize, - color: Theme.of(context).colorScheme.onPrimary, + child: Avatar( + mxContent: success.profile.avatarUrl, + name: displayName, + size: SettingsProfileViewMobileStyle.avatarSize, + fontSize: + SettingsProfileViewMobileStyle.avatarFontSize, ), + ); + } + return child!; + }, + ), + child: const SizedBox.shrink(), + ), + Positioned( + bottom: SettingsProfileViewMobileStyle.positionedBottomSize, + right: SettingsProfileViewMobileStyle.positionedRightSize, + child: InkWell( + onTap: onAvatarTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewMobileStyle.avatarSize, ), + border: Border.all( + color: Theme.of(context).colorScheme.onPrimary, + width: + SettingsProfileViewMobileStyle.iconEditBorderWidth, + ), + ), + padding: SettingsProfileViewMobileStyle.editIconPadding, + child: Icon( + Icons.edit, + size: SettingsProfileViewMobileStyle.iconEditSize, + color: Theme.of(context).colorScheme.onPrimary, ), ), - ], + ), ), - ), - settingsProfileOptions, - ], - ); - }, + ], + ), + ), + settingsProfileOptions, + ], ); } } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart index 3ba1345b5..76f99dfde 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart @@ -9,6 +9,7 @@ class SettingsProfileViewMobileStyle { static const double iconEditBorderWidth = 4; static const double iconEditSize = 24; static const double dividerHeight = 2; + static const int thumbnailSize = 28; static EdgeInsetsDirectional padding = const EdgeInsetsDirectional.symmetric(vertical: 16.0); diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart new file mode 100644 index 000000000..628e50771 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_style.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class SettingsProfileViewStyle { + static const sizeIcon = 24.0; + static const borderRadius = 20.0; + + static const EdgeInsetsDirectional paddingTextButton = + EdgeInsetsDirectional.symmetric( + vertical: 14, + horizontal: 12, + ); + + static const EdgeInsetsDirectional paddingBody = + EdgeInsetsDirectional.symmetric( + horizontal: 16, + ); +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart index 8af3562d0..2a4435ff6 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart @@ -1,4 +1,10 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; @@ -7,215 +13,245 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class SettingsProfileViewWeb extends StatelessWidget { - final ValueNotifier profileNotifier; + final ValueNotifier> settingsProfileUIState; final Widget basicInfoWidget; final Widget workIdentitiesInfoWidget; final VoidCallback onAvatarTap; - final String displayName; + final Client client; const SettingsProfileViewWeb({ super.key, - required this.profileNotifier, required this.basicInfoWidget, required this.onAvatarTap, required this.workIdentitiesInfoWidget, - required this.displayName, + required this.client, + required this.settingsProfileUIState, }); @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: profileNotifier, - builder: (context, profile, __) { - return Padding( - padding: SettingsProfileViewWebStyle.paddingBody, - child: Center( - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: SettingsProfileViewWebStyle.bodyWidth, - padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - SettingsProfileViewWebStyle.radiusCircular, - ), + return Padding( + padding: SettingsProfileViewWebStyle.paddingBody, + child: Center( + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: SettingsProfileViewWebStyle.bodyWidth, + padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.radiusCircular, + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + SettingsProfileViewWebStyle.paddingBasicInfoTitle, + child: Text( + L10n.of(context)!.basicInfo, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), ), - child: Column( - mainAxisSize: MainAxisSize.min, + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: - SettingsProfileViewWebStyle.paddingBasicInfoTitle, - child: Text( - L10n.of(context)!.basicInfo, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - Theme.of(context).colorScheme.onSurface, - ), - ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: SettingsProfileViewWebStyle - .paddingWidgetBasicInfo, - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - const SizedBox( - width: - SettingsProfileViewWebStyle.widthSize, - ), - Material( - elevation: Theme.of(context) - .appBarTheme - .scrolledUnderElevation ?? - 4, - shadowColor: Theme.of(context) - .appBarTheme - .shadowColor, - shape: RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular( - AvatarStyle.defaultSize, - ), - ), - child: Avatar( - mxContent: - profileNotifier.value.avatarUrl, - name: displayName, - size: SettingsProfileViewWebStyle - .avatarSize, - fontSize: SettingsProfileViewWebStyle - .avatarFontSize, - ), - ), - Positioned( - bottom: SettingsProfileViewWebStyle - .positionedBottomSize, - right: SettingsProfileViewWebStyle - .positionedRightSize, - child: InkWell( - onTap: onAvatarTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primary, - borderRadius: BorderRadius.circular( + padding: SettingsProfileViewWebStyle + .paddingWidgetBasicInfo, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + const SizedBox( + width: SettingsProfileViewWebStyle.widthSize, + ), + ValueListenableBuilder( + valueListenable: settingsProfileUIState, + builder: (context, uiState, child) => + uiState.fold( + (failure) => child!, + (success) { + if (success + is GetAvatarInBytesUIStateSuccess) { + if (success.filePickerResult == null || + success.filePickerResult?.files.single + .bytes == + null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( SettingsProfileViewWebStyle - .avatarSize, + .radiusImageMemory, + ), + child: Image.memory( + success.filePickerResult!.files + .single.bytes!, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) { + return const Center( + child: + Icon(Icons.error_outline), + ); + }, + ), + ), + ); + } + if (success is GetProfileUIStateSuccess) { + final displayName = + success.profile.displayName ?? + client.mxid(context).localpart ?? + client.mxid(context); + return Material( + elevation: Theme.of(context) + .appBarTheme + .scrolledUnderElevation ?? + 4, + shadowColor: Theme.of(context) + .appBarTheme + .shadowColor, + shape: RoundedRectangleBorder( + side: BorderSide( + color: + Theme.of(context).dividerColor, ), - border: Border.all( - color: Theme.of(context) - .colorScheme - .onPrimary, - width: 4, + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, ), ), - padding: SettingsProfileViewWebStyle - .paddingEditIcon, - child: Icon( - Icons.edit, + child: Avatar( + mxContent: success.profile.avatarUrl, + name: displayName, size: SettingsProfileViewWebStyle - .iconEditSize, - color: Theme.of(context) - .colorScheme - .onPrimary, + .avatarSize, + fontSize: SettingsProfileViewWebStyle + .avatarFontSize, ), + ); + } + return child!; + }, + ), + child: const SizedBox.shrink(), + ), + Positioned( + bottom: SettingsProfileViewWebStyle + .positionedBottomSize, + right: SettingsProfileViewWebStyle + .positionedRightSize, + child: InkWell( + onTap: onAvatarTap, + child: Container( + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.avatarSize, + ), + border: Border.all( + color: Theme.of(context) + .colorScheme + .onPrimary, + width: 4, ), ), + padding: SettingsProfileViewWebStyle + .paddingEditIcon, + child: Icon( + Icons.edit, + size: SettingsProfileViewWebStyle + .iconEditSize, + color: Theme.of(context) + .colorScheme + .onPrimary, + ), ), - ], + ), ), - ), - Expanded( - child: basicInfoWidget, - ), - ], - ), - ], - ), - ), - Padding( - padding: SettingsProfileViewWebStyle - .paddingWidgetEditProfileInfo, - child: Text( - L10n.of(context)!.editProfileDescriptions, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: LinagoraRefColors.material().tertiary[30], + ], ), - ), - ), - Container( - width: SettingsProfileViewWebStyle.bodyWidth, - padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - SettingsProfileViewWebStyle.radiusCircular, ), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: - SettingsProfileViewWebStyle.paddingBasicInfoTitle, - child: Text( - L10n.of(context)!.workIdentitiesInfo, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - Theme.of(context).colorScheme.onSurface, - ), - ), + Expanded( + child: basicInfoWidget, ), - Padding( - padding: SettingsProfileViewWebStyle - .paddingWorkIdentitiesInfoWidget, - child: workIdentitiesInfoWidget, - ) ], ), - ), - Padding( - padding: SettingsProfileViewWebStyle - .paddingWidgetEditProfileInfo, - child: Text( - L10n.of(context)!.editWorkIdentitiesDescriptions, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: LinagoraRefColors.material().tertiary[30], - ), + ], + ), + ), + Padding( + padding: + SettingsProfileViewWebStyle.paddingWidgetEditProfileInfo, + child: Text( + L10n.of(context)!.editProfileDescriptions, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), + ), + ), + Container( + width: SettingsProfileViewWebStyle.bodyWidth, + padding: SettingsProfileViewWebStyle.paddingWidgetBasicInfo, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle.radiusCircular, ), ), - ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + SettingsProfileViewWebStyle.paddingBasicInfoTitle, + child: Text( + L10n.of(context)!.workIdentitiesInfo, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + Padding( + padding: SettingsProfileViewWebStyle + .paddingWorkIdentitiesInfoWidget, + child: workIdentitiesInfoWidget, + ) + ], + ), + ), + Padding( + padding: + SettingsProfileViewWebStyle.paddingWidgetEditProfileInfo, + child: Text( + L10n.of(context)!.editWorkIdentitiesDescriptions, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), + ), ), - ), + ], ), - ); - }, + ), + ), ); } } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart index a6a4680f2..6c99274d4 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web_style.dart @@ -11,6 +11,7 @@ class SettingsProfileViewWebStyle { static const double iconEditSize = 24; static const double dividerHeight = 2; static const double radiusCircular = 16; + static const double radiusImageMemory = 48; static const EdgeInsetsDirectional paddingBody = EdgeInsetsDirectional.all(32); diff --git a/lib/presentation/state/failure.dart b/lib/presentation/state/failure.dart new file mode 100644 index 000000000..40b74011a --- /dev/null +++ b/lib/presentation/state/failure.dart @@ -0,0 +1,10 @@ +import 'package:fluffychat/app_state/failure.dart'; + +abstract class FeatureFailure extends Failure { + final dynamic exception; + + const FeatureFailure({this.exception}); + + @override + List get props => [exception]; +} diff --git a/lib/presentation/state/success.dart b/lib/presentation/state/success.dart new file mode 100644 index 000000000..333c5147f --- /dev/null +++ b/lib/presentation/state/success.dart @@ -0,0 +1,16 @@ +import 'package:fluffychat/app_state/success.dart'; + +abstract class ViewState extends Success {} + +abstract class ViewEvent extends Success {} + +class UIState extends ViewState { + static final idle = UIState(); + + UIState() : super(); + + @override + List get props => []; +} + +class LoadingState extends UIState {} diff --git a/lib/utils/dialog/twake_loading_dialog.dart b/lib/utils/dialog/twake_loading_dialog.dart new file mode 100644 index 000000000..6e1acbe63 --- /dev/null +++ b/lib/utils/dialog/twake_loading_dialog.dart @@ -0,0 +1,57 @@ +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; +import 'package:flutter/material.dart'; + +class TwakeLoadingDialog { + static void hideLoadingDialog(BuildContext context) { + if (PlatformInfos.isWeb) { + TwakeApp.router.routerDelegate.pop(); + } else { + Navigator.pop(context); + } + } + + static void showLoadingDialog(BuildContext context) { + showGeneralDialog( + useRootNavigator: PlatformInfos.isWeb, + transitionDuration: const Duration(milliseconds: 700), + transitionBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: Tween(begin: 0, end: 1).animate(animation), + child: WillPopScope( + onWillPop: () async => false, + child: const ProgressDialog(), + ), + ); + }, + context: context, + pageBuilder: (c, a1, a2) { + return const SizedBox(); + }, + ); + } +} + +class ProgressDialog extends StatelessWidget { + const ProgressDialog({super.key}); + + @override + Widget build(BuildContext context) { + return const AlertDialog( + content: Row( + children: [ + Padding( + padding: EdgeInsets.only(right: 16.0), + child: CircularProgressIndicator.adaptive(), + ), + Expanded( + child: Text( + 'Loading... Please Wait!', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ); + } +} diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart index 3739c32ec..5a883f93e 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart @@ -76,7 +76,10 @@ class _AdaptiveScaffoldPrimaryNavigationState void _handleOnAccountDataSubscription() { onAccountDataSubscription = client.onAccountData.stream.listen((event) { if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { - profileNotifier.value = Profile.fromJson(event.content); + final newProfile = Profile.fromJson(event.content); + if (newProfile.avatarUrl != profileNotifier.value.avatarUrl) { + profileNotifier.value = Profile.fromJson(event.content); + } } }); } From e25689a3cadecc9919ec97f6c91a3621550e9a33 Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Mon, 2 Oct 2023 15:57:49 +0200 Subject: [PATCH 30/60] fix: drag & drop on web version --- .../platform_file/platform_file_extension.dart | 12 ++++++++++++ .../usecase/send_file_on_web_interactor.dart | 15 ++------------- lib/pages/chat/chat.dart | 12 ++++-------- lib/pages/chat_draft/draft_chat.dart | 5 ++++- lib/presentation/mixins/send_files_mixin.dart | 9 +++++++-- 5 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 lib/domain/model/extensions/platform_file/platform_file_extension.dart diff --git a/lib/domain/model/extensions/platform_file/platform_file_extension.dart b/lib/domain/model/extensions/platform_file/platform_file_extension.dart new file mode 100644 index 000000000..01039f7d4 --- /dev/null +++ b/lib/domain/model/extensions/platform_file/platform_file_extension.dart @@ -0,0 +1,12 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:matrix/matrix.dart'; + +extension PlatformFileListExtension on PlatformFile { + MatrixFile toMatrixFile() { + return MatrixFile.fromMimeType( + bytes: bytes, + name: name, + filePath: '', + ); + } +} diff --git a/lib/domain/usecase/send_file_on_web_interactor.dart b/lib/domain/usecase/send_file_on_web_interactor.dart index 4d71e52c5..643b041e1 100644 --- a/lib/domain/usecase/send_file_on_web_interactor.dart +++ b/lib/domain/usecase/send_file_on_web_interactor.dart @@ -1,11 +1,10 @@ -import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/presentation/extensions/send_file_web_extension.dart'; import 'package:matrix/matrix.dart'; class SendFileOnWebInteractor { Future execute({ required Room room, - required FilePickerResult filePickerResult, + required List files, String? txId, Event? inReplyTo, String? editEventId, @@ -13,17 +12,7 @@ class SendFileOnWebInteractor { Map? extraContent, }) async { try { - final matrixFiles = filePickerResult.files - .map( - (xFile) => MatrixFile.fromMimeType( - bytes: xFile.bytes, - name: xFile.name, - filePath: '', - ), - ) - .toList(); - - for (final matrixFile in matrixFiles) { + for (final matrixFile in files) { await room.sendFileOnWebEvent( matrixFile, txid: txId, diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index ea426efb5..673d23077 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -13,6 +13,7 @@ import 'package:fluffychat/domain/model/download_file/download_file_for_preview_ 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'; +import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/pages/chat/chat_context_menu_actions.dart'; import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/pages/chat/chat_horizontal_action_menu.dart'; @@ -145,6 +146,8 @@ class ChatController extends State void onDragExited(_) => draggingNotifier.value = false; void onDragDone(DropDoneDetails details) async { + final sendFileOnWebInteractor = getIt.get(); + draggingNotifier.value = false; final bytesList = await showFutureLoadingDialog( context: context, @@ -166,14 +169,7 @@ class ChatController extends State ); } - await showDialog( - context: context, - useRootNavigator: false, - builder: (c) => SendFileDialog( - files: matrixFiles, - room: room!, - ), - ); + sendFileOnWebInteractor.execute(room: room!, files: matrixFiles); } bool get canSaveSelectedEvent => diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index f28396d2e..72e426267 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -4,6 +4,7 @@ import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/direct_chat/create_direct_chat_success.dart'; +import 'package:fluffychat/domain/model/extensions/platform_file/platform_file_extension.dart'; import 'package:fluffychat/domain/usecase/create_direct_chat_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/pages/chat/chat.dart'; @@ -282,11 +283,13 @@ class DraftChatController extends State ); if (result == null || result.files.isEmpty) return; + final matrixFilesList = + result.files.map((file) => file.toMatrixFile()).toList(); _createRoom( onRoomCreatedSuccess: (newRoom) { sendFileOnWebInteractor.execute( room: newRoom, - filePickerResult: result, + files: matrixFilesList, ); }, ); diff --git a/lib/presentation/mixins/send_files_mixin.dart b/lib/presentation/mixins/send_files_mixin.dart index c2ce84977..dd0bec543 100644 --- a/lib/presentation/mixins/send_files_mixin.dart +++ b/lib/presentation/mixins/send_files_mixin.dart @@ -1,5 +1,6 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/model/extensions/platform_file/platform_file_extension.dart'; import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; @@ -64,8 +65,12 @@ mixin SendFilesMixin { withData: true, ); if (result == null || result.files.isEmpty) return; - - sendFileOnWebInteractor.execute(room: room!, filePickerResult: result); + final matrixFilesList = + result.files.map((file) => file.toMatrixFile()).toList(); + sendFileOnWebInteractor.execute( + room: room!, + files: matrixFilesList, + ); } void onPickerTypeClick({ From 421ca3dfce4ff3944a7a0cb0ee266a820a3bded8 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Tue, 3 Oct 2023 10:32:59 +0700 Subject: [PATCH 31/60] TW-732 Load more in add member in group need support mouse scroll --- .../new_group/contacts_selection_view.dart | 16 +- .../widget/contacts_selection_list.dart | 38 +++-- .../search_contacts_controller.dart | 4 +- lib/pages/search/get_contacts_controller.dart | 4 +- .../twake_smart_refresher.dart | 158 +++++++++++++----- pubspec.lock | 8 - pubspec.yaml | 1 - 7 files changed, 149 insertions(+), 80 deletions(-) diff --git a/lib/pages/new_group/contacts_selection_view.dart b/lib/pages/new_group/contacts_selection_view.dart index 8dc772f7b..6e9103eef 100644 --- a/lib/pages/new_group/contacts_selection_view.dart +++ b/lib/pages/new_group/contacts_selection_view.dart @@ -59,13 +59,15 @@ class ContactsSelectionView extends StatelessWidget { controller: controller.refreshController!, onRefresh: controller.fetchContacts, onLoading: controller.loadMoreContacts, - child: ContactsSelectionList( - contactsNotifier: controller.contactsNotifier!, - selectedContactsMapNotifier: - controller.selectedContactsMapNotifier, - onSelectedContact: controller.onSelectedContact, - disabledContactIds: controller.disabledContactIds, - ), + slivers: [ + ContactsSelectionList( + contactsNotifier: controller.contactsNotifier!, + selectedContactsMapNotifier: + controller.selectedContactsMapNotifier, + onSelectedContact: controller.onSelectedContact, + disabledContactIds: controller.disabledContactIds, + ) + ], ), ), ), diff --git a/lib/pages/new_group/widget/contacts_selection_list.dart b/lib/pages/new_group/widget/contacts_selection_list.dart index a243784fe..dfaa1e769 100644 --- a/lib/pages/new_group/widget/contacts_selection_list.dart +++ b/lib/pages/new_group/widget/contacts_selection_list.dart @@ -31,37 +31,41 @@ class ContactsSelectionList extends StatelessWidget { return ValueListenableBuilder( valueListenable: contactsNotifier, builder: (context, value, child) => value.fold( - (failure) => Padding( - padding: ContactsSelectionListStyle.notFoundPadding, - child: NoContactsFound( - keyword: failure is GetContactsFailure ? failure.keyword : '', + (failure) => SliverToBoxAdapter( + child: Padding( + padding: ContactsSelectionListStyle.notFoundPadding, + child: NoContactsFound( + keyword: failure is GetContactsFailure ? failure.keyword : '', + ), ), ), (success) { if (success is PresentationExternalContactSuccess) { - return _ContactItem( - selectedContactsMapNotifier: selectedContactsMapNotifier, - onSelectedContact: onSelectedContact, - contact: success.contact, - paddingTop: ContactsSelectionListStyle.listPaddingTop, + return SliverToBoxAdapter( + child: _ContactItem( + selectedContactsMapNotifier: selectedContactsMapNotifier, + onSelectedContact: onSelectedContact, + contact: success.contact, + paddingTop: ContactsSelectionListStyle.listPaddingTop, + ), ); } if (success is! PresentationContactsSuccess) { - return const LoadingContactWidget(); + return const SliverToBoxAdapter(child: LoadingContactWidget()); } if (success.keyword.isNotEmpty && success.data.isEmpty) { - return Padding( - padding: ContactsSelectionListStyle.notFoundPadding, - child: NoContactsFound( - keyword: success.keyword, + return SliverToBoxAdapter( + child: Padding( + padding: ContactsSelectionListStyle.notFoundPadding, + child: NoContactsFound( + keyword: success.keyword, + ), ), ); } - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + return SliverList.builder( itemCount: success.data.length, itemBuilder: (context, index) { final contact = success.data[index]; diff --git a/lib/pages/new_private_chat/search_contacts_controller.dart b/lib/pages/new_private_chat/search_contacts_controller.dart index 00fccac1e..160024bf4 100644 --- a/lib/pages/new_private_chat/search_contacts_controller.dart +++ b/lib/pages/new_private_chat/search_contacts_controller.dart @@ -5,9 +5,9 @@ import 'package:fluffychat/app_state/success_converter.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/pages/search/get_contacts_controller.dart'; import 'package:fluffychat/presentation/converters/presentation_contact_converter.dart'; +import 'package:fluffychat/widgets/twake_components/twake_smart_refresher.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -import 'package:pull_to_refresh/pull_to_refresh.dart'; mixin class SearchContactsMixinController { static const _debouncerIntervalInMilliseconds = 300; @@ -25,7 +25,7 @@ mixin class SearchContactsMixinController { ValueNotifier>? get contactsNotifier => _getContactController?.contactsNotifier; - RefreshController? get refreshController => + TwakeRefreshController? get refreshController => _getContactController?.refreshController; void initSearchContacts({ diff --git a/lib/pages/search/get_contacts_controller.dart b/lib/pages/search/get_contacts_controller.dart index 052d8e158..75727bb70 100644 --- a/lib/pages/search/get_contacts_controller.dart +++ b/lib/pages/search/get_contacts_controller.dart @@ -7,8 +7,8 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart'; import 'package:fluffychat/domain/usecase/get_contacts_interactor.dart'; +import 'package:fluffychat/widgets/twake_components/twake_smart_refresher.dart'; import 'package:flutter/material.dart'; -import 'package:pull_to_refresh/pull_to_refresh.dart'; class GetContactsController { SuccessConverter converter; @@ -18,7 +18,7 @@ class GetContactsController { final contactsNotifier = ValueNotifier>( const Right(GetContactsInitial()), ); - final refreshController = RefreshController(); + final refreshController = TwakeRefreshController(); bool _isLoadMore = false; Success? _lastSuccess; diff --git a/lib/widgets/twake_components/twake_smart_refresher.dart b/lib/widgets/twake_components/twake_smart_refresher.dart index 359cf0508..ba43eafb4 100644 --- a/lib/widgets/twake_components/twake_smart_refresher.dart +++ b/lib/widgets/twake_components/twake_smart_refresher.dart @@ -1,59 +1,131 @@ import 'package:flutter/material.dart'; -import 'package:pull_to_refresh/pull_to_refresh.dart'; -class TwakeSmartRefresher extends StatelessWidget { - final RefreshController controller; - final VoidCallback? onRefresh; - final VoidCallback? onLoading; - final bool enablePullUp; - final bool enablePullDown; - final Widget? child; +import 'package:fluffychat/utils/scroll_controller_extension.dart'; + +class TwakeSmartRefresher extends StatefulWidget { + final TwakeRefreshController controller; + final Function? onRefresh; + final Function? onLoading; + final List slivers; const TwakeSmartRefresher({ Key? key, + this.onRefresh, + this.onLoading, required this.controller, - required this.onRefresh, - required this.onLoading, - this.enablePullUp = true, - this.enablePullDown = true, - this.child, + required this.slivers, }) : super(key: key); + @override + State createState() => _TwakeSmartRefresherController(); +} + +class TwakeRefreshController { + final refreshNotifier = ValueNotifier(false); + final loadNotifier = ValueNotifier(false); + final scrollController = ScrollController(); + + bool get isRefeshing => refreshNotifier.value; + bool get isLoading => loadNotifier.value; + + void onRefresh() { + refreshNotifier.value = true; + } + + void refreshCompleted() { + refreshNotifier.value = false; + } + + void onLoading() { + loadNotifier.value = true; + } + + void loadComplete() { + loadNotifier.value = false; + } +} + +class _TwakeSmartRefresherController extends State { + ScrollController get scrollController => widget.controller.scrollController; + + @override + void initState() { + super.initState(); + scrollController.addLoadMoreListener(onLoading); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + Future onRefresh() async { + if (widget.controller.isRefeshing) return; + widget.controller.onRefresh(); + widget.onRefresh?.call(); + } + + Future onLoading() async { + if (widget.controller.isLoading) return; + widget.controller.onLoading(); + await widget.onLoading!(); + } + @override Widget build(BuildContext context) { - return SmartRefresher( - enablePullUp: enablePullUp, - enablePullDown: enablePullDown, - header: CustomHeader( - builder: (context, mode) { - if (mode != RefreshStatus.refreshing) { - return const SizedBox.shrink(); - } - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: CircularProgressIndicator(), + return _TwakeSmartRefresherView( + controller: this, + refreshController: widget.controller, + slivers: widget.slivers, + ); + } +} + +class _TwakeSmartRefresherView extends StatelessWidget { + const _TwakeSmartRefresherView({ + required this.controller, + required this.refreshController, + required this.slivers, + }); + final List slivers; + final TwakeRefreshController refreshController; + final _TwakeSmartRefresherController controller; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: controller.onRefresh, + child: CustomScrollView( + controller: controller.scrollController, + slivers: [ + ValueListenableBuilder( + valueListenable: refreshController.refreshNotifier, + builder: (context, refreshing, child) => SliverToBoxAdapter( + child: refreshing ? const _LoadingIndicator() : const SizedBox(), ), - ); - }, - ), - footer: CustomFooter( - builder: (context, mode) { - if (mode != LoadStatus.loading) { - return const SizedBox.shrink(); - } - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: CircularProgressIndicator(), + ), + ...slivers, + ValueListenableBuilder( + valueListenable: refreshController.loadNotifier, + builder: (context, loading, child) => SliverToBoxAdapter( + child: loading ? const _LoadingIndicator() : const SizedBox(), ), - ); - }, + ) + ], ), - controller: controller, - onRefresh: onRefresh, - onLoading: onLoading, - child: child, + ); + } +} + +class _LoadingIndicator extends StatelessWidget { + const _LoadingIndicator(); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), ); } } diff --git a/pubspec.lock b/pubspec.lock index 60965d072..f2b9fb4f9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1995,14 +1995,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" - pull_to_refresh: - dependency: "direct main" - description: - name: pull_to_refresh - sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 - url: "https://pub.dev" - source: hosted - version: "2.0.0" punycode: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4fc6c98a5..dfe5f013e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -133,7 +133,6 @@ dependencies: mime: ^1.0.4 async: ^2.11.0 cached_network_image: ^3.2.3 - pull_to_refresh: ^2.0.0 flutter_image_compress: ^2.0.4 image_gallery_saver: ^2.0.3 file_saver: ^0.1.1 From ecccc9deb43d73a0958a20f2a76186de67e024d5 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Tue, 3 Oct 2023 10:33:47 +0700 Subject: [PATCH 32/60] TW-679 Load more not work in contact tab --- lib/pages/contacts_tab/contacts_tab.dart | 4 - .../contacts_tab/contacts_tab_body_view.dart | 143 ++++++++++-------- lib/pages/contacts_tab/contacts_tab_view.dart | 13 +- .../contacts_tab/contacts_tab_view_style.dart | 1 + 4 files changed, 79 insertions(+), 82 deletions(-) diff --git a/lib/pages/contacts_tab/contacts_tab.dart b/lib/pages/contacts_tab/contacts_tab.dart index 651c92640..ab1d41cbd 100644 --- a/lib/pages/contacts_tab/contacts_tab.dart +++ b/lib/pages/contacts_tab/contacts_tab.dart @@ -4,7 +4,6 @@ import 'package:fluffychat/pages/contacts_tab/contacts_tab_view.dart'; 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/scroll_controller_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; import 'package:fluffychat/pages/new_private_chat/search_contacts_controller.dart'; @@ -24,14 +23,12 @@ class ContactsTab extends StatefulWidget { class ContactsTabController extends State with ComparablePresentationContactMixin, SearchContactsMixinController { - final scrollController = ScrollController(); final responsive = getIt.get(); @override void initState() { initSearchContacts(); _listenFocusTextEditing(); - scrollController.addLoadMoreListener(loadMoreContacts); super.initState(); } @@ -83,7 +80,6 @@ class ContactsTabController extends State @override void dispose() { disposeSearchContacts(); - scrollController.dispose(); super.dispose(); } diff --git a/lib/pages/contacts_tab/contacts_tab_body_view.dart b/lib/pages/contacts_tab/contacts_tab_body_view.dart index fe0461c7c..26d966e27 100644 --- a/lib/pages/contacts_tab/contacts_tab_body_view.dart +++ b/lib/pages/contacts_tab/contacts_tab_body_view.dart @@ -1,9 +1,11 @@ import 'package:fluffychat/pages/contacts_tab/contacts_tab.dart'; +import 'package:fluffychat/pages/contacts_tab/contacts_tab_view_style.dart'; import 'package:fluffychat/pages/contacts_tab/empty_contacts_body.dart'; import 'package:fluffychat/pages/new_private_chat/widget/expansion_contact_list_tile.dart'; import 'package:fluffychat/pages/new_private_chat/widget/loading_contact_widget.dart'; import 'package:fluffychat/pages/new_private_chat/widget/no_contacts_found.dart'; import 'package:fluffychat/presentation/model/presentation_contact_success.dart'; +import 'package:fluffychat/widgets/twake_components/twake_smart_refresher.dart'; import 'package:flutter/material.dart'; class ContactsTabBodyView extends StatelessWidget { @@ -16,78 +18,87 @@ class ContactsTabBodyView extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - controller: controller.scrollController, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: controller.contactsNotifier == null - ? null - : ValueListenableBuilder( - valueListenable: controller.contactsNotifier!, - builder: (context, value, child) => value.fold( - (failure) => const EmptyContactBody(), - (success) { - if (success is! PresentationContactsSuccess) { - return const LoadingContactWidget(); - } + if (controller.refreshController == null) return const SizedBox(); + return TwakeSmartRefresher( + onLoading: controller.loadMoreContacts, + controller: controller.refreshController!, + slivers: [ + SliverToBoxAdapter( + child: Divider( + height: 1, + thickness: 1, + color: Colors.black.withOpacity(0.15), + ), + ), + const SliverToBoxAdapter( + child: SizedBox( + height: ContactsTabViewStyle.padding, + ), + ), + if (controller.contactsNotifier != null) + ValueListenableBuilder( + valueListenable: controller.contactsNotifier!, + builder: (context, value, child) => value.fold( + (failure) => const SliverToBoxAdapter(child: EmptyContactBody()), + (success) { + if (success is! PresentationContactsSuccess) { + return const SliverToBoxAdapter( + child: LoadingContactWidget(), + ); + } - if (success.data.isEmpty) { - if (success.keyword.isEmpty) { - return const EmptyContactBody(); - } else { - return Padding( - padding: const EdgeInsets.only(left: 8.0, top: 8.0), - child: NoContactsFound( - keyword: success.keyword, - ), - ); - } - } + if (success.data.isEmpty) { + if (success.keyword.isEmpty) { + return const SliverToBoxAdapter(child: EmptyContactBody()); + } else { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + left: ContactsTabViewStyle.padding, + top: ContactsTabViewStyle.padding, + ), + child: NoContactsFound( + keyword: success.keyword, + ), + ), + ); + } + } + return SliverList.builder( + itemCount: success.data.length, + itemBuilder: (context, index) { + final contact = success.data[index]; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: success.data - .map( - (contact) => InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - controller.onContactTap( - context: context, - path: 'rooms', - contact: contact, - ); - }, - child: ExpansionContactListTile( - contact: contact, - highlightKeyword: success.keyword, - ), - ), - ) - .toList() - ..addAll([ - ValueListenableBuilder( - valueListenable: controller.isSearchModeNotifier, - builder: (context, isSearchMode, child) { - if (isSearchMode || success.isEnd) { - return const SizedBox.shrink(); - } - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: CircularProgressIndicator(), - ), - ); - }, - ), - ]), + padding: const EdgeInsets.symmetric( + horizontal: ContactsTabViewStyle.padding, + ), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + controller.onContactTap( + context: context, + path: 'rooms', + contact: contact, + ); + }, + child: ExpansionContactListTile( + contact: contact, + highlightKeyword: success.keyword, + ), ), ); }, - ), - ), - ), + ); + }, + ), + ), + const SliverToBoxAdapter( + child: SizedBox( + height: ContactsTabViewStyle.padding, + ), + ), + ], ); } } diff --git a/lib/pages/contacts_tab/contacts_tab_view.dart b/lib/pages/contacts_tab/contacts_tab_view.dart index 6751fde9d..83ef1e935 100644 --- a/lib/pages/contacts_tab/contacts_tab_view.dart +++ b/lib/pages/contacts_tab/contacts_tab_view.dart @@ -27,18 +27,7 @@ class ContactsTabView extends StatelessWidget { ), ), bottomNavigationBar: bottomNavigationBar, - body: SingleChildScrollView( - child: Column( - children: [ - Divider( - height: 1, - thickness: 1, - color: Colors.black.withOpacity(0.15), - ), - ContactsTabBodyView(contactsController), - ], - ), - ), + body: ContactsTabBodyView(contactsController), ); } } diff --git a/lib/pages/contacts_tab/contacts_tab_view_style.dart b/lib/pages/contacts_tab/contacts_tab_view_style.dart index 12bfe258b..9bf699c71 100644 --- a/lib/pages/contacts_tab/contacts_tab_view_style.dart +++ b/lib/pages/contacts_tab/contacts_tab_view_style.dart @@ -2,4 +2,5 @@ import 'package:flutter/material.dart'; class ContactsTabViewStyle { static const Size preferredSizeAppBar = Size.fromHeight(96); + static const double padding = 8.0; } From 018248b2417583bb8abe9924e40b84318e727782 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Tue, 3 Oct 2023 20:08:59 +0700 Subject: [PATCH 33/60] fixup! TW-732 Load more in add member in group need support mouse scroll --- .../same_type_events_list_builder_view.dart | 21 +++++-------- .../center_loading_indicator.dart | 13 ++++++++ .../twake_smart_refresher.dart | 30 +++++++------------ 3 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 lib/widgets/twake_components/twake_loading/center_loading_indicator.dart diff --git a/lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder_view.dart b/lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder_view.dart index 4f357fe15..9e92cbdef 100644 --- a/lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder_view.dart +++ b/lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder_view.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart'; +import 'package:fluffychat/widgets/twake_components/twake_loading/center_loading_indicator.dart'; import 'package:flutter/material.dart'; class SameTypeEventsListBuilderView extends StatelessWidget { @@ -19,7 +20,9 @@ class SameTypeEventsListBuilderView extends StatelessWidget { ValueListenableBuilder( valueListenable: controller.refreshing, builder: (context, refreshing, child) => SliverToBoxAdapter( - child: refreshing ? const _LoadingIndicator() : const SizedBox(), + child: refreshing + ? const CenterLoadingIndicator() + : const SizedBox(), ), ), ValueListenableBuilder( @@ -30,7 +33,9 @@ class SameTypeEventsListBuilderView extends StatelessWidget { ValueListenableBuilder( valueListenable: controller.loadingMore, builder: (context, loadingMore, child) => SliverToBoxAdapter( - child: loadingMore ? const _LoadingIndicator() : const SizedBox(), + child: loadingMore + ? const CenterLoadingIndicator() + : const SizedBox(), ), ) ], @@ -38,15 +43,3 @@ class SameTypeEventsListBuilderView extends StatelessWidget { ); } } - -class _LoadingIndicator extends StatelessWidget { - const _LoadingIndicator(); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ); - } -} diff --git a/lib/widgets/twake_components/twake_loading/center_loading_indicator.dart b/lib/widgets/twake_components/twake_loading/center_loading_indicator.dart new file mode 100644 index 000000000..2f2e2a9b4 --- /dev/null +++ b/lib/widgets/twake_components/twake_loading/center_loading_indicator.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class CenterLoadingIndicator extends StatelessWidget { + const CenterLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ); + } +} diff --git a/lib/widgets/twake_components/twake_smart_refresher.dart b/lib/widgets/twake_components/twake_smart_refresher.dart index ba43eafb4..25700d7dc 100644 --- a/lib/widgets/twake_components/twake_smart_refresher.dart +++ b/lib/widgets/twake_components/twake_smart_refresher.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/twake_components/twake_loading/center_loading_indicator.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/utils/scroll_controller_extension.dart'; @@ -22,11 +23,11 @@ class TwakeSmartRefresher extends StatefulWidget { class TwakeRefreshController { final refreshNotifier = ValueNotifier(false); - final loadNotifier = ValueNotifier(false); + final loadingNotifier = ValueNotifier(false); final scrollController = ScrollController(); bool get isRefeshing => refreshNotifier.value; - bool get isLoading => loadNotifier.value; + bool get isLoading => loadingNotifier.value; void onRefresh() { refreshNotifier.value = true; @@ -37,11 +38,11 @@ class TwakeRefreshController { } void onLoading() { - loadNotifier.value = true; + loadingNotifier.value = true; } void loadComplete() { - loadNotifier.value = false; + loadingNotifier.value = false; } } @@ -102,14 +103,17 @@ class _TwakeSmartRefresherView extends StatelessWidget { ValueListenableBuilder( valueListenable: refreshController.refreshNotifier, builder: (context, refreshing, child) => SliverToBoxAdapter( - child: refreshing ? const _LoadingIndicator() : const SizedBox(), + child: refreshing + ? const CenterLoadingIndicator() + : const SizedBox(), ), ), ...slivers, ValueListenableBuilder( - valueListenable: refreshController.loadNotifier, + valueListenable: refreshController.loadingNotifier, builder: (context, loading, child) => SliverToBoxAdapter( - child: loading ? const _LoadingIndicator() : const SizedBox(), + child: + loading ? const CenterLoadingIndicator() : const SizedBox(), ), ) ], @@ -117,15 +121,3 @@ class _TwakeSmartRefresherView extends StatelessWidget { ); } } - -class _LoadingIndicator extends StatelessWidget { - const _LoadingIndicator(); - - @override - Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ); - } -} From 95a508d5ed56cffddb3ac13f637061600fcc608d Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Mon, 2 Oct 2023 16:31:31 +0200 Subject: [PATCH 34/60] feat: display only first message display name when multi --- lib/pages/chat/events/message.dart | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 4da3835b3..9f29d10ec 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -206,7 +206,7 @@ class Message extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ - ownMessage || event.room.isDirectChat + hideDisplayName(ownMessage) ? const SizedBox(height: 0) : FutureBuilder( future: event.fetchSenderUser(), @@ -576,6 +576,9 @@ class Message extends StatelessWidget { ); } + bool hideDisplayName(bool ownMessage) => + ownMessage || event.room.isDirectChat || !isSameSender(nextEvent, event); + Widget _menuActionsRowBuilder(BuildContext context, bool ownMessage) { return ValueListenableBuilder( valueListenable: isHover, @@ -680,26 +683,26 @@ class Message extends StatelessWidget { ); } - // Check if the sender of the current event is the same as the previous event. - bool isSameSender(Event? previousEvent, Event currentEvent) { - // If the previous event is null, it is assumed that the message is the newest. - if (previousEvent == null) { + // Check if the sender of the current event is the same as the compared event. + bool isSameSender(Event? comparedEvent, Event currentEvent) { + // If the compared event is null, it is assumed that the message is the newest. + if (comparedEvent == null) { return true; } - final isPreviousEventMessage = { + final isPreviousOrNextEventMessage = { EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted, EventTypes.Redaction, - }.contains(previousEvent.type); + }.contains(comparedEvent.type); // Ignoring events that are not messages, stickers, encrypted or redaction. - if (!isPreviousEventMessage) { + if (!isPreviousOrNextEventMessage) { return true; } - return previousEvent.senderId != currentEvent.senderId; + return currentEvent.senderId != comparedEvent.senderId; } } From 3db8bbeedec4929ba020984c15ab2caa9c0baf88 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Tue, 3 Oct 2023 21:42:56 +0700 Subject: [PATCH 35/60] TW-724 Improve composer with enter shortcut and need support Vietnamese --- lib/pages/chat/chat.dart | 6 +- lib/pages/chat/chat_input_row.dart | 1 + lib/pages/chat/input_bar.dart | 126 +++++++----------- lib/pages/chat_draft/draft_chat.dart | 1 + lib/pages/chat_draft/draft_chat_view.dart | 2 + .../extension/raw_key_event_extension.dart | 24 ++++ 6 files changed, 82 insertions(+), 78 deletions(-) create mode 100644 lib/utils/extension/raw_key_event_extension.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 673d23077..9af39f172 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -136,6 +136,7 @@ class ChatController extends State final ValueNotifier showScrollDownButtonNotifier = ValueNotifier(false); final ValueNotifier showEmojiPickerNotifier = ValueNotifier(false); FocusNode inputFocus = FocusNode(); + FocusNode keyboardFocus = FocusNode(); Timer? typingCoolDown; Timer? typingTimeout; @@ -1178,8 +1179,9 @@ class ChatController extends State return index + 1; } - void onInputBarSubmitted(_) { - send(); + void onInputBarSubmitted(_) async { + await send(); + await Future.delayed(const Duration(milliseconds: 100)); FocusScope.of(context).requestFocus(inputFocus); } diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 702d1579e..cdb2731d3 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -159,6 +159,7 @@ class ChatInputRow extends StatelessWidget { textInputAction: AppConfig.sendOnEnter ? TextInputAction.send : null, onSubmitted: controller.onInputBarSubmitted, focusNode: controller.inputFocus, + keyboardFocusNode: controller.keyboardFocus, controller: controller.sendController, decoration: InputDecoration( hintText: L10n.of(context)!.chatMessage, diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 803df0ffe..bce81fe17 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -1,7 +1,7 @@ +import 'package:fluffychat/utils/extension/raw_key_event_extension.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:emojis/emoji.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -23,6 +23,7 @@ class InputBar extends StatelessWidget { final TextInputAction? textInputAction; final ValueChanged? onSubmitted; final FocusNode? focusNode; + final FocusNode keyboardFocusNode; final TextEditingController? controller; final InputDecoration? decoration; final ValueChanged? onChanged; @@ -36,6 +37,7 @@ class InputBar extends StatelessWidget { this.keyboardType, this.onSubmitted, this.focusNode, + required this.keyboardFocusNode, this.controller, this.decoration, this.onChanged, @@ -302,82 +304,54 @@ class InputBar extends StatelessWidget { final useShortCuts = (PlatformInfos.isWeb || PlatformInfos.isDesktop || AppConfig.sendOnEnter); - return Shortcuts( - shortcuts: !useShortCuts - ? {} - : { - LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.enter): - NewLineIntent(), - LogicalKeySet(LogicalKeyboardKey.enter): SubmitLineIntent(), - }, - child: Actions( - actions: !useShortCuts - ? {} - : { - NewLineIntent: CallbackAction( - onInvoke: (i) { - final val = controller!.value; - final selection = val.selection.start; - final messageWithoutNewLine = - '${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}'; - controller!.value = TextEditingValue( - text: messageWithoutNewLine, - selection: TextSelection.fromPosition( - TextPosition(offset: selection + 1), - ), - ); - return null; - }, - ), - SubmitLineIntent: CallbackAction( - onInvoke: (i) { - onSubmitted!(controller!.text); - return null; - }, - ), - }, - child: TypeAheadField>( - direction: AxisDirection.up, - hideOnEmpty: true, - hideOnLoading: true, - keepSuggestionsOnSuggestionSelected: true, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - textFieldConfiguration: TextFieldConfiguration( - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - style: InputBarStyle.getTypeAheadTextStyle(context), - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); - }, - controller: controller, - decoration: decoration!, - focusNode: focusNode, - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); - }, - textCapitalization: TextCapitalization.sentences, - ), - suggestionsCallback: getSuggestions, - itemBuilder: (context, suggestion) => SuggestionTile( - suggestion: suggestion, - client: Matrix.of(context).client, - ), - onSuggestionSelected: (Map suggestion) => - insertSuggestion(context, suggestion), - errorBuilder: (BuildContext context, Object? error) => Container(), - loadingBuilder: (BuildContext context) => Container(), - // fix loading briefly flickering a dark box - noItemsFoundBuilder: (BuildContext context) => - Container(), // fix loading briefly showing no suggestions + return RawKeyboardListener( + focusNode: keyboardFocusNode, + onKey: (event) { + if (useShortCuts && event.isEnter) { + onSubmitted?.call(controller?.text ?? ''); + } + }, + child: TypeAheadField>( + direction: AxisDirection.up, + hideOnEmpty: true, + hideOnLoading: true, + keepSuggestionsOnSuggestionSelected: true, + debounceDuration: const Duration(milliseconds: 50), + // show suggestions after 50ms idle time (default is 300) + textFieldConfiguration: TextFieldConfiguration( + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + style: InputBarStyle.getTypeAheadTextStyle(context), + onSubmitted: (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + }, + controller: controller, + decoration: decoration!, + focusNode: focusNode, + onChanged: (text) { + // fix for the library for now + // it sets the types for the callback incorrectly + onChanged!(text); + }, + textCapitalization: TextCapitalization.sentences, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (context, suggestion) => SuggestionTile( + suggestion: suggestion, + client: Matrix.of(context).client, ), + onSuggestionSelected: (Map suggestion) => + insertSuggestion(context, suggestion), + errorBuilder: (BuildContext context, Object? error) => Container(), + loadingBuilder: (BuildContext context) => Container(), + // fix loading briefly flickering a dark box + noItemsFoundBuilder: (BuildContext context) => + Container(), // fix loading briefly showing no suggestions ), ); } diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index 72e426267..61a148596 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -52,6 +52,7 @@ class DraftChatController extends State final AutoScrollController forwardListController = AutoScrollController(); FocusNode inputFocus = FocusNode(); + FocusNode keyboardFocus = FocusNode(); bool showScrollDownButton = false; diff --git a/lib/pages/chat_draft/draft_chat_view.dart b/lib/pages/chat_draft/draft_chat_view.dart index 2e4cf0842..7d4b2e7d7 100644 --- a/lib/pages/chat_draft/draft_chat_view.dart +++ b/lib/pages/chat_draft/draft_chat_view.dart @@ -128,6 +128,8 @@ class DraftChatView extends StatelessWidget { onSubmitted: controller.onInputBarSubmitted, focusNode: controller.inputFocus, + keyboardFocusNode: + controller.keyboardFocus, controller: controller.sendController, decoration: DraftChatViewStyle .bottomBarInputDecoration(context), diff --git a/lib/utils/extension/raw_key_event_extension.dart b/lib/utils/extension/raw_key_event_extension.dart new file mode 100644 index 000000000..37916a9ff --- /dev/null +++ b/lib/utils/extension/raw_key_event_extension.dart @@ -0,0 +1,24 @@ +import 'package:flutter/services.dart'; + +// Refer: https://github.com/flutter/flutter/issues/35435#issuecomment-540582796 +extension RawKeyEventExtension on RawKeyEvent { + bool get isEnter { + if (this is! RawKeyUpEvent) { + return false; + } + if (logicalKey == LogicalKeyboardKey.enter) { + return true; + } + if (data is RawKeyEventDataWeb) { + if ((data as RawKeyEventDataWeb).keyLabel == 'Enter') { + return true; + } + } + if (data is RawKeyEventDataAndroid) { + if ((data as RawKeyEventDataAndroid).keyCode == 13) { + return true; + } + } + return false; + } +} From fe4d604cc9d48a6958b95ddceffec91c845fbe48 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Wed, 4 Oct 2023 16:38:44 +0700 Subject: [PATCH 36/60] TW-726 Notification android --- lib/utils/background_push.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 4f41479ee..1ab50d99b 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -281,6 +281,11 @@ class BackgroundPush { if (PlatformInfos.isIOS) { // Request iOS permission before getting the token await fcmSharedIsolate?.requestPermission(); + } else if (Platform.isAndroid) { + await _flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestPermission(); } _pushToken = await (Platform.isIOS ? apnChannel.invokeMethod("getToken") From 3adabfb44e46d873b235c779f6aef9b4c58b3096 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 4 Oct 2023 16:35:46 +0700 Subject: [PATCH 37/60] TW-740: remove change avatar event when user first set his avatar --- lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index a24167200..2e20e5d22 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -87,7 +87,6 @@ extension IsStateExtension on Event { bool isSomeoneChangeAvatar() { return stateKey != null && prevContent?["membership"] == 'join' && - prevContent?['avatar_url'] != null && prevContent?['avatar_url'] != content['avatar_url']; } From 1046e2a455c14e79f1dbe4409d54df4ec97a3c1a Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 5 Oct 2023 11:00:27 +0700 Subject: [PATCH 38/60] fix: Search contacts cached --- .../new_private_chat/widget/expansion_contact_list_tile.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart b/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart index a4a97b949..6b0e6202e 100644 --- a/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart +++ b/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart @@ -29,6 +29,7 @@ class ExpansionContactListTile extends StatelessWidget { return Padding( padding: const EdgeInsets.only(left: 8.0, top: 8.0, bottom: 12.0), child: FutureBuilder( + key: contact.matrixId != null ? Key(contact.matrixId!) : null, future: contact.status == ContactStatus.active ? getProfile(context) : null, builder: (context, snapshot) { From 5daf836e06a2cc5c126843dd734a83493339a34d Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 16:19:36 +0700 Subject: [PATCH 39/60] TW-729: swipe left to right to back from chat screen --- lib/config/go_routes/go_router.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 22955d5ef..622019338 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -352,7 +352,7 @@ abstract class AppRoutes { switch (extra.type) { case ChatRouterInputArgumentType.draft: if (extra.data is String?) { - return NoTransitionPage( + return CupertinoPage( child: Chat( roomId: state.pathParameters['roomid']!, key: Key(state.pathParameters['roomid']!), @@ -360,16 +360,15 @@ abstract class AppRoutes { ), ); } - return NoTransitionPage( + return CupertinoPage( child: Chat( roomId: state.pathParameters['roomid']!, key: Key(state.pathParameters['roomid']!), ), ); case ChatRouterInputArgumentType.share: - return defaultPageBuilder( - context, - Chat( + return CupertinoPage( + child: Chat( roomId: state.pathParameters['roomid']!, key: Key(state.pathParameters['roomid']!), shareFile: extra.data as MatrixFile?, @@ -377,9 +376,8 @@ abstract class AppRoutes { ); } } - return defaultPageBuilder( - context, - Chat( + return CupertinoPage( + child: Chat( roomId: state.pathParameters['roomid']!, key: Key(state.pathParameters['roomid']!), ), From 26891cea8eb229b97946818ea596f8cc3c09e78f Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 17:16:45 +0700 Subject: [PATCH 40/60] TW-730: add custom components to build animation when open to view image and video --- lib/utils/custom_disissible.dart | 165 ++++++++++++++++++++++ lib/utils/interactive_viewer_gallery.dart | 88 ++++++++++++ lib/widgets/hero_dialog_route.dart | 59 ++++++++ 3 files changed, 312 insertions(+) create mode 100644 lib/utils/custom_disissible.dart create mode 100644 lib/utils/interactive_viewer_gallery.dart create mode 100644 lib/widgets/hero_dialog_route.dart diff --git a/lib/utils/custom_disissible.dart b/lib/utils/custom_disissible.dart new file mode 100644 index 000000000..90bc2aad6 --- /dev/null +++ b/lib/utils/custom_disissible.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; + +class CustomDismissible extends StatefulWidget { + const CustomDismissible({ + super.key, + required this.child, + this.onDismissed, + this.dismissThreshold = 0.2, + this.enabled = true, + this.handleDragStart, + this.handleDragEnd, + }); + + final Widget child; + final double dismissThreshold; + final VoidCallback? onDismissed; + final bool enabled; + final Function(DragStartDetails dragStartDetails)? handleDragStart; + final Function(DragEndDetails dragEndDetails)? handleDragEnd; + + @override + State createState() => _CustomDismissibleState(); +} + +class _CustomDismissibleState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animateController; + late Animation _moveAnimation; + late Animation _scaleAnimation; + late Animation _opacityAnimation; + + double _dragExtent = 0; + bool _dragUnderway = false; + + bool get _isActive => _dragUnderway || _animateController.isAnimating; + + @override + void initState() { + super.initState(); + + _animateController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _updateMoveAnimation(); + } + + @override + void dispose() { + _animateController.dispose(); + + super.dispose(); + } + + void _updateMoveAnimation() { + final double end = _dragExtent.sign; + + _moveAnimation = _animateController.drive( + Tween( + begin: Offset.zero, + end: Offset(0, end), + ), + ); + + _scaleAnimation = _animateController.drive( + Tween( + begin: 1, + end: 0.5, + ), + ); + + _opacityAnimation = DecorationTween( + begin: const BoxDecoration( + color: Color(0xFF000000), + ), + end: const BoxDecoration( + color: Color(0x00000000), + ), + ).animate(_animateController); + } + + void _handleDragStart(DragStartDetails details) { + widget.handleDragStart?.call(details); + _dragUnderway = true; + + if (_animateController.isAnimating) { + _dragExtent = + _animateController.value * context.size!.height * _dragExtent.sign; + _animateController.stop(); + } else { + _dragExtent = 0.0; + _animateController.value = 0.0; + } + setState(_updateMoveAnimation); + } + + void _handleDragUpdate(DragUpdateDetails details) { + if (!_isActive || _animateController.isAnimating) { + return; + } + + final double delta = details.primaryDelta!; + final double oldDragExtent = _dragExtent; + + if (_dragExtent + delta < 0) { + _dragExtent += delta; + } else if (_dragExtent + delta > 0) { + _dragExtent += delta; + } + + if (oldDragExtent.sign != _dragExtent.sign) { + setState(_updateMoveAnimation); + } + + if (!_animateController.isAnimating) { + _animateController.value = _dragExtent.abs() / context.size!.height; + } + } + + void _handleDragEnd(DragEndDetails details) { + widget.handleDragEnd?.call(details); + if (!_isActive || _animateController.isAnimating) { + return; + } + + _dragUnderway = false; + + if (_animateController.isCompleted) { + return; + } + + if (!_animateController.isDismissed) { + // if the dragged value exceeded the dismissThreshold, call onDismissed + // else animate back to initial position. + if (_animateController.value > widget.dismissThreshold) { + widget.onDismissed?.call(); + } else { + _animateController.reverse(); + } + } + } + + @override + Widget build(BuildContext context) { + final Widget content = DecoratedBoxTransition( + decoration: _opacityAnimation, + child: SlideTransition( + position: _moveAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: widget.child, + ), + ), + ); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onVerticalDragStart: widget.enabled ? _handleDragStart : null, + onVerticalDragUpdate: widget.enabled ? _handleDragUpdate : null, + onVerticalDragEnd: widget.enabled ? _handleDragEnd : null, + child: content, + ); + } +} diff --git a/lib/utils/interactive_viewer_gallery.dart b/lib/utils/interactive_viewer_gallery.dart new file mode 100644 index 000000000..27239828e --- /dev/null +++ b/lib/utils/interactive_viewer_gallery.dart @@ -0,0 +1,88 @@ +import 'package:fluffychat/utils/custom_disissible.dart'; +import 'package:flutter/material.dart'; + +/// A callback for the [InteractiveViewerBoundary] that is called when the scale +/// changed. +typedef ScaleChanged = void Function(double scale); + +/// Builds an [InteractiveViewer] and provides callbacks that are called when a +/// horizontal boundary has been hit. +/// +/// The callbacks are called when an interaction ends by listening to the +/// [InteractiveViewer.onInteractionEnd] callback. +class InteractiveviewerGallery extends StatefulWidget { + const InteractiveviewerGallery({ + super.key, + required this.itemBuilder, + this.maxScale = 2.5, + this.minScale = 1.0, + this.handleDragStart, + this.handleDragEnd, + }); + + /// The item content + final Widget itemBuilder; + + final double maxScale; + + final double minScale; + + final Function(DragStartDetails dragStartDetails)? handleDragStart; + + final Function(DragEndDetails dragEndDetails)? handleDragEnd; + + @override + State createState() => + _InteractiveviewerGalleryState(); +} + +class _InteractiveviewerGalleryState extends State + with SingleTickerProviderStateMixin { + TransformationController? _transformationController; + + /// The controller to animate the transformation value of the + /// [InteractiveViewer] when it should reset. + late AnimationController _animationController; + Animation? _animation; + + /// `true` when an source is zoomed in to disable the [CustomDismissible]. + bool _enableDismiss = true; + + @override + void initState() { + super.initState(); + + _transformationController = TransformationController(); + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ) + ..addListener(() { + _transformationController!.value = + _animation?.value ?? Matrix4.identity(); + }) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed && !_enableDismiss) { + setState(() { + _enableDismiss = true; + }); + } + }); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomDismissible( + onDismissed: () => Navigator.of(context).pop(), + enabled: _enableDismiss, + child: widget.itemBuilder, + ); + } +} diff --git a/lib/widgets/hero_dialog_route.dart b/lib/widgets/hero_dialog_route.dart new file mode 100644 index 000000000..ac9af7eba --- /dev/null +++ b/lib/widgets/hero_dialog_route.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class HeroDialogRoute extends PageRoute { + HeroDialogRoute({ + required this.builder, + this.onBackgroundTap, + }) : super(); + + final WidgetBuilder builder; + + /// Called when the background is tapped. + final VoidCallback? onBackgroundTap; + + @override + bool get opaque => false; + + @override + bool get barrierDismissible => true; + + @override + String? get barrierLabel => null; + + @override + Duration get transitionDuration => const Duration(milliseconds: 300); + + @override + bool get maintainState => true; + + @override + Color? get barrierColor => null; + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition( + opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), + child: child, + ); + } + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + final Widget child = builder(context); + final Widget result = Semantics( + scopesRoute: true, + explicitChildNodes: true, + child: child, + ); + return result; + } +} From 4c2554c9c45863fa394eb391f41203d58cf55650 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 17:20:06 +0700 Subject: [PATCH 41/60] TW-730: apply new hero animation when open sending im --- .../events/sending_image_info_widget.dart | 119 ++++++++++-------- lib/widgets/mxc_image.dart | 19 +-- 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/lib/pages/chat/events/sending_image_info_widget.dart b/lib/pages/chat/events/sending_image_info_widget.dart index ed8d5283d..eed1bb361 100644 --- a/lib/pages/chat/events/sending_image_info_widget.dart +++ b/lib/pages/chat/events/sending_image_info_widget.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/presentation/model/file/display_image_info.dart'; +import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; +import 'package:fluffychat/widgets/hero_dialog_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; @@ -28,15 +30,17 @@ class SendingImageInfoWidget extends StatelessWidget { void _onTap(BuildContext context) async { if (onTapPreview != null) { - await showGeneralDialog( - context: context, - useRootNavigator: false, - barrierDismissible: true, - barrierLabel: - MaterialLocalizations.of(context).modalBarrierDismissLabel, - transitionDuration: const Duration(milliseconds: 200), - pageBuilder: (_, animationOne, animationTwo) => - ImageViewer(event, filePath: matrixFile.filePath), + Navigator.of(context).push( + HeroDialogRoute( + builder: (context) { + return InteractiveviewerGallery( + itemBuilder: ImageViewer( + event, + filePath: matrixFile.filePath, + ), + ); + }, + ), ); } } @@ -48,55 +52,62 @@ class SendingImageInfoWidget extends StatelessWidget { sendingFileProgressNotifier.value = 1; } - return ValueListenableBuilder( - key: ValueKey(event.eventId), - valueListenable: sendingFileProgressNotifier, - builder: (context, value, child) { - return Stack( - alignment: Alignment.center, - children: [ - child!, - if (sendingFileProgressNotifier.value != 1) ...[ - CircularProgressIndicator( - strokeWidth: 2, - color: LinagoraRefColors.material().primary[100], - ), - Icon( - Icons.close, - color: LinagoraRefColors.material().primary[100], - ), - ] - ], - ); - }, - child: InkWell( - onTap: () => _onTap(context), - child: ClipRRect( - borderRadius: BorderRadius.circular(12.0), - child: Stack( + return Hero( + tag: event.eventId, + child: ValueListenableBuilder( + key: ValueKey(event.eventId), + valueListenable: sendingFileProgressNotifier, + builder: (context, value, child) { + return Stack( alignment: Alignment.center, children: [ - if (displayImageInfo.hasBlur) - SizedBox( - width: MessageContentStyle.imageBubbleWidth( - displayImageInfo.size.width, - ), - height: MessageContentStyle.imageBubbleHeight( - displayImageInfo.size.height, - ), - child: - const BlurHash(hash: MessageContentStyle.defaultBlurHash), + child!, + if (sendingFileProgressNotifier.value != 1) ...[ + CircularProgressIndicator( + strokeWidth: 2, + color: LinagoraRefColors.material().primary[100], + ), + Icon( + Icons.close, + color: LinagoraRefColors.material().primary[100], ), - Image.file( - File(matrixFile.filePath!), - width: displayImageInfo.size.width, - height: displayImageInfo.size.height, - cacheHeight: displayImageInfo.size.height.toInt(), - cacheWidth: displayImageInfo.size.width.toInt(), - fit: BoxFit.cover, - filterQuality: FilterQuality.medium, - ) + ] ], + ); + }, + child: Material( + borderRadius: BorderRadius.circular(12.0), + child: InkWell( + onTap: () => _onTap(context), + child: ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Stack( + alignment: Alignment.center, + children: [ + if (displayImageInfo.hasBlur) + SizedBox( + width: MessageContentStyle.imageBubbleWidth( + displayImageInfo.size.width, + ), + height: MessageContentStyle.imageBubbleHeight( + displayImageInfo.size.height, + ), + child: const BlurHash( + hash: MessageContentStyle.defaultBlurHash, + ), + ), + Image.file( + File(matrixFile.filePath!), + width: displayImageInfo.size.width, + height: displayImageInfo.size.height, + cacheHeight: displayImageInfo.size.height.toInt(), + cacheWidth: displayImageInfo.size.width.toInt(), + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ) + ], + ), + ), ), ), ), diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index ef669b4de..e7bf44a3f 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -1,5 +1,7 @@ import 'dart:typed_data'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; +import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; +import 'package:fluffychat/widgets/hero_dialog_route.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; @@ -194,15 +196,14 @@ class _MxcImageState extends State void _onTap(BuildContext context) async { if (widget.onTapPreview != null) { widget.onTapPreview!(); - await showGeneralDialog( - context: context, - useRootNavigator: false, - barrierDismissible: true, - barrierLabel: - MaterialLocalizations.of(context).modalBarrierDismissLabel, - transitionDuration: const Duration(milliseconds: 200), - pageBuilder: (_, animationOne, animationTwo) => - ImageViewer(widget.event!), + Navigator.of(context).push( + HeroDialogRoute( + builder: (context) { + return InteractiveviewerGallery( + itemBuilder: ImageViewer(widget.event!), + ); + }, + ), ); } else if (widget.onTapSelectMode != null) { widget.onTapSelectMode!(); From 5691f4d9828cd9384b30a869c64ad4f2854851c2 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 17:22:08 +0700 Subject: [PATCH 42/60] TW-730: update navigation to have hero animation --- .../mixins/play_video_action_mixin.dart | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/presentation/mixins/play_video_action_mixin.dart b/lib/presentation/mixins/play_video_action_mixin.dart index 4ca3a97d1..e3f0ebc42 100644 --- a/lib/presentation/mixins/play_video_action_mixin.dart +++ b/lib/presentation/mixins/play_video_action_mixin.dart @@ -1,20 +1,41 @@ +import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/hero_dialog_route.dart'; import 'package:fluffychat/widgets/video_viewer_desktop_theme.dart'; import 'package:fluffychat/widgets/video_viewer_mobile_theme.dart'; import 'package:flutter/material.dart'; mixin PlayVideoActionMixin { - void playVideoAction(BuildContext context, String uriOrFilePath) async { - await showDialog( - context: context, - useRootNavigator: PlatformInfos.isWeb, - useSafeArea: false, - builder: (context) { - if (PlatformInfos.isWeb || PlatformInfos.isDesktop) { - return VideoViewerDesktopTheme(path: uriOrFilePath); - } - return VideoViewerMobileTheme(path: uriOrFilePath); - }, - ); + void playVideoAction( + BuildContext context, + String uriOrFilePath, { + String? eventId, + }) async { + if (!PlatformInfos.isWeb) { + Navigator.of(context).push( + HeroDialogRoute( + builder: (context) { + return InteractiveviewerGallery( + itemBuilder: VideoViewerMobileTheme( + path: uriOrFilePath, + eventId: eventId, + ), + ); + }, + ), + ); + } else { + await showDialog( + context: context, + useRootNavigator: PlatformInfos.isWeb, + useSafeArea: false, + builder: (context) { + if (PlatformInfos.isWeb || PlatformInfos.isDesktop) { + return VideoViewerDesktopTheme(path: uriOrFilePath); + } + return VideoViewerMobileTheme(path: uriOrFilePath, eventId: eventId); + }, + ); + } } } From 3b59345b45b13c6cf0f187622adcd4aa14c40955 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 17:22:44 +0700 Subject: [PATCH 43/60] TW-730: update image viewer to have hero animation on pop --- lib/pages/image_viewer/image_viewer_view.dart | 57 ++++++++++--------- lib/widgets/mxc_image.dart | 8 ++- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index a0ae6c361..7ca31c389 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -24,42 +23,46 @@ class ImageViewerView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.black, - extendBodyBehindAppBar: true, - body: Stack( - children: [ - GestureDetector( - onTap: controller.toggleAppbarPreview, - onDoubleTapDown: (details) => controller.onDoubleTapDown(details), - onDoubleTap: () => controller.onDoubleTap(), - child: InteractiveViewer( - transformationController: controller.transformationController, - minScale: 1.0, - maxScale: 10.0, - onInteractionEnd: controller.onInteractionEnds, - child: Center( - child: Hero( - tag: controller.widget.event.eventId, + backgroundColor: Colors.transparent, + body: GestureDetector( + onTap: () => controller.toggleAppbarPreview(), + onDoubleTapDown: (details) => controller.onDoubleTapDown(details), + onDoubleTap: () => controller.onDoubleTap(), + child: Stack( + children: [ + Hero( + tag: controller.widget.event.eventId, + child: InteractiveViewer( + onInteractionEnd: controller.onInteractionEnds, + transformationController: controller.transformationController, + minScale: 1.0, + maxScale: 10.0, + child: Center( child: filePath != null ? Image.file( File(filePath!), fit: BoxFit.contain, filterQuality: FilterQuality.none, ) - : MxcImage( - event: controller.widget.event, - fit: BoxFit.contain, - isThumbnail: false, - animated: false, - imageData: imageData, - isPreview: true, + : FutureBuilder( + future: controller.widget.event + .downloadAndDecryptAttachment( + getThumbnail: true, + ), + builder: (context, snapshot) { + if (snapshot.data == null || + snapshot.data!.bytes == null) { + return const CircularProgressIndicator(); + } + return Image.memory(snapshot.data!.bytes!); + }, ), ), ), ), - ), - _buildAppBarPreview(), - ], + _buildAppBarPreview(), + ], + ), ), ); } diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index e7bf44a3f..ef452a73e 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -236,9 +236,11 @@ class _MxcImageState extends State : _buildImageWidget(); if (widget.isPreview) { - return InkWell( - onTap: () => _onTap(context), - child: imageWidget, + return Material( + child: InkWell( + onTap: () => _onTap(context), + child: imageWidget, + ), ); } else { return imageWidget; From 0ff41162fa00eac5c6edca0fa1aceb6bfb0d866f Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 17:23:16 +0700 Subject: [PATCH 44/60] TW-730: update video player to have hero animation on pop --- lib/widgets/video_viewer_mobile_theme.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/widgets/video_viewer_mobile_theme.dart b/lib/widgets/video_viewer_mobile_theme.dart index f7fd82d9f..5cec765e5 100644 --- a/lib/widgets/video_viewer_mobile_theme.dart +++ b/lib/widgets/video_viewer_mobile_theme.dart @@ -10,10 +10,13 @@ class VideoViewerMobileTheme extends StatelessWidget { const VideoViewerMobileTheme({ super.key, required this.path, + this.eventId, }); final String path; + final String? eventId; + @override Widget build(BuildContext context) { return MaterialVideoControlsTheme( @@ -39,9 +42,14 @@ class VideoViewerMobileTheme extends StatelessWidget { seekBarThumbColor: Theme.of(context).colorScheme.primary, ), fullscreen: const MaterialVideoControlsThemeData(), - child: VideoPlayer( - path: path, - ), + child: eventId != null + ? Hero( + tag: eventId!, + child: VideoPlayer( + path: path, + ), + ) + : VideoPlayer(path: path), ); } } From f6d39ead8263b146a51db1c662369a042879fe4d Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 17:23:55 +0700 Subject: [PATCH 45/60] TW-730: update sending video player to have hero animation --- lib/pages/chat/events/sending_video_widget.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/events/sending_video_widget.dart b/lib/pages/chat/events/sending_video_widget.dart index cd500d06d..daedf2f04 100644 --- a/lib/pages/chat/events/sending_video_widget.dart +++ b/lib/pages/chat/events/sending_video_widget.dart @@ -77,10 +77,13 @@ class SendingVideoWidget extends StatelessWidget with PlayVideoActionMixin { ), ); }), - child: VideoWidget( - imageHeight: displayImageInfo.size.height, - imageWidth: displayImageInfo.size.width, - matrixFile: matrixFile, + child: Hero( + tag: event.eventId, + child: VideoWidget( + imageHeight: displayImageInfo.size.height, + imageWidth: displayImageInfo.size.width, + matrixFile: matrixFile, + ), ), ); } From ec91bf54e9345634b53abc23e9f86a8478bd803e Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 3 Oct 2023 17:24:35 +0700 Subject: [PATCH 46/60] TW-730: update play video action method to have eventId for hero tag --- lib/pages/chat/events/event_video_player.dart | 6 +++++- lib/pages/chat/events/message_content.dart | 6 +++++- .../chat/events/sending_image_info_widget.dart | 16 +++++++++------- lib/pages/chat/events/sending_video_widget.dart | 6 +++++- lib/pages/chat_details/chat_details.dart | 6 +++++- lib/pages/image_viewer/image_viewer_style.dart | 4 ++++ lib/pages/image_viewer/image_viewer_view.dart | 11 ++++++----- .../mixins/play_video_action_mixin.dart | 6 +++--- ...m_disissible.dart => custom_dismissable.dart} | 12 +++++++----- lib/utils/interactive_viewer_gallery.dart | 12 ++++++------ ...ro_dialog_route.dart => hero_page_route.dart} | 4 ++-- lib/widgets/mxc_image.dart | 6 +++--- 12 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 lib/pages/image_viewer/image_viewer_style.dart rename lib/utils/{custom_disissible.dart => custom_dismissable.dart} (94%) rename lib/widgets/{hero_dialog_route.dart => hero_page_route.dart} (94%) diff --git a/lib/pages/chat/events/event_video_player.dart b/lib/pages/chat/events/event_video_player.dart index 994ecf786..1670d06fe 100644 --- a/lib/pages/chat/events/event_video_player.dart +++ b/lib/pages/chat/events/event_video_player.dart @@ -151,7 +151,11 @@ class EventVideoPlayerState extends State icon: Icons.play_arrow, onTap: () { if (path != null) { - playVideoAction(context, path!); + playVideoAction( + context, + path!, + eventId: widget.event.eventId, + ); } }, ); diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 20eef6833..655de425c 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -166,7 +166,11 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { handleDownloadVideoEvent: (event) { return controller.handleDownloadVideoEvent( event: event, - playVideoAction: (path) => playVideoAction(context, path), + playVideoAction: (path) => playVideoAction( + context, + path, + eventId: event.eventId, + ), ); }, ); diff --git a/lib/pages/chat/events/sending_image_info_widget.dart b/lib/pages/chat/events/sending_image_info_widget.dart index eed1bb361..80034854e 100644 --- a/lib/pages/chat/events/sending_image_info_widget.dart +++ b/lib/pages/chat/events/sending_image_info_widget.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/presentation/model/file/display_image_info.dart'; import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; -import 'package:fluffychat/widgets/hero_dialog_route.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/widgets/hero_page_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; @@ -31,9 +33,9 @@ class SendingImageInfoWidget extends StatelessWidget { void _onTap(BuildContext context) async { if (onTapPreview != null) { Navigator.of(context).push( - HeroDialogRoute( + HeroPageRoute( builder: (context) { - return InteractiveviewerGallery( + return InteractiveViewerGallery( itemBuilder: ImageViewer( event, filePath: matrixFile.filePath, @@ -76,11 +78,11 @@ class SendingImageInfoWidget extends StatelessWidget { ); }, child: Material( - borderRadius: BorderRadius.circular(12.0), + borderRadius: MessageContentStyle.borderRadiusBubble, child: InkWell( onTap: () => _onTap(context), child: ClipRRect( - borderRadius: BorderRadius.circular(12.0), + borderRadius: MessageContentStyle.borderRadiusBubble, child: Stack( alignment: Alignment.center, children: [ @@ -92,8 +94,8 @@ class SendingImageInfoWidget extends StatelessWidget { height: MessageContentStyle.imageBubbleHeight( displayImageInfo.size.height, ), - child: const BlurHash( - hash: MessageContentStyle.defaultBlurHash, + child: BlurHash( + hash: event.blurHash ?? AppConfig.defaultImageBlurHash, ), ), Image.file( diff --git a/lib/pages/chat/events/sending_video_widget.dart b/lib/pages/chat/events/sending_video_widget.dart index daedf2f04..fe8001d24 100644 --- a/lib/pages/chat/events/sending_video_widget.dart +++ b/lib/pages/chat/events/sending_video_widget.dart @@ -92,7 +92,11 @@ class SendingVideoWidget extends StatelessWidget with PlayVideoActionMixin { if (matrixFile.filePath == null) { return; } - playVideoAction(context, matrixFile.filePath!); + playVideoAction( + context, + matrixFile.filePath!, + eventId: event.eventId, + ); } void _checkSendingFileStatus() { diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 197c84b0c..a161d16c1 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -492,7 +492,11 @@ class ChatDetailsController extends State Future _handleDownloadAndPlayVideo(Event event) { return handleDownloadVideoEvent( event: event, - playVideoAction: (path) => playVideoAction(context, path), + playVideoAction: (path) => playVideoAction( + context, + path, + eventId: event.eventId, + ), ); } diff --git a/lib/pages/image_viewer/image_viewer_style.dart b/lib/pages/image_viewer/image_viewer_style.dart new file mode 100644 index 000000000..d82902b80 --- /dev/null +++ b/lib/pages/image_viewer/image_viewer_style.dart @@ -0,0 +1,4 @@ +class ImageViewerStyle { + static const double minScaleInteractiveViewer = 1.0; + static const double maxScaleInteractiveViewer = 10.0; +} diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 7ca31c389..63e267e08 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:typed_data'; +import 'package:fluffychat/pages/image_viewer/image_viewer_style.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -22,9 +23,8 @@ class ImageViewerView extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.transparent, - body: GestureDetector( + return Center( + child: GestureDetector( onTap: () => controller.toggleAppbarPreview(), onDoubleTapDown: (details) => controller.onDoubleTapDown(details), onDoubleTap: () => controller.onDoubleTap(), @@ -35,8 +35,8 @@ class ImageViewerView extends StatelessWidget { child: InteractiveViewer( onInteractionEnd: controller.onInteractionEnds, transformationController: controller.transformationController, - minScale: 1.0, - maxScale: 10.0, + minScale: ImageViewerStyle.minScaleInteractiveViewer, + maxScale: ImageViewerStyle.maxScaleInteractiveViewer, child: Center( child: filePath != null ? Image.file( @@ -47,6 +47,7 @@ class ImageViewerView extends StatelessWidget { : FutureBuilder( future: controller.widget.event .downloadAndDecryptAttachment( + // FIXME: change to false after https://github.com/linagora/twake-on-matrix/issues/746 getThumbnail: true, ), builder: (context, snapshot) { diff --git a/lib/presentation/mixins/play_video_action_mixin.dart b/lib/presentation/mixins/play_video_action_mixin.dart index e3f0ebc42..006968900 100644 --- a/lib/presentation/mixins/play_video_action_mixin.dart +++ b/lib/presentation/mixins/play_video_action_mixin.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/hero_dialog_route.dart'; +import 'package:fluffychat/widgets/hero_page_route.dart'; import 'package:fluffychat/widgets/video_viewer_desktop_theme.dart'; import 'package:fluffychat/widgets/video_viewer_mobile_theme.dart'; import 'package:flutter/material.dart'; @@ -13,9 +13,9 @@ mixin PlayVideoActionMixin { }) async { if (!PlatformInfos.isWeb) { Navigator.of(context).push( - HeroDialogRoute( + HeroPageRoute( builder: (context) { - return InteractiveviewerGallery( + return InteractiveViewerGallery( itemBuilder: VideoViewerMobileTheme( path: uriOrFilePath, eventId: eventId, diff --git a/lib/utils/custom_disissible.dart b/lib/utils/custom_dismissable.dart similarity index 94% rename from lib/utils/custom_disissible.dart rename to lib/utils/custom_dismissable.dart index 90bc2aad6..3ec80085b 100644 --- a/lib/utils/custom_disissible.dart +++ b/lib/utils/custom_dismissable.dart @@ -29,6 +29,8 @@ class _CustomDismissibleState extends State late Animation _scaleAnimation; late Animation _opacityAnimation; + static const animationDuration = Duration(milliseconds: 300); + double _dragExtent = 0; bool _dragUnderway = false; @@ -39,7 +41,7 @@ class _CustomDismissibleState extends State super.initState(); _animateController = AnimationController( - duration: const Duration(milliseconds: 300), + duration: animationDuration, vsync: this, ); @@ -71,11 +73,11 @@ class _CustomDismissibleState extends State ); _opacityAnimation = DecorationTween( - begin: const BoxDecoration( - color: Color(0xFF000000), + begin: BoxDecoration( + color: Colors.black.withOpacity(1.0), ), - end: const BoxDecoration( - color: Color(0x00000000), + end: BoxDecoration( + color: Colors.black.withOpacity(0.0), ), ).animate(_animateController); } diff --git a/lib/utils/interactive_viewer_gallery.dart b/lib/utils/interactive_viewer_gallery.dart index 27239828e..4ef3fc9ff 100644 --- a/lib/utils/interactive_viewer_gallery.dart +++ b/lib/utils/interactive_viewer_gallery.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/utils/custom_disissible.dart'; +import 'package:fluffychat/utils/custom_dismissable.dart'; import 'package:flutter/material.dart'; /// A callback for the [InteractiveViewerBoundary] that is called when the scale @@ -10,8 +10,8 @@ typedef ScaleChanged = void Function(double scale); /// /// The callbacks are called when an interaction ends by listening to the /// [InteractiveViewer.onInteractionEnd] callback. -class InteractiveviewerGallery extends StatefulWidget { - const InteractiveviewerGallery({ +class InteractiveViewerGallery extends StatefulWidget { + const InteractiveViewerGallery({ super.key, required this.itemBuilder, this.maxScale = 2.5, @@ -32,11 +32,11 @@ class InteractiveviewerGallery extends StatefulWidget { final Function(DragEndDetails dragEndDetails)? handleDragEnd; @override - State createState() => - _InteractiveviewerGalleryState(); + State createState() => + _InteractiveViewerGalleryState(); } -class _InteractiveviewerGalleryState extends State +class _InteractiveViewerGalleryState extends State with SingleTickerProviderStateMixin { TransformationController? _transformationController; diff --git a/lib/widgets/hero_dialog_route.dart b/lib/widgets/hero_page_route.dart similarity index 94% rename from lib/widgets/hero_dialog_route.dart rename to lib/widgets/hero_page_route.dart index ac9af7eba..0ff97ef2e 100644 --- a/lib/widgets/hero_dialog_route.dart +++ b/lib/widgets/hero_page_route.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class HeroDialogRoute extends PageRoute { - HeroDialogRoute({ +class HeroPageRoute extends PageRoute { + HeroPageRoute({ required this.builder, this.onBackgroundTap, }) : super(); diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index ef452a73e..706dda3c5 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -1,7 +1,7 @@ import 'dart:typed_data'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; -import 'package:fluffychat/widgets/hero_dialog_route.dart'; +import 'package:fluffychat/widgets/hero_page_route.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; @@ -197,9 +197,9 @@ class _MxcImageState extends State if (widget.onTapPreview != null) { widget.onTapPreview!(); Navigator.of(context).push( - HeroDialogRoute( + HeroPageRoute( builder: (context) { - return InteractiveviewerGallery( + return InteractiveViewerGallery( itemBuilder: ImageViewer(widget.event!), ); }, From ab9137184d97d3614fd78647284f11e4419fd706 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 4 Oct 2023 15:55:11 +0700 Subject: [PATCH 47/60] TW-730: fix forward image not back to chat screen --- lib/pages/forward/forward.dart | 6 ++++-- lib/pages/image_viewer/image_viewer.dart | 6 +++++- lib/presentation/model/pop_result.dart | 8 ++++++++ lib/presentation/model/pop_result_from_forward.dart | 5 +++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 lib/presentation/model/pop_result.dart create mode 100644 lib/presentation/model/pop_result_from_forward.dart diff --git a/lib/pages/forward/forward.dart b/lib/pages/forward/forward.dart index c9dc80cc8..f1ae6da22 100644 --- a/lib/pages/forward/forward.dart +++ b/lib/pages/forward/forward.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/pages/chat/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/utils/extension/navigator_state_extension.dart'; +import 'package:fluffychat/presentation/model/pop_result_from_forward.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -107,7 +107,9 @@ class ForwardController extends State { switch (success.runtimeType) { case ForwardMessageSuccess: final dataOnSuccess = success as ForwardMessageSuccess; - Navigator.of(context).popAllDialogs(); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(const PopResultFromForward()); + } context.go('/rooms/${dataOnSuccess.room.id}'); break; case ForwardMessageIsShareFileState: diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index e469f85f9..43ddad33f 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; 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'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -46,12 +47,15 @@ class ImageViewerController extends State { /// Forward this image to another room. void forwardAction() async { Matrix.of(context).shareContent = widget.event.content; - await showDialog( + final result = await showDialog( context: context, useSafeArea: false, useRootNavigator: false, builder: (c) => const Forward(), ); + if (result is PopResultFromForward) { + Navigator.of(context).pop(); + } } void toggleAppbarPreview() { diff --git a/lib/presentation/model/pop_result.dart b/lib/presentation/model/pop_result.dart new file mode 100644 index 000000000..91c6b963f --- /dev/null +++ b/lib/presentation/model/pop_result.dart @@ -0,0 +1,8 @@ +import 'package:equatable/equatable.dart'; + +class PopResult with EquatableMixin { + const PopResult(); + + @override + List get props => []; +} diff --git a/lib/presentation/model/pop_result_from_forward.dart b/lib/presentation/model/pop_result_from_forward.dart new file mode 100644 index 000000000..3e93417ab --- /dev/null +++ b/lib/presentation/model/pop_result_from_forward.dart @@ -0,0 +1,5 @@ +import 'package:fluffychat/presentation/model/pop_result.dart'; + +class PopResultFromForward extends PopResult { + const PopResultFromForward(); +} From f14fd0b7a7cebc981de654ae103806db354ceab9 Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 5 Oct 2023 09:51:47 +0700 Subject: [PATCH 48/60] TW-730: support full screen for images --- lib/widgets/mxc_image.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 706dda3c5..6469ecad9 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/hero_page_route.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; @@ -196,7 +197,7 @@ class _MxcImageState extends State void _onTap(BuildContext context) async { if (widget.onTapPreview != null) { widget.onTapPreview!(); - Navigator.of(context).push( + Navigator.of(context, rootNavigator: PlatformInfos.isWeb).push( HeroPageRoute( builder: (context) { return InteractiveViewerGallery( From 7b620a340fa4f5e2d64cea76a37eff354b1dd922 Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 5 Oct 2023 10:37:24 +0700 Subject: [PATCH 49/60] improve: images/videos viewer --- .../image_viewer/image_viewer_style.dart | 7 ++++ lib/pages/image_viewer/image_viewer_view.dart | 3 +- .../mixins/play_video_action_mixin.dart | 33 +++++-------------- lib/utils/interactive_viewer_gallery.dart | 3 +- lib/widgets/video_viewer_desktop_theme.dart | 7 +++- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/lib/pages/image_viewer/image_viewer_style.dart b/lib/pages/image_viewer/image_viewer_style.dart index d82902b80..3d8842747 100644 --- a/lib/pages/image_viewer/image_viewer_style.dart +++ b/lib/pages/image_viewer/image_viewer_style.dart @@ -1,4 +1,11 @@ +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; + class ImageViewerStyle { static const double minScaleInteractiveViewer = 1.0; static const double maxScaleInteractiveViewer = 10.0; + static double? appBarHeight = PlatformInfos.isWeb ? 56 : null; + static EdgeInsetsGeometry paddingTopAppBar = EdgeInsetsDirectional.only( + top: PlatformInfos.isWeb ? 0 : 56, + ); } diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 63e267e08..31f1d1977 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -70,8 +70,9 @@ class ImageViewerView extends StatelessWidget { Widget _buildAppBarPreview() { return Container( + padding: ImageViewerStyle.paddingTopAppBar, + height: ImageViewerStyle.appBarHeight, color: LinagoraSysColors.material().onTertiaryContainer.withOpacity(0.5), - padding: const EdgeInsets.only(top: 56), child: ValueListenableBuilder( valueListenable: controller.showAppbarPreview, builder: (context, showAppbar, _) { diff --git a/lib/presentation/mixins/play_video_action_mixin.dart b/lib/presentation/mixins/play_video_action_mixin.dart index 006968900..4cf415de7 100644 --- a/lib/presentation/mixins/play_video_action_mixin.dart +++ b/lib/presentation/mixins/play_video_action_mixin.dart @@ -11,31 +11,16 @@ mixin PlayVideoActionMixin { String uriOrFilePath, { String? eventId, }) async { - if (!PlatformInfos.isWeb) { - Navigator.of(context).push( - HeroPageRoute( - builder: (context) { - return InteractiveViewerGallery( - itemBuilder: VideoViewerMobileTheme( - path: uriOrFilePath, - eventId: eventId, - ), - ); - }, - ), - ); - } else { - await showDialog( - context: context, - useRootNavigator: PlatformInfos.isWeb, - useSafeArea: false, + Navigator.of(context, rootNavigator: PlatformInfos.isWeb).push( + HeroPageRoute( builder: (context) { - if (PlatformInfos.isWeb || PlatformInfos.isDesktop) { - return VideoViewerDesktopTheme(path: uriOrFilePath); - } - return VideoViewerMobileTheme(path: uriOrFilePath, eventId: eventId); + return InteractiveViewerGallery( + itemBuilder: PlatformInfos.isMobile + ? VideoViewerMobileTheme(path: uriOrFilePath) + : VideoViewerDesktopTheme(path: uriOrFilePath), + ); }, - ); - } + ), + ); } } diff --git a/lib/utils/interactive_viewer_gallery.dart b/lib/utils/interactive_viewer_gallery.dart index 4ef3fc9ff..8b7290c38 100644 --- a/lib/utils/interactive_viewer_gallery.dart +++ b/lib/utils/interactive_viewer_gallery.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/utils/custom_dismissable.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; /// A callback for the [InteractiveViewerBoundary] that is called when the scale @@ -81,7 +82,7 @@ class _InteractiveViewerGalleryState extends State Widget build(BuildContext context) { return CustomDismissible( onDismissed: () => Navigator.of(context).pop(), - enabled: _enableDismiss, + enabled: _enableDismiss && !PlatformInfos.isWeb, child: widget.itemBuilder, ); } diff --git a/lib/widgets/video_viewer_desktop_theme.dart b/lib/widgets/video_viewer_desktop_theme.dart index 90d510749..8c2282019 100644 --- a/lib/widgets/video_viewer_desktop_theme.dart +++ b/lib/widgets/video_viewer_desktop_theme.dart @@ -31,7 +31,12 @@ class VideoViewerDesktopTheme extends StatelessWidget { seekBarHeight: VideoViewerStyle.seekBarHeight, seekBarThumbColor: Theme.of(context).colorScheme.primary, ), - fullscreen: const MaterialDesktopVideoControlsThemeData(), + fullscreen: MaterialDesktopVideoControlsThemeData( + seekBarColor: Theme.of(context).colorScheme.onSurfaceVariant, + seekBarPositionColor: Theme.of(context).colorScheme.primary, + seekBarHeight: VideoViewerStyle.seekBarHeight, + seekBarThumbColor: Theme.of(context).colorScheme.primary, + ), child: VideoPlayer( path: path, ), From 34e496dfb651e62e57b4afd2118f76225597eb49 Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Mon, 2 Oct 2023 15:12:44 +0200 Subject: [PATCH 50/60] feat: new encrypted message design --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat/encryption_button.dart | 45 ----------- lib/pages/chat/events/encrypted_content.dart | 56 +++++++++++++ .../chat/events/encrypted_content_style.dart | 11 +++ lib/pages/chat/events/encrypted_mixin.dart | 75 ++++++++++++++++++ lib/pages/chat/events/message.dart | 6 +- lib/pages/chat/events/message_content.dart | 78 +------------------ 7 files changed, 153 insertions(+), 121 deletions(-) delete mode 100644 lib/pages/chat/encryption_button.dart create mode 100644 lib/pages/chat/events/encrypted_content.dart create mode 100644 lib/pages/chat/events/encrypted_content_style.dart create mode 100644 lib/pages/chat/events/encrypted_mixin.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 4c38a9ffe..c2d9dc88b 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2754,5 +2754,6 @@ "workIdentitiesInfo": "WORK IDENTITIES INFO", "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name.", "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard.", - "changeProfilePhoto": "Change profile photo" + "changeProfilePhoto": "Change profile photo", + "thisMessageHasBeenEncrypted": "This message has been encrypted" } \ No newline at end of file diff --git a/lib/pages/chat/encryption_button.dart b/lib/pages/chat/encryption_button.dart deleted file mode 100644 index c51ba435c..000000000 --- a/lib/pages/chat/encryption_button.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - -import '../../widgets/matrix.dart'; - -class EncryptionButton extends StatelessWidget { - final Room room; - const EncryptionButton(this.room, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: Matrix.of(context) - .client - .onSync - .stream - .where((s) => s.deviceLists != null), - builder: (context, snapshot) { - return FutureBuilder( - future: room.calcEncryptionHealthState(), - builder: (BuildContext context, snapshot) => IconButton( - tooltip: room.encrypted - ? L10n.of(context)!.encrypted - : L10n.of(context)!.encryptionNotEnabled, - icon: Icon( - room.encrypted ? Icons.lock_outlined : Icons.lock_open_outlined, - size: 20, - color: room.joinRules != JoinRules.public && !room.encrypted - ? Colors.red - : room.joinRules != JoinRules.public && - snapshot.data == - EncryptionHealthState.unverifiedDevices - ? Colors.orange - : null, - ), - onPressed: () => context.go('/rooms/${room.id}/encryption'), - ), - ); - }, - ); - } -} diff --git a/lib/pages/chat/events/encrypted_content.dart b/lib/pages/chat/events/encrypted_content.dart new file mode 100644 index 000000000..dd8dfbaa3 --- /dev/null +++ b/lib/pages/chat/events/encrypted_content.dart @@ -0,0 +1,56 @@ +import 'package:fluffychat/pages/chat/events/encrypted_content_style.dart'; +import 'package:fluffychat/pages/chat/events/encrypted_mixin.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class EncryptedContent extends StatelessWidget with EncryptedMixin { + final Event event; + + const EncryptedContent({Key? key, required this.event}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EncryptedContentStyle.parentPadding, + child: InkWell( + onTap: () => verifyOrRequestKey(context, event), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onError, + shape: BoxShape.circle, + ), + padding: EncryptedContentStyle.leadingIconPadding, + child: Icon( + Icons.lock, + color: Theme.of(context).colorScheme.primary, + size: EncryptedContentStyle.leadingIconSize, + ), + ), + const SizedBox(width: EncryptedContentStyle.leadingAndTextGap), + Container( + constraints: const BoxConstraints( + maxWidth: EncryptedContentStyle.textMaxWidth, + ), + child: Text( + maxLines: 2, + L10n.of(context)!.thisMessageHasBeenEncrypted, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat/events/encrypted_content_style.dart b/lib/pages/chat/events/encrypted_content_style.dart new file mode 100644 index 000000000..cbde22376 --- /dev/null +++ b/lib/pages/chat/events/encrypted_content_style.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class EncryptedContentStyle { + static const EdgeInsets parentPadding = EdgeInsets.symmetric( + horizontal: 8, + ); + static const EdgeInsets leadingIconPadding = EdgeInsets.all(5); + static const double leadingIconSize = 20; + static const double leadingAndTextGap = 8; + static const double textMaxWidth = 165; +} diff --git a/lib/pages/chat/events/encrypted_mixin.dart b/lib/pages/chat/events/encrypted_mixin.dart new file mode 100644 index 000000000..23eb1cada --- /dev/null +++ b/lib/pages/chat/events/encrypted_mixin.dart @@ -0,0 +1,75 @@ +import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; +import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +mixin EncryptedMixin { + void verifyOrRequestKey(BuildContext context, Event event) async { + final l10n = L10n.of(context)!; + if (event.content['can_request_session'] != true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + event.type == EventTypes.Encrypted + ? l10n.needPantalaimonWarning + : event.calcLocalizedBodyFallback( + MatrixLocals(l10n), + ), + ), + ), + ); + return; + } + final client = Matrix.of(context).client; + if (client.isUnknownSession && client.encryption!.crossSigning.enabled) { + final success = await BootstrapDialog( + client: Matrix.of(context).client, + ).show(context); + if (success != true) return; + } + event.requestKey(); + final sender = event.senderFromMemoryOrFallback; + await showAdaptiveBottomSheet( + context: context, + builder: (context) => Scaffold( + appBar: AppBar( + leading: CloseButton(onPressed: Navigator.of(context).pop), + title: Text( + l10n.whyIsThisMessageEncrypted, + style: + const TextStyle(fontSize: MessageContentStyle.appBarFontSize), + ), + ), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + leading: Avatar( + mxContent: sender.avatarUrl, + name: sender.calcDisplayname(), + ), + title: Text(sender.calcDisplayname()), + subtitle: Text(event.originServerTs.localizedTime(context)), + trailing: const Icon(Icons.lock_outlined), + ), + const Divider(), + Text( + event.calcLocalizedBodyFallback( + MatrixLocals(l10n), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 9f29d10ec..8960b0d5b 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -116,6 +116,7 @@ class Message extends StatelessWidget { }.contains(event.messageType); final timelineText = { MessageTypes.Text, + MessageTypes.BadEncrypted, }.contains(event.messageType); final noPadding = { MessageTypes.File, @@ -577,7 +578,10 @@ class Message extends StatelessWidget { } bool hideDisplayName(bool ownMessage) => - ownMessage || event.room.isDirectChat || !isSameSender(nextEvent, event); + ownMessage || + event.room.isDirectChat || + !isSameSender(nextEvent, event) || + event.type == EventTypes.Encrypted; Widget _menuActionsRowBuilder(BuildContext context, bool ownMessage) { return ValueListenableBuilder( diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 655de425c..c87288ee8 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -2,7 +2,7 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/room/chat_room_search_state.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; +import 'package:fluffychat/pages/chat/events/encrypted_content.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/pages/chat/events/sending_image_info_widget.dart'; import 'package:fluffychat/pages/chat/events/sending_video_widget.dart'; @@ -20,11 +20,8 @@ import 'package:flutter_matrix_html/color_extension.dart'; import 'package:matrix/matrix.dart' hide Visibility; import 'package:fluffychat/pages/chat/events/event_video_player.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; -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/event_extension.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'audio_player.dart'; import 'cute_events.dart'; @@ -58,77 +55,15 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { required this.ownMessage, }) : super(key: key); - void _verifyOrRequestKey(BuildContext context) async { - final l10n = L10n.of(context)!; - if (event.content['can_request_session'] != true) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - event.type == EventTypes.Encrypted - ? l10n.needPantalaimonWarning - : event.calcLocalizedBodyFallback( - MatrixLocals(l10n), - ), - ), - ), - ); - return; - } - final client = Matrix.of(context).client; - if (client.isUnknownSession && client.encryption!.crossSigning.enabled) { - final success = await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - if (success != true) return; - } - event.requestKey(); - final sender = event.senderFromMemoryOrFallback; - await showAdaptiveBottomSheet( - context: context, - builder: (context) => Scaffold( - appBar: AppBar( - leading: CloseButton(onPressed: Navigator.of(context).pop), - title: Text( - l10n.whyIsThisMessageEncrypted, - style: - const TextStyle(fontSize: MessageContentStyle.appBarFontSize), - ), - ), - body: SafeArea( - child: ListView( - padding: const EdgeInsets.all(16), - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar( - mxContent: sender.avatarUrl, - name: sender.calcDisplayname(), - ), - title: Text(sender.calcDisplayname()), - subtitle: Text(event.originServerTs.localizedTime(context)), - trailing: const Icon(Icons.lock_outlined), - ), - const Divider(), - Text( - event.calcLocalizedBodyFallback( - MatrixLocals(l10n), - ), - ) - ], - ), - ), - ), - ); - } - @override Widget build(BuildContext context) { final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final buttonTextColor = event.senderId == Matrix.of(context).client.userID ? textColor : null; switch (event.type) { - case EventTypes.Message: case EventTypes.Encrypted: + return EncryptedContent(event: event); + case EventTypes.Message: case EventTypes.Sticker: switch (event.messageType) { case MessageTypes.Image: @@ -245,12 +180,7 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { continue textmessage; case MessageTypes.BadEncrypted: case EventTypes.Encrypted: - return _ButtonContent( - textColor: buttonTextColor, - onPressed: () => _verifyOrRequestKey(context), - icon: const Icon(Icons.lock_outline), - label: L10n.of(context)!.encrypted, - ); + return EncryptedContent(event: event); case MessageTypes.Location: final geoUri = Uri.tryParse(event.content.tryGet('geo_uri')!); From 6f014167d536be3807aa0171e726f410b793745e Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 5 Oct 2023 15:48:26 +0700 Subject: [PATCH 51/60] fix: remove pusher when logout --- lib/pages/settings_dashboard/settings/settings.dart | 7 ++++++- lib/utils/background_push.dart | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 17ae8a034..05759b6ba 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -79,7 +79,12 @@ class SettingsController extends State with ConnectPageMixin { final matrix = Matrix.of(context); await showFutureLoadingDialog( context: context, - future: () => matrix.client.logout(), + future: () async { + if (matrix.backgroundPush != null) { + await matrix.backgroundPush!.removeCurrentPusher(); + } + return matrix.client.logout(); + }, ); } diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 1ab50d99b..508009fad 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -570,4 +570,11 @@ class BackgroundPush { apnChannel.invokeMethod('clearAll'); } } + + Future removeCurrentPusher() async { + if (_pushToken == null) return; + await setupPusher( + oldTokens: {_pushToken}, + ); + } } From d7b937c8744f6d06be290bede1f16d01265d44d6 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 6 Oct 2023 11:08:41 +0700 Subject: [PATCH 52/60] TW-734: remove forward quick action in web --- lib/pages/chat/chat.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9af39f172..506c9a31e 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1383,7 +1383,6 @@ class ChatController extends State List listHorizontalActionMenuBuilder() { final listAction = [ ChatHorizontalActionMenu.reply, - ChatHorizontalActionMenu.forward, ChatHorizontalActionMenu.more, ]; return listAction From 7fbec71c52b772566221684620340798cb4eb30d Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 6 Oct 2023 11:10:45 +0700 Subject: [PATCH 53/60] TW-745: remove bigger bubble when hover --- lib/pages/chat/events/message.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 8960b0d5b..f48a585f3 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -601,7 +601,7 @@ class Message extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: listHorizontalActionMenu.map((item) { return Padding( - padding: const EdgeInsetsDirectional.all(4), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 4), child: TwakeIconButton( icon: item.action.getIcon(), imagePath: item.action.getImagePath(), From 4e39823ec289c20c02119e03edfe5646ec727119 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Fri, 6 Oct 2023 09:36:24 +0700 Subject: [PATCH 54/60] TW-765: Keep navigation bar when open search page --- .../layouts/adaptive_layout/adaptive_scaffold.dart | 3 +-- .../layouts/adaptive_layout/adaptive_scaffold_view.dart | 9 ++------- lib/widgets/layouts/enum/adaptive_destinations_enum.dart | 1 + 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart index 488ae0180..f438ee569 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart @@ -70,8 +70,7 @@ class AdaptiveScaffoldAppController extends State { } void _onOpenSearchPage() { - activeNavigationBar.value = AdaptiveDestinationEnum.search; - _jumpToPageByIndex(); + pageController.jumpToPage(AdaptiveDestinationEnum.search.index); } void _onCloseSearchPage() { diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart index 5fc9ccf55..235f5347f 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart @@ -63,8 +63,6 @@ class AppScaffoldView extends StatelessWidget { valueListenable: activeNavigationBar, builder: (_, navigatorBar, child) { switch (navigatorBar) { - case AdaptiveDestinationEnum.search: - return const SizedBox(); case AdaptiveDestinationEnum.contacts: case AdaptiveDestinationEnum.rooms: default: @@ -107,11 +105,8 @@ class AppScaffoldView extends StatelessWidget { _bottomNavigationBarBuilder(context), ), ), - _triggerPageViewBuilder( - navigatorBarType: AdaptiveDestinationEnum.search, - navigatorBarWidget: Search( - onCloseSearchPage: onCloseSearchPage, - ), + Search( + onCloseSearchPage: onCloseSearchPage, ), ], ), diff --git a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart index 2cb62815f..ef4801da2 100644 --- a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart +++ b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart @@ -19,6 +19,7 @@ enum AdaptiveDestinationEnum { ), label: L10n.of(context)!.contacts, ); + case AdaptiveDestinationEnum.search: case AdaptiveDestinationEnum.rooms: return NavigationDestination( icon: UnreadRoomsBadge( From 8e40db6afba46a8439f6081ae1091a82ed943ac5 Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Tue, 3 Oct 2023 17:08:15 +0200 Subject: [PATCH 55/60] TW-741: redesign reply content style --- lib/pages/chat/events/reply_content.dart | 107 ++++++++---------- .../chat/events/reply_content_style.dart | 53 +++++++++ 2 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 lib/pages/chat/events/reply_content_style.dart diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index 25c721b7d..278b92ea3 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pages/chat/events/reply_content_style.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:flutter/material.dart'; @@ -29,8 +30,6 @@ class ReplyContent extends StatelessWidget { final timeline = this.timeline; final displayEvent = timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent; - const fontSizeDisplayName = AppConfig.messageFontSize * 0.76; - const fontSizeDisplayContent = AppConfig.messageFontSize * 0.88; if (AppConfig.renderHtml && [EventTypes.Message, EventTypes.Encrypted] .contains(displayEvent.type) && @@ -45,14 +44,10 @@ class ReplyContent extends StatelessWidget { } replyBody = HtmlMessage( html: html!, - defaultTextStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onBackground, - fontSize: fontSizeDisplayContent, - overflow: TextOverflow.ellipsis, - ), + defaultTextStyle: ReplyContentStyle.replyBodyTextStyle(context), maxLines: 1, room: displayEvent.room, - emoteSize: fontSizeDisplayContent * 1.5, + emoteSize: ReplyContentStyle.fontSizeDisplayContent * 1.5, event: timeline!.events.first, chatController: chatController, ); @@ -65,65 +60,53 @@ class ReplyContent extends StatelessWidget { ), overflow: TextOverflow.ellipsis, maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.onBackground, - fontSize: fontSizeDisplayContent, - ), + style: ReplyContentStyle.replyBodyTextStyle(context), ); } final user = displayEvent.getUser(); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 3, - height: fontSizeDisplayContent * 2 + 6, - color: ownMessage - ? Theme.of(context).colorScheme.onPrimaryContainer - : Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 6), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (user != null) - Text( - '${user.calcDisplayname()}:', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - color: ownMessage - ? Theme.of(context).colorScheme.onPrimaryContainer - : Theme.of(context).colorScheme.primary, - fontSize: fontSizeDisplayName, + return Container( + padding: ReplyContentStyle.replyParentContainerPadding, + decoration: + ReplyContentStyle.replyParentContainerDecoration(context, ownMessage), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: ReplyContentStyle.prefixBarWidth, + height: ReplyContentStyle.fontSizeDisplayContent * 2, + decoration: ReplyContentStyle.prefixBarDecoration(context), + ), + const SizedBox(width: ReplyContentStyle.prefixAndDisplayNameSpacing), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (user != null) + Text( + user.calcDisplayname(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: ReplyContentStyle.displayNameTextStyle(context), ), - ), - if (displayEvent.getUser() == null) - FutureBuilder( - future: displayEvent.fetchSenderUser(), - builder: (context, snapshot) { - return Text( - '${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: FontWeight.bold, - color: ownMessage - ? Theme.of(context).colorScheme.onPrimaryContainer - : Theme.of(context).colorScheme.primary, - fontSize: fontSizeDisplayName, - ), - ); - }, - ), - replyBody, - ], + if (displayEvent.getUser() == null) + FutureBuilder( + future: displayEvent.fetchSenderUser(), + builder: (context, snapshot) { + return Text( + '${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: ReplyContentStyle.displayNameTextStyle(context), + ); + }, + ), + replyBody, + ], + ), ), - ), - ], + ], + ), ); } } diff --git a/lib/pages/chat/events/reply_content_style.dart b/lib/pages/chat/events/reply_content_style.dart new file mode 100644 index 000000000..31d9f13d7 --- /dev/null +++ b/lib/pages/chat/events/reply_content_style.dart @@ -0,0 +1,53 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class ReplyContentStyle { + static const double fontSizeDisplayName = AppConfig.messageFontSize * 0.76; + static const double fontSizeDisplayContent = AppConfig.messageFontSize * 0.88; + + static const EdgeInsets replyParentContainerPadding = EdgeInsets.only( + left: 4, + right: 8.0, + top: 8.0, + bottom: 8.0, + ); + + static BoxDecoration replyParentContainerDecoration( + BuildContext context, + bool ownMessage, + ) { + return BoxDecoration( + color: ownMessage + ? LinagoraRefColors.material().primary[95] + : Theme.of(context).colorScheme.surfaceTint.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + ); + } + + static const double prefixBarWidth = 3.0; + static BoxDecoration prefixBarDecoration(BuildContext context) { + return BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: Theme.of(context).colorScheme.primary, + ); + } + + static const double prefixAndDisplayNameSpacing = 6.0; + static TextStyle? displayNameTextStyle(BuildContext context) { + return Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.bold, + fontSize: fontSizeDisplayName, + ); + } + + static TextStyle? replyBodyTextStyle(BuildContext context) { + return Theme.of(context).textTheme.bodySmall?.copyWith( + color: LinagoraRefColors.material().neutral[50], + fontWeight: FontWeight.w500, + overflow: TextOverflow.ellipsis, + fontSize: fontSizeDisplayContent, + ); + } +} From d802efb9d504e49c06f4381a242a3ec2eaad53f7 Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Wed, 27 Sep 2023 17:52:48 +0200 Subject: [PATCH 56/60] TW-633: preview pdf on web client --- assets/l10n/intl_en.arb | 5 +- .../model/extensions/mime_type_extension.dart | 48 ++++++++++++----- .../supported_preview_file_types.dart | 10 ++++ lib/pages/chat/chat.dart | 52 ++++++++++++++++--- lib/pages/chat_draft/draft_chat.dart | 3 +- lib/resource/image_paths.dart | 2 +- .../event_extension.dart | 4 +- .../matrix_file_extension.dart | 18 +++++-- 8 files changed, 114 insertions(+), 28 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c2d9dc88b..424e32754 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2755,5 +2755,8 @@ "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name.", "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard.", "changeProfilePhoto": "Change profile photo", - "thisMessageHasBeenEncrypted": "This message has been encrypted" + "thisMessageHasBeenEncrypted": "This message has been encrypted", + "roomCreationFailed": "Room creation failed", + "errorGettingPdf": "Error getting PDF", + "errorPreviewingFile": "Error previewing file" } \ No newline at end of file diff --git a/lib/domain/model/extensions/mime_type_extension.dart b/lib/domain/model/extensions/mime_type_extension.dart index edbd1706c..24ca4afce 100644 --- a/lib/domain/model/extensions/mime_type_extension.dart +++ b/lib/domain/model/extensions/mime_type_extension.dart @@ -43,20 +43,26 @@ extension MediaTypeExtension on Event { 'AttachmentExtension::getIcon(): mediaType: $mimeType || fileType: $fileType', ); if (mimeType?.isEmpty == true || fileType == null) { - return ImagePaths.icFileUnKnow; + return ImagePaths.icFileUnknown; } - if (isDocFile()) { - return ImagePaths.icFileDocx; - } else if (isExcelFile()) { - return ImagePaths.icFileXlsx; - } else if (isPowerPointFile()) { - return ImagePaths.icFilePptx; - } else if (isPdfFile()) { - return ImagePaths.icFilePdf; - } else if (isZipFile()) { - return ImagePaths.icFileZip; + + switch (getPreviewIconFileType()) { + case SupportedIconFileTypesEnum.image: + return ImagePaths.icFileUnknown; + case SupportedIconFileTypesEnum.doc: + return ImagePaths.icFileDocx; + case SupportedIconFileTypesEnum.excel: + return ImagePaths.icFileXlsx; + case SupportedIconFileTypesEnum.powerPoint: + return ImagePaths.icFilePptx; + case SupportedIconFileTypesEnum.pdf: + return ImagePaths.icFilePdf; + case SupportedIconFileTypesEnum.zip: + return ImagePaths.icFileZip; + case SupportedIconFileTypesEnum.unknown: + default: + return ImagePaths.icFileUnknown; } - return ImagePaths.icFileUnKnow; } String getFileType(BuildContext context) { @@ -70,4 +76,22 @@ extension MediaTypeExtension on Event { return L10n.of(context)!.file.toUpperCase(); } } + + SupportedIconFileTypesEnum getPreviewIconFileType() { + if (isImageFile()) { + return SupportedIconFileTypesEnum.image; + } else if (isDocFile()) { + return SupportedIconFileTypesEnum.doc; + } else if (isExcelFile()) { + return SupportedIconFileTypesEnum.excel; + } else if (isPowerPointFile()) { + return SupportedIconFileTypesEnum.powerPoint; + } else if (isPdfFile()) { + return SupportedIconFileTypesEnum.pdf; + } else if (isZipFile()) { + return SupportedIconFileTypesEnum.zip; + } else { + return SupportedIconFileTypesEnum.unknown; + } + } } 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 68c9de0ef..18d280c92 100644 --- a/lib/domain/model/preview_file/supported_preview_file_types.dart +++ b/lib/domain/model/preview_file/supported_preview_file_types.dart @@ -1,3 +1,13 @@ +enum SupportedIconFileTypesEnum { + image, + doc, + excel, + powerPoint, + pdf, + zip, + unknown, +} + class SupportedPreviewFileTypes { static const imageMimeTypes = [ 'image/bmp', diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 506c9a31e..a82bd04c8 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -10,6 +10,7 @@ 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/domain/model/extensions/mime_type_extension.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'; @@ -62,6 +63,7 @@ import 'package:record/record.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:share_plus/share_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:universal_html/html.dart' as html; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; @@ -511,7 +513,19 @@ class ChatController extends State }); } - void onFileTapped(Event event) async { + void onFileTapped(Event event) { + if (PlatformInfos.isWeb) { + onFileTappedWeb(event); + } else { + onFileTappedMobile(event); + } + } + + void onFileTappedWeb(Event event) { + return _handlePreviewWeb(event: event); + } + + void onFileTappedMobile(Event event) async { final permissionHandler = PermissionHandlerService(); final storagePermissionStatus = await permissionHandler.storagePermissionStatus; @@ -531,11 +545,11 @@ class ChatController extends State ); if (await permissionHandler.storagePermissionStatus == PermissionStatus.granted) { - _handleDownloadFileForPreview(event: event); + _handleDownloadFileForPreviewMobile(event: event); } break; case PermissionStatus.granted: - _handleDownloadFileForPreview(event: event); + _handleDownloadFileForPreviewMobile(event: event); break; case PermissionStatus.permanentlyDenied: showDialog( @@ -560,7 +574,20 @@ class ChatController extends State } } - void _handleDownloadFileForPreview({required Event event}) async { + void _handlePreviewWeb({required Event event}) async { + if (!event.hasAttachment) { + Fluttertoast.showToast(msg: L10n.of(context)!.errorPreviewingFile); + return; + } + + if (event.isPdfFile()) { + return previewPdfWeb(context, event); + } + + downloadFileAction(context, event); + } + + void _handleDownloadFileForPreviewMobile({required Event event}) async { final downloadFileForPreviewInteractor = getIt.get(); final tempDirPath = (await getTemporaryDirectory()).path; @@ -1468,8 +1495,21 @@ class ChatController extends State } } - void downloadFileAction(BuildContext context, Event event) => - event.saveFile(context); + Future downloadFileAction(BuildContext context, Event event) async => + await event.saveFile(context); + + void previewPdfWeb(BuildContext context, Event event) async { + final pdf = await event.getFile(context); + if (pdf.result == null || event.sizeString != pdf.result?.sizeString) { + Fluttertoast.showToast(msg: L10n.of(context)!.errorGettingPdf); + return; + } + + final blob = html.Blob([pdf.result!.bytes], 'application/pdf'); + final url = html.Url.createObjectUrlFromBlob(blob); + html.window.open(url, "_blank"); + html.Url.revokeObjectUrl(url); + } void handleContextMenuAction( BuildContext context, diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index 61a148596..f56d441f6 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -26,6 +26,7 @@ import 'package:linagora_design_flutter/images_picker/images_picker.dart' hide ImagePicker; import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; typedef OnRoomCreatedSuccess = FutureOr Function(Room room)?; typedef OnRoomCreatedFailed = FutureOr Function()?; @@ -238,7 +239,7 @@ class DraftChatController extends State void onInputBarSubmitted(_) { sendText( onCreateRoomFailed: () { - Fluttertoast.showToast(msg: 'Create room failed'); + Fluttertoast.showToast(msg: L10n.of(context)!.roomCreationFailed); FocusScope.of(context).requestFocus(inputFocus); }, ); diff --git a/lib/resource/image_paths.dart b/lib/resource/image_paths.dart index bf1080c38..67b1f07d9 100644 --- a/lib/resource/image_paths.dart +++ b/lib/resource/image_paths.dart @@ -24,7 +24,7 @@ class ImagePaths { static String get icFilePdf => _getImagePath('ic_file_pdf.svg'); static String get icFilePptx => _getImagePath('ic_file_ppt.svg'); static String get icFileFolder => _getImagePath('ic_file_folder.svg'); - static String get icFileUnKnow => _getImagePath('ic_file_unknow.svg'); + static String get icFileUnknown => _getImagePath('ic_file_unknow.svg'); static String get icTwakeImageLogoDark => _getImagePath('ic_twake_image_logo_dark.svg'); static String get icApplicationGrid => diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 667d9dedf..354d465cb 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -17,10 +17,10 @@ extension LocalizedBody on Event { future: downloadAndDecryptAttachment, ); - void saveFile(BuildContext context) async { + Future saveFile(BuildContext context) async { final matrixFile = await getFile(context); - matrixFile.result?.downloadFile(context); + return await matrixFile.result?.downloadFile(context); } String get filename { diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 3d39eefa1..abd825334 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -15,14 +15,18 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:file_saver/file_saver.dart'; extension MatrixFileExtension on MatrixFile { - void downloadFile(BuildContext context) async { + Future downloadFile(BuildContext context) async { if (PlatformInfos.isWeb) { - return downloadFileInWeb(context); + return await downloadFileInWeb(context); } if (PlatformInfos.isMobile) { - return downloadImageInMobile(context); + return await downloadImageInMobile(context); } + + throw UnsupportedError( + 'Download feature is not supported on this platform', + ); } void share(BuildContext context) async { @@ -40,7 +44,7 @@ extension MatrixFileExtension on MatrixFile { return; } - void downloadFileInWeb(BuildContext context) async { + Future downloadFileInWeb(BuildContext context) async { Logs().d("MatrixFileExtension()::downloadFileInWeb()::download on Web"); final directory = await FileSaver.instance.saveFile( @@ -53,9 +57,11 @@ extension MatrixFileExtension on MatrixFile { Fluttertoast.showToast( msg: L10n.of(context)!.downloadFileInWeb(directory), ); + + return '$directory/$name'; } - void downloadImageInMobile(BuildContext context) async { + Future downloadImageInMobile(BuildContext context) async { Logs().d( "MatrixFileExtension()::downloadImageInMobile()::download on Mobile", ); @@ -70,6 +76,8 @@ extension MatrixFileExtension on MatrixFile { ? L10n.of(context)!.downloadImageSuccess : L10n.of(context)!.downloadImageError, ); + + return result?['filePath'] ?? ''; } FileType get filePickerFileType { From 771a4cf8b500539d7ff72f20198259a95c15a7c0 Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Thu, 5 Oct 2023 13:03:08 +0200 Subject: [PATCH 57/60] TW-633: add twake snackbar helper --- lib/pages/chat/add_widget_tile.dart | 5 +-- lib/pages/chat/chat.dart | 25 ++++-------- lib/pages/chat/chat_room_search_mixin.dart | 4 +- lib/pages/chat/events/audio_player.dart | 15 ++++---- lib/pages/chat/events/event_video_player.dart | 15 ++++---- lib/pages/chat_details/chat_details.dart | 18 +++++---- lib/pages/chat_draft/draft_chat.dart | 5 ++- lib/pages/chat_list/chat_list.dart | 16 +++----- lib/pages/chat_list/chat_list_item.dart | 8 ++-- .../chat_permissions_settings.dart | 5 +-- .../invitation_selection.dart | 8 ++-- lib/pages/login/login.dart | 5 +-- .../settings_profile/settings_profile.dart | 16 ++------ .../settings_profile_item_style.dart | 18 --------- .../settings_security/settings_security.dart | 5 +-- lib/pages/story/story_page.dart | 13 ++----- .../user_bottom_sheet/user_bottom_sheet.dart | 5 +-- .../mixins/media_picker_mixin.dart | 9 ++--- lib/utils/fluffy_share.dart | 5 +-- .../matrix_file_extension.dart | 12 +++--- lib/utils/twake_snackbar.dart | 38 +++++++++++++++++++ lib/utils/url_launcher.dart | 9 ++--- lib/utils/voip_plugin.dart | 11 ++---- lib/widgets/matrix.dart | 8 ++-- 24 files changed, 130 insertions(+), 148 deletions(-) create mode 100644 lib/utils/twake_snackbar.dart diff --git a/lib/pages/chat/add_widget_tile.dart b/lib/pages/chat/add_widget_tile.dart index 89ad8976d..16d22ea05 100644 --- a/lib/pages/chat/add_widget_tile.dart +++ b/lib/pages/chat/add_widget_tile.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -74,9 +75,7 @@ class AddWidgetTileState extends State { widget.room.addWidget(matrixWidget); Navigator.of(context).pop(); } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.errorAddingWidget)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.errorAddingWidget); } } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index a82bd04c8..b12c3f105 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -41,6 +41,7 @@ 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/responsive/responsive_utils.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; @@ -49,7 +50,6 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; @@ -251,13 +251,7 @@ class ChatController extends State try { await timeline!.requestHistory(historyCount: _loadHistoryCount); } catch (err) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - (err).toLocalizedString(context), - ), - ), - ); + TwakeSnackBar.show(context, (err).toLocalizedString(context)); rethrow; } } @@ -576,7 +570,7 @@ class ChatController extends State void _handlePreviewWeb({required Event event}) async { if (!event.hasAttachment) { - Fluttertoast.showToast(msg: L10n.of(context)!.errorPreviewingFile); + TwakeSnackBar.show(context, L10n.of(context)!.errorPreviewingFile); return; } @@ -599,7 +593,7 @@ class ChatController extends State .listen((event) { event.fold((failure) { if (failure is DownloadFileForPreviewFailure) { - Fluttertoast.showToast(msg: 'Error: ${failure.exception}'); + TwakeSnackBar.show(context, 'Error: ${failure.exception}'); } }, (success) { if (success is DownloadFileForPreviewSuccess) { @@ -838,9 +832,7 @@ class ChatController extends State setState(() { selectedEvents.clear(); }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.contentHasBeenReported); } void redactEventsAction() async { @@ -1343,9 +1335,7 @@ class ChatController extends State try { await voipPlugin!.voip.inviteToCall(room!.id, callType); } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toLocalizedString(context))), - ); + TwakeSnackBar.show(context, e.toLocalizedString(context)); } } else { await showOkAlertDialog( @@ -1501,7 +1491,8 @@ class ChatController extends State void previewPdfWeb(BuildContext context, Event event) async { final pdf = await event.getFile(context); if (pdf.result == null || event.sizeString != pdf.result?.sizeString) { - Fluttertoast.showToast(msg: L10n.of(context)!.errorGettingPdf); + TwakeSnackBar.show(context, L10n.of(context)!.errorGettingPdf); + return; } diff --git a/lib/pages/chat/chat_room_search_mixin.dart b/lib/pages/chat/chat_room_search_mixin.dart index d003041c2..7a5dc01c0 100644 --- a/lib/pages/chat/chat_room_search_mixin.dart +++ b/lib/pages/chat/chat_room_search_mixin.dart @@ -6,11 +6,11 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/room/chat_room_search_state.dart'; import 'package:fluffychat/domain/app_state/search/search_state.dart'; import 'package:fluffychat/domain/usecase/room/chat_room_search_interactor.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:dartz/dartz.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -157,7 +157,7 @@ mixin ChatRoomSearchMixin { default: } } else if (event.isLeft()) { - Fluttertoast.showToast(msg: L10n.of(context)!.noMoreResult); + TwakeSnackBar.show(context, L10n.of(context)!.noMoreResult); switch (direction) { case Direction.b: canGoUp.value = false; diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 7c2e559b2..cf40452b7 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -86,10 +87,9 @@ class AudioPlayerState extends State { _playAction(); } catch (e, s) { Logs().v('Could not download audio file', e, s); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toLocalizedString(context)), - ), + TwakeSnackBar.show( + context, + e.toLocalizedString(context), ); } } @@ -136,10 +136,9 @@ class AudioPlayerState extends State { await audioPlayer.setAudioSource(MatrixFileAudioSource(matrixFile!)); } audioPlayer.play().catchError((e, s) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.oopsSomethingWentWrong), - ), + TwakeSnackBar.show( + context, + L10n.of(context)!.oopsSomethingWentWrong, ); Logs().w('Error while playing audio', e, s); }); diff --git a/lib/pages/chat/events/event_video_player.dart b/lib/pages/chat/events/event_video_player.dart index 1670d06fe..20d947eb2 100644 --- a/lib/pages/chat/events/event_video_player.dart +++ b/lib/pages/chat/events/event_video_player.dart @@ -4,6 +4,7 @@ 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/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; @@ -66,17 +67,15 @@ class EventVideoPlayerState extends State _downloadStateNotifier.value = DownloadVideoState.done; } on MatrixConnectionException catch (e) { _downloadStateNotifier.value = DownloadVideoState.failed; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toLocalizedString(context)), - ), + TwakeSnackBar.show( + context, + e.toLocalizedString(context), ); } catch (e, s) { _downloadStateNotifier.value = DownloadVideoState.failed; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toLocalizedString(context)), - ), + TwakeSnackBar.show( + context, + e.toLocalizedString(context), ); Logs().w('Error while playing video', e, s); } diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index a161d16c1..da9a93a55 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/presentation/model/chat_details/chat_details_page_model.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -122,8 +123,9 @@ class ChatDetailsController extends State future: () => room.setName(input.single), ); if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged)), + TwakeSnackBar.show( + context, + L10n.of(context)!.displaynameHasBeenChanged, ); } } @@ -202,8 +204,9 @@ class ChatDetailsController extends State switch (option) { case AliasActions.copy: await Clipboard.setData(ClipboardData(text: select)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)), + TwakeSnackBar.show( + context, + L10n.of(context)!.copiedToClipboard, ); break; case AliasActions.delete: @@ -278,10 +281,9 @@ class ChatDetailsController extends State future: () => room.setDescription(input.single), ); if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged), - ), + TwakeSnackBar.show( + context, + L10n.of(context)!.groupDescriptionHasBeenChanged, ); } } diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index f56d441f6..0583f1eb5 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -17,9 +17,9 @@ import 'package:fluffychat/presentation/model/presentation_contact.dart'; import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; import 'package:fluffychat/utils/network_connection_service.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:fluttertoast/fluttertoast.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.dart' @@ -239,7 +239,8 @@ class DraftChatController extends State void onInputBarSubmitted(_) { sendText( onCreateRoomFailed: () { - Fluttertoast.showToast(msg: L10n.of(context)!.roomCreationFailed); + TwakeSnackBar.show(context, L10n.of(context)!.roomCreationFailed); + FocusScope.of(context).requestFocus(inputFocus); }, ); diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 88efb224b..0a477d39f 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -23,6 +23,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; 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/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/foundation.dart'; @@ -135,13 +136,7 @@ class ChatListController extends State ); } catch (e, s) { Logs().w('Searching has crashed', e, s); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - e.toLocalizedString(context), - ), - ), - ); + TwakeSnackBar.show(context, e.toLocalizedString(context)); } if (!isSearchMode) return; setState(() { @@ -420,10 +415,9 @@ class ChatListController extends State ); if (result.error == null) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.chatHasBeenAddedToThisSpace), - ), + TwakeSnackBar.show( + context, + L10n.of(context)!.chatHasBeenAddedToThisSpace, ); } diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index b4143af0f..0e67a69a8 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pages/chat_list/chat_list_item_style.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item_subtitle.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item_title.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -43,10 +44,9 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin { if (activeChat) return; switch (room.membership) { case Membership.ban: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat), - ), + TwakeSnackBar.show( + context, + L10n.of(context)!.youHaveBeenBannedFromThisChat, ); return; case Membership.leave: diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart index 16875b596..b99c803ab 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -29,9 +30,7 @@ class ChatPermissionsSettingsController extends State { }) async { final room = Matrix.of(context).client.getRoomById(roomId!)!; if (!room.canSendEvent(EventTypes.RoomPowerLevels)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.noPermission)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.noPermission); return; } final newLevel = await showPermissionChooser( diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 9f4959ca9..2e7ca9d3f 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pages/new_group/contacts_selection.dart'; import 'package:fluffychat/pages/new_group/contacts_selection_view.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/material.dart'; @@ -85,10 +86,9 @@ class InvitationSelectionController ), ); if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup), - ), + TwakeSnackBar.show( + context, + L10n.of(context)!.contactHasBeenInvitedToTheGroup, ); onCloseDialogInvite(); inviteSuccessAction(); diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index 429331e90..541fe7c9d 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -233,9 +234,7 @@ class LoginController extends State { ), ); if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.passwordHasBeenChanged); usernameController.text = input.single; passwordController.text = password.single; login(); diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index d05daeef8..8ed48faee 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -11,7 +11,6 @@ import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/event/twake_inapp_event_types.dart'; -import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; @@ -22,6 +21,7 @@ import 'package:fluffychat/presentation/mixins/single_image_picker_mixin.dart'; import 'package:fluffychat/utils/dialog/twake_loading_dialog.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.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:flutter/services.dart'; @@ -463,17 +463,9 @@ class SettingsProfileController extends State switch (settingsProfileEnum) { case SettingsProfileEnum.matrixId: Clipboard.setData(ClipboardData(text: client.mxid(context))); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - width: SettingsProfileItemStyle.widthSnackBar(context), - padding: SettingsProfileItemStyle.snackBarPadding, - content: Text( - L10n.of(context)!.copiedMatrixIdToClipboard, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.background, - ), - ), - ), + TwakeSnackBar.show( + context, + L10n.of(context)!.copiedMatrixIdToClipboard, ); break; default: diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart index 1c27526b3..dc359e972 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart @@ -1,27 +1,9 @@ -import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; class SettingsProfileItemStyle { - static ResponsiveUtils responsiveUtils = getIt.get(); - static const double iconSize = 24.0; static const double dividerSize = 2.0; static const EdgeInsetsDirectional itemBuilderPadding = EdgeInsetsDirectional.only(end: 8.0); - - static const EdgeInsetsDirectional snackBarPadding = - EdgeInsetsDirectional.symmetric( - horizontal: 16, - vertical: 14, - ); - - static double? widthSnackBar(BuildContext context) { - if (responsiveUtils.isWebDesktop(context)) { - return 334; - } else { - return null; - } - } } diff --git a/lib/pages/settings_dashboard/settings_security/settings_security.dart b/lib/pages/settings_dashboard/settings_security/settings_security.dart index 4652d7f1a..abf885f11 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:fluffychat/pages/bootstrap/bootstrap_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'; @@ -55,9 +56,7 @@ class SettingsSecurityController extends State { .changePassword(input.last, oldPassword: input.first), ); if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.passwordHasBeenChanged); } } diff --git a/lib/pages/story/story_page.dart b/lib/pages/story/story_page.dart index 691ac5b44..1f197fea6 100644 --- a/lib/pages/story/story_page.dart +++ b/lib/pages/story/story_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -91,14 +92,10 @@ class StoryPageController extends State { await client.getRoomById(roomId)!.sendTextEvent(message); replyController.clear(); replyFocus.unfocus(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.replyHasBeenSent)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.replyHasBeenSent); } catch (e, s) { Logs().w('Unable to reply to story', e, s); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toLocalizedString(context))), - ); + TwakeSnackBar.show(context, e.toLocalizedString(context)); } finally { setState(() { replyLoading = false; @@ -352,9 +349,7 @@ class StoryPageController extends State { ); _modalOpened = false; if (result.error != null) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.contentHasBeenReported); } Future downloadAndDecryptAttachment( diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index 2fbe25995..36850b245 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -91,9 +92,7 @@ class UserBottomSheetController extends State { ), ); if (result.error != null) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.contentHasBeenReported); break; case UserBottomSheetAction.mention: Navigator.of(context, rootNavigator: false).pop(); diff --git a/lib/presentation/mixins/media_picker_mixin.dart b/lib/presentation/mixins/media_picker_mixin.dart index 93dd7f567..f31db7a5f 100644 --- a/lib/presentation/mixins/media_picker_mixin.dart +++ b/lib/presentation/mixins/media_picker_mixin.dart @@ -1,10 +1,10 @@ import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/pages/chat/item_actions_bottom_widget.dart'; import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:linagora_design_flutter/images_picker/images_picker.dart' @@ -167,10 +167,9 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { children: [ Expanded( child: TextFormField( - onTap: () => Fluttertoast.showToast( - msg: L10n.of(context)! - .captionForImagesIsNotSupportYet, - gravity: ToastGravity.CENTER, + onTap: () => TwakeSnackBar.show( + context, + L10n.of(context)!.captionForImagesIsNotSupportYet, ), decoration: InputDecoration( prefixIcon: Icon( diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart index b5f1321bc..df0cef196 100644 --- a/lib/utils/fluffy_share.dart +++ b/lib/utils/fluffy_share.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,9 +19,7 @@ abstract class FluffyShare { await Clipboard.setData( ClipboardData(text: text), ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)), - ); + TwakeSnackBar.show(context, L10n.of(context)!.copiedToClipboard); return; } } diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index abd825334..1b4dadc09 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -2,8 +2,8 @@ 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:fluttertoast/fluttertoast.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; @@ -54,8 +54,9 @@ extension MatrixFileExtension on MatrixFile { mimeType: mimeType.toMimeTypeEnum(), ); - Fluttertoast.showToast( - msg: L10n.of(context)!.downloadFileInWeb(directory), + TwakeSnackBar.show( + context, + L10n.of(context)!.downloadFileInWeb(directory), ); return '$directory/$name'; @@ -71,8 +72,9 @@ extension MatrixFileExtension on MatrixFile { name: name, ); - Fluttertoast.showToast( - msg: result?['isSuccess'] + TwakeSnackBar.show( + context, + result?['isSuccess'] == true ? L10n.of(context)!.downloadImageSuccess : L10n.of(context)!.downloadImageError, ); diff --git a/lib/utils/twake_snackbar.dart b/lib/utils/twake_snackbar.dart new file mode 100644 index 000000000..a0e6bbc41 --- /dev/null +++ b/lib/utils/twake_snackbar.dart @@ -0,0 +1,38 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:flutter/material.dart'; + +class TwakeSnackBarStyle { + static ResponsiveUtils responsiveUtils = getIt.get(); + + static const EdgeInsetsDirectional snackBarPadding = + EdgeInsetsDirectional.symmetric( + horizontal: 16, + vertical: 14, + ); + + static double? widthSnackBar(BuildContext context) { + if (responsiveUtils.isWebDesktop(context)) { + return 334; + } else { + return null; + } + } +} + +class TwakeSnackBar { + static void show(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + width: TwakeSnackBarStyle.widthSnackBar(context), + padding: TwakeSnackBarStyle.snackBarPadding, + content: Text( + message, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.background, + ), + ), + ), + ); + } +} diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index 5d11408a3..9e5d394b4 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -34,9 +35,7 @@ class UrlLauncher { final uri = Uri.tryParse(url!); if (uri == null) { // we can't open this thing - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))), - ); + TwakeSnackBar.show(context, L10n.of(context)!.cantOpenUri(url!)); return; } if (!{'https', 'http'}.contains(uri.scheme)) { @@ -74,9 +73,7 @@ class UrlLauncher { return; } if (uri.host.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))), - ); + TwakeSnackBar.show(context, L10n.of(context)!.cantOpenUri(url!)); return; } // okay, we have either an http or an https URI. diff --git a/lib/utils/voip_plugin.dart b/lib/utils/voip_plugin.dart index a10414588..3085c9a03 100644 --- a/lib/utils/voip_plugin.dart +++ b/lib/utils/voip_plugin.dart @@ -1,5 +1,6 @@ import 'dart:core'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -165,13 +166,9 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate { addCallingOverlay(call.callId, call); try { if (!hasCallingAccount) { - ScaffoldMessenger.of(TwakeApp.routerKey.currentContext!) - .showSnackBar( - const SnackBar( - content: Text( - 'No calling accounts found (used for native calls UI)', - ), - ), + TwakeSnackBar.show( + TwakeApp.routerKey.currentContext!, + 'No calling accounts found (used for native calls UI)', ); } } catch (e) { diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 963c9cbb3..b7b5774b4 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -17,6 +17,7 @@ import 'package:fluffychat/domain/repository/tom_configurations_repository.dart' import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/localized_exception_extension.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'; @@ -346,10 +347,9 @@ class MatrixState extends State with WidgetsBindingObserver { _cancelSubs(c.clientName); widget.clients.remove(c); ClientManager.removeClientNameFromStore(c.clientName); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.oneClientLoggedOut), - ), + TwakeSnackBar.show( + TwakeApp.routerKey.currentContext!, + L10n.of(context)!.oneClientLoggedOut, ); if (state != LoginState.loggedIn) { From d76e92555e56f9e89342ed09b1b048b9256f7135 Mon Sep 17 00:00:00 2001 From: Julian KOUNE Date: Thu, 5 Oct 2023 16:42:07 +0200 Subject: [PATCH 58/60] feat: add spacing between messages bubbles --- lib/pages/chat/chat_event_list.dart | 2 +- lib/pages/chat/events/{ => message}/message.dart | 16 ++++++++++------ lib/pages/chat/events/message/message_style.dart | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) rename lib/pages/chat/events/{ => message}/message.dart (98%) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index eac742780..54c46182e 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -8,7 +8,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/events/message.dart'; +import 'package:fluffychat/pages/chat/events/message/message.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message/message.dart similarity index 98% rename from lib/pages/chat/events/message.dart rename to lib/pages/chat/events/message/message.dart index f48a585f3..49184d1d8 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message/message.dart @@ -1,12 +1,17 @@ import 'dart:math' as math; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_horizontal_action_menu.dart'; import 'package:fluffychat/pages/chat/context_item_chat_action.dart'; import 'package:fluffychat/pages/chat/events/message/message_style.dart'; +import 'package:fluffychat/pages/chat/events/message_content.dart'; import 'package:fluffychat/pages/chat/events/message_reactions.dart'; import 'package:fluffychat/pages/chat/events/message_time.dart'; +import 'package:fluffychat/pages/chat/events/reply_content.dart'; +import 'package:fluffychat/pages/chat/events/state_message.dart'; +import 'package:fluffychat/pages/chat/events/verification_request_content.dart'; import 'package:fluffychat/pages/chat/sticky_timstamp_widget.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; @@ -18,12 +23,6 @@ import 'package:flutter/services.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; -import '../../../config/app_config.dart'; -import 'message_content.dart'; -import 'reply_content.dart'; -import 'state_message.dart'; -import 'verification_request_content.dart'; - typedef OnMenuAction = Function(BuildContext, ChatHorizontalActionMenu, Event); class Message extends StatelessWidget { @@ -141,6 +140,11 @@ class Message extends StatelessWidget { Container( alignment: alignment, padding: EdgeInsetsDirectional.only( + top: MessageStyle.messageSpacing( + displayTime, + nextEvent, + event, + ), start: 8, end: selected || controller.responsive.isDesktop(context) ? 8 diff --git a/lib/pages/chat/events/message/message_style.dart b/lib/pages/chat/events/message/message_style.dart index 1a2add3fb..650100af1 100644 --- a/lib/pages/chat/events/message/message_style.dart +++ b/lib/pages/chat/events/message/message_style.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; +import 'package:matrix/matrix.dart'; class MessageStyle { static ResponsiveUtils responsiveUtils = getIt.get(); @@ -68,4 +69,19 @@ class MessageStyle { MediaQuery.of(context).size.width * messageBubbleMobileRatioMaxWidth, ); } + + static double messageSpacing( + bool displayTime, + Event? nextEvent, + Event currentEvent, + ) { + // add spaces to messages only + if (nextEvent == null || + displayTime || + nextEvent.type != EventTypes.Message) { + return 0; + } + + return currentEvent.senderId != nextEvent.senderId ? 8 : 4; + } } From c7af13483c6df0f8007cb7357fe45ef57f234b05 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 6 Oct 2023 14:42:18 +0700 Subject: [PATCH 59/60] hot-fix/remove black background when swipe video --- lib/pages/chat/events/message_content_style.dart | 2 -- .../mixins/play_video_action_mixin.dart | 5 ++++- lib/widgets/video_player.dart | 16 +++++----------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/pages/chat/events/message_content_style.dart b/lib/pages/chat/events/message_content_style.dart index a1496821e..51a35b7a5 100644 --- a/lib/pages/chat/events/message_content_style.dart +++ b/lib/pages/chat/events/message_content_style.dart @@ -66,7 +66,5 @@ class MessageContentStyle { static const BorderRadiusGeometry borderRadiusBubble = BorderRadius.all(Radius.circular(12.0)); - static const backgroundColorVideo = Colors.black; - static const backIconColor = Colors.white; } diff --git a/lib/presentation/mixins/play_video_action_mixin.dart b/lib/presentation/mixins/play_video_action_mixin.dart index 4cf415de7..a1f941b3b 100644 --- a/lib/presentation/mixins/play_video_action_mixin.dart +++ b/lib/presentation/mixins/play_video_action_mixin.dart @@ -16,7 +16,10 @@ mixin PlayVideoActionMixin { builder: (context) { return InteractiveViewerGallery( itemBuilder: PlatformInfos.isMobile - ? VideoViewerMobileTheme(path: uriOrFilePath) + ? VideoViewerMobileTheme( + path: uriOrFilePath, + eventId: eventId, + ) : VideoViewerDesktopTheme(path: uriOrFilePath), ); }, diff --git a/lib/widgets/video_player.dart b/lib/widgets/video_player.dart index d1d484e9e..2689bca7f 100644 --- a/lib/widgets/video_player.dart +++ b/lib/widgets/video_player.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:flutter/material.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; @@ -17,7 +16,6 @@ class VideoPlayer extends StatefulWidget { class _VideoPlayerState extends State { final VideoController videoController = VideoController(Player()); - bool isFullScreen = false; @override void initState() { @@ -33,15 +31,11 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - color: MessageContentStyle.backgroundColorVideo, - ), - child: Video( - pauseUponEnteringBackgroundMode: true, - resumeUponEnteringForegroundMode: true, - controller: videoController, - ), + return Video( + fill: Colors.transparent, + pauseUponEnteringBackgroundMode: true, + resumeUponEnteringForegroundMode: true, + controller: videoController, ); } } From f2109d786c97a56d8ca9f37f530431b8bc761e37 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Fri, 6 Oct 2023 16:54:38 +0700 Subject: [PATCH 60/60] Bump version to v2.3.2 --- CHANGELOG.md | 20 +++++++++++++++++--- pubspec.yaml | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3618aee..ae17ee658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1706,6 +1706,21 @@ Version 0.30.0 will be the first version with arm64 support. You can download bi This CHANGELOG.md was generated with [**Changelog for Dart**](https://pub.dartlang.org/packages/changelog) +## [2.3.2+2330] - 2023-10-06 + +### Added + +- New Settings screeen +- Load more error + +### Fixed + +- blink when scrolling chat +- UI chat wrong +- error in clear DB when logout in web +- drag n drop in web +- gesture to back + ## [2.3.1+2330] - 2023-09-29 ### Fixed @@ -1717,9 +1732,7 @@ Dart**](https://pub.dartlang.org/packages/changelog) - upload thumbnail and calculate blur hash ## [2.3.0+2330] - 2023-09-25 - ### Added - - Download file in all platform - Context menu for web - Search insie App @@ -1757,7 +1770,8 @@ Dart**](https://pub.dartlang.org/packages/changelog) - Reduce unnecessary API request to Profile API - Fix the placeholder for image to reduce memory consumption -[2.3.1+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.1 +[2.3.2+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.2 +[2.3.1+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.1 [2.3.0+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.0 [2.2.4+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.2.4 diff --git a/pubspec.yaml b/pubspec.yaml index dfe5f013e..b471a608a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fluffychat description: The open digital workplace. publish_to: none -version: 2.3.1+2330 +version: 2.3.2+2330 environment: sdk: '>=3.0.0 <4.0.0'