From 808ba611deb933c0ae70c220d2de25e46b1f3cea Mon Sep 17 00:00:00 2001 From: MinhDV Date: Mon, 18 Sep 2023 11:07:21 +0700 Subject: [PATCH] TW-616 List all image & video of Chat in Chat details --- lib/config/app_config.dart | 2 + lib/di/global/get_it_initializer.dart | 4 + .../room/timeline_search_event_state.dart | 42 ++++++++ .../timeline_search_event_interactor.dart | 35 ++++++ lib/pages/chat/events/image_bubble.dart | 6 +- lib/pages/chat/events/video_player.dart | 7 +- lib/pages/chat_details/chat_details.dart | 40 ++++++- .../links/chat_details_links_page.dart | 55 ++++++++++ .../links/chat_details_links_style.dart | 17 +++ .../media/chat_details_media_page.dart | 77 ++++++++++++++ .../media/chat_details_media_style.dart | 19 ++++ .../same_type_events_list_builder.dart | 37 +++++++ .../same_type_events_list_controller.dart | 100 ++++++++++++++++++ .../extensions/send_file_extension.dart | 13 +++ .../event_extension.dart | 13 +++ lib/widgets/mxc_image.dart | 17 ++- pubspec.lock | 4 +- pubspec.yaml | 2 +- 18 files changed, 471 insertions(+), 19 deletions(-) create mode 100644 lib/domain/app_state/room/timeline_search_event_state.dart create mode 100644 lib/domain/usecase/room/timeline_search_event_interactor.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/links/chat_details_links_style.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder.dart create mode 100644 lib/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index c5ec2c1195..f78e5355e9 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -71,6 +71,8 @@ abstract class AppConfig { static const int fetchContactsLimit = 20; static const int chatRoomSearchKeywordMin = 2; static const bool chatRoomSearchWordStrategy = false; + static const String defaultImageBlurHash = 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; + static const String defaultVideoBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; static String? issueId; diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index 6eabf68591..618ce3fe12 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -23,6 +23,7 @@ import 'package:fluffychat/domain/usecase/forward/forward_message_interactor.dar import 'package:fluffychat/domain/usecase/get_contacts_interactor.dart'; import 'package:fluffychat/domain/usecase/room/chat_room_search_interactor.dart'; import 'package:fluffychat/domain/usecase/room/create_new_group_chat_interactor.dart'; +import 'package:fluffychat/domain/usecase/room/timeline_search_event_interactor.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; import 'package:fluffychat/domain/usecase/search/pre_search_recent_contacts_interactor.dart'; import 'package:fluffychat/domain/usecase/search/search_recent_chat_interactor.dart'; @@ -134,5 +135,8 @@ class GetItInitializer { getIt.registerSingleton( ChatRoomSearchInteractor(), ); + getIt.registerSingleton( + TimelineSearchEventInteractor(), + ); } } diff --git a/lib/domain/app_state/room/timeline_search_event_state.dart b/lib/domain/app_state/room/timeline_search_event_state.dart new file mode 100644 index 0000000000..1e47d5784a --- /dev/null +++ b/lib/domain/app_state/room/timeline_search_event_state.dart @@ -0,0 +1,42 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:matrix/matrix.dart'; + +class TimelineSearchEventInitial extends Success { + @override + List get props => []; +} + +class TimelineSearchEventSuccess extends Success { + final List events; + + const TimelineSearchEventSuccess({required this.events}); + + @override + List get props => [events]; +} + +class TimelineSearchEventFailure extends Failure { + final dynamic exception; + + const TimelineSearchEventFailure({required this.exception}); + + @override + List get props => [exception]; +} + +extension TimelineSearchEventSuccessExtension on TimelineSearchEventSuccess { + TimelineSearchEventSuccess addMore(TimelineSearchEventSuccess other) { + return TimelineSearchEventSuccess( + events: events + other.events, + ); + } +} + +extension TimelineSearchEventEitherExtension on Either { + TimelineSearchEventSuccess? getSuccessOrNull() => fold( + (failure) => null, + (success) => success is TimelineSearchEventSuccess ? success : null, + ); +} diff --git a/lib/domain/usecase/room/timeline_search_event_interactor.dart b/lib/domain/usecase/room/timeline_search_event_interactor.dart new file mode 100644 index 0000000000..fbf4591347 --- /dev/null +++ b/lib/domain/usecase/room/timeline_search_event_interactor.dart @@ -0,0 +1,35 @@ +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/room/timeline_search_event_state.dart'; +import 'package:matrix/matrix.dart'; + +class TimelineSearchEventInteractor { + Stream> execute({ + required Timeline timeline, + required bool Function(Event) searchFunc, + required int requestHistoryCount, + required int maxHistoryRequests, + required int? limit, + String? sinceEventId, + }) async* { + try { + yield* timeline + .searchEvent( + searchFunc: searchFunc, + requestHistoryCount: requestHistoryCount, + maxHistoryRequests: maxHistoryRequests, + limit: limit, + sinceEventId: sinceEventId, + ) + .map((events) { + Logs().v( + 'TimelineSearchEventInteractor::events ${events.length} ${events.map((event) => event.eventId)}', + ); + return Right(TimelineSearchEventSuccess(events: events)); + }); + } catch (e) { + yield Left(TimelineSearchEventFailure(exception: e)); + } + } +} diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index b2b4f24e37..a4673101bf 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -1,5 +1,7 @@ import 'dart:typed_data'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:matrix/matrix.dart'; @@ -44,9 +46,7 @@ class ImageBubble extends StatelessWidget { ); } final String blurHashString = - event.infoMap['xyz.amorgan.blurhash'] is String - ? event.infoMap['xyz.amorgan.blurhash'] - : 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; + event.blurHash ?? AppConfig.defaultImageBlurHash; final ratio = event.infoMap['w'] is int && event.infoMap['h'] is int ? event.infoMap['w'] / event.infoMap['h'] : 1.0; diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index 7b19420af8..3aa2f15153 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -95,14 +96,10 @@ class EventVideoPlayerState extends State { super.dispose(); } - static const String fallbackBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; - @override Widget build(BuildContext context) { final hasThumbnail = widget.event.hasThumbnail; - final blurHash = (widget.event.infoMap as Map) - .tryGet('xyz.amorgan.blurhash') ?? - fallbackBlurHash; + final blurHash = widget.event.blurHash ?? AppConfig.defaultVideoBlurHash; final chewieManager = _chewieManager; return ClipRRect( diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 32e57621b0..3bfa011b9f 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -4,12 +4,17 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/pages/chat_details/chat_details_actions_enum.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/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_page_view/same_type_events_list_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/model/chat_details/chat_details_page_model.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -39,6 +44,8 @@ class ChatDetails extends StatefulWidget { } class ChatDetailsController extends State { + static const _mediaFetchLimit = 20; + static const _linksFetchLimit = 20; final invitationSelectionMobileAndTabletKey = const Key('InvitationSelectionMobileAndTabletKey'); @@ -66,6 +73,24 @@ class ChatDetailsController extends State { Room? get room => Matrix.of(context).client.getRoomById(roomId!); + Timeline? _timeline; + + Future getTimeline() async { + _timeline ??= await room!.getTimeline(); + return _timeline!; + } + + final mediaListController = SameTypeEventsListController( + searchFunc: (event) => event.isVideoOrImage, + limit: _mediaFetchLimit, + ); + final linksListController = SameTypeEventsListController( + searchFunc: (event) => event.isContainsLink, + limit: _linksFetchLimit, + ); + + final MxcImageCacheMap _mediaCacheMap = {}; + int get actualMembersCount => room!.summary.actualMembersCount; void toggleDisplaySettings() => @@ -436,17 +461,24 @@ class ChatDetailsController extends State { isMobileAndTablet: isMobileAndTablet, ), ), - const ChatDetailsPageModel( + ChatDetailsPageModel( page: ChatDetailsPage.media, - child: SizedBox.shrink(), + child: ChatDetailsMediaPage( + eventsListController: mediaListController, + getTimeline: getTimeline, + cacheMap: _mediaCacheMap, + ), ), const ChatDetailsPageModel( page: ChatDetailsPage.files, child: SizedBox.shrink(), ), - const ChatDetailsPageModel( + ChatDetailsPageModel( page: ChatDetailsPage.links, - child: SizedBox.shrink(), + child: ChatDetailsLinksPage( + eventsListController: linksListController, + getTimeline: getTimeline, + ), ), const ChatDetailsPageModel( page: ChatDetailsPage.downloads, diff --git a/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart b/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart new file mode 100644 index 0000000000..0fb638d7af --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_page.dart @@ -0,0 +1,55 @@ +import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/links/chat_details_links_style.dart'; +import 'package:fluffychat/utils/string_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat_details/chat_details_page_view/same_type_events_list_builder.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart'; + +class ChatDetailsLinksPage extends StatelessWidget { + final SameTypeEventsListController eventsListController; + final Future Function() getTimeline; + + const ChatDetailsLinksPage({ + Key? key, + required this.eventsListController, + required this.getTimeline, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SameTypeEventsListBuilder( + controller: eventsListController, + getTimeline: getTimeline, + builder: (context, eventsState) { + final events = eventsState.getSuccessOrNull()?.events ?? []; + return ListView.separated( + itemCount: events.length, + itemBuilder: (context, index) { + final body = events[index].body; + final link = body.getFirstValidUrl() ?? ''; + return ListTile( + leading: Container( + width: ChatDetailsLinksStyle.avatarSize, + height: ChatDetailsLinksStyle.avatarSize, + alignment: Alignment.center, + decoration: ChatDetailsLinksStyle.avatarDecoration(context), + child: Text( + link.getShortcutNameForAvatar(), + style: ChatDetailsLinksStyle.avatarTextStyle(context), + ), + ), + title: Text(link), + subtitle: Text( + link, + style: ChatDetailsLinksStyle.subtitleTextStyle(context), + ), + ); + }, + separatorBuilder: (context, index) => const Divider(), + ); + }, + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_style.dart b/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_style.dart new file mode 100644 index 0000000000..098f2332e3 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/links/chat_details_links_style.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ChatDetailsLinksStyle { + static const double avatarSize = 56; + static BoxDecoration avatarDecoration(BuildContext context) => BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(16), + ); + static TextStyle? avatarTextStyle(BuildContext context) => + Theme.of(context).textTheme.headlineLarge?.copyWith( + color: Theme.of(context).colorScheme.onSecondary, + ); + static TextStyle? subtitleTextStyle(BuildContext context) => + Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ); +} diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart new file mode 100644 index 0000000000..55a12f6461 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart @@ -0,0 +1,77 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/same_type_events_list_builder.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:matrix/matrix.dart'; + +class ChatDetailsMediaPage extends StatelessWidget { + final SameTypeEventsListController eventsListController; + final Future Function() getTimeline; + final MxcImageCacheMap cacheMap; + const ChatDetailsMediaPage({ + Key? key, + required this.eventsListController, + required this.getTimeline, + required this.cacheMap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SameTypeEventsListBuilder( + controller: eventsListController, + getTimeline: getTimeline, + builder: (context, eventsState) { + final events = eventsState.getSuccessOrNull()?.events ?? []; + return CustomScrollView( + slivers: [ + SliverGrid.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + ), + itemCount: events.length, + itemBuilder: (context, index) => Stack( + fit: StackFit.expand, + children: [ + MxcImage( + event: events[index], + isThumbnail: true, + fit: BoxFit.cover, + onTapPreview: () {}, + isPreview: true, + placeholder: (context) => BlurHash( + hash: events[index].blurHash ?? + AppConfig.defaultImageBlurHash, + ), + cacheKey: events[index].eventId, + cacheMap: cacheMap, + ), + if (events[index].messageType == MessageTypes.Video) + Positioned( + bottom: 10, + right: 10, + child: Container( + padding: ChatDetailsMediaStyle.durationPadding, + decoration: ChatDetailsMediaStyle.durationBoxDecoration( + context, + ), + child: Text( + "00:00", + style: + ChatDetailsMediaStyle.durationTextStyle(context), + ), + ), + ), + ], + ), + ) + ], + ); + }, + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart new file mode 100644 index 0000000000..15646095ea --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class ChatDetailsMediaStyle { + static const durationPadding = EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ); + + static Decoration durationBoxDecoration(BuildContext context) => + ShapeDecoration( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.5), + shape: const StadiumBorder(), + ); + + static TextStyle? durationTextStyle(BuildContext context) => + Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ); +} diff --git a/lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder.dart b/lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder.dart new file mode 100644 index 0000000000..f64363a8fc --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/same_type_events_list_builder.dart @@ -0,0 +1,37 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart'; +import 'package:fluffychat/widgets/twake_components/twake_smart_refresher.dart'; + +class SameTypeEventsListBuilder extends StatelessWidget { + final SameTypeEventsListController controller; + final Future Function() getTimeline; + final Widget Function(BuildContext, Either) builder; + + const SameTypeEventsListBuilder({ + Key? key, + required this.controller, + required this.getTimeline, + required this.builder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller.eventsNotifier, + builder: (context, eventsState, child) => TwakeSmartRefresher( + controller: controller.refreshController, + onRefresh: () => controller.refresh(getTimeline: getTimeline), + onLoading: () => controller.loadMore(getTimeline: getTimeline), + child: builder( + context, + eventsState, + ), + ), + ); + } +} diff --git a/lib/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart b/lib/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart new file mode 100644 index 0000000000..adf4b1ffd1 --- /dev/null +++ b/lib/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart @@ -0,0 +1,100 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; +import 'package:fluffychat/domain/usecase/room/timeline_search_event_interactor.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +class SameTypeEventsListController { + static const _requestHistoryCount = 100; + static const _maxHistoryRequests = 10; + + final bool Function(Event) searchFunc; + final int? limit; + + SameTypeEventsListController({ + required this.searchFunc, + required this.limit, + }); + + final eventsNotifier = ValueNotifier>( + Right(TimelineSearchEventInitial()), + ); + final refreshController = RefreshController(initialRefresh: true); + + final _searchInteractor = getIt.get(); + + void refresh({required Future Function() getTimeline}) async { + final timeline = await getTimeline(); + _searchInteractor + .execute( + timeline: timeline, + searchFunc: searchFunc, + requestHistoryCount: _requestHistoryCount, + maxHistoryRequests: _maxHistoryRequests, + limit: null, + ) + .listen( + (event) { + Logs().v('SameTypeEventsListController::refresh $event'); + eventsNotifier.value = event; + }, + onError: (_) { + refreshController.refreshFailed(); + }, + onDone: () { + refreshController.refreshCompleted(); + }, + ); + } + + void loadMore({required Future Function() getTimeline}) async { + final lastSuccess = eventsNotifier.value.getSuccessOrNull(); + if (lastSuccess == null || lastSuccess.events.isEmpty) { + refreshController.loadComplete(); + return; + } + final timeline = await getTimeline(); + var isEnd = false; + _searchInteractor + .execute( + timeline: timeline, + searchFunc: searchFunc, + requestHistoryCount: _requestHistoryCount, + maxHistoryRequests: _maxHistoryRequests, + limit: limit, + sinceEventId: lastSuccess.events.last.eventId, + ) + .listen( + (event) { + Logs().v('SameTypeEventsListController::loadMore $event'); + eventsNotifier.value = event.map( + (success) { + if (success is TimelineSearchEventSuccess) { + isEnd = limit != null + ? success.events.length < limit! + : success.events.isEmpty; + return lastSuccess.addMore(success); + } + return success; + }, + ); + }, + onError: (_) { + refreshController.loadFailed(); + }, + onDone: () { + if (isEnd) { + Logs().v('SameTypeEventsListController::loadMore loadNoData'); + refreshController.loadNoData(); + } else { + Logs().v('SameTypeEventsListController::loadMore loadComplete'); + refreshController.loadComplete(); + } + }, + ); + } +} diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index c59cd235fc..534f7c17d1 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -7,6 +7,8 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/fake_sending_file_info.dart'; import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:image/image.dart' as img; +import 'package:blurhash_dart/blurhash_dart.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; @@ -177,6 +179,9 @@ extension SendFileExtension on Room { Logs().d('RoomExtension::EncryptedThumbnail: $encryptedThumbnail'); } + final blurHash = thumbnail?.filePath != null + ? await _generateBlurHash(thumbnail!.filePath) + : null; // Send event final content = { 'msgtype': msgType, @@ -191,6 +196,7 @@ extension SendFileExtension on Room { if (thumbnail != null && encryptedThumbnail != null) 'thumbnail_file': encryptedThumbnail.toJson(), if (thumbnail != null) 'thumbnail_info': thumbnail.metadata, + if (blurHash != null) 'xyz.amorgan.blurhash': blurHash, }, if (extraContent != null) ...extraContent, }; @@ -329,4 +335,11 @@ extension SendFileExtension on Room { return null; } } + + Future _generateBlurHash(String filePath) async { + final data = File(filePath).readAsBytesSync(); + final image = img.decodeImage(data); + final blurHash = BlurHash.encode(image!, numCompX: 4, numCompY: 3); + return blurHash.hash; + } } diff --git a/lib/utils/matrix_sdk_extensions/event_extension.dart b/lib/utils/matrix_sdk_extensions/event_extension.dart index 483167caeb..4aaf0921ca 100644 --- a/lib/utils/matrix_sdk_extensions/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions/event_extension.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/domain/model/extensions/string_extension.dart'; import 'package:collection/collection.dart'; +import 'package:fluffychat/utils/string_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -26,6 +27,12 @@ extension LocalizedBody on Event { return (content.tryGet('filename') ?? body).ellipsizeFileName; } + String? get blurHash { + return infoMap['xyz.amorgan.blurhash'] is String + ? infoMap['xyz.amorgan.blurhash'] + : null; + } + String? get mimeType { return content .tryGetMap('info') @@ -41,6 +48,12 @@ extension LocalizedBody on Event { ?.toUpperCase()); } + bool get isVideoOrImage => + [MessageTypes.Image, MessageTypes.Video].contains(messageType); + + bool get isContainsLink => + messageType == MessageTypes.Text && text.getFirstValidUrl() != null; + void shareFile(BuildContext context) async { final matrixFile = await getFile(context); diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index ffdf96f148..9c637bbdd1 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -7,6 +7,8 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/widgets/matrix.dart'; +typedef MxcImageCacheMap = Map; + class MxcImage extends StatefulWidget { final Uri? uri; final Event? event; @@ -26,6 +28,7 @@ class MxcImage extends StatefulWidget { final void Function()? onTapSelectMode; final Uint8List? imageData; final bool isPreview; + final MxcImageCacheMap? cacheMap; const MxcImage({ this.uri, @@ -46,6 +49,7 @@ class MxcImage extends StatefulWidget { this.onTapSelectMode, this.imageData, this.isPreview = false, + this.cacheMap, Key? key, }) : super(key: key); @@ -56,14 +60,17 @@ class MxcImage extends StatefulWidget { class _MxcImageState extends State with SingleTickerProviderStateMixin { static const String placeholderKey = 'placeholder'; - static final Map _imageDataCache = {}; + static final MxcImageCacheMap _imageDataCache = {}; Uint8List? _imageDataNoCache; bool isLoadDone = false; Uint8List? get _imageData { final cacheKey = widget.cacheKey; - final image = - cacheKey == null ? _imageDataNoCache : _imageDataCache[cacheKey]; + final image = cacheKey == null + ? _imageDataNoCache + : widget.cacheMap != null + ? widget.cacheMap![cacheKey] + : _imageDataCache[cacheKey]; return image; } @@ -72,7 +79,9 @@ class _MxcImageState extends State final cacheKey = widget.cacheKey; cacheKey == null ? _imageDataNoCache = data - : _imageDataCache[cacheKey] = data; + : widget.cacheMap != null + ? widget.cacheMap![cacheKey] = data + : _imageDataCache[cacheKey] = data; } bool? _isCached; diff --git a/pubspec.lock b/pubspec.lock index d934aa9df1..3a3b33aa65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1525,8 +1525,8 @@ packages: dependency: "direct main" description: path: "." - ref: twake-supported - resolved-ref: "6768740728e153a7a76948cd19131c5239501800" + ref: "fix/search_event_issues" + resolved-ref: "542ce402214664fcf121b418652189de3572ea31" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.2" diff --git a/pubspec.yaml b/pubspec.yaml index 04f065011c..034e55a679 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: matrix: git: url: git@github.com:linagora/matrix-dart-sdk.git - ref: twake-supported + ref: fix/search_event_issues matrix_homeserver_recommendations: ^0.3.0 matrix_link_text: ^2.0.0 native_imaging: ^0.1.0