From fa012027e3130a16c73a2803258eff8278fe03a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sat, 8 Nov 2025 09:48:26 +0100 Subject: [PATCH] feat: Improved search --- lib/l10n/intl_en.arb | 12 +- .../chat_search/chat_search_files_tab.dart | 223 ++++++--------- .../chat_search/chat_search_images_tab.dart | 260 ++++++++---------- .../chat_search/chat_search_message_tab.dart | 134 ++++----- lib/pages/chat_search/chat_search_page.dart | 214 ++++++-------- lib/pages/chat_search/chat_search_view.dart | 21 +- lib/pages/chat_search/search_footer.dart | 63 +++++ pubspec.lock | 9 +- pubspec.yaml | 3 +- 9 files changed, 432 insertions(+), 507 deletions(-) create mode 100644 lib/pages/chat_search/search_footer.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f76710677..aa94fcb88 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3466,5 +3466,15 @@ "skipChatBackup": "Skip chat backup", "skipChatBackupWarning": "Are you sure? Without enabling the chat backup you may lose access to your messages if you switch your device.", "loadingMessages": "Loading messages", - "setupChatBackup": "Set up chat backup" + "setupChatBackup": "Set up chat backup", + "noMoreResultsFound": "No more results found", + "chatSearchedUntil": "Chat searched until {time}", + "@chatSearchedUntil": { + "type": "String", + "placeholders": { + "time": { + "type": "String" + } + } + } } diff --git a/lib/pages/chat_search/chat_search_files_tab.dart b/lib/pages/chat_search/chat_search_files_tab.dart index 229cb6a6e..24d27cece 100644 --- a/lib/pages/chat_search/chat_search_files_tab.dart +++ b/lib/pages/chat_search/chat_search_files_tab.dart @@ -4,167 +4,106 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat_search/search_footer.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; class ChatSearchFilesTab extends StatelessWidget { final Room room; - final Stream<(List, String?)>? searchStream; - final void Function({String? prevBatch, List? previousSearchResult}) - startSearch; + final List events; + final void Function() onStartSearch; + final bool endReached, isLoading; + final DateTime? searchedUntil; const ChatSearchFilesTab({ required this.room, - required this.startSearch, - required this.searchStream, + required this.events, + required this.onStartSearch, + required this.endReached, + required this.isLoading, super.key, + required this.searchedUntil, }); @override Widget build(BuildContext context) { - return StreamBuilder( - stream: searchStream, - builder: (context, snapshot) { - final theme = Theme.of(context); - final events = snapshot.data?.$1; - if (searchStream == null || events == null) { - return Column( - mainAxisAlignment: .center, - children: [ - const CircularProgressIndicator.adaptive(strokeWidth: 2), - const SizedBox(height: 8), - Text( - L10n.of(context).searchIn( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - ), - ), - ], - ); - } - - if (events.isEmpty) { - return Column( - mainAxisAlignment: .center, - children: [ - const Icon(Icons.file_present_outlined, size: 64), - const SizedBox(height: 8), - Text(L10n.of(context).nothingFound), - ], - ); - } - - return SelectionArea( - child: ListView.builder( + final theme = Theme.of(context); + return SelectionArea( + child: ListView.builder( + padding: const EdgeInsets.all(8.0), + itemCount: events.length + 1, + itemBuilder: (context, i) { + if (i == events.length) { + return SearchFooter( + searchedUntil: searchedUntil, + endReached: endReached, + isLoading: isLoading, + onStartSearch: onStartSearch, + ); + } + final event = events[i]; + final filename = + event.content.tryGet('filename') ?? + event.content.tryGet('body') ?? + L10n.of(context).unknownEvent('File'); + final filetype = (filename.contains('.') + ? filename.split('.').last.toUpperCase() + : event.content + .tryGetMap('info') + ?.tryGet('mimetype') + ?.toUpperCase() ?? + 'UNKNOWN'); + final sizeString = event.sizeString; + final prevEvent = i > 0 ? events[i - 1] : null; + final sameEnvironment = prevEvent == null + ? false + : prevEvent.originServerTs.sameEnvironment(event.originServerTs); + return Padding( padding: const EdgeInsets.all(8.0), - itemCount: events.length + 1, - itemBuilder: (context, i) { - if (i == events.length) { - if (snapshot.connectionState != ConnectionState.done) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - ); - } - final nextBatch = snapshot.data?.$2; - if (nextBatch == null) { - return const SizedBox.shrink(); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: TextButton.icon( - style: TextButton.styleFrom( - backgroundColor: theme.colorScheme.secondaryContainer, - foregroundColor: theme.colorScheme.onSecondaryContainer, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!sameEnvironment) ...[ + Row( + children: [ + Expanded( + child: Container(height: 1, color: theme.dividerColor), ), - onPressed: () => startSearch( - prevBatch: nextBatch, - previousSearchResult: events, - ), - icon: const Icon(Icons.arrow_downward_outlined), - label: Text(L10n.of(context).searchMore), - ), - ), - ); - } - final event = events[i]; - final filename = - event.content.tryGet('filename') ?? - event.content.tryGet('body') ?? - L10n.of(context).unknownEvent('File'); - final filetype = (filename.contains('.') - ? filename.split('.').last.toUpperCase() - : event.content - .tryGetMap('info') - ?.tryGet('mimetype') - ?.toUpperCase() ?? - 'UNKNOWN'); - final sizeString = event.sizeString; - final prevEvent = i > 0 ? events[i - 1] : null; - final sameEnvironment = prevEvent == null - ? false - : prevEvent.originServerTs.sameEnvironment( - event.originServerTs, - ); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: .min, - children: [ - if (!sameEnvironment) ...[ - Row( - children: [ - Expanded( - child: Container( - height: 1, - color: theme.dividerColor, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - event.originServerTs.localizedTime(context), - style: theme.textTheme.labelSmall, - textAlign: TextAlign.center, - ), - ), - Expanded( - child: Container( - height: 1, - color: theme.dividerColor, - ), - ), - ], - ), - const SizedBox(height: 4), - ], - Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - color: theme.colorScheme.onInverseSurface, - clipBehavior: Clip.hardEdge, - child: ListTile( - leading: const Icon(Icons.file_present_outlined), - title: Text( - filename, - maxLines: 1, - overflow: TextOverflow.ellipsis, + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + event.originServerTs.localizedTime(context), + style: theme.textTheme.labelSmall, + textAlign: TextAlign.center, ), - subtitle: Text('$sizeString | $filetype'), - onTap: () => event.saveFile(context), ), + Expanded( + child: Container(height: 1, color: theme.dividerColor), + ), + ], + ), + const SizedBox(height: 4), + ], + Material( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + color: theme.colorScheme.onInverseSurface, + clipBehavior: Clip.hardEdge, + child: ListTile( + leading: const Icon(Icons.file_present_outlined), + title: Text( + filename, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ], + subtitle: Text('$sizeString | $filetype'), + onTap: () => event.saveFile(context), + ), ), - ); - }, - ), - ); - }, + ], + ), + ); + }, + ), ); } } diff --git a/lib/pages/chat_search/chat_search_images_tab.dart b/lib/pages/chat_search/chat_search_images_tab.dart index ecd66a312..e5cd62a38 100644 --- a/lib/pages/chat_search/chat_search_images_tab.dart +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -6,169 +6,147 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; +import 'package:fluffychat/pages/chat_search/search_footer.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; class ChatSearchImagesTab extends StatelessWidget { final Room room; - final Stream<(List, String?)>? searchStream; - final void Function({String? prevBatch, List? previousSearchResult}) - startSearch; + final List events; + final void Function() onStartSearch; + final bool endReached, isLoading; + final DateTime? searchedUntil; const ChatSearchImagesTab({ required this.room, - required this.startSearch, - required this.searchStream, + required this.events, + required this.onStartSearch, + required this.endReached, + required this.isLoading, super.key, + required this.searchedUntil, }); @override Widget build(BuildContext context) { final borderRadius = BorderRadius.circular(AppConfig.borderRadius / 2); - return StreamBuilder( - stream: searchStream, - builder: (context, snapshot) { - final theme = Theme.of(context); - final events = snapshot.data?.$1; - if (searchStream == null || events == null) { - return Column( - mainAxisAlignment: .center, - children: [ - const CircularProgressIndicator.adaptive(strokeWidth: 2), - const SizedBox(height: 8), - Text( - L10n.of(context).searchIn( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - ), - ), - ], + final theme = Theme.of(context); + if (events.isEmpty) { + if (isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator.adaptive(strokeWidth: 2), + const SizedBox(height: 8), + Text( + L10n.of(context).searchIn( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), + ), + ), + ], + ); + } + if (events.isEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.photo_outlined, size: 64), + const SizedBox(height: 8), + Text(L10n.of(context).nothingFound), + ], + ); + } + final eventsByMonth = >{}; + for (final event in events) { + final month = DateTime( + event.originServerTs.year, + event.originServerTs.month, + ); + eventsByMonth[month] ??= []; + eventsByMonth[month]!.add(event); + } + final eventsByMonthList = eventsByMonth.entries.toList(); + + const padding = 8.0; + + return ListView.builder( + itemCount: eventsByMonth.length + 1, + itemBuilder: (context, i) { + if (i == eventsByMonth.length) { + return SearchFooter( + searchedUntil: searchedUntil, + endReached: endReached, + isLoading: isLoading, + onStartSearch: onStartSearch, ); } - if (events.isEmpty) { - return Column( - mainAxisAlignment: .center, - children: [ - const Icon(Icons.photo_outlined, size: 64), - const SizedBox(height: 8), - Text(L10n.of(context).nothingFound), - ], - ); - } - final eventsByMonth = >{}; - for (final event in events) { - final month = DateTime( - event.originServerTs.year, - event.originServerTs.month, - ); - eventsByMonth[month] ??= []; - eventsByMonth[month]!.add(event); - } - final eventsByMonthList = eventsByMonth.entries.toList(); - const padding = 8.0; - - return ListView.builder( - itemCount: eventsByMonth.length + 1, - itemBuilder: (context, i) { - if (i == eventsByMonth.length) { - if (snapshot.connectionState != ConnectionState.done) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - ); - } - final nextBatch = snapshot.data?.$2; - if (nextBatch == null) { - return const SizedBox.shrink(); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: TextButton.icon( - style: TextButton.styleFrom( - backgroundColor: theme.colorScheme.secondaryContainer, - foregroundColor: theme.colorScheme.onSecondaryContainer, - ), - onPressed: () => startSearch( - prevBatch: nextBatch, - previousSearchResult: events, - ), - icon: const Icon(Icons.arrow_downward_outlined), - label: Text(L10n.of(context).searchMore), - ), - ), - ); - } - - final monthEvents = eventsByMonthList[i].value; - return Column( - mainAxisSize: .min, + final monthEvents = eventsByMonthList[i].value; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 4), + Row( children: [ - const SizedBox(height: 4), - Row( - children: [ - Expanded( - child: Container(height: 1, color: theme.dividerColor), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - DateFormat.yMMMM( - Localizations.localeOf(context).languageCode, - ).format(eventsByMonthList[i].key), - style: theme.textTheme.labelSmall, - textAlign: TextAlign.center, - ), - ), - Expanded( - child: Container(height: 1, color: theme.dividerColor), - ), - ], + Expanded( + child: Container(height: 1, color: theme.dividerColor), ), - GridView.count( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - mainAxisSpacing: padding, - crossAxisSpacing: padding, - clipBehavior: Clip.hardEdge, - padding: const EdgeInsets.all(padding), - crossAxisCount: 3, - children: monthEvents.map((event) { - if (event.messageType == MessageTypes.Video) { - return Material( - clipBehavior: Clip.hardEdge, - borderRadius: borderRadius, - child: EventVideoPlayer(event), - ); - } - return InkWell( - onTap: () => showDialog( - context: context, - builder: (_) => - ImageViewer(event, outerContext: context), - ), - borderRadius: borderRadius, - child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: borderRadius, - child: MxcImage( - event: event, - width: 128, - height: 128, - fit: BoxFit.cover, - animated: true, - isThumbnail: true, - ), - ), - ); - }).toList(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + DateFormat.yMMMM( + Localizations.localeOf(context).languageCode, + ).format(eventsByMonthList[i].key), + style: theme.textTheme.labelSmall, + textAlign: TextAlign.center, + ), + ), + Expanded( + child: Container(height: 1, color: theme.dividerColor), ), ], - ); - }, + ), + GridView.count( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + mainAxisSpacing: padding, + crossAxisSpacing: padding, + clipBehavior: Clip.hardEdge, + padding: const EdgeInsets.all(padding), + crossAxisCount: 3, + children: monthEvents.map((event) { + if (event.messageType == MessageTypes.Video) { + return Material( + clipBehavior: Clip.hardEdge, + borderRadius: borderRadius, + child: EventVideoPlayer(event), + ); + } + return InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => ImageViewer(event, outerContext: context), + ), + borderRadius: borderRadius, + child: Material( + clipBehavior: Clip.hardEdge, + borderRadius: borderRadius, + child: MxcImage( + event: event, + width: 128, + height: 128, + fit: BoxFit.cover, + animated: true, + isThumbnail: true, + ), + ), + ); + }).toList(), + ), + ], ); }, ); diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart index 8b1ca3166..10b8ef8ad 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat_search/search_footer.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/url_launcher.dart'; @@ -13,93 +14,74 @@ import 'package:fluffychat/widgets/avatar.dart'; class ChatSearchMessageTab extends StatelessWidget { final String searchQuery; final Room room; - final Stream<(List, String?)>? searchStream; - final void Function({String? prevBatch, List? previousSearchResult}) - startSearch; + final List events; + final void Function() onStartSearch; + final bool endReached, isLoading; + final DateTime? searchedUntil; const ChatSearchMessageTab({ required this.searchQuery, required this.room, - required this.searchStream, - required this.startSearch, + required this.onStartSearch, + required this.events, + required this.searchedUntil, + required this.endReached, + required this.isLoading, super.key, }); @override Widget build(BuildContext context) { - return StreamBuilder( - key: ValueKey(searchQuery), - stream: searchStream, - builder: (context, snapshot) { - final theme = Theme.of(context); - if (searchStream == null) { - return Column( - mainAxisAlignment: .center, - children: [ - const Icon(Icons.search_outlined, size: 64), - const SizedBox(height: 8), - Text( - L10n.of(context).searchIn( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - ), + final theme = Theme.of(context); + if (events.isEmpty) { + if (isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.search_outlined, size: 64), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Text( + L10n.of(context).searchIn( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), ), - ], - ); - } - final events = snapshot.data?.$1 ?? []; - - return SelectionArea( - child: ListView.separated( - itemCount: events.length + 1, - separatorBuilder: (context, _) => - Divider(color: theme.dividerColor, height: 1), - itemBuilder: (context, i) { - if (i == events.length) { - if (snapshot.connectionState != ConnectionState.done) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - ); - } - final nextBatch = snapshot.data?.$2; - if (nextBatch == null) { - return const SizedBox.shrink(); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: TextButton.icon( - style: TextButton.styleFrom( - backgroundColor: theme.colorScheme.secondaryContainer, - foregroundColor: theme.colorScheme.onSecondaryContainer, - ), - onPressed: () => startSearch( - prevBatch: nextBatch, - previousSearchResult: events, - ), - icon: const Icon(Icons.arrow_downward_outlined), - label: Text(L10n.of(context).searchMore), - ), - ), - ); - } - final event = events[i]; - final sender = event.senderFromMemoryOrFallback; - final displayname = sender.calcDisplayname( - i18n: MatrixLocals(L10n.of(context)), - ); - return _MessageSearchResultListTile( - sender: sender, - displayname: displayname, - event: event, - room: room, - ); - }, + textAlign: TextAlign.center, + ), ), - ); - }, + ], + ); + } + + return SelectionArea( + child: ListView.separated( + itemCount: events.length + 1, + separatorBuilder: (context, _) => + Divider(color: theme.dividerColor, height: 1), + itemBuilder: (context, i) { + if (i == events.length) { + return SearchFooter( + searchedUntil: searchedUntil, + endReached: endReached, + isLoading: isLoading, + onStartSearch: onStartSearch, + ); + } + final event = events[i]; + final sender = event.senderFromMemoryOrFallback; + final displayname = sender.calcDisplayname( + i18n: MatrixLocals(L10n.of(context)), + ); + return _MessageSearchResultListTile( + sender: sender, + displayname: displayname, + event: event, + room: room, + ); + }, + ), ); } } diff --git a/lib/pages/chat_search/chat_search_page.dart b/lib/pages/chat_search/chat_search_page.dart index 40109d0b6..965658eed 100644 --- a/lib/pages/chat_search/chat_search_page.dart +++ b/lib/pages/chat_search/chat_search_page.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -22,156 +20,100 @@ class ChatSearchController extends State final TextEditingController searchController = TextEditingController(); late final TabController tabController; - Timeline? timeline; - - Stream<(List, String?)>? searchStream; - Stream<(List, String?)>? galleryStream; - Stream<(List, String?)>? fileStream; + final List messages = []; + final List images = []; + final List files = []; + String? messagesNextBatch, imagesNextBatch, filesNextBatch; + bool messagesEndReached = false; + bool imagesEndReached = false; + bool filesEndReached = false; + bool isLoading = false; + DateTime? searchedUntil; void restartSearch() { - if (searchController.text.isEmpty) { - setState(() { - searchStream = null; - }); - return; - } setState(() { - searchStream = const Stream.empty(); + messages.clear(); + images.clear(); + files.clear(); + messagesNextBatch = imagesNextBatch = filesNextBatch = searchedUntil = + null; + messagesEndReached = imagesEndReached = filesEndReached = false; }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - startMessageSearch(); + startSearch(); }); } - void startMessageSearch({ - String? prevBatch, - List? previousSearchResult, - }) async { - final timeline = this.timeline ??= await room!.getTimeline(); - - if (tabController.index == 0 && searchController.text.isEmpty) { - return; + void startSearch() async { + switch (tabController.index) { + case 0: + final searchQuery = searchController.text.trim(); + if (searchQuery.isEmpty) return; + setState(() { + isLoading = true; + }); + final result = await room!.searchEvents( + searchTerm: searchController.text.trim(), + nextBatch: messagesNextBatch, + ); + setState(() { + isLoading = false; + messages.addAll(result.events); + messagesNextBatch = result.nextBatch; + messagesEndReached = result.nextBatch == null; + searchedUntil = result.searchedUntil; + }); + return; + case 1: + setState(() { + isLoading = true; + }); + final result = await room!.searchEvents( + searchFunc: (event) => { + MessageTypes.Image, + MessageTypes.Video, + }.contains(event.messageType), + nextBatch: imagesNextBatch, + ); + setState(() { + isLoading = false; + images.addAll(result.events); + imagesNextBatch = result.nextBatch; + imagesEndReached = result.nextBatch == null; + searchedUntil = result.searchedUntil; + }); + return; + case 2: + setState(() { + isLoading = true; + }); + final result = await room!.searchEvents( + searchFunc: (event) => + event.messageType == MessageTypes.File || + (event.messageType == MessageTypes.Audio && + !event.content.containsKey('org.matrix.msc3245.voice')), + nextBatch: filesNextBatch, + ); + setState(() { + isLoading = false; + files.addAll(result.events); + filesNextBatch = result.nextBatch; + filesEndReached = result.nextBatch == null; + searchedUntil = result.searchedUntil; + }); + return; + default: + return; } - - setState(() { - searchStream = timeline - .startSearch( - searchTerm: searchController.text, - prevBatch: prevBatch, - requestHistoryCount: 1000, - limit: 32, - ) - .map( - (result) => ( - [ - if (previousSearchResult != null) ...previousSearchResult, - ...result.$1, - ], - result.$2, - ), - ) - // Deduplication workaround for - // https://github.com/famedly/matrix-dart-sdk/issues/1831 - .map( - (result) => ( - { - for (final event in result.$1) event.eventId: event, - }.values.toList(), - result.$2, - ), - ) - .asBroadcastStream(); - }); - } - - void startGallerySearch({ - String? prevBatch, - List? previousSearchResult, - }) async { - final timeline = this.timeline ??= await room!.getTimeline(); - - setState(() { - galleryStream = timeline - .startSearch( - searchFunc: (event) => { - MessageTypes.Image, - MessageTypes.Video, - }.contains(event.messageType), - prevBatch: prevBatch, - requestHistoryCount: 1000, - limit: 32, - ) - .map( - (result) => ( - [ - if (previousSearchResult != null) ...previousSearchResult, - ...result.$1, - ], - result.$2, - ), - ) - // Deduplication workaround for - // https://github.com/famedly/matrix-dart-sdk/issues/1831 - .map( - (result) => ( - { - for (final event in result.$1) event.eventId: event, - }.values.toList(), - result.$2, - ), - ) - .asBroadcastStream(); - }); - } - - void startFileSearch({ - String? prevBatch, - List? previousSearchResult, - }) async { - final timeline = this.timeline ??= await room!.getTimeline(); - - setState(() { - fileStream = timeline - .startSearch( - searchFunc: (event) => - event.messageType == MessageTypes.File || - (event.messageType == MessageTypes.Audio && - !event.content.containsKey('org.matrix.msc3245.voice')), - prevBatch: prevBatch, - requestHistoryCount: 1000, - limit: 32, - ) - .map( - (result) => ( - [ - if (previousSearchResult != null) ...previousSearchResult, - ...result.$1, - ], - result.$2, - ), - ) - // Deduplication workaround for - // https://github.com/famedly/matrix-dart-sdk/issues/1831 - .map( - (result) => ( - { - for (final event in result.$1) event.eventId: event, - }.values.toList(), - result.$2, - ), - ) - .asBroadcastStream(); - }); } void _onTabChanged() { switch (tabController.index) { case 1: - startGallerySearch(); - break; case 2: - startFileSearch(); + startSearch(); break; + case 0: default: restartSearch(); break; diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index fc55dc4cb..278478fa3 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -85,18 +85,27 @@ class ChatSearchView extends StatelessWidget { ChatSearchMessageTab( searchQuery: controller.searchController.text, room: room, - startSearch: controller.startMessageSearch, - searchStream: controller.searchStream, + onStartSearch: controller.startSearch, + events: controller.messages, + endReached: controller.messagesEndReached, + isLoading: controller.isLoading, + searchedUntil: controller.searchedUntil, ), ChatSearchImagesTab( room: room, - startSearch: controller.startGallerySearch, - searchStream: controller.galleryStream, + onStartSearch: controller.startSearch, + events: controller.images, + endReached: controller.imagesEndReached, + isLoading: controller.isLoading, + searchedUntil: controller.searchedUntil, ), ChatSearchFilesTab( room: room, - startSearch: controller.startFileSearch, - searchStream: controller.fileStream, + onStartSearch: controller.startSearch, + events: controller.files, + endReached: controller.filesEndReached, + isLoading: controller.isLoading, + searchedUntil: controller.searchedUntil, ), ], ), diff --git a/lib/pages/chat_search/search_footer.dart b/lib/pages/chat_search/search_footer.dart new file mode 100644 index 000000000..3b4e30c91 --- /dev/null +++ b/lib/pages/chat_search/search_footer.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/date_time_extension.dart'; + +class SearchFooter extends StatelessWidget { + final DateTime? searchedUntil; + final bool endReached, isLoading; + final void Function() onStartSearch; + + const SearchFooter({ + super.key, + required this.searchedUntil, + required this.endReached, + required this.isLoading, + required this.onStartSearch, + }); + + @override + Widget build(BuildContext context) { + if (endReached) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(L10n.of(context).noMoreResultsFound), + ), + ); + } + final theme = Theme.of(context); + final searchedUntil = this.searchedUntil; + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: .min, + children: [ + if (searchedUntil != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + L10n.of( + context, + ).chatSearchedUntil(searchedUntil.localizedTime(context)), + style: TextStyle(fontSize: 10.5), + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + backgroundColor: theme.colorScheme.secondaryContainer, + foregroundColor: theme.colorScheme.onSecondaryContainer, + ), + onPressed: isLoading ? null : onStartSearch, + icon: isLoading + ? const CircularProgressIndicator.adaptive() + : const Icon(Icons.arrow_downward_outlined), + label: Text(L10n.of(context).searchMore), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 6fd576ce3..c5f9e30e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1087,10 +1087,11 @@ packages: matrix: dependency: "direct main" description: - name: matrix - sha256: "2f2f697a3e2d744e5deb43800eea59ac47df58b90275c31ba272f4115880e946" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: "9bf0a7fe207deb483ccb99c939a001fa9999036c" + url: "https://github.com/famedly/matrix-dart-sdk.git" + source: git version: "4.0.1" meta: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index ec0e802a7..370028644 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,8 @@ dependencies: just_audio: ^0.10.5 latlong2: ^0.9.1 linkify: ^5.0.0 - matrix: ^4.0.1 + matrix: #^4.0.1 + git: https://github.com/famedly/matrix-dart-sdk.git mime: ^2.0.0 native_imaging: ^0.2.0 opus_caf_converter_dart: ^1.0.1