diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2d1614306b..65ced8e53c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,7 +6,7 @@ on: name: Build env: - FLUTTER_VERSION: 3.16.5 + FLUTTER_VERSION: 3.22.2 XCODE_VERSION: ^15.0.1 jobs: @@ -19,7 +19,7 @@ jobs: - os: android runner: ubuntu-latest - os: ios - runner: macos-13 + runner: macos-latest fail-fast: false steps: - name: Checkout repository @@ -49,7 +49,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: "temurin" # See 'Supported distributions' for available options - java-version: "11" + java-version: "17" - name: Select Xcode version if: matrix.os == 'ios' diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index 752a9acc41..a05f24d3f1 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -2,7 +2,7 @@ on: pull_request: env: - FLUTTER_VERSION: 3.16.5 + FLUTTER_VERSION: 3.22.2 LIBOLM_VERSION: 3.2.16 name: Deploying on GitHub Pages diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 87059d1728..1a47ab02d1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,7 +4,7 @@ on: - "v*.*.*" env: - FLUTTER_VERSION: 3.16.5 + FLUTTER_VERSION: 3.22.2 XCODE_VERSION: ^15.0.1 name: Release @@ -21,7 +21,7 @@ jobs: - os: android runner: ubuntu-latest - os: ios - runner: macos-13 + runner: macos-latest fail-fast: false steps: @@ -52,7 +52,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: "temurin" # See 'Supported distributions' for available options - java-version: "11" + java-version: "17" - name: Select Xcode version if: matrix.os == 'ios' diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a830f2f320..69d251ff18 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,7 +4,7 @@ on: name: Tests env: - FLUTTER_VERSION: 3.16.5 + FLUTTER_VERSION: 3.22.2 jobs: code_analyze: diff --git a/.gitignore b/.gitignore index ffacbe6231..b21c33414b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,10 @@ .history .svn/ prime + +#generated file *.g.dart +**/*.mocks.dart # libolm package /assets/js/package @@ -61,5 +64,4 @@ ios/Podfile.lock /linux/out /macos/out .vs -olm -test/interceptor/*.mocks.dart +olm \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bf37f9041d..2dcffc6a5a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - FLUTTER_VERSION: 3.16.5 + FLUTTER_VERSION: 3.22.2 image: name: cirrusci/flutter:${FLUTTER_VERSION} diff --git a/CHANGELOG.md b/CHANGELOG.md index 105086f2c2..19c3dc5b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,47 @@ -## [2.5.3+2330] - 2024-05-03 +## [2.5.9+2330] - 2024-06-18 + +### Fixed +- #1615 Not auto jump to lastest message when user send an attachment in case user seeing older message +- members list always have to reload +- #1793 Recent contacts aren't recent +- #1356 Rework account picker +- #1635 Irrelevant search results +- #1698 Group details is small in width +- #1855 hot-fix: fix the error l10n not found after build runner build +- #1841 Can't close invitation dialog in Chat +- #1787 Improve when leave group chat +- #1812 Remove X from the search bar when it's empty +- #1644 “Tap to allow access to your Gallery” button is not in center of its container +- #1602 When typing and shift + ctrl to get a line break, the internal mouse cursor disappears +- #1825 When adding members to a group chat, search results aren't displayed correctly +- #1791 Improve display contacts on multiple homeserver +- #1869 Can not open keyboard when tap on the textfield in IOS +- #1806 Images are displayed weirdly if they didn't yet load +- #1587 Can't open chat in chat list when click on notification +- #1777 Changing greeting message: include tag in greeting message + +### Added +- #1645 Missing counting selected image at send icon +- Update store metadata +- update of README.md +- #979 Improve tom bootstrap dialog +- #1836 Move the Shared media part to the Profile part + +## [2.5.8+2330] - 2024-05-25 +- #1781 Upgrade to Flutter SDK 3.22.0 + +## [2.5.5+2330] - 2024-05-25 + +### Fixed +- #1526 cannot unmute more than 2 chats +- #1785 Improve logout for multiple accounts + +### Added +- #1722 Recents contacts for contact/search/create group chat screen +- #1735 Search external contact in search screen +- #1780 add parameter `app=chat` when connect with TWP platform + +## [2.5.3+2330] - 2024-05-19 ### Fixed - #1650: Memory leak problems: Web and Mobile app diff --git a/Dockerfile b/Dockerfile index a3478cb6a0..8843fe8c2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,11 @@ # Specify versions -ARG FLUTTER_VERSION=3.16.5 -ARG OLM_VERSION=3.2.15 +ARG FLUTTER_VERSION=3.22.2 +ARG OLM_VERSION=3.2.16 +ARG NIX_VERSION=2.22.1 # Building libolm # libolm only has amd64 -FROM --platform=linux/amd64 nixos/nix AS olm-builder +FROM --platform=linux/amd64 nixos/nix:${NIX_VERSION} AS olm-builder ARG OLM_VERSION RUN nix build -v --extra-experimental-features flakes --extra-experimental-features nix-command gitlab:matrix-org/olm/${OLM_VERSION}?host=gitlab.matrix.org\#javascript @@ -15,7 +16,6 @@ WORKDIR /app RUN DEBIAN_FRONTEND=noninteractive apt update && \ apt install -y openssh-client && \ rm -rf assets/js/* && \ - mkdir ~/.ssh && \ ssh-keyscan github.com >> ~/.ssh/known_hosts COPY --from=olm-builder /result/javascript assets/js/package RUN --mount=type=ssh,required=true ./scripts/build-web.sh diff --git a/README.md b/README.md index e6c0949941..083454110c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Twake +# Twake Chat Client [![Contributors](https://img.shields.io/github/contributors/linagora/twake-on-matrix?label=Contributors )]( https://github.com/linagora/twake-on-matrix/graphs/contributors @@ -12,14 +12,21 @@
- + + +

twake-chat.com

+

+ Website + • View DemoReport Bug • + Roadmap + • Translate Twake

diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3207d8a0df..f58fd15636 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ + `surfaceContainerHighest`, `MaterialStateProperty` => `WidgetStateProperty`, `MaterialState` => `WidgetState`, use `super.key` for shorter form (new lint rule) +- Migration from `RawKeyEvent` to `KeyEvent`. [Read more](https://docs.flutter.dev/release/breaking-changes/key-event-migration#deprecated-apis-that-have-an-equivalent) (in conclusion, add ignore_deprecated, because that when i test it again, the up/down not work) +- Upgrade flutter_local_notification from `requestPermission` => `requestNotificationsPermission` [Changelog](https://pub.dev/packages/flutter_local_notifications/changelog#16001), [Readmore](https://developer.android.com/develop/ui/views/notifications/notification-permission?hl=vi) +- Upgrade `url_laucher`, change from `Uri` to `WebUri`, remove `ChromeSafariBrowserSettings` in web +- Upgrade `the index.html` file in web folder +- Upgrade other packages in pubspec.yaml to resolve conflicts \ No newline at end of file diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db27..5e31d3d342 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ fallbackValue, (success) => success is T ? success : fallbackValue, ); + + T? getFailureOrNull({T? fallbackValue}) => fold( + (failure) => failure is T ? failure : fallbackValue, + (success) => fallbackValue, + ); } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index a991e38881..09f84c35b5 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -40,8 +40,14 @@ abstract class AppConfig { ///`HOME_SERVER`: Homeserver, sample is `https://example.com` static String homeserver = sampleValue; + static String appParameter = 'chat'; + static String? platform; + static String _appPolicy = 'https://twake.app/policy'; + + static String appTermsOfUse = 'https://twake.app/terms'; + static double toolbarHeight(BuildContext context) => responsive.isMobile(context) ? 48 : 56; static const Color chatColor = primaryColor; @@ -52,9 +58,8 @@ abstract class AppConfig { static const Color primaryColor = Color.fromARGB(255, 135, 103, 172); static const Color primaryColorLight = Color(0xFFCCBDEA); static const Color secondaryColor = Color(0xFF41a2bc); - static String _privacyUrl = 'https://twake.app/en/privacy/'; - static String get privacyUrl => _privacyUrl; + static String get privacyUrl => _appPolicy; static const String enablePushTutorial = 'https://gitlab.com/famedly/fluffychat/-/wikis/Push-Notifications-without-Google-Services'; static const String encryptionTutorial = @@ -208,7 +213,7 @@ abstract class AppConfig { _webBaseUrl = json['privacy_url']; } if (json['web_base_url'] is String) { - _privacyUrl = json['web_base_url']; + _appPolicy = json['web_base_url']; } if (json['render_html'] is bool) { renderHtml = json['render_html']; diff --git a/lib/config/first_column_inner_routes.dart b/lib/config/first_column_inner_routes.dart index 7da75e08b8..faaca731da 100644 --- a/lib/config/first_column_inner_routes.dart +++ b/lib/config/first_column_inner_routes.dart @@ -2,7 +2,7 @@ import 'package:fluffychat/pages/new_group/new_group.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; import 'package:fluffychat/pages/search/search.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/cupertino.dart'; diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index d585d5dd32..808467d4c1 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -22,7 +22,7 @@ import 'package:fluffychat/pages/story/story_page.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; import 'package:fluffychat/presentation/model/chat/chat_router_input_argument.dart'; import 'package:fluffychat/presentation/model/forward/forward_argument.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart'; @@ -375,7 +375,11 @@ abstract class AppRoutes { redirect: loggedOutRedirect, pageBuilder: (context, state) => defaultPageBuilder( context, - const HomeserverPicker(), + TwakeWelcome( + arg: state.extra is TwakeWelcomeArg? + ? state.extra as TwakeWelcomeArg? + : null, + ), ), routes: [ GoRoute( @@ -386,21 +390,12 @@ abstract class AppRoutes { ), redirect: loggedOutRedirect, ), - GoRoute( - path: 'twakeWelcome', - pageBuilder: (context, state) => defaultPageBuilder( - context, - const TwakeWelcome(), - ), - redirect: loggedInRedirect, - ), GoRoute( path: 'homeserverpicker', pageBuilder: (context, state) => defaultPageBuilder( context, const HomeserverPicker(), ), - redirect: loggedInRedirect, ), ], ), diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 4f9cec6b6d..d474774b73 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -199,9 +199,13 @@ abstract class TwakeThemes { onSecondaryContainer: brightness == Brightness.light ? LinagoraSysColors.material().onSecondaryContainer : LinagoraSysColors.material().onSecondaryContainerDark, + // TODO: remove when the color scheme is updated + // ignore: deprecated_member_use background: brightness == Brightness.light ? LinagoraSysColors.material().background : LinagoraSysColors.material().backgroundDark, + // TODO: remove when the color scheme is updated + // ignore: deprecated_member_use onBackground: brightness == Brightness.light ? LinagoraSysColors.material().onBackground : LinagoraSysColors.material().onBackgroundDark, @@ -226,7 +230,7 @@ abstract class TwakeThemes { surfaceTint: brightness == Brightness.light ? LinagoraSysColors.material().surfaceTint : LinagoraSysColors.material().surfaceTintDark, - surfaceVariant: brightness == Brightness.light + surfaceContainerHighest: brightness == Brightness.light ? LinagoraSysColors.material().surfaceVariant : LinagoraSysColors.material().surfaceVariantDark, onSurfaceVariant: brightness == Brightness.light @@ -252,8 +256,8 @@ abstract class TwakeThemes { ), iconButtonTheme: IconButtonThemeData( style: ButtonStyle( - iconSize: MaterialStateProperty.all(iconSize), - iconColor: MaterialStateProperty.all( + iconSize: WidgetStateProperty.all(iconSize), + iconColor: WidgetStateProperty.all( brightness == Brightness.light ? LinagoraSysColors.material().onSurface : LinagoraSysColors.material().onSurfaceDark, @@ -268,9 +272,9 @@ abstract class TwakeThemes { ), switchTheme: SwitchThemeData( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - overlayColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.selected)) { + overlayColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.selected)) { return brightness == Brightness.light ? LinagoraSysColors.material().primary : LinagoraSysColors.material().primaryDark; @@ -281,9 +285,9 @@ abstract class TwakeThemes { } }, ), - thumbColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.selected)) { + thumbColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.selected)) { return brightness == Brightness.light ? LinagoraSysColors.material().onPrimary : LinagoraSysColors.material().onPrimaryDark; @@ -294,9 +298,9 @@ abstract class TwakeThemes { } }, ), - trackColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.selected)) { + trackColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.selected)) { return brightness == Brightness.light ? LinagoraSysColors.material().primary : LinagoraSysColors.material().primaryDark; diff --git a/lib/data/datasource/tom_configurations_datasource.dart b/lib/data/datasource/tom_configurations_datasource.dart index a39ae18400..6cfac3820c 100644 --- a/lib/data/datasource/tom_configurations_datasource.dart +++ b/lib/data/datasource/tom_configurations_datasource.dart @@ -7,4 +7,6 @@ abstract class ToMConfigurationsDatasource { String userId, ToMConfigurations toMConfigurations, ); + + Future deleteTomConfigurations(String userId); } diff --git a/lib/data/datasource_impl/tom_configurations_datasource_impl.dart b/lib/data/datasource_impl/tom_configurations_datasource_impl.dart index b6914654ed..2a51dc247a 100644 --- a/lib/data/datasource_impl/tom_configurations_datasource_impl.dart +++ b/lib/data/datasource_impl/tom_configurations_datasource_impl.dart @@ -46,4 +46,10 @@ class HiveToMConfigurationDatasource implements ToMConfigurationsDatasource { .toJson(), ); } + + @override + Future deleteTomConfigurations(String userId) { + final hiveCollectionToMDatabase = getIt.get(); + return hiveCollectionToMDatabase.tomConfigurationsBox.delete(userId); + } } diff --git a/lib/data/network/exception/dio_duplicate_download_exception.dart b/lib/data/network/exception/dio_duplicate_download_exception.dart index b9ad49d8d2..c2a8ee41fc 100644 --- a/lib/data/network/exception/dio_duplicate_download_exception.dart +++ b/lib/data/network/exception/dio_duplicate_download_exception.dart @@ -2,10 +2,9 @@ import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; class DioDuplicateDownloadException extends DioException with EquatableMixin { - DioDuplicateDownloadException({required RequestOptions requestOptions}) + DioDuplicateDownloadException({required super.requestOptions}) : super( message: 'Download already in progress', - requestOptions: requestOptions, type: DioExceptionType.unknown, ); diff --git a/lib/data/network/extensions/file_info_extension.dart b/lib/data/network/extensions/file_info_extension.dart index 7ba5ae3005..fa1ffa2a30 100644 --- a/lib/data/network/extensions/file_info_extension.dart +++ b/lib/data/network/extensions/file_info_extension.dart @@ -1,13 +1,10 @@ -import 'package:flutter/foundation.dart'; +import 'package:fluffychat/utils/mime_type_uitls.dart'; import 'package:matrix/matrix.dart'; -import 'package:mime/mime.dart'; extension FileInfoExtension on FileInfo { String get fileExtension => fileName.split('.').last; - String get mimeType => - lookupMimeType(kIsWeb ? fileName : filePath) ?? - 'application/octet-stream'; + String get mimeType => MimeTypeUitls.instance.getTwakeMimeType(filePath); Map get metadata => ({ 'mimetype': mimeType, diff --git a/lib/data/repository/contact/tom_contact_repository_impl.dart b/lib/data/repository/contact/tom_contact_repository_impl.dart index 0abbb32e13..ef0a03b30d 100644 --- a/lib/data/repository/contact/tom_contact_repository_impl.dart +++ b/lib/data/repository/contact/tom_contact_repository_impl.dart @@ -11,18 +11,16 @@ class TomContactRepositoryImpl implements ContactRepository { TomContactRepositoryImpl(); @override - Stream> fetchContacts({ + Future> fetchContacts({ required ContactQuery query, int? limit, int? offset, - }) async* { - final response = await datasource.fetchContacts( + }) async { + return datasource.fetchContacts( query: query, limit: limit, offset: offset, ); - - yield response; } @override diff --git a/lib/data/repository/tom_configurations_repository_impl.dart b/lib/data/repository/tom_configurations_repository_impl.dart index 4a8bcdc115..9ddecd06f0 100644 --- a/lib/data/repository/tom_configurations_repository_impl.dart +++ b/lib/data/repository/tom_configurations_repository_impl.dart @@ -22,4 +22,9 @@ class ToMConfigurationsRepositoryImpl implements ToMConfigurationsRepository { toMConfigurations, ); } + + @override + Future deleteTomConfigurations(String userId) { + return tomConfigurationsDatasource.deleteTomConfigurations(userId); + } } diff --git a/lib/domain/app_state/contact/get_contacts_state.dart b/lib/domain/app_state/contact/get_contacts_state.dart index 59b2811991..66976f4645 100644 --- a/lib/domain/app_state/contact/get_contacts_state.dart +++ b/lib/domain/app_state/contact/get_contacts_state.dart @@ -28,6 +28,13 @@ class GetContactsSuccess extends Success { List get props => [contacts]; } +class GetContactsIsEmpty extends Failure { + const GetContactsIsEmpty(); + + @override + List get props => []; +} + class GetContactsFailure extends Failure { final String keyword; final dynamic exception; diff --git a/lib/domain/app_state/contact/get_phonebook_contacts_state.dart b/lib/domain/app_state/contact/get_phonebook_contacts_state.dart index 71de9144e1..707da157bf 100644 --- a/lib/domain/app_state/contact/get_phonebook_contacts_state.dart +++ b/lib/domain/app_state/contact/get_phonebook_contacts_state.dart @@ -28,6 +28,13 @@ class GetPhonebookContactsSuccess extends Success { List get props => [contacts]; } +class GetPhonebookContactsIsEmpty extends Failure { + const GetPhonebookContactsIsEmpty(); + + @override + List get props => []; +} + class GetPhonebookContactsFailure extends Failure { final dynamic exception; diff --git a/lib/domain/app_state/search/pre_search_state.dart b/lib/domain/app_state/search/pre_search_state.dart index f4c77b46cd..4919916a82 100644 --- a/lib/domain/app_state/search/pre_search_state.dart +++ b/lib/domain/app_state/search/pre_search_state.dart @@ -3,12 +3,12 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:matrix/matrix.dart'; class PreSearchRecentContactsSuccess extends Success { - final List users; + final List rooms; - const PreSearchRecentContactsSuccess({required this.users}); + const PreSearchRecentContactsSuccess({required this.rooms}); @override - List get props => [users]; + List get props => [rooms]; } class PreSearchRecentContactsFailed extends Failure { diff --git a/lib/domain/contact_manager/contacts_manager.dart b/lib/domain/contact_manager/contacts_manager.dart index fe2dae4d70..81a86b2fdd 100644 --- a/lib/domain/contact_manager/contacts_manager.dart +++ b/lib/domain/contact_manager/contacts_manager.dart @@ -6,7 +6,7 @@ import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart'; import 'package:fluffychat/domain/app_state/contact/get_phonebook_contacts_state.dart'; import 'package:fluffychat/domain/usecase/contacts/get_tom_contacts_interactor.dart'; import 'package:fluffychat/domain/usecase/contacts/phonebook_contact_interactor.dart'; -import 'package:flutter/foundation.dart'; +import 'package:fluffychat/presentation/extensions/value_notifier_custom.dart'; class ContactsManager { static const int _lookupChunkSize = 50; @@ -17,24 +17,25 @@ class ContactsManager { bool _doNotShowWarningContactsBannerAgain = false; - final ValueNotifier> _contactsNotifier = - ValueNotifier(const Right(ContactsInitial())); + final ValueNotifierCustom> _contactsNotifier = + ValueNotifierCustom(const Right(ContactsInitial())); - final ValueNotifier> _phonebookContactsNotifier = - ValueNotifier(const Right(GetPhonebookContactsInitial())); + final ValueNotifierCustom> + _phonebookContactsNotifier = + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())); ContactsManager({ required this.getTomContactsInteractor, required this.phonebookContactInteractor, }); - ValueNotifier> getContactsNotifier() => + ValueNotifierCustom> getContactsNotifier() => _contactsNotifier; - ValueNotifier> getPhonebookContactsNotifier() => - _phonebookContactsNotifier; + ValueNotifierCustom> + getPhonebookContactsNotifier() => _phonebookContactsNotifier; - bool get _isInitial => + bool get _isSynchronizedTomContacts => _contactsNotifier.value.getSuccessOrNull() != null; bool get isDoNotShowWarningContactsBannerAgain => @@ -44,10 +45,16 @@ class ContactsManager { _doNotShowWarningContactsBannerAgain = value; } + Future reSyncContacts() async { + _contactsNotifier.value = const Right(ContactsInitial()); + _phonebookContactsNotifier.value = + const Right(GetPhonebookContactsInitial()); + } + void initialSynchronizeContacts({ bool isAvailableSupportPhonebookContacts = false, }) async { - if (!_isInitial) { + if (!_isSynchronizedTomContacts) { return; } _getAllContacts( @@ -76,6 +83,7 @@ class ContactsManager { if (!isAvailableSupportPhonebookContacts) { return; } + phonebookContactInteractor .execute(lookupChunkSize: _lookupChunkSize) .listen( diff --git a/lib/domain/model/extensions/platform_file/platform_file_extension.dart b/lib/domain/model/extensions/platform_file/platform_file_extension.dart index 74da8aa8f9..99ce7910de 100644 --- a/lib/domain/model/extensions/platform_file/platform_file_extension.dart +++ b/lib/domain/model/extensions/platform_file/platform_file_extension.dart @@ -1,4 +1,5 @@ import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/utils/mime_type_uitls.dart'; import 'package:matrix/matrix.dart'; extension PlatformFileListExtension on PlatformFile { @@ -9,6 +10,7 @@ extension PlatformFileListExtension on PlatformFile { bytes: bytes, name: name, filePath: path ?? '$temporaryDirectoryPath/$name', + mimeType: MimeTypeUitls.instance.getTwakeMimeType(name), readStream: readStream, sizeInBytes: size, ); @@ -21,6 +23,7 @@ extension PlatformFileListExtension on PlatformFile { filePath: '', readStream: readStream, sizeInBytes: size, + mimeType: MimeTypeUitls.instance.getTwakeMimeType(name), ); } 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 0b3cd1371c..6c3acbd2f3 100644 --- a/lib/domain/model/preview_file/supported_preview_file_types.dart +++ b/lib/domain/model/preview_file/supported_preview_file_types.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; enum SupportedIconFileTypesEnum { image, @@ -40,14 +41,38 @@ class SupportedPreviewFileTypes { static const imageMimeTypes = [ 'image/bmp', 'image/jpeg', + 'image/jpg', 'image/gif', 'image/png', ]; + static const imageMimeTypesAndroid = [ + ...imageMimeTypes, + 'image/webp', + ]; + + static const imageMimeTypesIOS = [ + ...imageMimeTypes, + 'image/heic', + 'image/heif', + ]; + + static List get crossPlatformImageMimeTypes { + if (PlatformInfos.isAndroid) { + return imageMimeTypesAndroid; + } else if (PlatformInfos.isIOS) { + return imageMimeTypesIOS; + } else { + return imageMimeTypes; + } + } + static const videoMimeTypes = [ 'video/mp4', 'video/3gpp', 'video/quicktime', + 'video/mov', + 'video/mpeg', ]; static const audioMimeTypes = [ diff --git a/lib/domain/model/search/contact_search_model.dart b/lib/domain/model/search/contact_search_model.dart index 1070211340..fbafbdd969 100644 --- a/lib/domain/model/search/contact_search_model.dart +++ b/lib/domain/model/search/contact_search_model.dart @@ -8,10 +8,8 @@ class ContactSearchModel extends SearchModel { const ContactSearchModel( this.matrixId, this.email, { - String? displayName, - }) : super( - displayName: displayName, - ); + super.displayName, + }); @override String get id => matrixId ?? email ?? ''; diff --git a/lib/domain/model/search/recent_chat_model.dart b/lib/domain/model/search/recent_chat_model.dart index d80111b8b0..e0b479619a 100644 --- a/lib/domain/model/search/recent_chat_model.dart +++ b/lib/domain/model/search/recent_chat_model.dart @@ -9,12 +9,9 @@ class RecentChatSearchModel extends SearchModel { const RecentChatSearchModel( this.roomId, { this.roomSummary, - String? displayName, - String? directChatMatrixID, - }) : super( - displayName: displayName, - directChatMatrixID: directChatMatrixID, - ); + super.displayName, + super.directChatMatrixID, + }); @override String get id => roomId ?? ''; diff --git a/lib/domain/repository/contact_repository.dart b/lib/domain/repository/contact_repository.dart index b1c87464da..e282e35b71 100644 --- a/lib/domain/repository/contact_repository.dart +++ b/lib/domain/repository/contact_repository.dart @@ -3,7 +3,7 @@ import 'package:fluffychat/domain/model/contact/contact_query.dart'; import 'package:fluffychat/domain/model/contact/lookup_mxid_request.dart'; abstract class ContactRepository { - Stream> fetchContacts({ + Future> fetchContacts({ required ContactQuery query, int? limit, int? offset, diff --git a/lib/domain/repository/tom_configurations_repository.dart b/lib/domain/repository/tom_configurations_repository.dart index 673f1ee6ce..de71cb74bc 100644 --- a/lib/domain/repository/tom_configurations_repository.dart +++ b/lib/domain/repository/tom_configurations_repository.dart @@ -7,4 +7,6 @@ abstract class ToMConfigurationsRepository { String userId, ToMConfigurations toMConfigurations, ); + + Future deleteTomConfigurations(String userId); } diff --git a/lib/domain/usecase/contacts/get_tom_contacts_interactor.dart b/lib/domain/usecase/contacts/get_tom_contacts_interactor.dart index fbe2298b68..a9294c4b91 100644 --- a/lib/domain/usecase/contacts/get_tom_contacts_interactor.dart +++ b/lib/domain/usecase/contacts/get_tom_contacts_interactor.dart @@ -16,16 +16,16 @@ class GetTomContactsInteractor { }) async* { try { yield const Right(ContactsLoading()); - yield* contactRepository - .fetchContacts( + final response = await contactRepository.fetchContacts( query: ContactQuery(keyword: ''), limit: limit, - ) - .map((contacts) { - return Right( - GetContactsSuccess(contacts: contacts), - ); - }); + ); + + if (response.isEmpty) { + yield const Left(GetContactsIsEmpty()); + } else { + yield Right(GetContactsSuccess(contacts: response)); + } } catch (e) { yield Left(GetContactsFailure(keyword: '', exception: e)); } diff --git a/lib/domain/usecase/contacts/phonebook_contact_interactor.dart b/lib/domain/usecase/contacts/phonebook_contact_interactor.dart index b7f046a486..314a62f54c 100644 --- a/lib/domain/usecase/contacts/phonebook_contact_interactor.dart +++ b/lib/domain/usecase/contacts/phonebook_contact_interactor.dart @@ -44,7 +44,11 @@ class PhonebookContactInteractor { thirdPartyIdToHashMap.values.whereNotNull().slices(lookupChunkSize); if (chunks.isEmpty) { - yield Right(GetPhonebookContactsSuccess(contacts: contacts)); + if (contacts.isEmpty) { + yield const Left(GetPhonebookContactsIsEmpty()); + } else { + yield Right(GetPhonebookContactsSuccess(contacts: contacts)); + } return; } @@ -85,7 +89,11 @@ class PhonebookContactInteractor { return contact; }).toList(); - yield Right(GetPhonebookContactsSuccess(contacts: lookupContacts)); + if (lookupContacts.isEmpty) { + yield const Left(GetPhonebookContactsIsEmpty()); + } else { + yield Right(GetPhonebookContactsSuccess(contacts: lookupContacts)); + } } catch (e) { Logs().e('PhonebookContactInteractor::error', e); yield Left(GetPhonebookContactsFailure(exception: e)); diff --git a/lib/domain/usecase/search/pre_search_recent_contacts_interactor.dart b/lib/domain/usecase/search/pre_search_recent_contacts_interactor.dart index 852c0c9b9d..d07e27a12c 100644 --- a/lib/domain/usecase/search/pre_search_recent_contacts_interactor.dart +++ b/lib/domain/usecase/search/pre_search_recent_contacts_interactor.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:dartz/dartz.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; @@ -8,43 +10,19 @@ class PreSearchRecentContactsInteractor { PreSearchRecentContactsInteractor(); Stream> execute({ - required List recentRooms, - int? limit, + required List allRooms, + int limit = 5, }) async* { try { - final List result = []; - - for (final room in recentRooms) { - final users = room - .getParticipants() - .where( - (user) => - user.membership.isInvite == true && user.displayName != null, - ) - .toSet(); - - for (final user in users) { - final isDuplicateUser = - result.any((existingUser) => existingUser.id == user.id); - - if (!isDuplicateUser) { - result.add(user); - } - - if (result.length == limit) { - break; - } - } - - if (result.length == limit) { - break; - } - } - if (result.isEmpty) { + final directRooms = allRooms.where((room) => room.isDirectChat).toList(); + if (directRooms.isEmpty) { yield const Left(PreSearchRecentContactsEmpty()); - } else { - yield Right(PreSearchRecentContactsSuccess(users: result)); + return; } + final recentRooms = + directRooms.getRange(0, min(directRooms.length, limit)).toList(); + + yield Right(PreSearchRecentContactsSuccess(rooms: recentRooms)); } catch (e) { yield Left(PreSearchRecentContactsFailed(exception: e)); } diff --git a/lib/pages/add_story/add_story.dart b/lib/pages/add_story/add_story.dart index 25c49df8d6..84b84a4cfc 100644 --- a/lib/pages/add_story/add_story.dart +++ b/lib/pages/add_story/add_story.dart @@ -21,7 +21,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/matrix_sdk_extensions/client_stories_extension.dart'; class AddStoryPage extends StatefulWidget { - const AddStoryPage({Key? key}) : super(key: key); + const AddStoryPage({super.key}); @override AddStoryController createState() => AddStoryController(); diff --git a/lib/pages/add_story/add_story_view.dart b/lib/pages/add_story/add_story_view.dart index 3d9eac89df..26df156bf0 100644 --- a/lib/pages/add_story/add_story_view.dart +++ b/lib/pages/add_story/add_story_view.dart @@ -8,7 +8,7 @@ import 'add_story.dart'; class AddStoryView extends StatelessWidget { final AddStoryController controller; - const AddStoryView(this.controller, {Key? key}) : super(key: key); + const AddStoryView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/add_story/invite_story_page.dart b/lib/pages/add_story/invite_story_page.dart index 91bbd5796f..44863b391a 100644 --- a/lib/pages/add_story/invite_story_page.dart +++ b/lib/pages/add_story/invite_story_page.dart @@ -15,8 +15,8 @@ class InviteStoryPage extends StatefulWidget { final Room? storiesRoom; const InviteStoryPage({ required this.storiesRoom, - Key? key, - }) : super(key: key); + super.key, + }); @override InviteStoryPageState createState() => InviteStoryPageState(); diff --git a/lib/pages/app_grid/app_grid_dashboard_item.dart b/lib/pages/app_grid/app_grid_dashboard_item.dart index c3685abf18..bfccf45e74 100644 --- a/lib/pages/app_grid/app_grid_dashboard_item.dart +++ b/lib/pages/app_grid/app_grid_dashboard_item.dart @@ -11,7 +11,7 @@ class AppGridDashboardItem extends StatelessWidget { final LinagoraApp app; - const AppGridDashboardItem(this.app, {Key? key}) : super(key: key); + const AppGridDashboardItem(this.app, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/app_grid/app_grid_dashboard_overlay.dart b/lib/pages/app_grid/app_grid_dashboard_overlay.dart index ca7a14623b..fadbd07bfe 100644 --- a/lib/pages/app_grid/app_grid_dashboard_overlay.dart +++ b/lib/pages/app_grid/app_grid_dashboard_overlay.dart @@ -6,8 +6,7 @@ import 'package:flutter/material.dart'; class AppGridDashboardOverlay extends StatelessWidget { final LinagoraApplications _linagoraApplications; - const AppGridDashboardOverlay(this._linagoraApplications, {Key? key}) - : super(key: key); + const AppGridDashboardOverlay(this._linagoraApplications, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/archive/archive.dart b/lib/pages/archive/archive.dart index 03f00b626a..c3f9903d97 100644 --- a/lib/pages/archive/archive.dart +++ b/lib/pages/archive/archive.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/pages/archive/archive_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; class Archive extends StatefulWidget { - const Archive({Key? key}) : super(key: key); + const Archive({super.key}); @override ArchiveController createState() => ArchiveController(); diff --git a/lib/pages/archive/archive_view.dart b/lib/pages/archive/archive_view.dart index 05758424b9..841664a601 100644 --- a/lib/pages/archive/archive_view.dart +++ b/lib/pages/archive/archive_view.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; class ArchiveView extends StatelessWidget { final ArchiveController controller; - const ArchiveView(this.controller, {Key? key}) : super(key: key); + const ArchiveView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index bca0fa0442..0e51f627f1 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -21,10 +21,10 @@ class BootstrapDialog extends StatefulWidget { final Client client; const BootstrapDialog({ - Key? key, + super.key, this.wipe = false, required this.client, - }) : super(key: key); + }); Future show() => PlatformInfos.isCupertinoStyle ? TwakeDialog.showCupertinoDialogFullScreen( @@ -106,7 +106,7 @@ class BootstrapDialogState extends State { centerTitle: true, leading: IconButton( icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, + onPressed: () => Navigator.of(context).pop(false), ), title: Text(L10n.of(context)!.recoveryKey), ), @@ -233,7 +233,7 @@ class BootstrapDialogState extends State { centerTitle: true, leading: IconButton( icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, + onPressed: () => Navigator.of(context).pop(false), ), title: Text(L10n.of(context)!.chatBackup), ), @@ -372,8 +372,6 @@ class BootstrapDialogState extends State { isDestructiveAction: true, )) { await TomBootstrapDialog( - wipe: true, - wipeRecovery: true, client: widget.client, ).show().then( (value) => Navigator.of( @@ -426,12 +424,17 @@ class BootstrapDialogState extends State { ); break; case BootstrapState.done: - titleText = L10n.of(context)!.everythingReady; + titleText = null; body = Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset('assets/backup.png', fit: BoxFit.contain), - Text(L10n.of(context)!.yourChatBackupHasBeenSetUp), + Flexible( + child: Text( + L10n.of(context)!.yourChatBackupHasBeenSetUp, + textAlign: TextAlign.center, + ), + ), ], ); buttons.add( @@ -448,16 +451,19 @@ class BootstrapDialogState extends State { return AlertDialog( content: Row( children: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: body, - ), Expanded( - child: Text( - titleText!, - overflow: TextOverflow.ellipsis, + child: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: body, ), ), + if (titleText != null) + Expanded( + child: Text( + titleText!, + overflow: TextOverflow.ellipsis, + ), + ), ], ), actions: buttons.isNotEmpty ? buttons : null, diff --git a/lib/pages/bootstrap/init_client_dialog.dart b/lib/pages/bootstrap/init_client_dialog.dart new file mode 100644 index 0000000000..8636934b0a --- /dev/null +++ b/lib/pages/bootstrap/init_client_dialog.dart @@ -0,0 +1,137 @@ +import 'dart:async'; +import 'package:fluffychat/presentation/model/client_login_state_event.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class InitClientDialog extends StatefulWidget { + final Future Function() future; + + const InitClientDialog({ + super.key, + required this.future, + }); + + @override + State createState() => _InitClientDialogState(); +} + +class _InitClientDialogState extends State + with TickerProviderStateMixin { + late AnimationController loginSSOProgressController; + + Client? _clientFirstLoggedIn; + + Client? _clientAddAnotherAccount; + + StreamSubscription? _clientLoginStateChangedSubscription; + + @override + void initState() { + _initial(); + _clientLoginStateChangedSubscription = + Matrix.of(context).onClientLoginStateChanged.stream.listen( + _listenClientLoginStateChanged, + ); + WidgetsBinding.instance.addPostFrameCallback( + (_) async { + _startLoginSSOProgress(); + await widget + .future() + .then( + (_) => _handleFunctionOnDone(), + ) + .onError( + (error, _) => _handleFunctionOnError(error), + ); + }, + ); + + super.initState(); + } + + void _listenClientLoginStateChanged(ClientLoginStateEvent event) { + Logs().i( + 'StreamDialogBuilder::_listenClientLoginStateChanged - ${event.multipleAccountLoginType}', + ); + if (event.multipleAccountLoginType == + MultipleAccountLoginType.firstLoggedIn) { + _clientFirstLoggedIn = event.client; + return; + } + + if (event.multipleAccountLoginType == + MultipleAccountLoginType.otherAccountLoggedIn) { + _clientAddAnotherAccount = event.client; + return; + } + } + + void _handleFunctionOnDone() async { + Logs().i('StreamDialogBuilder::_handleFunctionOnDone'); + Navigator.of(context, rootNavigator: false).pop(); + if (_clientFirstLoggedIn != null) { + _handleFirstLoggedIn(_clientFirstLoggedIn!); + return; + } + + if (_clientAddAnotherAccount != null) { + _handleAddAnotherAccount(_clientAddAnotherAccount!); + return; + } + } + + void _handleFunctionOnError(Object? error) { + Logs().e('StreamDialogBuilder::_handleFunctionOnError - $error'); + Navigator.pop(context); + } + + void _handleFirstLoggedIn(Client client) { + TwakeApp.router.go( + '/rooms', + extra: LoggedInBodyArgs( + newActiveClient: client, + ), + ); + } + + void _handleAddAnotherAccount(Client client) { + TwakeApp.router.go( + '/rooms', + extra: LoggedInOtherAccountBodyArgs( + newActiveClient: client, + ), + ); + } + + void _initial() { + loginSSOProgressController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + ); + } + + void _startLoginSSOProgress() { + loginSSOProgressController.addListener(() { + setState(() {}); + }); + loginSSOProgressController.repeat(); + } + + @override + void dispose() { + loginSSOProgressController.dispose(); + _clientLoginStateChangedSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + backgroundColor: Colors.transparent, + ); + } +} diff --git a/lib/pages/bootstrap/tom_bootstrap_dialog.dart b/lib/pages/bootstrap/tom_bootstrap_dialog.dart index 02f327f93a..3ae3a9d612 100644 --- a/lib/pages/bootstrap/tom_bootstrap_dialog.dart +++ b/lib/pages/bootstrap/tom_bootstrap_dialog.dart @@ -1,80 +1,147 @@ +import 'package:fluffychat/di/global/dio_cache_interceptor_for_client.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; import 'package:fluffychat/domain/usecase/recovery/delete_recovery_words_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; import 'package:fluffychat/domain/usecase/recovery/save_recovery_words_interactor.dart'; +import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog_style.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/widgets/adaptive_flat_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:lottie/lottie.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/encryption/utils/bootstrap.dart'; import 'package:matrix/matrix.dart'; -import 'bootstrap_dialog.dart'; - class TomBootstrapDialog extends StatefulWidget { - final bool wipe; - final bool wipeRecovery; final Client client; - final RecoveryWords? recoveryWords; const TomBootstrapDialog({ - Key? key, - this.recoveryWords, - this.wipe = false, - this.wipeRecovery = false, + super.key, required this.client, - }) : super(key: key); + }); Future show() => TwakeDialog.showDialogFullScreen( builder: () => this, + barrierColor: TomBootstrapDialogStyle.barrierColor, ); @override TomBootstrapDialogState createState() => TomBootstrapDialogState(); } -class TomBootstrapDialogState extends State { +class TomBootstrapDialogState extends State + with TickerProviderStateMixin { final _saveRecoveryWordsInteractor = getIt.get(); + + final _getRecoveryWordsInteractor = getIt.get(); + final _deleteRecoveryWordsInteractor = getIt.get(); Bootstrap? bootstrap; - String? titleText; - Widget? body; - final buttons = []; - UploadRecoveryKeyState _uploadRecoveryKeyState = - UploadRecoveryKeyState.initial; + UploadRecoveryKeyState.dataLoading; - bool? _wipe; + bool _wipe = false; + RecoveryWords? _recoveryWords; @override void initState() { super.initState(); - _createBootstrap(widget.wipe); + bootstrap = + widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); + _createBootstrap(); } - void _createBootstrap(bool wipe) async { - _wipe = wipe; - titleText = null; - _uploadRecoveryKeyState = _initializeRecoveryKeyState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - bootstrap = - widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); + void _createBootstrap() async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _loadingData(); }); } - UploadRecoveryKeyState _initializeRecoveryKeyState() { - if (widget.wipeRecovery) { - return UploadRecoveryKeyState.wipeRecovery; + Future setupAdditionalDioCacheOption(String userId) async { + Logs().d('TomBootstrapDialog::setupAdditionalDioCacheOption: $userId'); + DioCacheInterceptorForClient(userId).setup(getIt); + } + + Future _getRecoveryWords() async { + final result = await _getRecoveryWordsInteractor.execute(); + return result.fold( + (failure) => null, + (success) => success.words, + ); + } + + Future _loadingData() async { + _uploadRecoveryKeyState = UploadRecoveryKeyState.dataLoading; + await widget.client.roomsLoading; + await widget.client.accountDataLoading; + if (widget.client.userID != null) { + await setupAdditionalDioCacheOption(widget.client.userID!); } + setState(() { + _uploadRecoveryKeyState = UploadRecoveryKeyState.checkingRecoveryWork; + }); + await _getRecoveryKeyState(); + } - if (widget.recoveryWords != null) { - return UploadRecoveryKeyState.useExisting; + Future _getRecoveryKeyState() async { + await widget.client.onSync.stream.first; + await widget.client.initCompleter?.future; + + // Display first login bootstrap if enabled + if (widget.client.encryption?.keyManager.enabled == true) { + Logs().d( + 'TomBootstrapDialog::_initializeRecoveryKeyState: Showing bootstrap dialog when encryption is enabled', + ); + if (await widget.client.encryption?.keyManager.isCached() == false || + await widget.client.encryption?.crossSigning.isCached() == false || + widget.client.isUnknownSession && mounted) { + final recoveryWords = await _getRecoveryWords(); + if (recoveryWords != null) { + _recoveryWords = recoveryWords; + _uploadRecoveryKeyState = UploadRecoveryKeyState.useExisting; + setState(() {}); + return; + } else { + Logs().d( + 'TomBootstrapDialog::_initializeRecoveryKeyState(): no recovery existed then call bootstrap', + ); + Navigator.of(context, rootNavigator: false).pop(false); + } + } + } else { + Logs().d( + 'TomBootstrapDialog::_initializeRecoveryKeyState(): encryption is not enabled', + ); + final recoveryWords = await _getRecoveryWords(); + _wipe = recoveryWords != null; + if (recoveryWords != null) { + _uploadRecoveryKeyState = UploadRecoveryKeyState.wipeRecovery; + } else { + _uploadRecoveryKeyState = UploadRecoveryKeyState.initial; + } + setState(() {}); + return; } + } + + bool get isDataLoadingState => + _uploadRecoveryKeyState == UploadRecoveryKeyState.dataLoading; + + bool get isCheckingRecoveryWorkState => + _uploadRecoveryKeyState == UploadRecoveryKeyState.checkingRecoveryWork; - return UploadRecoveryKeyState.initial; + String get _description { + if (isDataLoadingState) { + return L10n.of(context)!.backingUpYourMessage; + } else if (isCheckingRecoveryWorkState) { + return L10n.of(context)!.configureDataEncryption; + } else { + return L10n.of(context)!.recoveringYourEncryptedChats; + } } @override @@ -82,31 +149,22 @@ class TomBootstrapDialogState extends State { Logs().d( 'TomBootstrapDialogState::build(): BootstrapState = ${bootstrap?.state}', ); - _wipe ??= widget.wipe; - body = _loadingContent(context); + + Logs().d( + 'TomBootstrapDialogState::build(): RecoveryKeyState = $_uploadRecoveryKeyState', + ); switch (_uploadRecoveryKeyState) { + case UploadRecoveryKeyState.dataLoading: + break; + case UploadRecoveryKeyState.checkingRecoveryWork: + break; case UploadRecoveryKeyState.wipeRecovery: WidgetsBinding.instance.addPostFrameCallback((_) { _wipeRecoveryWord(); }); break; case UploadRecoveryKeyState.wipeRecoveryFailed: - titleText = L10n.of(context)!.chatBackup; - body = Text( - L10n.of(context)!.cannotEnableKeyBackup, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ); break; case UploadRecoveryKeyState.created: if (_createNewRecoveryKeySuccess()) { @@ -118,7 +176,7 @@ class TomBootstrapDialogState extends State { Logs().d( 'TomBootstrapDialogState::build(): check if key is already in TOM = ${_existedRecoveryWordsInTom( key, - )} - ${widget.recoveryWords?.words}', + )} - ${_recoveryWords?.words}', ); if (_existedRecoveryWordsInTom(key)) { _uploadRecoveryKeyState = UploadRecoveryKeyState.uploaded; @@ -135,69 +193,68 @@ class TomBootstrapDialogState extends State { _handleBootstrapState(); break; case UploadRecoveryKeyState.unlockError: - titleText = L10n.of(context)!.chatBackup; - body = Text( - L10n.of(context)!.cannotUnlockBackupKey, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - buttons.clear(); - buttons - ..add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ) - ..add( - AdaptiveFlatButton( - label: L10n.of(context)!.next, - onPressed: () async { - await BootstrapDialog(client: widget.client).show().then( - (value) => Navigator.of(context, rootNavigator: false) - .pop(false), - ); - }, - ), - ); + WidgetsBinding.instance.addPostFrameCallback((_) async { + Navigator.of(context, rootNavigator: false).pop(false); + }); break; case UploadRecoveryKeyState.uploadError: Logs().e('TomBootstrapDialogState::build(): upload recovery key error'); - titleText = L10n.of(context)!.chatBackup; - body = Text( - L10n.of(context)!.cannotUploadKey, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context, rootNavigator: false).pop(); + }); break; default: _handleBootstrapState(); break; } - return AlertDialog( - title: titleText != null ? Text(titleText!) : null, - content: body, - actions: buttons, + return Scaffold( + backgroundColor: Colors.transparent, + body: Center( + child: Container( + height: TomBootstrapDialogStyle.sizedDialogWeb, + width: TomBootstrapDialogStyle.sizedDialogWeb, + decoration: TomBootstrapDialogStyle.decorationDialog, + child: Padding( + padding: TomBootstrapDialogStyle.paddingDialog, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + L10n.of(context)!.settingUpYourTwake, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: LinagoraSysColors.material().onBackground, + ), + textAlign: TextAlign.center, + ), + Padding( + padding: TomBootstrapDialogStyle.lottiePadding, + child: LottieBuilder.asset( + 'assets/twake_loading.json', + width: TomBootstrapDialogStyle.lottieSize, + height: TomBootstrapDialogStyle.lottieSize, + ), + ), + Text( + _description, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: LinagoraSysColors.material().onBackground, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), ); } bool _existedRecoveryWordsInTom(String? key) { - if (key == null && widget.recoveryWords != null) { + if (key == null && _recoveryWords != null) { return true; } - return widget.recoveryWords != null && widget.recoveryWords!.words == key; + return _recoveryWords != null && _recoveryWords!.words == key; } bool _createNewRecoveryKeySuccess() { @@ -205,14 +262,18 @@ class TomBootstrapDialogState extends State { _uploadRecoveryKeyState == UploadRecoveryKeyState.created; } + bool get _setUpSuccess => + _uploadRecoveryKeyState != UploadRecoveryKeyState.dataLoading && + _uploadRecoveryKeyState != UploadRecoveryKeyState.checkingRecoveryWork; + void _handleBootstrapState() { - if (bootstrap != null) { + if (bootstrap != null && _setUpSuccess) { switch (bootstrap!.state) { case BootstrapState.loading: break; case BootstrapState.askWipeSsss: WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeSsss(_wipe!), + (_) => bootstrap?.wipeSsss(_wipe), ); break; case BootstrapState.askBadSsss: @@ -223,7 +284,7 @@ class TomBootstrapDialogState extends State { case BootstrapState.askUseExistingSsss: _uploadRecoveryKeyState = UploadRecoveryKeyState.useExisting; WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.useExistingSsss(!_wipe!), + (_) => bootstrap?.useExistingSsss(!_wipe), ); break; case BootstrapState.askUnlockSsss: @@ -246,7 +307,7 @@ class TomBootstrapDialogState extends State { break; case BootstrapState.askWipeCrossSigning: WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeCrossSigning(_wipe!), + (_) => bootstrap?.wipeCrossSigning(_wipe), ); break; case BootstrapState.askSetupCrossSigning: @@ -262,7 +323,7 @@ class TomBootstrapDialogState extends State { break; case BootstrapState.askWipeOnlineKeyBackup: WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeOnlineKeyBackup(_wipe!), + (_) => bootstrap?.wipeOnlineKeyBackup(_wipe), ); break; case BootstrapState.askSetupOnlineKeyBackup: @@ -271,34 +332,14 @@ class TomBootstrapDialogState extends State { ); break; case BootstrapState.error: - titleText = L10n.of(context)!.oopsSomethingWentWrong; - body = const Icon(Icons.error_outline, color: Colors.red, size: 40); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context, rootNavigator: false).pop(); + }); break; case BootstrapState.done: - titleText = L10n.of(context)!.everythingReady; - body = Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset('assets/backup.png', fit: BoxFit.contain), - Text(L10n.of(context)!.yourChatBackupHasBeenSetUp), - ], - ); - buttons.clear(); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => - Navigator.of(context, rootNavigator: false).pop(false), - ), - ); + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context, rootNavigator: false).pop(true); + }); break; } } @@ -351,7 +392,7 @@ class TomBootstrapDialogState extends State { } Future _unlockBackUp() async { - final recoveryWords = widget.recoveryWords; + final recoveryWords = _recoveryWords; if (recoveryWords == null) { Logs().e('TomBootstrapDialogState::_unlockBackUp(): recoveryWords null'); setState(() { @@ -383,27 +424,11 @@ class TomBootstrapDialogState extends State { setState(() {}); } } - - Widget _loadingContent(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.only(right: 16.0), - child: CircularProgressIndicator.adaptive(), - ), - Expanded( - child: Text( - L10n.of(context)!.loadingPleaseWait, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - } } enum UploadRecoveryKeyState { + dataLoading, + checkingRecoveryWork, initial, wipeRecovery, wipeRecoveryFailed, diff --git a/lib/pages/bootstrap/tom_bootstrap_dialog_style.dart b/lib/pages/bootstrap/tom_bootstrap_dialog_style.dart new file mode 100644 index 0000000000..32c4fd5846 --- /dev/null +++ b/lib/pages/bootstrap/tom_bootstrap_dialog_style.dart @@ -0,0 +1,27 @@ +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; + +class TomBootstrapDialogStyle { + static EdgeInsets paddingDialog = const EdgeInsets.symmetric( + horizontal: 56, + ); + + static Color? barrierColor = + PlatformInfos.isMobile ? LinagoraSysColors.material().onPrimary : null; + + static double? sizedDialogWeb = PlatformInfos.isMobile ? null : 400; + + static Decoration? decorationDialog = PlatformInfos.isMobile + ? null + : BoxDecoration( + color: LinagoraSysColors.material().surface, + borderRadius: BorderRadius.circular(24), + ); + + static EdgeInsets lottiePadding = EdgeInsets.symmetric( + vertical: PlatformInfos.isMobile ? 16 : 24, + ); + + static double lottieSize = PlatformInfos.isMobile ? 64 : 96; +} diff --git a/lib/pages/chat/add_widget_tile.dart b/lib/pages/chat/add_widget_tile.dart index 16d22ea05b..b75e2ce5ac 100644 --- a/lib/pages/chat/add_widget_tile.dart +++ b/lib/pages/chat/add_widget_tile.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat/add_widget_tile_view.dart'; class AddWidgetTile extends StatefulWidget { final Room room; - const AddWidgetTile({Key? key, required this.room}) : super(key: key); + const AddWidgetTile({super.key, required this.room}); @override State createState() => AddWidgetTileState(); diff --git a/lib/pages/chat/add_widget_tile_view.dart b/lib/pages/chat/add_widget_tile_view.dart index 7ce441e84a..d7ac53ef2b 100644 --- a/lib/pages/chat/add_widget_tile_view.dart +++ b/lib/pages/chat/add_widget_tile_view.dart @@ -8,8 +8,7 @@ import 'package:fluffychat/pages/chat/add_widget_tile.dart'; class AddWidgetTileView extends StatelessWidget { final AddWidgetTileState controller; - const AddWidgetTileView({Key? key, required this.controller}) - : super(key: key); + const AddWidgetTileView({super.key, required this.controller}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index a77e36cb2d..1f627d4486 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/presentation/model/chat/view_event_list_ui_state.dart import 'package:fluffychat/utils/extension/basic_event_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_style.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; @@ -86,12 +87,12 @@ class Chat extends StatefulWidget { final void Function(RightColumnType)? onChangeRightColumnType; const Chat({ - Key? key, + super.key, required this.roomId, this.shareFiles, this.roomName, this.onChangeRightColumnType, - }) : super(key: key); + }); @override ChatController createState() => ChatController(); @@ -378,7 +379,12 @@ class ChatController extends State void handleDragDone(DropDoneDetails details) async { final matrixFiles = await onDragDone(details); - sendFileOnWebAction(context, room: room, matrixFilesList: matrixFiles); + sendFileOnWebAction( + context, + room: room, + matrixFilesList: matrixFiles, + onSendFileCallback: scrollDown, + ); } void _handleReceivedShareFiles() { @@ -1292,7 +1298,12 @@ class ChatController extends State _showMediaPicker(context); } else { final matrixFiles = await pickFilesFromSystem(); - sendFileOnWebAction(context, room: room, matrixFilesList: matrixFiles); + sendFileOnWebAction( + context, + room: room, + matrixFilesList: matrixFiles, + onSendFileCallback: scrollDown, + ); } } @@ -1309,6 +1320,7 @@ class ChatController extends State type: action, room: room, context: context, + onSendFileCallback: scrollDown, ), onSendTap: () { sendMedia( @@ -1316,9 +1328,13 @@ class ChatController extends State room: room, caption: _captionsController.text, ); + scrollDown(); _captionsController.clear(); }, - onCameraPicked: (_) => sendMedia(imagePickerController, room: room), + onCameraPicked: (_) { + sendMedia(imagePickerController, room: room); + scrollDown(); + }, captionController: _captionsController, focusSuggestionController: _focusSuggestionController, typeAheadKey: _chatMediaPickerTypeAheadKey, @@ -1379,10 +1395,7 @@ class ChatController extends State } } - List _popupMenuActionTile( - BuildContext context, - Event event, - ) { + List _getListPopupMenuActions(Event event) { final listAction = [ ChatContextMenuActions.select, if (event.isCopyable) ChatContextMenuActions.copyMessage, @@ -1391,25 +1404,26 @@ class ChatController extends State if (PlatformInfos.isWeb && event.hasAttachment) ChatContextMenuActions.downloadFile, ]; - return listAction.map((action) { - return popupItemByTwakeAppRouter( - context, - action.getTitle( + return listAction; + } + + List _mapPopupMenuActionsToContextMenuActions( + List listActions, + Event event, + ) { + return listActions.map((action) { + return ContextMenuAction( + name: action.getTitle( context, unpin: isUnpinEvent(event), isSelected: isSelected(event), ), - iconAction: action.getIconData( + icon: action.getIconData( unpin: isUnpinEvent(event), ), imagePath: action.getImagePath( unpin: isUnpinEvent(event), ), - isClearCurrentPage: false, - onCallbackAction: () => _handleClickOnContextMenuItem( - action, - event, - ), ); }).toList(); } @@ -1446,15 +1460,27 @@ class ChatController extends State BuildContext context, Event event, TapDownDetails tapDownDetails, - ) { + ) async { final offset = tapDownDetails.globalPosition; + final listPopupMenuActions = _getListPopupMenuActions(event); + final listContextMenuActions = _mapPopupMenuActionsToContextMenuActions( + listPopupMenuActions, + event, + ); _handleStateContextMenu(); - showTwakeContextMenu( + final selectedActionIndex = await showTwakeContextMenu( offset: offset, context: context, - builder: (context) => _popupMenuActionTile(context, event), + listActions: listContextMenuActions, onClose: _handleStateContextMenu, ); + + if (selectedActionIndex != null && selectedActionIndex is int) { + _handleClickOnContextMenuItem( + listPopupMenuActions[selectedActionIndex], + event, + ); + } } void hideKeyboardChatScreen() { @@ -1733,7 +1759,8 @@ class ChatController extends State Logs().d( 'Chat::_listenRoomUpdateEvent():: Event Update Content ${eventUpdate.content}', ); - if (eventUpdate.isPinnedEventsHasChanged) { + if (eventUpdate.isPinnedEventsHasChanged && + room?.id == eventUpdate.roomID) { WidgetsBinding.instance.addPostFrameCallback((_) async { eventUpdate.updatePinnedMessage( onPinnedMessageUpdated: _handlePinnedMessageCallBack, @@ -1776,14 +1803,22 @@ class ChatController extends State void handleAppbarMenuAction( BuildContext context, TapDownDetails tapDownDetails, - ) { + ) async { final offset = tapDownDetails.globalPosition; - showTwakeContextMenu( + final listAppBarActions = _getListActionAppBarMenu(); + final listContextMenuActions = + _mapAppbarMenuActionToContextMenuAction(listAppBarActions); + + final selectedActionIndex = await showTwakeContextMenu( offset: offset, context: context, - builder: (_) => - _appbarMenuActionTile(context, _getListActionAppBarMenu()), + listActions: listContextMenuActions, ); + + if (selectedActionIndex != null && selectedActionIndex is int) { + final selectedAction = listAppBarActions[selectedActionIndex]; + onSelectedAppBarActions(selectedAction); + } } List _getListActionAppBarMenu() { @@ -1813,23 +1848,19 @@ class ChatController extends State ]; } - List _appbarMenuActionTile( - BuildContext context, + List _mapAppbarMenuActionToContextMenuAction( List listAction, ) { return listAction.map((action) { - return popupItemByTwakeAppRouter( - context, - action.getTitle(context), - iconAction: action.getIcon(), + return ContextMenuAction( + name: action.getTitle(context), + icon: action.getIcon(), colorIcon: action.getColorIcon(context), styleName: action == ChatAppBarActions.leaveGroup ? PopupMenuWidgetStyle.defaultItemTextStyle(context)?.copyWith( color: action.getColorIcon(context), ) : null, - isClearCurrentPage: false, - onCallbackAction: () => onSelectedAppBarActions(action), ); }).toList(); } @@ -1875,7 +1906,9 @@ class ChatController extends State @override void initState() { _initializePinnedEvents(); - registerPasteShortcutListeners(); + registerPasteShortcutListeners( + onSendFileCallback: scrollDown, + ); keyboardVisibilitySubscription = keyboardVisibilityController.onChange.listen(_keyboardListener); scrollController.addListener(_updateScrollController); diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index a8b9b22bc1..caf6bb1ba2 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -24,7 +24,7 @@ class ChatAppBarTitle extends StatelessWidget { final String? roomName; const ChatAppBarTitle({ - Key? key, + super.key, required this.actions, this.room, this.roomName, @@ -33,7 +33,7 @@ class ChatAppBarTitle extends StatelessWidget { required this.sendController, required this.connectivityResultStream, required this.onPushDetails, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/chat_emoji_picker.dart b/lib/pages/chat/chat_emoji_picker.dart index f1db4cc96d..fa86b288c4 100644 --- a/lib/pages/chat/chat_emoji_picker.dart +++ b/lib/pages/chat/chat_emoji_picker.dart @@ -8,11 +8,11 @@ class ChatEmojiPicker extends StatelessWidget { final void Function() emojiPickerBackspace; const ChatEmojiPicker({ - Key? key, + super.key, required this.showEmojiPickerNotifier, required this.onEmojiSelected, required this.emojiPickerBackspace, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 1514143825..6592c74fcd 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/pages/chat/group_chat_empty_view.dart'; import 'package:fluffychat/pages/chat_draft/draft_chat_empty_widget.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -21,9 +22,9 @@ class ChatEventList extends StatelessWidget { final ChatController controller; const ChatEventList({ - Key? key, + super.key, required this.controller, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -58,13 +59,13 @@ class ChatEventList extends StatelessWidget { return NotificationListener( onNotification: (notification) { switch (notification.runtimeType) { - case ScrollStartNotification: + case const (ScrollStartNotification): controller.handleScrollStartNotification(); break; - case ScrollEndNotification: + case const (ScrollEndNotification): controller.handleScrollEndNotification(); break; - case ScrollUpdateNotification: + case const (ScrollUpdateNotification): controller.handleScrollUpdateNotification(); break; default: @@ -189,6 +190,13 @@ class ChatEventList extends StatelessWidget { ); }, onLongPress: controller.onSelectMessage, + listAction: controller + .listHorizontalActionMenuBuilder(event) + .map((action) { + return ContextMenuAction( + name: action.action.name, + ); + }).toList(), ) : const SizedBox(), ); diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 0557ad104b..8f03eb7690 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -19,7 +19,7 @@ import 'input_bar/input_bar.dart'; class ChatInputRow extends StatelessWidget { final ChatController controller; - const ChatInputRow(this.controller, {Key? key}) : super(key: key); + const ChatInputRow(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -53,7 +53,7 @@ class ChatInputRow extends StatelessWidget { Container( height: ChatInputRowStyle.chatInputRowHeight, alignment: Alignment.center, - child: _ChatAccountPicker(controller), + child: ChatAccountPicker(controller), ), Expanded( child: @@ -206,10 +206,10 @@ class ActionSelectModeWidget extends StatelessWidget { } } -class _ChatAccountPicker extends StatelessWidget { +class ChatAccountPicker extends StatelessWidget { final ChatController controller; - const _ChatAccountPicker(this.controller, {Key? key}) : super(key: key); + const ChatAccountPicker(this.controller, {super.key}); void _popupMenuButtonSelected(String mxid) { final client = controller.matrix!.currentBundle! diff --git a/lib/pages/chat/chat_input_row_send_btn.dart b/lib/pages/chat/chat_input_row_send_btn.dart index f27011aee8..5941344378 100644 --- a/lib/pages/chat/chat_input_row_send_btn.dart +++ b/lib/pages/chat/chat_input_row_send_btn.dart @@ -12,11 +12,11 @@ class ChatInputRowSendBtn extends StatelessWidget { final void Function() onTap; const ChatInputRowSendBtn({ - Key? key, + super.key, required this.inputText, required this.onTap, this.sendingNotifier, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/chat_invitation_body.dart b/lib/pages/chat/chat_invitation_body.dart index 78b898854c..009a4fb67e 100644 --- a/lib/pages/chat/chat_invitation_body.dart +++ b/lib/pages/chat/chat_invitation_body.dart @@ -9,7 +9,7 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class ChatInvitationBody extends StatelessWidget with MessageContentMixin { final ChatController controller; - const ChatInvitationBody(this.controller, {Key? key}) : super(key: key); + const ChatInvitationBody(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -47,6 +47,8 @@ class ChatInvitationBody extends StatelessWidget with MessageContentMixin { child: Container( width: ChatInvitationBodyStyle.dialogWidth, decoration: BoxDecoration( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular( ChatInvitationBodyStyle.dialogBorderRadius, @@ -130,21 +132,21 @@ class InvitationAcceptButton extends StatelessWidget { Widget build(BuildContext context) { return OutlinedButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.primary, ), - foregroundColor: MaterialStateProperty.all( + foregroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.onPrimary, ), - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular( ChatInvitationBodyStyle.dialogButtonBorderRadius, ), ), ), - side: MaterialStateProperty.all(BorderSide.none), - fixedSize: MaterialStateProperty.all( + side: WidgetStateProperty.all(BorderSide.none), + fixedSize: WidgetStateProperty.all( const Size.fromHeight(ChatInvitationBodyStyle.dialogButtonHeight), ), ), @@ -166,21 +168,21 @@ class InvitationRejectButton extends StatelessWidget { Widget build(BuildContext context) { return OutlinedButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.surface, ), - foregroundColor: MaterialStateProperty.all( + foregroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.error, ), - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular( ChatInvitationBodyStyle.dialogButtonBorderRadius, ), ), ), - side: MaterialStateProperty.all(BorderSide.none), - fixedSize: MaterialStateProperty.all( + side: WidgetStateProperty.all(BorderSide.none), + fixedSize: WidgetStateProperty.all( const Size.fromHeight(ChatInvitationBodyStyle.dialogButtonHeight), ), ), @@ -201,7 +203,7 @@ class InvitationBottomBar extends StatelessWidget { padding: const EdgeInsets.all( ChatInvitationBodyStyle.chatInvitationBottomBarPadding, ), - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, height: ChatInvitationBodyStyle.chatInvitationBottomBarHeight, child: Center( child: Row( diff --git a/lib/pages/chat/chat_loading_view.dart b/lib/pages/chat/chat_loading_view.dart index eb46ef9953..fffbd627b0 100644 --- a/lib/pages/chat/chat_loading_view.dart +++ b/lib/pages/chat/chat_loading_view.dart @@ -6,8 +6,8 @@ import 'package:skeletons/skeletons.dart'; class ChatLoadingView extends StatelessWidget { const ChatLoadingView({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/chat_pinned_events/bottom_menu/bottom_menu_web.dart b/lib/pages/chat/chat_pinned_events/bottom_menu/bottom_menu_web.dart index 8d08f42931..e1a525caf7 100644 --- a/lib/pages/chat/chat_pinned_events/bottom_menu/bottom_menu_web.dart +++ b/lib/pages/chat/chat_pinned_events/bottom_menu/bottom_menu_web.dart @@ -34,7 +34,7 @@ class BottomMenuWeb extends StatelessWidget { width: PinnedMessagesStyle.unpinButtonWidth, padding: PinnedMessagesStyle.actionBarPadding, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular( PinnedMessagesStyle.actionBarBorderRadius, ), diff --git a/lib/pages/chat/chat_pinned_events/pinned_events_view.dart b/lib/pages/chat/chat_pinned_events/pinned_events_view.dart index 20aab7ca7b..8443f59353 100644 --- a/lib/pages/chat/chat_pinned_events/pinned_events_view.dart +++ b/lib/pages/chat/chat_pinned_events/pinned_events_view.dart @@ -18,7 +18,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; class PinnedEventsView extends StatelessWidget { final ChatController controller; - const PinnedEventsView(this.controller, {Key? key}) : super(key: key); + const PinnedEventsView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -36,7 +36,7 @@ class PinnedEventsView extends StatelessWidget { (failure) => child!, (success) { switch (success.runtimeType) { - case ChatGetPinnedEventsSuccess: + case const (ChatGetPinnedEventsSuccess): final data = success as ChatGetPinnedEventsSuccess; return Material( color: LinagoraSysColors.material().onPrimary, diff --git a/lib/pages/chat/chat_pinned_events/pinned_messages.dart b/lib/pages/chat/chat_pinned_events/pinned_messages.dart index 5364944819..51040c3047 100644 --- a/lib/pages/chat/chat_pinned_events/pinned_messages.dart +++ b/lib/pages/chat/chat_pinned_events/pinned_messages.dart @@ -22,6 +22,7 @@ import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action.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'; @@ -216,7 +217,26 @@ class PinnedMessagesController extends State builder: (context) { return Column( mainAxisSize: MainAxisSize.min, - children: pinnedMessagesActionsList(context, event), + children: getPinnedMessagesActionsList(event).map((action) { + return popupItemByTwakeAppRouter( + context, + action.getTitle( + context, + unpin: event.isPinned, + isSelected: isSelected(event), + ), + iconAction: action.getIconData(unpin: event.isPinned), + imagePath: action.getImagePath(unpin: event.isPinned), + colorIcon: + action == ChatContextMenuActions.pinChat && event.isPinned + ? Theme.of(context).colorScheme.onSurface + : null, + onCallbackAction: () => _handleClickOnContextMenuItem( + action, + event, + ), + ); + }).toList(), ); }, ); @@ -284,21 +304,12 @@ class PinnedMessagesController extends State openingPopupMenu.toggle(); } - // Used for "Right Click" Context Menu List pinnedMessagesActionsList( BuildContext context, + List actions, Event event, ) { - final listAction = [ - ChatContextMenuActions.pinChat, - ChatContextMenuActions.select, - ChatContextMenuActions.jumpToMessage, - ChatContextMenuActions.copyMessage, - ChatContextMenuActions.forward, - if (PlatformInfos.isWeb && event.hasAttachment) - ChatContextMenuActions.downloadFile, - ]; - return listAction.map((action) { + return actions.map((action) { return popupItemByTwakeAppRouter( context, action.getTitle( @@ -319,13 +330,36 @@ class PinnedMessagesController extends State }).toList(); } - // Used for "More" Context Menu - List _pinnedMessagesActionsTileList( + List getPinnedMessagesActionsList(Event event) { + final listAction = [ + ChatContextMenuActions.pinChat, + ChatContextMenuActions.select, + ChatContextMenuActions.jumpToMessage, + ChatContextMenuActions.copyMessage, + ChatContextMenuActions.forward, + if (PlatformInfos.isWeb && event.hasAttachment) + ChatContextMenuActions.downloadFile, + ]; + return listAction; + } + + List pinnedMessagesContextMenuActionsList( BuildContext context, Event event, ) { - final actionTiles = pinnedMessagesActionsList(context, event).map((action) { - return action; + final actionTiles = getPinnedMessagesActionsList(event).map((action) { + return ContextMenuAction( + name: action.getTitle( + context, + unpin: event.isPinned, + isSelected: isSelected(event), + ), + icon: action.getIconData(unpin: event.isPinned), + imagePath: action.getImagePath(unpin: event.isPinned), + colorIcon: action == ChatContextMenuActions.pinChat && event.isPinned + ? Theme.of(context).colorScheme.onSurface + : null, + ); }).toList(); return actionTiles; } @@ -382,15 +416,22 @@ class PinnedMessagesController extends State BuildContext context, Event event, TapDownDetails tapDownDetails, - ) { + ) async { final offset = tapDownDetails.globalPosition; + final listActions = pinnedMessagesContextMenuActionsList(context, event); _handleStateContextMenu(); - showTwakeContextMenu( + final selectedActionIndex = await showTwakeContextMenu( context: context, offset: offset, - builder: (context) => _pinnedMessagesActionsTileList(context, event), + listActions: listActions, onClose: _handleStateContextMenu, ); + if (selectedActionIndex != null && selectedActionIndex is int) { + _handleClickOnContextMenuItem( + getPinnedMessagesActionsList(event)[selectedActionIndex], + event, + ); + } } void _listenRoomUpdateEvent() { diff --git a/lib/pages/chat/chat_pinned_events/pinned_messages_screen.dart b/lib/pages/chat/chat_pinned_events/pinned_messages_screen.dart index 5f28afcec7..c4421d7bb2 100644 --- a/lib/pages/chat/chat_pinned_events/pinned_messages_screen.dart +++ b/lib/pages/chat/chat_pinned_events/pinned_messages_screen.dart @@ -98,13 +98,22 @@ class PinnedMessagesScreen extends StatelessWidget { selectMode: selectedEvents.isNotEmpty, onSelect: controller.onSelectMessage, selected: controller.isSelected(event), - menuChildren: (context) => controller - .pinnedMessagesActionsList(context, event), + menuChildren: (context) => + controller.pinnedMessagesActionsList( + context, + controller.getPinnedMessagesActionsList(event), + event, + ), onLongPress: (event) => controller.onLongPressMessage( context, event, ), + listAction: controller + .pinnedMessagesContextMenuActionsList( + context, + event, + ), ); }, ); diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index d2b0b17b91..337bb82972 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -18,7 +18,7 @@ import 'package:matrix/matrix.dart'; class ChatView extends StatelessWidget with MessageContentMixin { final ChatController controller; - const ChatView(this.controller, {Key? key}) : super(key: key); + const ChatView(this.controller, {super.key}); Widget _appBarActions(BuildContext context) { if (controller.selectMode) { diff --git a/lib/pages/chat/chat_view_body.dart b/lib/pages/chat/chat_view_body.dart index 6d281d0bcf..f392b68562 100644 --- a/lib/pages/chat/chat_view_body.dart +++ b/lib/pages/chat/chat_view_body.dart @@ -22,7 +22,7 @@ import 'chat_input_row.dart'; class ChatViewBody extends StatelessWidget with MessageContentMixin { final ChatController controller; - const ChatViewBody(this.controller, {Key? key}) : super(key: key); + const ChatViewBody(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/chat_view_style.dart b/lib/pages/chat/chat_view_style.dart index c706eb832b..cb06aa864d 100644 --- a/lib/pages/chat/chat_view_style.dart +++ b/lib/pages/chat/chat_view_style.dart @@ -38,6 +38,8 @@ class ChatViewStyle { }) => active ? Theme.of(context).colorScheme.primary + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use : Theme.of(context).colorScheme.onBackground; static const paddingBottomContextMenu = 16.0; diff --git a/lib/pages/chat/cupertino_widgets_bottom_sheet.dart b/lib/pages/chat/cupertino_widgets_bottom_sheet.dart index 0bc7ac3469..23dddfa9b6 100644 --- a/lib/pages/chat/cupertino_widgets_bottom_sheet.dart +++ b/lib/pages/chat/cupertino_widgets_bottom_sheet.dart @@ -9,8 +9,7 @@ import 'edit_widgets_dialog.dart'; class CupertinoWidgetsBottomSheet extends StatelessWidget { final Room room; - const CupertinoWidgetsBottomSheet({Key? key, required this.room}) - : super(key: key); + const CupertinoWidgetsBottomSheet({super.key, required this.room}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/edit_widgets_dialog.dart b/lib/pages/chat/edit_widgets_dialog.dart index 7d9579a109..2ba163feb0 100644 --- a/lib/pages/chat/edit_widgets_dialog.dart +++ b/lib/pages/chat/edit_widgets_dialog.dart @@ -8,7 +8,7 @@ import 'add_widget_tile.dart'; class EditWidgetsDialog extends StatelessWidget { final Room room; - const EditWidgetsDialog({Key? key, required this.room}) : super(key: key); + const EditWidgetsDialog({super.key, required this.room}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/event_info_dialog.dart b/lib/pages/chat/event_info_dialog.dart index 2dd051ddad..0a738b541f 100644 --- a/lib/pages/chat/event_info_dialog.dart +++ b/lib/pages/chat/event_info_dialog.dart @@ -24,8 +24,8 @@ class EventInfoDialog extends StatelessWidget { const EventInfoDialog({ required this.event, required this.l10n, - Key? key, - }) : super(key: key); + super.key, + }); String get prettyJson { const JsonDecoder decoder = JsonDecoder(); diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 26893069d0..2877936e2a 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -22,8 +22,7 @@ class AudioPlayerWidget extends StatefulWidget { static const int wavesCount = 40; - const AudioPlayerWidget(this.event, {this.color = Colors.black, Key? key}) - : super(key: key); + const AudioPlayerWidget(this.event, {this.color = Colors.black, super.key}); @override AudioPlayerState createState() => AudioPlayerState(); diff --git a/lib/pages/chat/events/button_content.dart b/lib/pages/chat/events/button_content.dart index 33dd298dcd..62423c2928 100644 --- a/lib/pages/chat/events/button_content.dart +++ b/lib/pages/chat/events/button_content.dart @@ -7,11 +7,11 @@ class ButtonContent extends StatelessWidget { final String title; const ButtonContent({ - Key? key, + super.key, required this.onTap, required this.icon, required this.title, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/call_invite_content.dart b/lib/pages/chat/events/call_invite_content.dart index 3ef73870f1..7b39108b12 100644 --- a/lib/pages/chat/events/call_invite_content.dart +++ b/lib/pages/chat/events/call_invite_content.dart @@ -7,7 +7,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class CallInviteContent extends StatelessWidget with MessageContentMixin { final Event event; - const CallInviteContent({Key? key, required this.event}) : super(key: key); + const CallInviteContent({super.key, required this.event}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/cute_events.dart b/lib/pages/chat/events/cute_events.dart index d296c1befa..3a93f978db 100644 --- a/lib/pages/chat/events/cute_events.dart +++ b/lib/pages/chat/events/cute_events.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/config/app_config.dart'; class CuteContent extends StatefulWidget { final Event event; - const CuteContent(this.event, {Key? key}) : super(key: key); + const CuteContent(this.event, {super.key}); @override State createState() => _CuteContentState(); @@ -103,10 +103,10 @@ class CuteEventOverlay extends StatefulWidget { final VoidCallback onAnimationEnd; const CuteEventOverlay({ - Key? key, + super.key, required this.emoji, required this.onAnimationEnd, - }) : super(key: key); + }); @override State createState() => _CuteEventOverlayState(); @@ -141,8 +141,8 @@ class _CuteEventOverlayState extends State animation: controller!, builder: (context, _) => LayoutBuilder( builder: (context, constraints) { - final width = constraints.maxWidth - _CuteOverlayContent.size; - final height = constraints.maxHeight + _CuteOverlayContent.size; + final width = constraints.maxWidth - CuteOverlayContent.size; + final height = constraints.maxHeight + CuteOverlayContent.size; return SizedBox( height: constraints.maxHeight, width: constraints.maxWidth, @@ -157,8 +157,8 @@ class _CuteEventOverlayState extends State .25 * position.height * (controller?.value ?? 0)) - - _CuteOverlayContent.size, - child: _CuteOverlayContent( + CuteOverlayContent.size, + child: CuteOverlayContent( emoji: widget.emoji, ), ), @@ -178,11 +178,11 @@ class _CuteEventOverlayState extends State } } -class _CuteOverlayContent extends StatelessWidget { +class CuteOverlayContent extends StatelessWidget { static const double size = 64.0; final String emoji; - const _CuteOverlayContent({Key? key, required this.emoji}) : super(key: key); + const CuteOverlayContent({super.key, required this.emoji}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/encrypted_content.dart b/lib/pages/chat/events/encrypted_content.dart index f8c636ccb3..1bc49569a0 100644 --- a/lib/pages/chat/events/encrypted_content.dart +++ b/lib/pages/chat/events/encrypted_content.dart @@ -7,7 +7,7 @@ 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); + const EncryptedContent({super.key, required this.event}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/event_video_player.dart b/lib/pages/chat/events/event_video_player.dart index 05a115fefe..992693b722 100644 --- a/lib/pages/chat/events/event_video_player.dart +++ b/lib/pages/chat/events/event_video_player.dart @@ -38,7 +38,7 @@ class EventVideoPlayer extends StatelessWidget { const EventVideoPlayer( this.event, { - Key? key, + super.key, this.width, this.height, this.rounded = true, @@ -48,7 +48,7 @@ class EventVideoPlayer extends StatelessWidget { this.noResizeThumbnail = false, this.onVideoTapped, this.centerWidget = const CenterVideoButton(icon: Icons.play_arrow), - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index d72ff4120f..f7189aa397 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -24,7 +24,7 @@ class HtmlMessage extends StatelessWidget { final Widget? bottomWidgetSpan; const HtmlMessage({ - Key? key, + super.key, required this.html, this.maxLines, required this.room, @@ -32,7 +32,7 @@ class HtmlMessage extends StatelessWidget { this.linkStyle, this.emoteSize, this.bottomWidgetSpan, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -180,7 +180,8 @@ class HtmlMessage extends StatelessWidget { final user = room.getUser(identifier); final displayName = user?.displayName ?? identifier; return MentionedUser( - displayName: displayName.displayMentioned, + displayName: + !room.isDirectChat ? displayName.displayMentioned : displayName, url: url, onTap: !room.isDirectChat ? onTap : null, textStyle: !room.isDirectChat diff --git a/lib/pages/chat/events/images_builder/image_bubble.dart b/lib/pages/chat/events/images_builder/image_bubble.dart index d3b34a31ce..e9dd5104a6 100644 --- a/lib/pages/chat/events/images_builder/image_bubble.dart +++ b/lib/pages/chat/events/images_builder/image_bubble.dart @@ -46,8 +46,8 @@ class ImageBubble extends StatelessWidget { this.noResizeThumbnail = false, this.isPreview = true, this.imageData, - Key? key, - }) : super(key: key); + super.key, + }); static const animationSwitcherDuration = Duration(seconds: 1); diff --git a/lib/pages/chat/events/images_builder/unencrypted_image_builder_web.dart b/lib/pages/chat/events/images_builder/unencrypted_image_builder_web.dart index df50019f02..f13103198f 100644 --- a/lib/pages/chat/events/images_builder/unencrypted_image_builder_web.dart +++ b/lib/pages/chat/events/images_builder/unencrypted_image_builder_web.dart @@ -1,9 +1,11 @@ import 'package:fluffychat/pages/chat/events/images_builder/image_placeholder.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:matrix/matrix.dart'; +import 'package:flutter_avif/flutter_avif.dart'; class UnencryptedImageWidget extends StatelessWidget { const UnencryptedImageWidget({ @@ -23,6 +25,17 @@ class UnencryptedImageWidget extends StatelessWidget { @override Widget build(BuildContext context) { + if (event.mimeType == TwakeMimeTypeExtension.avifMimeType) { + return AvifImage.network( + event + .attachmentOrThumbnailMxcUrl(getThumbnail: isThumbnail)! + .getDownloadLink(event.room.client) + .toString(), + height: height, + width: width, + fit: BoxFit.cover, + ); + } return Image.network( event .attachmentOrThumbnailMxcUrl(getThumbnail: isThumbnail)! @@ -51,8 +64,22 @@ class UnencryptedImageWidget extends StatelessWidget { cacheHeight: (height * MediaQuery.devicePixelRatioOf(context)).round(), filterQuality: FilterQuality.medium, errorBuilder: (context, error, stackTrace) { - return BlurHash( - hash: event.blurHash ?? MessageContentStyle.defaultBlurHash, + return Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: width, + height: height, + child: BlurHash( + hash: event.blurHash ?? MessageContentStyle.defaultBlurHash, + ), + ), + Icon( + Icons.error, + size: MessageContentStyle.iconErrorSize, + color: Theme.of(context).colorScheme.onError, + ), + ], ); }, loadingBuilder: (context, child, loadingProgress) { diff --git a/lib/pages/chat/events/map_bubble.dart b/lib/pages/chat/events/map_bubble.dart index b003bc6edd..c441c7d64c 100644 --- a/lib/pages/chat/events/map_bubble.dart +++ b/lib/pages/chat/events/map_bubble.dart @@ -17,8 +17,8 @@ class MapBubble extends StatelessWidget { this.width = 400, this.height = 400, this.radius = 10.0, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/message/message.dart b/lib/pages/chat/events/message/message.dart index c5c9fea97a..6790e63dc8 100644 --- a/lib/pages/chat/events/message/message.dart +++ b/lib/pages/chat/events/message/message.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; import 'package:fluffychat/widgets/swipeable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -61,6 +62,7 @@ class Message extends StatefulWidget { final FocusNode? focusNode; final void Function(Event)? timestampCallback; final void Function(Event)? onLongPress; + final List listAction; const Message( this.event, { @@ -78,13 +80,14 @@ class Message extends StatefulWidget { required this.isHoverNotifier, required this.listHorizontalActionMenu, this.menuChildren, - Key? key, + super.key, this.onMenuAction, this.markedUnreadLocation, this.focusNode, this.timestampCallback, this.onLongPress, - }) : super(key: key); + required this.listAction, + }); /// Indicates wheither the user may use a mouse instead /// of touchscreen. @@ -204,6 +207,7 @@ class _MessageState extends State { menuChildren: widget.menuChildren, focusNode: widget.focusNode, onLongPress: widget.onLongPress, + listActions: widget.listAction, ), ), ]; diff --git a/lib/pages/chat/events/message/message_content_builder.dart b/lib/pages/chat/events/message/message_content_builder.dart index 31bb5b63d9..aebab9e54e 100644 --- a/lib/pages/chat/events/message/message_content_builder.dart +++ b/lib/pages/chat/events/message/message_content_builder.dart @@ -31,6 +31,8 @@ class MessageContentBuilder extends StatelessWidget @override Widget build(BuildContext context) { + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use final textColor = Theme.of(context).colorScheme.onBackground; final displayEvent = event.getDisplayEvent(timeline); final noPadding = { diff --git a/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart b/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart index 7a6584fe2e..163bf980b6 100644 --- a/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart +++ b/lib/pages/chat/events/message/message_content_with_timestamp_builder.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/extension/event_status_custom_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; import 'package:fluffychat/widgets/context_menu/context_menu_action_item.dart'; import 'package:fluffychat/widgets/context_menu/twake_context_menu_area.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; @@ -32,6 +33,7 @@ class MessageContentWithTimestampBuilder extends StatelessWidget { final bool selectMode; final ContextMenuBuilder? menuChildren; final FocusNode? focusNode; + final List listActions; static final responsiveUtils = getIt.get(); @@ -50,6 +52,7 @@ class MessageContentWithTimestampBuilder extends StatelessWidget { this.onMenuAction, this.menuChildren, this.focusNode, + required this.listActions, }); @override @@ -78,6 +81,7 @@ class MessageContentWithTimestampBuilder extends StatelessWidget { builder: menuChildren != null ? (context) => menuChildren!.call(context) : null, + listActions: listActions, child: Container( alignment: event.isOwnMessage ? Alignment.topRight : Alignment.topLeft, @@ -104,7 +108,9 @@ class MessageContentWithTimestampBuilder extends StatelessWidget { borderRadius: MessageStyle.bubbleBorderRadius, color: event.isOwnMessage ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceVariant, + : Theme.of(context) + .colorScheme + .surfaceContainerHighest, ), padding: noBubble ? const EdgeInsets.symmetric( diff --git a/lib/pages/chat/events/message/reply_icon_widget.dart b/lib/pages/chat/events/message/reply_icon_widget.dart index 792d682184..c68bfb0b33 100644 --- a/lib/pages/chat/events/message/reply_icon_widget.dart +++ b/lib/pages/chat/events/message/reply_icon_widget.dart @@ -7,9 +7,9 @@ class ReplyIconWidget extends StatelessWidget { final bool isOwnMessage; const ReplyIconWidget({ - Key? key, + super.key, required this.isOwnMessage, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 5067a8bb55..16e38e6ba4 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -42,14 +42,14 @@ class MessageContent extends StatelessWidget const MessageContent( this.event, { - Key? key, + super.key, required this.textColor, required this.endOfBubbleWidget, required this.backgroundColor, this.onTapPreview, this.onTapSelectMode, required this.ownMessage, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/message_content_style.dart b/lib/pages/chat/events/message_content_style.dart index 76dd06cf28..5b935424a2 100644 --- a/lib/pages/chat/events/message_content_style.dart +++ b/lib/pages/chat/events/message_content_style.dart @@ -86,4 +86,6 @@ class MessageContentStyle { ); static const blurhashSize = 32; + + static const iconErrorSize = 36.0; } diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 056298b4ab..6e7120f28c 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -16,9 +16,9 @@ class MessageDownloadContent extends StatefulWidget { const MessageDownloadContent( this.event, { - Key? key, + super.key, this.highlightText, - }) : super(key: key); + }); @override State createState() => _MessageDownloadContentState(); diff --git a/lib/pages/chat/events/message_download_content_web.dart b/lib/pages/chat/events/message_download_content_web.dart index 56261c388a..69d9992299 100644 --- a/lib/pages/chat/events/message_download_content_web.dart +++ b/lib/pages/chat/events/message_download_content_web.dart @@ -14,9 +14,9 @@ import 'package:matrix/matrix.dart'; class MessageDownloadContentWeb extends StatefulWidget { const MessageDownloadContentWeb( this.event, { - Key? key, + super.key, this.highlightText, - }) : super(key: key); + }); final Event event; diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index 3e9ed0910b..3b901053cf 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -17,8 +17,7 @@ class MessageReactions extends StatelessWidget { final Event event; final Timeline timeline; - const MessageReactions(this.event, this.timeline, {Key? key}) - : super(key: key); + const MessageReactions(this.event, this.timeline, {super.key}); @override Widget build(BuildContext context) { @@ -59,12 +58,12 @@ class MessageReactions extends StatelessWidget { class ReactionsList extends StatelessWidget { const ReactionsList({ - Key? key, + super.key, required this.reactionList, required this.allReactionEvents, required this.event, required this.client, - }) : super(key: key); + }); final List reactionList; final Set allReactionEvents; @@ -94,36 +93,34 @@ class ReactionsList extends StatelessWidget { ); }, children: [ - ...reactionList - .map( - (r) => _Reaction( - reactionKey: r.key, - count: r.count, - reacted: r.reacted, - onTap: () { - if (r.reacted) { - final evt = allReactionEvents.firstWhereOrNull((e) { - final relatedTo = e.content['m.relates_to']; - return e.senderId == e.room.client.userID && - relatedTo is Map && - relatedTo['key'] == r.key; - }); - if (evt != null) { - TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => evt.redactEvent(), - ); - } - } else { - event.room.sendReaction(event.eventId, r.key!); - } - }, - onLongPress: () async => await _AdaptableReactorsDialog( - client: client, - reactionEntry: r, - ).show(context), - ), - ) - .toList(), + ...reactionList.map( + (r) => _Reaction( + reactionKey: r.key, + count: r.count, + reacted: r.reacted, + onTap: () { + if (r.reacted) { + final evt = allReactionEvents.firstWhereOrNull((e) { + final relatedTo = e.content['m.relates_to']; + return e.senderId == e.room.client.userID && + relatedTo is Map && + relatedTo['key'] == r.key; + }); + if (evt != null) { + TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => evt.redactEvent(), + ); + } + } else { + event.room.sendReaction(event.eventId, r.key!); + } + }, + onLongPress: () async => await _AdaptableReactorsDialog( + client: client, + reactionEntry: r, + ).show(context), + ), + ), if (allReactionEvents.any((e) => e.status.isSending)) SizedBox( width: MessageReactionsStyle.loadingReactionSize, @@ -244,10 +241,9 @@ class _AdaptableReactorsDialog extends StatelessWidget { final ReactionEntry? reactionEntry; const _AdaptableReactorsDialog({ - Key? key, this.client, this.reactionEntry, - }) : super(key: key); + }); Future show(BuildContext context) => PlatformInfos.isCupertinoStyle ? showCupertinoDialog( diff --git a/lib/pages/chat/events/message_time.dart b/lib/pages/chat/events/message_time.dart index 0a22c54d34..f9ccd27762 100644 --- a/lib/pages/chat/events/message_time.dart +++ b/lib/pages/chat/events/message_time.dart @@ -13,13 +13,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class MessageTime extends StatelessWidget { const MessageTime({ - Key? key, + super.key, required this.event, required this.ownMessage, required this.timeline, required this.timelineOverlayMessage, required this.room, - }) : super(key: key); + }); final Event event; final bool ownMessage; diff --git a/lib/pages/chat/events/redacted_content.dart b/lib/pages/chat/events/redacted_content.dart index 635665c537..806b78ce23 100644 --- a/lib/pages/chat/events/redacted_content.dart +++ b/lib/pages/chat/events/redacted_content.dart @@ -8,7 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class RedactedContent extends StatelessWidget with MessageContentMixin { final Event event; - const RedactedContent({Key? key, required this.event}) : super(key: key); + const RedactedContent({super.key, required this.event}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index f78cf44f53..0188a559c8 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -22,9 +22,9 @@ class ReplyContent extends StatelessWidget { const ReplyContent( this.replyEvent, { this.ownMessage = false, - Key? key, + super.key, this.timeline, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -135,9 +135,9 @@ class ReplyPreviewIconBuilder extends StatelessWidget { final Event event; const ReplyPreviewIconBuilder({ - Key? key, + super.key, required this.event, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -176,9 +176,9 @@ class BlurHashPlaceHolder extends StatelessWidget { final Event event; const BlurHashPlaceHolder({ - Key? key, + super.key, required this.event, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index bcca83cdd6..b69caeb0dd 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -8,7 +8,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; class StateMessage extends StatelessWidget { final Event event; - const StateMessage(this.event, {Key? key}) : super(key: key); + const StateMessage(this.event, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/sticker.dart b/lib/pages/chat/events/sticker.dart index 5d573ecd48..b89a146be2 100644 --- a/lib/pages/chat/events/sticker.dart +++ b/lib/pages/chat/events/sticker.dart @@ -10,7 +10,7 @@ import 'images_builder/image_bubble.dart'; class Sticker extends StatefulWidget { final Event event; - const Sticker(this.event, {Key? key}) : super(key: key); + const Sticker(this.event, {super.key}); @override StickerState createState() => StickerState(); diff --git a/lib/pages/chat/events/unknown_content.dart b/lib/pages/chat/events/unknown_content.dart index 8f0c6f0971..e2a4ccad48 100644 --- a/lib/pages/chat/events/unknown_content.dart +++ b/lib/pages/chat/events/unknown_content.dart @@ -7,7 +7,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class UnknownContent extends StatelessWidget with MessageContentMixin { final Event event; - const UnknownContent({Key? key, required this.event}) : super(key: key); + const UnknownContent({super.key, required this.event}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/events/verification_request_content.dart b/lib/pages/chat/events/verification_request_content.dart index a7271125cb..bfaa4de3ad 100644 --- a/lib/pages/chat/events/verification_request_content.dart +++ b/lib/pages/chat/events/verification_request_content.dart @@ -12,8 +12,8 @@ class VerificationRequestContent extends StatelessWidget { const VerificationRequestContent({ required this.event, required this.timeline, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -39,6 +39,8 @@ class VerificationRequestContent extends StatelessWidget { color: Theme.of(context).dividerColor, ), borderRadius: BorderRadius.circular(AppConfig.borderRadius), + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.background, ), child: Row( diff --git a/lib/pages/chat/input_bar/input_bar.dart b/lib/pages/chat/input_bar/input_bar.dart index a46f3f5eaa..1ebac66e21 100644 --- a/lib/pages/chat/input_bar/input_bar.dart +++ b/lib/pages/chat/input_bar/input_bar.dart @@ -1,3 +1,6 @@ +// ignore_for_file: deprecated_member_use +// TODO: When changing from RawKeyboardListener to KeyboardListener, the keyboard up and down not working anymore. We will dive deeper into this issue later. + import 'package:emojis/emoji.dart'; import 'package:fluffychat/pages/chat/command_hints.dart'; import 'package:fluffychat/pages/chat/input_bar/focus_suggestion_controller.dart'; @@ -20,7 +23,7 @@ import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:slugify/slugify.dart'; -class InputBar extends StatelessWidget with PasteImageMixin { +class InputBar extends StatefulWidget { final Room? room; final int? minLines; final int? maxLines; @@ -38,8 +41,9 @@ class InputBar extends StatelessWidget with PasteImageMixin { final ValueKey? typeAheadKey; final ValueNotifier? showEmojiPickerNotifier; final SuggestionsController>? suggestionsController; + final bool isDraftChat; - InputBar({ + const InputBar({ this.room, this.minLines, this.maxLines, @@ -57,28 +61,42 @@ class InputBar extends StatelessWidget with PasteImageMixin { this.rawKeyboardFocusNode, this.suggestionsController, this.showEmojiPickerNotifier, - Key? key, - }) : super(key: key); + this.isDraftChat = false, + super.key, + }); static const debounceDuration = Duration(milliseconds: 50); static const debounceDurationTap = Duration(milliseconds: 100); + @override + State createState() => _InputBarState(); +} + +class _InputBarState extends State with PasteImageMixin { + final textFieldScrollController = ScrollController(); + + @override + void dispose() { + textFieldScrollController.dispose(); + super.dispose(); + } + List> getSuggestions(String text) { - if (controller!.selection.baseOffset != - controller!.selection.extentOffset || - controller!.selection.baseOffset < 0) { + if (widget.controller!.selection.baseOffset != + widget.controller!.selection.extentOffset || + widget.controller!.selection.baseOffset < 0) { return []; // no entries if there is selected text } - final searchText = - controller!.text.substring(0, controller!.selection.baseOffset); + final searchText = widget.controller!.text + .substring(0, widget.controller!.selection.baseOffset); final List> ret = >[]; const maxResults = 30; final commandMatch = RegExp(r'^/(\w*)$').firstMatch(searchText); - if (commandMatch != null && room != null) { + if (commandMatch != null && widget.room != null) { final commandSearch = commandMatch[1]!.toLowerCase(); - for (final command in room!.client.commands.keys) { + for (final command in widget.room!.client.commands.keys) { if (command.contains(commandSearch)) { ret.add({ 'type': 'command', @@ -91,10 +109,10 @@ class InputBar extends StatelessWidget with PasteImageMixin { } final emojiMatch = RegExp(r'(?:\s|^):(?:([-\w]+)~)?([-\w]+)$').firstMatch(searchText); - if (emojiMatch != null && room != null) { + if (emojiMatch != null && widget.room != null) { final packSearch = emojiMatch[1]; final emoteSearch = emojiMatch[2]!.toLowerCase(); - final emotePacks = room!.getImagePacks(ImagePackUsage.emoticon); + final emotePacks = widget.room!.getImagePacks(ImagePackUsage.emoticon); if (packSearch == null || packSearch.isEmpty) { for (final pack in emotePacks.entries) { for (final emote in pack.value.images.entries) { @@ -178,11 +196,11 @@ class InputBar extends StatelessWidget with PasteImageMixin { const userMentionsRegex = r'(?:\s|^)@([-\w]*)$'; final userMatch = RegExp(userMentionsRegex).firstMatch(searchText); - if (userMatch != null && room != null) { + if (userMatch != null && widget.room != null) { final userSearch = userMatch[1]!.toLowerCase(); - final users = room! + final users = widget.room! .getParticipants() - .where((user) => user.senderId != room!.client.userID) + .where((user) => user.senderId != widget.room!.client.userID) .toList(); for (final user in users) { if ((user.displayName != null && @@ -204,9 +222,9 @@ class InputBar extends StatelessWidget with PasteImageMixin { } } final roomMatch = RegExp(r'(?:\s|^)#([-\w]+)$').firstMatch(searchText); - if (roomMatch != null && room != null) { + if (roomMatch != null && widget.room != null) { final roomSearch = roomMatch[1]!.toLowerCase(); - for (final r in room!.client.rooms) { + for (final r in widget.room!.client.rooms) { if (r.getState(EventTypes.RoomTombstone) != null) { continue; // we don't care about tombstoned rooms } @@ -245,13 +263,14 @@ class InputBar extends StatelessWidget with PasteImageMixin { } void insertSuggestion(Map suggestion) { - if (room!.isDirectChat) return; - final replaceText = - controller!.text.substring(0, controller!.selection.baseOffset); + if (widget.room!.isDirectChat && !widget.isDraftChat) return; + final replaceText = widget.controller!.text + .substring(0, widget.controller!.selection.baseOffset); var startText = ''; - final afterText = replaceText == controller!.text + final afterText = replaceText == widget.controller!.text ? '' - : controller!.text.substring(controller!.selection.baseOffset + 1); + : widget.controller!.text + .substring(widget.controller!.selection.baseOffset + 1); var insertText = ''; if (suggestion['type'] == 'command') { insertText = '${suggestion['name']!} '; @@ -267,11 +286,11 @@ class InputBar extends StatelessWidget with PasteImageMixin { (Match m) => insertText, ); } - if (suggestion['type'] == 'emote' && room != null) { + if (suggestion['type'] == 'emote' && widget.room != null) { var isUnique = true; final insertEmote = suggestion['name']; final insertPack = suggestion['pack']; - final emotePacks = room!.getImagePacks(ImagePackUsage.emoticon); + final emotePacks = widget.room!.getImagePacks(ImagePackUsage.emoticon); for (final pack in emotePacks.entries) { if (pack.key == insertPack) { continue; @@ -313,8 +332,8 @@ class InputBar extends StatelessWidget with PasteImageMixin { ); } if (insertText.isNotEmpty && startText.isNotEmpty) { - controller!.text = startText + afterText; - controller!.selection = TextSelection( + widget.controller!.text = startText + afterText; + widget.controller!.selection = TextSelection( baseOffset: startText.length, extentOffset: startText.length, ); @@ -322,21 +341,22 @@ class InputBar extends StatelessWidget with PasteImageMixin { } Future handlePaste(BuildContext context) async { - if (await TwakeClipboard.instance.isReadableImageFormat() && room != null) { - await pasteImage(context, room!); + if (await TwakeClipboard.instance.isReadableImageFormat() && + widget.room != null) { + await pasteImage(context, widget.room!); } else { - await controller?.pasteText(); + await widget.controller?.pasteText(); } } void _onEnter(String text) { - if (focusSuggestionController.suggestions.isNotEmpty) { + if (widget.focusSuggestionController.suggestions.isNotEmpty) { insertSuggestion( - focusSuggestionController - .suggestions[focusSuggestionController.currentIndex.value], + widget.focusSuggestionController + .suggestions[widget.focusSuggestionController.currentIndex.value], ); } else { - onSubmitted?.call(text); + widget.onSubmitted?.call(text); } } @@ -353,45 +373,47 @@ class InputBar extends StatelessWidget with PasteImageMixin { } void onRawKeyEvent(RawKeyEvent event) { - if (focusSuggestionController.hasSuggestions) { - typeAheadFocusNode?.onKey = _onBlockUpDownArrowEvent; + if (widget.focusSuggestionController.hasSuggestions) { + widget.typeAheadFocusNode?.onKey = _onBlockUpDownArrowEvent; if (event.isKeyPressed(service.LogicalKeyboardKey.arrowUp)) { - focusSuggestionController.up(); + widget.focusSuggestionController.up(); } else if (event.isKeyPressed(service.LogicalKeyboardKey.arrowDown)) { - focusSuggestionController.down(); + widget.focusSuggestionController.down(); } } else { - typeAheadFocusNode?.onKey = _onIgnoreUpDownArrowEvent; + widget.typeAheadFocusNode?.onKey = _onIgnoreUpDownArrowEvent; } } void _handleSuggestionsCallbackWeb(List> suggestions) { if (suggestions.isNotEmpty) { - suggestionsController?.open(); + widget.suggestionsController?.open(); } else { - suggestionsController?.close(); - if (PlatformInfos.isWeb || showEmojiPickerNotifier?.value == false) { - typeAheadFocusNode?.requestFocus(); + widget.suggestionsController?.close(); + if (PlatformInfos.isWeb || + widget.showEmojiPickerNotifier?.value == false) { + widget.typeAheadFocusNode?.requestFocus(); } } } void _handleSuggestionsCallbackMobile() { - if (showEmojiPickerNotifier?.value == false) { - typeAheadFocusNode?.requestFocus(); + if (widget.showEmojiPickerNotifier?.value == false) { + widget.typeAheadFocusNode?.requestFocus(); } } @override Widget build(BuildContext context) { return InputBarShortcuts( - controller: controller, - focusSuggestionController: focusSuggestionController, - room: room, + controller: widget.controller, + focusSuggestionController: widget.focusSuggestionController, + scrollController: textFieldScrollController, + room: widget.room, onEnter: _onEnter, child: RawKeyboardListener( - key: typeAheadKey, - focusNode: rawKeyboardFocusNode ?? FocusNode(), + key: widget.typeAheadKey, + focusNode: widget.rawKeyboardFocusNode ?? FocusNode(), onKey: (event) { onRawKeyEvent(event); }, @@ -400,26 +422,27 @@ class InputBar extends StatelessWidget with PasteImageMixin { hideOnEmpty: true, hideOnLoading: true, hideOnSelect: false, - debounceDuration: debounceDuration, + debounceDuration: InputBar.debounceDuration, autoFlipDirection: true, - scrollController: suggestionScrollController, - suggestionsController: suggestionsController, - controller: controller, - focusNode: typeAheadFocusNode, + scrollController: widget.suggestionScrollController, + suggestionsController: widget.suggestionsController, + controller: widget.controller, + focusNode: widget.typeAheadFocusNode, builder: (context, controller, focusNode) => TextField( - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType, - textInputAction: textInputAction, - autofocus: autofocus, + minLines: widget.minLines, + maxLines: widget.maxLines, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + scrollController: textFieldScrollController, + autofocus: widget.autofocus, style: InputBarStyle.getTypeAheadTextStyle(context), controller: controller, - decoration: decoration, + decoration: widget.decoration, focusNode: focusNode, onChanged: (text) { - suggestionsController?.open(); - if (onChanged != null) { - onChanged!(text); + widget.suggestionsController?.open(); + if (widget.onChanged != null) { + widget.onChanged!(text); } }, contextMenuBuilder: PlatformInfos.isWeb @@ -429,20 +452,20 @@ class InputBar extends StatelessWidget with PasteImageMixin { editableTextState: editableTextState, ), onTap: () async { - await Future.delayed(debounceDurationTap); + await Future.delayed(InputBar.debounceDurationTap); FocusScope.of(context).requestFocus(focusNode); }, onSubmitted: PlatformInfos.isMobile ? (text) { - if (onSubmitted != null) { - onSubmitted!(text); + if (widget.onSubmitted != null) { + widget.onSubmitted!(text); } } : null, textCapitalization: TextCapitalization.sentences, ), suggestionsCallback: (text) { - if (room!.isDirectChat) return []; + if (widget.room!.isDirectChat) return []; final suggestions = getSuggestions(text); if (PlatformInfos.isMobile) { _handleSuggestionsCallbackMobile(); @@ -451,7 +474,7 @@ class InputBar extends StatelessWidget with PasteImageMixin { if (PlatformInfos.isWeb) { _handleSuggestionsCallbackWeb(suggestions); } - focusSuggestionController.suggestions = suggestions; + widget.focusSuggestionController.suggestions = suggestions; return suggestions; }, itemBuilder: (context, suggestion) => SuggestionTile( @@ -467,8 +490,8 @@ class InputBar extends StatelessWidget with PasteImageMixin { // fix loading briefly showing no suggestions listBuilder: (context, widgets) => FocusSuggestionList( items: widgets, - scrollController: suggestionScrollController, - focusSuggestionController: focusSuggestionController, + scrollController: widget.suggestionScrollController, + focusSuggestionController: widget.focusSuggestionController, ), ), ), diff --git a/lib/pages/chat/input_bar/input_bar_shortcut.dart b/lib/pages/chat/input_bar/input_bar_shortcut.dart index ee4194a403..b1541cca9b 100644 --- a/lib/pages/chat/input_bar/input_bar_shortcut.dart +++ b/lib/pages/chat/input_bar/input_bar_shortcut.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fluffychat/pages/chat/input_bar/focus_suggestion_controller.dart'; import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart'; import 'package:fluffychat/utils/one_time_debouncer.dart'; @@ -16,13 +18,16 @@ class InputBarShortcuts extends StatelessWidget { final FocusSuggestionController? focusSuggestionController; + final ScrollController scrollController; + InputBarShortcuts({ super.key, - required this.child, + required this.scrollController, this.room, this.controller, this.onEnter, this.focusSuggestionController, + required this.child, }); final _debouncer = OneTimeDebouncer(milliseconds: 50); @@ -37,6 +42,10 @@ class InputBarShortcuts extends StatelessWidget { ): () { _debouncer.run(() { controller?.addNewLine(); + Timer(const Duration(milliseconds: 50), () { + scrollController + .jumpTo(scrollController.position.maxScrollExtent); + }); }); }, const SingleActivator( diff --git a/lib/pages/chat/reactions_picker.dart b/lib/pages/chat/reactions_picker.dart index 6892a63d5f..77d90e256d 100644 --- a/lib/pages/chat/reactions_picker.dart +++ b/lib/pages/chat/reactions_picker.dart @@ -11,7 +11,7 @@ import '../../config/themes.dart'; class ReactionsPicker extends StatelessWidget { final ChatController controller; - const ReactionsPicker(this.controller, {Key? key}) : super(key: key); + const ReactionsPicker(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/recording_dialog.dart b/lib/pages/chat/recording_dialog.dart index 317ab7919d..7913efed18 100644 --- a/lib/pages/chat/recording_dialog.dart +++ b/lib/pages/chat/recording_dialog.dart @@ -15,8 +15,8 @@ import 'events/audio_player.dart'; class RecordingDialog extends StatefulWidget { static const String recordingFileType = 'm4a'; const RecordingDialog({ - Key? key, - }) : super(key: key); + super.key, + }); @override RecordingDialogState createState() => RecordingDialogState(); diff --git a/lib/pages/chat/reply_display.dart b/lib/pages/chat/reply_display.dart index 17f585e5d8..2514a5a767 100644 --- a/lib/pages/chat/reply_display.dart +++ b/lib/pages/chat/reply_display.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; class ReplyDisplay extends StatelessWidget { final ChatController controller; - const ReplyDisplay(this.controller, {Key? key}) : super(key: key); + const ReplyDisplay(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/seen_by_row.dart b/lib/pages/chat/seen_by_row.dart index 369b3430da..736d11bc7e 100644 --- a/lib/pages/chat/seen_by_row.dart +++ b/lib/pages/chat/seen_by_row.dart @@ -18,12 +18,12 @@ class SeenByRow extends StatelessWidget { const SeenByRow({ this.eventStatus, - Key? key, + super.key, required this.getSeenByUsers, required this.participants, required this.timelineOverlayMessage, required this.event, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/send_file_dialog/hover_actions_widget.dart b/lib/pages/chat/send_file_dialog/hover_actions_widget.dart index a5c06755c0..385e04d279 100644 --- a/lib/pages/chat/send_file_dialog/hover_actions_widget.dart +++ b/lib/pages/chat/send_file_dialog/hover_actions_widget.dart @@ -20,6 +20,8 @@ class SendFileDialogActionsWidget extends StatelessWidget { child: Container( decoration: BoxDecoration( shape: BoxShape.circle, + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.background, ), margin: SendFileDialogStyle.paddingRemoveButton, diff --git a/lib/pages/chat/send_file_dialog/send_file_dialog.dart b/lib/pages/chat/send_file_dialog/send_file_dialog.dart index 0ec1abd09a..a886be89b9 100644 --- a/lib/pages/chat/send_file_dialog/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog/send_file_dialog.dart @@ -18,8 +18,8 @@ class SendFileDialog extends StatefulWidget { const SendFileDialog({ this.room, required this.files, - Key? key, - }) : super(key: key); + super.key, + }); @override SendFileDialogController createState() => SendFileDialogController(); diff --git a/lib/pages/chat/send_file_dialog/send_file_dialog_style.dart b/lib/pages/chat/send_file_dialog/send_file_dialog_style.dart index 644fedfe31..60e6ccff99 100644 --- a/lib/pages/chat/send_file_dialog/send_file_dialog_style.dart +++ b/lib/pages/chat/send_file_dialog/send_file_dialog_style.dart @@ -36,7 +36,7 @@ class SendFileDialogStyle { ) .copyWith(letterSpacing: -0.15), filled: true, - fillColor: Theme.of(context).colorScheme.surfaceVariant, + fillColor: Theme.of(context).colorScheme.surfaceContainerHighest, ); static const spaceBwInputBarAndButton = SizedBox(height: 8.0); diff --git a/lib/pages/chat/send_file_dialog/send_file_dialog_view.dart b/lib/pages/chat/send_file_dialog/send_file_dialog_view.dart index dfce62a82f..e577280479 100644 --- a/lib/pages/chat/send_file_dialog/send_file_dialog_view.dart +++ b/lib/pages/chat/send_file_dialog/send_file_dialog_view.dart @@ -122,14 +122,14 @@ class SendFileDialogView extends StatelessWidget { SendMediaWithCaptionStatus.cancel, ), style: ButtonStyle( - shape: MaterialStatePropertyAll( + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular( SendFileDialogStyle.buttonBorderRadius, ), ), ), - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( SendFileDialogStyle.buttonPadding, ), ), @@ -147,17 +147,17 @@ class SendFileDialogView extends StatelessWidget { onPressed: controller.send, autofocus: true, style: ButtonStyle( - shape: MaterialStatePropertyAll( + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular( SendFileDialogStyle.buttonBorderRadius, ), ), ), - backgroundColor: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll( Theme.of(context).colorScheme.primary, ), - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( SendFileDialogStyle.buttonPadding, ), ), diff --git a/lib/pages/chat/sticker_picker_dialog.dart b/lib/pages/chat/sticker_picker_dialog.dart index 00246bd056..f7ed9fe691 100644 --- a/lib/pages/chat/sticker_picker_dialog.dart +++ b/lib/pages/chat/sticker_picker_dialog.dart @@ -9,7 +9,7 @@ import 'events/images_builder/image_bubble.dart'; class StickerPickerDialog extends StatefulWidget { final Room room; - const StickerPickerDialog({required this.room, Key? key}) : super(key: key); + const StickerPickerDialog({required this.room, super.key}); @override StickerPickerDialogState createState() => StickerPickerDialogState(); diff --git a/lib/pages/chat/sticky_timestamp_widget.dart b/lib/pages/chat/sticky_timestamp_widget.dart index 974ce12045..e509add13c 100644 --- a/lib/pages/chat/sticky_timestamp_widget.dart +++ b/lib/pages/chat/sticky_timestamp_widget.dart @@ -6,10 +6,10 @@ class StickyTimestampWidget extends StatelessWidget { final bool isStickyHeader; const StickyTimestampWidget({ - Key? key, + super.key, required this.content, this.isStickyHeader = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/tombstone_display.dart b/lib/pages/chat/tombstone_display.dart index ad8b971bc2..d2ca79ba49 100644 --- a/lib/pages/chat/tombstone_display.dart +++ b/lib/pages/chat/tombstone_display.dart @@ -7,7 +7,7 @@ import 'chat.dart'; class TombstoneDisplay extends StatelessWidget { final ChatController controller; - const TombstoneDisplay(this.controller, {Key? key}) : super(key: key); + const TombstoneDisplay(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -17,7 +17,7 @@ class TombstoneDisplay extends StatelessWidget { return SizedBox( height: 72, child: Material( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, elevation: 1, child: ListTile( leading: CircleAvatar( diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index acbf9760e1..da2588e09a 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/widgets/matrix.dart'; class TypingIndicators extends StatelessWidget { final ChatController controller; - const TypingIndicators(this.controller, {Key? key}) : super(key: key); + const TypingIndicators(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat/widgets_bottom_sheet.dart b/lib/pages/chat/widgets_bottom_sheet.dart index 7a52929cef..9ea89f614a 100644 --- a/lib/pages/chat/widgets_bottom_sheet.dart +++ b/lib/pages/chat/widgets_bottom_sheet.dart @@ -9,7 +9,7 @@ import 'edit_widgets_dialog.dart'; class WidgetsBottomSheet extends StatelessWidget { final Room room; - const WidgetsBottomSheet({Key? key, required this.room}) : super(key: key); + const WidgetsBottomSheet({super.key, required this.room}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart index 19525e500c..1811a5731d 100644 --- a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart +++ b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold.dart @@ -13,11 +13,11 @@ class ChatAdaptiveScaffold extends StatefulWidget { final String? roomName; const ChatAdaptiveScaffold({ - Key? key, + super.key, required this.roomId, this.shareFiles, this.roomName, - }) : super(key: key); + }); @override State createState() => ChatAdaptiveScaffoldController(); diff --git a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart index 324db4a8e4..bfc570fc92 100644 --- a/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart +++ b/lib/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_builder.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pages/chat/events/message/message_style.dart'; import 'package:fluffychat/pages/chat_adaptive_scaffold/chat_adaptive_scaffold_style.dart'; import 'package:fluffychat/presentation/enum/chat/right_column_type_enum.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -38,6 +37,8 @@ class ChatAdaptiveScaffoldBuilderController extends State { final rightColumnTypeNotifier = ValueNotifier(null); + final responsiveUtils = ResponsiveUtils(); + void hideRightColumn() { if (PlatformInfos.isMobile) { Navigator.of(context).pop(); @@ -70,8 +71,7 @@ class ChatAdaptiveScaffoldBuilderController @override Widget build(BuildContext context) { - const breakpoint = ResponsiveUtils.minTabletWidth + - MessageStyle.messageBubbleDesktopMaxWidth; + final breakpoint = responsiveUtils.getMinDesktopWidth(context); return ValueListenableBuilder( valueListenable: rightColumnTypeNotifier, builder: (context, rightColumnType, body) { @@ -80,7 +80,7 @@ class ChatAdaptiveScaffoldBuilderController child: AdaptiveLayout( body: SlotLayout( config: { - const WidthPlatformBreakpoint( + WidthPlatformBreakpoint( end: breakpoint, ): SlotLayout.from( key: AppAdaptiveScaffold.breakpointMobileKey, @@ -96,7 +96,7 @@ class ChatAdaptiveScaffoldBuilderController ], ), ), - const WidthPlatformBreakpoint( + WidthPlatformBreakpoint( begin: breakpoint, ): SlotLayout.from( key: AppAdaptiveScaffold.breakpointWebAndDesktopKey, @@ -117,13 +117,13 @@ class ChatAdaptiveScaffoldBuilderController secondaryBody: rightColumnType != null ? SlotLayout( config: { - const WidthPlatformBreakpoint( + WidthPlatformBreakpoint( end: breakpoint, ): SlotLayout.from( key: AppAdaptiveScaffold.breakpointMobileKey, builder: null, ), - const WidthPlatformBreakpoint( + WidthPlatformBreakpoint( begin: breakpoint, ): SlotLayout.from( key: AppAdaptiveScaffold.breakpointWebAndDesktopKey, diff --git a/lib/pages/chat_blank/chat_blank.dart b/lib/pages/chat_blank/chat_blank.dart index 3620c84da5..7fa0461aff 100644 --- a/lib/pages/chat_blank/chat_blank.dart +++ b/lib/pages/chat_blank/chat_blank.dart @@ -13,7 +13,7 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class ChatBlank extends StatelessWidget { final bool loading; - const ChatBlank({this.loading = false, Key? key}) : super(key: key); + const ChatBlank({this.loading = false, super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 9d3a2c4a69..9016b1ecce 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -1,36 +1,16 @@ -import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/room/room_extension.dart'; import 'package:fluffychat/pages/chat_details/chat_details_edit.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_members_page.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_page_enum.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart'; import 'package:fluffychat/pages/chat_details/chat_details_view_style.dart'; -import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_controller.dart'; -import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; -import 'package:fluffychat/pages/invitation_selection/invitation_selection_web.dart'; -import 'package:fluffychat/presentation/extensions/room_summary_extension.dart'; +import 'package:fluffychat/presentation/enum/chat/chat_details_screen_enum.dart'; +import 'package:fluffychat/presentation/mixins/chat_details_tab_mixin.dart'; import 'package:fluffychat/presentation/mixins/handle_video_download_mixin.dart'; 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/clipboard.dart'; -import 'package:fluffychat/utils/dialog/twake_dialog.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/responsive/responsive_utils.dart'; -import 'package:fluffychat/utils/scroll_controller_extension.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/cupertino.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:matrix/matrix.dart'; - import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; enum AliasActions { copy, delete, setCanonical } @@ -55,266 +35,38 @@ class ChatDetailsController extends State with HandleVideoDownloadMixin, PlayVideoActionMixin, - SingleTickerProviderStateMixin { - static const _mediaFetchLimit = 20; - - static const _linksFetchLimit = 20; - - static const _filesFetchLimit = 20; - - final invitationSelectionMobileAndTabletKey = - const Key('InvitationSelectionMobileAndTabletKey'); - - final invitationSelectionWebAndDesktopKey = - const Key('InvitationSelectionWebAndDesktopKey'); - + SingleTickerProviderStateMixin, + ChatDetailsTabMixin { final actionsMobileAndTabletKey = const Key('ActionsMobileAndTabletKey'); final actionsWebAndDesktopKey = const Key('ActionsWebAndDesktopKey'); - final GlobalKey nestedScrollViewState = GlobalKey(); - - final List chatDetailsPageView = [ - ChatDetailsPage.members, - ChatDetailsPage.media, - ChatDetailsPage.links, - ChatDetailsPage.files, - ]; - - final responsive = getIt.get(); - - final Map _mediaCacheMap = {}; - final muteNotifier = ValueNotifier( PushRuleState.notify, ); - SameTypeEventsBuilderController? mediaListController; - SameTypeEventsBuilderController? linksListController; - SameTypeEventsBuilderController? filesListController; - - Room? room; - - TabController? tabController; + @override + Room? get room => Matrix.of(context).client.getRoomById(widget.roomId); - ValueNotifier?> membersNotifier = ValueNotifier(null); + @override + ChatDetailsScreenEnum get chatType => ChatDetailsScreenEnum.group; String? get roomId => widget.roomId; - bool get isMobileAndTablet => - responsive.isMobile(context) || responsive.isTablet(context); - - Timeline? _timeline; - - Future getTimeline() async { - _timeline ??= await room!.getTimeline(); - return _timeline!; - } - - int get actualMembersCount => room!.summary.actualMembersCount; - @override void initState() { super.initState(); - tabController = TabController( - length: chatDetailsPageView.length, - vsync: this, - ); - mediaListController = SameTypeEventsBuilderController( - getTimeline: getTimeline, - searchFunc: (event) => event.isVideoOrImage, - limit: _mediaFetchLimit, - ); - linksListController = SameTypeEventsBuilderController( - getTimeline: getTimeline, - searchFunc: (event) => event.isContainsLink, - limit: _linksFetchLimit, - ); - filesListController = SameTypeEventsBuilderController( - getTimeline: getTimeline, - searchFunc: (event) => event.isAFile, - limit: _filesFetchLimit, - ); - WidgetsBinding.instance.addPostFrameCallback((_) { - nestedScrollViewState.currentState?.innerController.addListener( - _listenerInnerController, - ); - _refreshDataInTabviewInit(); - }); - room = Matrix.of(context).client.getRoomById(roomId!); + initValueNotifiers(); + } + + void initValueNotifiers() { muteNotifier.value = room?.pushRuleState ?? PushRuleState.notify; - membersNotifier.value ??= - Matrix.of(context).client.getRoomById(roomId!)!.getParticipants(); } @override void dispose() { - tabController?.dispose(); - muteNotifier.dispose(); - mediaListController?.dispose(); - linksListController?.dispose(); - filesListController?.dispose(); - nestedScrollViewState.currentState?.innerController.dispose(); super.dispose(); - } - - void _listenerInnerController() { - Logs().d("ChatDetails::currentTab - ${tabController?.index}"); - if (nestedScrollViewState.currentState?.innerController.shouldLoadMore == - true && - tabController?.index != null) { - switch (chatDetailsPageView[tabController!.index]) { - case ChatDetailsPage.media: - mediaListController?.loadMore(); - break; - case ChatDetailsPage.links: - linksListController?.loadMore(); - break; - case ChatDetailsPage.files: - filesListController?.loadMore(); - break; - default: - break; - } - } - } - - void _refreshDataInTabviewInit() { - linksListController?.refresh(); - mediaListController?.refresh(); - filesListController?.refresh(); - } - - void requestMoreMembersAction() async { - final room = Matrix.of(context).client.getRoomById(roomId!); - final participants = await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => room!.requestParticipants(), - ); - if (participants.error == null) { - membersNotifier.value = participants.result; - } - } - - void openDialogInvite() { - if (PlatformInfos.isMobile) { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (_) => InvitationSelection( - roomId: roomId!, - ), - ), - ); - return; - } - showDialog( - context: context, - barrierDismissible: false, - useSafeArea: false, - useRootNavigator: !PlatformInfos.isMobile, - builder: (context) { - return SlotLayout( - config: { - const WidthPlatformBreakpoint( - begin: ResponsiveUtils.minDesktopWidth, - ): SlotLayout.from( - key: invitationSelectionWebAndDesktopKey, - builder: (_) => InvitationSelectionWebView( - roomId: roomId!, - ), - ), - const WidthPlatformBreakpoint( - end: ResponsiveUtils.minDesktopWidth, - ): SlotLayout.from( - key: invitationSelectionMobileAndTabletKey, - builder: (_) => InvitationSelection( - roomId: roomId!, - ), - ), - }, - ); - }, - ); - } - - Future onUpdateMembers() async { - final members = await room!.requestParticipantsFromServer(); - membersNotifier.value = members; - } - - List chatDetailsPages() => chatDetailsPageView.map( - (page) { - switch (page) { - case ChatDetailsPage.members: - return ChatDetailsPageModel( - page: page, - child: ChatDetailsMembersPage( - key: const PageStorageKey("members"), - membersNotifier: membersNotifier, - actualMembersCount: actualMembersCount, - canRequestMoreMembers: - (membersNotifier.value?.length ?? 0) < actualMembersCount, - requestMoreMembersAction: requestMoreMembersAction, - openDialogInvite: openDialogInvite, - isMobileAndTablet: isMobileAndTablet, - onUpdatedMembers: onUpdateMembers, - ), - ); - case ChatDetailsPage.media: - return ChatDetailsPageModel( - page: page, - child: mediaListController == null - ? const SizedBox() - : ChatDetailsMediaPage( - key: const PageStorageKey('Media'), - controller: mediaListController!, - cacheMap: _mediaCacheMap, - handleDownloadVideoEvent: _handleDownloadAndPlayVideo, - closeRightColumn: widget.closeRightColumn, - ), - ); - case ChatDetailsPage.links: - return ChatDetailsPageModel( - page: page, - child: linksListController == null - ? const SizedBox() - : ChatDetailsLinksPage( - key: const PageStorageKey('Links'), - controller: linksListController!, - ), - ); - case ChatDetailsPage.files: - return ChatDetailsPageModel( - page: page, - child: filesListController == null - ? const SizedBox() - : ChatDetailsFilesPage( - key: const PageStorageKey('Files'), - controller: filesListController!, - ), - ); - default: - return ChatDetailsPageModel( - page: page, - child: const SizedBox(), - ); - } - }, - ).toList(); - - Future _handleDownloadAndPlayVideo(Event event) { - return handleDownloadVideoEvent( - event: event, - playVideoAction: (path) => playVideoAction( - context, - path, - event: event, - isReplacement: false, - ), - ); - } - - void onTapAddMembers() { - openDialogInvite(); + muteNotifier.dispose(); } void onToggleNotification() async { diff --git a/lib/pages/chat_details/chat_details_edit_view.dart b/lib/pages/chat_details/chat_details_edit_view.dart index 61572242d5..5c638648ae 100644 --- a/lib/pages/chat_details/chat_details_edit_view.dart +++ b/lib/pages/chat_details/chat_details_edit_view.dart @@ -20,8 +20,8 @@ class ChatDetailsEditView extends StatelessWidget { const ChatDetailsEditView( this.controller, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -138,17 +138,17 @@ class ChatDetailsEditView extends StatelessWidget { : menuController.open(), }, style: ButtonStyle( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( const CircleBorder(), ), - padding: MaterialStateProperty.all( + padding: WidgetStateProperty.all( ChatDetailEditViewStyle .editIconMaterialPadding, ), - iconColor: MaterialStateProperty.all( + iconColor: WidgetStateProperty.all( Theme.of(context).colorScheme.onPrimary, ), - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.primary, ), ), @@ -185,7 +185,7 @@ class ChatDetailsEditView extends StatelessWidget { ), ), Container( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, alignment: Alignment.centerLeft, padding: const EdgeInsets.all(8.0), child: Text( @@ -307,9 +307,8 @@ class _AvatarBuilder extends StatelessWidget { class _GroupNameField extends StatelessWidget { const _GroupNameField({ - Key? key, required this.controller, - }) : super(key: key); + }); final ChatDetailsEditController controller; @@ -357,9 +356,8 @@ class _GroupNameField extends StatelessWidget { class _DescriptionField extends StatelessWidget { const _DescriptionField({ - Key? key, required this.controller, - }) : super(key: key); + }); final ChatDetailsEditController controller; diff --git a/lib/pages/chat_details/chat_details_navigator.dart b/lib/pages/chat_details/chat_details_navigator.dart index 0ecf44cdc1..3837b56011 100644 --- a/lib/pages/chat_details/chat_details_navigator.dart +++ b/lib/pages/chat_details/chat_details_navigator.dart @@ -2,7 +2,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_details/chat_details_edit.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:flutter/material.dart'; class ChatDetailsRoutes { @@ -17,12 +17,12 @@ class ChatDetailsNavigator extends StatelessWidget { final bool isInStack; const ChatDetailsNavigator({ - Key? key, + super.key, this.closeRightColumn, this.roomId, this.contact, required this.isInStack, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart b/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart index f64e6ce794..45bc7f1c87 100644 --- a/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/chat_details_members_page.dart @@ -50,7 +50,10 @@ class ChatDetailsMembersPage extends StatelessWidget { onUpdatedMembers: onUpdatedMembers, ); } - + final haveMoreMembers = actualMembersCount > members.length; + if (!haveMoreMembers) { + return const SizedBox.shrink(); + } return ListTile( title: Text( L10n.of(context)!.loadCountMoreParticipants( diff --git a/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart b/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart index f8c49d4328..d8ef6aa168 100644 --- a/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart @@ -10,9 +10,9 @@ class ChatDetailsLinksPage extends StatelessWidget { final SameTypeEventsBuilderController controller; const ChatDetailsLinksPage({ - Key? key, + super.key, required this.controller, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart index 1ca9f2278d..3e7aa34edf 100644 --- a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart @@ -25,12 +25,12 @@ class ChatDetailsMediaPage extends StatelessWidget { final VoidCallback? closeRightColumn; const ChatDetailsMediaPage({ - Key? key, + super.key, required this.controller, required this.handleDownloadVideoEvent, this.cacheMap, this.closeRightColumn, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart index ce58e957a7..2391737b90 100644 --- a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart @@ -20,6 +20,8 @@ class ChatDetailsMediaStyle { static Decoration durationBoxDecoration(BuildContext context) => ShapeDecoration( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), shape: const StadiumBorder(), ); diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 21bd455249..331b30592e 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -14,7 +14,7 @@ import 'package:fluffychat/utils/string_extension.dart'; class ChatDetailsView extends StatelessWidget { final ChatDetailsController controller; - const ChatDetailsView(this.controller, {Key? key}) : super(key: key); + const ChatDetailsView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -178,7 +178,7 @@ class ChatDetailsView extends StatelessWidget { forceElevated: innerBoxIsScrolled, bottom: TabBar( physics: const NeverScrollableScrollPhysics(), - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( Colors.transparent, ), indicatorSize: TabBarIndicatorSize.tab, @@ -198,10 +198,10 @@ class ChatDetailsView extends StatelessWidget { color: Theme.of(context).colorScheme.onSurfaceVariant, ), - tabs: controller.chatDetailsPages().map((pages) { + tabs: controller.tabList.map((page) { return Tab( child: Text( - pages.page.getTitle(context), + page.getTitle(context), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.fade, @@ -226,7 +226,7 @@ class ChatDetailsView extends StatelessWidget { child: TabBarView( physics: const NeverScrollableScrollPhysics(), controller: controller.tabController, - children: controller.chatDetailsPages().map((pages) { + children: controller.sharedPages().map((pages) { return pages.child; }).toList(), ), @@ -241,9 +241,8 @@ class ChatDetailsView extends StatelessWidget { class _AddMembersButton extends StatelessWidget { const _AddMembersButton({ - Key? key, required this.controller, - }) : super(key: key); + }); final ChatDetailsController controller; @@ -296,9 +295,8 @@ class _AddMembersButton extends StatelessWidget { class _TileSubtitleText extends StatelessWidget { const _TileSubtitleText({ - Key? key, required this.subtitle, - }) : super(key: key); + }); final String subtitle; @@ -316,9 +314,8 @@ class _TileSubtitleText extends StatelessWidget { class _TileTitleText extends StatelessWidget { const _TileTitleText({ - Key? key, required this.title, - }) : super(key: key); + }); final String title; @@ -335,11 +332,10 @@ class _TileTitleText extends StatelessWidget { class _GroupInformation extends StatelessWidget { const _GroupInformation({ - Key? key, this.avatarUri, this.displayName, this.membersCount, - }) : super(key: key); + }); final Uri? avatarUri; final String? displayName; diff --git a/lib/pages/chat_details/participant_list_item/participant_list_item.dart b/lib/pages/chat_details/participant_list_item/participant_list_item.dart index 82bc93f237..e059fa7509 100644 --- a/lib/pages/chat_details/participant_list_item/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item/participant_list_item.dart @@ -9,7 +9,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; @@ -21,9 +20,9 @@ class ParticipantListItem extends StatelessWidget { const ParticipantListItem( this.member, { - Key? key, + super.key, this.onUpdatedMembers, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -129,7 +128,7 @@ class ParticipantListItem extends StatelessWidget { userId: member.id, onUpdatedMembers: onUpdatedMembers, onNewChatOpen: () { - dialogContext.pop(); + Navigator.of(dialogContext).pop(); }, ); }, @@ -228,7 +227,7 @@ class ParticipantListItem extends StatelessWidget { child: Padding( padding: ParticipantListItemStyle.closeButtonPadding, child: IconButton( - onPressed: () => dialogContext.pop(), + onPressed: () => Navigator.of(dialogContext).pop(), icon: const Icon(Icons.close), ), ), @@ -236,7 +235,7 @@ class ParticipantListItem extends StatelessWidget { ProfileInfoBody( user: member, onNewChatOpen: () { - dialogContext.pop(); + Navigator.of(dialogContext).pop(); }, onUpdatedMembers: onUpdatedMembers, ), diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index 4c2f3f87bf..dedc30a039 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -17,7 +17,7 @@ import 'package:fluffychat/presentation/mixins/common_media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/send_files_mixin.dart'; import 'package:fluffychat/presentation/model/chat/chat_router_input_argument.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/network_connection_service.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -89,6 +89,8 @@ class DraftChatController extends State ValueNotifier showEmojiPickerNotifier = ValueNotifier(false); + final ValueNotifier _userProfile = ValueNotifier(null); + EmojiPickerType emojiPickerType = EmojiPickerType.keyboard; final isSendingNotifier = ValueNotifier(false); @@ -146,6 +148,9 @@ class DraftChatController extends State void initState() { scrollController.addListener(_updateScrollController); keyboardVisibilityController.onChange.listen(_keyboardListener); + WidgetsBinding.instance.addPostFrameCallback((_) { + _getProfile(); + }); super.initState(); } @@ -157,6 +162,7 @@ class DraftChatController extends State focusSuggestionController.dispose(); inputText.dispose(); showEmojiPickerNotifier.dispose(); + _userProfile.dispose(); super.dispose(); } @@ -166,6 +172,22 @@ class DraftChatController extends State Matrix.of(context).setActiveClient(c); }); + Future _triggerTagGreetingMessage() async { + if (_userProfile.value == null) { + return sendController.value.text; + } else { + final displayName = _userProfile.value?.displayName; + if (sendController.value.text.contains(displayName ?? '') == true) { + return sendController.value.text.replaceAll( + "${displayName ?? ''}!", + presentationContact?.matrixId ?? '', + ); + } else { + return sendController.value.text; + } + } + } + Future sendText({ OnRoomCreatedFailed onCreateRoomFailed, }) async { @@ -175,11 +197,12 @@ class DraftChatController extends State selection: const TextSelection.collapsed(offset: 0), ); inputFocus.unfocus(); + final textEvent = await _triggerTagGreetingMessage(); isSendingNotifier.value = true; _createRoom( onRoomCreatedSuccess: (room) { room.sendTextEvent( - sendController.text, + textEvent, ); }, onRoomCreatedFailed: onCreateRoomFailed, @@ -206,12 +229,11 @@ class DraftChatController extends State final room = Matrix.of(context).client.getRoomById(success.roomId); if (room != null) { onRoomCreatedSuccess?.call(room); - final user = await _getProfile(); context.go( '/rooms/${room.id}/', extra: ChatRouterInputArgument( type: ChatRouterInputArgumentType.draft, - data: user.displayName ?? + data: _userProfile.value?.displayName ?? presentationContact?.displayName ?? room.name, ), @@ -361,26 +383,28 @@ class DraftChatController extends State } Future handleDraftAction(BuildContext context) async { - Profile? profile; - try { - profile = await _getProfile(); - } catch (e) { - Logs().e('Error getting profile: $e'); - } inputFocus.requestFocus(); sendController.value = TextEditingValue( text: L10n.of(context)!.draftChatHookPhrase( - profile?.displayName ?? presentationContact?.displayName ?? '', + _userProfile.value?.displayName ?? + presentationContact?.displayName ?? + '', ), ); onInputBarChanged(sendController.text); } - Future _getProfile() async { - return await Matrix.of(context).client.getProfileFromUserId( - presentationContact!.matrixId!, - getFromRooms: false, - ); + Future _getProfile() async { + try { + final profile = await Matrix.of(context).client.getProfileFromUserId( + presentationContact!.matrixId!, + getFromRooms: false, + ); + _userProfile.value = profile; + } catch (e) { + Logs().e('Error _getProfile profile: $e'); + _userProfile.value = null; + } } void hideKeyboardChatScreen() { diff --git a/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart b/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart index 29cf4d800c..8c749ded4f 100644 --- a/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart +++ b/lib/pages/chat_draft/draft_chat_adaptive_scaffold.dart @@ -1,8 +1,8 @@ import 'package:fluffychat/pages/chat_draft/draft_chat.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_navigator.dart'; import 'package:fluffychat/presentation/enum/chat/right_column_type_enum.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_constant.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -12,9 +12,9 @@ class DraftChatAdaptiveScaffold extends StatelessWidget { final GoRouterState state; const DraftChatAdaptiveScaffold({ - Key? key, + super.key, required this.state, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_draft/draft_chat_empty_widget.dart b/lib/pages/chat_draft/draft_chat_empty_widget.dart index 3225bc1563..1e69142ceb 100644 --- a/lib/pages/chat_draft/draft_chat_empty_widget.dart +++ b/lib/pages/chat_draft/draft_chat_empty_widget.dart @@ -5,9 +5,9 @@ class DraftChatEmpty extends StatelessWidget { final void Function()? onTap; const DraftChatEmpty({ - Key? key, + super.key, this.onTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_draft/draft_chat_input_row.dart b/lib/pages/chat_draft/draft_chat_input_row.dart index b3175270ac..c88fc16819 100644 --- a/lib/pages/chat_draft/draft_chat_input_row.dart +++ b/lib/pages/chat_draft/draft_chat_input_row.dart @@ -27,7 +27,7 @@ class DraftChatInputRow extends StatelessWidget { final FocusSuggestionController focusSuggestionController; const DraftChatInputRow({ - Key? key, + super.key, required this.onSendFileClick, required this.inputText, required this.onInputBarSubmitted, @@ -40,7 +40,7 @@ class DraftChatInputRow extends StatelessWidget { this.typeAheadFocusNode, this.textEditingController, required this.focusSuggestionController, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -110,6 +110,7 @@ class DraftChatInputRow extends StatelessWidget { ), onChanged: onInputBarChanged, focusSuggestionController: focusSuggestionController, + isDraftChat: true, ); } } diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart index e777bb23a2..ec8a9c4585 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart @@ -13,7 +13,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import '../key_verification/key_verification_dialog.dart'; class ChatEncryptionSettings extends StatefulWidget { - const ChatEncryptionSettings({Key? key}) : super(key: key); + const ChatEncryptionSettings({super.key}); @override ChatEncryptionSettingsController createState() => diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index 172fdc4cfd..fe1b04aa2f 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -11,8 +11,7 @@ import 'package:fluffychat/utils/beautify_string_extension.dart'; class ChatEncryptionSettingsView extends StatelessWidget { final ChatEncryptionSettingsController controller; - const ChatEncryptionSettingsView(this.controller, {Key? key}) - : super(key: key); + const ChatEncryptionSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index cbb0720a55..fa0d3b8306 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -6,12 +6,9 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/first_column_inner_routes.dart'; import 'package:fluffychat/di/global/dio_cache_interceptor_for_client.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; import 'package:fluffychat/domain/model/room/room_extension.dart'; -import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; -import 'package:fluffychat/pages/multiple_accounts/multiple_accounts_picker.dart'; -import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; +import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_security/settings_security.dart'; @@ -26,7 +23,9 @@ 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/context_menu/context_menu_action.dart'; import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; @@ -53,12 +52,12 @@ class ChatList extends StatefulWidget { final AbsAppAdaptiveScaffoldBodyArgs? adaptiveScaffoldBodyArgs; const ChatList({ - Key? key, + super.key, required this.activeRoomIdNotifier, this.bottomNavigationBar, this.onOpenSettings, this.adaptiveScaffoldBodyArgs, - }) : super(key: key); + }); @override ChatListController createState() => ChatListController(); @@ -74,8 +73,6 @@ class ChatListController extends State PopupMenuWidgetMixin, GoToGroupChatMixin, TwakeContextMenuMixin { - final _getRecoveryWordsInteractor = getIt.get(); - final responsive = getIt.get(); final ValueNotifier expandRoomsForAllNotifier = ValueNotifier(true); @@ -85,10 +82,6 @@ class ChatListController extends State final ValueNotifier selectModeNotifier = ValueNotifier(SelectMode.normal); - final ValueNotifier currentProfileNotifier = ValueNotifier( - Profile(userId: ''), - ); - final ValueNotifier> conversationSelectionNotifier = ValueNotifier([]); @@ -108,8 +101,6 @@ class ChatListController extends State bool isTorBrowser = false; - bool waitForFirstSync = false; - bool scrolledToTop = true; Client get activeClient => matrixState.client; @@ -309,12 +300,13 @@ class ChatListController extends State Future toggleMutedSelections() async { await TwakeDialog.showFutureLoadingDialogFullScreen( future: () async { + final newRuleState = pushRuleState; for (final conversation in conversationSelectionNotifier.value) { final room = activeClient.getRoomById(conversation.roomId)!; - if (room.pushRuleState == pushRuleState) continue; + if (room.pushRuleState == newRuleState) continue; await activeClient .getRoomById(conversation.roomId)! - .setPushRuleState(pushRuleState); + .setPushRuleState(newRuleState); } }, ); @@ -416,51 +408,43 @@ class ChatListController extends State DioCacheInterceptorForClient(userId).setup(getIt); } + Future _trySync() async { + if (widget.adaptiveScaffoldBodyArgs is LoggedInBodyArgs || + widget.adaptiveScaffoldBodyArgs is LoggedInOtherAccountBodyArgs) { + _waitForFirstSyncAfterLogin(); + } else { + _waitForFirstSync(); + } + } + + Future _waitForFirstSyncAfterLogin() async { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final result = await TomBootstrapDialog( + client: activeClient, + ).show(); + + setState(() {}); + + if (result == false) { + await BootstrapDialog(client: activeClient).show(); + } + }); + + if (!mounted) return; + setState(() { + matrixState.waitForFirstSync = true; + }); + } + Future _waitForFirstSync() async { await activeClient.roomsLoading; await activeClient.accountDataLoading; if (activeClient.userID != null) { await setupAdditionalDioCacheOption(activeClient.userID!); } - if (activeClient.prevBatch == null) { - await activeClient.onSync.stream.first; - await activeClient.initCompleter?.future; - - // Display first login bootstrap if enabled - if (activeClient.encryption?.keyManager.enabled == true) { - Logs().d( - 'ChatList::_waitForFirstSync: Showing bootstrap dialog when encryption is enabled', - ); - if (await activeClient.encryption?.keyManager.isCached() == false || - await activeClient.encryption?.crossSigning.isCached() == false || - activeClient.isUnknownSession && mounted) { - final recoveryWords = await _getRecoveryWords(); - if (recoveryWords != null) { - await TomBootstrapDialog( - client: activeClient, - recoveryWords: recoveryWords, - ).show(); - } else { - Logs().d( - 'ChatListController::_waitForFirstSync(): no recovery existed then call bootstrap', - ); - await BootstrapDialog(client: activeClient).show(); - } - } - } else { - Logs().d( - 'ChatListController::_waitForFirstSync(): encryption is not enabled', - ); - final recoveryWords = await _getRecoveryWords(); - await TomBootstrapDialog( - client: activeClient, - wipeRecovery: recoveryWords != null, - ).show(); - } - } if (!mounted) return; setState(() { - waitForFirstSync = true; + matrixState.waitForFirstSync = true; }); } @@ -545,19 +529,28 @@ class ChatListController extends State BuildContext context, Room room, TapDownDetails details, - ) { + ) async { final offset = details.globalPosition; - showTwakeContextMenu( + final listPopupActions = _popupMenuActions(room); + final listContextActions = _mapPopupMenuActionsToContextMenuActions( + context, + room, + listPopupActions, + ); + final selectedActionIndex = await showTwakeContextMenu( offset: offset, context: context, - builder: (context) => _popupMenuActionTile(context, room), + listActions: listContextActions, ); + if (selectedActionIndex != null && selectedActionIndex is int) { + _handleClickOnContextMenuItem( + listPopupActions[selectedActionIndex], + room, + ); + } } - List _popupMenuActionTile( - BuildContext context, - Room room, - ) { + List _popupMenuActions(Room room) { final listAction = [ if (!room.isInvitation) ...[ ChatListSelectionActions.read, @@ -565,15 +558,18 @@ class ChatListController extends State ], ChatListSelectionActions.mute, ]; - return listAction.map((action) { - return popupItemByTwakeAppRouter( - context, - action.getTitleContextMenuSelection(context, room), - iconAction: action.getIconContextMenuSelection(room), - onCallbackAction: () => _handleClickOnContextMenuItem( - action, - room, - ), + return listAction; + } + + List _mapPopupMenuActionsToContextMenuActions( + BuildContext context, + Room room, + List listActions, + ) { + return listActions.map((action) { + return ContextMenuAction( + name: action.getTitleContextMenuSelection(context, room), + icon: action.getIconContextMenuSelection(room), ); }).toList(); } @@ -713,15 +709,6 @@ class ChatListController extends State isTorBrowser = isTor; } - Future _getRecoveryWords() async { - return await _getRecoveryWordsInteractor.execute().then( - (either) => either.fold( - (failure) => null, - (success) => success.words, - ), - ); - } - Future dehydrate() => SettingsSecurityController.dehydrateDevice(context); @@ -748,27 +735,8 @@ class ChatListController extends State } } - void _getCurrentProfile(Client client) async { - final profile = await client.getProfileFromUserId( - client.userID!, - getFromRooms: false, - ); - Logs().d( - 'ChatList::_getCurrentProfile() - currentProfile: $profile', - ); - currentProfileNotifier.value = profile; - } - - void onGoToAccountSettings() { - widget.onOpenSettings?.call(); - } - void onClickAvatar() { - MultipleAccountsPickerController(context: context) - .showMultipleAccountsPicker( - activeClient, - onGoToAccountSettings: onGoToAccountSettings, - ); + context.push('/rooms/profile'); } void _handleRecovery() { @@ -776,7 +744,9 @@ class ChatListController extends State Logs().d( "ChatList::_handleAnotherAccountAdded(): Handle recovery data for another account", ); - _waitForFirstSync(); + if (!matrixState.waitForFirstSync) { + _trySync(); + } } } @@ -791,7 +761,6 @@ class ChatListController extends State ); if (newActiveClient != null && newActiveClient.userID != null) { setState(() { - _getCurrentProfile(newActiveClient); _clientStream.add(newActiveClient); _handleRecovery(); }); @@ -806,16 +775,16 @@ class ChatListController extends State } activeRoomIdNotifier.value = widget.activeRoomIdNotifier.value; scrollController.addListener(_onScroll); - _waitForFirstSync(); + if (!matrixState.waitForFirstSync) { + _trySync(); + } _hackyWebRTCFixForWeb(); - _getCurrentProfile(activeClient); // TODO: 28Dec2023 Disable callkeep for util we support audio/video calls // CallKeepManager().initialize(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { Matrix.of(context).backgroundPush?.setupPush(); await matrixState.retrievePersistedActiveClient(); - _getCurrentProfile(activeClient); } }); _checkTorBrowser(); diff --git a/lib/pages/chat_list/chat_list_body_view.dart b/lib/pages/chat_list/chat_list_body_view.dart index a62988a163..e2ff36e10d 100644 --- a/lib/pages/chat_list/chat_list_body_view.dart +++ b/lib/pages/chat_list/chat_list_body_view.dart @@ -19,7 +19,7 @@ import 'package:matrix/matrix.dart'; class ChatListBodyView extends StatelessWidget { final ChatListController controller; - const ChatListBodyView(this.controller, {Key? key}) : super(key: key); + const ChatListBodyView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -59,7 +59,7 @@ class ChatListBodyView extends StatelessWidget { key: Key(controller.activeSpaceId ?? 'Spaces'), ); } - if (controller.waitForFirstSync && + if (controller.matrixState.waitForFirstSync && controller.activeClient.prevBatch != null) { if (controller.chatListBodyIsEmpty) { return Column( diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index e141149380..4c8bbe65d8 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -12,18 +12,22 @@ class ChatListHeader extends StatelessWidget { final VoidCallback? onOpenSearchPageInMultipleColumns; const ChatListHeader({ - Key? key, + super.key, required this.controller, this.onOpenSearchPageInMultipleColumns, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return Column( children: [ TwakeHeader( - controller: controller, onClearSelection: controller.onClickClearSelection, + client: controller.activeClient, + selectModeNotifier: controller.selectModeNotifier, + conversationSelectionNotifier: + controller.conversationSelectionNotifier, + onClickAvatar: controller.onClickAvatar, ), Container( height: ChatListHeaderStyle.searchBarContainerHeight, diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 092435ff90..e1c78d5e74 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -38,8 +38,8 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin { this.onTapAvatar, this.onSecondaryTapDown, this.onLongPress, - Key? key, - }) : super(key: key); + super.key, + }); void clickAction(BuildContext context) async { if (onTap != null) return onTap!(); diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 6b04f3f108..865c135fdd 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -23,12 +23,12 @@ class ChatListView extends StatelessWidget { final responsiveUtils = getIt.get(); ChatListView({ - Key? key, + super.key, required this.controller, this.bottomNavigationBar, this.onOpenSearchPageInMultipleColumns, required this.onTapBottomNavigation, - }) : super(key: key); + }); static const ValueKey bottomNavigationKey = ValueKey('BottomNavigation'); diff --git a/lib/pages/chat_list/navi_rail_item.dart b/lib/pages/chat_list/navi_rail_item.dart index 83fb6a5775..b337ce8a11 100644 --- a/lib/pages/chat_list/navi_rail_item.dart +++ b/lib/pages/chat_list/navi_rail_item.dart @@ -16,8 +16,8 @@ class NaviRailItem extends StatelessWidget { required this.onTap, required this.icon, this.selectedIcon, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -51,6 +51,8 @@ class NaviRailItem extends StatelessWidget { borderRadius: BorderRadius.circular(AppConfig.borderRadius), color: isSelected ? Theme.of(context).colorScheme.primaryContainer + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use : Theme.of(context).colorScheme.background, child: Padding( padding: const EdgeInsets.symmetric( diff --git a/lib/pages/chat_list/search_title.dart b/lib/pages/chat_list/search_title.dart index 3ed0e64d4d..62bcfb6848 100644 --- a/lib/pages/chat_list/search_title.dart +++ b/lib/pages/chat_list/search_title.dart @@ -13,8 +13,8 @@ class SearchTitle extends StatelessWidget { this.trailing, this.onTap, this.color, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) => Material( diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index aad5a1b29f..b06e93fe53 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -20,9 +20,9 @@ class SpaceView extends StatefulWidget { final ScrollController scrollController; const SpaceView( this.controller, { - Key? key, + super.key, required this.scrollController, - }) : super(key: key); + }); @override State createState() => _SpaceViewState(); @@ -160,6 +160,8 @@ class _SpaceViewState extends State { MatrixLocals(L10n.of(context)!), ); return Material( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.background, child: ListTile( leading: Avatar( @@ -324,7 +326,7 @@ class _SpaceViewState extends State { : L10n.of(context)!.enterRoom), maxLines: 1, style: TextStyle( - color: Theme.of(context).colorScheme.onBackground, + color: Theme.of(context).colorScheme.onSurface, ), ), trailing: diff --git a/lib/pages/chat_list/stories_header.dart b/lib/pages/chat_list/stories_header.dart index 9fe253fae9..6cad580f14 100644 --- a/lib/pages/chat_list/stories_header.dart +++ b/lib/pages/chat_list/stories_header.dart @@ -21,7 +21,7 @@ enum ContextualRoomAction { class StoriesHeader extends StatelessWidget { final String filter; - const StoriesHeader({required this.filter, Key? key}) : super(key: key); + const StoriesHeader({required this.filter, super.key}); void _addToStoryAction(BuildContext context) => context.go('/stories/create'); @@ -170,8 +170,7 @@ class _StoryButton extends StatelessWidget { this.hasPosts = true, this.unread = false, this.onLongPressed, - Key? key, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -206,7 +205,9 @@ class _StoryButton extends StatelessWidget { : null, color: unread ? null - : Theme.of(context).colorScheme.surfaceVariant, + : Theme.of(context) + .colorScheme + .surfaceContainerHighest, borderRadius: BorderRadius.circular(AvatarStyle.defaultSize), ), diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart index 3c6dccb721..c64ac2ca89 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart @@ -14,7 +14,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/permission_slider_dialog.dart'; class ChatPermissionsSettings extends StatefulWidget { - const ChatPermissionsSettings({Key? key}) : super(key: key); + const ChatPermissionsSettings({super.key}); @override ChatPermissionsSettingsController createState() => diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart index f0bb64fb02..2fd2de41c6 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart @@ -11,8 +11,7 @@ import 'package:fluffychat/widgets/matrix.dart'; class ChatPermissionsSettingsView extends StatelessWidget { final ChatPermissionsSettingsController controller; - const ChatPermissionsSettingsView(this.controller, {Key? key}) - : super(key: key); + const ChatPermissionsSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/chat_permissions_settings/permission_list_tile.dart b/lib/pages/chat_permissions_settings/permission_list_tile.dart index f16a40d8a0..75d695db6e 100644 --- a/lib/pages/chat_permissions_settings/permission_list_tile.dart +++ b/lib/pages/chat_permissions_settings/permission_list_tile.dart @@ -10,12 +10,12 @@ class PermissionsListTile extends StatelessWidget { final void Function()? onTap; const PermissionsListTile({ - Key? key, + super.key, required this.permissionKey, required this.permission, this.category, this.onTap, - }) : super(key: key); + }); String getLocalizedPowerLevelString(BuildContext context) { if (category == null) { diff --git a/lib/pages/chat_profile_info/chat_profile_info.dart b/lib/pages/chat_profile_info/chat_profile_info.dart index b68e439815..b99f4081c9 100644 --- a/lib/pages/chat_profile_info/chat_profile_info.dart +++ b/lib/pages/chat_profile_info/chat_profile_info.dart @@ -1,16 +1,18 @@ import 'dart:async'; - import 'package:dartz/dartz.dart' hide State; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; import 'package:fluffychat/domain/usecase/contacts/lookup_match_contact_interactor.dart'; -import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_view.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/enum/chat/chat_details_screen_enum.dart'; +import 'package:fluffychat/presentation/mixins/chat_details_tab_mixin.dart'; +import 'package:fluffychat/presentation/mixins/handle_video_download_mixin.dart'; +import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; class ChatProfileInfo extends StatefulWidget { @@ -33,7 +35,12 @@ class ChatProfileInfo extends StatefulWidget { State createState() => ChatProfileInfoController(); } -class ChatProfileInfoController extends State { +class ChatProfileInfoController extends State + with + HandleVideoDownloadMixin, + PlayVideoActionMixin, + SingleTickerProviderStateMixin, + ChatDetailsTabMixin { final _lookupMatchContactInteractor = getIt.get(); @@ -44,10 +51,14 @@ class ChatProfileInfoController extends State { const Right(LookupContactsInitial()), ); + @override Room? get room => widget.roomId != null ? Matrix.of(context).client.getRoomById(widget.roomId!) : null; + @override + ChatDetailsScreenEnum get chatType => ChatDetailsScreenEnum.direct; + User? get user => room?.unsafeGetUserFromMemoryOrFallback(room?.directChatMatrixID ?? ''); @@ -61,31 +72,25 @@ class ChatProfileInfoController extends State { ); } - void goToProfileShared() { - if (widget.isDraftInfo || widget.roomId == null) return; - Navigator.of(context).push( - CupertinoPageRoute( - builder: (context) { - return ChatProfileInfoShared( - roomId: widget.roomId!, - closeRightColumn: widget.onBack, - ); - }, - ), - ); + ScrollPhysics getScrollPhysics() { + if (tabList.isEmpty) { + return const NeverScrollableScrollPhysics(); + } else { + return const ClampingScrollPhysics(); + } } @override void initState() { - lookupMatchContactAction(); super.initState(); + lookupMatchContactAction(); } @override void dispose() { + super.dispose(); lookupContactNotifier.dispose(); lookupContactNotifierSub?.cancel(); - super.dispose(); } @override diff --git a/lib/pages/chat_profile_info/chat_profile_info_navigator.dart b/lib/pages/chat_profile_info/chat_profile_info_navigator.dart index 5a3f66159a..eeb053a3fe 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_navigator.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_navigator.dart @@ -1,13 +1,11 @@ -import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/cupertino.dart'; import 'package:fluffychat/pages/chat_profile_info/chat_profile_info.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; class ChatProfileInfoRoutes { static const String profileInfo = '/profileInfo'; - static const String profileInfoShared = 'profileInfo/shared'; } class ChatProfileInfoNavigator extends StatelessWidget { @@ -18,13 +16,13 @@ class ChatProfileInfoNavigator extends StatelessWidget { final bool isDraftInfo; const ChatProfileInfoNavigator({ - Key? key, + super.key, this.onBack, this.roomId, this.contact, required this.isInStack, this.isDraftInfo = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -51,10 +49,6 @@ class ChatProfileInfoNavigator extends StatelessWidget { isDraftInfo: isDraftInfo, ); - case ChatProfileInfoRoutes.profileInfoShared: - return ChatProfileInfoShared( - roomId: route.arguments as String, - ); default: return const SizedBox(); } diff --git a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart deleted file mode 100644 index d609c635ea..0000000000 --- a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_page_enum.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart'; -import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart'; -import 'package:fluffychat/presentation/mixins/handle_video_download_mixin.dart'; -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/presentation/same_type_events_builder/same_type_events_controller.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/scroll_controller_extension.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; - -class ChatProfileInfoShared extends StatefulWidget { - final String roomId; - final VoidCallback? closeRightColumn; - - const ChatProfileInfoShared({ - super.key, - required this.roomId, - this.closeRightColumn, - }); - - @override - State createState() => - ChatProfileInfoSharedController(); -} - -class ChatProfileInfoSharedController extends State - with - HandleVideoDownloadMixin, - PlayVideoActionMixin, - SingleTickerProviderStateMixin { - static const _mediaFetchLimit = 20; - - static const _linksFetchLimit = 20; - - static const _filesFetchLimit = 20; - - SameTypeEventsBuilderController? mediaListController; - - SameTypeEventsBuilderController? linksListController; - - SameTypeEventsBuilderController? filesListController; - - TabController? tabController; - - Timeline? _timeline; - - final GlobalKey nestedScrollViewState = GlobalKey(); - - final List profileSharedPageView = [ - ChatDetailsPage.media, - ChatDetailsPage.links, - ChatDetailsPage.files, - ]; - - Future getTimeline() async { - _timeline ??= await room!.getTimeline(); - return _timeline!; - } - - Room? get room => Matrix.of(context).client.getRoomById(widget.roomId); - - List profileSharedPages() => profileSharedPageView.map( - (page) { - switch (page) { - case ChatDetailsPage.media: - return ChatDetailsPageModel( - page: page, - child: mediaListController == null - ? const SizedBox() - : ChatDetailsMediaPage( - key: const PageStorageKey( - 'ChatProfileInfoSharedMedia', - ), - controller: mediaListController!, - handleDownloadVideoEvent: _handleDownloadAndPlayVideo, - closeRightColumn: widget.closeRightColumn, - ), - ); - case ChatDetailsPage.links: - return ChatDetailsPageModel( - page: page, - child: linksListController == null - ? const SizedBox() - : ChatDetailsLinksPage( - key: const PageStorageKey( - 'ChatProfileInfoSharedLinks', - ), - controller: linksListController!, - ), - ); - case ChatDetailsPage.files: - return ChatDetailsPageModel( - page: page, - child: filesListController == null - ? const SizedBox() - : ChatDetailsFilesPage( - key: const PageStorageKey( - 'ChatProfileInfoSharedFiles', - ), - controller: filesListController!, - ), - ); - default: - return ChatDetailsPageModel( - page: page, - child: const SizedBox(), - ); - } - }, - ).toList(); - - Future _handleDownloadAndPlayVideo(Event event) { - return handleDownloadVideoEvent( - event: event, - playVideoAction: (path) => playVideoAction( - context, - path, - event: event, - ), - ); - } - - void _listenerInnerController() { - Logs().d("ChatDetails::currentTab - ${tabController?.index}"); - if (nestedScrollViewState.currentState?.innerController.shouldLoadMore == - true && - tabController?.index != null) { - switch (profileSharedPageView[tabController!.index]) { - case ChatDetailsPage.media: - mediaListController?.loadMore(); - break; - case ChatDetailsPage.links: - linksListController?.loadMore(); - break; - case ChatDetailsPage.files: - filesListController?.loadMore(); - break; - default: - break; - } - } - } - - void _refreshDataInTabviewInit() { - linksListController?.refresh(); - mediaListController?.refresh(); - filesListController?.refresh(); - } - - @override - void initState() { - tabController = TabController( - length: profileSharedPageView.length, - vsync: this, - ); - mediaListController = SameTypeEventsBuilderController( - getTimeline: getTimeline, - searchFunc: (event) => event.isVideoOrImage, - limit: _mediaFetchLimit, - ); - linksListController = SameTypeEventsBuilderController( - getTimeline: getTimeline, - searchFunc: (event) => event.isContainsLink, - limit: _linksFetchLimit, - ); - filesListController = SameTypeEventsBuilderController( - getTimeline: getTimeline, - searchFunc: (event) => event.isAFile, - limit: _filesFetchLimit, - ); - WidgetsBinding.instance.addPostFrameCallback((_) { - nestedScrollViewState.currentState?.innerController.addListener( - _listenerInnerController, - ); - _refreshDataInTabviewInit(); - }); - super.initState(); - } - - @override - void dispose() { - tabController?.dispose(); - mediaListController?.dispose(); - linksListController?.dispose(); - filesListController?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ChatProfileInfoSharedView( - controller: this, - ); - } -} diff --git a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart deleted file mode 100644 index f7bdd78d9d..0000000000 --- a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'package:fluffychat/pages/chat_details/chat_details_view_style.dart'; -import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart'; -import 'package:fluffychat/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view_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'; - -class ChatProfileInfoSharedView extends StatelessWidget { - final ChatProfileInfoSharedController controller; - - const ChatProfileInfoSharedView({ - super.key, - required this.controller, - }); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - centerTitle: false, - leading: Padding( - padding: ChatProfileInfoSharedViewStyle.backIconPadding, - child: IconButton( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - highlightColor: Colors.transparent, - onPressed: () => Navigator.pop(context), - icon: const Icon( - Icons.arrow_back, - size: ChatProfileInfoSharedViewStyle.leadingSize, - ), - ), - ), - leadingWidth: ChatProfileInfoSharedViewStyle.leadingWidth, - title: Text( - L10n.of(context)!.sharedMediaAndLinks, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - body: NestedScrollView( - physics: const ClampingScrollPhysics(), - key: controller.nestedScrollViewState, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - toolbarHeight: ChatProfileInfoSharedViewStyle.toolbarHeight, - automaticallyImplyLeading: false, - pinned: true, - floating: true, - forceElevated: innerBoxIsScrolled, - bottom: TabBar( - physics: const NeverScrollableScrollPhysics(), - overlayColor: MaterialStateProperty.all( - Colors.transparent, - ), - tabs: controller.profileSharedPages().map((pages) { - return Tab( - child: Text( - pages.page.getTitle(context), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleSmall, - ), - ); - }).toList(), - controller: controller.tabController, - ), - ), - ), - ]; - }, - body: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular( - ChatDetailViewStyle.chatDetailsPageViewWebBorderRadius, - ), - ), - child: Container( - width: ChatDetailViewStyle.chatDetailsPageViewWebWidth, - padding: ChatDetailViewStyle.paddingTabBarView, - decoration: BoxDecoration( - color: LinagoraRefColors.material().primary[100], - ), - child: TabBarView( - physics: const NeverScrollableScrollPhysics(), - controller: controller.tabController, - children: controller.profileSharedPages().map((pages) { - return pages.child; - }).toList(), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view_style.dart b/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view_style.dart deleted file mode 100644 index 3f4cc74425..0000000000 --- a/lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view_style.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class ChatProfileInfoSharedViewStyle { - static const double leadingSize = 24; - static const double leadingWidth = 40; - static const double toolbarHeight = 0; - - static const EdgeInsetsGeometry backIconPadding = - EdgeInsets.symmetric(vertical: 8, horizontal: 4); -} diff --git a/lib/pages/chat_profile_info/chat_profile_info_style.dart b/lib/pages/chat_profile_info/chat_profile_info_style.dart index cc250dbb88..6e2a6518b9 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_style.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_style.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; class ChatProfileInfoStyle { static const double iconPadding = 8; @@ -11,6 +12,10 @@ class ChatProfileInfoStyle { static const double avatarFontSize = 36; static const double avatarSize = 96; + static const double toolbarHeightSliverAppBar = 340.0; + + static const double indicatorWeight = 3.0; + static BorderRadius copiableContainerBorderRadius = BorderRadius.circular(16); static const EdgeInsetsGeometry mainPadding = @@ -30,4 +35,22 @@ class ChatProfileInfoStyle { static const EdgeInsetsGeometry titleSharedMediaAndFilesPadding = EdgeInsets.only(top: 30); + + static const EdgeInsetsGeometry indicatorPadding = EdgeInsets.symmetric( + horizontal: 12.0, + ); + + static TextStyle? tabBarLabelStyle(BuildContext context) => + Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + ); + + static TextStyle? tabBarUnselectedLabelStyle(BuildContext context) => + Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ); + + static Decoration tabViewDecoration = BoxDecoration( + color: LinagoraRefColors.material().primary[100], + ); } diff --git a/lib/pages/chat_profile_info/chat_profile_info_view.dart b/lib/pages/chat_profile_info/chat_profile_info_view.dart index c0cfcc492d..29660f9e15 100644 --- a/lib/pages/chat_profile_info/chat_profile_info_view.dart +++ b/lib/pages/chat_profile_info/chat_profile_info_view.dart @@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/contact/lookup_match_contact_state.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_view_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/string_extension.dart'; @@ -55,48 +56,108 @@ class ChatProfileInfoView extends StatelessWidget { ], ), ), - body: SingleChildScrollView( - child: Center( - child: ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: ChatProfileInfoStyle.maxWidth), - child: Builder( - builder: (context) { - if (contact?.matrixId != null) { - return FutureBuilder( - future: Matrix.of(context).client.getProfileFromUserId( - contact!.matrixId!, - getFromRooms: false, - ), - builder: (context, snapshot) => _Information( - avatarUri: snapshot.data?.avatarUrl, - displayName: - snapshot.data?.displayName ?? contact.displayName, - matrixId: contact.matrixId, - lookupContactNotifier: controller.lookupContactNotifier, - goToProfileShared: controller.goToProfileShared, - isDraftInfo: controller.widget.isDraftInfo, + body: NestedScrollView( + physics: controller.getScrollPhysics(), + key: controller.nestedScrollViewState, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + backgroundColor: LinagoraSysColors.material().onPrimary, + toolbarHeight: ChatDetailViewStyle.toolbarHeightSliverAppBar, + title: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: ChatProfileInfoStyle.maxWidth, ), - ); - } - if (contact != null) { - return _Information( - displayName: contact.displayName, - matrixId: contact.matrixId, - lookupContactNotifier: controller.lookupContactNotifier, - goToProfileShared: controller.goToProfileShared, - isDraftInfo: controller.widget.isDraftInfo, - ); - } - return _Information( - avatarUri: user?.avatarUrl, - displayName: user?.calcDisplayname(), - matrixId: user?.id, - lookupContactNotifier: controller.lookupContactNotifier, - goToProfileShared: controller.goToProfileShared, - isDraftInfo: controller.widget.isDraftInfo, - ); - }, + child: Builder( + builder: (context) { + if (contact?.matrixId != null) { + return FutureBuilder( + future: + Matrix.of(context).client.getProfileFromUserId( + contact!.matrixId!, + getFromRooms: false, + ), + builder: (context, snapshot) => _Information( + avatarUri: snapshot.data?.avatarUrl, + displayName: snapshot.data?.displayName ?? + contact.displayName, + matrixId: contact.matrixId, + lookupContactNotifier: + controller.lookupContactNotifier, + isDraftInfo: controller.widget.isDraftInfo, + ), + ); + } + if (contact != null) { + return _Information( + displayName: contact.displayName, + matrixId: contact.matrixId, + lookupContactNotifier: + controller.lookupContactNotifier, + isDraftInfo: controller.widget.isDraftInfo, + ); + } + return _Information( + avatarUri: user?.avatarUrl, + displayName: user?.calcDisplayname(), + matrixId: user?.id, + lookupContactNotifier: + controller.lookupContactNotifier, + isDraftInfo: controller.widget.isDraftInfo, + ); + }, + ), + ), + ), + automaticallyImplyLeading: false, + pinned: true, + floating: true, + forceElevated: innerBoxIsScrolled, + bottom: TabBar( + physics: const NeverScrollableScrollPhysics(), + overlayColor: WidgetStateProperty.all(Colors.transparent), + indicatorSize: TabBarIndicatorSize.tab, + indicatorColor: Theme.of(context).colorScheme.primary, + indicatorPadding: ChatProfileInfoStyle.indicatorPadding, + indicatorWeight: ChatProfileInfoStyle.indicatorWeight, + labelStyle: ChatProfileInfoStyle.tabBarLabelStyle(context), + unselectedLabelStyle: + ChatProfileInfoStyle.tabBarUnselectedLabelStyle(context), + tabs: controller.tabList.map((page) { + return Tab( + child: Text( + page.getTitle(context), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.fade, + ), + ); + }).toList(), + controller: controller.tabController, + ), + ), + ), + ]; + }, + body: ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular( + ChatDetailViewStyle.chatDetailsPageViewWebBorderRadius, + ), + ), + child: Container( + width: ChatDetailViewStyle.chatDetailsPageViewWebWidth, + padding: ChatDetailViewStyle.paddingTabBarView, + decoration: ChatProfileInfoStyle.tabViewDecoration, + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + controller: controller.tabController, + children: controller.sharedPages().map((page) { + return page.child; + }).toList(), ), ), ), @@ -107,20 +168,17 @@ class ChatProfileInfoView extends StatelessWidget { class _Information extends StatelessWidget { const _Information({ - Key? key, this.avatarUri, this.displayName, this.matrixId, required this.lookupContactNotifier, - this.goToProfileShared, required this.isDraftInfo, - }) : super(key: key); + }); final Uri? avatarUri; final String? displayName; final String? matrixId; final ValueNotifier> lookupContactNotifier; - final Function()? goToProfileShared; final bool isDraftInfo; @override @@ -242,31 +300,6 @@ class _Information extends StatelessWidget { ], ), ), - if (!isDraftInfo) - InkWell( - splashColor: Colors.transparent, - hoverColor: Colors.transparent, - highlightColor: Colors.transparent, - onTap: goToProfileShared, - child: Padding( - padding: - ChatProfileInfoStyle.titleSharedMediaAndFilesPadding, - child: Row( - children: [ - Text( - L10n.of(context)!.sharedMediaAndLinks, - style: Theme.of(context).textTheme.titleMedium, - ), - const Spacer(), - Icon( - Icons.arrow_forward, - size: 18, - color: LinagoraSysColors.material().onSurface, - ), - ], - ), - ), - ), ], ), ), @@ -277,10 +310,9 @@ class _Information extends StatelessWidget { class _CopiableRowWithMaterialIcon extends StatelessWidget { const _CopiableRowWithMaterialIcon({ - Key? key, required this.icon, required this.text, - }) : super(key: key); + }); final IconData icon; final String text; @@ -328,10 +360,9 @@ class _CopiableRowWithMaterialIcon extends StatelessWidget { class _CopiableRowWithSvgIcon extends StatelessWidget { const _CopiableRowWithSvgIcon({ - Key? key, required this.iconPath, required this.text, - }) : super(key: key); + }); final String iconPath; final String text; diff --git a/lib/pages/chat_search/chat_search.dart b/lib/pages/chat_search/chat_search.dart index 9c642ff181..f9331b155e 100644 --- a/lib/pages/chat_search/chat_search.dart +++ b/lib/pages/chat_search/chat_search.dart @@ -59,6 +59,7 @@ class ChatSearchController extends State { ), ); serverSearchController.initSearch( + context: context, onSearchEncryptedMessage: sameTypeEventsBuilderController != null ? _listenSearchEncryptedMessage : null, diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index 61ea3c69fb..13552e785a 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -34,8 +34,8 @@ class ChatSearchView extends StatelessWidget { const ChatSearchView( this.controller, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/connect/connect_page.dart b/lib/pages/connect/connect_page.dart index 915371ee71..03a7514d71 100644 --- a/lib/pages/connect/connect_page.dart +++ b/lib/pages/connect/connect_page.dart @@ -5,7 +5,7 @@ import 'package:fluffychat/pages/connect/connect_page_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ConnectPage extends StatefulWidget { - const ConnectPage({Key? key}) : super(key: key); + const ConnectPage({super.key}); @override State createState() => ConnectPageController(); diff --git a/lib/pages/connect/connect_page_view.dart b/lib/pages/connect/connect_page_view.dart index 6d1c8f8322..f91250a08a 100644 --- a/lib/pages/connect/connect_page_view.dart +++ b/lib/pages/connect/connect_page_view.dart @@ -13,7 +13,7 @@ import 'sso_button.dart'; class ConnectPageView extends StatelessWidget { final ConnectPageController controller; - const ConnectPageView(this.controller, {Key? key}) : super(key: key); + const ConnectPageView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/connect/sso_button.dart b/lib/pages/connect/sso_button.dart index f8fe1e1641..bf6c0aeef4 100644 --- a/lib/pages/connect/sso_button.dart +++ b/lib/pages/connect/sso_button.dart @@ -10,10 +10,10 @@ class SsoButton extends StatelessWidget { final IdentityProvider identityProvider; final void Function()? onPressed; const SsoButton({ - Key? key, + super.key, required this.identityProvider, this.onPressed, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/connect/sso_login_state.dart b/lib/pages/connect/sso_login_state.dart new file mode 100644 index 0000000000..c4e5257d8b --- /dev/null +++ b/lib/pages/connect/sso_login_state.dart @@ -0,0 +1,5 @@ +enum SsoLoginState { + success, + error, + tokenEmpty, +} diff --git a/lib/pages/contacts_tab/contacts_tab.dart b/lib/pages/contacts_tab/contacts_tab.dart index 7dc5b3bd5d..801d685863 100644 --- a/lib/pages/contacts_tab/contacts_tab.dart +++ b/lib/pages/contacts_tab/contacts_tab.dart @@ -2,10 +2,13 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/mixins/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/pages/contacts_tab/contacts_tab_view.dart'; import 'package:fluffychat/presentation/mixins/contacts_view_controller_mixin.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_constant.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/utils/string_extension.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; @@ -28,7 +31,15 @@ class ContactsTabController extends State @override void initState() { - initialFetchContacts(); + SchedulerBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + initialFetchContacts( + client: Matrix.of(context).client, + matrixLocalizations: MatrixLocals(L10n.of(context)!), + ); + } + }); + _listenFocusTextEditing(); super.initState(); } diff --git a/lib/pages/contacts_tab/contacts_tab_body_view.dart b/lib/pages/contacts_tab/contacts_tab_body_view.dart index d169d347dc..bd4387826f 100644 --- a/lib/pages/contacts_tab/contacts_tab_body_view.dart +++ b/lib/pages/contacts_tab/contacts_tab_body_view.dart @@ -7,8 +7,12 @@ 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.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_success.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_empty.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_failure.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/contacts_warning_banner/contacts_warning_banner_view.dart'; import 'package:fluffychat/widgets/sliver_expandable_list.dart'; import 'package:flutter/material.dart'; @@ -29,6 +33,7 @@ class ContactsTabBodyView extends StatelessWidget { slivers: [ _SliverWarningBanner(controller: controller), _SliverPhonebookLoading(controller: controller), + _SliverRecentContacts(controller: controller), _SliverContactsList(controller: controller), _SliverPhonebookList(controller: controller), const _SliverPadding(), @@ -62,21 +67,91 @@ class _SliverPhonebookList extends StatelessWidget { return ValueListenableBuilder( valueListenable: controller.presentationPhonebookContactNotifier, builder: (context, phonebookContactState, child) { - final success = phonebookContactState - .getSuccessOrNull(); - if (success == null || success.contacts.isEmpty) { - return const SliverToBoxAdapter(); - } - final contacts = success.contacts; - return SliverExpandableList( - title: L10n.of(context)!.contactsCount(contacts.length), - itemCount: contacts.length, - itemBuilder: (context, index) => _Contact( - contact: contacts[index], - controller: controller, - ), + return phonebookContactState.fold( + (failure) { + if (!PlatformInfos.isMobile) { + return child!; + } + final presentationRecentContact = + controller.presentationRecentContactNotifier.value; + if (failure is GetPresentationContactsFailure) { + if (presentationRecentContact.isEmpty) { + return controller.presentationContactNotifier.value.fold( + (failure) { + if (failure is GetPresentationContactsFailure || + failure is GetPresentationContactsEmpty) { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + left: ContactsTabViewStyle.padding, + top: ContactsTabViewStyle.padding, + ), + child: NoContactsFound( + keyword: + controller.textEditingController.text.isEmpty + ? null + : controller.textEditingController.text, + ), + ), + ); + } + return child!; + }, + (_) => child!, + ); + } + } + if (failure is GetPresentationContactsEmpty) { + if (presentationRecentContact.isEmpty) { + return controller.presentationContactNotifier.value.fold( + (failure) { + if (failure is GetPresentationContactsFailure || + failure is GetPresentationContactsEmpty) { + if (controller.textEditingController.text.isEmpty) { + return const SliverToBoxAdapter( + child: EmptyContactBody(), + ); + } else { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + left: ContactsTabViewStyle.padding, + top: ContactsTabViewStyle.padding, + ), + child: NoContactsFound( + keyword: controller.textEditingController.text, + ), + ), + ); + } + } + return child!; + }, + (_) => child!, + ); + } + } + return child!; + }, + (success) { + if (success is PresentationContactsSuccess) { + final contacts = success.contacts; + return SliverExpandableList( + title: L10n.of(context)!.contactsCount(contacts.length), + itemCount: contacts.length, + itemBuilder: (context, index) => _Contact( + contact: contacts[index], + controller: controller, + ), + ); + } + return child!; + }, ); }, + child: const SliverToBoxAdapter( + child: SizedBox(), + ), ); } } @@ -92,51 +167,91 @@ class _SliverContactsList extends StatelessWidget { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: controller.presentationContactNotifier, - builder: (context, state, child) => state.fold( - (_) => child!, - (success) { - if (success is ContactsLoading) { - return const SliverToBoxAdapter( - child: LoadingContactWidget(), - ); - } - - if (success is PresentationExternalContactSuccess) { - return SliverToBoxAdapter( - child: ExpansionContactListTile( - contact: success.contact, - highlightKeyword: controller.textEditingController.text, - ), - ); - } - - if (success is PresentationContactsSuccess) { - final contacts = success.contacts; - if (contacts.isEmpty) { - if (controller.textEditingController.text.isEmpty) { - return const SliverToBoxAdapter(child: EmptyContactBody()); - } else { - final presentationPhoneBookContact = controller - .presentationPhonebookContactNotifier.value - .getSuccessOrNull(); - if (presentationPhoneBookContact == null || - presentationPhoneBookContact.contacts.isEmpty) { - return SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only( - left: ContactsTabViewStyle.padding, - top: ContactsTabViewStyle.padding, - ), - child: NoContactsFound( - keyword: controller.textEditingController.text, - ), - ), - ); - } + builder: (context, state, child) { + return state.fold( + (failure) { + if (PlatformInfos.isMobile) { + return child!; + } + final presentationRecentContact = + controller.presentationRecentContactNotifier.value; + if (failure is GetPresentationContactsFailure) { + if (presentationRecentContact.isEmpty) { + return controller.presentationContactNotifier.value.fold( + (failure) { + if (failure is GetPresentationContactsFailure || + failure is GetPresentationContactsEmpty) { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + left: ContactsTabViewStyle.padding, + top: ContactsTabViewStyle.padding, + ), + child: NoContactsFound( + keyword: + controller.textEditingController.text.isEmpty + ? null + : controller.textEditingController.text, + ), + ), + ); + } + return child!; + }, + (_) => child!, + ); + } + } + if (failure is GetPresentationContactsEmpty) { + if (presentationRecentContact.isEmpty) { + return controller.presentationContactNotifier.value.fold( + (failure) { + if (failure is GetPresentationContactsFailure || + failure is GetPresentationContactsEmpty) { + if (controller.textEditingController.text.isEmpty) { + return const SliverToBoxAdapter( + child: EmptyContactBody(), + ); + } else { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + left: ContactsTabViewStyle.padding, + top: ContactsTabViewStyle.padding, + ), + child: NoContactsFound( + keyword: controller.textEditingController.text, + ), + ), + ); + } + } + return child!; + }, + (_) => child!, + ); } } + return child!; + }, + (success) { + if (success is ContactsLoading) { + return const SliverToBoxAdapter( + child: LoadingContactWidget(), + ); + } - if (contacts.isNotEmpty) { + if (success is PresentationExternalContactSuccess) { + return SliverToBoxAdapter( + child: ExpansionContactListTile( + contact: success.contact, + highlightKeyword: controller.textEditingController.text, + ), + ); + } + + if (success is PresentationContactsSuccess) { + final contacts = success.contacts; return SliverExpandableList( title: L10n.of(context)!.linagoraContactsCount(contacts.length), itemCount: contacts.length, @@ -146,11 +261,11 @@ class _SliverContactsList extends StatelessWidget { ), ); } - } - return child!; - }, - ), + return child!; + }, + ); + }, child: const SliverToBoxAdapter( child: SizedBox(), ), @@ -183,6 +298,51 @@ class _SliverPhonebookLoading extends StatelessWidget { } } +class _SliverRecentContacts extends StatelessWidget { + final ContactsTabController controller; + + const _SliverRecentContacts({required this.controller}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller.presentationContactNotifier, + builder: (context, state, child) { + return state.fold( + (failure) => child!, + (success) { + if (success is ContactsLoading) { + return const SliverToBoxAdapter( + child: SizedBox(), + ); + } + return child!; + }, + ); + }, + child: ValueListenableBuilder( + valueListenable: controller.presentationRecentContactNotifier, + builder: (context, recentContacts, child) { + if (recentContacts.isEmpty) { + return child!; + } + return SliverExpandableList( + title: L10n.of(context)!.recent, + itemCount: recentContacts.length, + itemBuilder: (context, index) => _Contact( + contact: recentContacts[index].toPresentationContact(), + controller: controller, + ), + ); + }, + child: const SliverToBoxAdapter( + child: SizedBox(), + ), + ), + ); + } +} + class _SliverWarningBanner extends StatelessWidget { const _SliverWarningBanner({ required this.controller, diff --git a/lib/pages/contacts_tab/empty_contacts_body.dart b/lib/pages/contacts_tab/empty_contacts_body.dart index a50a527a13..341d1233b0 100644 --- a/lib/pages/contacts_tab/empty_contacts_body.dart +++ b/lib/pages/contacts_tab/empty_contacts_body.dart @@ -24,6 +24,8 @@ class EmptyContactBody extends StatelessWidget { Text( L10n.of(context)!.soonThereHaveContacts, style: Theme.of(context).textTheme.titleLarge?.copyWith( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.onBackground, ), textAlign: TextAlign.center, diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index bb509884c3..84d0bce299 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -13,7 +13,7 @@ import 'package:fluffychat/pages/key_verification/key_verification_dialog.dart'; import '../../widgets/matrix.dart'; class DevicesSettings extends StatefulWidget { - const DevicesSettings({Key? key}) : super(key: key); + const DevicesSettings({super.key}); @override DevicesSettingsController createState() => DevicesSettingsController(); diff --git a/lib/pages/device_settings/device_settings_view.dart b/lib/pages/device_settings/device_settings_view.dart index fd32ff04b0..36e0b0d950 100644 --- a/lib/pages/device_settings/device_settings_view.dart +++ b/lib/pages/device_settings/device_settings_view.dart @@ -11,7 +11,7 @@ import 'user_device_list_item.dart'; class DevicesSettingsView extends StatelessWidget { final DevicesSettingsController controller; - const DevicesSettingsView(this.controller, {Key? key}) : super(key: key); + const DevicesSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/device_settings/user_device_list_item.dart b/lib/pages/device_settings/user_device_list_item.dart index 887b3b4451..db793ac357 100644 --- a/lib/pages/device_settings/user_device_list_item.dart +++ b/lib/pages/device_settings/user_device_list_item.dart @@ -31,8 +31,8 @@ class UserDeviceListItem extends StatelessWidget { required this.verify, required this.block, required this.unblock, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/dialer/dialer.dart b/lib/pages/dialer/dialer.dart index c1a5cfd739..b9360577e1 100644 --- a/lib/pages/dialer/dialer.dart +++ b/lib/pages/dialer/dialer.dart @@ -38,10 +38,9 @@ import 'pip/pip_view.dart'; class _StreamView extends StatelessWidget { const _StreamView( this.wrappedStream, { - Key? key, this.mainView = false, required this.matrixClient, - }) : super(key: key); + }); final WrappedMediaStream wrappedStream; final Client matrixClient; @@ -128,8 +127,8 @@ class Calling extends StatefulWidget { required this.client, required this.callId, this.onClear, - Key? key, - }) : super(key: key); + super.key, + }); @override MyCallingPage createState() => MyCallingPage(); diff --git a/lib/pages/dialer/pip/pip_view.dart b/lib/pages/dialer/pip/pip_view.dart index a793373c11..62a924cacb 100644 --- a/lib/pages/dialer/pip/pip_view.dart +++ b/lib/pages/dialer/pip/pip_view.dart @@ -14,13 +14,13 @@ class PIPView extends StatefulWidget { ) builder; const PIPView({ - Key? key, + super.key, required this.builder, this.initialCorner = PIPViewCorner.topRight, this.floatingWidth, this.floatingHeight, this.avoidKeyboard = true, - }) : super(key: key); + }); @override PIPViewState createState() => PIPViewState(); diff --git a/lib/pages/error_page/error_page.dart b/lib/pages/error_page/error_page.dart index 5e6433f4af..526300565f 100644 --- a/lib/pages/error_page/error_page.dart +++ b/lib/pages/error_page/error_page.dart @@ -6,7 +6,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; class ErrorPage extends StatelessWidget { - const ErrorPage({Key? key}) : super(key: key); + const ErrorPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/pages/error_page/error_page_style.dart b/lib/pages/error_page/error_page_style.dart index 19721de8da..a9a7f755ee 100644 --- a/lib/pages/error_page/error_page_style.dart +++ b/lib/pages/error_page/error_page_style.dart @@ -39,20 +39,20 @@ class ErrorPageStyle { ); static ButtonStyle buttonStyle(BuildContext context) => ButtonStyle( - iconSize: MaterialStateProperty.all(18), - textStyle: MaterialStateProperty.all( + iconSize: WidgetStateProperty.all(18), + textStyle: WidgetStateProperty.all( Theme.of(context).textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onPrimary, ), ), - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.primary, ), - foregroundColor: MaterialStateProperty.all( + foregroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.onPrimary, ), - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), ), diff --git a/lib/pages/forward/forward.dart b/lib/pages/forward/forward.dart index bd41acdd0a..36824763de 100644 --- a/lib/pages/forward/forward.dart +++ b/lib/pages/forward/forward.dart @@ -24,10 +24,10 @@ class Forward extends StatefulWidget { final bool? isFullScreen; const Forward({ - Key? key, + super.key, this.sendFromRoomId, this.isFullScreen = true, - }) : super(key: key); + }); @override ForwardController createState() => ForwardController(); @@ -122,14 +122,14 @@ class ForwardController extends State 'ForwardController::_handleForwardMessageOnData() - success: $success', ); switch (success.runtimeType) { - case ForwardMessageSuccess: + case const (ForwardMessageSuccess): final dataOnSuccess = success as ForwardMessageSuccess; if (Navigator.of(context).canPop()) { Navigator.of(context).pop(const PopResultFromForward()); } context.go('/rooms/${dataOnSuccess.room.id}'); break; - case ForwardMessageIsShareFileState: + case const (ForwardMessageIsShareFileState): final dataOnSuccess = success as ForwardMessageIsShareFileState; await showDialog( context: context, diff --git a/lib/pages/homeserver_picker/homeserver_app_bar.dart b/lib/pages/homeserver_picker/homeserver_app_bar.dart index f9d2c19a6b..239ef26b5d 100644 --- a/lib/pages/homeserver_picker/homeserver_app_bar.dart +++ b/lib/pages/homeserver_picker/homeserver_app_bar.dart @@ -8,8 +8,7 @@ import 'homeserver_picker.dart'; class HomeserverAppBar extends StatelessWidget { final HomeserverPickerController controller; - const HomeserverAppBar({Key? key, required this.controller}) - : super(key: key); + const HomeserverAppBar({super.key, required this.controller}); @override Widget build(BuildContext context) { diff --git a/lib/pages/homeserver_picker/homeserver_bottom_sheet.dart b/lib/pages/homeserver_picker/homeserver_bottom_sheet.dart index 91e6b2d7d1..c30312f525 100644 --- a/lib/pages/homeserver_picker/homeserver_bottom_sheet.dart +++ b/lib/pages/homeserver_picker/homeserver_bottom_sheet.dart @@ -5,8 +5,7 @@ import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendati class HomeserverBottomSheet extends StatelessWidget { final HomeserverBenchmarkResult homeserver; - const HomeserverBottomSheet({required this.homeserver, Key? key}) - : super(key: key); + const HomeserverBottomSheet({required this.homeserver, super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 0da2919616..a98fb33d81 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; +import 'package:fluffychat/pages/connect/sso_login_state.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_state.dart'; +import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/foundation.dart'; @@ -27,7 +29,7 @@ import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; class HomeserverPicker extends StatefulWidget { - const HomeserverPicker({Key? key}) : super(key: key); + const HomeserverPicker({super.key}); @override HomeserverPickerController createState() => HomeserverPickerController(); @@ -145,9 +147,12 @@ class HomeserverPickerController extends State homeserver = Uri.https(homeserverController.text, ''); } final matrix = Matrix.of(context); - + final allHomeserverLoggedIn = (await ClientManager.getClients()) + .map((client) => client.homeserver.toString()) + .toList(); + Logs().i('All homeservers: $allHomeserverLoggedIn'); final homeserverExists = - homeserver == matrix.client.homeserver && matrix.client.isLogged(); + allHomeserverLoggedIn.contains(homeserver.toString()); if (homeserverExists && !AppConfig.supportMultipleAccountsInTheSameHomeserver) { @@ -187,9 +192,14 @@ class HomeserverPickerController extends State identityProviders(rawLoginTypes: rawLoginTypes); if (supportsSso(context) && identitiesProvider?.length == 1) { - ssoLoginAction(context: context, id: identitiesProvider!.single.id!); + final result = await ssoLoginAction( + context: context, + id: identitiesProvider!.single.id!, + ); + if (result == SsoLoginState.error) { + state = HomeserverState.ssoLoginServer; + } } - state = HomeserverState.ssoLoginServer; FocusManager.instance.primaryFocus?.unfocus(); setState(() {}); } else { @@ -243,7 +253,10 @@ class HomeserverPickerController extends State @override Widget build(BuildContext context) { - return HomeserverPickerView(this); + return PopScope( + canPop: state != HomeserverState.loading, + child: HomeserverPickerView(this), + ); } Future restoreBackup() async { diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index f1975b0861..fca89fd529 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -13,7 +13,7 @@ import 'homeserver_picker.dart'; class HomeserverPickerView extends StatelessWidget { final HomeserverPickerController controller; - const HomeserverPickerView(this.controller, {Key? key}) : super(key: key); + const HomeserverPickerView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -23,7 +23,9 @@ class HomeserverPickerView extends StatelessWidget { ? AppBar( leading: TwakeIconButton( icon: Icons.arrow_back, - onTap: () => context.pop(), + onTap: controller.state != HomeserverState.loading + ? () => context.pop() + : null, tooltip: L10n.of(context)!.back, ), ) diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index d654443575..0619fedb55 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -20,13 +20,13 @@ class ImageViewer extends StatefulWidget { final double? height; const ImageViewer({ - Key? key, + super.key, this.event, this.imageData, this.filePath, this.width, this.height, - }) : super(key: key); + }); @override ImageViewerController createState() => ImageViewerController(); @@ -42,6 +42,8 @@ class ImageViewerController extends State { String? filePath; + String? thumbnailFilePath; + final downloadMediaFileInteractor = getIt.get(); StreamSubscription? streamSubcription; @@ -51,6 +53,7 @@ class ImageViewerController extends State { super.initState(); if (!PlatformInfos.isWeb && widget.event != null) { handleDownloadFile(widget.event!); + handleDownloadThumbnailFile(widget.event!); } } @@ -78,6 +81,31 @@ class ImageViewerController extends State { } } + Future handleDownloadThumbnailFile(Event event) async { + try { + streamSubcription = downloadMediaFileInteractor + .execute(event: event, getThumbnail: true) + .listen((state) { + state.fold( + (failure) { + if (failure is DownloadMediaFileFailure) { + Logs().e('Error downloading file', failure.exception); + } + }, + (success) { + if (success is DownloadMediaFileSuccess) { + setState(() { + thumbnailFilePath = success.filePath; + }); + } + }, + ); + }); + } catch (e) { + Logs().e('Error downloading file', e); + } + } + @override void dispose() { streamSubcription?.cancel(); diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index dcdac7e06d..d374346114 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -3,10 +3,13 @@ import 'dart:typed_data'; import 'package:fluffychat/pages/image_viewer/image_viewer_style.dart'; import 'package:fluffychat/pages/image_viewer/media_viewer_app_bar.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_avif/flutter_avif.dart'; import 'package:matrix/matrix.dart'; import 'image_viewer.dart'; @@ -20,12 +23,12 @@ class ImageViewerView extends StatelessWidget { const ImageViewerView( this.controller, { - Key? key, + super.key, this.imageData, this.filePath, this.width, this.height, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -117,6 +120,17 @@ class _ImageWidget extends StatelessWidget { @override Widget build(BuildContext context) { if (PlatformInfos.isWeb) { + if (event.mimeType == TwakeMimeTypeExtension.avifMimeType) { + return AvifImage.network( + event + .attachmentOrThumbnailMxcUrl()! + .getDownloadLink(event.room.client) + .toString(), + height: height, + width: width, + fit: BoxFit.cover, + ); + } return FutureBuilder( future: event.downloadAndDecryptAttachment( getThumbnail: true, @@ -135,10 +149,25 @@ class _ImageWidget extends StatelessWidget { ); } else { if (controller.filePath != null) { + if (event.mimeType == TwakeMimeTypeExtension.avifMimeType) { + return AvifImage.file( + File(controller.filePath!), + height: height, + width: width, + fit: BoxFit.cover, + ); + } return Image.file( File(controller.filePath!), fit: BoxFit.contain, filterQuality: FilterQuality.none, + errorBuilder: (context, error, stackTrace) { + return Image.file( + File(controller.thumbnailFilePath!), + fit: BoxFit.contain, + filterQuality: FilterQuality.none, + ); + }, ); } else { return const CupertinoActivityIndicator( diff --git a/lib/pages/image_viewer/media_viewer_app_bar.dart b/lib/pages/image_viewer/media_viewer_app_bar.dart index a54b767670..f848018b6a 100644 --- a/lib/pages/image_viewer/media_viewer_app_bar.dart +++ b/lib/pages/image_viewer/media_viewer_app_bar.dart @@ -9,11 +9,11 @@ import 'package:matrix/matrix.dart'; class MediaViewerAppBar extends StatefulWidget { const MediaViewerAppBar({ - Key? key, + super.key, this.showAppbarPreviewNotifier, this.event, this.enablePaddingAppbar = true, - }) : super(key: key); + }); final ValueNotifier? showAppbarPreviewNotifier; final Event? event; diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 4748fc2253..eea16d8d6e 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:matrix/matrix.dart'; @@ -17,10 +16,10 @@ class InvitationSelection extends StatefulWidget { final bool? isFullScreen; const InvitationSelection({ - Key? key, + super.key, required this.roomId, this.isFullScreen = true, - }) : super(key: key); + }); @override InvitationSelectionController createState() => @@ -94,7 +93,7 @@ class InvitationSelectionController } void inviteSuccessAction() { - context.pop(); + Navigator.of(context).pop(); } @override diff --git a/lib/pages/key_verification/key_verification_dialog.dart b/lib/pages/key_verification/key_verification_dialog.dart index e480510077..7d7e0ccbbd 100644 --- a/lib/pages/key_verification/key_verification_dialog.dart +++ b/lib/pages/key_verification/key_verification_dialog.dart @@ -25,9 +25,9 @@ class KeyVerificationDialog extends StatefulWidget { final KeyVerification request; const KeyVerificationDialog({ - Key? key, + super.key, required this.request, - }) : super(key: key); + }); @override KeyVerificationPageState createState() => KeyVerificationPageState(); diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index 905fd94e4e..a8ae3ad58b 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -15,7 +15,7 @@ import '../../utils/platform_infos.dart'; import 'login_view.dart'; class Login extends StatefulWidget { - const Login({Key? key}) : super(key: key); + const Login({super.key}); @override LoginController createState() => LoginController(); diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index 0b660f78a3..34993de54a 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -9,7 +9,7 @@ import 'login.dart'; class LoginView extends StatelessWidget { final LoginController controller; - const LoginView(this.controller, {Key? key}) : super(key: key); + const LoginView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/login/on_auth_redirect.dart b/lib/pages/login/on_auth_redirect.dart index eed6c4a390..a22371da98 100644 --- a/lib/pages/login/on_auth_redirect.dart +++ b/lib/pages/login/on_auth_redirect.dart @@ -1,12 +1,16 @@ +import 'dart:async'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; -import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/presentation/model/client_login_state_event.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; +import 'package:flutter/material.dart'; class OnAuthRedirect extends StatefulWidget { const OnAuthRedirect({super.key}); @@ -16,14 +20,59 @@ class OnAuthRedirect extends StatefulWidget { } class _OnAuthRedirectState extends State with ConnectPageMixin { + Client? _clientFirstLoggedIn; + + StreamSubscription? _clientLoginStateChangedSubscription; + @override void initState() { super.initState(); + _clientLoginStateChangedSubscription = + Matrix.of(context).onClientLoginStateChanged.stream.listen( + _listenClientLoginStateChanged, + ); WidgetsBinding.instance.addPostFrameCallback((_) { tryLoggingUsingToken(context: context); }); } + @override + void dispose() { + _clientLoginStateChangedSubscription?.cancel(); + super.dispose(); + } + + void _listenClientLoginStateChanged(ClientLoginStateEvent event) { + Logs().i( + 'StreamDialogBuilder::_listenClientLoginStateChanged - ${event.multipleAccountLoginType}', + ); + if (event.multipleAccountLoginType == + MultipleAccountLoginType.firstLoggedIn) { + _clientFirstLoggedIn = event.client; + _handleLoginSuccess(); + return; + } + } + + void _handleLoginSuccess() { + Logs().i('OnAuthRedirect::_handleLoginSuccess'); + if (_clientFirstLoggedIn != null) { + context.go( + '/rooms', + extra: LoggedInBodyArgs( + newActiveClient: _clientFirstLoggedIn, + ), + ); + } else { + context.go('/home'); + } + } + + void _handleLoginError(Object? error) { + Logs().e('OnAuthRedirect::_handleLoginError - $error'); + context.go('/home'); + } + Future tryLoggingUsingToken({ required BuildContext context, }) async { @@ -58,27 +107,23 @@ class _OnAuthRedirectState extends State with ConnectPageMixin { .getLoginClient() .checkHomeserver(Uri.parse(homeserver)); - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context).getLoginClient().login( - LoginType.mLoginToken, - token: loginToken, - initialDeviceDisplayName: PlatformInfos.clientName, - ), - ); + await Matrix.of(context).getLoginClient().login( + LoginType.mLoginToken, + token: loginToken, + initialDeviceDisplayName: PlatformInfos.clientName, + ); } catch (e) { - Logs().e('tryLoggingUsingToken::error: $e'); - TwakeApp.router.go('/home', extra: true); + _handleLoginError(e); } } @override Widget build(BuildContext context) { - return const Scaffold( - backgroundColor: Colors.black, + return Scaffold( body: Center( child: CupertinoActivityIndicator( animating: true, - color: Colors.white, + color: LinagoraSysColors.material().onSurfaceVariant, ), ), ); diff --git a/lib/pages/multiple_accounts/multiple_accounts_picker.dart b/lib/pages/multiple_accounts/multiple_accounts_picker.dart index 6d6aad7257..abd4ddfe08 100644 --- a/lib/pages/multiple_accounts/multiple_accounts_picker.dart +++ b/lib/pages/multiple_accounts/multiple_accounts_picker.dart @@ -1,91 +1,53 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; -import 'package:fluffychat/presentation/extensions/multiple_accounts/client_profile_extension.dart'; -import 'package:fluffychat/presentation/multiple_account/client_profile_presentation.dart'; import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; -import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/widgets/layouts/agruments/switch_active_account_body_args.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/twake_components/twake_header_style.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; import 'package:matrix/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +typedef OnGoToAccountSettings = void Function(TwakePresentationAccount account); + class MultipleAccountsPickerController { final BuildContext context; + final List multipleAccounts; MultipleAccountsPickerController({ required this.context, + required this.multipleAccounts, }); MatrixState get _matrixState => Matrix.of(context); - Future> _getClientProfiles() async { - final profiles = await Future.wait( - _matrixState.widget.clients.map((client) async { - final profileBundle = await client.fetchOwnProfile(); - Logs().d( - 'MultipleAccountsPicker::getProfileBundles() - ClientName - ${client.clientName}', - ); - Logs().d( - 'MultipleAccountsPicker::getProfileBundles() - UserId - ${client.userID}', - ); - return ClientProfilePresentation( - profile: profileBundle, - client: client, - ); - }), - ); - - return profiles.toList(); - } - - Future> _getMultipleAccounts( - Client currentActiveClient, - ) async { - final profileBundles = await _getClientProfiles(); - return profileBundles - .where((clientProfile) => clientProfile != null) - .map( - (clientProfile) => clientProfile!.toTwakeChatPresentationAccount( - currentActiveClient, - ), - ) - .toList(); - } - void showMultipleAccountsPicker( Client currentActiveClient, { required VoidCallback onGoToAccountSettings, }) async { - final multipleAccount = await _getMultipleAccounts( - currentActiveClient, - ); - multipleAccount.sort((pre, next) { + multipleAccounts.sort((pre, next) { return pre.accountActiveStatus.index .compareTo(next.accountActiveStatus.index); }); MultipleAccountPicker.showMultipleAccountPicker( - accounts: multipleAccount, + accounts: multipleAccounts, context: context, onAddAnotherAccount: _onAddAnotherAccount, onGoToAccountSettings: onGoToAccountSettings, - onSetAccountAsActive: (account) => _onSetAccountAsActive.call( - multipleAccounts: multipleAccount, + onSetAccountAsActive: (account) => _onSetAccountAsActive( + multipleAccounts: multipleAccounts, account: account, ), titleAddAnotherAccount: L10n.of(context)!.addAnotherAccount, titleAccountSettings: L10n.of(context)!.accountSettings, logoApp: Padding( padding: TwakeHeaderStyle.logoAppOfMultiplePadding, - child: SvgPicture.asset( - ImagePaths.icTwakeImageLogo, - width: TwakeHeaderStyle.logoAppOfMultipleWidth, - height: TwakeHeaderStyle.logoAppOfMultipleHeight, + child: Text( + L10n.of(context)!.selectAccount, + style: TwakeHeaderStyle.selectAccountTextStyle(context), ), ), accountNameStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( @@ -114,11 +76,11 @@ class MultipleAccountsPickerController { ) ?.clientAccount; if (client == null || client == _matrixState.client) return; - _setActiveClient(client); + await _setActiveClient(client); } void _onAddAnotherAccount() { - context.go( + context.push( '/rooms/addaccount', extra: const TwakeWelcomeArg( twakeIdType: TwakeWelcomeType.otherAccounts, @@ -126,9 +88,10 @@ class MultipleAccountsPickerController { ); } - void _setActiveClient(Client newClient) async { + Future _setActiveClient(Client newClient) async { final result = await _matrixState.setActiveClient(newClient); if (result.isSuccess) { + _matrixState.reSyncContacts(); context.go( '/rooms', extra: SwitchActiveAccountBodyArgs( diff --git a/lib/pages/new_group/contacts_selection.dart b/lib/pages/new_group/contacts_selection.dart index 45023095f5..b8cd478738 100644 --- a/lib/pages/new_group/contacts_selection.dart +++ b/lib/pages/new_group/contacts_selection.dart @@ -3,8 +3,13 @@ import 'package:fluffychat/presentation/mixins/contacts_view_controller_mixin.da import 'package:fluffychat/presentation/mixins/invite_external_contact_mixin.dart'; import 'package:fluffychat/pages/new_group/contacts_selection_view.dart'; import 'package:fluffychat/pages/new_group/selected_contacts_map_change_notifier.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter/cupertino.dart'; +import 'package:matrix/matrix.dart'; abstract class ContactsSelectionController extends State @@ -27,9 +32,18 @@ abstract class ContactsSelectionController bool get isFullScreen => true; + Client get client => Matrix.of(context).client; + @override void initState() { - initialFetchContacts(); + SchedulerBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + initialFetchContacts( + client: client, + matrixLocalizations: MatrixLocals(L10n.of(context)!), + ); + } + }); super.initState(); } diff --git a/lib/pages/new_group/contacts_selection_view.dart b/lib/pages/new_group/contacts_selection_view.dart index 98bb611816..7c9ac986f4 100644 --- a/lib/pages/new_group/contacts_selection_view.dart +++ b/lib/pages/new_group/contacts_selection_view.dart @@ -1,20 +1,22 @@ import 'package:fluffychat/pages/new_group/contacts_selection.dart'; import 'package:fluffychat/pages/new_group/contacts_selection_view_style.dart'; +import 'package:fluffychat/pages/new_group/widget/contact_item.dart'; import 'package:fluffychat/pages/new_group/widget/contacts_selection_list.dart'; import 'package:fluffychat/pages/new_group/widget/selected_participants_list.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/widgets/app_bars/searchable_app_bar.dart'; import 'package:fluffychat/widgets/contacts_warning_banner/contacts_warning_banner_view.dart'; +import 'package:fluffychat/widgets/sliver_expandable_list.dart'; import 'package:fluffychat/widgets/twake_components/twake_fab.dart'; import 'package:fluffychat/widgets/twake_components/twake_text_button.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class ContactsSelectionView extends StatelessWidget { final ContactsSelectionController controller; - const ContactsSelectionView(this.controller, {Key? key}) : super(key: key); + const ContactsSelectionView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -61,9 +63,43 @@ class ContactsSelectionView extends StatelessWidget { contactsSelectionController: controller, ), ), + ValueListenableBuilder( + valueListenable: + controller.presentationRecentContactNotifier, + builder: (context, recentContacts, child) { + if (recentContacts.isEmpty) { + return child!; + } + return SliverExpandableList( + title: L10n.of(context)!.recent, + itemCount: recentContacts.length, + itemBuilder: (context, index) { + final disabled = + controller.disabledContactIds.contains( + recentContacts[index].directChatMatrixID, + ); + return ContactItem( + contact: + recentContacts[index].toPresentationContact(), + selectedContactsMapNotifier: + controller.selectedContactsMapNotifier, + onSelectedContact: controller.onSelectedContact, + highlightKeyword: + controller.textEditingController.text, + disabled: disabled, + ); + }, + ); + }, + child: const SliverToBoxAdapter( + child: SizedBox(), + ), + ), ContactsSelectionList( presentationContactNotifier: controller.presentationContactNotifier, + presentationRecentContactNotifier: + controller.presentationRecentContactNotifier, selectedContactsMapNotifier: controller.selectedContactsMapNotifier, onSelectedContact: controller.onSelectedContact, @@ -103,7 +139,7 @@ class ContactsSelectionView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ TwakeTextButton( - onTap: () => context.pop(), + onTap: () => Navigator.of(context).pop(), message: L10n.of(context)!.cancel, borderHover: ContactsSelectionViewStyle.webActionsButtonBorder, margin: ContactsSelectionViewStyle.webActionsButtonMargin, diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 9fbf36eb1c..a6e52de0fb 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -8,7 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; class NewGroup extends StatefulWidget { - const NewGroup({Key? key}) : super(key: key); + const NewGroup({super.key}); @override NewGroupController createState() => NewGroupController(); diff --git a/lib/pages/new_group/new_group_chat_info.dart b/lib/pages/new_group/new_group_chat_info.dart index 34f0dce634..ccdfd22f69 100644 --- a/lib/pages/new_group/new_group_chat_info.dart +++ b/lib/pages/new_group/new_group_chat_info.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pages/new_group/new_group_chat_info_view.dart'; import 'package:fluffychat/pages/new_group/new_group_info_controller.dart'; import 'package:fluffychat/presentation/mixins/common_media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/single_image_picker_mixin.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/power_level_manager.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; diff --git a/lib/pages/new_group/new_group_info_controller.dart b/lib/pages/new_group/new_group_info_controller.dart index 7ead1b8057..49abfacaef 100644 --- a/lib/pages/new_group/new_group_info_controller.dart +++ b/lib/pages/new_group/new_group_info_controller.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/pages/new_group/selected_contacts_map_change_notifier.dart b/lib/pages/new_group/selected_contacts_map_change_notifier.dart index 73805378f4..af292fbc2f 100644 --- a/lib/pages/new_group/selected_contacts_map_change_notifier.dart +++ b/lib/pages/new_group/selected_contacts_map_change_notifier.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:flutter/widgets.dart'; class SelectedContactsMapChangeNotifier extends ChangeNotifier { diff --git a/lib/pages/new_group/widget/contact_item.dart b/lib/pages/new_group/widget/contact_item.dart new file mode 100644 index 0000000000..7e8112b017 --- /dev/null +++ b/lib/pages/new_group/widget/contact_item.dart @@ -0,0 +1,79 @@ +import 'package:fluffychat/pages/new_group/selected_contacts_map_change_notifier.dart'; +import 'package:fluffychat/pages/new_group/widget/contacts_selection_list_style.dart'; +import 'package:fluffychat/pages/new_private_chat/widget/expansion_contact_list_tile.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:flutter/material.dart'; + +class ContactItem extends StatelessWidget { + final PresentationContact contact; + final SelectedContactsMapChangeNotifier selectedContactsMapNotifier; + final VoidCallback? onSelectedContact; + final bool disabled; + final double paddingTop; + final String highlightKeyword; + + const ContactItem({ + super.key, + required this.contact, + required this.selectedContactsMapNotifier, + this.onSelectedContact, + this.highlightKeyword = '', + this.disabled = false, + this.paddingTop = 0, + }); + + @override + Widget build(BuildContext context) { + final contactNotifier = + selectedContactsMapNotifier.getNotifierAtContact(contact); + return Padding( + padding: ContactsSelectionListStyle.contactItemPadding, + child: InkWell( + key: ValueKey(contact.matrixId), + onTap: disabled + ? null + : () { + onSelectedContact?.call(); + selectedContactsMapNotifier.onContactTileTap( + context, + contact, + ); + }, + borderRadius: ContactsSelectionListStyle.contactItemBorderRadius, + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Padding( + padding: ContactsSelectionListStyle.checkBoxPadding(paddingTop), + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: contactNotifier, + builder: (context, isCurrentSelected, child) { + return Checkbox( + value: disabled || contactNotifier.value, + onChanged: disabled + ? null + : (newValue) { + onSelectedContact?.call(); + selectedContactsMapNotifier.onContactTileTap( + context, + contact, + ); + }, + ); + }, + ), + Expanded( + child: ExpansionContactListTile( + contact: contact, + highlightKeyword: highlightKeyword, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/new_group/widget/contacts_selection_list.dart b/lib/pages/new_group/widget/contacts_selection_list.dart index defcaa1bc3..b5fd9b48c0 100644 --- a/lib/pages/new_group/widget/contacts_selection_list.dart +++ b/lib/pages/new_group/widget/contacts_selection_list.dart @@ -2,31 +2,37 @@ 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/contact/get_contacts_state.dart'; +import 'package:fluffychat/pages/new_group/widget/contact_item.dart'; import 'package:fluffychat/pages/new_group/widget/contacts_selection_list_style.dart'; import 'package:fluffychat/pages/new_private_chat/widget/loading_contact_widget.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_success.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_empty.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_failure.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:fluffychat/widgets/sliver_expandable_list.dart'; import 'package:flutter/material.dart'; - +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/pages/new_group/selected_contacts_map_change_notifier.dart'; -import 'package:fluffychat/pages/new_private_chat/widget/expansion_contact_list_tile.dart'; import 'package:fluffychat/pages/new_private_chat/widget/no_contacts_found.dart'; class ContactsSelectionList extends StatelessWidget { final SelectedContactsMapChangeNotifier selectedContactsMapNotifier; + final ValueNotifier> + presentationRecentContactNotifier; final ValueNotifier> presentationContactNotifier; final Function() onSelectedContact; final List disabledContactIds; final TextEditingController textEditingController; const ContactsSelectionList({ - Key? key, + super.key, required this.presentationContactNotifier, required this.selectedContactsMapNotifier, required this.onSelectedContact, this.disabledContactIds = const [], required this.textEditingController, - }) : super(key: key); + required this.presentationRecentContactNotifier, + }); @override Widget build(BuildContext context) { @@ -36,7 +42,27 @@ class ContactsSelectionList extends StatelessWidget { final isSearchModeEnable = textEditingController.text.isNotEmpty; return state.fold( - (_) => child!, + (failure) { + final recentContact = + presentationRecentContactNotifier.value.isEmpty; + final textControllerIsEmpty = textEditingController.text.isEmpty; + if (failure is GetPresentationContactsEmpty || + failure is GetPresentationContactsFailure) { + if (recentContact) { + return SliverToBoxAdapter( + child: Padding( + padding: ContactsSelectionListStyle.notFoundPadding, + child: NoContactsFound( + keyword: textControllerIsEmpty + ? null + : textEditingController.text, + ), + ), + ); + } + } + return child!; + }, (success) { if (success is ContactsLoading) { return const SliverToBoxAdapter( @@ -46,7 +72,7 @@ class ContactsSelectionList extends StatelessWidget { if (success is PresentationExternalContactSuccess) { return SliverToBoxAdapter( - child: _ContactItem( + child: ContactItem( contact: success.contact, selectedContactsMapNotifier: selectedContactsMapNotifier, onSelectedContact: onSelectedContact, @@ -68,13 +94,14 @@ class ContactsSelectionList extends StatelessWidget { ), ); } - return SliverList.builder( + return SliverExpandableList( + title: L10n.of(context)!.linagoraContactsCount(contacts.length), itemCount: contacts.length, itemBuilder: (context, index) { final disabled = disabledContactIds.contains( contacts[index].matrixId, ); - return _ContactItem( + return ContactItem( contact: contacts[index], selectedContactsMapNotifier: selectedContactsMapNotifier, onSelectedContact: onSelectedContact, @@ -95,76 +122,3 @@ class ContactsSelectionList extends StatelessWidget { ); } } - -class _ContactItem extends StatelessWidget { - final PresentationContact contact; - final SelectedContactsMapChangeNotifier selectedContactsMapNotifier; - final VoidCallback? onSelectedContact; - final bool disabled; - final double paddingTop; - final String highlightKeyword; - - const _ContactItem({ - required this.contact, - required this.selectedContactsMapNotifier, - this.onSelectedContact, - this.highlightKeyword = '', - this.disabled = false, - this.paddingTop = 0, - }); - - @override - Widget build(BuildContext context) { - final contactNotifier = - selectedContactsMapNotifier.getNotifierAtContact(contact); - return Padding( - padding: ContactsSelectionListStyle.contactItemPadding, - child: InkWell( - key: ValueKey(contact.matrixId), - onTap: disabled - ? null - : () { - onSelectedContact?.call(); - selectedContactsMapNotifier.onContactTileTap( - context, - contact, - ); - }, - borderRadius: ContactsSelectionListStyle.contactItemBorderRadius, - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Padding( - padding: ContactsSelectionListStyle.checkBoxPadding(paddingTop), - child: Row( - children: [ - ValueListenableBuilder( - valueListenable: contactNotifier, - builder: (context, isCurrentSelected, child) { - return Checkbox( - value: disabled || contactNotifier.value, - onChanged: disabled - ? null - : (newValue) { - onSelectedContact?.call(); - selectedContactsMapNotifier.onContactTileTap( - context, - contact, - ); - }, - ); - }, - ), - Expanded( - child: ExpansionContactListTile( - contact: contact, - highlightKeyword: highlightKeyword, - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/new_group/widget/contacts_selection_list_style.dart b/lib/pages/new_group/widget/contacts_selection_list_style.dart index c8b27596e2..431716c448 100644 --- a/lib/pages/new_group/widget/contacts_selection_list_style.dart +++ b/lib/pages/new_group/widget/contacts_selection_list_style.dart @@ -1,7 +1,11 @@ import 'package:flutter/widgets.dart'; class ContactsSelectionListStyle { - static const notFoundPadding = EdgeInsetsDirectional.only(top: 12.0); + static const notFoundPadding = EdgeInsetsDirectional.only( + top: 12.0, + start: 16.0, + end: 16.0, + ); static const listPaddingTop = 8.0; static BorderRadius contactItemBorderRadius = BorderRadius.circular(16.0); diff --git a/lib/pages/new_group/widget/expansion_participants_list.dart b/lib/pages/new_group/widget/expansion_participants_list.dart index db7d966ed6..60c1da55d3 100644 --- a/lib/pages/new_group/widget/expansion_participants_list.dart +++ b/lib/pages/new_group/widget/expansion_participants_list.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/pages/new_private_chat/widget/expansion_contact_list_tile.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/new_private_chat/new_private_chat.dart b/lib/pages/new_private_chat/new_private_chat.dart index 63e016ad1d..05140cd85b 100644 --- a/lib/pages/new_private_chat/new_private_chat.dart +++ b/lib/pages/new_private_chat/new_private_chat.dart @@ -4,14 +4,17 @@ import 'package:fluffychat/presentation/mixins/go_to_group_chat_mixin.dart'; import 'package:fluffychat/presentation/mixins/invite_external_contact_mixin.dart'; import 'package:fluffychat/pages/new_private_chat/new_private_chat_view.dart'; import 'package:fluffychat/presentation/mixins/go_to_direct_chat_mixin.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class NewPrivateChat extends StatefulWidget { - const NewPrivateChat({Key? key}) : super(key: key); + const NewPrivateChat({super.key}); @override NewPrivateChatController createState() => NewPrivateChatController(); @@ -30,7 +33,14 @@ class NewPrivateChatController extends State @override void initState() { super.initState(); - initialFetchContacts(); + SchedulerBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + initialFetchContacts( + client: Matrix.of(context).client, + matrixLocalizations: MatrixLocals(L10n.of(context)!), + ); + } + }); // FIXME: Find out solution for disable load more in search // searchContactsController.onSearchKeywordChanged = (searchKey) { // disableLoadMoreInSearch(); diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index cb8a96a128..652d19b696 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -10,7 +10,7 @@ import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class NewPrivateChatView extends StatelessWidget { final NewPrivateChatController controller; - const NewPrivateChatView(this.controller, {Key? key}) : super(key: key); + const NewPrivateChatView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/new_private_chat/qr_scanner_modal.dart b/lib/pages/new_private_chat/qr_scanner_modal.dart index c18ca61f4f..c08785bdeb 100644 --- a/lib/pages/new_private_chat/qr_scanner_modal.dart +++ b/lib/pages/new_private_chat/qr_scanner_modal.dart @@ -9,7 +9,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:fluffychat/utils/url_launcher.dart'; class QrScannerModal extends StatefulWidget { - const QrScannerModal({Key? key}) : super(key: key); + const QrScannerModal({super.key}); @override QrScannerModalState createState() => QrScannerModalState(); 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 91083f91be..54efe2d7d0 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 @@ -1,5 +1,5 @@ import 'package:fluffychat/domain/model/contact/contact_status.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/pages/new_private_chat/widget/contact_status_widget.dart'; import 'package:fluffychat/utils/display_name_widget.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; diff --git a/lib/pages/new_private_chat/widget/expansion_list.dart b/lib/pages/new_private_chat/widget/expansion_list.dart index 8adbd508e0..c4ef272367 100644 --- a/lib/pages/new_private_chat/widget/expansion_list.dart +++ b/lib/pages/new_private_chat/widget/expansion_list.dart @@ -5,8 +5,10 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart'; import 'package:fluffychat/pages/new_private_chat/widget/loading_contact_widget.dart'; import 'package:fluffychat/presentation/enum/contacts/warning_contacts_banner_enum.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_success.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_empty.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_failure.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/contacts_warning_banner/contacts_warning_banner_view.dart'; import 'package:flutter/material.dart'; @@ -51,7 +53,26 @@ class ExpansionList extends StatelessWidget { valueListenable: presentationContactsNotifier, builder: (context, state, child) { return state.fold( - (_) => child!, + (failure) { + final textControllerIsEmpty = textEditingController.text.isEmpty; + if (failure is GetPresentationContactsEmpty || + failure is GetPresentationContactsFailure) { + return Column( + children: [ + ..._buildResponsiveButtons(context), + const SizedBox( + height: 12, + ), + NoContactsFound( + keyword: textControllerIsEmpty + ? null + : textEditingController.text, + ), + ], + ); + } + return child!; + }, (success) { if (success is ContactsLoading) { return Column( @@ -283,9 +304,8 @@ class _NewGroupButton extends StatelessWidget { final Function() onPressed; const _NewGroupButton({ - Key? key, required this.onPressed, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/new_private_chat/widget/no_contacts_found.dart b/lib/pages/new_private_chat/widget/no_contacts_found.dart index d2e9c6dda3..fbb7cb5523 100644 --- a/lib/pages/new_private_chat/widget/no_contacts_found.dart +++ b/lib/pages/new_private_chat/widget/no_contacts_found.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; class NoContactsFound extends StatelessWidget { - final String keyword; + final String? keyword; - const NoContactsFound({super.key, required this.keyword}); + const NoContactsFound({super.key, this.keyword}); @override Widget build(BuildContext context) { @@ -14,10 +14,11 @@ class NoContactsFound extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - L10n.of(context)!.noResultForKeyword(keyword), - style: Theme.of(context).textTheme.titleLarge, - ), + if (keyword != null) + Text( + L10n.of(context)!.noResultForKeyword(keyword!), + style: Theme.of(context).textTheme.titleLarge, + ), const SizedBox( height: 8.0, ), diff --git a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart index 01394743a1..75ed082b34 100644 --- a/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart +++ b/lib/pages/profile_info/copiable_profile_row/copiable_profile_row.dart @@ -19,8 +19,8 @@ class CopiableProfileRow extends StatelessWidget { required this.leadingIcon, required this.caption, required this.copiableText, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart b/lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart index c68d05251a..c5b606ac74 100644 --- a/lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart +++ b/lib/pages/profile_info/copiable_profile_row/icon_copiable_profile_row.dart @@ -9,9 +9,8 @@ class IconCopiableProfileRow extends CopiableProfileRow { required IconData icon, required super.caption, required super.copiableText, - Key? key, + super.key, }) : super( - key: key, leadingIcon: Icon( icon, size: ChatProfileInfoStyle.iconSize, diff --git a/lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart b/lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart index ff4d4afcd4..0101107071 100644 --- a/lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart +++ b/lib/pages/profile_info/copiable_profile_row/svg_copiable_profile_row.dart @@ -10,9 +10,8 @@ class SvgCopiableProfileRow extends CopiableProfileRow { required String leadingIconPath, required super.caption, required super.copiableText, - Key? key, + super.key, }) : super( - key: key, leadingIcon: SvgPicture.asset( leadingIconPath, width: ChatProfileInfoStyle.iconSize, diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body.dart b/lib/pages/profile_info/profile_info_body/profile_info_body.dart index b9cad46666..f5740d9258 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/domain/usecase/contacts/lookup_match_contact_interact import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body_view_style.dart'; import 'package:fluffychat/presentation/enum/profile_info/profile_info_body_enum.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_constant.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/dialog/warning_dialog.dart'; @@ -28,8 +28,8 @@ class ProfileInfoBody extends StatefulWidget { required this.user, this.onNewChatOpen, this.onUpdatedMembers, - Key? key, - }) : super(key: key); + super.key, + }); final User? user; diff --git a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart index f5b55ce9fb..b8fea5d925 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_body_view.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; class ProfileInfoBodyView extends StatelessWidget { const ProfileInfoBodyView({ required this.controller, - Key? key, - }) : super(key: key); + super.key, + }); final ProfileInfoBodyController controller; @override diff --git a/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart b/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart index 3d7ea59ccb..76872ef15f 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_contact_rows.dart @@ -12,8 +12,8 @@ class ProfileInfoContactRows extends StatelessWidget { const ProfileInfoContactRows({ required this.user, required this.lookupContactNotifier, - Key? key, - }) : super(key: key); + super.key, + }); final User user; final ValueListenable lookupContactNotifier; diff --git a/lib/pages/profile_info/profile_info_body/profile_info_header.dart b/lib/pages/profile_info/profile_info_body/profile_info_header.dart index a3cba044ad..2518c46efa 100644 --- a/lib/pages/profile_info/profile_info_body/profile_info_header.dart +++ b/lib/pages/profile_info/profile_info_body/profile_info_header.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; class ProfileInfoHeader extends StatelessWidget { - const ProfileInfoHeader(this.user, {Key? key}) : super(key: key); + const ProfileInfoHeader(this.user, {super.key}); final User user; @override diff --git a/lib/pages/profile_info/profile_info_view.dart b/lib/pages/profile_info/profile_info_view.dart index 5fec66f637..4940112bec 100644 --- a/lib/pages/profile_info/profile_info_view.dart +++ b/lib/pages/profile_info/profile_info_view.dart @@ -2,7 +2,6 @@ import 'package:fluffychat/pages/profile_info/profile_info_page.dart'; import 'package:fluffychat/pages/profile_info/profile_info_body/profile_info_body.dart'; import 'package:fluffychat/pages/profile_info/profile_info_view_style.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_state_layer.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -43,7 +42,7 @@ class ProfileInfoView extends StatelessWidget { splashColor: Colors.transparent, hoverColor: Colors.transparent, highlightColor: Colors.transparent, - onPressed: () => context.pop(), + onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back), ), ), diff --git a/lib/pages/search/recent_contacts_banner_widget.dart b/lib/pages/search/recent_contacts_banner_widget.dart index 744ca963ee..65725c9774 100644 --- a/lib/pages/search/recent_contacts_banner_widget.dart +++ b/lib/pages/search/recent_contacts_banner_widget.dart @@ -1,22 +1,25 @@ import 'package:fluffychat/pages/search/recent_contacts_banner_widget_style.dart'; import 'package:fluffychat/pages/search/search.dart'; import 'package:fluffychat/utils/display_name_widget.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:flutter/material.dart' hide SearchController; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class PreSearchRecentContactsContainer extends StatelessWidget { final SearchController searchController; - final List contactsList; + final List recentRooms; const PreSearchRecentContactsContainer({ super.key, required this.searchController, - required this.contactsList, + required this.recentRooms, }); @override Widget build(BuildContext context) { - if (contactsList.isEmpty) { + if (recentRooms.isEmpty) { return const SizedBox.shrink(); } else { return Align( @@ -27,11 +30,10 @@ class PreSearchRecentContactsContainer extends StatelessWidget { shrinkWrap: true, physics: const ClampingScrollPhysics(), scrollDirection: Axis.horizontal, - itemCount: contactsList.length, + itemCount: recentRooms.length, itemBuilder: (context, index) { return PreSearchRecentContactWidget( - user: contactsList[index], - searchController: searchController, + room: recentRooms[index], ); }, ), @@ -41,20 +43,19 @@ class PreSearchRecentContactsContainer extends StatelessWidget { } class PreSearchRecentContactWidget extends StatelessWidget { - final SearchController searchController; - final User user; + final Room room; const PreSearchRecentContactWidget({ super.key, - required this.user, - required this.searchController, + required this.room, }); @override Widget build(BuildContext context) { + final displayName = room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)!), + ); return InkWell( - onTap: () { - searchController.goToChatScreenFormRecentChat(user); - }, + onTap: () => context.go('/rooms/${room.id}'), child: SizedBox( width: RecentContactsBannerWidgetStyle.chatRecentContactItemWidth, child: Column( @@ -64,15 +65,15 @@ class PreSearchRecentContactWidget extends StatelessWidget { width: RecentContactsBannerWidgetStyle.avatarWidthSize, height: RecentContactsBannerWidgetStyle.avatarWidthSize, child: Avatar( - mxContent: user.avatarUrl, - name: user.displayName ?? "", + mxContent: room.avatar, + name: displayName, ), ), Padding( padding: RecentContactsBannerWidgetStyle.chatRecentContactItemPadding, child: BuildDisplayName( - profileDisplayName: user.displayName ?? "", + profileDisplayName: displayName, ), ), ], diff --git a/lib/pages/search/recent_item_widget.dart b/lib/pages/search/recent_item_widget.dart index 7ee8eca2c2..987324c53b 100644 --- a/lib/pages/search/recent_item_widget.dart +++ b/lib/pages/search/recent_item_widget.dart @@ -22,9 +22,9 @@ class RecentItemWidget extends StatelessWidget { required this.presentationSearch, required this.highlightKeyword, this.onTap, - Key? key, + super.key, required this.client, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 7eedc6978a..458e201aa5 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -7,7 +7,7 @@ import 'package:fluffychat/domain/usecase/search/pre_search_recent_contacts_inte import 'package:fluffychat/pages/search/search_contacts_and_chats_controller.dart'; import 'package:fluffychat/pages/search/search_view.dart'; import 'package:fluffychat/pages/search/server_search_controller.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_constant.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -81,7 +81,7 @@ class SearchController extends State { void fetchPreSearchRecentContacts() { _preSearchRecentContactsInteractor .execute( - recentRooms: Matrix.of(context).client.rooms, + allRooms: Matrix.of(context).client.rooms, limit: limitPrefetchedRecentContacts, ) .listen((value) { @@ -167,7 +167,9 @@ class SearchController extends State { SchedulerBinding.instance.addPostFrameCallback((_) async { if (mounted) { searchContactAndRecentChatController?.init(); - serverSearchController.initSearch(); + serverSearchController.initSearch( + context: context, + ); fetchPreSearchRecentContacts(); textEditingController.addListener(() { onSearchBarChanged(textEditingController.text); diff --git a/lib/pages/search/search_external_contact.dart b/lib/pages/search/search_external_contact.dart new file mode 100644 index 0000000000..ce6365f574 --- /dev/null +++ b/lib/pages/search/search_external_contact.dart @@ -0,0 +1,47 @@ +import 'package:fluffychat/domain/model/contact/contact_type.dart'; +import 'package:fluffychat/pages/new_private_chat/widget/expansion_contact_list_tile.dart'; +import 'package:fluffychat/pages/search/search.dart'; +import 'package:fluffychat/pages/search/search_external_contact_style.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:flutter/material.dart' hide SearchController; + +class SearchExternalContactWidget extends StatelessWidget { + const SearchExternalContactWidget({ + super.key, + required this.keyword, + required this.searchController, + }); + + final String keyword; + final SearchController searchController; + + @override + Widget build(BuildContext context) { + final newContact = PresentationContact( + matrixId: keyword, + displayName: keyword.substring(1), + type: ContactType.external, + ); + + return Padding( + padding: SearchExternalContactStyle.contentPadding, + child: InkWell( + onTap: () { + searchController.onSearchItemTap( + ContactPresentationSearch( + matrixId: newContact.matrixId, + email: newContact.email, + displayName: newContact.displayName, + ), + ); + }, + borderRadius: SearchExternalContactStyle.borderRadius, + child: ExpansionContactListTile( + contact: newContact, + highlightKeyword: searchController.textEditingController.text, + ), + ), + ); + } +} diff --git a/lib/pages/search/search_external_contact_style.dart b/lib/pages/search/search_external_contact_style.dart new file mode 100644 index 0000000000..495883328a --- /dev/null +++ b/lib/pages/search/search_external_contact_style.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SearchExternalContactStyle { + static EdgeInsetsGeometry contentPadding = const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ); + + static BorderRadius borderRadius = BorderRadius.circular(8.0); +} diff --git a/lib/pages/search/search_text_field.dart b/lib/pages/search/search_text_field.dart index 02e2c61e33..edfe83ed6b 100644 --- a/lib/pages/search/search_text_field.dart +++ b/lib/pages/search/search_text_field.dart @@ -39,12 +39,18 @@ class SearchTextField extends StatelessWidget { size: SearchViewStyle.searchIconSize, color: Theme.of(context).colorScheme.onSurface, ), - suffixIcon: TwakeIconButton( - tooltip: L10n.of(context)!.close, - icon: Icons.close, - onTap: () { - textEditingController.clear(); + suffixIcon: ValueListenableBuilder( + valueListenable: textEditingController, + builder: (context, value, child) { + return value.text.isNotEmpty ? child! : const SizedBox.shrink(); }, + child: TwakeIconButton( + tooltip: L10n.of(context)!.close, + icon: Icons.close, + onTap: () { + textEditingController.clear(); + }, + ), ), ), ), diff --git a/lib/pages/search/search_view.dart b/lib/pages/search/search_view.dart index 1e747f7bd3..ec49ace267 100644 --- a/lib/pages/search/search_view.dart +++ b/lib/pages/search/search_view.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/domain/app_state/search/pre_search_state.dart'; import 'package:fluffychat/pages/search/recent_contacts_banner_widget.dart'; import 'package:fluffychat/pages/search/recent_item_widget.dart'; import 'package:fluffychat/pages/search/search.dart'; +import 'package:fluffychat/pages/search/search_external_contact.dart'; import 'package:fluffychat/pages/search/search_text_field.dart'; import 'package:fluffychat/pages/search/search_view_style.dart'; import 'package:fluffychat/pages/search/server_search_view.dart'; @@ -12,6 +13,7 @@ import 'package:fluffychat/widgets/twake_components/twake_loading/center_loading import 'package:flutter/material.dart' hide SearchController; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:matrix/matrix.dart'; class SearchView extends StatelessWidget { final SearchController searchController; @@ -35,7 +37,7 @@ class SearchView extends StatelessWidget { builder: (context, value, emptyChild) => value.fold((failure) => emptyChild!, (success) { switch (success.runtimeType) { - case PreSearchRecentContactsSuccess: + case const (PreSearchRecentContactsSuccess): final data = success as PreSearchRecentContactsSuccess; return ValueListenableBuilder( valueListenable: searchController.textEditingController, @@ -47,7 +49,7 @@ class SearchView extends StatelessWidget { flexibleSpace: FlexibleSpaceBar( title: PreSearchRecentContactsContainer( searchController: searchController, - contactsList: data.users, + recentRooms: data.rooms, ), titlePadding: const EdgeInsetsDirectional.only(start: 0.0), @@ -125,6 +127,13 @@ class SearchView extends StatelessWidget { .recentAndContactsNotifier, builder: (context, contacts, emptyChild) { if (contacts.isEmpty) { + final keyword = searchController.textEditingController.text; + if (keyword.isValidMatrixId && keyword.startsWith("@")) { + return SearchExternalContactWidget( + keyword: keyword, + searchController: searchController, + ); + } return emptyChild!; } diff --git a/lib/pages/search/server_search_controller.dart b/lib/pages/search/server_search_controller.dart index 6543178f92..938426a5f3 100644 --- a/lib/pages/search/server_search_controller.dart +++ b/lib/pages/search/server_search_controller.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/domain/usecase/search/server_search_interactor.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_state.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_empty_search.dart'; import 'package:fluffychat/presentation/model/search/presentation_server_side_search.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/result_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/domain/app_state/search/server_search_state.dart'; @@ -18,6 +19,8 @@ import 'package:matrix/matrix.dart'; class ServerSearchController with SearchDebouncerMixin { final String? inRoomId; + BuildContext? currentContext; + ServerSearchController({ this.inRoomId, }); @@ -42,8 +45,10 @@ class ServerSearchController with SearchDebouncerMixin { _searchCategories?.searchTerm.isNotEmpty == true; void initSearch({ + BuildContext? context, Function(String)? onSearchEncryptedMessage, }) { + currentContext = context; initializeDebouncer((searchTerm) { if (onSearchEncryptedMessage != null) { onSearchEncryptedMessage(searchTerm); @@ -92,8 +97,13 @@ class ServerSearchController with SearchDebouncerMixin { ...(searchResultsNotifier.value as PresentationServerSideSearch) .searchResults, - ...success.results ?? [], - ], + ...success.results ?? [], + ] + .where( + (result) => + result.isDisplayableResult(context: currentContext), + ) + .toList(), ); } } diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 4a56c4edfd..18e3c2534b 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -1,8 +1,8 @@ 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/domain/repository/tom_configurations_repository.dart'; import 'package:fluffychat/event/twake_inapp_event_types.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; @@ -39,6 +39,8 @@ class SettingsController extends State with ConnectPageMixin { final ValueNotifier avatarUriNotifier = ValueNotifier(Uri()); final ValueNotifier displayNameNotifier = ValueNotifier(''); + final tomConfigurationRepository = getIt.get(); + StreamSubscription? onAccountDataSubscription; final List getListSettingItem = [ @@ -86,13 +88,19 @@ class SettingsController extends State with ConnectPageMixin { OkCancelResult.cancel) { return; } - final matrix = Matrix.of(context); if (PlatformInfos.isMobile) { - await tryLogoutSso(context); + await _logoutActionsOnMobile(); + } else { + await _logoutActions(); } - if (matrix.twakeSupported == true) { - final hiveCollectionToMDatabase = getIt.get(); - await hiveCollectionToMDatabase.clear(); + } + + Future _logoutActionsOnMobile() async { + try { + await tryLogoutSso(context); + } catch (e) { + Logs().e('SettingsController()::_logoutActionsOnMobile - error: $e'); + return; } await TwakeDialog.showFutureLoadingDialogFullScreen( future: () async { @@ -100,12 +108,36 @@ class SettingsController extends State with ConnectPageMixin { if (matrix.backgroundPush != null) { await matrix.backgroundPush!.removeCurrentPusher(); } - await matrix.client.logout(); + await Future.wait([ + matrix.client.logout(), + _deleteTomConfigurations(matrix.client), + ]); + } catch (e) { + Logs().e('SettingsController()::_logoutActionsOnMobile - error: $e'); + } + }, + ); + } + + Future _logoutActions() async { + await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () async { + try { + if (matrix.backgroundPush != null) { + await matrix.backgroundPush!.removeCurrentPusher(); + } + await Future.wait([ + matrix.client.logout(), + _deleteTomConfigurations(matrix.client), + ]); } catch (e) { - Logs().e('SettingsController()::logoutAction - error: $e'); + Logs().e('SettingsController()::_logoutActions - error: $e'); } finally { - if (PlatformInfos.isWeb) { + try { await tryLogoutSso(context); + } catch (e) { + Logs().e('SettingsController()::_logoutActions - error: $e'); + return; } } }, @@ -244,6 +276,25 @@ class SettingsController extends State with ConnectPageMixin { }); } + Future _deleteTomConfigurations(Client currentClient) async { + try { + Logs().d( + 'SettingsController::_deleteTomConfigurations - Client ID: ${currentClient.userID}', + ); + if (matrix.twakeSupported) { + await tomConfigurationRepository + .deleteTomConfigurations(currentClient.userID!); + } + Logs().d( + 'SettingsController::_deleteTomConfigurations - Success', + ); + } catch (e) { + Logs().e( + 'SettingsController::_deleteTomConfigurations - error: $e', + ); + } + } + @override void initState() { _getCurrentProfile(client); @@ -254,6 +305,14 @@ class SettingsController extends State with ConnectPageMixin { super.initState(); } + @override + void didUpdateWidget(Settings oldWidget) { + if (oldWidget != widget) { + _getCurrentProfile(client); + } + super.didUpdateWidget(oldWidget); + } + @override void dispose() { onAccountDataSubscription?.cancel(); diff --git a/lib/pages/settings_dashboard/settings/settings_view.dart b/lib/pages/settings_dashboard/settings/settings_view.dart index 699e401eec..03ad9e90da 100644 --- a/lib/pages/settings_dashboard/settings/settings_view.dart +++ b/lib/pages/settings_dashboard/settings/settings_view.dart @@ -43,6 +43,8 @@ class SettingsView extends StatelessWidget { ), bottomNavigationBar: bottomNavigationBar, body: ListTileTheme( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use iconColor: Theme.of(context).colorScheme.onBackground, child: ListView( key: const Key('SettingsListViewContent'), diff --git a/lib/pages/settings_dashboard/settings_3pid/settings_3pid.dart b/lib/pages/settings_dashboard/settings_3pid/settings_3pid.dart index c01715f3e6..a1afd5056b 100644 --- a/lib/pages/settings_dashboard/settings_3pid/settings_3pid.dart +++ b/lib/pages/settings_dashboard/settings_3pid/settings_3pid.dart @@ -12,7 +12,7 @@ import 'settings_3pid_view.dart'; class Settings3Pid extends StatefulWidget { static int sendAttempt = 0; - const Settings3Pid({Key? key}) : super(key: key); + const Settings3Pid({super.key}); @override Settings3PidController createState() => Settings3PidController(); diff --git a/lib/pages/settings_dashboard/settings_3pid/settings_3pid_view.dart b/lib/pages/settings_dashboard/settings_3pid/settings_3pid_view.dart index ecb21542cc..f34b36e7ad 100644 --- a/lib/pages/settings_dashboard/settings_3pid/settings_3pid_view.dart +++ b/lib/pages/settings_dashboard/settings_3pid/settings_3pid_view.dart @@ -12,7 +12,7 @@ import 'package:fluffychat/widgets/matrix.dart'; class Settings3PidView extends StatelessWidget { final Settings3PidController controller; - const Settings3PidView(this.controller, {Key? key}) : super(key: key); + const Settings3PidView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_dashboard/settings_chat/settings_chat.dart b/lib/pages/settings_dashboard/settings_chat/settings_chat.dart index b25941669b..1c10355597 100644 --- a/lib/pages/settings_dashboard/settings_chat/settings_chat.dart +++ b/lib/pages/settings_dashboard/settings_chat/settings_chat.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'settings_chat_view.dart'; class SettingsChat extends StatefulWidget { - const SettingsChat({Key? key}) : super(key: key); + const SettingsChat({super.key}); @override SettingsChatController createState() => SettingsChatController(); diff --git a/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart b/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart index 3ca9bb4a2a..5814f1e71f 100644 --- a/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart @@ -15,7 +15,7 @@ import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { final SettingsChatController controller; - const SettingsChatView(this.controller, {Key? key}) : super(key: key); + const SettingsChatView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_dashboard/settings_emotes/settings_emotes.dart b/lib/pages/settings_dashboard/settings_emotes/settings_emotes.dart index 647d4a9f8d..d9d62f5621 100644 --- a/lib/pages/settings_dashboard/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_dashboard/settings_emotes/settings_emotes.dart @@ -13,7 +13,7 @@ import 'package:fluffychat/utils/client_manager.dart'; import 'settings_emotes_view.dart'; class EmotesSettings extends StatefulWidget { - const EmotesSettings({Key? key}) : super(key: key); + const EmotesSettings({super.key}); @override EmotesSettingsController createState() => EmotesSettingsController(); diff --git a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart index fb43918231..26929dfc54 100644 --- a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart @@ -15,7 +15,7 @@ import 'settings_emotes.dart'; class EmotesSettingsView extends StatelessWidget { final EmotesSettingsController controller; - const EmotesSettingsView(this.controller, {Key? key}) : super(key: key); + const EmotesSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart index a208cd6fcb..632719d86d 100644 --- a/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart +++ b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list.dart @@ -7,7 +7,7 @@ import 'settings_ignore_list_view.dart'; class SettingsIgnoreList extends StatefulWidget { final String? initialUserId; - const SettingsIgnoreList({Key? key, this.initialUserId}) : super(key: key); + const SettingsIgnoreList({super.key, this.initialUserId}); @override SettingsIgnoreListController createState() => SettingsIgnoreListController(); diff --git a/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart index cb3cf425e4..c86b8da69d 100644 --- a/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart +++ b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart @@ -15,7 +15,7 @@ import 'settings_ignore_list.dart'; class SettingsIgnoreListView extends StatelessWidget { final SettingsIgnoreListController controller; - const SettingsIgnoreListView(this.controller, {Key? key}) : super(key: key); + const SettingsIgnoreListView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart b/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart index 1bc65019cd..ff3b560729 100644 --- a/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart +++ b/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes.dart @@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart'; import 'settings_multiple_emotes_view.dart'; class MultipleEmotesSettings extends StatefulWidget { - const MultipleEmotesSettings({Key? key}) : super(key: key); + const MultipleEmotesSettings({super.key}); @override MultipleEmotesSettingsController createState() => diff --git a/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes_view.dart b/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes_view.dart index 5232f47caf..d50922701f 100644 --- a/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes_view.dart +++ b/lib/pages/settings_dashboard/settings_multiple_emotes/settings_multiple_emotes_view.dart @@ -12,8 +12,7 @@ import 'package:fluffychat/widgets/matrix.dart'; class MultipleEmotesSettingsView extends StatelessWidget { final MultipleEmotesSettingsController controller; - const MultipleEmotesSettingsView(this.controller, {Key? key}) - : super(key: key); + const MultipleEmotesSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_dashboard/settings_notifications/settings_notifications.dart b/lib/pages/settings_dashboard/settings_notifications/settings_notifications.dart index 66ca48480d..fdebc33c70 100644 --- a/lib/pages/settings_dashboard/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_dashboard/settings_notifications/settings_notifications.dart @@ -50,7 +50,7 @@ class NotificationSettingsItem { } class SettingsNotifications extends StatefulWidget { - const SettingsNotifications({Key? key}) : super(key: key); + const SettingsNotifications({super.key}); @override SettingsNotificationsController createState() => diff --git a/lib/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart b/lib/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart index 611826b3af..322cc01766 100644 --- a/lib/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart +++ b/lib/pages/settings_dashboard/settings_notifications/settings_notifications_view.dart @@ -15,8 +15,7 @@ import 'settings_notifications.dart'; class SettingsNotificationsView extends StatelessWidget { final SettingsNotificationsController controller; - const SettingsNotificationsView(this.controller, {Key? key}) - : super(key: key); + const SettingsNotificationsView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index ada07b9f99..cc015013c2 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -12,14 +12,20 @@ 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/multiple_accounts/multiple_accounts_picker.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_context_menu_actions.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_clients_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/extensions/multiple_accounts/client_profile_extension.dart'; +import 'package:fluffychat/presentation/multiple_account/client_profile_presentation.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/presentation/multiple_account/twake_chat_presentation_account.dart'; +import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -29,6 +35,7 @@ import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:go_router/go_router.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'; @@ -65,9 +72,14 @@ class SettingsProfileController extends State getIt.get(); final ValueNotifier isEditedProfileNotifier = ValueNotifier(false); + final ValueNotifier> settingsProfileUIState = ValueNotifier>(Right(GetAvatarInitialUIState())); + final settingsMultiAccountsUIState = ValueNotifier>( + Right(GetClientsInitialUIState()), + ); + Client get client => Matrix.of(context).client; bool get _hasEditedDisplayName => @@ -538,6 +550,78 @@ class SettingsProfileController extends State } } + Future _getMultipleAccounts( + Client currentActiveClient, + ) async { + try { + settingsMultiAccountsUIState.value = Right(GetClientsLoadingUIState()); + final profileBundles = await _getClientProfiles(); + final multipleAccounts = profileBundles + .where((clientProfile) => clientProfile != null) + .map( + (clientProfile) => clientProfile!.toTwakeChatPresentationAccount( + currentActiveClient, + ), + ) + .toList(); + settingsMultiAccountsUIState.value = Right( + GetClientsSuccessUIState( + multipleAccounts: multipleAccounts, + ), + ); + } catch (e) { + Logs().e( + 'SettingsProfileController::_getMultipleAccounts() - Error: $e', + ); + settingsMultiAccountsUIState.value = Left( + GetClientsFailureUIState( + exception: e, + ), + ); + } + } + + Future> _getClientProfiles() async { + try { + final profiles = await Future.wait( + (await ClientManager.getClients()).map((client) async { + final profileBundle = await client.fetchOwnProfile(); + Logs().d( + 'SettingsProfileController::getProfileBundles() - ClientName - ${client.clientName}', + ); + Logs().d( + 'SettingsProfileController::getProfileBundles() - UserId - ${client.userID}', + ); + return ClientProfilePresentation( + profile: profileBundle, + client: client, + ); + }), + ); + + return profiles.toList(); + } catch (e) { + Logs().e( + 'SettingsProfileController::getProfileBundles() - Error: $e', + ); + rethrow; + } + } + + void onBottomButtonTap({ + required List multipleAccounts, + }) { + MultipleAccountsPickerController( + context: context, + multipleAccounts: multipleAccounts, + ).showMultipleAccountsPicker( + client, + onGoToAccountSettings: () { + context.go('/rooms/profile'); + }, + ); + } + void _handleViewState() { settingsProfileUIState.addListener(() { Logs().d( @@ -547,15 +631,15 @@ class SettingsProfileController extends State (failure) => null, (success) { switch (success.runtimeType) { - case GetAvatarInStreamUIStateSuccess: + case const (GetAvatarInStreamUIStateSuccess): final uiState = success as GetAvatarInStreamUIStateSuccess; assetEntity = uiState.assetEntity; break; - case GetAvatarInBytesUIStateSuccess: + case const (GetAvatarInBytesUIStateSuccess): final uiState = success as GetAvatarInBytesUIStateSuccess; filePickerResult = uiState.filePickerResult; break; - case GetProfileUIStateSuccess: + case const (GetProfileUIStateSuccess): final uiState = success as GetProfileUIStateSuccess; currentProfile = uiState.profile; break; @@ -583,6 +667,9 @@ class SettingsProfileController extends State void initState() { _handleViewState(); _getCurrentProfile(client); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + _getMultipleAccounts(client); + }); super.initState(); } @@ -593,6 +680,7 @@ class SettingsProfileController extends State displayNameFocusNode.dispose(); settingsProfileUIState.dispose(); isEditedProfileNotifier.dispose(); + settingsMultiAccountsUIState.dispose(); super.dispose(); } diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_clients_ui_state.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_clients_ui_state.dart new file mode 100644 index 0000000000..a786577f08 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_state/get_clients_ui_state.dart @@ -0,0 +1,35 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/presentation/multiple_account/twake_chat_presentation_account.dart'; + +class GetClientsInitialUIState extends Success { + @override + List get props => []; +} + +class GetClientsLoadingUIState extends Success { + @override + List get props => []; +} + +class GetClientsSuccessUIState extends Success { + final List multipleAccounts; + + const GetClientsSuccessUIState({ + required this.multipleAccounts, + }); + + bool get haveMultipleAccounts => multipleAccounts.length > 1; + + @override + List get props => [multipleAccounts]; +} + +class GetClientsFailureUIState extends Failure { + final dynamic exception; + + const GetClientsFailureUIState({this.exception}); + + @override + List get props => [exception]; +} 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 2c4aac22c2..807d68131b 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -75,7 +75,7 @@ class SettingsProfileView extends StatelessWidget { backgroundColor: responsive.isWebDesktop(context) ? Theme.of(context).colorScheme.surface : LinagoraSysColors.material().onPrimary, - body: SingleChildScrollView( + body: Padding( padding: SettingsProfileViewStyle.paddingBody, child: SlotLayout( config: { @@ -88,6 +88,12 @@ class SettingsProfileView extends StatelessWidget { client: controller.client, settingsProfileUIState: controller.settingsProfileUIState, onTapAvatar: controller.onTapAvatarInMobile, + onTapMultipleAccountsButton: (multipleAccounts) => + controller.onBottomButtonTap( + multipleAccounts: multipleAccounts, + ), + settingsMultiAccountsUIState: + controller.settingsMultiAccountsUIState, menuChildren: controller.listContextMenuBuilder(context), menuController: controller.menuController, settingsProfileOptions: ListView.separated( 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 c3d36bcc7b..8746e021c6 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 @@ -2,9 +2,11 @@ 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_clients_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/presentation/multiple_account/twake_chat_presentation_account.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; @@ -12,13 +14,20 @@ 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'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +typedef OnTapMultipleAccountsButton = void Function( + List multipleAccounts, +); class SettingsProfileViewMobile extends StatelessWidget { final ValueNotifier> settingsProfileUIState; + final OnTapMultipleAccountsButton onTapMultipleAccountsButton; final Widget settingsProfileOptions; final VoidCallback? onTapAvatar; final List? menuChildren; final MenuController? menuController; + final ValueNotifier> settingsMultiAccountsUIState; final Client client; const SettingsProfileViewMobile({ @@ -27,6 +36,8 @@ class SettingsProfileViewMobile extends StatelessWidget { required this.onTapAvatar, required this.settingsProfileUIState, required this.client, + required this.onTapMultipleAccountsButton, + required this.settingsMultiAccountsUIState, this.menuChildren, this.menuController, }); @@ -35,167 +46,276 @@ class SettingsProfileViewMobile extends StatelessWidget { Widget build(BuildContext 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, - ), - ValueListenableBuilder( - valueListenable: settingsProfileUIState, - builder: (context, uiState, child) => uiState.fold( - (failure) => child!, - (success) { - if (success is GetAvatarInStreamUIStateSuccess && - PlatformInfos.isMobile) { - if (success.assetEntity == null) { - return child!; - } - return ClipOval( - child: SizedBox.fromSize( - size: const Size.fromRadius( - AvatarStyle.defaultSize, - ), - child: AssetEntityImage( - width: AvatarStyle.defaultSize, - height: AvatarStyle.defaultSize, - success.assetEntity!, - thumbnailSize: const ThumbnailSize( - SettingsProfileViewMobileStyle.thumbnailSize, - SettingsProfileViewMobileStyle.thumbnailSize, + 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 && + PlatformInfos.isMobile) { + if (success.assetEntity == null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( + AvatarStyle.defaultSize, + ), + child: AssetEntityImage( + width: AvatarStyle.defaultSize, + height: AvatarStyle.defaultSize, + 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), + ); + }, + ), ), - 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 GetAvatarInBytesUIStateSuccess && - PlatformInfos.isWeb) { - if (success.filePickerResult == null || - success.filePickerResult?.files.single.bytes == - null) { + ); + } + if (success is GetAvatarInBytesUIStateSuccess && + PlatformInfos.isWeb) { + if (success.filePickerResult == null || + success.filePickerResult?.files.single.bytes == + null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( + AvatarStyle.defaultSize, + ), + 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, + ), + borderRadius: BorderRadius.circular( + AvatarStyle.defaultSize, + ), + ), + child: Avatar( + mxContent: success.profile.avatarUrl, + name: displayName, + size: SettingsProfileViewMobileStyle.avatarSize, + fontSize: + SettingsProfileViewMobileStyle.avatarFontSize, + ), + ); + } return child!; - } - return ClipOval( - child: SizedBox.fromSize( - size: const Size.fromRadius( - AvatarStyle.defaultSize, + }, + ), + child: const SizedBox.shrink(), + ), + Positioned( + bottom: SettingsProfileViewMobileStyle.positionedBottomSize, + right: SettingsProfileViewMobileStyle.positionedRightSize, + child: MenuAnchor( + controller: menuController, + builder: ( + BuildContext context, + MenuController menuController, + Widget? child, + ) { + return GestureDetector( + onTap: () { + if (PlatformInfos.isWeb) { + menuController.isOpen + ? menuController.close() + : menuController.open(); + } else { + onTapAvatar?.call(); + } + }, + 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, + ), ), - child: Image.memory( - success.filePickerResult!.files.single.bytes!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Center( - child: Icon(Icons.error_outline), - ); - }, + ); + }, + menuChildren: menuChildren ?? [], + ), + ), + ], + ), + ), + settingsProfileOptions, + ], + ), + const Expanded(child: SizedBox()), + if (PlatformInfos.isMobile) + ValueListenableBuilder( + valueListenable: settingsMultiAccountsUIState, + builder: (context, uiState, child) => uiState.fold( + (failure) => child!, + (success) { + if (success is GetClientsLoadingUIState) { + return Container( + width: double.infinity, + height: SettingsProfileViewMobileStyle.bottomButtonHeight, + padding: SettingsProfileViewMobileStyle.paddingBottomButton, + margin: SettingsProfileViewMobileStyle.marginBottomButton, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + SettingsProfileViewMobileStyle.bottomButtonRadius, + ), + color: Theme.of(context).colorScheme.primary, + ), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Transform.scale( + scale: SettingsProfileViewMobileStyle.indicatorScale, + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.onPrimary, + strokeWidth: SettingsProfileViewMobileStyle + .indicatorStrokeWidth, ), ), - ); - } - 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, - ), + SettingsProfileViewMobileStyle.paddingIconAndText, + Text( + L10n.of(context)!.loadingPleaseWait, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), ), - child: Avatar( - mxContent: success.profile.avatarUrl, - name: displayName, - size: SettingsProfileViewMobileStyle.avatarSize, - fontSize: - SettingsProfileViewMobileStyle.avatarFontSize, + ], + ), + ); + } + + if (success is GetClientsSuccessUIState) { + return InkWell( + onTap: () => onTapMultipleAccountsButton.call( + success.multipleAccounts, + ), + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + child: Container( + width: double.infinity, + height: SettingsProfileViewMobileStyle.bottomButtonHeight, + padding: + SettingsProfileViewMobileStyle.paddingBottomButton, + margin: SettingsProfileViewMobileStyle.marginBottomButton, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + SettingsProfileViewMobileStyle.bottomButtonRadius, ), - ); - } - return child!; - }, - ), - child: const SizedBox.shrink(), - ), - Positioned( - bottom: SettingsProfileViewMobileStyle.positionedBottomSize, - right: SettingsProfileViewMobileStyle.positionedRightSize, - child: MenuAnchor( - controller: menuController, - builder: ( - BuildContext context, - MenuController menuController, - Widget? child, - ) { - return GestureDetector( - onTap: () { - if (PlatformInfos.isWeb) { - menuController.isOpen - ? menuController.close() - : menuController.open(); - } else { - onTapAvatar?.call(); - } - }, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular( - SettingsProfileViewMobileStyle.avatarSize, - ), - border: Border.all( + color: Theme.of(context).colorScheme.primary, + ), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + success.haveMultipleAccounts + ? Icons.group_outlined + : Icons.person_add_alt_outlined, + size: SettingsProfileViewMobileStyle.iconSize, 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, - ), + SettingsProfileViewMobileStyle.paddingIconAndText, + Text( + success.haveMultipleAccounts + ? L10n.of(context)!.switchAccounts + : L10n.of(context)!.addAnotherAccount, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: + Theme.of(context).colorScheme.onPrimary, + ), + ), + ], ), - ); - }, - menuChildren: menuChildren ?? [], - ), - ), - ], + ), + ); + } + + return child!; + }, + ), + child: const SizedBox.shrink(), ), - ), - 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 76f99dfdef..7cf06fa94d 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 @@ -16,4 +16,14 @@ class SettingsProfileViewMobileStyle { static EdgeInsetsDirectional editIconPadding = const EdgeInsetsDirectional.all(8); + + static const bottomButtonHeight = 48.0; + static const paddingBottomButton = EdgeInsets.only(left: 16.0, right: 16.0); + static const marginBottomButton = EdgeInsets.only(bottom: 32.0); + static const bottomButtonRadius = 100.0; + static const iconSize = 18.0; + static const indicatorStrokeWidth = 2.0; + static const indicatorScale = 0.7; + + static const paddingIconAndText = SizedBox(width: 8.0); } 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 edd834aade..42e68e6241 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 @@ -34,7 +34,8 @@ class SettingsProfileViewWeb extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: SettingsProfileViewWebStyle.paddingBody, - child: Center( + child: Align( + alignment: Alignment.topCenter, child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: Column( diff --git a/lib/pages/settings_dashboard/settings_security/settings_security.dart b/lib/pages/settings_dashboard/settings_security/settings_security.dart index e4a3830e56..fec4f6d038 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security.dart @@ -21,7 +21,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'settings_security_view.dart'; class SettingsSecurity extends StatefulWidget { - const SettingsSecurity({Key? key}) : super(key: key); + const SettingsSecurity({super.key}); @override SettingsSecurityController createState() => SettingsSecurityController(); diff --git a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart index 473630eea6..dc435c600b 100644 --- a/lib/pages/settings_dashboard/settings_security/settings_security_view.dart +++ b/lib/pages/settings_dashboard/settings_security/settings_security_view.dart @@ -11,7 +11,7 @@ import 'settings_security.dart'; class SettingsSecurityView extends StatelessWidget { final SettingsSecurityController controller; - const SettingsSecurityView(this.controller, {Key? key}) : super(key: key); + const SettingsSecurityView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -22,6 +22,8 @@ class SettingsSecurityView extends StatelessWidget { context: context, ), body: ListTileTheme( + // TODO: remove when the color scheme is updated + // ignore: deprecated_member_use iconColor: Theme.of(context).colorScheme.onBackground, child: MaxWidthBody( withScrolling: true, diff --git a/lib/pages/settings_dashboard/settings_stories/settings_stories.dart b/lib/pages/settings_dashboard/settings_stories/settings_stories.dart index 5dd029fb95..b81e06479e 100644 --- a/lib/pages/settings_dashboard/settings_stories/settings_stories.dart +++ b/lib/pages/settings_dashboard/settings_stories/settings_stories.dart @@ -7,7 +7,7 @@ import 'package:fluffychat/pages/settings_dashboard/settings_stories/settings_st import 'package:fluffychat/widgets/matrix.dart'; class SettingsStories extends StatefulWidget { - const SettingsStories({Key? key}) : super(key: key); + const SettingsStories({super.key}); @override SettingsStoriesController createState() => SettingsStoriesController(); diff --git a/lib/pages/settings_dashboard/settings_stories/settings_stories_view.dart b/lib/pages/settings_dashboard/settings_stories/settings_stories_view.dart index c4836118df..63cb52536a 100644 --- a/lib/pages/settings_dashboard/settings_stories/settings_stories_view.dart +++ b/lib/pages/settings_dashboard/settings_stories/settings_stories_view.dart @@ -9,7 +9,7 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class SettingsStoriesView extends StatelessWidget { final SettingsStoriesController controller; - const SettingsStoriesView(this.controller, {Key? key}) : super(key: key); + const SettingsStoriesView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_dashboard/settings_style/settings_style.dart b/lib/pages/settings_dashboard/settings_style/settings_style.dart index 4f2bc67087..4cee371c7f 100644 --- a/lib/pages/settings_dashboard/settings_style/settings_style.dart +++ b/lib/pages/settings_dashboard/settings_style/settings_style.dart @@ -8,7 +8,7 @@ import 'package:fluffychat/widgets/theme_builder.dart'; import 'settings_style_view.dart'; class SettingsStyle extends StatefulWidget { - const SettingsStyle({Key? key}) : super(key: key); + const SettingsStyle({super.key}); @override SettingsStyleController createState() => SettingsStyleController(); diff --git a/lib/pages/settings_dashboard/settings_style/settings_style_view.dart b/lib/pages/settings_dashboard/settings_style/settings_style_view.dart index 9c4258f4cf..8f7d45821d 100644 --- a/lib/pages/settings_dashboard/settings_style/settings_style_view.dart +++ b/lib/pages/settings_dashboard/settings_style/settings_style_view.dart @@ -11,7 +11,7 @@ import 'settings_style.dart'; class SettingsStyleView extends StatelessWidget { final SettingsStyleController controller; - const SettingsStyleView(this.controller, {Key? key}) : super(key: key); + const SettingsStyleView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/sign_up/signup.dart b/lib/pages/sign_up/signup.dart index 2f5a8b7964..7bb8092601 100644 --- a/lib/pages/sign_up/signup.dart +++ b/lib/pages/sign_up/signup.dart @@ -7,7 +7,7 @@ import 'package:go_router/go_router.dart'; import '../../utils/localized_exception_extension.dart'; class SignupPage extends StatefulWidget { - const SignupPage({Key? key}) : super(key: key); + const SignupPage({super.key}); @override SignupPageController createState() => SignupPageController(); diff --git a/lib/pages/sign_up/signup_view.dart b/lib/pages/sign_up/signup_view.dart index 698cff44b6..bad1399e08 100644 --- a/lib/pages/sign_up/signup_view.dart +++ b/lib/pages/sign_up/signup_view.dart @@ -7,7 +7,7 @@ import 'signup.dart'; class SignupPageView extends StatelessWidget { final SignupPageController controller; - const SignupPageView(this.controller, {Key? key}) : super(key: key); + const SignupPageView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/story/story_page.dart b/lib/pages/story/story_page.dart index 63d4e29d40..4bbeb682b5 100644 --- a/lib/pages/story/story_page.dart +++ b/lib/pages/story/story_page.dart @@ -26,7 +26,7 @@ import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; class StoryPage extends StatefulWidget { - const StoryPage({Key? key}) : super(key: key); + const StoryPage({super.key}); @override StoryPageController createState() => StoryPageController(); diff --git a/lib/pages/story/story_view.dart b/lib/pages/story/story_view.dart index 6cb6f9fd55..131ff35faa 100644 --- a/lib/pages/story/story_view.dart +++ b/lib/pages/story/story_view.dart @@ -20,7 +20,7 @@ import '../../config/themes.dart'; class StoryView extends StatelessWidget { final StoryPageController controller; - const StoryView(this.controller, {Key? key}) : super(key: key); + const StoryView(this.controller, {super.key}); static const List textShadows = [ Shadow( @@ -377,6 +377,8 @@ class StoryView extends StatelessWidget { onPressed: controller.replyAction, icon: const Icon(Icons.send_outlined), ), + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use fillColor: Theme.of(context).colorScheme.background, ), ), diff --git a/lib/pages/twake_welcome/twake_welcome.dart b/lib/pages/twake_welcome/twake_welcome.dart index a091123c57..23b8867089 100644 --- a/lib/pages/twake_welcome/twake_welcome.dart +++ b/lib/pages/twake_welcome/twake_welcome.dart @@ -1,9 +1,15 @@ +import 'dart:async'; + import 'package:fluffychat/config/app_config.dart'; import 'package:equatable/equatable.dart'; import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome_view.dart'; +import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -37,8 +43,8 @@ class TwakeWelcome extends StatefulWidget { class TwakeWelcomeController extends State with ConnectPageMixin { void goToHomeserverPicker() { - if (widget.arg?.isAddAnotherAccount == true) { - context.push('/rooms/addhomeserver'); + if (widget.arg != null && widget.arg?.isAddAnotherAccount == true) { + context.push('/rooms/addaccount/homeserverpicker'); } else { context.push('/home/homeserverpicker'); } @@ -51,10 +57,10 @@ class TwakeWelcomeController extends State with ConnectPageMixin { 'post_registered_redirect_url'; String get loginUrl => - "${AppConfig.registrationUrl}?$postLoginRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; + "${AppConfig.registrationUrl}?$postLoginRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect&app=${AppConfig.appParameter}"; String get signupUrl => - "${AppConfig.registrationUrl}?$postRegisteredRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect"; + "${AppConfig.registrationUrl}?$postRegisteredRedirectUrlPathParams=${AppConfig.appOpenUrlScheme}://redirect&app=${AppConfig.appParameter}"; MatrixState get matrix => Matrix.of(context); @@ -64,19 +70,25 @@ class TwakeWelcomeController extends State with ConnectPageMixin { } void _redirectRegistrationUrl(String url) async { - matrix.loginHomeserverSummary = - await matrix.getLoginClient().checkHomeserver( - Uri.parse(AppConfig.twakeWorkplaceHomeserver), - ); - final uri = await FlutterWebAuth2.authenticate( - url: url, - callbackUrlScheme: AppConfig.appOpenUrlScheme, - options: const FlutterWebAuth2Options( - intentFlags: ephemeralIntentFlags, - ), - ); - Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); - handleTokenFromRegistrationSite(matrix: matrix, uri: uri); + try { + final homeserverExisted = await _homeserverExisted(); + if (homeserverExisted) return; + matrix.loginHomeserverSummary = + await matrix.getLoginClient().checkHomeserver( + Uri.parse(AppConfig.twakeWorkplaceHomeserver), + ); + final uri = await FlutterWebAuth2.authenticate( + url: url, + callbackUrlScheme: AppConfig.appOpenUrlScheme, + options: const FlutterWebAuth2Options( + intentFlags: ephemeralIntentFlags, + ), + ); + Logs().d("TwakeIdController:_redirectRegistrationUrl: URI - $uri"); + await handleTokenFromRegistrationSite(matrix: matrix, uri: uri); + } catch (e) { + Logs().e("TwakeIdController::_redirectRegistrationUrl: $e"); + } } void onClickCreateTwakeId() { @@ -86,8 +98,46 @@ class TwakeWelcomeController extends State with ConnectPageMixin { _redirectRegistrationUrl(signupUrl); } + Future _homeserverExisted() async { + if (widget.arg != null && widget.arg?.isAddAnotherAccount == false) { + return false; + } + try { + final allHomeserverLoggedIn = (await ClientManager.getClients()) + .where((client) => client.homeserver != null) + .map((client) => client.homeserver.toString()) + .toList(); + Logs().i('All homeservers: $allHomeserverLoggedIn'); + final homeserverExists = allHomeserverLoggedIn.any( + (homeserver) => "$homeserver/".contains(AppConfig.homeserver), + ); + + if (homeserverExists && + !AppConfig.supportMultipleAccountsInTheSameHomeserver) { + TwakeSnackBar.show( + context, + L10n.of(context)!.isSingleAccountOnHomeserver, + ); + return true; + } + } catch (e) { + Logs().e('TwakeIdController::_homeserverExisted: $e'); + return false; + } + return false; + } + + void onClickPrivacyPolicy() { + UrlLauncher( + context, + url: AppConfig.privacyUrl, + ).openUrlInAppBrowser(); + } + @override Widget build(BuildContext context) { - return TwakeWelcomeView(controller: this); + return TwakeWelcomeView( + controller: this, + ); } } diff --git a/lib/pages/twake_welcome/twake_welcome_view.dart b/lib/pages/twake_welcome/twake_welcome_view.dart index 82468e2b42..84ce238e28 100644 --- a/lib/pages/twake_welcome/twake_welcome_view.dart +++ b/lib/pages/twake_welcome/twake_welcome_view.dart @@ -2,9 +2,11 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome_view_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class TwakeWelcomeView extends StatelessWidget { @@ -15,10 +17,21 @@ class TwakeWelcomeView extends StatelessWidget { @override Widget build(BuildContext context) { return TwakeWelcomeScreen( + appBar: controller.widget.arg?.isAddAnotherAccount == true + ? AppBar( + backgroundColor: Colors.transparent, + leading: TwakeIconButton( + icon: Icons.arrow_back, + onTap: () => context.pop(), + tooltip: L10n.of(context)!.back, + ), + elevation: 0, + ) + : null, focusColor: Colors.transparent, hoverColor: Colors.transparent, highlightColor: Colors.transparent, - overlayColor: MaterialStateProperty.all(Colors.transparent), + overlayColor: WidgetStateProperty.all(Colors.transparent), signInTitle: AppConfig.isSaasPlatForm ? L10n.of(context)!.signIn : null, createTwakeIdTitle: AppConfig.isSaasPlatForm ? L10n.of(context)!.createTwakeId : null, @@ -26,6 +39,9 @@ class TwakeWelcomeView extends StatelessWidget { description: L10n.of(context)!.descriptionTwakeId, onUseCompanyServerOnTap: controller.goToHomeserverPicker, onSignInOnTap: AppConfig.isSaasPlatForm ? controller.onClickSignIn : null, + privacyPolicy: L10n.of(context)!.privacyPolicy, + descriptionPrivacyPolicy: L10n.of(context)!.byContinuingYourAgreeingToOur, + onPrivacyPolicyOnTap: controller.onClickPrivacyPolicy, onCreateTwakeIdOnTap: AppConfig.isSaasPlatForm ? controller.onClickCreateTwakeId : null, logo: SvgPicture.asset( diff --git a/lib/presentation/enum/chat/chat_details_screen_enum.dart b/lib/presentation/enum/chat/chat_details_screen_enum.dart new file mode 100644 index 0000000000..d211f22cb0 --- /dev/null +++ b/lib/presentation/enum/chat/chat_details_screen_enum.dart @@ -0,0 +1 @@ +enum ChatDetailsScreenEnum { group, direct } diff --git a/lib/presentation/extensions/contact/presentation_contact_extension.dart b/lib/presentation/extensions/contact/presentation_contact_extension.dart index 2e7567da4e..e5bf658f26 100644 --- a/lib/presentation/extensions/contact/presentation_contact_extension.dart +++ b/lib/presentation/extensions/contact/presentation_contact_extension.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/domain/model/contact/contact.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; extension PresentaionContactExtension on PresentationContact { diff --git a/lib/presentation/extensions/event_update_extension.dart b/lib/presentation/extensions/event_update_extension.dart index fc02f270dc..1b9c889994 100644 --- a/lib/presentation/extensions/event_update_extension.dart +++ b/lib/presentation/extensions/event_update_extension.dart @@ -20,6 +20,10 @@ extension EventUpdateExtension on EventUpdate { return content['type'] == EventTypes.RoomPinnedEvents; } + bool get isMemberChangedEvent { + return content['type'] == EventTypes.RoomMember; + } + bool _isPinnedListChanged( Map content, Map prevContent, diff --git a/lib/presentation/extensions/go_router_extensions.dart b/lib/presentation/extensions/go_router_extensions.dart new file mode 100644 index 0000000000..7747afe0d2 --- /dev/null +++ b/lib/presentation/extensions/go_router_extensions.dart @@ -0,0 +1,14 @@ +import 'package:go_router/go_router.dart'; + +extension GoRouterExtensions on GoRouter { + String? get activeRoomId { + try { + final path = routeInformationProvider.value.uri.path; + if (path.isEmpty) return null; + if (!path.startsWith('/rooms/')) return null; + return path.split('/')[2]; + } catch (e) { + return null; + } + } +} diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index 1f0aed96a3..86aaefa346 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/presentation/extensions/image_extension.dart'; import 'package:fluffychat/presentation/fake_sending_file_info.dart'; import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:flutter/widgets.dart'; import 'package:image/image.dart' as img; @@ -70,8 +71,25 @@ extension SendFileExtension on Room { rethrow; } - final encryptedService = EncryptedService(); final tempDir = await getTemporaryDirectory(); + + if (TwakeMimeTypeExtension.heicMimeTypes.contains(fileInfo.mimeType) && + fileInfo is ImageFileInfo) { + final formattedDateTime = DateTime.now().getFormattedCurrentDateTime(); + final targetPath = + await File('${tempDir.path}/$formattedDateTime${fileInfo.fileName}') + .create(); + await _generateThumbnail(fileInfo, targetPath: targetPath.path); + fileInfo = ImageFileInfo( + fileInfo.fileName, + targetPath.path, + await targetPath.length(), + width: fileInfo.width, + height: fileInfo.height, + ); + } + + final encryptedService = EncryptedService(); final formattedDateTime = DateTime.now().getFormattedCurrentDateTime(); final tempEncryptedFile = await File('${tempDir.path}/$formattedDateTime${fileInfo.fileName}') @@ -358,6 +376,9 @@ extension SendFileExtension on Room { 'msgtype': messageType, 'body': fileInfo.fileName, 'filename': fileInfo.fileName, + 'info': { + ...fileInfo.metadata, + }, }, type: EventTypes.Message, eventId: txid, diff --git a/lib/presentation/extensions/value_notifier_custom.dart b/lib/presentation/extensions/value_notifier_custom.dart new file mode 100644 index 0000000000..835b41c681 --- /dev/null +++ b/lib/presentation/extensions/value_notifier_custom.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart'; + +class ValueNotifierCustom extends ValueNotifier { + bool _isDisposed = false; + + ValueNotifierCustom(super.value); + + bool get isDisposed => _isDisposed; + + @override + void dispose() { + _isDisposed = true; + super.dispose(); + } +} diff --git a/lib/presentation/mixins/chat_details_tab_mixin.dart b/lib/presentation/mixins/chat_details_tab_mixin.dart new file mode 100644 index 0000000000..7390110151 --- /dev/null +++ b/lib/presentation/mixins/chat_details_tab_mixin.dart @@ -0,0 +1,315 @@ +import 'dart:async'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_members_page.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/chat_details_page_enum.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/files/chat_details_files_page.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart'; +import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; +import 'package:fluffychat/pages/invitation_selection/invitation_selection_web.dart'; +import 'package:fluffychat/presentation/enum/chat/chat_details_screen_enum.dart'; +import 'package:fluffychat/presentation/extensions/event_update_extension.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'; +import 'package:fluffychat/presentation/model/chat_details/chat_details_page_model.dart'; +import 'package:fluffychat/presentation/same_type_events_builder/same_type_events_controller.dart'; +import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/utils/scroll_controller_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:matrix/matrix.dart'; + +mixin ChatDetailsTabMixin + on + SingleTickerProviderStateMixin, + HandleVideoDownloadMixin, + PlayVideoActionMixin { + final GlobalKey nestedScrollViewState = GlobalKey(); + + final responsive = getIt.get(); + + final ValueNotifier?> _membersNotifier = ValueNotifier(null); + + late final List tabList; + + Room? get room; + + ChatDetailsScreenEnum get chatType; + + int get actualMembersCount => room!.summary.actualMembersCount; + + bool get isMobileAndTablet => + responsive.isMobile(context) || responsive.isTablet(context); + + SameTypeEventsBuilderController? _mediaListController; + SameTypeEventsBuilderController? _linksListController; + SameTypeEventsBuilderController? _filesListController; + TabController? tabController; + + Timeline? _timeline; + + StreamSubscription? _onRoomEventChangedSubscription; + + static const _mediaFetchLimit = 20; + static const _linksFetchLimit = 20; + static const _filesFetchLimit = 20; + + static const _memberPageKey = PageStorageKey('members'); + static const _mediaPageKey = PageStorageKey('media'); + static const _linksPageKey = PageStorageKey('links'); + static const _filesPageKey = PageStorageKey('files'); + + static const invitationSelectionMobileAndTabletKey = + Key('InvitationSelectionMobileAndTabletKey'); + static const invitationSelectionWebAndDesktopKey = + Key('InvitationSelectionWebAndDesktopKey'); + + Future _getTimeline() async { + _timeline ??= await room!.getTimeline(); + return _timeline!; + } + + Future _handleDownloadAndPlayVideo(Event event) { + return handleDownloadVideoEvent( + event: event, + playVideoAction: (path) => playVideoAction( + context, + path, + event: event, + ), + ); + } + + void _initTabList() { + if (room != null) { + tabList = [ + if (chatType == ChatDetailsScreenEnum.group) ChatDetailsPage.members, + ChatDetailsPage.media, + ChatDetailsPage.links, + ChatDetailsPage.files, + ]; + } else { + tabList = []; + } + } + + void _listenForRoomMembersChanged() { + _onRoomEventChangedSubscription = + Matrix.of(context).client.onEvent.stream.listen((event) { + if (event.isMemberChangedEvent && room?.id == event.roomID) { + _membersNotifier.value = room?.getParticipants(); + } + }); + } + + void _requestMoreMembersAction() async { + final participants = await TwakeDialog.showFutureLoadingDialogFullScreen( + future: () => room!.requestParticipants(), + ); + if (participants.error == null) { + _membersNotifier.value = participants.result; + } + } + + void _openDialogInvite() { + if (PlatformInfos.isMobile) { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (_) => InvitationSelection( + roomId: room!.id, + ), + ), + ); + return; + } + showDialog( + context: context, + barrierDismissible: false, + useSafeArea: false, + useRootNavigator: !PlatformInfos.isMobile, + builder: (context) { + return SlotLayout( + config: { + const WidthPlatformBreakpoint( + begin: ResponsiveUtils.minDesktopWidth, + ): SlotLayout.from( + key: invitationSelectionWebAndDesktopKey, + builder: (_) => InvitationSelectionWebView( + roomId: room!.id, + ), + ), + const WidthPlatformBreakpoint( + end: ResponsiveUtils.minDesktopWidth, + ): SlotLayout.from( + key: invitationSelectionMobileAndTabletKey, + builder: (_) => InvitationSelection( + roomId: room!.id, + ), + ), + }, + ); + }, + ); + } + + Future _onUpdateMembers() async { + final members = await room!.requestParticipantsFromServer(); + _membersNotifier.value = members; + } + + void _initControllers() { + tabController = TabController( + length: tabList.length, + vsync: this, + ); + _mediaListController = SameTypeEventsBuilderController( + getTimeline: () => _getTimeline(), + searchFunc: (event) => event.isVideoOrImage, + limit: _mediaFetchLimit, + ); + _linksListController = SameTypeEventsBuilderController( + getTimeline: () => _getTimeline(), + searchFunc: (event) => event.isContainsLink, + limit: _linksFetchLimit, + ); + _filesListController = SameTypeEventsBuilderController( + getTimeline: () => _getTimeline(), + searchFunc: (event) => event.isAFile, + limit: _filesFetchLimit, + ); + } + + void _initMembers() { + if (chatType == ChatDetailsScreenEnum.group) { + _membersNotifier.value ??= room?.getParticipants(); + } + } + + void _listenerInnerController() { + if (nestedScrollViewState.currentState?.innerController.shouldLoadMore == + true && + tabController?.index != null) { + switch (tabList[tabController!.index]) { + case ChatDetailsPage.media: + _mediaListController?.loadMore(); + break; + case ChatDetailsPage.links: + _linksListController?.loadMore(); + break; + case ChatDetailsPage.files: + _filesListController?.loadMore(); + break; + default: + break; + } + } + } + + void _refreshDataInTabViewInit() { + _linksListController?.refresh(); + _mediaListController?.refresh(); + _filesListController?.refresh(); + } + + void _disposeControllers() { + _mediaListController?.dispose(); + _linksListController?.dispose(); + _filesListController?.dispose(); + tabController?.dispose(); + } + + void onTapAddMembers() { + _openDialogInvite(); + } + + List sharedPages() => tabList.map( + (page) { + if (chatType == ChatDetailsScreenEnum.group && + page == ChatDetailsPage.members) { + return ChatDetailsPageModel( + page: page, + child: ChatDetailsMembersPage( + key: _memberPageKey, + membersNotifier: _membersNotifier, + actualMembersCount: actualMembersCount, + canRequestMoreMembers: + (_membersNotifier.value?.length ?? 0) < actualMembersCount, + requestMoreMembersAction: _requestMoreMembersAction, + openDialogInvite: _openDialogInvite, + isMobileAndTablet: isMobileAndTablet, + onUpdatedMembers: _onUpdateMembers, + ), + ); + } + switch (page) { + case ChatDetailsPage.media: + return ChatDetailsPageModel( + page: page, + child: _mediaListController == null + ? const SizedBox() + : ChatDetailsMediaPage( + key: _mediaPageKey, + controller: _mediaListController!, + handleDownloadVideoEvent: _handleDownloadAndPlayVideo, + // closeRightColumn: widget.closeRightColumn, + ), + ); + case ChatDetailsPage.links: + return ChatDetailsPageModel( + page: page, + child: _linksListController == null + ? const SizedBox() + : ChatDetailsLinksPage( + key: _linksPageKey, + controller: _linksListController!, + ), + ); + case ChatDetailsPage.files: + return ChatDetailsPageModel( + page: page, + child: _filesListController == null + ? const SizedBox() + : ChatDetailsFilesPage( + key: _filesPageKey, + controller: _filesListController!, + ), + ); + default: + return ChatDetailsPageModel( + page: page, + child: const SizedBox(), + ); + } + }, + ).toList(); + + @override + void initState() { + super.initState(); + _initTabList(); + _initMembers(); + _initControllers(); + WidgetsBinding.instance.addPostFrameCallback((_) { + nestedScrollViewState.currentState?.innerController.addListener( + () => _listenerInnerController(), + ); + _refreshDataInTabViewInit(); + }); + _listenForRoomMembersChanged(); + } + + @override + void dispose() { + _disposeControllers(); + _membersNotifier.dispose(); + _onRoomEventChangedSubscription?.cancel(); + nestedScrollViewState.currentState?.innerController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/mixins/comparable_presentation_contact_mixin.dart b/lib/presentation/mixins/comparable_presentation_contact_mixin.dart index d0a973b587..d25ba499ab 100644 --- a/lib/presentation/mixins/comparable_presentation_contact_mixin.dart +++ b/lib/presentation/mixins/comparable_presentation_contact_mixin.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; mixin ComparablePresentationContactMixin { int comparePresentationContacts( diff --git a/lib/presentation/mixins/connect_page_mixin.dart b/lib/presentation/mixins/connect_page_mixin.dart index 6bfcf26c78..de2aeaf7a9 100644 --- a/lib/presentation/mixins/connect_page_mixin.dart +++ b/lib/presentation/mixins/connect_page_mixin.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/auto_homeserver_picker/auto_homeserver_picker.dart'; import 'package:fluffychat/pages/connect/connect_page.dart'; +import 'package:fluffychat/pages/connect/sso_login_state.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/exception/homeserver_exception.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -22,7 +23,7 @@ mixin ConnectPageMixin { static const windowNameValue = '_self'; - static const redirectPublicPlatformOnWeb = 'post_login_redirect_url'; + static const redirectPublicPlatformOnWeb = 'post_registered_redirect_url'; bool supportsFlow({ required BuildContext context, @@ -80,7 +81,7 @@ mixin ConnectPageMixin { required String redirectUrl, }) { final redirectUrlEncode = Uri.encodeQueryComponent(redirectUrl); - return '${AppConfig.registrationUrl}?$redirectPublicPlatformOnWeb=$redirectUrlEncode'; + return '${AppConfig.registrationUrl}?$redirectPublicPlatformOnWeb=$redirectUrlEncode&app=${AppConfig.appParameter}'; } String? _getLogoutUrl( @@ -116,47 +117,54 @@ mixin ConnectPageMixin { ); } - Future ssoLoginAction({ + Future ssoLoginAction({ required BuildContext context, required String id, }) async { if (PlatformInfos.isWeb) { - await ssoLoginActionWeb(context: context, id: id); + return ssoLoginActionWeb(context: context, id: id); } else { - ssoLoginActionMobile(context: context, id: id); + return ssoLoginActionMobile(context: context, id: id); } } - Future ssoLoginActionWeb({ + Future ssoLoginActionWeb({ required BuildContext context, required String id, }) async { await authenticateWithWebAuth(context: context, id: id); + return SsoLoginState.success; } - void ssoLoginActionMobile({ + Future ssoLoginActionMobile({ required BuildContext context, required String id, }) async { - final result = await authenticateWithWebAuth(context: context, id: id); - final token = Uri.parse(result).queryParameters['loginToken']; - if (token?.isEmpty ?? false) return; - Matrix.of(context).loginType = LoginType.mLoginToken; - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => Matrix.of(context) - .getLoginClient() - .login( - LoginType.mLoginToken, - token: token, - initialDeviceDisplayName: PlatformInfos.clientName, - ) - .timeout( - AutoHomeserverPickerController.autoHomeserverPickerTimeout, - onTimeout: () { - throw CheckHomeserverTimeoutException(); - }, - ), - ); + try { + final result = await authenticateWithWebAuth(context: context, id: id); + final token = Uri.parse(result).queryParameters['loginToken']; + if (token?.isEmpty ?? false) return SsoLoginState.tokenEmpty; + Matrix.of(context).loginType = LoginType.mLoginToken; + await TwakeDialog.showStreamDialogFullScreen( + future: () => Matrix.of(context) + .getLoginClient() + .login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ) + .timeout( + AutoHomeserverPickerController.autoHomeserverPickerTimeout, + onTimeout: () { + throw CheckHomeserverTimeoutException(); + }, + ), + ); + return SsoLoginState.success; + } catch (e) { + Logs().e('ConnectPageMixin:: ssoLoginActionMobil(): error: $e'); + return SsoLoginState.error; + } } Future tryLogoutSso(BuildContext context) async { @@ -177,6 +185,7 @@ mixin ConnectPageMixin { Logs().d('tryLogoutSso::result: $result'); } catch (e) { Logs().d('tryLogoutSso::error: $e'); + rethrow; } } @@ -245,23 +254,40 @@ mixin ConnectPageMixin { return list; } - void handleTokenFromRegistrationSite({ + Future handleTokenFromRegistrationSite({ required MatrixState matrix, required String uri, }) async { - final token = Uri.parse(uri).queryParameters['loginToken']; - Logs().d( - "ConnectPageMixin: handleTokenFromRegistrationSite: token: $token", - ); - if (token == null || token.isEmpty == true) return; - matrix.loginType = LoginType.mLoginToken; - await TwakeDialog.showFutureLoadingDialogFullScreen( - future: () => matrix.getLoginClient().login( - LoginType.mLoginToken, - token: token, - initialDeviceDisplayName: PlatformInfos.clientName, - ), - ); + try { + final token = Uri.parse(uri).queryParameters['loginToken']; + Logs().d( + "ConnectPageMixin: handleTokenFromRegistrationSite: token: $token", + ); + if (token == null || token.isEmpty == true) { + return SsoLoginState.tokenEmpty; + } + matrix.loginType = LoginType.mLoginToken; + await TwakeDialog.showStreamDialogFullScreen( + future: () => matrix + .getLoginClient() + .login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ) + .timeout( + AutoHomeserverPickerController.autoHomeserverPickerTimeout, + onTimeout: () { + throw CheckHomeserverTimeoutException(); + }, + ), + ); + return SsoLoginState.success; + } catch (e) { + Logs() + .e('ConnectPageMixin:: handleTokenFromRegistrationSite(): error: $e'); + return SsoLoginState.error; + } } void resetLocationPathWithLoginToken({ diff --git a/lib/presentation/mixins/contacts_view_controller_mixin.dart b/lib/presentation/mixins/contacts_view_controller_mixin.dart index 6cf70d1f1a..358f3f0ebf 100644 --- a/lib/presentation/mixins/contacts_view_controller_mixin.dart +++ b/lib/presentation/mixins/contacts_view_controller_mixin.dart @@ -5,14 +5,21 @@ import 'package:fluffychat/app_state/success.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/app_state/contact/get_phonebook_contacts_state.dart'; +import 'package:fluffychat/domain/app_state/search/search_state.dart'; import 'package:fluffychat/domain/contact_manager/contacts_manager.dart'; import 'package:fluffychat/domain/model/contact/contact_type.dart'; import 'package:fluffychat/domain/model/extensions/contact/contacts_extension.dart'; +import 'package:fluffychat/domain/usecase/search/search_recent_chat_interactor.dart'; import 'package:fluffychat/presentation/enum/contacts/warning_contacts_banner_enum.dart'; import 'package:fluffychat/presentation/extensions/contact/presentation_contact_extension.dart'; -import 'package:fluffychat/presentation/model/get_presentation_contacts_success.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; -import 'package:fluffychat/presentation/model/presentation_contact_success.dart'; +import 'package:fluffychat/presentation/extensions/value_notifier_custom.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_empty.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_failure.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_success.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search_state_extension.dart'; import 'package:fluffychat/utils/permission_service.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; @@ -22,23 +29,32 @@ import 'package:permission_handler/permission_handler.dart'; mixin class ContactsViewControllerMixin { static const _debouncerIntervalInMilliseconds = 300; + static const _defaultLimitRecentContacts = 6; + final TextEditingController textEditingController = TextEditingController(); final PermissionHandlerService _permissionHandlerService = PermissionHandlerService(); + final SearchRecentChatInteractor _searchRecentChatInteractor = + getIt.get(); + ValueNotifier warningBannerNotifier = ValueNotifier(WarningContactsBannerState.hide); // FIXME: Consider can use FocusNode instead ? final ValueNotifier isSearchModeNotifier = ValueNotifier(false); - final presentationContactNotifier = ValueNotifier>( + final presentationRecentContactNotifier = + ValueNotifierCustom>([]); + + final presentationContactNotifier = + ValueNotifierCustom>( const Right(ContactsInitial()), ); final presentationPhonebookContactNotifier = - ValueNotifier>( + ValueNotifierCustom>( const Right(GetPhonebookContactsInitial()), ); @@ -53,19 +69,31 @@ mixin class ContactsViewControllerMixin { PermissionStatus contactsPermissionStatus = PermissionStatus.granted; - void initialFetchContacts() async { + void initialFetchContacts({ + required Client client, + required MatrixLocalizations matrixLocalizations, + }) async { if (PlatformInfos.isMobile && !contactsManager.isDoNotShowWarningContactsBannerAgain) { await _handleRequestContactsPermission(); } - _refreshContacts(); - _listenContactsDataChange(); + _refreshAllContacts( + client: client, + matrixLocalizations: matrixLocalizations, + ); + _listenContactsDataChange( + client: client, + matrixLocalizations: matrixLocalizations, + ); textEditingController.addListener(() { _debouncer.value = textEditingController.text; }); _debouncer.values.listen((keyword) { - _refreshContacts(); + _refreshAllContacts( + client: client, + matrixLocalizations: matrixLocalizations, + ); }); contactsManager.initialSynchronizeContacts( isAvailableSupportPhonebookContacts: PlatformInfos.isMobile && @@ -73,16 +101,34 @@ mixin class ContactsViewControllerMixin { ); } - void _listenContactsDataChange() { - contactsManager.getContactsNotifier().addListener(_refreshContacts); - contactsManager - .getPhonebookContactsNotifier() - .addListener(_refreshContacts); + void _listenContactsDataChange({ + required Client client, + required MatrixLocalizations matrixLocalizations, + }) { + contactsManager.getContactsNotifier().addListener( + () => _refreshAllContacts( + client: client, + matrixLocalizations: matrixLocalizations, + ), + ); + contactsManager.getPhonebookContactsNotifier().addListener( + () => _refreshAllContacts( + client: client, + matrixLocalizations: matrixLocalizations, + ), + ); } - void _refreshContacts() { + void _refreshAllContacts({ + required Client client, + required MatrixLocalizations matrixLocalizations, + }) { final keyword = _debouncer.value; if (keyword.isValidMatrixId && keyword.startsWith("@")) { + if (presentationContactNotifier.isDisposed && + presentationPhonebookContactNotifier.isDisposed) { + return; + } presentationContactNotifier.value = Right( PresentationExternalContactSuccess( contact: PresentationContact( @@ -94,34 +140,148 @@ mixin class ContactsViewControllerMixin { ); presentationPhonebookContactNotifier.value = const Right(GetPhonebookContactsInitial()); + _refreshRecentContacts( + client: client, + keyword: keyword.isEmpty ? null : keyword, + matrixLocalizations: matrixLocalizations, + ); return; } + _refreshContacts(keyword); + _refreshPhoneBookContacts(keyword); + _refreshRecentContacts( + client: client, + keyword: keyword.isEmpty ? null : keyword, + matrixLocalizations: matrixLocalizations, + ); + } + + Future _refreshContacts(String keyword) async { + if (presentationContactNotifier.isDisposed) return; presentationContactNotifier.value = - contactsManager.getContactsNotifier().value.map((success) { - if (success is GetContactsSuccess) { - return GetPresentationContactsSuccess( - contacts: success.contacts + contactsManager.getContactsNotifier().value.fold( + (failure) { + if (failure is GetContactsFailure) { + return Left( + GetPresentationContactsFailure( + keyword: keyword, + ), + ); + } + + if (failure is GetContactsIsEmpty) { + return Left( + GetPresentationContactsEmpty( + keyword: keyword, + ), + ); + } + return Left(failure); + }, + (success) { + if (success is GetContactsSuccess) { + final filteredContacts = success.contacts .searchContacts(keyword) .expand((contact) => contact.toPresentationContacts()) - .toList(), - keyword: keyword, - ); - } - return success; - }); + .toList(); + if (filteredContacts.isEmpty) { + return Left( + GetPresentationContactsEmpty( + keyword: keyword, + ), + ); + } else { + return Right( + GetPresentationContactsSuccess( + contacts: filteredContacts, + keyword: keyword, + ), + ); + } + } + return Right(success); + }, + ); + } + + Future _refreshPhoneBookContacts(String keyword) async { + if (presentationPhonebookContactNotifier.isDisposed) return; presentationPhonebookContactNotifier.value = - contactsManager.getPhonebookContactsNotifier().value.map((success) { - if (success is GetPhonebookContactsSuccess) { - return GetPresentationContactsSuccess( - contacts: success.contacts + contactsManager.getPhonebookContactsNotifier().value.fold( + (failure) { + if (failure is GetPhonebookContactsFailure) { + return Left( + GetPresentationContactsFailure( + keyword: keyword, + ), + ); + } + + if (failure is GetPhonebookContactsIsEmpty) { + return Left( + GetPresentationContactsEmpty( + keyword: keyword, + ), + ); + } + return Left(failure); + }, + (success) { + if (success is GetPhonebookContactsSuccess) { + final filteredContacts = success.contacts .searchContacts(keyword) .expand((contact) => contact.toPresentationContacts()) - .toList(), - keyword: keyword, - ); - } - return success; - }); + .toList(); + if (filteredContacts.isEmpty) { + return Left( + GetPresentationContactsEmpty( + keyword: keyword, + ), + ); + } else { + return Right( + GetPresentationContactsSuccess( + contacts: filteredContacts, + keyword: keyword, + ), + ); + } + } + return Right(success); + }, + ); + } + + Future _refreshRecentContacts({ + required Client client, + required MatrixLocalizations matrixLocalizations, + String? keyword, + }) async { + _searchRecentChatInteractor + .execute( + keyword: keyword ?? '', + matrixLocalizations: matrixLocalizations, + rooms: client.rooms, + ) + .listen( + (event) { + event.map((success) { + if (success is SearchRecentChatSuccess) { + final recent = success + .toPresentation() + .contacts + .where((contact) => contact.directChatMatrixID != null) + .toList(); + if (presentationRecentContactNotifier.isDisposed) return; + presentationRecentContactNotifier.value = recent + .take( + keyword == null ? _defaultLimitRecentContacts : recent.length, + ) + .toList(); + } + }); + }, + ); } void openSearchBar() { @@ -169,11 +329,21 @@ mixin class ContactsViewControllerMixin { textEditingController.clear(); searchFocusNode.dispose(); textEditingController.dispose(); + warningBannerNotifier.dispose(); + isSearchModeNotifier.dispose(); + presentationRecentContactNotifier.dispose(); presentationContactNotifier.dispose(); presentationPhonebookContactNotifier.dispose(); - contactsManager.getContactsNotifier().removeListener(_refreshContacts); - contactsManager - .getPhonebookContactsNotifier() - .removeListener(_refreshContacts); + } + + @visibleForTesting + void refreshAllContactsTest({ + required Client client, + required MatrixLocalizations matrixLocalizations, + }) { + _refreshAllContacts( + client: client, + matrixLocalizations: matrixLocalizations, + ); } } diff --git a/lib/presentation/mixins/go_to_direct_chat_mixin.dart b/lib/presentation/mixins/go_to_direct_chat_mixin.dart index 977a7f14a1..7fb22a428e 100644 --- a/lib/presentation/mixins/go_to_direct_chat_mixin.dart +++ b/lib/presentation/mixins/go_to_direct_chat_mixin.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/model/presentation_contact_constant.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact_constant.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/presentation/mixins/handle_clipboard_action_mixin.dart b/lib/presentation/mixins/handle_clipboard_action_mixin.dart index c203eb39b3..581b690d7f 100644 --- a/lib/presentation/mixins/handle_clipboard_action_mixin.dart +++ b/lib/presentation/mixins/handle_clipboard_action_mixin.dart @@ -14,15 +14,25 @@ mixin HandleClipboardActionMixin on PasteImageMixin { TextEditingController get sendController; - void registerPasteShortcutListeners() { - ClipboardEvents.instance?.registerPasteEventListener(_onPasteEvent); + void registerPasteShortcutListeners({ + VoidCallback? onSendFileCallback, + }) { + ClipboardEvents.instance?.registerPasteEventListener( + (event) => _onPasteEvent( + event, + onSendFileCallback: onSendFileCallback, + ), + ); } void unregisterPasteShortcutListeners() { ClipboardEvents.instance?.unregisterPasteEventListener(_onPasteEvent); } - void _onPasteEvent(ClipboardReadEvent event) async { + void _onPasteEvent( + ClipboardReadEvent event, { + VoidCallback? onSendFileCallback, + }) async { if (chatFocusNode.hasFocus != true) { return; } @@ -30,7 +40,12 @@ mixin HandleClipboardActionMixin on PasteImageMixin { if (await TwakeClipboard.instance .isReadableImageFormat(clipboardReader: clipboardReader) && room != null) { - await pasteImage(context, room!, clipboardReader: clipboardReader); + await pasteImage( + context, + room!, + clipboardReader: clipboardReader, + onSendFileCallback: onSendFileCallback, + ); } else { sendController.pasteText(clipboardReader: clipboardReader); } diff --git a/lib/presentation/mixins/media_picker_mixin.dart b/lib/presentation/mixins/media_picker_mixin.dart index 07243b2205..4877314899 100644 --- a/lib/presentation/mixins/media_picker_mixin.dart +++ b/lib/presentation/mixins/media_picker_mixin.dart @@ -1,8 +1,10 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:fluffychat/pages/chat/chat_actions.dart'; import 'package:fluffychat/pages/chat/input_bar/focus_suggestion_controller.dart'; import 'package:fluffychat/pages/chat/input_bar/input_bar.dart'; import 'package:fluffychat/pages/chat/item_actions_bottom_widget.dart'; import 'package:fluffychat/pages/chat/send_file_dialog/send_file_dialog_style.dart'; +import 'package:fluffychat/presentation/style/media_picker_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; @@ -87,22 +89,17 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { return await linagora_image_picker.ImagePicker.showImagesGridBottomSheet( context: context, controller: imagePickerController, - backgroundImageCamera: const AssetImage("assets/verification.png"), - initialChildSize: 0.6, + backgroundImageCamera: MediaPickerStyle.cameraIcon, + initialChildSize: MediaPickerStyle.initialChildSize, permissionStatus: permissionStatusPhotos, - gridPadding: const EdgeInsets.only(bottom: 150), + gridPadding: MediaPickerStyle.gridPadding, assetBackgroundColor: LinagoraSysColors.material().background, counterImageBuilder: (counterImage) { if (counterImage == 0) { return const SizedBox.shrink(); } return Padding( - padding: const EdgeInsets.only( - left: 8.0, - right: 8.0, - bottom: 12.0, - top: 16.0, - ), + padding: MediaPickerStyle.textSelectedCounterPadding, child: Row( children: [ Text( @@ -129,7 +126,7 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { builder: (context, value, child) { if (value == 0 && onPickerTypeTap != null) { return Container( - padding: const EdgeInsets.only(top: 8.0, bottom: 34.0), + padding: MediaPickerStyle.itemPickerPadding, decoration: BoxDecoration( color: Colors.white, border: Border( @@ -164,12 +161,7 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { alignment: Alignment.bottomRight, children: [ Container( - padding: const EdgeInsets.only( - right: 20.0, - top: 8.0, - bottom: 8.0, - left: 4.0, - ), + padding: MediaPickerStyle.composerPadding, decoration: BoxDecoration( border: Border( top: BorderSide( @@ -208,20 +200,81 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { ), ), Padding( - padding: const EdgeInsets.only(left: 8), + padding: MediaPickerStyle.sendButtonPadding, child: InkWell( borderRadius: - const BorderRadius.all(Radius.circular(100)), + MediaPickerStyle.sendButtonBorderRadius, onTap: () { if (onSendTap != null) { onSendTap(); } Navigator.of(context).pop(); }, - child: SvgPicture.asset( - ImagePaths.icSend, - width: 40, - height: 40, + child: SizedBox( + width: MediaPickerStyle.sendButtonSize, + height: MediaPickerStyle.sendButtonSize, + child: Stack( + children: [ + SvgPicture.asset( + ImagePaths.icSend, + width: MediaPickerStyle.sendIconSize, + height: MediaPickerStyle.sendIconSize, + ), + ValueListenableBuilder( + valueListenable: + numberSelectedImagesNotifier, + builder: + (context, numberSelectedImages, child) { + if (numberSelectedImages == 0 && + onPickerTypeTap != null) { + return child!; + } + return Positioned( + bottom: 0, + right: 0, + child: Container( + width: + MediaPickerStyle.counterIconSize, + height: + MediaPickerStyle.counterIconSize, + padding: + MediaPickerStyle.counterPadding, + decoration: ShapeDecoration( + color: Theme.of(context) + .colorScheme + .primary, + shape: + const CircleBorder().copyWith( + side: BorderSide( + color: Theme.of(context) + .colorScheme + .surface, + width: MediaPickerStyle + .borderSideWidth, + ), + ), + ), + alignment: Alignment.center, + child: AutoSizeText( + "$numberSelectedImages", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .surface, + ), + minFontSize: + MediaPickerStyle.minFontSize, + ), + ), + ); + }, + child: const SizedBox.shrink(), + ), + ], + ), ), ), ), @@ -236,18 +289,20 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { ), ), goToSettingsWidget: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset( ImagePaths.icPhotosSettingPermission, - width: 40, - height: 40, + width: MediaPickerStyle.photoPermissionIconSize, + height: MediaPickerStyle.photoPermissionIconSize, ), Text( L10n.of(context)!.tapToAllowAccessToYourGallery, - style: Theme.of(context) - .textTheme - .titleSmall - ?.copyWith(color: LinagoraRefColors.material().neutral), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: LinagoraRefColors.material().neutral, + fontWeight: MediaPickerStyle.photoPermissionFontWeight, + fontSize: MediaPickerStyle.photoPermissionFontSize, + ), textAlign: TextAlign.center, ), ], diff --git a/lib/presentation/mixins/paste_image_mixin.dart b/lib/presentation/mixins/paste_image_mixin.dart index 7dff5f4421..03a6e17c8f 100644 --- a/lib/presentation/mixins/paste_image_mixin.dart +++ b/lib/presentation/mixins/paste_image_mixin.dart @@ -1,5 +1,8 @@ import 'package:fluffychat/pages/chat/send_file_dialog/send_file_dialog.dart'; +import 'package:fluffychat/presentation/enum/chat/send_media_with_caption_status_enum.dart'; import 'package:fluffychat/utils/clipboard.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/utils/mime_type_uitls.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; @@ -12,6 +15,7 @@ mixin PasteImageMixin { BuildContext context, Room room, { ClipboardReader? clipboardReader, + VoidCallback? onSendFileCallback, }) async { if (!(await TwakeClipboard.instance .isReadableImageFormat(clipboardReader: clipboardReader))) { @@ -33,15 +37,15 @@ mixin PasteImageMixin { (matrixFile) => matrixFile != null, ) .map( - (matrixFile) => MatrixImageFile( + (matrixFile) => MatrixFile( name: matrixFile!.name, - mimeType: matrixFile.mimeType, + mimeType: MimeTypeUitls.instance.getTwakeMimeType(matrixFile.name), bytes: matrixFile.bytes, - ), + ).detectFileType, ) .cast() .toList(); - await showDialog( + final result = await showDialog( context: context, useRootNavigator: PlatformInfos.isWeb, builder: (context) { @@ -51,5 +55,15 @@ mixin PasteImageMixin { ); }, ); + if (result is SendMediaWithCaptionStatus) { + switch (result) { + case SendMediaWithCaptionStatus.done: + case SendMediaWithCaptionStatus.error: + onSendFileCallback?.call(); + break; + case SendMediaWithCaptionStatus.cancel: + break; + } + } } } diff --git a/lib/presentation/mixins/send_files_mixin.dart b/lib/presentation/mixins/send_files_mixin.dart index 1b7fa60c87..8c139f5907 100644 --- a/lib/presentation/mixins/send_files_mixin.dart +++ b/lib/presentation/mixins/send_files_mixin.dart @@ -34,6 +34,7 @@ mixin SendFilesMixin { BuildContext context, { Room? room, List? fileInfos, + VoidCallback? onSendFileCallback, }) async { if (room == null) {} final sendFileInteractor = getIt.get(); @@ -43,18 +44,15 @@ mixin SendFilesMixin { allowMultiple: true, ); final temporaryDirectory = await getTemporaryDirectory(); - fileInfos ??= result?.files - .map( - (xFile) => FileInfo.fromMatrixFile( - xFile.toMatrixFileOnMobile( - temporaryDirectoryPath: temporaryDirectory.path, - ), - ), - ) - .toList(); + fileInfos ??= result?.files.map((xFile) { + final matrixFile = xFile.toMatrixFileOnMobile( + temporaryDirectoryPath: temporaryDirectory.path, + ); + return FileInfo.fromMatrixFile(matrixFile); + }).toList(); if (fileInfos == null || fileInfos.isEmpty == true) return; - + onSendFileCallback?.call(); sendFileInteractor.execute(room: room!, fileInfos: fileInfos); } @@ -72,12 +70,17 @@ mixin SendFilesMixin { required BuildContext context, Room? room, required PickerType type, + VoidCallback? onSendFileCallback, }) async { switch (type) { case PickerType.gallery: break; case PickerType.documents: - sendFileAction(context, room: room); + sendFileAction( + context, + room: room, + onSendFileCallback: onSendFileCallback, + ); break; case PickerType.location: break; diff --git a/lib/presentation/mixins/send_files_with_caption_web_mixin.dart b/lib/presentation/mixins/send_files_with_caption_web_mixin.dart index 45949fa373..d657f1faed 100644 --- a/lib/presentation/mixins/send_files_with_caption_web_mixin.dart +++ b/lib/presentation/mixins/send_files_with_caption_web_mixin.dart @@ -12,6 +12,7 @@ mixin SendFilesWithCaptionWebMixin { BuildContext context, { Room? room, required List matrixFilesList, + VoidCallback? onSendFileCallback, }) async { if (matrixFilesList.length <= AppConfig.maxFilesSendPerDialog && matrixFilesList.isNotEmpty) { @@ -25,6 +26,7 @@ mixin SendFilesWithCaptionWebMixin { ); }, ); + onSendFileCallback?.call(); if (result is SendMediaWithCaptionStatus) { switch (result) { case SendMediaWithCaptionStatus.done: diff --git a/lib/presentation/mixins/single_image_picker_mixin.dart b/lib/presentation/mixins/single_image_picker_mixin.dart index fc63f04bf3..f4a08282db 100644 --- a/lib/presentation/mixins/single_image_picker_mixin.dart +++ b/lib/presentation/mixins/single_image_picker_mixin.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/presentation/style/media_picker_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -31,24 +32,27 @@ mixin SingleImagePickerMixin on CommonMediaPickerMixin { context: context, controller: imagePickerController, backgroundImageCamera: const AssetImage("assets/verification.png"), - initialChildSize: 0.6, + initialChildSize: MediaPickerStyle.initialChildSize, permissionStatus: permissionStatusPhotos, assetBackgroundColor: LinagoraSysColors.material().background, - expandedWidget: const SizedBox(height: 50), + expandedWidget: + const SizedBox(height: MediaPickerStyle.expandedWidgetHeight), counterImageBuilder: (_) => const SizedBox.shrink(), goToSettingsWidget: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset( ImagePaths.icPhotosSettingPermission, - width: 40, - height: 40, + width: MediaPickerStyle.photoPermissionIconSize, + height: MediaPickerStyle.photoPermissionIconSize, ), Text( L10n.of(context)!.tapToAllowAccessToYourGallery, - style: Theme.of(context) - .textTheme - .titleSmall - ?.copyWith(color: LinagoraRefColors.material().neutral), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: LinagoraRefColors.material().neutral, + fontWeight: MediaPickerStyle.photoPermissionFontWeight, + fontSize: MediaPickerStyle.photoPermissionFontSize, + ), textAlign: TextAlign.center, ), ], diff --git a/lib/presentation/model/client_login_state_event.dart b/lib/presentation/model/client_login_state_event.dart new file mode 100644 index 0000000000..fac8889979 --- /dev/null +++ b/lib/presentation/model/client_login_state_event.dart @@ -0,0 +1,26 @@ +import 'package:equatable/equatable.dart'; +import 'package:matrix/matrix.dart'; + +enum MultipleAccountLoginType { + firstLoggedIn, + otherAccountLoggedIn, +} + +class ClientLoginStateEvent with EquatableMixin { + final Client client; + final LoginState loginState; + final MultipleAccountLoginType multipleAccountLoginType; + + ClientLoginStateEvent({ + required this.client, + required this.loginState, + required this.multipleAccountLoginType, + }); + + @override + List get props => [ + client, + loginState, + multipleAccountLoginType, + ]; +} diff --git a/lib/presentation/model/contact/get_presentation_contacts_empty.dart b/lib/presentation/model/contact/get_presentation_contacts_empty.dart new file mode 100644 index 0000000000..572c7e15b6 --- /dev/null +++ b/lib/presentation/model/contact/get_presentation_contacts_empty.dart @@ -0,0 +1,12 @@ +import 'package:fluffychat/app_state/failure.dart'; + +class GetPresentationContactsEmpty extends Failure { + final String? keyword; + + const GetPresentationContactsEmpty({ + this.keyword, + }); + + @override + List get props => [keyword]; +} diff --git a/lib/presentation/model/contact/get_presentation_contacts_failure.dart b/lib/presentation/model/contact/get_presentation_contacts_failure.dart new file mode 100644 index 0000000000..ad6b2f6dbe --- /dev/null +++ b/lib/presentation/model/contact/get_presentation_contacts_failure.dart @@ -0,0 +1,10 @@ +import 'package:fluffychat/app_state/failure.dart'; + +class GetPresentationContactsFailure extends Failure { + final String keyword; + + const GetPresentationContactsFailure({required this.keyword}); + + @override + List get props => [keyword]; +} diff --git a/lib/presentation/model/get_presentation_contacts_success.dart b/lib/presentation/model/contact/get_presentation_contacts_success.dart similarity index 100% rename from lib/presentation/model/get_presentation_contacts_success.dart rename to lib/presentation/model/contact/get_presentation_contacts_success.dart diff --git a/lib/presentation/model/presentation_contact.dart b/lib/presentation/model/contact/presentation_contact.dart similarity index 100% rename from lib/presentation/model/presentation_contact.dart rename to lib/presentation/model/contact/presentation_contact.dart diff --git a/lib/presentation/model/presentation_contact_constant.dart b/lib/presentation/model/contact/presentation_contact_constant.dart similarity index 100% rename from lib/presentation/model/presentation_contact_constant.dart rename to lib/presentation/model/contact/presentation_contact_constant.dart diff --git a/lib/presentation/model/presentation_contact_success.dart b/lib/presentation/model/contact/presentation_contact_success.dart similarity index 68% rename from lib/presentation/model/presentation_contact_success.dart rename to lib/presentation/model/contact/presentation_contact_success.dart index e1ab6ea794..2d80f6458b 100644 --- a/lib/presentation/model/presentation_contact_success.dart +++ b/lib/presentation/model/contact/presentation_contact_success.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/app_state/success.dart'; -import 'package:fluffychat/presentation/model/get_presentation_contacts_success.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_success.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; typedef PresentationContactsSuccess = GetPresentationContactsSuccess; diff --git a/lib/presentation/model/search/presentation_search.dart b/lib/presentation/model/search/presentation_search.dart index 5fa9d1c7fe..e8be191992 100644 --- a/lib/presentation/model/search/presentation_search.dart +++ b/lib/presentation/model/search/presentation_search.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:fluffychat/domain/model/search/contact_search_model.dart'; import 'package:fluffychat/domain/model/search/recent_chat_model.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:matrix/matrix.dart'; abstract class PresentationSearch extends Equatable { @@ -31,12 +32,9 @@ class ContactPresentationSearch extends PresentationSearch { const ContactPresentationSearch({ this.matrixId, - String? email, - String? displayName, - }) : super( - displayName: displayName, - email: email, - ); + super.email, + super.displayName, + }); @override String get id => matrixId ?? email ?? ''; @@ -60,12 +58,9 @@ class RecentChatPresentationSearch extends PresentationSearch { const RecentChatPresentationSearch({ this.roomId, this.roomSummary, - String? displayName, - String? directChatMatrixID, - }) : super( - displayName: displayName, - directChatMatrixID: directChatMatrixID, - ); + super.displayName, + super.directChatMatrixID, + }); @override String get id => roomId ?? ''; @@ -104,3 +99,13 @@ extension UserExtension on User { ); } } + +extension PresentationSearchExtension on PresentationSearch { + PresentationContact toPresentationContact() { + return PresentationContact( + matrixId: directChatMatrixID, + email: email, + displayName: displayName, + ); + } +} diff --git a/lib/presentation/model/search/presentation_search_state.dart b/lib/presentation/model/search/presentation_search_state.dart index b30382d726..3ebf5ef0d3 100644 --- a/lib/presentation/model/search/presentation_search_state.dart +++ b/lib/presentation/model/search/presentation_search_state.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/model/get_presentation_contacts_success.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_success.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; typedef GetContactAndRecentChatPresentation diff --git a/lib/presentation/multiple_account/twake_chat_presentation_account.dart b/lib/presentation/multiple_account/twake_chat_presentation_account.dart index 6585764896..4da9dbf1ae 100644 --- a/lib/presentation/multiple_account/twake_chat_presentation_account.dart +++ b/lib/presentation/multiple_account/twake_chat_presentation_account.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/multiple_account/models/twake_presentation_account.dart'; import 'package:matrix/matrix.dart'; @@ -7,16 +6,11 @@ class TwakeChatPresentationAccount extends TwakePresentationAccount { const TwakeChatPresentationAccount({ required this.clientAccount, - required String accountName, - required String accountId, - required Widget avatar, - required AccountActiveStatus accountActiveStatus, - }) : super( - accountName: accountName, - accountId: accountId, - avatar: avatar, - accountActiveStatus: accountActiveStatus, - ); + required super.accountName, + required super.accountId, + required super.avatar, + required super.accountActiveStatus, + }); @override List get props => [ diff --git a/lib/presentation/same_type_events_builder/same_type_events_builder.dart b/lib/presentation/same_type_events_builder/same_type_events_builder.dart index c9610b9e19..c6d947587f 100644 --- a/lib/presentation/same_type_events_builder/same_type_events_builder.dart +++ b/lib/presentation/same_type_events_builder/same_type_events_builder.dart @@ -14,11 +14,11 @@ class SameTypeEventsBuilder extends StatelessWidget { builder; const SameTypeEventsBuilder({ - Key? key, + super.key, required this.controller, required this.builder, this.scrollController, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/presentation/style/media_picker_style.dart b/lib/presentation/style/media_picker_style.dart new file mode 100644 index 0000000000..c8d31a491f --- /dev/null +++ b/lib/presentation/style/media_picker_style.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class MediaPickerStyle { + static const AssetImage cameraIcon = AssetImage("assets/verification.png"); + + static const initialChildSize = 0.6; + + static const EdgeInsets gridPadding = EdgeInsets.only(bottom: 150); + + static const EdgeInsets textSelectedCounterPadding = EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: 12.0, + top: 16.0, + ); + + static const EdgeInsets itemPickerPadding = EdgeInsets.only( + top: 8.0, + bottom: 34.0, + ); + + static const EdgeInsets composerPadding = EdgeInsets.only( + right: 20.0, + top: 8.0, + bottom: 8.0, + left: 4.0, + ); + + static const EdgeInsets sendButtonPadding = EdgeInsets.only(left: 8); + + static const BorderRadius sendButtonBorderRadius = + BorderRadius.all(Radius.circular(100)); + + static const double sendButtonSize = 48.0; + + static const double sendIconSize = 40.0; + + static const double counterIconSize = 24.0; + + static const EdgeInsets counterPadding = EdgeInsets.all(1.0); + + static const double borderSideWidth = 1.5; + + static const double minFontSize = 8; + + static double photoPermissionIconSize = 48.0; + + static double photoPermissionFontSize = 16; + + static FontWeight photoPermissionFontWeight = FontWeight.w600; + + static const double expandedWidgetHeight = 50; +} diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index befe10a51a..605f502d4e 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -24,6 +24,7 @@ import 'dart:io'; import 'package:fcm_shared_isolate/fcm_shared_isolate.dart'; import 'package:fluffychat/domain/model/extensions/push/push_notification_extension.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; +import 'package:fluffychat/presentation/extensions/go_router_extensions.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:fluffychat/utils/push_helper.dart'; import 'package:fluffychat/widgets/twake_app.dart'; @@ -311,7 +312,7 @@ class BackgroundPush { await _flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() - ?.requestPermission(); + ?.requestNotificationsPermission(); } _pushToken = await (Platform.isIOS ? apnChannel.invokeMethod("getToken") @@ -369,8 +370,8 @@ class BackgroundPush { Future goToRoom(String? roomId) async { try { - _clearAllNavigatorAvailable(); Logs().v('[Push] Attempting to go to room $roomId...'); + _clearAllNavigatorAvailable(roomId: roomId); if (_matrixState == null || roomId == null) { return; } @@ -609,8 +610,18 @@ class BackgroundPush { ); } - void _clearAllNavigatorAvailable() { - final canPopNavigation = TwakeApp.router.canPop(); + void _clearAllNavigatorAvailable({ + String? roomId, + }) { + Logs().d( + "BackgroundPush:: - Current active room id @2 ${TwakeApp.router.activeRoomId}", + ); + if (roomId != null && + TwakeApp.router.activeRoomId?.contains(roomId) == true) { + return; + } + + final canPopNavigation = TwakeApp.router.routerDelegate.canPop(); Logs().d("BackgroundPush:: - Can pop other Navigation $canPopNavigation"); if (canPopNavigation) { TwakeApp.router.routerDelegate.pop(); diff --git a/lib/utils/dialog/twake_dialog.dart b/lib/utils/dialog/twake_dialog.dart index c87f7cbecf..d117818ecf 100644 --- a/lib/utils/dialog/twake_dialog.dart +++ b/lib/utils/dialog/twake_dialog.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:fluffychat/pages/bootstrap/init_client_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/cupertino.dart'; @@ -54,9 +57,30 @@ class TwakeDialog { ); } + static Future showStreamDialogFullScreen({ + required Future Function() future, + }) async { + final twakeContext = TwakeApp.routerKey.currentContext; + if (twakeContext == null) { + Logs().e( + 'TwakeLoadingDialog()::showStreamDialogFullScreen - Twake context is null', + ); + } + return await showDialog( + context: twakeContext!, + builder: (context) => InitClientDialog( + future: future, + ), + barrierDismissible: true, + barrierColor: Colors.transparent, + useRootNavigator: false, + ); + } + static Future showDialogFullScreen({ required Widget Function() builder, bool barrierDismissible = true, + Color? barrierColor, }) { final twakeContext = TwakeApp.routerKey.currentContext; if (twakeContext == null) { @@ -68,6 +92,7 @@ class TwakeDialog { return showDialog( context: twakeContext, builder: (context) => builder(), + barrierColor: barrierColor, barrierDismissible: barrierDismissible, useRootNavigator: false, ); diff --git a/lib/utils/extension/mime_type_extension.dart b/lib/utils/extension/mime_type_extension.dart index 430a068cbe..7f362491fe 100644 --- a/lib/utils/extension/mime_type_extension.dart +++ b/lib/utils/extension/mime_type_extension.dart @@ -7,13 +7,19 @@ import 'package:matrix/matrix.dart'; typedef TwakeMimeType = String?; extension TwakeMimeTypeExtension on TwakeMimeType { + static const String defaultUnsupportedImageMimeType = 'file/image'; + + static const String defaultUnsupportedVideoMimeType = 'file/video'; + bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(this); bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(this); - bool isImageFile() => SupportedPreviewFileTypes.imageMimeTypes.contains(this); + bool isImageFile() => + SupportedPreviewFileTypes.imageMimeTypes.contains(this) || + defaultUnsupportedImageMimeType == this; bool isDocFile({String? fileType}) => SupportedPreviewFileTypes.docMimeTypes.contains(this) || @@ -39,7 +45,10 @@ extension TwakeMimeTypeExtension on TwakeMimeType { SupportedPreviewFileTypes.zipFileTypes .contains(fileType.toLowerCase()); - bool isVideoFile() => SupportedPreviewFileTypes.videoMimeTypes.contains(this); + bool isVideoFile() { + return SupportedPreviewFileTypes.videoMimeTypes.contains(this) || + defaultUnsupportedVideoMimeType == this; + } bool isPdfFile({String? fileType}) => SupportedPreviewFileTypes.pdfMimeTypes.contains(this) || @@ -96,4 +105,11 @@ extension TwakeMimeTypeExtension on TwakeMimeType { return L10n.of(context)!.file.toUpperCase(); } } + + static const String avifMimeType = 'image/avif'; + + static const List heicMimeTypes = [ + 'image/heic', + 'image/heif', + ]; } diff --git a/lib/utils/extension/raw_key_event_extension.dart b/lib/utils/extension/raw_key_event_extension.dart deleted file mode 100644 index 37916a9ff3..0000000000 --- a/lib/utils/extension/raw_key_event_extension.dart +++ /dev/null @@ -1,24 +0,0 @@ -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; - } -} diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart deleted file mode 100644 index df0cef1965..0000000000 --- a/lib/utils/fluffy_share.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:fluffychat/utils/twake_snackbar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:share_plus/share_plus.dart'; - -import 'package:fluffychat/utils/platform_infos.dart'; - -abstract class FluffyShare { - static Future share(String text, BuildContext context) async { - if (PlatformInfos.isMobile) { - final box = context.findRenderObject() as RenderBox; - return Share.share( - text, - sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size, - ); - } - await Clipboard.setData( - ClipboardData(text: text), - ); - TwakeSnackBar.show(context, L10n.of(context)!.copiedToClipboard); - return; - } -} 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 f19c6df01c..f10f278784 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -17,16 +17,11 @@ import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { FlutterHiveCollectionsDatabase( - String name, - String path, { - HiveCipher? key, - OnStartMigrating? onStartMigrating, - }) : super( - name, - path, - key: key, - onStartMigrating: onStartMigrating, - ); + super.name, + String super.path, { + super.key, + super.onStartMigrating, + }); @override int get version => 7; diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 1930cbd93c..250a9c1b2a 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -6,7 +6,6 @@ import 'package:fluffychat/utils/stream_list_int_extension.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; import 'package:share_plus/share_plus.dart'; import 'package:fluffychat/utils/platform_infos.dart'; diff --git a/lib/utils/matrix_sdk_extensions/result_extension.dart b/lib/utils/matrix_sdk_extensions/result_extension.dart index 7f06b3a7d7..b9e3040752 100644 --- a/lib/utils/matrix_sdk_extensions/result_extension.dart +++ b/lib/utils/matrix_sdk_extensions/result_extension.dart @@ -13,4 +13,14 @@ extension ResultExtension on Result { } return Event.fromMatrixEvent(result!, room); } + + bool isDisplayableResult({BuildContext? context}) { + if (context == null) { + return false; + } + final event = getEvent(context); + return event != null && + (event.relationshipType == null || + event.relationshipType != RelationshipTypes.reply); + } } diff --git a/lib/utils/mime_type_uitls.dart b/lib/utils/mime_type_uitls.dart new file mode 100644 index 0000000000..4573a87560 --- /dev/null +++ b/lib/utils/mime_type_uitls.dart @@ -0,0 +1,32 @@ +import 'package:fluffychat/domain/model/preview_file/supported_preview_file_types.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; +import 'package:mime/mime.dart'; + +class MimeTypeUitls { + MimeTypeUitls._(); + + static MimeTypeUitls get instance => MimeTypeUitls._(); + + String getTwakeMimeType(String path) { + final mimeType = lookupMimeType(path); + if (mimeType == null) { + return 'application/octet-stream'; + } + if (mimeType.startsWith('image/')) { + if (SupportedPreviewFileTypes.crossPlatformImageMimeTypes + .contains(mimeType)) { + return mimeType; + } else { + return TwakeMimeTypeExtension.defaultUnsupportedImageMimeType; + } + } else if (mimeType.startsWith('video/')) { + if (SupportedPreviewFileTypes.videoMimeTypes.contains(mimeType)) { + return mimeType; + } else { + return TwakeMimeTypeExtension.defaultUnsupportedVideoMimeType; + } + } else { + return mimeType; + } + } +} diff --git a/lib/utils/permission_dialog.dart b/lib/utils/permission_dialog.dart index 51a3a8511f..59aa1ed099 100644 --- a/lib/utils/permission_dialog.dart +++ b/lib/utils/permission_dialog.dart @@ -14,12 +14,12 @@ class PermissionDialog extends StatefulWidget { final OnAcceptButton onAcceptButton; const PermissionDialog({ - Key? key, + super.key, required this.permission, required this.explainTextRequestPermission, this.icon, this.onAcceptButton, - }) : super(key: key); + }); @override State createState() => _PermissionDialogState(); diff --git a/lib/utils/responsive/responsive_utils.dart b/lib/utils/responsive/responsive_utils.dart index 136f6a16cb..74f63f2e4b 100644 --- a/lib/utils/responsive/responsive_utils.dart +++ b/lib/utils/responsive/responsive_utils.dart @@ -20,7 +20,16 @@ class ResponsiveUtils { static const double heightBottomNavigation = 72; static const double heightBottomNavigationBar = 56; - static const double bodyWithRightColumnRatio = 0.7; + static const double bodyWithRightColumnRatio = 0.64; + static const double groupDetailsMinWidth = 370; + + double getChatBodyRatio(BuildContext context) => + bodyRadioWidth / context.width; + + double getMinDesktopWidth(BuildContext context) => + ResponsiveUtils.groupDetailsMinWidth / + (1 - ResponsiveUtils.bodyWithRightColumnRatio) / + (1 - ResponsiveUtils().getChatBodyRatio(context)); bool isScreenWithShortestSide(BuildContext context) => context.mediaQueryShortestSide < minTabletWidth; diff --git a/lib/utils/twake_snackbar.dart b/lib/utils/twake_snackbar.dart index dae362bb2e..69c8b449bb 100644 --- a/lib/utils/twake_snackbar.dart +++ b/lib/utils/twake_snackbar.dart @@ -38,6 +38,8 @@ class TwakeSnackBar { content: Text( message, style: Theme.of(context).textTheme.bodyMedium?.copyWith( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.background, ), ), diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index ba31018129..acc1a64f0f 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -26,7 +26,8 @@ class UrlLauncher with GoToDraftChatMixin { UrlLauncher(this.context, {this.url, this.room}); - final ChromeSafariBrowser browser = ChromeSafariBrowser(); + final ChromeSafariBrowser? browser = + PlatformInfos.isMobile ? ChromeSafariBrowser() : null; void launchUrl() { if (url!.toLowerCase().startsWith(AppConfig.deepLinkPrefix) || @@ -222,13 +223,11 @@ class UrlLauncher with GoToDraftChatMixin { void openUrlInAppBrowser() async { if (url != null) { if (PlatformInfos.isMobile) { - await browser.open( - url: Uri.parse(url!), - options: ChromeSafariBrowserClassOptions( - android: AndroidChromeCustomTabsOptions( - shareState: CustomTabsShareState.SHARE_STATE_ON, - ), - ios: IOSSafariOptions(barCollapsingEnabled: true), + await browser?.open( + url: WebUri(url!), + settings: ChromeSafariBrowserSettings( + shareState: CustomTabsShareState.SHARE_STATE_ON, + barCollapsingEnabled: true, ), ); } else { diff --git a/lib/utils/warning_dialog.dart b/lib/utils/warning_dialog.dart index 2f0c1e7618..734392e956 100644 --- a/lib/utils/warning_dialog.dart +++ b/lib/utils/warning_dialog.dart @@ -8,11 +8,11 @@ class WarningDialogWidget extends StatelessWidget { final List? actions; const WarningDialogWidget({ - Key? key, + super.key, this.title, this.message, this.actions, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/adaptive_flat_button.dart b/lib/widgets/adaptive_flat_button.dart index b2d58a94f1..139a82bda4 100644 --- a/lib/widgets/adaptive_flat_button.dart +++ b/lib/widgets/adaptive_flat_button.dart @@ -6,11 +6,11 @@ class AdaptiveFlatButton extends StatelessWidget { final void Function()? onPressed; const AdaptiveFlatButton({ - Key? key, + super.key, required this.label, this.textColor, this.onPressed, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/app_bars/searchable_app_bar.dart b/lib/widgets/app_bars/searchable_app_bar.dart index 5f64d4e6cb..97fb7d3404 100644 --- a/lib/widgets/app_bars/searchable_app_bar.dart +++ b/lib/widgets/app_bars/searchable_app_bar.dart @@ -132,7 +132,7 @@ class SearchableAppBar extends StatelessWidget { ] else ...[ if (displayBackButton) TwakeIconButton( - onTap: () => context.pop(), + onTap: () => Navigator.of(context).pop(), tooltip: L10n.of(context)!.close, icon: Icons.close, paddingAll: SearchableAppBarStyle.closeButtonPaddingAll, @@ -195,6 +195,8 @@ class SearchableAppBar extends StatelessWidget { prefixIcon: !isFullScreen ? Icon( Icons.search_outlined, + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.onBackground, ) : null, diff --git a/lib/widgets/avatar/avatar.dart b/lib/widgets/avatar/avatar.dart index ce22db6d0a..07e4ac0505 100644 --- a/lib/widgets/avatar/avatar.dart +++ b/lib/widgets/avatar/avatar.dart @@ -27,8 +27,8 @@ class Avatar extends StatelessWidget { this.boxShadows, this.textStyle, this.textColor, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -45,6 +45,7 @@ class Avatar extends StatelessWidget { height: size, cacheWidth: (size * MediaQuery.devicePixelRatioOf(context)).round(), cacheKey: mxContent.toString(), + animated: true, placeholder: (context) => _fallbackAvatar(), ), ), diff --git a/lib/widgets/avatar/avatar_with_bottom_icon_widget.dart b/lib/widgets/avatar/avatar_with_bottom_icon_widget.dart index cf9d41b338..bb8f6a80f7 100644 --- a/lib/widgets/avatar/avatar_with_bottom_icon_widget.dart +++ b/lib/widgets/avatar/avatar_with_bottom_icon_widget.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/model/presentation_contact.dart'; +import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 24edaff9f5..941db14228 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -21,8 +21,7 @@ class ChatSettingsPopupMenu extends StatefulWidget { final Room room; final bool displayChatDetails; - const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {Key? key}) - : super(key: key); + const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {super.key}); @override ChatSettingsPopupMenuState createState() => ChatSettingsPopupMenuState(); diff --git a/lib/widgets/clean_rich_text.dart b/lib/widgets/clean_rich_text.dart index c6e8e89072..a538155b40 100644 --- a/lib/widgets/clean_rich_text.dart +++ b/lib/widgets/clean_rich_text.dart @@ -12,7 +12,7 @@ class TwakeCleanRichText extends StatelessWidget { final TextSpanBuilder? textSpanBuilder; const TwakeCleanRichText({ - Key? key, + super.key, required this.text, required this.childWidget, this.textStyle, @@ -21,7 +21,7 @@ class TwakeCleanRichText extends StatelessWidget { this.onLinkTap, this.maxLines, this.textSpanBuilder, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/connection_status_header.dart b/lib/widgets/connection_status_header.dart index 37f65155c7..d0b7181c5b 100644 --- a/lib/widgets/connection_status_header.dart +++ b/lib/widgets/connection_status_header.dart @@ -10,7 +10,7 @@ import '../utils/localized_exception_extension.dart'; import 'matrix.dart'; class ConnectionStatusHeader extends StatefulWidget { - const ConnectionStatusHeader({Key? key}) : super(key: key); + const ConnectionStatusHeader({super.key}); @override ConnectionStatusHeaderState createState() => ConnectionStatusHeaderState(); diff --git a/lib/widgets/context_menu/context_menu_action.dart b/lib/widgets/context_menu/context_menu_action.dart new file mode 100644 index 0000000000..7d02be5594 --- /dev/null +++ b/lib/widgets/context_menu/context_menu_action.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +class ContextMenuAction extends Equatable { + final String name; + final IconData? icon; + final String? imagePath; + final Color? colorIcon; + final double? iconSize; + final TextStyle? styleName; + final EdgeInsets? padding; + + const ContextMenuAction({ + required this.name, + this.icon, + this.imagePath, + this.colorIcon, + this.iconSize, + this.styleName, + this.padding, + }); + + @override + List get props => [ + name, + icon, + imagePath, + colorIcon, + iconSize, + styleName, + padding, + ]; +} diff --git a/lib/widgets/context_menu/context_menu_action_item_widget.dart b/lib/widgets/context_menu/context_menu_action_item_widget.dart new file mode 100644 index 0000000000..de1285492d --- /dev/null +++ b/lib/widgets/context_menu/context_menu_action_item_widget.dart @@ -0,0 +1,91 @@ +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; +import 'package:fluffychat/widgets/mixins/twake_context_menu_style.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ContextMenuActionItemWidget extends StatelessWidget { + final ContextMenuAction action; + final void Function()? closeMenuAction; + + const ContextMenuActionItemWidget({ + super.key, + required this.action, + this.closeMenuAction, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + closeMenuAction?.call(); + }, + child: _itemBuilder( + context, + action.name, + iconAction: action.icon, + imagePath: action.imagePath, + colorIcon: action.colorIcon, + iconSize: action.iconSize, + styleName: action.styleName, + padding: action.padding, + ), + ); + } + + Widget _itemBuilder( + BuildContext context, + String nameAction, { + IconData? iconAction, + String? imagePath, + Color? colorIcon, + double? iconSize, + TextStyle? styleName, + EdgeInsets? padding, + }) { + Widget buildIcon() { + // We try to get the SVG first and then the IconData + if (imagePath != null) { + return SvgPicture.asset( + imagePath, + width: iconSize ?? TwakeContextMenuStyle.defaultItemIconSize, + height: iconSize ?? TwakeContextMenuStyle.defaultItemIconSize, + fit: BoxFit.fill, + colorFilter: ColorFilter.mode( + colorIcon ?? TwakeContextMenuStyle.defaultItemColorIcon(context)!, + BlendMode.srcIn, + ), + ); + } + + if (iconAction != null) { + return Icon( + iconAction, + size: iconSize ?? TwakeContextMenuStyle.defaultItemIconSize, + color: + colorIcon ?? TwakeContextMenuStyle.defaultItemColorIcon(context), + ); + } + + return const SizedBox.shrink(); + } + + return Padding( + padding: padding ?? TwakeContextMenuStyle.defaultItemPadding, + child: SizedBox( + child: Row( + children: [ + buildIcon(), + const SizedBox(width: TwakeContextMenuStyle.defaultItemElementsGap), + Expanded( + child: Text( + nameAction, + style: styleName ?? + TwakeContextMenuStyle.defaultItemTextStyle(context), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/context_menu/twake_context_menu.dart b/lib/widgets/context_menu/twake_context_menu.dart index 84a4d7eb85..7a7c612983 100644 --- a/lib/widgets/context_menu/twake_context_menu.dart +++ b/lib/widgets/context_menu/twake_context_menu.dart @@ -1,5 +1,7 @@ // reference to: https://pub.dev/packages/contextmenu import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action_item_widget.dart'; import 'package:fluffychat/widgets/context_menu/context_menu_position.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_style.dart'; import 'package:flutter/material.dart'; @@ -15,21 +17,25 @@ const double _kMinTileHeight = 24; /// If you just want to use a normal [TwakeContextMenu], please use [TwakeContextMenuArea]. class TwakeContextMenu extends StatefulWidget { + /// The [BuildContext] of the dialog/modal that will display the [TwakeContextMenu]. This is used to close the dialog/modal when the [TwakeContextMenu] is closed. + final BuildContext dialogContext; + + /// The list of items to be displayed in the [TwakeContextMenu]. This is used to build the UI of items + final List listActions; + /// The [Offset] from coordinate origin the [TwakeContextMenu] will be displayed at. final Offset position; - /// The builder for the items to be displayed. [ListTile] is very useful in most cases. - final ContextMenuBuilder builder; - /// The padding value at the top an bottom between the edge of the [TwakeContextMenu] and the first / last item final double? verticalPadding; const TwakeContextMenu({ - Key? key, + super.key, + required this.dialogContext, + required this.listActions, required this.position, - required this.builder, this.verticalPadding, - }) : super(key: key); + }); @override TwakeContextMenuState createState() => TwakeContextMenuState(); @@ -66,8 +72,7 @@ class TwakeContextMenuState extends State @override Widget build(BuildContext context) { - final children = widget.builder(context); - final contextMenuPosition = _calculatePosition(children); + final contextMenuPosition = _calculatePosition(widget.listActions); return GestureDetector( onTap: () => closeContextMenu(), @@ -125,21 +130,21 @@ class TwakeContextMenuState extends State ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: children + children: widget.listActions .map( - (e) => Listener( - onPointerDown: (_) => - PlatformInfos.isMobile - ? closeContextMenu() - : null, - child: _GrowingWidget( - child: e, - onHeightChange: (height) { - setState(() { - _heights[ValueKey(e)] = height; - }); - }, - ), + (action) => _GrowingWidget( + child: action, + closeMenuAction: () { + closeContextMenu( + popResult: widget.listActions + .indexOf(action), + ); + }, + onHeightChange: (height) { + setState(() { + _heights[ValueKey(action)] = height; + }); + }, ), ) .toList(), @@ -159,13 +164,13 @@ class TwakeContextMenuState extends State ); } - void closeContextMenu() { - _animationController - .reverse() - .whenComplete(() => Navigator.of(context).pop()); + void closeContextMenu({dynamic popResult}) { + _animationController.reverse().whenComplete(() { + Navigator.of(widget.dialogContext).pop(popResult); + }); } - ContextMenuPosition _calculatePosition(List children) { + ContextMenuPosition _calculatePosition(List children) { double height = 2 * (widget.verticalPadding ?? TwakeContextMenuStyle.defaultVerticalPadding); @@ -215,14 +220,15 @@ class TwakeContextMenuState extends State } class _GrowingWidget extends StatefulWidget { - final Widget child; + final ContextMenuAction child; final ValueChanged onHeightChange; + final void Function()? closeMenuAction; const _GrowingWidget({ - Key? key, required this.child, required this.onHeightChange, - }) : super(key: key); + this.closeMenuAction, + }); @override __GrowingWidgetState createState() => __GrowingWidgetState(); @@ -235,7 +241,10 @@ class __GrowingWidgetState extends State<_GrowingWidget> with AfterLayoutMixin { Widget build(BuildContext context) { return Container( key: _key, - child: widget.child, + child: ContextMenuActionItemWidget( + action: widget.child, + closeMenuAction: widget.closeMenuAction, + ), ); } diff --git a/lib/widgets/context_menu/twake_context_menu_area.dart b/lib/widgets/context_menu/twake_context_menu_area.dart index b9fa3505d0..dd8a3d32f3 100644 --- a/lib/widgets/context_menu/twake_context_menu_area.dart +++ b/lib/widgets/context_menu/twake_context_menu_area.dart @@ -1,4 +1,5 @@ // reference to: https://pub.dev/packages/contextmenu +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_style.dart'; import 'package:flutter/material.dart'; @@ -13,6 +14,9 @@ typedef ContextMenuBuilder = List Function(BuildContext context); /// with the corresponding location [Offset]. class TwakeContextMenuArea extends StatelessWidget with TwakeContextMenuMixin { + /// The list of items to be displayed in the [TwakeContextMenu]. This is used to build the UI of items + final List listActions; + /// The widget displayed inside the [TwakeContextMenuArea] final Widget child; @@ -25,11 +29,12 @@ class TwakeContextMenuArea extends StatelessWidget with TwakeContextMenuMixin { final double? verticalPadding; const TwakeContextMenuArea({ - Key? key, + super.key, + required this.listActions, required this.child, this.builder, this.verticalPadding, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -40,7 +45,7 @@ class TwakeContextMenuArea extends StatelessWidget with TwakeContextMenuMixin { onSecondaryTapDown: (details) => showTwakeContextMenu( offset: details.globalPosition, context: context, - builder: builder!, + listActions: listActions, verticalPadding: verticalPadding ?? TwakeContextMenuStyle.defaultVerticalPadding, ), diff --git a/lib/widgets/file_widget/file_tile_widget_style.dart b/lib/widgets/file_widget/file_tile_widget_style.dart index 961683ea44..f6dd83c08f 100644 --- a/lib/widgets/file_widget/file_tile_widget_style.dart +++ b/lib/widgets/file_widget/file_tile_widget_style.dart @@ -26,6 +26,8 @@ class FileTileWidgetStyle { TextStyle highlightTextStyle(BuildContext context) { return TextStyle( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.onBackground, fontWeight: FontWeight.bold, backgroundColor: CssColor.fromCss('gold'), diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold.dart index b165dbb9d3..b3de7e43aa 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_route_style.dart'; @@ -26,6 +25,8 @@ class AppAdaptiveScaffold extends StatelessWidget { this.displayAppBar = true, }) : super(key: key ?? scaffoldWithNestedNavigationKey); + static final _responsiveUtils = ResponsiveUtils(); + @override Widget build(BuildContext context) { return Portal( @@ -60,7 +61,7 @@ class AppAdaptiveScaffold extends StatelessWidget { ), }, ), - bodyRatio: ResponsiveUtils.bodyRadioWidth / context.width, + bodyRatio: _responsiveUtils.getChatBodyRatio(context), secondaryBody: SlotLayout( config: { const WidthPlatformBreakpoint( diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index fb2cb4e065..2c06f52fb2 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart'; import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/switch_active_account_body_args.dart'; import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -114,16 +115,13 @@ class AppAdaptiveScaffoldBodyController extends State } void _handleLogout(AppAdaptiveScaffoldBody oldWidget) { - Logs().d( - 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():oldWidget - ${oldWidget.args}', - ); - Logs().d( - 'AppAdaptiveScaffoldBodyController::_onLogoutMultipleAccountSuccess():newWidget - ${widget.args}', - ); - if (oldWidget.args != widget.args && widget.args is LogoutBodyArgs) { - activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; - pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); - } + activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; + pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); + } + + void _handleSwitchAccount(AppAdaptiveScaffoldBody oldWidget) { + activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; + pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); } MatrixState get matrix => Matrix.of(context); @@ -138,7 +136,20 @@ class AppAdaptiveScaffoldBodyController extends State @override void didUpdateWidget(covariant AppAdaptiveScaffoldBody oldWidget) { activeRoomIdNotifier.value = widget.activeRoomId; - _handleLogout(oldWidget); + Logs().d( + 'AppAdaptiveScaffoldBodyController::didUpdateWidget():oldWidget - ${oldWidget.args}', + ); + Logs().d( + 'AppAdaptiveScaffoldBodyController::didUpdateWidget():newWidget - ${widget.args}', + ); + if (oldWidget.args != widget.args && widget.args is LogoutBodyArgs) { + _handleLogout(oldWidget); + } + + if (oldWidget.args != widget.args && + widget.args is SwitchActiveAccountBodyArgs) { + _handleSwitchAccount(oldWidget); + } super.didUpdateWidget(oldWidget); } diff --git a/lib/widgets/layouts/loading_view.dart b/lib/widgets/layouts/loading_view.dart index 8a40b0d4cc..1c75e0f730 100644 --- a/lib/widgets/layouts/loading_view.dart +++ b/lib/widgets/layouts/loading_view.dart @@ -7,7 +7,7 @@ import 'package:fluffychat/utils/update_checker_no_store.dart'; import 'package:fluffychat/widgets/matrix.dart'; class LoadingView extends StatelessWidget { - const LoadingView({Key? key}) : super(key: key); + const LoadingView({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index 37d3b35bd6..601765240f 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -7,10 +7,10 @@ class LoginScaffold extends StatelessWidget { final AppBar? appBar; const LoginScaffold({ - Key? key, + super.key, required this.body, this.appBar, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -37,6 +37,8 @@ class LoginScaffold extends StatelessWidget { if (isMobileMode) return scaffold; return Container( decoration: BoxDecoration( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.onBackground, ), child: Center( diff --git a/lib/widgets/layouts/max_width_body.dart b/lib/widgets/layouts/max_width_body.dart index d813ce2b16..a31393b163 100644 --- a/lib/widgets/layouts/max_width_body.dart +++ b/lib/widgets/layouts/max_width_body.dart @@ -11,8 +11,8 @@ class MaxWidthBody extends StatelessWidget { this.child, this.maxWidth = 600, this.withScrolling = false, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { return SafeArea( diff --git a/lib/widgets/link_browser_widget.dart b/lib/widgets/link_browser_widget.dart index b42a165c5b..06a44d00ad 100644 --- a/lib/widgets/link_browser_widget.dart +++ b/lib/widgets/link_browser_widget.dart @@ -5,8 +5,7 @@ class LinkBrowserWidget extends StatelessWidget { final Uri uri; final Widget child; - const LinkBrowserWidget({Key? key, required this.uri, required this.child}) - : super(key: key); + const LinkBrowserWidget({super.key, required this.uri, required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/lock_screen.dart b/lib/widgets/lock_screen.dart index 04bdb669d8..ff6f36faa7 100644 --- a/lib/widgets/lock_screen.dart +++ b/lib/widgets/lock_screen.dart @@ -12,7 +12,7 @@ import 'package:fluffychat/config/themes.dart'; import 'layouts/login_scaffold.dart'; class LockScreen extends StatefulWidget { - const LockScreen({Key? key}) : super(key: key); + const LockScreen({super.key}); @override LockScreenState createState() => LockScreenState(); @@ -41,6 +41,8 @@ class LockScreenState extends State { ), body: Container( decoration: BoxDecoration( + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use color: Theme.of(context).colorScheme.background, gradient: LinearGradient( begin: Alignment.topRight, @@ -55,6 +57,8 @@ class LockScreenState extends State { Theme.of(context).secondaryHeaderColor.withAlpha(16), Theme.of(context).primaryColor.withAlpha(16), Theme.of(context).colorScheme.secondary.withAlpha(16), + // TODO: change to colorSurface when its approved + // ignore: deprecated_member_use Theme.of(context).colorScheme.background.withAlpha(16), ], ), diff --git a/lib/widgets/log_view.dart b/lib/widgets/log_view.dart index 94d9b72d03..ed639b645f 100644 --- a/lib/widgets/log_view.dart +++ b/lib/widgets/log_view.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; class LogViewer extends StatefulWidget { - const LogViewer({Key? key}) : super(key: key); + const LogViewer({super.key}); @override LogViewerState createState() => LogViewerState(); diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 887bcee2cf..1386b849df 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'dart:io'; +import 'package:fluffychat/domain/contact_manager/contacts_manager.dart'; import 'package:fluffychat/presentation/mixins/init_config_mixin.dart'; +import 'package:fluffychat/presentation/model/client_login_state_event.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; import 'package:universal_html/html.dart' as html hide File; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -25,9 +28,6 @@ import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; -import 'package:fluffychat/widgets/layouts/agruments/logged_in_body_args.dart'; -import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; -import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; import 'package:fluffychat/widgets/set_active_client_state.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/foundation.dart'; @@ -62,8 +62,8 @@ class Matrix extends StatefulWidget { this.child, required this.clients, this.queryParameters, - Key? key, - }) : super(key: key); + super.key, + }); @override MatrixState createState() => MatrixState(); @@ -77,16 +77,20 @@ class MatrixState extends State with WidgetsBindingObserver, ReceiveSharingIntentMixin, InitConfigMixin { final tomConfigurationRepository = getIt.get(); + final _contactsManager = getIt.get(); + int _activeClient = -1; String? activeBundle; Store store = Store(); HomeserverSummary? loginHomeserverSummary; - String? authUrl; + String? _authUrl; XFile? loginAvatar; String? loginUsername; LoginType? loginType; bool? loginRegistrationSupported; + bool waitForFirstSync = false; + bool get twakeSupported { final tomServerUrlInterceptor = getIt.get( instanceName: NetworkDI.tomServerUrlInterceptorName, @@ -96,11 +100,16 @@ class MatrixState extends State BackgroundPush? backgroundPush; + bool get isValidActiveClient => + _activeClient >= 0 && _activeClient < widget.clients.length; + + String? get authUrl => _authUrl; + Client get client { if (widget.clients.isEmpty) { widget.clients.add(getLoginClient()); } - if (_activeClient < 0 || _activeClient >= widget.clients.length) { + if (!isValidActiveClient) { return currentBundle!.first!; } return widget.clients[_activeClient]; @@ -120,12 +129,14 @@ class MatrixState extends State RequestTokenResponse? currentThreepidCreds; Future setActiveClient(Client? newClient) async { - final index = widget.clients.indexWhere((client) => client == newClient); + final index = widget.clients.indexWhere( + (client) => newClient != null && client.userID == newClient.userID, + ); if (index != -1) { _activeClient = index; // TODO: Multi-client VoiP support createVoipPlugin(); - _setUpToMServicesWhenChangingActiveClient(newClient); + await _setUpToMServicesWhenChangingActiveClient(newClient); await _storePersistActiveAccount(newClient!); return SetActiveClientState.success; } else { @@ -189,7 +200,7 @@ class MatrixState extends State .stream .where((l) => l == LoginState.loggedIn) .first - .then((_) => _handleAddAnotherAccount()); + .then((state) => _handleAddAnotherAccount(state)); return candidate; } @@ -247,6 +258,8 @@ class MatrixState extends State final onNotification = {}; final onLoginStateChanged = >{}; final onUiaRequest = >{}; + final StreamController onClientLoginStateChanged = + StreamController.broadcast(); StreamSubscription? onFocusSub; StreamSubscription? onBlurSub; @@ -373,15 +386,10 @@ class MatrixState extends State } else { if (state == LoginState.loggedIn) { Logs().v('[MATRIX]:_listenLoginStateChanged:: First Log in successful'); - _handleFirstLoggedIn(client); + _handleFirstLoggedIn(client, state); } else { - Logs().v('[MATRIX]:_listenLoginStateChanged:: Log out successful'); - if (PlatformInfos.isMobile) { - _deletePersistActiveAccount(state); - TwakeApp.router.go('/home/twakeWelcome'); - } else { - TwakeApp.router.go('/home', extra: true); - } + Logs().v('[MATRIX]:_listenLoginStateChanged:: Last Log out successful'); + await _handleLastLogout(); } } } @@ -392,7 +400,8 @@ class MatrixState extends State ) async { await _cancelSubs(currentClient.clientName); widget.clients.remove(currentClient); - ClientManager.removeClientNameFromStore(currentClient.clientName); + await ClientManager.removeClientNameFromStore(currentClient.clientName); + matrixState.reSyncContacts(); TwakeSnackBar.show( TwakeApp.routerKey.currentContext!, L10n.of(context)!.oneClientLoggedOut, @@ -411,18 +420,24 @@ class MatrixState extends State } } - void _handleFirstLoggedIn(Client newActiveClient) { - setUpToMServicesInLogin(newActiveClient); - _storePersistActiveAccount(newActiveClient); - TwakeApp.router.go( - '/rooms', - extra: LoggedInBodyArgs( - newActiveClient: newActiveClient, + Future _handleFirstLoggedIn( + Client newActiveClient, + LoginState loginState, + ) async { + waitForFirstSync = false; + await setUpToMServicesInLogin(newActiveClient); + await _storePersistActiveAccount(newActiveClient); + matrixState.reSyncContacts(); + onClientLoginStateChanged.add( + ClientLoginStateEvent( + client: client, + loginState: loginState, + multipleAccountLoginType: MultipleAccountLoginType.firstLoggedIn, ), ); } - Future _handleAddAnotherAccount() async { + Future _handleAddAnotherAccount(LoginState loginState) async { Logs().d( 'MatrixState::_handleAddAnotherAccount() - Add another account successful', ); @@ -439,20 +454,24 @@ class MatrixState extends State _loginClientCandidate!.clientName, ); if (activeClient == null) return; - setUpToMServicesInLogin(activeClient); + waitForFirstSync = false; + await setUpToMServicesInLogin(activeClient); final result = await setActiveClient(activeClient); + matrixState.reSyncContacts(); if (result.isSuccess) { - TwakeApp.router.go( - '/rooms', - extra: LoggedInOtherAccountBodyArgs( - newActiveClient: activeClient, + onClientLoginStateChanged.add( + ClientLoginStateEvent( + client: client, + loginState: loginState, + multipleAccountLoginType: + MultipleAccountLoginType.otherAccountLoggedIn, ), ); _loginClientCandidate = null; } } - void _deletePersistActiveAccount(LoginState state) async { + Future _deletePersistActiveAccount() async { try { final multipleAccountRepository = getIt.get(); await multipleAccountRepository.deletePersistActiveAccount(); @@ -561,12 +580,15 @@ class MatrixState extends State if (client.userID == null) return; try { final toMConfigurations = await getTomConfigurations(client.userID!); - if (toMConfigurations == null) return; + if (toMConfigurations == null) { + _setupAuthUrl(); + return; + } setUpToMServices( toMConfigurations.tomServerInformation, toMConfigurations.identityServerInformation, ); - authUrl = toMConfigurations.authUrl; + _setupAuthUrl(url: toMConfigurations.authUrl); loginType = toMConfigurations.loginType; } catch (e) { Logs().e('MatrixState::_retrieveToMConfiguration: $e'); @@ -577,7 +599,9 @@ class MatrixState extends State ToMServerInformation tomServer, IdentityServerInformation? identityServer, ) { - Logs().d('MatrixState::setUpToMServices: $tomServer, $identityServer'); + Logs().d( + 'MatrixState::setUpToMServices: $tomServer, ${identityServer?.baseUrl}', + ); _setUpToMServer(tomServer); if (identityServer != null) { _setUpIdentityServer(identityServer); @@ -588,7 +612,7 @@ class MatrixState extends State } } - void setUpToMServicesInLogin(Client client) { + Future setUpToMServicesInLogin(Client client) async { final tomServer = loginHomeserverSummary?.tomServer; Logs().d('MatrixState::setUpToMServicesInLogin: $tomServer'); if (tomServer != null) { @@ -598,9 +622,7 @@ class MatrixState extends State loginHomeserverSummary?.discoveryInformation?.mIdentityServer; final homeServer = loginHomeserverSummary?.discoveryInformation?.mHomeserver; - final newAuthUrl = loginHomeserverSummary?.discoveryInformation - ?.additionalProperties["m.authentication"]?["issuer"]; - authUrl = newAuthUrl is String ? newAuthUrl : null; + _setupAuthUrl(); if (identityServer != null) { _setUpIdentityServer(identityServer); } @@ -608,7 +630,7 @@ class MatrixState extends State _setUpHomeServer(homeServer.baseUrl); } if (tomServer != null) { - _storeToMConfiguration( + await _storeToMConfiguration( client, ToMConfigurations( tomServerInformation: tomServer, @@ -623,6 +645,9 @@ class MatrixState extends State void setUpAuthorization(Client client) { final authorizationInterceptor = getIt.get(); + Logs().d( + 'MatrixState::setUpAuthorization: accessToken ${client.accessToken}', + ); authorizationInterceptor.accessToken = client.accessToken; } @@ -657,10 +682,10 @@ class MatrixState extends State .changeBaseUrl(identityServer.baseUrl.toString()); } - void _storeToMConfiguration( + Future _storeToMConfiguration( Client client, ToMConfigurations config, - ) { + ) async { try { Logs().e( 'Matrix::_storeToMConfiguration: clientName - ${client.clientName}', @@ -671,7 +696,7 @@ class MatrixState extends State if (client.userID == null) return; final ToMConfigurationsRepository configurationRepository = getIt.get(); - configurationRepository.saveTomConfigurations( + await configurationRepository.saveTomConfigurations( client.userID!, config, ); @@ -683,7 +708,7 @@ class MatrixState extends State } } - void _setUpToMServicesWhenChangingActiveClient(Client? client) async { + Future _setUpToMServicesWhenChangingActiveClient(Client? client) async { Logs().d( 'Matrix::_checkHomeserverExists: Old twakeSupported - $twakeSupported', ); @@ -695,11 +720,19 @@ class MatrixState extends State ); if (toMConfigurations == null) { _setUpToMServer(null); + _setupAuthUrl(); + setUpAuthorization(client); } else { - _setUpToMServer(toMConfigurations.tomServerInformation); + _setupAuthUrl(url: toMConfigurations.authUrl); + setUpToMServices( + toMConfigurations.tomServerInformation, + toMConfigurations.identityServerInformation, + ); } } catch (e) { _setUpToMServer(null); + _setupAuthUrl(); + setUpAuthorization(client!); Logs().e('Matrix::_checkHomeserverExists: error - $e'); } Logs().d( @@ -713,12 +746,12 @@ class MatrixState extends State final persistActiveAccount = await multipleAccountRepository.getPersistActiveAccount(); if (persistActiveAccount == null) { - _storePersistActiveAccount(client); + await _storePersistActiveAccount(client); return; } else { final newActiveClient = getClientByUserId(persistActiveAccount); if (newActiveClient != null) { - setActiveClient(newActiveClient); + await setActiveClient(newActiveClient); } } } catch (e) { @@ -731,10 +764,10 @@ class MatrixState extends State Future _storePersistActiveAccount(Client newClient) async { if (newClient.userID == null) return; try { - Logs().e( + Logs().d( 'Matrix::_storePersistActiveAccount: clientName - ${newClient.clientName}', ); - Logs().e( + Logs().d( 'Matrix::_storePersistActiveAccount: userId - ${newClient.userID}', ); final MultipleAccountRepository multipleAccountRepository = @@ -757,6 +790,47 @@ class MatrixState extends State didChangeAppLifecycleState(AppLifecycleState.paused); } + Future _setupAuthUrl({ + String? url, + }) async { + if (url != null) { + Logs().e( + 'Matrix::_setupAuthUrl: newAuthUrl - $url', + ); + _authUrl = url; + } else { + final newAuthUrl = loginHomeserverSummary?.discoveryInformation + ?.additionalProperties["m.authentication"]?["issuer"]; + Logs().e( + 'Matrix::_setupAuthUrl: newAuthUrl - $newAuthUrl', + ); + _authUrl = newAuthUrl is String ? newAuthUrl : url; + } + } + + Future _deleteAllTomConfigurations() async { + final hiveCollectionToMDatabase = getIt.get(); + await hiveCollectionToMDatabase.clear(); + Logs().d( + 'MatrixState::_deleteAllTomConfigurations: Delete ToM database success', + ); + } + + Future _handleLastLogout() async { + matrixState.reSyncContacts(); + if (PlatformInfos.isMobile) { + await _deletePersistActiveAccount(); + TwakeApp.router.go('/home/twakeWelcome'); + } else { + TwakeApp.router.go('/home', extra: true); + } + await _deleteAllTomConfigurations(); + } + + Future reSyncContacts() async { + _contactsManager.reSyncContacts(); + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { Logs().i('didChangeAppLifecycleState: AppLifecycleState = $state'); @@ -829,6 +903,7 @@ class MatrixState extends State onKeyVerificationRequestSub.values.map((s) => s.cancel()); onLoginStateChanged.values.map((s) => s.cancel()); onNotification.values.map((s) => s.cancel()); + onClientLoginStateChanged.close(); client.httpClient.close(); onFocusSub?.cancel(); onBlurSub?.cancel(); @@ -853,16 +928,11 @@ class MatrixState extends State class FixedThreepidCreds extends ThreepidCreds { FixedThreepidCreds({ - required String sid, - required String clientSecret, - String? idServer, - String? idAccessToken, - }) : super( - sid: sid, - clientSecret: clientSecret, - idServer: idServer, - idAccessToken: idAccessToken, - ); + required super.sid, + required super.clientSecret, + super.idServer, + super.idAccessToken, + }); @override Map toJson() { diff --git a/lib/widgets/mentioned_user.dart b/lib/widgets/mentioned_user.dart index dd532049e2..6605382895 100644 --- a/lib/widgets/mentioned_user.dart +++ b/lib/widgets/mentioned_user.dart @@ -9,13 +9,13 @@ class MentionedUser extends StatelessWidget { final int? maxLines; const MentionedUser({ - Key? key, + super.key, required this.displayName, required this.url, this.textStyle, this.onTap, this.maxLines, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/mixins/drag_drog_file_mixin.dart b/lib/widgets/mixins/drag_drog_file_mixin.dart index 04c59341fb..79a96b3915 100644 --- a/lib/widgets/mixins/drag_drog_file_mixin.dart +++ b/lib/widgets/mixins/drag_drog_file_mixin.dart @@ -2,6 +2,7 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:fluffychat/pages/chat/send_file_dialog/send_file_dialog.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/utils/mime_type_uitls.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -30,6 +31,8 @@ mixin DragDrogFileMixin { MatrixFile( bytes: bytesList.result![i], name: details.files[i].name, + mimeType: + MimeTypeUitls.instance.getTwakeMimeType(details.files[i].name), ).detectFileType, ); } diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index 6af3307dfc..d1ab88c892 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -14,7 +14,6 @@ import 'package:fluffychat/utils/permission_service.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/lib/widgets/mixins/twake_context_menu_mixin.dart b/lib/widgets/mixins/twake_context_menu_mixin.dart index 62d94918e9..9333efb17d 100644 --- a/lib/widgets/mixins/twake_context_menu_mixin.dart +++ b/lib/widgets/mixins/twake_context_menu_mixin.dart @@ -1,27 +1,31 @@ -import 'package:fluffychat/pages/chat/events/message/message_content_with_timestamp_builder.dart'; +import 'package:fluffychat/widgets/context_menu/context_menu_action.dart'; import 'package:fluffychat/widgets/context_menu/twake_context_menu.dart'; import 'package:flutter/material.dart'; /// Show a [TwakeContextMenu] on the given [BuildContext]. For other parameters, see [TwakeContextMenu]. mixin TwakeContextMenuMixin { - void showTwakeContextMenu({ + Future showTwakeContextMenu({ + required List listActions, required Offset offset, required BuildContext context, - required ContextMenuBuilder builder, double? verticalPadding, VoidCallback? onClose, }) async { + dynamic result; await showDialog( context: context, barrierColor: Colors.transparent, - barrierDismissible: true, - builder: (context) => TwakeContextMenu( + barrierDismissible: false, + builder: (dialogContext) => TwakeContextMenu( + dialogContext: dialogContext, + listActions: listActions, position: offset, - builder: builder, verticalPadding: verticalPadding, ), ).then((value) { + result = value; onClose?.call(); }); + return result; } } diff --git a/lib/widgets/mixins/twake_context_menu_style.dart b/lib/widgets/mixins/twake_context_menu_style.dart index 6433ee2ba9..19d1691c62 100644 --- a/lib/widgets/mixins/twake_context_menu_style.dart +++ b/lib/widgets/mixins/twake_context_menu_style.dart @@ -5,9 +5,22 @@ class TwakeContextMenuStyle { return Theme.of(context).colorScheme.surface; } + static Color? defaultItemColorIcon(BuildContext context) { + return Theme.of(context).colorScheme.onSurfaceVariant; + } + static const double defaultVerticalPadding = 8.0; static const double menuElevation = 2.0; static const double menuBorderRadius = 4.0; static const double menuMinWidth = 196.0; static const double menuMaxWidth = 306.0; + static const double defaultItemIconSize = 24.0; + static const EdgeInsets defaultItemPadding = EdgeInsets.all(12.0); + static const double defaultItemElementsGap = 12.0; + static TextStyle? defaultItemTextStyle(BuildContext context) { + return Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + overflow: TextOverflow.ellipsis, + ); + } } diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index d52d329cbc..b805845e4f 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -3,11 +3,14 @@ import 'dart:typed_data'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/presentation/enum/chat/media_viewer_popup_result_enum.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; +import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/download_file_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/hero_page_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_avif/flutter_avif.dart'; import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; @@ -72,8 +75,8 @@ class MxcImage extends StatefulWidget { this.closeRightColumn, this.cacheWidth, this.cacheHeight, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _MxcImageState(); @@ -304,6 +307,7 @@ class _MxcImageState extends State { : BorderRadius.zero, child: _ImageWidget( filePath: filePath, + event: widget.event, data: data, width: widget.width, height: widget.height, @@ -326,6 +330,7 @@ class _ImageWidget extends StatelessWidget { final String? filePath; final Uint8List? data; final double? width; + final Event? event; final double? height; final bool needResize; final BoxFit? fit; @@ -337,6 +342,7 @@ class _ImageWidget extends StatelessWidget { this.filePath, this.data, this.width, + this.event, this.height, required this.needResize, this.fit, @@ -348,43 +354,97 @@ class _ImageWidget extends StatelessWidget { @override Widget build(BuildContext context) { return filePath != null && filePath!.isNotEmpty - ? Image.file( - File(filePath!), + ? _ImageNativeBuilder( + filePath: filePath, width: width, height: height, - cacheWidth: cacheWidth != null - ? cacheWidth! - : (width != null && needResize) - ? context.getCacheSize(width!) - : null, - cacheHeight: cacheHeight != null - ? cacheHeight! - : (height != null && needResize) - ? context.getCacheSize(height!) - : null, + cacheWidth: cacheWidth, + needResize: needResize, + cacheHeight: cacheHeight, fit: fit, - filterQuality: FilterQuality.medium, - errorBuilder: imageErrorWidgetBuilder, + event: event, + imageErrorWidgetBuilder: imageErrorWidgetBuilder, ) : data != null - ? Image.memory( - data!, - width: width, - height: height, - cacheWidth: cacheWidth != null - ? cacheWidth! - : (width != null && needResize) - ? context.getCacheSize(width!) - : null, - cacheHeight: cacheHeight != null - ? cacheHeight! - : (height != null && needResize) - ? context.getCacheSize(height!) - : null, - fit: fit, - filterQuality: FilterQuality.medium, - errorBuilder: imageErrorWidgetBuilder, - ) + ? event?.mimeType == TwakeMimeTypeExtension.avifMimeType + ? AvifImage.memory( + data!, + height: height, + width: width, + fit: BoxFit.cover, + ) + : Image.memory( + data!, + width: width, + height: height, + cacheWidth: cacheWidth != null + ? cacheWidth! + : (width != null && needResize) + ? context.getCacheSize(width!) + : null, + cacheHeight: cacheHeight != null + ? cacheHeight! + : (height != null && needResize) + ? context.getCacheSize(height!) + : null, + fit: fit, + filterQuality: FilterQuality.medium, + errorBuilder: imageErrorWidgetBuilder, + ) : const SizedBox.shrink(); } } + +class _ImageNativeBuilder extends StatelessWidget { + const _ImageNativeBuilder({ + this.filePath, + this.width, + this.height, + this.cacheWidth, + required this.needResize, + this.cacheHeight, + this.fit, + required this.imageErrorWidgetBuilder, + this.event, + }); + + final String? filePath; + final Event? event; + final double? width; + final double? height; + final int? cacheWidth; + final bool needResize; + final int? cacheHeight; + final BoxFit? fit; + final ImageErrorWidgetBuilder imageErrorWidgetBuilder; + + @override + Widget build(BuildContext context) { + if (event?.mimeType == TwakeMimeTypeExtension.avifMimeType) { + return AvifImage.file( + File(filePath!), + height: height, + width: width, + fit: BoxFit.cover, + ); + } + return Image.file( + File(filePath!), + width: width, + height: height, + cacheWidth: cacheWidth != null + ? cacheWidth! + : (width != null && needResize) + ? context.getCacheSize(width!) + : null, + cacheHeight: cacheHeight != null + ? cacheHeight! + : (height != null && needResize) + ? context.getCacheSize(height!) + : null, + fit: fit, + filterQuality: FilterQuality.medium, + errorBuilder: imageErrorWidgetBuilder, + ); + } +} diff --git a/lib/widgets/profile_bottom_sheet.dart b/lib/widgets/profile_bottom_sheet.dart index 15f8a8b5e0..81f93f52ce 100644 --- a/lib/widgets/profile_bottom_sheet.dart +++ b/lib/widgets/profile_bottom_sheet.dart @@ -16,8 +16,8 @@ class ProfileBottomSheet extends StatelessWidget { const ProfileBottomSheet({ required this.userId, required this.outerContext, - Key? key, - }) : super(key: key); + super.key, + }); void _startDirectChat(BuildContext context) async { final client = Matrix.of(context).client; diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index e668e221c6..5983855bdd 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -23,8 +23,8 @@ class PublicRoomBottomSheet extends StatelessWidget { required this.outerContext, this.chunk, this.onRoomJoined, - Key? key, - }) : super(key: key) { + super.key, + }) { assert(roomAlias != null || chunk != null); } diff --git a/lib/widgets/settings_switch_list_tile.dart b/lib/widgets/settings_switch_list_tile.dart index a3da2dbb68..268e5f187f 100644 --- a/lib/widgets/settings_switch_list_tile.dart +++ b/lib/widgets/settings_switch_list_tile.dart @@ -9,12 +9,12 @@ class SettingsSwitchListTile extends StatefulWidget { final Function(bool)? onChanged; const SettingsSwitchListTile.adaptive({ - Key? key, + super.key, this.defaultValue = false, required this.storeKey, required this.title, this.onChanged, - }) : super(key: key); + }); @override SettingsSwitchListTileState createState() => SettingsSwitchListTileState(); diff --git a/lib/widgets/sliver_expandable_list.dart b/lib/widgets/sliver_expandable_list.dart index 27c2286273..5823923261 100644 --- a/lib/widgets/sliver_expandable_list.dart +++ b/lib/widgets/sliver_expandable_list.dart @@ -8,11 +8,11 @@ class SliverExpandableList extends StatefulWidget { final Widget Function(BuildContext, int) itemBuilder; const SliverExpandableList({ - Key? key, + super.key, required this.title, required this.itemCount, required this.itemBuilder, - }) : super(key: key); + }); @override State createState() => _SliverExpandableListState(); diff --git a/lib/widgets/theme_builder.dart b/lib/widgets/theme_builder.dart index b1ca4aa42f..b35a0b4495 100644 --- a/lib/widgets/theme_builder.dart +++ b/lib/widgets/theme_builder.dart @@ -19,8 +19,8 @@ class ThemeBuilder extends StatefulWidget { required this.builder, this.themeModeSettingsKey = 'theme_mode', this.primaryColorSettingsKey = 'primary_color', - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => ThemeController(); diff --git a/lib/widgets/twake_app.dart b/lib/widgets/twake_app.dart index baa7c281b3..f998ad1de1 100644 --- a/lib/widgets/twake_app.dart +++ b/lib/widgets/twake_app.dart @@ -22,10 +22,10 @@ class TwakeApp extends StatefulWidget { static GlobalKey routerKey = GlobalKey(); const TwakeApp({ - Key? key, + super.key, this.testWidget, required this.clients, - }) : super(key: key); + }); /// getInitialLink may rereturn the value multiple times if this view is /// opened multiple times for example if the user logs out after they logged diff --git a/lib/widgets/twake_components/twake_avatar.dart b/lib/widgets/twake_components/twake_avatar.dart index b165f5ff74..3765efb1d3 100644 --- a/lib/widgets/twake_components/twake_avatar.dart +++ b/lib/widgets/twake_components/twake_avatar.dart @@ -20,8 +20,8 @@ class TwakeAvatar extends StatelessWidget { this.onTap, this.client, this.fontSize = 18, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index 0ceac1313b..96968d7f63 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -1,5 +1,5 @@ -import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; +import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -10,17 +10,65 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; -class TwakeHeader extends StatelessWidget - with ShowDialogMixin - implements PreferredSizeWidget { - final ChatListController controller; +class TwakeHeader extends StatefulWidget implements PreferredSizeWidget { final VoidCallback onClearSelection; + final ValueNotifier selectModeNotifier; + final ValueNotifier> + conversationSelectionNotifier; + final VoidCallback onClickAvatar; + final Client client; const TwakeHeader({ - Key? key, - required this.controller, + super.key, required this.onClearSelection, - }) : super(key: key); + required this.client, + required this.selectModeNotifier, + required this.conversationSelectionNotifier, + required this.onClickAvatar, + }); + + @override + State createState() => _TwakeHeaderState(); + + @override + Size get preferredSize => + const Size.fromHeight(TwakeHeaderStyle.toolbarHeight); +} + +class _TwakeHeaderState extends State with ShowDialogMixin { + final ValueNotifier currentProfileNotifier = ValueNotifier( + Profile(userId: ''), + ); + + void getCurrentProfile(Client client) async { + currentProfileNotifier.value = Profile(userId: ''); + final profile = await client.getProfileFromUserId( + widget.client.userID!, + getFromRooms: false, + ); + Logs().d( + 'ChatList::_getCurrentProfile() - currentProfile1: $profile', + ); + currentProfileNotifier.value = profile; + } + + @override + void didUpdateWidget(covariant TwakeHeader oldWidget) { + super.didUpdateWidget(oldWidget); + if (Matrix.of(context).isValidActiveClient && + widget.client != oldWidget.client) { + getCurrentProfile(widget.client); + } + if (currentProfileNotifier.value.userId.isEmpty) { + getCurrentProfile(widget.client); + } + } + + @override + void dispose() { + super.dispose(); + currentProfileNotifier.dispose(); + } @override Widget build(BuildContext context) { @@ -30,7 +78,7 @@ class TwakeHeader extends StatelessWidget automaticallyImplyLeading: false, leadingWidth: TwakeHeaderStyle.leadingWidth, title: ValueListenableBuilder( - valueListenable: controller.selectModeNotifier, + valueListenable: widget.selectModeNotifier, builder: (context, selectMode, _) { return Align( alignment: TwakeHeaderStyle.alignment, @@ -55,7 +103,7 @@ class TwakeHeader extends StatelessWidget children: [ InkWell( onTap: selectMode == SelectMode.select - ? onClearSelection + ? widget.onClearSelection : null, borderRadius: BorderRadius.circular( TwakeHeaderStyle.closeIconSize, @@ -72,7 +120,7 @@ class TwakeHeader extends StatelessWidget ), ValueListenableBuilder( valueListenable: - controller.conversationSelectionNotifier, + widget.conversationSelectionNotifier, builder: (context, conversationSelection, _) { return Padding( padding: @@ -109,18 +157,14 @@ class TwakeHeader extends StatelessWidget hoverColor: Colors.transparent, splashColor: Colors.transparent, highlightColor: Colors.transparent, - onTap: controller.onClickAvatar, + onTap: widget.onClickAvatar, child: ValueListenableBuilder( - valueListenable: - controller.currentProfileNotifier, + valueListenable: currentProfileNotifier, builder: (context, profile, _) { return Avatar( mxContent: profile.avatarUrl, name: profile.displayName ?? - Matrix.of(context) - .client - .userID! - .localpart, + profile.userId.localpart, size: TwakeHeaderStyle.avatarSize, fontSize: TwakeHeaderStyle.avatarFontSizeInAppBar, @@ -140,8 +184,4 @@ class TwakeHeader extends StatelessWidget centerTitle: true, ); } - - @override - Size get preferredSize => - const Size.fromHeight(TwakeHeaderStyle.toolbarHeight); } diff --git a/lib/widgets/twake_components/twake_header_style.dart b/lib/widgets/twake_components/twake_header_style.dart index 922c98bff9..caf0da79f6 100644 --- a/lib/widgets/twake_components/twake_header_style.dart +++ b/lib/widgets/twake_components/twake_header_style.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; class TwakeHeaderStyle { static ResponsiveUtils responsive = getIt.get(); @@ -16,8 +16,6 @@ class TwakeHeaderStyle { static double get avatarFontSizeInAppBar => 14.0; static const double avatarOfMultipleAccountSize = 48.0; - static const double logoAppOfMultipleHeight = 28.0; - static const double logoAppOfMultipleWidth = 152.0; static bool isDesktop(BuildContext context) => responsive.isDesktop(context); @@ -46,4 +44,9 @@ class TwakeHeaderStyle { static const EdgeInsetsDirectional counterSelectionPadding = EdgeInsetsDirectional.only(start: 4); + + static TextStyle? selectAccountTextStyle(BuildContext context) => + Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ); } diff --git a/lib/widgets/twake_components/twake_icon_button.dart b/lib/widgets/twake_components/twake_icon_button.dart index 5184d53eed..09cca8ca67 100644 --- a/lib/widgets/twake_components/twake_icon_button.dart +++ b/lib/widgets/twake_components/twake_icon_button.dart @@ -42,7 +42,7 @@ class TwakeIconButton extends StatelessWidget { final Color? splashColor; const TwakeIconButton({ - Key? key, + super.key, this.tooltip, this.onTap, this.icon, @@ -60,7 +60,7 @@ class TwakeIconButton extends StatelessWidget { this.iconColor, this.highlightColor, this.splashColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { 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 031cd0383b..7a7b9fb194 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 @@ -7,11 +7,11 @@ class TwakeLinkView extends StatelessWidget { final String? firstValidUrl; const TwakeLinkView({ - Key? key, + super.key, required this.body, required this.previewItemWidget, this.firstValidUrl, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/twake_components/twake_preview_placeholder.dart b/lib/widgets/twake_components/twake_preview_placeholder.dart index fdff4eb530..60adcdb41e 100644 --- a/lib/widgets/twake_components/twake_preview_placeholder.dart +++ b/lib/widgets/twake_components/twake_preview_placeholder.dart @@ -3,11 +3,11 @@ import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class TwakePreviewPlaceHolder extends StatelessWidget { const TwakePreviewPlaceHolder({ - Key? key, + super.key, required this.width, required this.height, this.borderRadius = 12, - }) : super(key: key); + }); final double width; final double height; diff --git a/lib/widgets/twake_components/twake_smart_refresher.dart b/lib/widgets/twake_components/twake_smart_refresher.dart index 0636af1a50..46cf6c82d6 100644 --- a/lib/widgets/twake_components/twake_smart_refresher.dart +++ b/lib/widgets/twake_components/twake_smart_refresher.dart @@ -10,12 +10,12 @@ class TwakeSmartRefresher extends StatefulWidget { final List slivers; const TwakeSmartRefresher({ - Key? key, + super.key, this.onRefresh, this.onLoading, required this.controller, required this.slivers, - }) : super(key: key); + }); @override State createState() => _TwakeSmartRefresherController(); diff --git a/lib/widgets/twake_components/twake_text_button.dart b/lib/widgets/twake_components/twake_text_button.dart index 6328f4b80d..70eeec604d 100644 --- a/lib/widgets/twake_components/twake_text_button.dart +++ b/lib/widgets/twake_components/twake_text_button.dart @@ -30,7 +30,7 @@ class TwakeTextButton extends StatelessWidget { final double? borderHover; const TwakeTextButton({ - Key? key, + super.key, required this.message, this.styleMessage, this.onTap, @@ -44,7 +44,7 @@ class TwakeTextButton extends StatelessWidget { this.margin = const EdgeInsetsDirectional.all(0), this.buttonDecoration, this.borderHover, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/unread_rooms_badge.dart b/lib/widgets/unread_rooms_badge.dart index 0a8a49a63d..4c691b9db8 100644 --- a/lib/widgets/unread_rooms_badge.dart +++ b/lib/widgets/unread_rooms_badge.dart @@ -10,9 +10,9 @@ class UnreadRoomsBadge extends StatelessWidget { final bool Function(Room) filter; const UnreadRoomsBadge({ - Key? key, + super.key, required this.filter, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 799a8af7b3..090402fe16 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_avif_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAvifLinuxPlugin"); + flutter_avif_linux_plugin_register_with_registrar(flutter_avif_linux_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index af3187e68b..e59cbc86d0 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST emoji_picker_flutter file_saver file_selector_linux + flutter_avif_linux flutter_secure_storage_linux flutter_webrtc handy_window diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0a989c2082..5a9d26a988 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -17,7 +17,9 @@ import file_saver import file_selector_macos import firebase_core import flutter_app_badger +import flutter_avif_macos import flutter_image_compress_macos +import flutter_inappwebview_macos import flutter_local_notifications import flutter_secure_storage_macos import flutter_web_auth_2 @@ -34,8 +36,8 @@ import path_provider_foundation import photo_manager import record_macos import screen_brightness_macos -import share_plus_macos -import shared_preferences_macos +import share_plus +import shared_preferences_foundation import sqflite import super_native_extensions import url_launcher_macos @@ -58,7 +60,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) + FlutterAvifPlugin.register(with: registry.registrar(forPlugin: "FlutterAvifPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 34d9e2f1c9..1e88dd260d 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -41,6 +41,9 @@ PODS: - FlutterMacOS - flutter_image_compress_macos (1.0.0): - FlutterMacOS + - flutter_inappwebview_macos (0.0.1): + - FlutterMacOS + - OrderedSet (~> 5.0) - flutter_local_notifications (0.0.1): - FlutterMacOS - flutter_secure_storage_macos (6.1.1): @@ -49,7 +52,7 @@ PODS: - FlutterMacOS - flutter_webrtc (0.9.36): - FlutterMacOS - - WebRTC-SDK (= 114.5735.08) + - WebRTC-SDK (= 114.5735.10) - FlutterMacOS (1.0.0) - gal (1.0.0): - Flutter @@ -58,15 +61,15 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.0): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - "GoogleUtilities/NSData+zlib (7.13.0)": + - "GoogleUtilities/NSData+zlib (7.13.3)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.0) + - GoogleUtilities/Privacy (7.13.3) - irondash_engine_context (0.0.1): - FlutterMacOS - just_audio (0.0.1): @@ -86,6 +89,7 @@ PODS: - nanopb/encode (= 2.30909.1) - nanopb/decode (2.30909.1) - nanopb/encode (2.30909.1) + - OrderedSet (5.0.0) - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -95,14 +99,15 @@ PODS: - Flutter - FlutterMacOS - PromisesObjC (2.4.0) - - ReachabilitySwift (5.2.1) + - ReachabilitySwift (5.2.3) - record_macos (0.2.0): - FlutterMacOS - screen_brightness_macos (0.1.0): - FlutterMacOS - - share_plus_macos (0.0.1): + - share_plus (0.0.1): - FlutterMacOS - - shared_preferences_macos (0.0.1): + - shared_preferences_foundation (0.0.1): + - Flutter - FlutterMacOS - sqflite (0.0.3): - Flutter @@ -120,7 +125,7 @@ PODS: - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS - - WebRTC-SDK (114.5735.08) + - WebRTC-SDK (114.5735.10) - window_to_front (0.0.1): - FlutterMacOS @@ -138,6 +143,7 @@ DEPENDENCIES: - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`) - flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`) + - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - flutter_web_auth_2 (from `Flutter/ephemeral/.symlinks/plugins/flutter_web_auth_2/macos`) @@ -156,8 +162,8 @@ DEPENDENCIES: - photo_manager (from `Flutter/ephemeral/.symlinks/plugins/photo_manager/macos`) - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -176,6 +182,7 @@ SPEC REPOS: - GoogleDataTransport - GoogleUtilities - nanopb + - OrderedSet - PromisesObjC - ReachabilitySwift - WebRTC-SDK @@ -207,6 +214,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos flutter_image_compress_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos + flutter_inappwebview_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_secure_storage_macos: @@ -243,10 +252,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos screen_brightness_macos: :path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos - share_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos - shared_preferences_macos: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin super_native_extensions: @@ -270,11 +279,11 @@ SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_lifecycle: a600c10e12fe033c7be9078f2e929b8241f2c1e3 - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f emoji_picker_flutter: 533634326b1c5de9a181ba14b9758e6dfe967a20 file_saver: 44e6fbf666677faf097302460e214e977fdd977b - file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 Firebase: 5ae8b7cf8efce559a653aef0ad95bab3f427c351 firebase_core: 970bc7db019f0985976324d90cdc370527c31461 FirebaseCore: 2082fffcd855f95f883c0a1641133eb9bbe76d40 @@ -282,14 +291,15 @@ SPEC CHECKSUMS: FirebaseCoreInternal: bca76517fe1ed381e989f5e7d8abb0da8d85bed3 flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730 flutter_image_compress_macos: c26c3c13ea0f28ae6dea4e139b3292e7729f99f1 + flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 - flutter_secure_storage_macos: 75c8cadfdba05ca007c0fa4ea0c16e5cf85e521b + flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 flutter_web_auth_2: 2e1dc2d2139973e4723c5286ce247dd590390d70 - flutter_webrtc: cf7dc44d26cbb5c5f1ae5f583dab545871f287f9 + flutter_webrtc: 823284e171ecb2487b7210c214886a949c122a59 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 macos_ui: 6229a8922cd97bafb7d9636c8eb8dfb0744183ca @@ -298,25 +308,26 @@ SPEC CHECKSUMS: media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 + ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 record_macos: 937889e0f2a7a12b6fc14e97a3678e5a18943de6 screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda - share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 - shared_preferences_macos: 8b221d457159a85f478c0b9d2f19aeae9feff475 + share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 video_compress: aebf9865ccccab1f4038be7aa72af525ef2c10d7 - video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 - WebRTC-SDK: c24d2a6c9f571f2ed42297cb8ffba9557093142b + WebRTC-SDK: 8c0edd05b880a39648118192c252667ea06dea51 window_to_front: 4cdc24ddd8461ad1a55fa06286d6a79d8b29e8d8 PODFILE CHECKSUM: 9b8d08a513b178c33212d1b54cc9e3cba756d95b -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 24ba24f44e..9f7368bd71 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -265,6 +265,7 @@ "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", "${BUILT_PRODUCTS_DIR}/appkit_ui_element_colors/appkit_ui_element_colors.framework", @@ -279,6 +280,7 @@ "${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", "${BUILT_PRODUCTS_DIR}/flutter_image_compress_macos/flutter_image_compress_macos.framework", + "${BUILT_PRODUCTS_DIR}/flutter_inappwebview_macos/flutter_inappwebview_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_macos/flutter_secure_storage_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_web_auth_2/flutter_web_auth_2.framework", @@ -297,8 +299,8 @@ "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", "${BUILT_PRODUCTS_DIR}/record_macos/record_macos.framework", "${BUILT_PRODUCTS_DIR}/screen_brightness_macos/screen_brightness_macos.framework", - "${BUILT_PRODUCTS_DIR}/share_plus_macos/share_plus_macos.framework", - "${BUILT_PRODUCTS_DIR}/shared_preferences_macos/shared_preferences_macos.framework", + "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/super_native_extensions/super_native_extensions.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework", @@ -334,6 +336,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/appkit_ui_element_colors.framework", @@ -348,6 +351,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_auth_2.framework", @@ -366,8 +370,8 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/record_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/screen_brightness_macos.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus_macos.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/super_native_extensions.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework", diff --git a/pubspec.lock b/pubspec.lock index dcc623e3b3..e86706e1ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" adaptive_dialog: dependency: "direct main" description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" animations: dependency: "direct main" description: @@ -61,18 +61,18 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: "direct main" description: @@ -85,12 +85,12 @@ packages: dependency: transitive description: name: audio_session - sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" + sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e url: "https://pub.dev" source: hosted - version: "0.1.18" + version: "0.1.19" auto_size_text: - dependency: transitive + dependency: "direct main" description: name: auto_size_text sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" build_config: dependency: transitive description: @@ -141,10 +149,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -157,10 +165,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.10" build_runner_core: dependency: transitive description: @@ -181,10 +189,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" cached_network_image: dependency: "direct main" description: @@ -205,10 +213,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" callkeep: dependency: "direct main" description: @@ -221,26 +229,26 @@ packages: dependency: transitive description: name: camera - sha256: "9499cbc2e51d8eb0beadc158b288380037618ce4e30c9acbc4fae1ac3ecb5797" + sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 url: "https://pub.dev" source: hosted - version: "0.10.5+9" + version: "0.10.6" camera_android: dependency: transitive description: name: camera_android - sha256: "15a6543878a41c141807ffab496f66b7fef6da0f23372f5513fc6349e60f437e" + sha256: b350ac087f111467e705b2b76cc1322f7f5bdc122aa83b4b243b0872f390d229 url: "https://pub.dev" source: hosted - version: "0.10.8+17" + version: "0.10.9+2" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "608b56b0880722f703871329c4d7d4c2f379c8e2936940851df7fc041abc6f51" + sha256: "7d021e8cd30d9b71b8b92b4ad669e80af432d722d18d6aac338572754a786c15" url: "https://pub.dev" source: hosted - version: "0.9.13+10" + version: "0.9.16" camera_platform_interface: dependency: transitive description: @@ -253,10 +261,10 @@ packages: dependency: transitive description: name: camera_web - sha256: f18ccfb33b2a7c49a52ad5aa3f07330b7422faaecbdfd9b9fe8e51182f6ad67d + sha256: "9e9aba2fbab77ce2472924196ff8ac4dd8f9126c4f9a3096171cd1d870d6b26c" url: "https://pub.dev" source: hosted - version: "0.3.2+4" + version: "0.3.3" canonical_json: dependency: transitive description: @@ -293,10 +301,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" + sha256: e53da939709efb9aad0f3d72a69a8d05f889168b7a138af60ce78bab5c94b135 url: "https://pub.dev" source: hosted - version: "1.7.5" + version: "1.8.1" cli_util: dependency: transitive description: @@ -374,10 +382,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+1" crypto: dependency: "direct main" description: @@ -398,26 +406,26 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dart_webrtc: dependency: transitive description: name: dart_webrtc - sha256: "5cbc40bd9b33d0c9b8004cff52e9883c71f0f54799afc8faca77535eeb9ef857" + sha256: fe4db21dc389b99e04cb7bf43bc927dba2e42768d4c28211b66a4b5a16e4d516 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.4.5" dartz: dependency: "direct main" description: @@ -442,6 +450,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + dependency_validator: + dependency: transitive + description: + name: dependency_validator + sha256: f727a5627aa405965fab4aef4f468e50a9b632ba0737fd2f98c932fec6d712b9 + url: "https://pub.dev" + source: hosted + version: "3.2.3" desktop_drop: dependency: "direct main" description: @@ -470,10 +486,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "10.1.0" device_info_plus_platform_interface: dependency: transitive description: @@ -486,10 +502,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "50fec96118958b97c727d0d8f67255d3683f16cc1f90d9bc917b5d4fe3abeca9" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.2" + version: "5.4.3+1" dio_cache_interceptor: dependency: "direct main" description: @@ -562,6 +578,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + exif: + dependency: transitive + description: + name: exif + sha256: a7980fdb3b7ffcd0b035e5b8a5e1eef7cadfe90ea6a4e85ebb62f87b96c7a172 + url: "https://pub.dev" + source: hosted + version: "3.3.0" external_path: dependency: "direct main" description: @@ -598,18 +622,18 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: name: file_picker - sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "8.0.3" file_saver: dependency: "direct main" description: @@ -630,18 +654,18 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "1cd66575f063b689e041aec836905ba7be18d76c9f0634d0d75daec825f67095" + sha256: "57265ec9591e8fd8508f613544cde6f7d045731f6b09644057e49a4c9c672b7c" url: "https://pub.dev" source: hosted - version: "0.5.0+7" + version: "0.5.1+1" file_selector_ios: dependency: transitive description: name: file_selector_ios - sha256: b015154e6d9fddbc4d08916794df170b44531798c8dd709a026df162d07ad81d + sha256: "2bacc6d8725b88f4124c2a780fbf939e96eb1f804362867be4f617dcccec959c" url: "https://pub.dev" source: hosted - version: "0.5.1+8" + version: "0.5.2+1" file_selector_linux: dependency: "direct overridden" description: @@ -654,10 +678,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.4" file_selector_platform_interface: dependency: transitive description: @@ -670,10 +694,10 @@ packages: dependency: transitive description: name: file_selector_web - sha256: c0f025d460de3301b7bbbf837fc8d0759df85f182c635f1dd94934b4cdc92352 + sha256: "619e431b224711a3869e30dbd7d516f5f5a4f04b265013a50912f39e1abc88c8" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.4+1" file_selector_windows: dependency: transitive description: @@ -723,10 +747,10 @@ packages: dependency: "direct main" description: name: flutter_adaptive_scaffold - sha256: "4257142551ec97761d44f4258b8ad53ac76593dd0992197b876769df19f8a018" + sha256: "9a1d5e9f728815e27b7b612883db19107ba8a35a46a97c757ea00896cb027451" url: "https://pub.dev" source: hosted - version: "0.1.8" + version: "0.1.10+2" flutter_app_badger: dependency: "direct main" description: @@ -743,6 +767,70 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_avif: + dependency: "direct main" + description: + name: flutter_avif + sha256: e42b9c114890180b225ec74307698c099881bc6fe0edd5eb2e577d2e559a5c1c + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_avif_android: + dependency: transitive + description: + name: flutter_avif_android + sha256: "6ffa49d90739a443fff169c4913dee6db4262360a027d7de402de18dd25040ff" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_avif_ios: + dependency: transitive + description: + name: flutter_avif_ios + sha256: "79e456038230bfbbcdad8047db27adc3c8541645ab5e0f1a38c4819bcdebc9cf" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_avif_linux: + dependency: transitive + description: + name: flutter_avif_linux + sha256: a038d7542851f225227c9dcf6cd1c70b30df9b6b98e9f14b438b71d5d29a72fe + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_avif_macos: + dependency: transitive + description: + name: flutter_avif_macos + sha256: e27444f3a00875cf336547317329b701a9c5190ab1d232bebc2a21155822be0e + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_avif_platform_interface: + dependency: transitive + description: + name: flutter_avif_platform_interface + sha256: "8d12f0efa7aad88c825cafe16bb63bcccd89ac297dfff2f69f9a3c5f87928457" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_avif_web: + dependency: transitive + description: + name: flutter_avif_web + sha256: b56ff53c6fd1e2030f62909c44e560700127fd92b222d1a8d73c7ec392d88cc6 + url: "https://pub.dev" + source: hosted + version: "2.4.1" + flutter_avif_windows: + dependency: transitive + description: + name: flutter_avif_windows + sha256: "43a8da46865bd12e10fcdc1b2aa1e87687092dcc6b9091861b4e6e84c6132d08" + url: "https://pub.dev" + source: hosted + version: "2.4.1" flutter_blurhash: dependency: "direct main" description: @@ -792,10 +880,10 @@ packages: dependency: "direct main" description: name: flutter_image_compress - sha256: "4edadb0ca2f957b85190e9c3aa728569b91b64b6e06e0eec5b622d47a8692ab2" + sha256: "45a3071868092a61b11044c70422b04d39d4d9f2ef536f3c5b11fb65a1e7dd90" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" flutter_image_compress_common: dependency: transitive description: @@ -816,10 +904,10 @@ packages: dependency: transitive description: name: flutter_image_compress_ohos - sha256: "70360371698be994786e5dd2e364a6525b1c5a4f843bff8af9b8a2fbe808d8d8" + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.3" flutter_image_compress_platform_interface: dependency: transitive description: @@ -840,10 +928,58 @@ packages: dependency: "direct main" description: name: flutter_inappwebview - sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "1.0.13" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_keyboard_visibility: dependency: "direct main" description: @@ -904,34 +1040,34 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.2" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 + sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" url: "https://pub.dev" source: hosted - version: "12.0.4" + version: "17.1.2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "3c6d6db334f609a92be0c0915f40871ec56f5d2adf01e77ae364162c587c0ca8" + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.1.0" flutter_localizations: dependency: "direct main" description: flutter @@ -949,10 +1085,10 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "59dfd14267b691bea55760786b47d3172d47cdcc0d79ff930746a5ad123491b8" + sha256: "52c65a977daae42f9aae6748418dd1535eaf27186e9bac9bf431843082bc75a3" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" flutter_math_fork: dependency: transitive description: @@ -974,10 +1110,10 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.0" flutter_olm: dependency: "direct main" description: @@ -990,18 +1126,18 @@ packages: dependency: "direct main" description: name: flutter_openssl_crypto - sha256: b64a0825d79f10b6d5f5951f7ce2d5ddc12ed532129fc5a7e0ce472f5b97d78e + sha256: "6dcecf6f7c1804ae6f5d73ee05df8af72ea8133bf2447d25979d739503186c96" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.3.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.19" flutter_portal: dependency: "direct main" description: @@ -1018,46 +1154,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "2d31289a022f8b0a97e952d553686c50dff2ed5b58ac03628a13bc8cdf5f8ece" + url: "https://pub.dev" + source: hosted + version: "1.82.3" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: f2afec1f1762c040a349ea2a588e32f442da5d0db3494a52a929a97c9e550bc5 + sha256: "8496a89eea74e23f92581885f876455d9d460e71201405dffe5f55dfe1155864" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "9.2.1" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: ff0768a6700ea1d9620e03518e2e25eac86a8bd07ca3556e9617bfa5ace4bd00 + sha256: b768a7dab26d6186b68e2831b3104f8968154f0f4fdbf66e7c2dd7bdf299daaf url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.1.1" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" flutter_secure_storage_windows: dependency: "direct overridden" description: @@ -1100,10 +1244,10 @@ packages: dependency: "direct main" description: name: flutter_web_auth_2 - sha256: "3ea3a0cc539ca74319f4f2f7484f62742fe5b2ff9a0fca37575426d6e6f07901" + sha256: "4d3d2fd3d26bf1a26b3beafd4b4b899c0ffe10dc99af25abc58ffe24e991133c" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" flutter_web_auth_2_platform_interface: dependency: transitive description: @@ -1121,26 +1265,26 @@ packages: dependency: "direct main" description: name: flutter_webrtc - sha256: "2f17fb96e0c9c6ff75f6b1c36d94755461fc7f36a5c28386f5ee5a18b98688c8" + sha256: "2852cd5758d01ce7761a56170ad64e79173624915100df53748e5f0dab3f19f7" url: "https://pub.dev" source: hosted - version: "0.9.48+hotfix.1" + version: "0.10.6" fluttertoast: dependency: "direct main" description: name: fluttertoast - sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + sha256: "81b68579e23fcbcada2db3d50302813d2371664afe6165bc78148050ab94bf66" url: "https://pub.dev" source: hosted - version: "8.2.4" + version: "8.2.5" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -1182,10 +1326,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 url: "https://pub.dev" source: hosted - version: "7.6.7" + version: "7.7.0" glob: dependency: transitive description: @@ -1198,10 +1342,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: e1a30a66d734f9e498b1b6522d6a75ded28242bad2359a9158df38a1c30bcf1f + sha256: aa073287b8f43553678e6fa9e8bb9c83212ff76e09542129a8099bbc8db4df65 url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "14.1.2" google_fonts: dependency: "direct main" description: @@ -1262,10 +1406,10 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "65998cc4d2cd9680a3d9709d893d2f6bb15e6c1f92626c3f1fa650b4b3281521" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" html: dependency: transitive description: @@ -1286,10 +1430,10 @@ packages: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -1326,34 +1470,34 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + sha256: "33974eca2e87e8b4e3727f1b94fa3abcb25afe80b6bc2c4d449a0e150aedf720" url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.1.1" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1" url: "https://pub.dev" source: hosted - version: "0.8.9+3" + version: "0.8.12+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 + sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.4" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + sha256: "4824d8c7f6f89121ef0122ff79bb00b009607faecc8545b86bca9ab5ce1e95bf" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.11+2" image_picker_linux: dependency: transitive description: @@ -1374,10 +1518,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.10.0" image_picker_windows: dependency: transitive description: @@ -1411,10 +1555,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" inview_notifier_list: dependency: "direct main" description: @@ -1436,18 +1580,18 @@ packages: dependency: transitive description: name: irondash_engine_context - sha256: "4f5e2629296430cce08cdff42e47cef07b8f74a64fdbdfb0525d147bc1a969a2" + sha256: e8398cca5e28dc280c87b8c35a6ff4e15be844eabec51e713631f83903563681 url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.5.3" irondash_message_channel: dependency: transitive description: name: irondash_message_channel - sha256: dd581214215dca054bd9873209d690ec3609288c28774cb509dbd86b21180cf8 + sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.0" isolate: dependency: transitive description: @@ -1468,26 +1612,26 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" just_audio: dependency: "direct main" description: name: just_audio - sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823 + sha256: "5abfab1d199e01ab5beffa61b3e782350df5dad036cb8c83b79fa45fc656614e" url: "https://pub.dev" source: hosted - version: "0.9.36" + version: "0.9.38" just_audio_mpv: dependency: "direct main" description: @@ -1500,18 +1644,18 @@ packages: dependency: transitive description: name: just_audio_platform_interface - sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1 + sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.3.0" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70" + sha256: "0edb481ad4aa1ff38f8c40f1a3576013c3420bf6669b686fe661627d49bc606c" url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.4.11" keyboard_shortcuts: dependency: "direct main" description: @@ -1529,12 +1673,36 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" linagora_design_flutter: dependency: "direct main" description: path: "." ref: master - resolved-ref: "7ed486ea00f7e91254555a4a828ff3a625b4e1ae" + resolved-ref: ffbcb9dd4d6cefb7fb60b7a9b15ad684dae6bdff url: "git@github.com:linagora/linagora-design-flutter.git" source: git version: "0.0.1" @@ -1542,10 +1710,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" lists: dependency: transitive description: @@ -1566,18 +1734,18 @@ packages: dependency: "direct main" description: name: lottie - sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 + sha256: "1f0ce68112072d66ea271a9841994fa8d16442e23d8cf8996c9fa74174e58b4e" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "3.0.0" macos_ui: dependency: transitive description: name: macos_ui - sha256: d351f0bada7e5b0cee8cf394299878a6c04e5cfcd784fa1d40e44299501124d8 + sha256: "91c7f3427f763fd96b65831342b896b18751140e6bf55f8572fcb41f7b30bcab" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" macos_window_utils: dependency: transitive description: @@ -1598,24 +1766,24 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" matrix: dependency: "direct main" description: path: "." ref: "twake-supported-0.22.6" - resolved-ref: "272cb73ab18cff7ae534ef4eafa61c6a3494cb00" + resolved-ref: ad561ad17606bac31357a6107dbe977d806ed070 url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.6" @@ -1721,10 +1889,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.0" mgrs_dart: dependency: transitive description: @@ -1745,10 +1913,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" url: "https://pub.dev" source: hosted - version: "5.4.2" + version: "5.4.4" mpv_dart: dependency: transitive description: @@ -1833,26 +2001,26 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "8.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1865,26 +2033,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -1913,18 +2081,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" url: "https://pub.dev" source: hosted - version: "10.4.5" + version: "11.0.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e url: "https://pub.dev" source: hosted - version: "10.3.6" + version: "11.1.0" permission_handler_apple: dependency: transitive description: @@ -1961,10 +2129,10 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: df594f989f0c31cdb3ed48f3d49cb9ffadf11cc3700d2c3460b1912c93432621 + sha256: "1ddee48659025df5c80153a2e85c3fa0f5b5733ecffcda9769a9447da2c51352" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.1" photo_manager_image_provider: dependency: "direct main" description: @@ -1993,10 +2161,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" platform_detect: dependency: transitive description: @@ -2017,10 +2185,10 @@ packages: dependency: transitive description: name: pointer_interceptor - sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a + sha256: d0a8e660d1204eaec5bd34b34cc92174690e076d2e4f893d9d68c486a13b07c4 url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.10.1+1" pointer_interceptor_ios: dependency: transitive description: @@ -2041,18 +2209,10 @@ packages: dependency: transitive description: name: pointer_interceptor_web - sha256: "9386e064097fd16419e935c23f08f35b58e6aaec155dd39bd6a003b88f9c14b4" + sha256: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e url: "https://pub.dev" source: hosted - version: "0.10.1+2" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" + version: "0.10.2" polylabel: dependency: transitive description: @@ -2069,22 +2229,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - positioned_tap_detector_2: - dependency: transitive - description: - name: positioned_tap_detector_2 - sha256: "52e06863ad3e1f82b058fd05054fc8c9caeeb3b47d5cea7a24bd9320746059c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" process: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" proj4dart: dependency: transitive description: @@ -2125,6 +2277,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + puppeteer: + dependency: transitive + description: + name: puppeteer + sha256: c45c51b4ad8d70acdffeb1cfb9d16b60a7eaab7bfef314dd5b02c3607269b556 + url: "https://pub.dev" + source: hosted + version: "3.11.0" qr: dependency: transitive description: @@ -2314,10 +2474,10 @@ packages: dependency: transitive description: name: sensors_plus - sha256: "8e7fa79b4940442bb595bfc0ee9da4af5a22a0fe6ebacc74998245ee9496a82d" + sha256: "6898cd4490ffc27fea4de5976585e92fae55355175d46c6c3b3d719d42f9e230" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "5.0.1" sensors_plus_platform_interface: dependency: transitive description: @@ -2338,66 +2498,42 @@ packages: dependency: "direct main" description: name: share_plus - sha256: f582d5741930f3ad1bf0211d358eddc0508cc346e5b4b248bd1e569c995ebb7a - url: "https://pub.dev" - source: hosted - version: "4.5.3" - share_plus_linux: - dependency: transitive - description: - name: share_plus_linux - sha256: dc32bf9f1151b9864bb86a997c61a487967a08f2e0b4feaa9a10538712224da4 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - share_plus_macos: - dependency: transitive - description: - name: share_plus_macos - sha256: "44daa946f2845045ecd7abb3569b61cd9a55ae9cc4cbec9895b2067b270697ae" + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "9.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" - url: "https://pub.dev" - source: hosted - version: "3.4.0" - share_plus_web: - dependency: transitive - description: - name: share_plus_web - sha256: eaef05fa8548b372253e772837dd1fbe4ce3aca30ea330765c945d7d4f7c9935 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - share_plus_windows: - dependency: transitive - description: - name: share_plus_windows - sha256: "3a21515ae7d46988d42130cd53294849e280a5de6ace24bae6912a1bffd757d4" + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "76917b7d4b9526b2ba416808a7eb9fb2863c1a09cf63ec85f1453da240fa818a" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" shared_preferences_ios: dependency: transitive description: @@ -2414,14 +2550,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - sha256: "81b6a60b2d27020eb0fc41f4cebc91353047309967901a79ee8203e40c42ed46" - url: "https://pub.dev" - source: hosted - version: "2.0.5" shared_preferences_platform_interface: dependency: transitive description: @@ -2434,10 +2562,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -2454,6 +2582,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" shelf_web_socket: dependency: transitive description: @@ -2474,18 +2610,19 @@ packages: dependency: "direct main" description: name: skeletonizer - sha256: "2eb80153c80507359ff05f6a18ed50ae0bafa1b999aa867a8cef0a53387b5650" + sha256: "50c3fdf3d1bf6182129c03b53bc7ff6dca10ca0b8e71ccdee148b9322caabdba" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2+1" skeletons: dependency: "direct main" description: - name: skeletons - sha256: "5b2d08ae7f908ee1f7007ca99f8dcebb4bfc1d3cb2143dec8d112a5be5a45c8f" - url: "https://pub.dev" - source: hosted - version: "0.0.3" + path: "." + ref: master + resolved-ref: a0d5b44d3e0c04f587ea3e4997c4a3dfa06677b1 + url: "https://github.com/alirezat66/skeletons.git" + source: git + version: "0.0.4" sky_engine: dependency: transitive description: flutter @@ -2535,18 +2672,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" stack_trace: dependency: transitive description: @@ -2583,18 +2720,18 @@ packages: dependency: "direct main" description: name: super_clipboard - sha256: "77f044320934386e0b7a3911e05312426d7f33deb6e8cdb28886663430b0e5b0" + sha256: f81058a9b3cadaaf60f37c2a37dd2647c6e5eda4533e335f1512605e3b9fb860 url: "https://pub.dev" source: hosted - version: "0.8.4" + version: "0.8.15" super_native_extensions: dependency: transitive description: name: super_native_extensions - sha256: f96db6b137a0b135e43034289bb55ca6447b65225076036e81f97ebb6381ffeb + sha256: bb6499c83484c1dbe293e68907a9b6d51e30b699502c5e11940e834c310df261 url: "https://pub.dev" source: hosted - version: "0.8.5" + version: "0.8.15" sync_http: dependency: transitive description: @@ -2623,18 +2760,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timezone: dependency: transitive description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.3" timing: dependency: transitive description: @@ -2719,26 +2856,26 @@ packages: dependency: "direct main" description: name: unifiedpush - sha256: fa0f38104cacd258b750d400c1842fa71ac4bbf29b3c741944d2c6d4572d789e + sha256: ef7f3ae6139d27169604e3844379ef7929af573a2be21d9e82187f44ab7b9a32 url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "5.0.1" unifiedpush_android: dependency: transitive description: name: unifiedpush_android - sha256: f69a30edcd6f777d0d2877429558ab8615fe6691a21ea7d4563406373582c5e0 + sha256: "610ad746294541f56d632adf9afba5d1c164c44e23ec0dd2162a41a6ff00a00e" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.2.3" unifiedpush_platform_interface: dependency: transitive description: name: unifiedpush_platform_interface - sha256: "29412ec89f361c43ba06061a7ab9d50a09704e03f6df724b822a39b802bfb666" + sha256: "7782b18a15d22bb184fa766ef1e0c675eef862055ff815453df7041dfd026146" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.1" universal_html: dependency: "direct main" description: @@ -2783,26 +2920,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.0" url_launcher_linux: dependency: transitive description: @@ -2815,10 +2952,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: @@ -2831,10 +2968,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" url_launcher_windows: dependency: transitive description: @@ -2847,10 +2984,10 @@ packages: dependency: transitive description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" value_layout_builder: dependency: transitive description: @@ -2895,10 +3032,18 @@ packages: dependency: "direct main" description: name: vibration - sha256: "778ace40e84852e6cf6017cdbaf6790a837d73ff3dd50b27da9ac232a19de8fc" + sha256: "06588a845a4ebc73ab7ff7da555c2b3dbcd9676164b5856a38bf0b2287f1045d" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + vibration_platform_interface: + dependency: transitive + description: + name: vibration_platform_interface + sha256: "735a5fef0f284de0ad9449a5ed7d36ba017c6f59b5b20ac64418af4a6bd35ee7" url: "https://pub.dev" source: hosted - version: "1.8.4" + version: "0.0.1" video_compress: dependency: "direct main" description: @@ -2912,26 +3057,26 @@ packages: dependency: "direct main" description: name: video_player - sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23 + sha256: db6a72d8f4fd155d0189845678f55ad2fd54b02c10dcafd11c068dbb631286c0 url: "https://pub.dev" source: hosted - version: "2.8.3" + version: "2.8.6" video_player_android: dependency: transitive description: name: video_player_android - sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2" + sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.14" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.6.1" video_player_platform_interface: dependency: transitive description: @@ -2944,10 +3089,10 @@ packages: dependency: transitive description: name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.0" video_thumbnail: dependency: "direct main" description: @@ -2987,10 +3132,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "14.2.1" volume_controller: dependency: transitive description: @@ -3027,18 +3172,18 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "14758533319a462ffb5aa3b7ddb198e59b29ac3b02da14173a1715d65d4e6e68" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.5" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" wakelock_web: dependency: transitive description: @@ -3068,42 +3213,42 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" webdriver: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webrtc_interface: dependency: "direct main" description: name: webrtc_interface - sha256: "2efbd3e4e5ebeb2914253bcc51dafd3053c4b87b43f3076c74835a9deecbae3a" + sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" wechat_camera_picker: dependency: "direct main" description: name: wechat_camera_picker - sha256: "682d4cd5606d5f95af2f6efe3224ebb5fe5ac014980eeaba0eddd211219c6f4a" + sha256: "8841e30c8ce7adb6e0b99e75736cfd2f10f40203ea7f82863e2db7e155ad6f42" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" wechat_picker_library: dependency: transitive description: @@ -3116,18 +3261,18 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.1" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" window_to_front: dependency: transitive description: @@ -3148,10 +3293,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" xml: dependency: transitive description: @@ -3169,5 +3314,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index b7fbc48751..a4ebbc8ef9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fluffychat -description: The open digital workplace. +description: A convenient Matrix-based tool for personal and corporate communication. publish_to: none -version: 2.5.3+2330 +version: 2.5.9+2330 environment: sdk: ">=3.1.3 <4.0.0" @@ -67,13 +67,13 @@ dependencies: desktop_drop: ^0.4.4 desktop_lifecycle: ^0.1.0 desktop_notifications: ^0.6.3 - device_info_plus: ^9.0.3 + device_info_plus: ^10.1.0 dynamic_color: ^1.6.0 emoji_picker_flutter: ^1.5.1 emoji_proposal: ^0.0.1 emojis: ^0.9.9 fcm_shared_isolate: ^0.1.0 - file_picker: ^5.3.0 + file_picker: ^8.0.3 flutter: sdk: flutter flutter_app_badger: ^1.5.0 @@ -81,21 +81,21 @@ dependencies: flutter_blurhash: ^0.8.2 flutter_cache_manager: ^3.3.0 flutter_foreground_task: ^3.10.0 - flutter_local_notifications: ^12.0.2 + flutter_local_notifications: ^17.1.2 flutter_localizations: sdk: flutter - flutter_map: ^3.1.0 + flutter_map: ^4.0.0 # flutter_matrix_html: ^1.1.0 flutter_olm: ^1.2.0 - flutter_openssl_crypto: ^0.1.0 + flutter_openssl_crypto: ^0.3.0 flutter_ringtone_player: ^3.1.1 - flutter_secure_storage: ^7.0.1 + flutter_secure_storage: ^9.2.1 flutter_svg: ^0.22.0 flutter_typeahead: ^5.1.0 flutter_web_auth_2: ^3.1.1 # flutter_webrtc: # Until https://github.com/flutter-webrtc/flutter-webrtc/issues/1212 is fixed # git: https://github.com/radzio-it/flutter-webrtc.git - flutter_webrtc: ^0.9.31 + flutter_webrtc: ^0.10.6 future_loading_dialog: ^0.2.3 handy_window: ^0.1.9 hive: ^2.2.3 @@ -111,9 +111,9 @@ dependencies: matrix_homeserver_recommendations: ^0.3.0 matrix_link_text: ^2.0.0 native_imaging: ^0.1.0 - package_info_plus: ^4.0.0 + package_info_plus: ^8.0.0 path_provider: ^2.0.15 - permission_handler: ^10.4.5 + permission_handler: ^11.0.1 pin_code_text_field: ^1.8.0 provider: ^6.0.2 punycode: ^1.0.0 @@ -121,21 +121,25 @@ dependencies: qr_flutter: ^4.0.0 record: ^4.4.4 scroll_to_index: ^3.0.1 - share_plus: ^4.0.10+1 - shared_preferences: 2.0.15 # Pinned because https://github.com/flutter/flutter/issues/118401 + share_plus: ^9.0.0 + shared_preferences: ^2.2.3 slugify: ^2.0.0 - skeletons: ^0.0.3 + skeletons: + # TODO: Remove when https://github.com/badjio/skeletons/pull/9 is merged + git: + url: https://github.com/alirezat66/skeletons.git + ref: master tor_detector_web: ^1.1.0 uni_links: ^0.5.1 - unifiedpush: ^4.0.3 + unifiedpush: ^5.0.1 universal_html: ^2.0.8 url_launcher: ^6.0.20 vibration: ^1.7.4-nullsafety.0 - go_router: ^10.0.0 + go_router: ^14.1.2 wakelock: ^0.6.2 webrtc_interface: ^1.0.10 overflow_view: ^0.3.1 - json_annotation: ^4.8.1 + json_annotation: 4.9.0 equatable: ^2.0.5 get_it: ^7.2.0 dio: ^5.1.1 @@ -146,10 +150,10 @@ dependencies: fluttertoast: ^8.2.2 rxdart: ^0.27.7 photo_manager: ^3.0.0-dev.5 - flutter_inappwebview: ^5.8.0 + flutter_inappwebview: ^6.0.0 tuple: ^2.0.2 - lottie: ^2.3.2 - wechat_camera_picker: 4.2.1 + lottie: ^3.0.0 + wechat_camera_picker: 4.2.2 open_file: ^3.3.2 mime: ^1.0.4 async: ^2.11.0 @@ -163,7 +167,7 @@ dependencies: media_kit_libs_video: ^1.0.1 video_player: ^2.7.2 js: ^0.6.7 - super_clipboard: 0.8.4 + super_clipboard: 0.8.15 google_fonts: ^4.0.4 crypto: ^3.0.3 flutter_contacts: ^1.1.7+1 @@ -171,26 +175,29 @@ dependencies: after_layout: ^1.2.0 photo_manager_image_provider: ^2.1.0 flutter_slidable: ^3.0.1 - skeletonizer: 1.1.0 + skeletonizer: 1.1.2+1 flutter_portal: 1.1.4 external_path: 1.0.3 gal: 2.3.0 + auto_size_text: 3.0.0 + flutter_avif: 2.4.1 dev_dependencies: build_runner: ^2.3.3 flutter_launcher_icons: ^0.13.1 - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.2 flutter_native_splash: ^2.0.3+1 flutter_test: sdk: flutter - hive_generator: 2.0.0 + hive_generator: 2.0.1 import_sorter: ^4.6.0 integration_test: sdk: flutter json_serializable: ^6.6.2 msix: ^3.6.2 translations_cleaner: ^0.0.5 - mockito: 5.4.2 + mockito: 5.4.4 + flutter_launcher_icons: android: false @@ -235,6 +242,7 @@ msix_config: dependency_overrides: # Until all dependencies are compatible. Missing: file_picker_cross, flutter_matrix_html ffi: 2.0.0 + http: 1.2.1 # This otherwise breaks on linux with flutter 3.7.0, let's override it for now. file_selector: ^0.9.2+2 file_selector_linux: ^0.9.1 diff --git a/scripts/package-windows-debug.sh b/scripts/package-windows-debug.sh index e47608d2cc..7be8c89b74 100755 --- a/scripts/package-windows-debug.sh +++ b/scripts/package-windows-debug.sh @@ -5,4 +5,5 @@ curl -OL "https://files.jrsoftware.org/is/6/innosetup-6.2.2.exe" ./innosetup-6.2.2.exe //verysilent echo "Packaging." +flutter pub get flutter pub global run flutter_distributor:main.dart package --platform windows --targets exe --skip-clean --flutter-build-args="profile" diff --git a/scripts/package-windows.sh b/scripts/package-windows.sh index 22d9f1b057..a4453933f3 100755 --- a/scripts/package-windows.sh +++ b/scripts/package-windows.sh @@ -5,4 +5,5 @@ curl -OL "https://files.jrsoftware.org/is/6/innosetup-6.2.2.exe" ./innosetup-6.2.2.exe //verysilent echo "Packaging." +flutter pub get flutter pub global run flutter_distributor:main.dart package --platform windows --targets exe --skip-clean --flutter-build-args="release" diff --git a/scripts/prepare-ios.sh b/scripts/prepare-ios.sh index fc43cb8167..fb718bcc3e 100755 --- a/scripts/prepare-ios.sh +++ b/scripts/prepare-ios.sh @@ -1,6 +1,4 @@ #!/usr/bin/env bash flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs -# Use alternate beautifier -brew install xcbeautify cd ios pod install && pod update diff --git a/test/domain/contacts/contacts_manager_test.dart b/test/domain/contacts/contacts_manager_test.dart index bc10c05ae7..4254c460c9 100644 --- a/test/domain/contacts/contacts_manager_test.dart +++ b/test/domain/contacts/contacts_manager_test.dart @@ -672,5 +672,746 @@ void main() { ).called(1); }, ); + + test( + '[Account-A] WHEN it is available get Phonebook contact.\n' + '[Account-A] AND contactsNotifier return GetContactsSuccess with contacts is empty.\n' + '[Account-A] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is empty.\n' + '[Account-A] THEN contactsNotifier in ContactsManager SHOULD have GetContactsSuccess state.\n' + '[Account-A] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsSuccess state.\n' + '[Account-A] THEN list ToM contact SHOULD is empty.\n' + '[Account-A] THEN list Phonebook contact SHOULD is empty.\n' + 'Trigger UI => switch to another account and call synchronize contacts.\n' + '[Account-B] AND contactsNotifier return GetContactsSuccess with contacts is empty.\n' + '[Account-B] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is empty.\n' + '[Account-B] THEN contactsNotifier in ContactsManager SHOULD have GetContactsSuccess state.\n' + '[Account-B] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsSuccess state.\n' + '[Account-B] THEN list ToM contact SHOULD is empty.\n' + '[Account-B] THEN list Phonebook contact SHOULD is empty.\n', + () async { + final mockGetTomContactsInteractor = MockGetTomContactsInteractor(); + final mockPhonebookContactInteractor = MockPhonebookContactInteractor(); + + final contactsManager = ContactsManager( + getTomContactsInteractor: mockGetTomContactsInteractor, + phonebookContactInteractor: mockPhonebookContactInteractor, + ); + + final List listTomContactsSuccessState = []; + + final List listPhonebookContactsSuccessState = []; + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + const Left(GetContactsIsEmpty()), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + const Left(GetPhonebookContactsIsEmpty()), + ]), + ); + + contactsManager.getContactsNotifier().addListener(() { + contactsManager.getContactsNotifier().value.fold( + (failure) => null, + (success) => listTomContactsSuccessState.add(success), + ); + }); + + contactsManager.getPhonebookContactsNotifier().addListener(() { + contactsManager.getPhonebookContactsNotifier().value.fold( + (failure) => null, + (success) => listPhonebookContactsSuccessState.add(success), + ); + }); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 1); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 1); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + ], + ); + + /// Trigger switch account + contactsManager.reSyncContacts(); + + listTomContactsSuccessState.clear(); + + listPhonebookContactsSuccessState.clear(); + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + const Left(GetContactsIsEmpty()), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + const Left(GetPhonebookContactsIsEmpty()), + ]), + ); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 1); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 1); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + ], + ); + }, + ); + + test( + '[Account-A] WHEN it is available get Phonebook contact.\n' + '[Account-A] AND call initialSynchronizeContacts success.\n' + '[Account-A] AND contactsNotifier return GetContactsSuccess with contacts is not empty.\n' + '[Account-A] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is not empty.\n' + '[Account-A] THEN contactsNotifier in ContactsManager SHOULD have GetContactsSuccess state.\n' + '[Account-A] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsSuccess state.\n' + '[Account-A] THEN list ToM contact SHOULD is not empty.\n' + '[Account-A] THEN list Phonebook contact SHOULD is not empty.\n' + '[Account-A] THEN contactsNotifier and phonebookContactInteractor just call only one time.\n' + 'Trigger UI => switch to another account and call synchronize contacts.\n' + '[Account-B] AND THEN call initialSynchronizeContacts again.\n' + '[Account-B] AND contactsNotifier return GetContactsSuccess with contacts is not empty.\n' + '[Account-B] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is not empty.\n' + '[Account-B] THEN contactsNotifier in ContactsManager SHOULD have GetContactsSuccess state.\n' + '[Account-B] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsSuccess state.\n' + '[Account-B] THEN list ToM contact SHOULD is not empty.\n' + '[Account-B] THEN list Phonebook contact SHOULD is not empty.\n' + '[Account-B] THEN contactsNotifier and phonebookContactInteractor just call only one time.\n', + () async { + final mockGetTomContactsInteractor = MockGetTomContactsInteractor(); + final mockPhonebookContactInteractor = MockPhonebookContactInteractor(); + + final contactsManager = ContactsManager( + getTomContactsInteractor: mockGetTomContactsInteractor, + phonebookContactInteractor: mockPhonebookContactInteractor, + ); + + final List listTomContactsSuccessState = []; + + final List listPhonebookContactsSuccessState = []; + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + Right(GetContactsSuccess(contacts: tomContacts)), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ]), + ); + + contactsManager.getContactsNotifier().addListener(() { + contactsManager.getContactsNotifier().value.fold( + (failure) => null, + (success) => listTomContactsSuccessState.add(success), + ); + }); + + contactsManager.getPhonebookContactsNotifier().addListener(() { + contactsManager.getPhonebookContactsNotifier().value.fold( + (failure) => null, + (success) => listPhonebookContactsSuccessState.add(success), + ); + }); + + expect( + contactsManager + .getContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 2); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + GetContactsSuccess(contacts: tomContacts), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 2); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + GetPhonebookContactsSuccess(contacts: phonebookContacts), + ], + ); + + /// Trigger switch account + + contactsManager.reSyncContacts(); + + listTomContactsSuccessState.clear(); + + listPhonebookContactsSuccessState.clear(); + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + Right(GetContactsSuccess(contacts: tomContacts)), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ]), + ); + + expect( + contactsManager + .getContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 2); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + GetContactsSuccess(contacts: tomContacts), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 2); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + GetPhonebookContactsSuccess(contacts: phonebookContacts), + ], + ); + }, + ); + + test( + '[Account-A] WHEN it is available get Phonebook contact.\n' + '[Account-A] AND call initialSynchronizeContacts success.\n' + '[Account-A] AND contactsNotifier return GetContactsSuccess with contacts is not empty.\n' + '[Account-A] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is not empty.\n' + '[Account-A] THEN contactsNotifier in ContactsManager SHOULD have GetContactsSuccess state.\n' + '[Account-A] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsSuccess state.\n' + '[Account-A] THEN list ToM contact SHOULD is not empty.\n' + '[Account-A] THEN list Phonebook contact SHOULD is not empty.\n' + '[Account-A] THEN contactsNotifier and phonebookContactInteractor just call only one time.\n' + 'Trigger UI => switch to another account and call synchronize contacts.\n' + '[Account-B] AND THEN call initialSynchronizeContacts again.\n' + '[Account-B] AND contactsNotifier return GetContactsSuccess with contacts is empty.\n' + '[Account-B] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is not empty.\n' + '[Account-B] THEN contactsNotifier in ContactsManager SHOULD have GetContactsIsEmpty state.\n' + '[Account-B] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsSuccess state.\n' + '[Account-B] THEN list ToM contact SHOULD is empty.\n' + '[Account-B] THEN list Phonebook contact SHOULD is not empty.\n' + '[Account-B] THEN contactsNotifier and phonebookContactInteractor just call only one time.\n', + () async { + final mockGetTomContactsInteractor = MockGetTomContactsInteractor(); + final mockPhonebookContactInteractor = MockPhonebookContactInteractor(); + + final contactsManager = ContactsManager( + getTomContactsInteractor: mockGetTomContactsInteractor, + phonebookContactInteractor: mockPhonebookContactInteractor, + ); + + final List listTomContactsSuccessState = []; + + final List listPhonebookContactsSuccessState = []; + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + Right(GetContactsSuccess(contacts: tomContacts)), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ]), + ); + + contactsManager.getContactsNotifier().addListener(() { + contactsManager.getContactsNotifier().value.fold( + (failure) => null, + (success) => listTomContactsSuccessState.add(success), + ); + }); + + contactsManager.getPhonebookContactsNotifier().addListener(() { + contactsManager.getPhonebookContactsNotifier().value.fold( + (failure) => null, + (success) => listPhonebookContactsSuccessState.add(success), + ); + }); + + expect( + contactsManager + .getContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + expect( + contactsManager + .getContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + + expect( + contactsManager + .getPhonebookContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 2); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + GetContactsSuccess(contacts: tomContacts), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 2); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + GetPhonebookContactsSuccess(contacts: phonebookContacts), + ], + ); + + /// Trigger switch account + + contactsManager.reSyncContacts(); + + listTomContactsSuccessState.clear(); + + listPhonebookContactsSuccessState.clear(); + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + const Left(GetContactsIsEmpty()), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ]), + ); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + expect( + contactsManager + .getContactsNotifier() + .value + .getFailureOrNull() != + null, + true, + ); + + expect( + contactsManager + .getPhonebookContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 1); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 2); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + GetPhonebookContactsSuccess(contacts: phonebookContacts), + ], + ); + }, + ); + + test( + '[Account-A] WHEN it is available get Phonebook contact.\n' + '[Account-A] AND call initialSynchronizeContacts success.\n' + '[Account-A] AND contactsNotifier return GetContactsSuccess with contacts is empty.\n' + '[Account-A] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is empty.\n' + '[Account-A] THEN contactsNotifier in ContactsManager SHOULD have GetContactsIsEmpty state.\n' + '[Account-A] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsIsEmpty state.\n' + '[Account-A] THEN list ToM contact SHOULD is empty.\n' + '[Account-A] THEN list Phonebook contact SHOULD is empty.\n' + '[Account-A] THEN contactsNotifier and phonebookContactInteractor just call only one time.\n' + 'Trigger UI => switch to another account and call synchronize contacts.\n' + '[Account-B] AND THEN call initialSynchronizeContacts again.\n' + '[Account-B] AND contactsNotifier return GetContactsSuccess with contacts is empty.\n' + '[Account-B] AND phonebookContactInteractor return GetPhonebookContactsSuccess with contacts is not empty.\n' + '[Account-B] THEN contactsNotifier in ContactsManager SHOULD have GetContactsIsEmpty state.\n' + '[Account-B] THEN phonebookContactInteractor in ContactsManager SHOULD have GetPhonebookContactsSuccess state.\n' + '[Account-B] THEN list ToM contact SHOULD is empty.\n' + '[Account-B] THEN list Phonebook contact SHOULD is not empty.\n' + '[Account-B] THEN contactsNotifier and phonebookContactInteractor just call only one time.\n', + () async { + final mockGetTomContactsInteractor = MockGetTomContactsInteractor(); + final mockPhonebookContactInteractor = MockPhonebookContactInteractor(); + + final contactsManager = ContactsManager( + getTomContactsInteractor: mockGetTomContactsInteractor, + phonebookContactInteractor: mockPhonebookContactInteractor, + ); + + final List listTomContactsSuccessState = []; + + final List listPhonebookContactsSuccessState = []; + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + const Left(GetContactsIsEmpty()), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + const Left(GetPhonebookContactsIsEmpty()), + ]), + ); + + contactsManager.getContactsNotifier().addListener(() { + contactsManager.getContactsNotifier().value.fold( + (failure) => null, + (success) => listTomContactsSuccessState.add(success), + ); + }); + + contactsManager.getPhonebookContactsNotifier().addListener(() { + contactsManager.getPhonebookContactsNotifier().value.fold( + (failure) => null, + (success) => listPhonebookContactsSuccessState.add(success), + ); + }); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + expect( + contactsManager + .getContactsNotifier() + .value + .getFailureOrNull() != + null, + true, + ); + + expect( + contactsManager + .getPhonebookContactsNotifier() + .value + .getFailureOrNull() != + null, + true, + ); + + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 1); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 1); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + ], + ); + + /// Trigger switch account + + contactsManager.reSyncContacts(); + + listTomContactsSuccessState.clear(); + + listPhonebookContactsSuccessState.clear(); + + when( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).thenAnswer( + (_) => Stream.fromIterable([ + const Right(ContactsLoading()), + Right(GetContactsSuccess(contacts: tomContacts)), + ]), + ); + + when(mockPhonebookContactInteractor.execute()).thenAnswer( + (_) => Stream.fromIterable([ + const Right(GetPhonebookContactsLoading(progress: 0)), + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ]), + ); + + contactsManager.initialSynchronizeContacts( + isAvailableSupportPhonebookContacts: true, + ); + + await Future.delayed(const Duration(seconds: 1)); + + expect( + contactsManager + .getContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + + expect( + contactsManager + .getPhonebookContactsNotifier() + .value + .getSuccessOrNull() != + null, + true, + ); + verify( + mockGetTomContactsInteractor.execute( + limit: AppConfig.maxFetchContacts, + ), + ).called(1); + + verify( + mockPhonebookContactInteractor.execute(), + ).called(1); + + expectLater(listTomContactsSuccessState.length, 2); + + expectLater( + listTomContactsSuccessState, + [ + const ContactsLoading(), + GetContactsSuccess(contacts: tomContacts), + ], + ); + + expectLater(listPhonebookContactsSuccessState.length, 2); + + expectLater( + listPhonebookContactsSuccessState, + [ + const GetPhonebookContactsLoading(progress: 0), + GetPhonebookContactsSuccess(contacts: phonebookContacts), + ], + ); + }, + ); }); } diff --git a/test/domain/contacts/contacts_manager_test.mocks.dart b/test/domain/contacts/contacts_manager_test.mocks.dart index 07a953663c..662ba3df12 100644 --- a/test/domain/contacts/contacts_manager_test.mocks.dart +++ b/test/domain/contacts/contacts_manager_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in fluffychat/test/domain/contacts/contacts_manager_test.dart. // Do not manually edit this file. @@ -19,6 +19,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/list_notifier_test.dart b/test/list_notifier_test.dart index 2789c5d2a0..34448c3cb6 100644 --- a/test/list_notifier_test.dart +++ b/test/list_notifier_test.dart @@ -86,7 +86,7 @@ void main() { expect( listNotifier.value, equals([ - ...listNotifier.value.getRange(0, 2).toList(), + ...listNotifier.value.getRange(0, 2), replaceFile, ]), ); diff --git a/test/mixin/contacts_view_controller_mixin_test.dart b/test/mixin/contacts_view_controller_mixin_test.dart new file mode 100644 index 0000000000..6b12c652ae --- /dev/null +++ b/test/mixin/contacts_view_controller_mixin_test.dart @@ -0,0 +1,2767 @@ +import 'package:dartz/dartz.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/domain/app_state/contact/get_contacts_state.dart'; +import 'package:fluffychat/domain/app_state/contact/get_phonebook_contacts_state.dart'; +import 'package:fluffychat/domain/model/contact/contact_status.dart'; +import 'package:fluffychat/presentation/extensions/value_notifier_custom.dart'; +import 'package:fluffychat/presentation/mixins/contacts_view_controller_mixin.dart'; +import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_success.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fluffychat/domain/model/contact/contact.dart'; +import 'package:matrix/matrix.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'contacts_view_controller_mixin_test.mocks.dart'; + +class ConcretePresentationSearch extends PresentationSearch { + const ConcretePresentationSearch({ + required String displayName, + required String email, + required String phoneNumber, + required ContactStatus status, + }) : super( + displayName: displayName, + email: email, + ); + + @override + String get id => "@test:domain.com"; +} + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + const debouncerIntervalInMilliseconds = 300; + + final List tomContacts = [ + const Contact( + email: "alice1@domain.com", + displayName: "Alice 1", + matrixId: "@alice1:domain.com", + phoneNumber: "07 81 12 38 61", + status: ContactStatus.active, + ), + const Contact( + email: "alice2@domain.com", + displayName: "Alice 2", + matrixId: "@alice2:domain.com", + phoneNumber: "02 81 12 38 61", + status: ContactStatus.active, + ), + const Contact( + email: "alice3@domain.com", + displayName: "Alice 3", + matrixId: "@alice3:domain.com", + phoneNumber: "03 81 12 38 61", + status: ContactStatus.active, + ), + const Contact( + email: "alice4@domain.com", + displayName: "Alice 4", + matrixId: "@alice4:domain.com", + phoneNumber: "04 81 12 38 61", + status: ContactStatus.inactive, + ), + const Contact( + email: "alice5@domain.com", + displayName: "Alice 5", + matrixId: "@alice5:domain.com", + phoneNumber: "05 81 12 38 61", + status: ContactStatus.inactive, + ), + ]; + + final List phonebookContacts = [ + const Contact( + email: "bob@domain.com", + displayName: "BoB", + ), + const Contact( + displayName: "BoB1", + phoneNumber: "11 22 33 44 55", + ), + const Contact( + email: "bob2@domain.com", + displayName: "BoB2", + phoneNumber: "+84000000000", + ), + const Contact( + email: "bob3@domain.com", + displayName: "BoB 3", + ), + const Contact( + email: "bob@domain.com", + displayName: "BoB", + ), + ]; + + final List recentContacts = [ + const ConcretePresentationSearch( + displayName: "Alice test", + email: "alicetest@domain.com", + phoneNumber: "+84111111111", + status: ContactStatus.active, + ), + ]; + + late MockContactsViewControllerMixin mockContactsViewControllerMixin; + late Client mockClient; + late MatrixLocalizations mockMatrixLocalizations; + + setUp(() { + mockContactsViewControllerMixin = MockContactsViewControllerMixin(); + mockMatrixLocalizations = MockMatrixLocalizations(); + mockClient = MockClient(); + }); + + group('Test ContactsViewControllerMixin on Web', () { + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value, + [], + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn(ValueNotifierCustom(const Left(GetContactsIsEmpty()))); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + final presentationContact = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull(); + + expect( + presentationContact is GetContactsSuccess, + true, + ); + + expect( + (presentationContact as GetContactsSuccess).contacts.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is empty.\n' + 'AND presentationContactNotifier return GetContactsFailure' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsFailure state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + const Left(GetContactsFailure(keyword: '', exception: dynamic)), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsFailure(keyword: '', exception: dynamic)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n' + 'AFTER that, enable search mode with keyword is 123' + 'THEN search by keyword return no result' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = '123'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value, + [], + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value, + [], + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n' + 'AFTER that, enable search mode with keyword is 123' + 'THEN search by keyword return no result' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = '123'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n' + 'AFTER that, enable search mode with keyword is 123' + 'THEN search by keyword return no result' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = '123'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n' + 'AFTER that, enable search mode' + 'THEN search by keyword return contacts with keyword a' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = 'a'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right( + GetPresentationContactsSuccess( + contacts: tomContacts, + keyword: searchKeyword, + ), + ), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.first.displayName, + 'Alice test', + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value + .getSuccessOrNull() is GetPresentationContactsSuccess, + true, + ); + + final presentationContact = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull() as GetPresentationContactsSuccess; + + expect(presentationContact.keyword, keyword); + + expect(presentationContact.contacts.isNotEmpty, true); + + expect(presentationContact.contacts.length, 5); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsInitial.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n' + 'AFTER that, enable search mode' + '[Search 1] search by keyword return contacts with keyword A' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n' + '[Search 2] search by keyword return contacts with keyword Alice test' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsInitial state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeywordFirst = 'A'; + const searchKeywordSecond = 'Alice test'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeywordFirst; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right( + GetPresentationContactsSuccess( + contacts: tomContacts, + keyword: searchKeywordFirst, + ), + ), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeywordFirst, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeywordFirst); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.first.displayName, + 'Alice test', + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value + .getSuccessOrNull() is GetPresentationContactsSuccess, + true, + ); + + final presentationContact = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull() as GetPresentationContactsSuccess; + + expect(presentationContact.keyword, keyword); + + expect(presentationContact.contacts.isNotEmpty, true); + + expect(presentationContact.contacts.length, 5); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeywordSecond; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + const Right( + GetPresentationContactsSuccess( + contacts: [], + keyword: searchKeywordSecond, + ), + ), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Right(GetPhonebookContactsInitial())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeywordSecond, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeywordSecond); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.first.displayName, + 'Alice test', + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value + .getSuccessOrNull() is GetPresentationContactsSuccess, + true, + ); + + final presentationContactTwo = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull() as GetPresentationContactsSuccess; + + expect(presentationContactTwo.keyword, searchKeywordSecond); + + expect(presentationContactTwo.contacts.isNotEmpty, false); + + expect(presentationContactTwo.contacts.length, 0); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Right(GetPhonebookContactsInitial()), + ); + }, + ); + }); + + group('Test ContactsViewControllerMixin on Mobile', () { + test( + 'WHEN it is available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsIsEmpty.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value, + [], + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + }, + ); + + test( + 'WHEN it is available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsIsEmpty.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn(ValueNotifierCustom(const Left(GetContactsIsEmpty()))); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + }, + ); + + test( + 'WHEN it is available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsSuccess.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsSuccess state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + final presentationContact = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull(); + + expect( + presentationContact is GetContactsSuccess, + true, + ); + + expect( + (presentationContact as GetContactsSuccess).contacts.isNotEmpty, + true, + ); + + final presentationPhonebookContact = mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value + .getSuccessOrNull(); + + expect( + presentationPhonebookContact is GetPhonebookContactsSuccess, + true, + ); + + expect( + (presentationPhonebookContact as GetPhonebookContactsSuccess) + .contacts + .isNotEmpty, + true, + ); + }, + ); + + test( + 'WHEN it is available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is empty.\n' + 'AND presentationContactNotifier return GetContactsFailure' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsFailure.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsFailure state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsFailure state.\n', + () { + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + const Left(GetContactsFailure(keyword: '', exception: dynamic)), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom( + const Left(GetPhonebookContactsFailure(exception: dynamic)), + ), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsFailure(keyword: '', exception: dynamic)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsFailure(exception: dynamic)), + ); + }, + ); + + test( + 'WHEN it is available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsIsEmpty.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n' + 'AFTER that, enable search mode with keyword is 123' + 'THEN search by keyword return no result' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = '123'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value, + [], + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value, + [], + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + }, + ); + + test( + 'WHEN it is available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsIsEmpty' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsIsEmpty.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n' + 'AFTER that, enable search mode with keyword is 123' + 'THEN search by keyword return no result' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = '123'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsSuccess.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n' + 'AFTER that, enable search mode with keyword is 123' + 'THEN search by keyword return no result' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = '123'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom([])); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetContactsIsEmpty())), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + const Left(GetContactsIsEmpty()), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsSuccess.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsSuccess state.\n' + 'AFTER that, enable search mode' + 'THEN search by keyword return contacts with keyword a' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeyword = 'a'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeyword; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right( + GetPresentationContactsSuccess( + contacts: tomContacts, + keyword: searchKeyword, + ), + ), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeyword, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeyword); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.first.displayName, + 'Alice test', + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value + .getSuccessOrNull() is GetPresentationContactsSuccess, + true, + ); + + final presentationContact = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull() as GetPresentationContactsSuccess; + + expect(presentationContact.keyword, keyword); + + expect(presentationContact.contacts.isNotEmpty, true); + + expect(presentationContact.contacts.length, 5); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + }, + ); + + test( + 'WHEN it is not available get Phonebook contact.\n' + 'AND search mode is disable.\n' + 'AND presentationRecentContactNotifier return SearchRecentChatSuccess with contacts is not empty.\n' + 'AND presentationContactNotifier return GetContactsSuccess' + 'AND presentationPhonebookContactNotifier return GetPhonebookContactsSuccess.\n' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsSuccess state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsSuccess state.\n' + 'AFTER that, enable search mode' + '[Search 1] search by keyword return contacts with keyword A' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n' + '[Search 2] search by keyword return contacts with keyword Alice test' + 'THEN presentationRecentContactNotifier SHOULD have SearchRecentChatSuccess state.\n' + 'THEN presentationContactNotifier SHOULD have GetContactsIsEmpty state.\n' + 'THEN presentationPhonebookContactNotifier SHOULD have GetPhonebookContactsIsEmpty state.\n', + () async { + final Debouncer debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + const searchKeywordFirst = 'A'; + const searchKeywordSecond = 'Alice test'; + String? keyword; + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(false)); + + when( + mockContactsViewControllerMixin.textEditingController, + ).thenReturn(TextEditingController()); + + mockContactsViewControllerMixin.textEditingController.addListener(() { + debouncer.value = + mockContactsViewControllerMixin.textEditingController.text; + }); + + debouncer.values.listen((value) { + keyword = value; + }); + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom(Right(GetContactsSuccess(contacts: tomContacts))), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ), + ); + + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + verify( + mockContactsViewControllerMixin.initialFetchContacts( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value, + Right(GetContactsSuccess(contacts: tomContacts)), + ); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + Right(GetPhonebookContactsSuccess(contacts: phonebookContacts)), + ); + + when( + mockContactsViewControllerMixin.isSearchModeNotifier, + ).thenReturn(ValueNotifier(true)); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeywordFirst; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + Right( + GetPresentationContactsSuccess( + contacts: tomContacts, + keyword: searchKeywordFirst, + ), + ), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeywordFirst, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeywordFirst); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.first.displayName, + 'Alice test', + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value + .getSuccessOrNull() is GetPresentationContactsSuccess, + true, + ); + + final presentationContact = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull() as GetPresentationContactsSuccess; + + expect(presentationContact.keyword, keyword); + + expect(presentationContact.contacts.isNotEmpty, true); + + expect(presentationContact.contacts.length, 5); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + + mockContactsViewControllerMixin.textEditingController.text = + searchKeywordSecond; + + when( + mockContactsViewControllerMixin.presentationRecentContactNotifier, + ).thenReturn(ValueNotifierCustom(recentContacts)); + + when( + mockContactsViewControllerMixin.presentationContactNotifier, + ).thenReturn( + ValueNotifierCustom( + const Right( + GetPresentationContactsSuccess( + contacts: [], + keyword: searchKeywordSecond, + ), + ), + ), + ); + + when( + mockContactsViewControllerMixin.presentationPhonebookContactNotifier, + ).thenReturn( + ValueNotifierCustom(const Left(GetPhonebookContactsIsEmpty())), + ); + + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ); + + await Future.delayed(const Duration(seconds: 1)); + + verify( + mockContactsViewControllerMixin.refreshAllContactsTest( + client: mockClient, + matrixLocalizations: mockMatrixLocalizations, + ), + ).called(1); + + expect( + mockContactsViewControllerMixin.isSearchModeNotifier.value, + true, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text.isEmpty, + false, + ); + + expect( + mockContactsViewControllerMixin.textEditingController.text, + searchKeywordSecond, + ); + + expect(keyword != null, true); + + expect(keyword, searchKeywordSecond); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.isNotEmpty, + true, + ); + + expect( + mockContactsViewControllerMixin + .presentationRecentContactNotifier.value.first.displayName, + 'Alice test', + ); + + expect( + mockContactsViewControllerMixin.presentationContactNotifier.value + .getSuccessOrNull() is GetPresentationContactsSuccess, + true, + ); + + final presentationContactTwo = mockContactsViewControllerMixin + .presentationContactNotifier.value + .getSuccessOrNull() as GetPresentationContactsSuccess; + + expect(presentationContactTwo.keyword, searchKeywordSecond); + + expect(presentationContactTwo.contacts.isNotEmpty, false); + + expect(presentationContactTwo.contacts.length, 0); + + expect( + mockContactsViewControllerMixin + .presentationPhonebookContactNotifier.value, + const Left(GetPhonebookContactsIsEmpty()), + ); + }, + ); + }); +} diff --git a/web/index.html b/web/index.html index ef24aed826..f196b8e096 100644 --- a/web/index.html +++ b/web/index.html @@ -41,7 +41,7 @@ @@ -79,7 +79,10 @@ diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 86dea8d600..fd7b1097da 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +42,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSaverPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterAvifWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterAvifWindowsPlugin")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); GalPluginCApiRegisterWithRegistrar( @@ -56,6 +60,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); ScreenBrightnessWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); SuperNativeExtensionsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f0f48fefd0..c03bed2529 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST emoji_picker_flutter file_saver file_selector_windows + flutter_avif_windows flutter_webrtc gal irondash_engine_context @@ -18,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST permission_handler_windows record_windows screen_brightness_windows + share_plus super_native_extensions url_launcher_windows window_to_front