From 3c88bfa8ac08adc2e270c7ad51cb0c111c72a833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=B9i=20Trung=20Hi=E1=BA=BFu?= Date: Tue, 18 Jun 2024 14:26:27 +0700 Subject: [PATCH] TW-1836: Move shared media to direct chat profile (#1856) * TW-1836: Add shared media tabs into chat profile * TW-1836: Delete Shared media screen * TW-1836: Update `chat_details` --- lib/pages/chat_details/chat_details.dart | 289 +--------------- lib/pages/chat_details/chat_details_view.dart | 6 +- .../chat_profile_info/chat_profile_info.dart | 41 ++- .../chat_profile_info_navigator.dart | 6 - .../chat_profile_info_shared.dart | 200 ----------- .../chat_profile_info_shared_view.dart | 98 ------ .../chat_profile_info_shared_view_style.dart | 10 - .../chat_profile_info_style.dart | 23 ++ .../chat_profile_info_view.dart | 170 ++++++---- .../enum/chat/chat_details_screen_enum.dart | 1 + .../mixins/chat_details_tab_mixin.dart | 315 ++++++++++++++++++ 11 files changed, 476 insertions(+), 683 deletions(-) delete mode 100644 lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared.dart delete mode 100644 lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view.dart delete mode 100644 lib/pages/chat_profile_info/chat_profile_info_shared/chat_profile_info_shared_view_style.dart create mode 100644 lib/presentation/enum/chat/chat_details_screen_enum.dart create mode 100644 lib/presentation/mixins/chat_details_tab_mixin.dart diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 6647e820b4..9016b1ecce 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -1,38 +1,16 @@ -import 'dart:async'; -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/extensions/event_update_extension.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 } @@ -57,287 +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; - - StreamSubscription? _onRoomEventChangedSubscription; - @override void initState() { super.initState(); - initControllers(); - WidgetsBinding.instance.addPostFrameCallback((_) { - nestedScrollViewState.currentState?.innerController.addListener( - _listenerInnerController, - ); - _refreshDataInTabviewInit(); - }); initValueNotifiers(); - _listenForRoomMembersChanged(); - } - - void initControllers() { - 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, - ); } void initValueNotifiers() { - room = Matrix.of(context).client.getRoomById(roomId!); muteNotifier.value = room?.pushRuleState ?? PushRuleState.notify; - membersNotifier.value ??= room?.getParticipants(); - } - - void _listenForRoomMembersChanged() { - _onRoomEventChangedSubscription = - Matrix.of(context).client.onEvent.stream.listen((event) { - if (event.isMemberChangedEvent && room?.id == event.roomID) { - membersNotifier.value = room?.getParticipants(); - } - }); } @override void dispose() { - tabController?.dispose(); - muteNotifier.dispose(); - membersNotifier.dispose(); - mediaListController?.dispose(); - linksListController?.dispose(); - filesListController?.dispose(); - nestedScrollViewState.currentState?.innerController.dispose(); - _onRoomEventChangedSubscription?.cancel(); 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_view.dart b/lib/pages/chat_details/chat_details_view.dart index 010305f3e9..331b30592e 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -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(), ), diff --git a/lib/pages/chat_profile_info/chat_profile_info.dart b/lib/pages/chat_profile_info/chat_profile_info.dart index 1b3523a243..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/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 d1812e9464..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,4 +1,3 @@ -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'; @@ -7,7 +6,6 @@ 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 { @@ -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 527c641f2c..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: WidgetStateProperty.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 5eae8fd69c..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(), ), ), ), @@ -111,7 +172,6 @@ class _Information extends StatelessWidget { this.displayName, this.matrixId, required this.lookupContactNotifier, - this.goToProfileShared, required this.isDraftInfo, }); @@ -119,7 +179,6 @@ class _Information extends StatelessWidget { final String? displayName; final String? matrixId; final ValueNotifier> lookupContactNotifier; - final Function()? goToProfileShared; final bool isDraftInfo; @override @@ -241,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, - ), - ], - ), - ), - ), ], ), ), 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/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(); + } +}