diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 7aeb9fa4dc..cce35b990b 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/di/chat/chat_di.dart'; import 'package:fluffychat/di/contact/contact_di.dart'; +import 'package:fluffychat/di/search/search_di.dart'; import 'package:fluffychat/pages/add_story/add_story.dart'; import 'package:fluffychat/pages/archive/archive.dart'; import 'package:fluffychat/pages/chat/chat.dart'; @@ -34,6 +35,7 @@ import 'package:fluffychat/widgets/layouts/loading_view.dart'; import 'package:fluffychat/widgets/layouts/side_view_layout.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; import 'package:fluffychat/widgets/log_view.dart'; +import 'package:fluffychat/widgets/vwidget_with_dependencies.dart'; import 'package:fluffychat/widgets/vwidget_with_dependency.dart'; import 'package:flutter/material.dart'; import 'package:vrouter/vrouter.dart'; @@ -173,11 +175,11 @@ class AppRoutes { ), ] ), - VWidgetWithDependency( - di: ContactDI(), - path: '/search', - widget: const Search(), - buildTransition: rightToLeftTransition, + VWidgetWithDependencies( + dies: [SearchDI()], + path: '/search', + widget: const Search(), + buildTransition: rightToLeftTransition, ), ], ) @@ -264,11 +266,11 @@ class AppRoutes { ), ], ), - VWidgetWithDependency( - di: ContactDI(), - path: '/search', - widget: const Search(), - buildTransition: rightToLeftTransition + VWidgetWithDependencies( + dies: [SearchDI()], + path: '/search', + widget: const Search(), + buildTransition: rightToLeftTransition ), ], ), diff --git a/lib/di/search/search_di.dart b/lib/di/search/search_di.dart new file mode 100644 index 0000000000..ac3107113b --- /dev/null +++ b/lib/di/search/search_di.dart @@ -0,0 +1,36 @@ +import 'package:fluffychat/data/datasource/tom_contacts_datasource.dart'; +import 'package:fluffychat/data/datasource_impl/contact/tom_contacts_datasource_impl.dart'; +import 'package:fluffychat/data/network/contact/tom_contact_api.dart'; +import 'package:fluffychat/data/repository/contact/tom_contact_repository_impl.dart'; +import 'package:fluffychat/di/base_di.dart'; +import 'package:fluffychat/domain/repository/contact_repository.dart'; +import 'package:fluffychat/domain/usecase/fetch_contacts_interactor.dart'; +import 'package:fluffychat/domain/usecase/lookup_contacts_interactor.dart'; +import 'package:fluffychat/domain/usecase/search/search_interactor.dart'; +import 'package:get_it/get_it.dart'; +import 'package:matrix/matrix.dart'; + +class SearchDI extends BaseDI { + + @override + String get scopeName => 'Search'; + + @override + void setUp(GetIt get) { + Logs().d('SearchDI::setUp()'); + + get.registerSingleton(TomContactAPI()); + + get.registerSingleton(TomContactsDatasourceImpl()); + + get.registerSingleton(TomContactRepositoryImpl()); + + get.registerSingleton(LookupContactsInteractor()); + + get.registerSingleton(FetchContactsInteractor()); + + get.registerSingleton(SearchContactsAndRecentChatInteractor()); + + Logs().d('SearchDI::setUp() - done'); + } +} \ No newline at end of file diff --git a/lib/domain/app_state/search/search_interactor_state.dart b/lib/domain/app_state/search/search_interactor_state.dart new file mode 100644 index 0000000000..904abcba8d --- /dev/null +++ b/lib/domain/app_state/search/search_interactor_state.dart @@ -0,0 +1,23 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/domain/model/search/search_model.dart'; + +class GetContactAndRecentChatSuccess extends Success { + final List search; + + const GetContactAndRecentChatSuccess({required this.search}); + + @override + List get props => [search]; +} + +class GetContactAndRecentChatFailed extends Failure { + + final dynamic exception; + + const GetContactAndRecentChatFailed({required this.exception}); + + @override + List get props => [exception]; + +} \ No newline at end of file diff --git a/lib/domain/model/extensions/contact/contact_extension.dart b/lib/domain/model/extensions/contact/contact_extension.dart index c63a735204..82c1dd3753 100644 --- a/lib/domain/model/extensions/contact/contact_extension.dart +++ b/lib/domain/model/extensions/contact/contact_extension.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/domain/model/contact/contact.dart'; +import 'package:fluffychat/domain/model/search/search_model.dart'; import 'package:fluffychat/presentation/model/presentation_contact.dart'; extension ContactExtension on Contact { @@ -21,4 +22,22 @@ extension ContactExtension on Contact { return listContacts; } + + Set toSearch() { + final listContacts = emails?.map((email) => SearchModel( + email: email, + displayName: displayName, + matrixId: matrixId, + )).toSet() ?? {}; + + if (emails == null || emails!.isEmpty) { + listContacts.add(SearchModel( + email: null, + displayName: displayName, + matrixId: matrixId, + )); + } + + return listContacts; + } } \ No newline at end of file diff --git a/lib/domain/model/extensions/search/search_extension.dart b/lib/domain/model/extensions/search/search_extension.dart new file mode 100644 index 0000000000..c10678f47c --- /dev/null +++ b/lib/domain/model/extensions/search/search_extension.dart @@ -0,0 +1,15 @@ +import 'package:fluffychat/domain/model/search/search_model.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; + +extension SearchExtension on SearchModel { + PresentationSearch toPresentationSearch() { + return PresentationSearch( + email: email, + displayName: displayName, + matrixId: matrixId, + searchElementTypeEnum: searchElementTypeEnum, + roomSummary: roomSummary, + directChatMatrixID: directChatMatrixID + ); + } +} \ No newline at end of file diff --git a/lib/domain/model/extensions/search/search_list_extension.dart b/lib/domain/model/extensions/search/search_list_extension.dart new file mode 100644 index 0000000000..2274af8f28 --- /dev/null +++ b/lib/domain/model/extensions/search/search_list_extension.dart @@ -0,0 +1,9 @@ +import 'package:fluffychat/domain/model/extensions/search/search_extension.dart'; +import 'package:fluffychat/domain/model/search/search_model.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; + +extension SearchListExtenstion on List { + List toPresentationSearch() { + return map((search) => search.toPresentationSearch()).toList(); + } +} \ No newline at end of file diff --git a/lib/domain/model/room/room_extension.dart b/lib/domain/model/room/room_extension.dart new file mode 100644 index 0000000000..2b67854432 --- /dev/null +++ b/lib/domain/model/room/room_extension.dart @@ -0,0 +1,40 @@ + +import 'package:fluffychat/domain/model/search/search_model.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; +import 'package:matrix/matrix.dart'; + +extension RoomExtension on Room { + SearchModel toSearch(MatrixLocalizations matrixLocalizations) { + return SearchModel( + displayName: getLocalizedDisplayname(matrixLocalizations), + roomSummary: summary, + directChatMatrixID: directChatMatrixID, + matrixId: id, + searchElementTypeEnum: SearchElementTypeEnum.recentChat + ); + } + + bool isNotSpaceAndStoryRoom() { + return !isSpace && !isStoryRoom; + } + + bool isShowInChatList() { + return _isDirectChatHaveMessage() || _isGroupChat(); + } + + bool _isGroupChat() { + return !isDirectChat; + } + + bool _isDirectChatHaveMessage() { + return isDirectChat && _isLastEventInRoomIsMessage(); + } + + bool _isLastEventInRoomIsMessage() { + return [ + EventTypes.Message, + EventTypes.Sticker, + EventTypes.Encrypted, + ].contains(lastEvent?.type); + } +} \ No newline at end of file diff --git a/lib/domain/model/room/room_list_extension.dart b/lib/domain/model/room/room_list_extension.dart new file mode 100644 index 0000000000..a545e75e19 --- /dev/null +++ b/lib/domain/model/room/room_list_extension.dart @@ -0,0 +1,9 @@ +import 'package:fluffychat/domain/model/room/room_extension.dart'; +import 'package:fluffychat/domain/model/search/search_model.dart'; +import 'package:matrix/matrix.dart'; + +extension RoomListExtension on List { + List toSearchList(MatrixLocalizations matrixLocalizations) { + return map((room) => room.toSearch(matrixLocalizations)).toList(); + } +} \ No newline at end of file diff --git a/lib/domain/model/search/search_model.dart b/lib/domain/model/search/search_model.dart new file mode 100644 index 0000000000..ceaba3d2b8 --- /dev/null +++ b/lib/domain/model/search/search_model.dart @@ -0,0 +1,41 @@ +import 'package:equatable/equatable.dart'; +import 'package:matrix/matrix.dart'; + +enum SearchElementTypeEnum { + contact, + recentChat, +} + +class SearchModel extends Equatable { + + final String? email; + + final String? displayName; + + final String? matrixId; + + final SearchElementTypeEnum? searchElementTypeEnum; + + final RoomSummary? roomSummary; + + final String? directChatMatrixID; + + const SearchModel({ + this.email, + this.displayName, + this.matrixId, + this.searchElementTypeEnum, + this.roomSummary, + this.directChatMatrixID + }); + + @override + List get props => [ + email, + displayName, + matrixId, + searchElementTypeEnum, + roomSummary, + directChatMatrixID + ]; +} \ No newline at end of file diff --git a/lib/domain/usecase/search/search_interactor.dart b/lib/domain/usecase/search/search_interactor.dart new file mode 100644 index 0000000000..5a6ccecf50 --- /dev/null +++ b/lib/domain/usecase/search/search_interactor.dart @@ -0,0 +1,89 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/search/search_interactor_state.dart'; +import 'package:fluffychat/domain/model/contact/contact_query.dart'; +import 'package:fluffychat/domain/model/extensions/contact/contact_extension.dart'; +import 'package:fluffychat/domain/model/room/room_extension.dart'; +import 'package:fluffychat/domain/model/room/room_list_extension.dart'; +import 'package:fluffychat/domain/model/search/search_model.dart'; +import 'package:fluffychat/domain/repository/contact_repository.dart'; +import 'package:matrix/matrix.dart'; + +class SearchContactsAndRecentChatInteractor { + final ContactRepository contactRepository = getIt.get(); + + SearchContactsAndRecentChatInteractor(); + + Stream> execute({ + required List rooms, + required MatrixLocalizations matrixLocalizations, + required String keyword, + int? limitContacts, + int? limitRecentChats, + }) async* { + try { + if (keyword.isNotEmpty) { + final recentChat = await _searchRecentChat(rooms: rooms, matrixLocalizations: matrixLocalizations, keyword: keyword); + final contacts = await contactRepository.searchContact(query: ContactQuery(keyword: keyword), limit: limitContacts); + + final presentationSearches = _comparePresentationSearches( + recentChat.toSearchList(matrixLocalizations), + contacts.expand((contact) => contact.toSearch()) + .where((contact) => _compareDisplayNameWithKeyword(contact, keyword)) + .toList() + ); + yield Right(GetContactAndRecentChatSuccess(search: presentationSearches)); + } else { + final recentChat = await _getRecentChat(rooms: rooms, limitRecentChats: limitRecentChats); + yield Right(GetContactAndRecentChatSuccess(search: recentChat.toSearchList(matrixLocalizations))); + } + } catch (e) { + yield Left(GetContactAndRecentChatFailed(exception: e)); + } + } + + bool _compareDisplayNameWithKeyword(SearchModel search, String keyword) { + if (search.displayName != null) { + return search.displayName!.toLowerCase().contains(keyword.toLowerCase()); + } else { + return false; + } + } + + Future> _getRecentChat({ + required List rooms, + int? limitRecentChats + }) async { + return rooms.where((room) => room.isNotSpaceAndStoryRoom()) + .where((room) => room.isShowInChatList()) + .take(limitRecentChats ?? 0) + .toList(); + } + + Future> _searchRecentChat({ + required List rooms, + required MatrixLocalizations matrixLocalizations, + required String keyword, + }) async { + return rooms.where((room) => room.isNotSpaceAndStoryRoom()) + .where((room) => + room.getLocalizedDisplayname(matrixLocalizations) + .toLowerCase() + .contains(keyword.toLowerCase()) + ).toList(); + } + + List _comparePresentationSearches(List recentChat, List contacts) { + final isDuplicateElement = contacts.where((contact) { + return recentChat.any((recentChat) => recentChat.directChatMatrixID == contact.directChatMatrixID); + }).toList(); + if (isDuplicateElement.isNotEmpty) { + final presentationSearches = recentChat + contacts; + presentationSearches.removeWhere((contact) => isDuplicateElement.contains(contact)); + return presentationSearches; + } else { + return recentChat + contacts; + } + } +} \ No newline at end of file diff --git a/lib/domain/usecase/send_file_interactor.dart b/lib/domain/usecase/send_file_interactor.dart index 5b40cfda32..41f0d195de 100644 --- a/lib/domain/usecase/send_file_interactor.dart +++ b/lib/domain/usecase/send_file_interactor.dart @@ -1,6 +1,6 @@ import 'package:file_picker/file_picker.dart'; -import 'package:fluffychat/presentation/extensions/room_extension.dart'; +import 'package:fluffychat/presentation/extensions/send_image_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/domain/usecase/send_image_interactor.dart b/lib/domain/usecase/send_image_interactor.dart index e6d738865e..2b195a22c4 100644 --- a/lib/domain/usecase/send_image_interactor.dart +++ b/lib/domain/usecase/send_image_interactor.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/presentation/extensions/asset_entity_extension.dart'; -import 'package:fluffychat/presentation/extensions/room_extension.dart'; +import 'package:fluffychat/presentation/extensions/send_image_extension.dart'; import 'package:matrix/matrix.dart'; import 'package:photo_manager/photo_manager.dart'; diff --git a/lib/domain/usecase/send_images_interactor.dart b/lib/domain/usecase/send_images_interactor.dart index 45acc582c2..7f289b7b1c 100644 --- a/lib/domain/usecase/send_images_interactor.dart +++ b/lib/domain/usecase/send_images_interactor.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/presentation/extensions/room_extension.dart'; +import 'package:fluffychat/presentation/extensions/send_image_extension.dart'; import 'package:matrix/matrix.dart'; import 'package:photo_manager/photo_manager.dart'; diff --git a/lib/mixin/comparable_presentation_search_mixin.dart b/lib/mixin/comparable_presentation_search_mixin.dart new file mode 100644 index 0000000000..b3b489953f --- /dev/null +++ b/lib/mixin/comparable_presentation_search_mixin.dart @@ -0,0 +1,25 @@ +import 'package:fluffychat/domain/model/search/search_model.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; + +class ComparablePresentationSearchMixin { + int comparePresentationSearch(PresentationSearch searchResultOne, PresentationSearch searchResultTwo) { + final bufferOne = StringBuffer(); + final bufferTwo = StringBuffer(); + + bufferOne.writeAll([ + searchResultOne.displayName ?? "", + searchResultOne.matrixId ?? "", + searchResultOne.email ?? "", + searchResultTwo.searchElementTypeEnum ?? SearchElementTypeEnum.contact + ]); + + bufferTwo.writeAll([ + searchResultTwo.displayName ?? "", + searchResultTwo.matrixId ?? "", + searchResultTwo.email ?? "", + searchResultTwo.searchElementTypeEnum ?? SearchElementTypeEnum.contact + ]); + + return bufferOne.toString().compareTo(bufferTwo.toString()); + } +} \ No newline at end of file diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 988e8de9c5..43784fb4d5 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/presentation/extensions/room_summary_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; @@ -37,8 +38,7 @@ class ChatDetailsView extends StatelessWidget { } controller.members!.removeWhere((u) => u.membership == Membership.leave); - final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) + - (room.summary.mJoinedMemberCount ?? 0); + final actualMembersCount = room.summary.actualMembersCount; final canRequestMoreMembers = controller.members!.length < actualMembersCount; final iconColor = Theme.of(context).textTheme.bodyLarge!.color; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index f278a76228..e2fe0058cc 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/domain/usecases/get_recovery_words_interactor.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_security/settings_security.dart'; +import 'package:fluffychat/presentation/extensions/send_image_extension.dart'; import 'package:fluffychat/utils/famedlysdk_store.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; @@ -135,7 +136,7 @@ class ChatListController extends State .client .rooms .where(getRoomFilterByActiveFilter(_activeFilterAllChats)) - .where(isShowInChatList) + .where((room) => room.isShowInChatList()) .toList(); bool isSearchMode = false; @@ -148,26 +149,6 @@ class ChatListController extends State bool isSearching = false; static const String _serverStoreNamespace = 'im.fluffychat.search.server'; - bool isShowInChatList(Room room) { - return isDirectChatHaveMessage(room) || isGroupChat(room); - } - - bool isGroupChat(Room room) { - return !room.isDirectChat; - } - - bool isDirectChatHaveMessage(Room room) { - return room.isDirectChat && isLastEventInRoomIsMessage(room); - } - - bool isLastEventInRoomIsMessage(Room room) { - return [ - EventTypes.Message, - EventTypes.Sticker, - EventTypes.Encrypted, - ].contains(room.lastEvent?.type); - } - void setServer() async { final newServer = await showTextInputDialog( useRootNavigator: false, diff --git a/lib/pages/new_group/new_group_chat_info.dart b/lib/pages/new_group/new_group_chat_info.dart index e01a20ab6f..d6e2808b4e 100644 --- a/lib/pages/new_group/new_group_chat_info.dart +++ b/lib/pages/new_group/new_group_chat_info.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:fluffychat/presentation/model/presentation_contact.dart'; import 'package:fluffychat/pages/new_group/new_group.dart'; import 'package:fluffychat/pages/new_group/new_group_info_controller.dart'; diff --git a/lib/pages/search/recent_contacts_banner_widget.dart b/lib/pages/search/recent_contacts_banner_widget.dart index 7f53809d01..98023b08e9 100644 --- a/lib/pages/search/recent_contacts_banner_widget.dart +++ b/lib/pages/search/recent_contacts_banner_widget.dart @@ -1,10 +1,5 @@ -import 'package:collection/collection.dart'; -import 'package:dartz/dartz.dart'; -import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/domain/app_state/contact/get_contacts_success.dart'; import 'package:fluffychat/pages/search/recent_contacts_banner_widget_style.dart'; import 'package:fluffychat/pages/search/search.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; import 'package:fluffychat/utils/display_name_widget.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:flutter/material.dart'; @@ -16,75 +11,59 @@ class RecentContactsBannerWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return StreamBuilder>( - stream: searchController.contactsStreamController.stream, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator(strokeWidth: 2)); - } - - if (snapshot.hasError || snapshot.data!.isLeft()) { - return const SizedBox(); - } - - final contactsList = searchController.fetchContactsController.getContactsFromFetchStream(snapshot.data!); - - final contactsListSorted = contactsList.sorted((pre, next) => searchController.comparePresentationContacts(pre, next)); - - if (contactsListSorted.isEmpty) { - return const SizedBox(); - } - - return ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - scrollDirection: Axis.horizontal, - itemCount: contactsListSorted.length, - itemBuilder: (context, index) { - return ChatRecentContactItemWidget( - contact: contactsListSorted[index], - searchController: searchController, - ); - }, - ); - }, - ); + final contactsList = searchController.getContactsFromRecentChat(); + if (contactsList.isEmpty) { + return const SizedBox.shrink(); + } else { + return ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: contactsList.length, + itemBuilder: (context, index) { + return ChatRecentContactItemWidget( + user: contactsList[index], + searchController: searchController, + ); + }, + ); + } } } class ChatRecentContactItemWidget extends StatelessWidget { final SearchController searchController; - final PresentationContact contact; - const ChatRecentContactItemWidget({super.key, required this.contact, required this.searchController}); + final User user; + const ChatRecentContactItemWidget({super.key, required this.user, required this.searchController}); @override Widget build(BuildContext context) { - return FutureBuilder( - future: searchController.getProfile(context, contact), - builder: (context, snapshot) { - return SizedBox( - width: RecentContactsBannerWidgetStyle.chatRecentContactItemWidth, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: RecentContactsBannerWidgetStyle.avatarWidthSize, - child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: contact.displayName, - ), + return InkWell( + onTap: () { + searchController.goToChatScreenFormRecentChat(user); + }, + child: SizedBox( + width: RecentContactsBannerWidgetStyle.chatRecentContactItemWidth, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: RecentContactsBannerWidgetStyle.avatarWidthSize, + height: RecentContactsBannerWidgetStyle.avatarWidthSize, + child: Avatar( + mxContent: user.avatarUrl, + name: user.displayName ?? "", ), - Padding( - padding: RecentContactsBannerWidgetStyle.chatRecentContactItemPadding, - child: BuildDisplayName( - profileDisplayName: snapshot.data?.displayname, - contactDisplayName: contact.displayName - ), + ), + Padding( + padding: RecentContactsBannerWidgetStyle.chatRecentContactItemPadding, + child: BuildDisplayName( + profileDisplayName: user.displayName ?? "", ), - ], - ), - ); - }, + ), + ], + ), + ), ); } } diff --git a/lib/pages/search/recent_item_widget.dart b/lib/pages/search/recent_item_widget.dart index b5b8a4fc91..2c456f61ec 100644 --- a/lib/pages/search/recent_item_widget.dart +++ b/lib/pages/search/recent_item_widget.dart @@ -1,30 +1,28 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/search/recent_item_widget_style.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/pages/search/search.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:fluffychat/presentation/extensions/room_summary_extension.dart'; import 'package:matrix/matrix.dart'; class RecentItemWidget extends StatelessWidget { - final Room room; + final PresentationSearch presentationSearch; + final SearchController searchController; final void Function()? onTap; - const RecentItemWidget( - this.room, - { - this.onTap, - Key? key, - } - ) : super(key: key); + const RecentItemWidget({ + required this.presentationSearch, + required this.searchController, + this.onTap, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { - final displayName = room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ); - final directChatMatrixID = room.directChatMatrixID; return Material( borderRadius: BorderRadius.circular(AppConfig.borderRadius), clipBehavior: Clip.hardEdge, @@ -39,15 +37,20 @@ class RecentItemWidget extends StatelessWidget { child: ListTile( contentPadding: EdgeInsets.zero, title: Row( - crossAxisAlignment: directChatMatrixID != null + crossAxisAlignment: presentationSearch.isContact ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ SizedBox( width: RecentItemStyle.avatarSize, - child: Avatar( - mxContent: room.avatar, - name: displayName, + child: FutureBuilder( + future: searchController.getProfile(context, presentationSearch), + builder: (context, snapshot) { + return Avatar( + mxContent: snapshot.data?.avatarUrl, + name: presentationSearch.displayName, + ); + } ), ), const SizedBox(width: 8), @@ -56,7 +59,7 @@ class RecentItemWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - displayName, + presentationSearch.displayName ?? "", overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, @@ -68,7 +71,7 @@ class RecentItemWidget extends StatelessWidget { ), ), ), - _buildInformationWidget(context, directChatMatrixID: directChatMatrixID) + _buildInformationWidget(context), ], ), ), @@ -81,28 +84,26 @@ class RecentItemWidget extends StatelessWidget { ); } - Widget _buildInformationWidget( - BuildContext context, - { - String? directChatMatrixID, - } - ) { - if (directChatMatrixID == null) { - return _GroupChatInformation(room: room); + Widget _buildInformationWidget(BuildContext context) { + if (presentationSearch.isContact) { + return _ContactInformation(presentationSearch: presentationSearch); } else { - return _DirectChatInformation(room: room); + if (presentationSearch.directChatMatrixID == null) { + return _GroupChatInformation(presentationSearch: presentationSearch); + } else { + return _DirectChatInformation(presentationSearch: presentationSearch); + } } } } class _GroupChatInformation extends StatelessWidget { - final Room room; - const _GroupChatInformation({required this.room}); + final PresentationSearch presentationSearch; + const _GroupChatInformation({required this.presentationSearch}); @override Widget build(BuildContext context) { - final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) + - (room.summary.mJoinedMemberCount ?? 0); + final actualMembersCount = presentationSearch.roomSummary?.actualMembersCount ?? 0; return Text( L10n.of(context)!.membersCount(actualMembersCount), overflow: TextOverflow.ellipsis, @@ -121,16 +122,39 @@ class _GroupChatInformation extends StatelessWidget { class _DirectChatInformation extends StatelessWidget { - final Room room; - const _DirectChatInformation({required this.room}); + final PresentationSearch presentationSearch; + const _DirectChatInformation({required this.presentationSearch}); + + @override + Widget build(BuildContext context) { + return Text( + presentationSearch.roomSummary?.mHeroes?.first ?? "", + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: Theme.of(context).textTheme.bodyMedium?.merge( + TextStyle( + overflow: TextOverflow.ellipsis, + letterSpacing: 0.15, + color: LinagoraRefColors.material().tertiary[30], + ), + ), + ); + } +} + +class _ContactInformation extends StatelessWidget { + final PresentationSearch presentationSearch; + const _ContactInformation({required this.presentationSearch}); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - room.summary.mHeroes?.first ?? "", + if (presentationSearch.email != null) + Text( + presentationSearch.email ?? "", overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, @@ -142,8 +166,9 @@ class _DirectChatInformation extends StatelessWidget { ), ), ), - Text( - room.name, + if (presentationSearch.directChatMatrixID != null) + Text( + presentationSearch.directChatMatrixID ?? "", overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, @@ -154,7 +179,7 @@ class _DirectChatInformation extends StatelessWidget { color: LinagoraRefColors.material().tertiary[30], ), ), - ) + ), ], ); } diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 0c0d85a779..26be5e495b 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -1,16 +1,26 @@ +import 'dart:async'; + import 'package:dartz/dartz.dart' hide State; import 'package:fluffychat/app_state/failure.dart'; -import 'package:fluffychat/domain/app_state/contact/get_contacts_success.dart'; -import 'package:fluffychat/mixin/comparable_presentation_contact_mixin.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/search/search_interactor_state.dart'; +import 'package:fluffychat/domain/model/extensions/search/search_list_extension.dart'; +import 'package:fluffychat/domain/usecase/search/search_interactor.dart'; +import 'package:fluffychat/mixin/comparable_presentation_search_mixin.dart'; +import 'package:fluffychat/pages/search/search_controller.dart'; import 'package:fluffychat/pages/search/search_view.dart'; -import 'package:fluffychat/pages/new_private_chat/fetch_contacts_controller.dart'; -import 'package:fluffychat/presentation/model/presentation_contact.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; +import 'package:fluffychat/presentation/mixin/load_more_search_mixin.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/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:rxdart/rxdart.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:vrouter/vrouter.dart'; class Search extends StatefulWidget { const Search({super.key}); @@ -19,35 +29,24 @@ class Search extends StatefulWidget { State createState() => SearchController(); } -class SearchController extends State with ComparablePresentationContactMixin { - - static const int limitRecentChats = 3; - static const int limitRecentContacts = 7; +class SearchController extends State with ComparablePresentationSearchMixin, LoadMoreSearchMixin { - AutoScrollController recentChatsController = AutoScrollController(); - ScrollController customScrollController = ScrollController(); - final fetchContactsController = FetchContactsController(); - final contactsStreamController = BehaviorSubject>(); + static const int limitPrefetchedRecentChats = 3; + static const int limitSearchingPrefetchedRecentContacts = 30; + static const int limitPrefetchedRecentContacts = 7; - isFilteredRecentChat(Room room) { - return !room.isSpace && room.isDirectChat && !room.isStoryRoom; - } + bool isSearching = false; + bool isSearchMode = false; + ValueNotifier? isSearchModeNotifier; - List get filteredRoomsForAll => Matrix.of(context) - .client - .rooms - .where((room) => !room.isSpace && !room.isStoryRoom) - .take(limitRecentChats) - .toList(); + SearchContactAndRecentChatController? searchContactAndRecentChatController; + final AutoScrollController recentChatsController = AutoScrollController(); + final ScrollController customScrollController = ScrollController(); + final _searchContactsAndRecentChatInteractor = getIt.get(); + final getContactAndRecentChatStream = StreamController>(); + final contactsAndRecentChatStreamController = BehaviorSubject>(); - void listenContactsStartList() { - fetchContactsController.streamController.stream.listen((event) { - Logs().d('SearchController::fetchContacts() - event: $event'); - contactsStreamController.add(event); - }); - } - - Future getProfile(BuildContext context, PresentationContact contact) async { + Future getProfile(BuildContext context, PresentationSearch contact) async { final client = Matrix.of(context).client; if (contact.matrixId == null) { return Future.error(Exception("MatrixId is null")); @@ -61,19 +60,127 @@ class SearchController extends State with ComparablePresentationContact } } + + void listenContactsStartList() { + getContactAndRecentChatStream.stream.listen((event) { + Logs().d('SearchController::getContactAndRecentChatStream() - event: $event'); + contactsAndRecentChatStreamController.add(event); + }); + } + + void _getContactAndRecentChat() { + _searchContactsAndRecentChatInteractor.execute( + rooms: Matrix.of(context).client.rooms, + matrixLocalizations: MatrixLocals(L10n.of(context)!), + keyword: '', + limitRecentChats: limitPrefetchedRecentChats, + limitContacts: limitSearchingPrefetchedRecentContacts, + ).listen((event) { + getContactAndRecentChatStream.add(event); + }); + } + + List getContactsFromRecentChat() { + final recentRooms = Matrix.of(context).client.rooms; + final List recentChatListPresentationSearch = []; + + for (final room in recentRooms) { + final users = room.getParticipants() + .where((user) => user.membership.isInvite == true && user.displayName != null) + .toSet() + .toList(); + + for (final user in users) { + final isDuplicateUser = recentChatListPresentationSearch + .any((existingUser) => existingUser.displayName == user.displayName); + + if (!isDuplicateUser) { + recentChatListPresentationSearch.add(user); + } + + if (recentChatListPresentationSearch.length == limitPrefetchedRecentContacts) { + break; // Stop getting participants after 7 or more have been added + } + } + + if (recentChatListPresentationSearch.length == limitPrefetchedRecentContacts) { + break; // Stop getting participants after 7 or more have been added + } + } + + Logs().d('SearchController::getContactsFromRecentChat() - event: $recentChatListPresentationSearch'); + + return recentChatListPresentationSearch; + } + + List getContactsAndRecentChatStream(Either event) { + return event.fold>( + (failure) => [], + (success) { + final contactsAndRecentChat = success.search.toPresentationSearch(); + updateLastContactAndRecentChatIndex(contactsAndRecentChat.length); + return contactsAndRecentChat; + }, + ).toList(); + } + + void listenSearchContactAndRecentChat() { + searchContactAndRecentChatController?.getContactAndRecentChatStream.stream.listen((event) { + Logs().d('SearchController::getContactAndRecentChatStream() - event: $event'); + getContactAndRecentChatStream.add(event); + }); + } + + void goToChatScreen(PresentationSearch presentationSearch) { + if (presentationSearch.isContact) { + showFutureLoadingDialog( + context: context, + future: () async { + if (presentationSearch.directChatMatrixID != null && presentationSearch.directChatMatrixID!.isNotEmpty) { + final roomId = await Matrix.of(context).client.startDirectChat(presentationSearch.directChatMatrixID!); + VRouter.of(context).toSegments(['rooms', roomId]); + } + }, + ); + } else { + if (presentationSearch.matrixId != null) { + VRouter.of(context).toSegments(['rooms', presentationSearch.matrixId!]); + } + } + } + + void goToChatScreenFormRecentChat(User user) async { + Logs().d('SearchController::getContactAndRecentChatStream() - event: $user'); + final roomIdResult = await showFutureLoadingDialog( + context: context, + future: () => user.startDirectChat(), + ); + if (roomIdResult.error != null) return; + VRouter.of(context).toSegments(['rooms', roomIdResult.result!]); + } + @override void initState() { + searchContactAndRecentChatController = SearchContactAndRecentChatController(context); + isSearchModeNotifier = ValueNotifier(false); listenContactsStartList(); + listenSearchContactAndRecentChat(); super.initState(); - fetchContactsController.fetchCurrentTomContacts(limit: limitRecentContacts); + SchedulerBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + searchContactAndRecentChatController?.init(); + _getContactAndRecentChat(); + } + }); } @override void dispose() { recentChatsController.dispose(); customScrollController.dispose(); - fetchContactsController.dispose(); - contactsStreamController.close(); + getContactAndRecentChatStream.close(); + contactsAndRecentChatStreamController.close(); + searchContactAndRecentChatController?.dispose(); super.dispose(); } diff --git a/lib/pages/search/search_controller.dart b/lib/pages/search/search_controller.dart new file mode 100644 index 0000000000..b50cfd8cac --- /dev/null +++ b/lib/pages/search/search_controller.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:dartz/dartz.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/search/search_interactor_state.dart'; +import 'package:fluffychat/domain/usecase/search/search_interactor.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class SearchContactAndRecentChatController { + final BuildContext context; + SearchContactAndRecentChatController(this.context); + static const int limitPrefetchedRecentChats = 3; + static const debouncerIntervalInMilliseconds = 300; + final SearchContactsAndRecentChatInteractor _searchContactsAndRecentChatInteractor = getIt.get(); + final TextEditingController textEditingController = TextEditingController(); + StreamController> getContactAndRecentChatStream = StreamController(); + void Function(String)? onSearchKeywordChanged; + Debouncer? _debouncer; + String searchKeyword = ""; + final isSearchModeNotifier = ValueNotifier(false); + + void init() { + _initializeDebouncer(); + textEditingController.addListener(() { + isSearchModeNotifier.value = textEditingController.text.isNotEmpty; + onSearchBarChanged(textEditingController.text); + }); + } + + void _initializeDebouncer() { + _debouncer = Debouncer( + const Duration(milliseconds: debouncerIntervalInMilliseconds), + initialValue: '', + ); + + _debouncer?.values.listen((keyword) async { + Logs().d("SearchContactAndRecentChatController::_initializeDebouncer: searchKeyword: $searchKeyword"); + searchKeyword = keyword; + if (onSearchKeywordChanged != null) { + onSearchKeywordChanged!(textEditingController.text); + } + final enableSearch = searchKeyword.isNotEmpty && searchKeyword != ''; + fetchLookupContacts( + enableSearch: enableSearch, + limitRecentChats: !enableSearch ? limitPrefetchedRecentChats : null + ); + }); + } + + void fetchLookupContacts({ + int? limitContacts, + int? limitRecentChats, + bool enableSearch = false, + }) { + _searchContactsAndRecentChatInteractor.execute( + keyword: searchKeyword, + matrixLocalizations: MatrixLocals(L10n.of(context)!), + rooms: Matrix.of(context).client.rooms, + limitContacts: limitContacts, + limitRecentChats: limitRecentChats, + ).listen((event) { + getContactAndRecentChatStream.add(event); + }); + } + + void onSearchBarChanged(String keyword) { + _debouncer?.setValue(keyword); + searchKeyword = keyword; + } + + void onCloseSearchTapped() { + textEditingController.clear(); + } + + void clearSearchBar() { + textEditingController.clear(); + } + + void dispose() { + _debouncer?.cancel(); + textEditingController.dispose(); + getContactAndRecentChatStream.close(); + } +} \ No newline at end of file diff --git a/lib/pages/search/search_view.dart b/lib/pages/search/search_view.dart index 2c1c35690b..7037b01386 100644 --- a/lib/pages/search/search_view.dart +++ b/lib/pages/search/search_view.dart @@ -1,3 +1,6 @@ +import 'package:dartz/dartz.dart' hide State; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/domain/app_state/search/search_interactor_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'; @@ -6,6 +9,7 @@ import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:matrix/matrix.dart'; import 'package:vrouter/vrouter.dart'; class SearchView extends StatefulWidget { @@ -70,26 +74,44 @@ class _SearchViewState extends State { } Widget _recentChatsWidget() { - final rooms = widget.searchController.filteredRoomsForAll; - if (rooms.isEmpty) { - const SizedBox(); - } else { - return ListView.builder( - padding: SearchViewStyle.paddingRecentChats, - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - controller: widget.searchController.recentChatsController, - itemCount: rooms.length, - itemBuilder: (BuildContext context, int i) { - return RecentItemWidget( - rooms[i], - key: Key('chat_recent_${rooms[i].id}'), - onTap: () {}, + return StreamBuilder>( + stream: widget.searchController.contactsAndRecentChatStreamController.stream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator(strokeWidth: 2)); + } + + if (snapshot.hasError || snapshot.data!.isLeft()) { + return const SizedBox(); + } + + final contactsList = widget.searchController.getContactsAndRecentChatStream(snapshot.data!); + + if (widget.searchController.isSearchMode) { + contactsList.sort((pre, cur) => widget.searchController.comparePresentationSearch(pre, cur)); + } + + Logs().d("SearchView:_recentChatsWidget(): --- contactsListSorted $contactsList"); + + return ListView.builder( + padding: SearchViewStyle.paddingRecentChats, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + controller: widget.searchController.recentChatsController, + itemCount: contactsList.length, + itemBuilder: (BuildContext context, int i) { + return RecentItemWidget( + searchController: widget.searchController, + presentationSearch: contactsList[i], + key: Key('chat_recent_${contactsList[i].matrixId}'), + onTap: () { + widget.searchController.goToChatScreen(contactsList[i]); + }, + ); + }, ); - }, - ); - } - return const SizedBox(); + } + ); } @@ -114,10 +136,9 @@ class _SearchViewState extends State { const SizedBox(width: 4.0), Expanded( child: TextField( - // controller: controller.searchChatController, + controller: widget.searchController.searchContactAndRecentChatController?.textEditingController, textInputAction: TextInputAction.search, - onChanged: (value) {}, - enabled: false, + enabled: true, decoration: InputDecoration( filled: true, contentPadding: SearchViewStyle.contentPaddingAppBar, diff --git a/lib/domain/model/extensions/contact/presentation_contact_extension.dart b/lib/presentation/extensions/contact/presentation_contact_extension.dart similarity index 87% rename from lib/domain/model/extensions/contact/presentation_contact_extension.dart rename to lib/presentation/extensions/contact/presentation_contact_extension.dart index 3dcad07780..666a57befa 100644 --- a/lib/domain/model/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/search/presentation_search.dart'; extension PresentaionContactExtension on PresentationContact { diff --git a/lib/presentation/extensions/contact/presentation_search_extension.dart b/lib/presentation/extensions/contact/presentation_search_extension.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/presentation/extensions/room_summary_extension.dart b/lib/presentation/extensions/room_summary_extension.dart new file mode 100644 index 0000000000..7aa026c210 --- /dev/null +++ b/lib/presentation/extensions/room_summary_extension.dart @@ -0,0 +1,6 @@ + +import 'package:matrix/matrix.dart'; + +extension RoomSummaryExtension on RoomSummary { + int get actualMembersCount => (mInvitedMemberCount ?? 0) + (mJoinedMemberCount ?? 0); +} \ No newline at end of file diff --git a/lib/presentation/extensions/room_extension.dart b/lib/presentation/extensions/send_image_extension.dart similarity index 94% rename from lib/presentation/extensions/room_extension.dart rename to lib/presentation/extensions/send_image_extension.dart index 43ea1c481f..3eeb2c9020 100644 --- a/lib/presentation/extensions/room_extension.dart +++ b/lib/presentation/extensions/send_image_extension.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:dartz/dartz.dart' hide id; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/extensions/asset_entity_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:matrix/matrix.dart'; import 'package:matrix/src/utils/file_send_request_credentials.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -11,7 +12,7 @@ import 'package:photo_manager/photo_manager.dart'; typedef TransactionId = String; typedef FakeImageEvent = SyncUpdate; -extension SendImage on Room { +extension SendImageExtension on Room { static const maxImagesCacheInRoom = 10; @@ -275,4 +276,24 @@ extension SendImage on Room { User? getUser(mxId) { return getParticipants().firstWhereOrNull((user) => user.id == mxId); } + + bool isShowInChatList() { + return _isDirectChatHaveMessage() || _isGroupChat(); + } + + bool _isGroupChat() { + return !isDirectChat; + } + + bool _isDirectChatHaveMessage() { + return isDirectChat && _isLastEventInRoomIsMessage(); + } + + bool _isLastEventInRoomIsMessage() { + return [ + EventTypes.Message, + EventTypes.Sticker, + EventTypes.Encrypted, + ].contains(lastEvent?.type); + } } \ No newline at end of file diff --git a/lib/presentation/mixin/load_more_search_mixin.dart b/lib/presentation/mixin/load_more_search_mixin.dart new file mode 100644 index 0000000000..53fda0c8af --- /dev/null +++ b/lib/presentation/mixin/load_more_search_mixin.dart @@ -0,0 +1,30 @@ +import 'package:fluffychat/pages/new_private_chat/fetch_contacts_controller.dart'; +import 'package:fluffychat/presentation/model/search/presentation_search.dart'; +import 'package:flutter/material.dart'; + +mixin LoadMoreSearchMixin { + final scrollController = ScrollController(); + + final haveMoreCountactsNotifier = ValueNotifier(true); + + final lastContactIndexNotifier = ValueNotifier(0); + + List oldContactList = []; + List oldContactAndRecentChatList = []; + + void listenForScrollChanged({required FetchContactsController fetchContactsController}) { + scrollController.addListener(() {}); + } + + void updateLastContactIndex(int value) { + lastContactIndexNotifier.value = value; + } + + void updateLastContactAndRecentChatIndex(int value) { + lastContactIndexNotifier.value = value; + } + + bool get isLoadMoreAction { + return scrollController.offset >= scrollController.position.maxScrollExtent; + } +} \ No newline at end of file diff --git a/lib/presentation/model/search/presentation_search.dart b/lib/presentation/model/search/presentation_search.dart new file mode 100644 index 0000000000..a8605187cf --- /dev/null +++ b/lib/presentation/model/search/presentation_search.dart @@ -0,0 +1,39 @@ +import 'package:equatable/equatable.dart'; +import 'package:fluffychat/domain/model/search/search_model.dart'; +import 'package:matrix/matrix.dart'; + +class PresentationSearch extends Equatable { + + final String? email; + + final String? displayName; + + final String? matrixId; + + final SearchElementTypeEnum? searchElementTypeEnum; + + final RoomSummary? roomSummary; + + final String? directChatMatrixID; + + const PresentationSearch({ + this.email, + this.displayName, + this.matrixId, + this.searchElementTypeEnum, + this.roomSummary, + this.directChatMatrixID + }); + + bool get isContact => searchElementTypeEnum == SearchElementTypeEnum.contact; + + @override + List get props => [ + email, + displayName, + matrixId, + searchElementTypeEnum, + roomSummary, + directChatMatrixID + ]; +} \ No newline at end of file diff --git a/lib/widgets/pill.dart b/lib/widgets/pill.dart index c7b9a820ab..44c0d027af 100644 --- a/lib/widgets/pill.dart +++ b/lib/widgets/pill.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/presentation/extensions/room_extension.dart'; +import 'package:fluffychat/presentation/extensions/send_image_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; diff --git a/lib/widgets/vwidget_with_dependencies.dart b/lib/widgets/vwidget_with_dependencies.dart new file mode 100644 index 0000000000..f4c5ce7df6 --- /dev/null +++ b/lib/widgets/vwidget_with_dependencies.dart @@ -0,0 +1,153 @@ +import 'package:fluffychat/di/abstract_di.dart'; +import 'package:fluffychat/di/base_di.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:vrouter/vrouter.dart'; + + +class VWidgetWithDependencies extends VGuard { + + final String? path; + + /// A name for the route which will allow you to easily navigate to it + /// using [VRouter.of(context).pushNamed] + /// + /// Note that [name] should be unique w.r.t every [VRouteElement] + final String? name; + + /// Alternative paths that will be matched to this route + /// + /// Note that path is match first, then every aliases in order + final List aliases; + + /// The widget which will be displayed for this [VRouteElement] + final Widget widget; + + /// A LocalKey that will be given to the page which contains the given [widget] + /// + /// This key mostly controls the page animation. If a page remains the same but the key is changes, + /// the page gets animated + /// The key is by default the value of the current [path] (or [aliases]) with + /// the path parameters replaced + /// + /// Do provide a constant [key] if you don't want this page to animate even if [path] or + /// [aliases] path parameters change + final LocalKey? key; + + /// The duration of [VWidgetBase.buildTransition] + final Duration? transitionDuration; + + /// The reverse duration of [VWidgetBase.buildTransition] + final Duration? reverseTransitionDuration; + + /// Create a custom transition effect when coming to and + /// going to this route + /// This has the priority over [VRouter.buildTransition] + /// + /// Also see: + /// * [VRouter.buildTransition] for default transitions for all routes + final Widget Function(Animation animation, + Animation secondaryAnimation, Widget child)? buildTransition; + + /// Whether this page route is a full-screen dialog. + /// + /// In Material and Cupertino, being fullscreen has the effects of making the app bars + /// have a close button instead of a back button. On iOS, dialogs transitions animate + /// differently and are also not closeable with the back swipe gesture. + final bool fullscreenDialog; + + final List dies; + + final OnFinishedBind? onFinishedBind; + + @override + Future beforeEnter(VRedirector vRedirector) async { + super.beforeEnter(vRedirector); + Logs().d('VWidgetWithDependencies::beforeEnter()'); + if (beforeDI != null) { + beforeDI!.call(vRedirector); + } + for (final di in dies) { + Logs().d('VWidgetWithDependencies::di $di'); + di.bind(onFinishedBind: onFinishedBind); + } + } + + final Future Function(VRedirector vRedirector)? beforeDI; + + @override + Future beforeUpdate(VRedirector vRedirector) => + _beforeUpdate(vRedirector); + final Future Function(VRedirector vRedirector) _beforeUpdate; + + @override + Future beforeLeave(VRedirector vRedirector, + void Function(Map state) saveHistoryState) async { + super.beforeLeave(vRedirector, saveHistoryState); + await Future.forEach(dies, (di) async { + await di.unbind(); + }); + } + + @override + void afterEnter(BuildContext context, String? from, String to) => + _afterEnter(context, from, to); + final void Function(BuildContext context, String? from, String to) + _afterEnter; + + @override + void afterUpdate(BuildContext context, String? from, String to) => + _afterUpdate(context, from, to); + final void Function(BuildContext context, String? from, String to) + _afterUpdate; + + + VWidgetWithDependencies({ + required this.path, + required this.widget, + this.beforeDI, + required this.dies, + this.onFinishedBind, + super.beforeEnter, + super.beforeUpdate, + super.afterEnter, + super.afterUpdate, + super.stackedRoutes = const [], + this.key, + this.name, + this.aliases = const [], + this.transitionDuration, + this.reverseTransitionDuration, + this.buildTransition, + this.fullscreenDialog = false, + }): _beforeUpdate = beforeUpdate, + _afterEnter = afterEnter, + _afterUpdate = afterUpdate; + + @override + List buildRoutes() => [ + VPath( + path: path, + aliases: aliases, + mustMatchStackedRoute: mustMatchStackedRoute, + stackedRoutes: [ + VWidgetBase( + widget: widget, + key: key, + name: name, + stackedRoutes: stackedRoutes, + buildTransition: buildTransition, + transitionDuration: transitionDuration, + reverseTransitionDuration: reverseTransitionDuration, + fullscreenDialog: fullscreenDialog, + ), + ], + ), + ]; + + /// A boolean to indicate whether this can be a valid [VRouteElement] of the [VRoute] if no + /// [VRouteElement] in its [stackedRoute] is matched + /// + /// This is mainly useful for [VRouteElement]s which are NOT [VRouteElementWithPage] + bool get mustMatchStackedRoute => false; +} \ No newline at end of file