diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 88a4d3d2d..6c32e50f6 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3467,6 +3467,16 @@ "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", + "noMoreResultsFound": "No more results found", + "chatSearchedUntil": "Chat searched until {time}", + "@chatSearchedUntil": { + "type": "String", + "placeholders": { + "time": { + "type": "String" + } + } + }, "ignore": "Block", "ignoredUsers": "Blocked users", "writeAMessageLangCodes": "Type in {l1} or {l2}...", @@ -3527,7 +3537,6 @@ "updateLanguage": "My languages", "whatLanguageYouWantToLearn": "What language do you want to learn?", "whatIsYourBaseLanguage": "What is your base language?", - "saveChanges": "Save changes", "publicProfileTitle": "Allow my profile to be found in search", "publicProfileDesc": "By turning on, you enable other users to find your profile in the global search bar and send requests to chat. At this point, you can choose to accept or deny the request.", "errorDisableIT": "Translation assistance is turned off.", 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 c254791ae..f221b419e 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -4,6 +4,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat_search/search_footer.dart'; import 'package:fluffychat/pangea/navigation/navigation_util.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -13,101 +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 ?? []; - // #Pangea - events.removeWhere( - (event) => - event.type != EventTypes.Message || - event.messageType != MessageTypes.Text || - event.redacted, - ); - // Pangea# - - 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 353563d45..98a31a8eb 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,170 +20,112 @@ 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; - } - - 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, - // #Pangea - // }.values.toList(), - }.values - .toList() - .where( - (e) => !e.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - ), - ) - .toList(), - // Pangea# - result.$2, - ), - ) + 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; // #Pangea - .where((result) => result.$1.isNotEmpty) + // messages.addAll(result.events); + messages.addAll( + result.events.where( + (e) => + room?.timeline == null || + !e.hasAggregatedEvents( + room!.timeline!, + RelationshipTypes.edit, + ), + ), + ); // Pangea# - .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(); - }); + 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; + } } 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/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 6f6c6b597..26fed6714 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -1,3 +1,8 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; @@ -5,10 +10,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart' import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - import 'settings_security_view.dart'; class SettingsSecurity extends StatefulWidget { diff --git a/pubspec.lock b/pubspec.lock index dea6a9176..40a365a85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1475,7 +1475,7 @@ packages: path: "/Users/ggurdin/pangea/matrix-dart-sdk" relative: false source: path - version: "4.0.0" + version: "4.0.1" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 575d9c487..8a5c8ccd7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,14 +53,15 @@ dependencies: highlight: ^0.7.0 html: ^0.15.4 http: ^1.6.0 - image: ^4.6.0 + image: ^4.7.1 image_picker: ^1.2.1 intl: any just_audio: ^0.10.5 latlong2: ^0.9.1 linkify: ^5.0.0 # #Pangea - # matrix: ^4.0.1 + # matrix: #^4.0.1 + # git: https://github.com/famedly/matrix-dart-sdk.git matrix: path: /Users/ggurdin/pangea/matrix-dart-sdk # Pangea#