From e9d04b7217128c27b41bfa693e4558bc9db1720f Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 5 May 2024 09:22:44 +0200 Subject: [PATCH 01/11] refactor: Make reaction key not nullable --- lib/pages/chat/events/message_reactions.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index 94482d527..f3f4c0980 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -68,7 +68,7 @@ class MessageReactions extends StatelessWidget { ); } } else { - event.room.sendReaction(event.eventId, r.key!); + event.room.sendReaction(event.eventId, r.key); } }, onLongPress: () async => await _AdaptableReactorsDialog( @@ -92,7 +92,7 @@ class MessageReactions extends StatelessWidget { } class _Reaction extends StatelessWidget { - final String? reactionKey; + final String reactionKey; final int count; final bool? reacted; final void Function()? onTap; @@ -114,12 +114,12 @@ class _Reaction extends StatelessWidget { final color = Theme.of(context).colorScheme.background; final fontSize = DefaultTextStyle.of(context).style.fontSize; Widget content; - if (reactionKey!.startsWith('mxc://')) { + if (reactionKey.startsWith('mxc://')) { content = Row( mainAxisSize: MainAxisSize.min, children: [ MxcImage( - uri: Uri.parse(reactionKey!), + uri: Uri.parse(reactionKey), width: 9999, height: fontSize, ), @@ -136,7 +136,7 @@ class _Reaction extends StatelessWidget { ], ); } else { - var renderKey = Characters(reactionKey!); + var renderKey = Characters(reactionKey); if (renderKey.length > 10) { renderKey = renderKey.getRange(0, 9) + Characters('…'); } @@ -171,13 +171,13 @@ class _Reaction extends StatelessWidget { } class _ReactionEntry { - String? key; + String key; int count; bool reacted; List? reactors; _ReactionEntry({ - this.key, + required this.key, required this.count, required this.reacted, this.reactors, @@ -222,7 +222,7 @@ class _AdaptableReactorsDialog extends StatelessWidget { ), ); - final title = Center(child: Text(reactionEntry!.key!)); + final title = Center(child: Text(reactionEntry!.key)); return AlertDialog.adaptive( title: title, From 8260480d900585ecea23831ad16f5880dbd7fde8 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 5 May 2024 09:28:57 +0200 Subject: [PATCH 02/11] fix: mxc reactions not rendered correctly --- lib/pages/chat/events/message_reactions.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index f3f4c0980..c786af59f 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -112,7 +112,6 @@ class _Reaction extends StatelessWidget { ? Colors.white : Colors.black; final color = Theme.of(context).colorScheme.background; - final fontSize = DefaultTextStyle.of(context).style.fontSize; Widget content; if (reactionKey.startsWith('mxc://')) { content = Row( @@ -120,8 +119,9 @@ class _Reaction extends StatelessWidget { children: [ MxcImage( uri: Uri.parse(reactionKey), - width: 9999, - height: fontSize, + width: 20, + height: 20, + animated: false, ), if (count > 1) ...[ const SizedBox(width: 4), From 6ea4d0c26363fb64531c46f0f0c24ba8ebd47016 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Wed, 17 Apr 2024 09:26:22 +0200 Subject: [PATCH 03/11] feat: Search feature --- assets/l10n/intl_en.arb | 9 + lib/config/routes.dart | 14 ++ lib/pages/chat/chat.dart | 23 +-- lib/pages/chat/events/image_bubble.dart | 1 + .../chat_search/chat_search_files_tab.dart | 179 ++++++++++++++++ .../chat_search/chat_search_images_tab.dart | 167 +++++++++++++++ .../chat_search/chat_search_message_tab.dart | 191 ++++++++++++++++++ lib/pages/chat_search/chat_search_page.dart | 95 +++++++++ lib/pages/chat_search/chat_search_view.dart | 105 ++++++++++ lib/widgets/chat_settings_popup_menu.dart | 124 ++++++------ 10 files changed, 836 insertions(+), 72 deletions(-) create mode 100644 lib/pages/chat_search/chat_search_files_tab.dart create mode 100644 lib/pages/chat_search/chat_search_images_tab.dart create mode 100644 lib/pages/chat_search/chat_search_message_tab.dart create mode 100644 lib/pages/chat_search/chat_search_page.dart create mode 100644 lib/pages/chat_search/chat_search_view.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f93b70043..09d28fe2d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2533,6 +2533,15 @@ "level": {} } }, + "searchIn": "Search in {chat}...", + "@searchIn": { + "type": "text", + "placeholders": { + "chat": {} + } + }, + "photos": "Photos", + "files": "Files", "databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}", "@databaseBuildErrorBody": { "type": "text", diff --git a/lib/config/routes.dart b/lib/config/routes.dart index e7efe9d29..3bfbcf91f 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -13,6 +13,7 @@ import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settin import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_members/chat_members.dart'; import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart'; +import 'package:fluffychat/pages/chat_search/chat_search_page.dart'; import 'package:fluffychat/pages/device_settings/device_settings.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; @@ -131,6 +132,7 @@ abstract class AppRoutes { state, ChatPage( roomId: state.pathParameters['roomid']!, + eventId: state.uri.queryParameters['event'], ), ), redirect: loggedOutRedirect, @@ -311,10 +313,22 @@ abstract class AppRoutes { ChatPage( roomId: state.pathParameters['roomid']!, shareText: state.uri.queryParameters['body'], + eventId: state.uri.queryParameters['event'], ), ), redirect: loggedOutRedirect, routes: [ + GoRoute( + path: 'search', + pageBuilder: (context, state) => defaultPageBuilder( + context, + state, + ChatSearchPage( + roomId: state.pathParameters['roomid']!, + ), + ), + redirect: loggedOutRedirect, + ), GoRoute( path: 'encryption', pageBuilder: (context, state) => defaultPageBuilder( diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 64f6a2e28..1f0724ce6 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -43,10 +42,12 @@ import 'send_location_dialog.dart'; class ChatPage extends StatelessWidget { final String roomId; final String? shareText; + final String? eventId; const ChatPage({ super.key, required this.roomId, + this.eventId, this.shareText, }); @@ -70,6 +71,7 @@ class ChatPage extends StatelessWidget { key: Key('chat_page_$roomId'), room: room, shareText: shareText, + eventId: eventId, ); } } @@ -77,11 +79,13 @@ class ChatPage extends StatelessWidget { class ChatPageWithRoom extends StatefulWidget { final Room room; final String? shareText; + final String? eventId; const ChatPageWithRoom({ super.key, required this.room, this.shareText, + this.eventId, }); @override @@ -257,12 +261,14 @@ class ChatController extends State void initState() { scrollController.addListener(_updateScrollController); inputFocus.addListener(_inputFocusListener); + _loadDraft(); super.initState(); _displayChatDetailsColumn = ValueNotifier( Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ?? false, ); + sendingClient = Matrix.of(context).client; WidgetsBinding.instance.addObserver(this); _tryLoadTimeline(); @@ -272,7 +278,8 @@ class ChatController extends State } void _tryLoadTimeline() async { - loadTimelineFuture = _getTimeline(); + readMarkerEventId = widget.eventId; + loadTimelineFuture = _getTimeline(eventContextId: readMarkerEventId); try { await loadTimelineFuture; final fullyRead = room.fullyRead; @@ -352,18 +359,6 @@ class ChatController extends State timeline!.requestKeys(onlineKeyBackupOnly: false); if (room.markedUnread) room.markUnread(false); - // when the scroll controller is attached we want to scroll to an event id, if specified - // and update the scroll controller...which will trigger a request history, if the - // "load more" button is visible on the screen - SchedulerBinding.instance.addPostFrameCallback((_) async { - if (mounted) { - final event = GoRouterState.of(context).uri.queryParameters['event']; - if (event != null) { - scrollToEventId(event); - } - } - }); - return; } diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index 83d902d28..f5219b054 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -71,6 +71,7 @@ class ImageBubble extends StatelessWidget { this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); return Material( color: Colors.transparent, + clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: borderRadius, side: BorderSide( diff --git a/lib/pages/chat_search/chat_search_files_tab.dart b/lib/pages/chat_search/chat_search_files_tab.dart new file mode 100644 index 000000000..37030ba03 --- /dev/null +++ b/lib/pages/chat_search/chat_search_files_tab.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.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; + + const ChatSearchFilesTab({ + required this.room, + required this.startSearch, + required this.searchStream, + super.key, + }); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: searchStream, + builder: (context, snapshot) { + if (searchStream == null) { + return Column( + mainAxisAlignment: 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 events = snapshot.data?.$1 + .where((event) => event.messageType == MessageTypes.File) + .toList() ?? + []; + + if (events.isEmpty) { + return Column( + mainAxisAlignment: 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( + 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.of(context).colorScheme.secondaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onSecondaryContainer, + ), + onPressed: () => startSearch( + prevBatch: nextBatch, + previousSearchResult: events, + ), + icon: const Icon( + Icons.arrow_downward_outlined, + ), + label: const Text('Search more...'), + ), + ), + ); + } + 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: MainAxisSize.min, + children: [ + if (!sameEnvironment) ...[ + Row( + children: [ + Expanded( + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + event.originServerTs.localizedTime(context), + style: Theme.of(context).textTheme.labelSmall, + textAlign: TextAlign.center, + ), + ), + Expanded( + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + ], + ), + const SizedBox(height: 4), + ], + Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + color: Theme.of(context).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 new file mode 100644 index 000000000..809f0f2ed --- /dev/null +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:intl/intl.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat/events/image_bubble.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; + +class ChatSearchImagesTab extends StatelessWidget { + final Room room; + final Stream<(List, String?)>? searchStream; + final void Function({ + String? prevBatch, + List? previousSearchResult, + }) startSearch; + + const ChatSearchImagesTab({ + required this.room, + required this.startSearch, + required this.searchStream, + super.key, + }); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: searchStream, + builder: (context, snapshot) { + if (searchStream == null) { + return Column( + mainAxisAlignment: 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 events = snapshot.data?.$1 + .where((event) => event.messageType == MessageTypes.Image) + .toList() ?? + []; + 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) { + 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.of(context).colorScheme.secondaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onSecondaryContainer, + ), + onPressed: () => startSearch( + prevBatch: nextBatch, + previousSearchResult: events, + ), + icon: const Icon( + Icons.arrow_downward_outlined, + ), + label: const Text('Search more...'), + ), + ), + ); + } + + final monthEvents = eventsByMonthList[i].value; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + DateFormat.yMMMM( + Localizations.localeOf(context).languageCode, + ).format(eventsByMonthList[i].key), + style: Theme.of(context).textTheme.labelSmall, + textAlign: TextAlign.center, + ), + ), + Expanded( + child: Container( + height: 1, + color: Theme.of(context).dividerColor, + ), + ), + ], + ), + GridView.count( + shrinkWrap: true, + mainAxisSpacing: padding, + crossAxisSpacing: padding, + padding: const EdgeInsets.all(padding), + crossAxisCount: 3, + children: monthEvents + .map( + (event) => ImageBubble( + event, + fit: BoxFit.cover, + ), + ) + .toList(), + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart new file mode 100644 index 000000000..a904ed34a --- /dev/null +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.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'; +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; + + const ChatSearchMessageTab({ + required this.searchQuery, + required this.room, + required this.searchStream, + required this.startSearch, + super.key, + }); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + key: ValueKey(searchQuery), + stream: searchStream, + builder: (context, snapshot) { + if (searchStream == null) { + return Column( + mainAxisAlignment: 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 events = snapshot.data?.$1 + .where( + (event) => { + MessageTypes.Text, + MessageTypes.Notice, + }.contains(event.messageType), + ) + .toList() ?? + []; + + return SelectionArea( + child: ListView.separated( + itemCount: events.length + 1, + separatorBuilder: (context, _) => Divider( + color: Theme.of(context).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.of(context).colorScheme.secondaryContainer, + foregroundColor: + Theme.of(context).colorScheme.onSecondaryContainer, + ), + onPressed: () => startSearch( + prevBatch: nextBatch, + previousSearchResult: events, + ), + icon: const Icon( + Icons.arrow_downward_outlined, + ), + label: const Text('Search more...'), + ), + ), + ); + } + 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, + ); + }, + ), + ); + }, + ); + } +} + +class _MessageSearchResultListTile extends StatelessWidget { + const _MessageSearchResultListTile({ + required this.sender, + required this.displayname, + required this.event, + required this.room, + }); + + final User sender; + final String displayname; + final Event event; + final Room room; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Row( + children: [ + Avatar( + mxContent: sender.avatarUrl, + name: displayname, + size: 16, + ), + const SizedBox(width: 8), + Text( + displayname, + ), + Expanded( + child: Text( + ' | ${event.originServerTs.localizedTimeShort(context)}', + style: const TextStyle(fontSize: 12), + ), + ), + ], + ), + subtitle: Linkify( + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + decorationColor: Theme.of(context).colorScheme.primary, + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + text: event.calcLocalizedBodyFallback( + plaintextBody: true, + removeMarkdown: true, + MatrixLocals( + L10n.of(context)!, + ), + ), + maxLines: 4, + ), + trailing: IconButton( + icon: const Icon( + Icons.chevron_right_outlined, + ), + onPressed: () => context.go( + '/${Uri( + pathSegments: ['rooms', room.id], + queryParameters: {'event': event.eventId}, + )}', + ), + ), + ); + } +} diff --git a/lib/pages/chat_search/chat_search_page.dart b/lib/pages/chat_search/chat_search_page.dart new file mode 100644 index 000000000..a07bc4e9c --- /dev/null +++ b/lib/pages/chat_search/chat_search_page.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/chat_search/chat_search_view.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ChatSearchPage extends StatefulWidget { + final String roomId; + const ChatSearchPage({required this.roomId, super.key}); + + @override + ChatSearchController createState() => ChatSearchController(); +} + +class ChatSearchController extends State + with SingleTickerProviderStateMixin { + Room? get room => Matrix.of(context).client.getRoomById(widget.roomId); + + final TextEditingController searchController = TextEditingController(); + late final TabController tabController; + + Timeline? timeline; + + Stream<(List, String?)>? searchStream; + + void restartSearch() { + if (tabController.index == 0 && searchController.text.isEmpty) { + setState(() { + searchStream = null; + }); + return; + } + setState(() { + searchStream = const Stream.empty(); + }); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + startSearch(); + }); + } + + void startSearch({ + 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: tabController.index == 0 ? searchController.text : null, + searchFunc: switch (tabController.index) { + 1 => (event) => event.messageType == MessageTypes.Image, + 2 => (event) => event.messageType == MessageTypes.File, + int() => null, + }, + prevBatch: prevBatch, + requestHistoryCount: 1000, + limit: 32, + ) + .map( + (result) => ( + [ + if (previousSearchResult != null) ...previousSearchResult, + ...result.$1, + ], + result.$2, + ), + ) + .asBroadcastStream(); + }); + } + + @override + void initState() { + super.initState(); + tabController = TabController(initialIndex: 0, length: 3, vsync: this); + tabController.addListener(restartSearch); + } + + @override + void dispose() { + tabController.removeListener(restartSearch); + super.dispose(); + } + + @override + Widget build(BuildContext context) => ChatSearchView(this); +} diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart new file mode 100644 index 000000000..48c7cb603 --- /dev/null +++ b/lib/pages/chat_search/chat_search_view.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat_search/chat_search_files_tab.dart'; +import 'package:fluffychat/pages/chat_search/chat_search_images_tab.dart'; +import 'package:fluffychat/pages/chat_search/chat_search_message_tab.dart'; +import 'package:fluffychat/pages/chat_search/chat_search_page.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; + +class ChatSearchView extends StatelessWidget { + final ChatSearchController controller; + + const ChatSearchView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final room = controller.room; + if (room == null) { + return Scaffold( + appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: + Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat), + ), + ), + ); + } + + return Scaffold( + appBar: AppBar( + leading: const Center(child: BackButton()), + titleSpacing: 0, + title: Text( + L10n.of(context)!.searchIn( + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + ), + ), + ), + body: MaxWidthBody( + withScrolling: false, + child: Column( + children: [ + if (FluffyThemes.isThreeColumnMode(context)) + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: TextField( + controller: controller.searchController, + onSubmitted: (_) => controller.restartSearch(), + autofocus: true, + enabled: controller.tabController.index == 0, + decoration: InputDecoration( + hintText: L10n.of(context)!.searchIn( + room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)!), + ), + ), + suffixIcon: const Icon(Icons.search_outlined), + ), + ), + ), + TabBar( + controller: controller.tabController, + tabs: [ + Tab(child: Text(L10n.of(context)!.messages)), + Tab(child: Text(L10n.of(context)!.photos)), + Tab(child: Text(L10n.of(context)!.files)), + ], + ), + Expanded( + child: TabBarView( + controller: controller.tabController, + children: [ + ChatSearchMessageTab( + searchQuery: controller.searchController.text, + room: room, + startSearch: controller.startSearch, + searchStream: controller.searchStream, + ), + ChatSearchImagesTab( + room: room, + startSearch: controller.startSearch, + searchStream: controller.searchStream, + ), + ChatSearchFilesTab( + room: room, + startSearch: controller.startSearch, + searchStream: controller.searchStream, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 277e7170f..23732aeb5 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -12,6 +12,8 @@ import 'package:matrix/matrix.dart'; import 'matrix.dart'; +enum ChatPopupMenuActions { details, mute, unmute, leave, search } + class ChatSettingsPopupMenu extends StatefulWidget { final Room room; final bool displayChatDetails; @@ -41,54 +43,6 @@ class ChatSettingsPopupMenuState extends State { .listen( (u) => setState(() {}), ); - final items = >[ - widget.room.pushRuleState == PushRuleState.notify - ? PopupMenuItem( - value: 'mute', - child: Row( - children: [ - const Icon(Icons.notifications_off_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.muteChat), - ], - ), - ) - : PopupMenuItem( - value: 'unmute', - child: Row( - children: [ - const Icon(Icons.notifications_on_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.unmuteChat), - ], - ), - ), - PopupMenuItem( - value: 'leave', - child: Row( - children: [ - const Icon(Icons.delete_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.leave), - ], - ), - ), - ]; - if (widget.displayChatDetails) { - items.insert( - 0, - PopupMenuItem( - value: 'details', - child: Row( - children: [ - const Icon(Icons.info_outline_rounded), - const SizedBox(width: 12), - Text(L10n.of(context)!.chatDetails), - ], - ), - ), - ); - } return Stack( alignment: Alignment.center, children: [ @@ -101,10 +55,10 @@ class ChatSettingsPopupMenuState extends State { onKeysPressed: _showChatDetails, child: const SizedBox.shrink(), ), - PopupMenuButton( - onSelected: (String choice) async { + PopupMenuButton( + onSelected: (choice) async { switch (choice) { - case 'leave': + case ChatPopupMenuActions.leave: final confirmed = await showOkCancelAlertDialog( useRootNavigator: false, context: context, @@ -123,29 +77,83 @@ class ChatSettingsPopupMenuState extends State { } } break; - case 'mute': + case ChatPopupMenuActions.mute: await showFutureLoadingDialog( context: context, future: () => widget.room.setPushRuleState(PushRuleState.mentionsOnly), ); break; - case 'unmute': + case ChatPopupMenuActions.unmute: await showFutureLoadingDialog( context: context, future: () => widget.room.setPushRuleState(PushRuleState.notify), ); break; - case 'todos': - context.go('/rooms/${widget.room.id}/tasks'); - break; - case 'details': + case ChatPopupMenuActions.details: _showChatDetails(); break; + case ChatPopupMenuActions.search: + context.go('/rooms/${widget.room.id}/search'); + break; } }, - itemBuilder: (BuildContext context) => items, + itemBuilder: (BuildContext context) => [ + if (widget.displayChatDetails) + PopupMenuItem( + value: ChatPopupMenuActions.details, + child: Row( + children: [ + const Icon(Icons.info_outline_rounded), + const SizedBox(width: 12), + Text(L10n.of(context)!.chatDetails), + ], + ), + ), + if (widget.room.pushRuleState == PushRuleState.notify) + PopupMenuItem( + value: ChatPopupMenuActions.mute, + child: Row( + children: [ + const Icon(Icons.notifications_off_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.muteChat), + ], + ), + ) + else + PopupMenuItem( + value: ChatPopupMenuActions.unmute, + child: Row( + children: [ + const Icon(Icons.notifications_on_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.unmuteChat), + ], + ), + ), + PopupMenuItem( + value: ChatPopupMenuActions.search, + child: Row( + children: [ + const Icon(Icons.search_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.search), + ], + ), + ), + PopupMenuItem( + value: ChatPopupMenuActions.leave, + child: Row( + children: [ + const Icon(Icons.delete_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.leave), + ], + ), + ), + ], ), ], ); From d8191288819f67c5d647a4561688c2e0ac545eed Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 5 May 2024 13:37:43 +0200 Subject: [PATCH 04/11] chore: Follow up search --- .../chat_search/chat_search_files_tab.dart | 9 +- .../chat_search/chat_search_images_tab.dart | 9 +- .../chat_search/chat_search_message_tab.dart | 29 +++--- lib/pages/chat_search/chat_search_page.dart | 93 ++++++++++++++++--- lib/pages/chat_search/chat_search_view.dart | 10 +- 5 files changed, 105 insertions(+), 45 deletions(-) diff --git a/lib/pages/chat_search/chat_search_files_tab.dart b/lib/pages/chat_search/chat_search_files_tab.dart index 37030ba03..db85b2af3 100644 --- a/lib/pages/chat_search/chat_search_files_tab.dart +++ b/lib/pages/chat_search/chat_search_files_tab.dart @@ -28,11 +28,12 @@ class ChatSearchFilesTab extends StatelessWidget { return StreamBuilder( stream: searchStream, builder: (context, snapshot) { - if (searchStream == null) { + final events = snapshot.data?.$1; + if (searchStream == null || events == null) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.search_outlined, size: 64), + const CircularProgressIndicator.adaptive(strokeWidth: 2), const SizedBox(height: 8), Text( L10n.of(context)!.searchIn( @@ -44,10 +45,6 @@ class ChatSearchFilesTab extends StatelessWidget { ], ); } - final events = snapshot.data?.$1 - .where((event) => event.messageType == MessageTypes.File) - .toList() ?? - []; if (events.isEmpty) { return Column( diff --git a/lib/pages/chat_search/chat_search_images_tab.dart b/lib/pages/chat_search/chat_search_images_tab.dart index 809f0f2ed..958055b2f 100644 --- a/lib/pages/chat_search/chat_search_images_tab.dart +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -27,11 +27,12 @@ class ChatSearchImagesTab extends StatelessWidget { return StreamBuilder( stream: searchStream, builder: (context, snapshot) { - if (searchStream == null) { + final events = snapshot.data?.$1; + if (searchStream == null || events == null) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.search_outlined, size: 64), + const CircularProgressIndicator.adaptive(strokeWidth: 2), const SizedBox(height: 8), Text( L10n.of(context)!.searchIn( @@ -43,10 +44,6 @@ class ChatSearchImagesTab extends StatelessWidget { ], ); } - final events = snapshot.data?.$1 - .where((event) => event.messageType == MessageTypes.Image) - .toList() ?? - []; if (events.isEmpty) { return Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart index a904ed34a..44f2adf00 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -49,15 +49,7 @@ class ChatSearchMessageTab extends StatelessWidget { ], ); } - final events = snapshot.data?.$1 - .where( - (event) => { - MessageTypes.Text, - MessageTypes.Notice, - }.contains(event.messageType), - ) - .toList() ?? - []; + final events = snapshot.data?.$1 ?? []; return SelectionArea( child: ListView.separated( @@ -166,14 +158,17 @@ class _MessageSearchResultListTile extends StatelessWidget { decorationColor: Theme.of(context).colorScheme.primary, ), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), - text: event.calcLocalizedBodyFallback( - plaintextBody: true, - removeMarkdown: true, - MatrixLocals( - L10n.of(context)!, - ), - ), - maxLines: 4, + text: event + .calcLocalizedBodyFallback( + plaintextBody: true, + removeMarkdown: true, + MatrixLocals( + L10n.of(context)!, + ), + ) + .trim(), + maxLines: 7, + overflow: TextOverflow.ellipsis, ), trailing: IconButton( icon: const Icon( diff --git a/lib/pages/chat_search/chat_search_page.dart b/lib/pages/chat_search/chat_search_page.dart index a07bc4e9c..306c6130a 100644 --- a/lib/pages/chat_search/chat_search_page.dart +++ b/lib/pages/chat_search/chat_search_page.dart @@ -25,9 +25,11 @@ class ChatSearchController extends State Timeline? timeline; Stream<(List, String?)>? searchStream; + Stream<(List, String?)>? galleryStream; + Stream<(List, String?)>? fileStream; void restartSearch() { - if (tabController.index == 0 && searchController.text.isEmpty) { + if (searchController.text.isEmpty) { setState(() { searchStream = null; }); @@ -37,11 +39,11 @@ class ChatSearchController extends State searchStream = const Stream.empty(); }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - startSearch(); + startMessageSearch(); }); } - void startSearch({ + void startMessageSearch({ String? prevBatch, List? previousSearchResult, }) async { @@ -54,12 +56,67 @@ class ChatSearchController extends State setState(() { searchStream = timeline .startSearch( - searchTerm: tabController.index == 0 ? searchController.text : null, - searchFunc: switch (tabController.index) { - 1 => (event) => event.messageType == MessageTypes.Image, - 2 => (event) => event.messageType == MessageTypes.File, - int() => null, - }, + searchTerm: searchController.text, + prevBatch: prevBatch, + requestHistoryCount: 1000, + limit: 32, + ) + .map( + (result) => ( + [ + if (previousSearchResult != null) ...previousSearchResult, + ...result.$1, + ], + result.$2, + ), + ) + .asBroadcastStream(); + }); + } + + void startGallerySearch({ + String? prevBatch, + List? previousSearchResult, + }) async { + final timeline = this.timeline ??= await room!.getTimeline(); + + setState(() { + galleryStream = 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, + ), + ) + .asBroadcastStream(); + }); + } + + void startFileSearch({ + String? prevBatch, + List? previousSearchResult, + }) async { + final timeline = this.timeline ??= await room!.getTimeline(); + + setState(() { + fileStream = timeline + .startSearch( + searchFunc: (event) => { + MessageTypes.Image, + MessageTypes.Video, + }.contains(event.messageType), prevBatch: prevBatch, requestHistoryCount: 1000, limit: 32, @@ -77,16 +134,30 @@ class ChatSearchController extends State }); } + void _onTabChanged() { + switch (tabController.index) { + case 1: + startGallerySearch(); + break; + case 2: + startFileSearch(); + break; + default: + restartSearch(); + break; + } + } + @override void initState() { super.initState(); tabController = TabController(initialIndex: 0, length: 3, vsync: this); - tabController.addListener(restartSearch); + tabController.addListener(_onTabChanged); } @override void dispose() { - tabController.removeListener(restartSearch); + tabController.removeListener(_onTabChanged); super.dispose(); } diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index 48c7cb603..c4c4824ee 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -81,18 +81,18 @@ class ChatSearchView extends StatelessWidget { ChatSearchMessageTab( searchQuery: controller.searchController.text, room: room, - startSearch: controller.startSearch, + startSearch: controller.startMessageSearch, searchStream: controller.searchStream, ), ChatSearchImagesTab( room: room, - startSearch: controller.startSearch, - searchStream: controller.searchStream, + startSearch: controller.startGallerySearch, + searchStream: controller.galleryStream, ), ChatSearchFilesTab( room: room, - startSearch: controller.startSearch, - searchStream: controller.searchStream, + startSearch: controller.startFileSearch, + searchStream: controller.fileStream, ), ], ), From 9d4145b49d4dc93df2738a4e460cbfd80beb6d59 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 5 May 2024 13:52:06 +0200 Subject: [PATCH 05/11] chore: Follow up search feature --- lib/pages/chat_search/chat_search_page.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/pages/chat_search/chat_search_page.dart b/lib/pages/chat_search/chat_search_page.dart index 306c6130a..c2b08d34d 100644 --- a/lib/pages/chat_search/chat_search_page.dart +++ b/lib/pages/chat_search/chat_search_page.dart @@ -83,10 +83,10 @@ class ChatSearchController extends State setState(() { galleryStream = timeline .startSearch( - searchFunc: (event) => - event.messageType == MessageTypes.File || - (event.messageType == MessageTypes.Audio && - !event.content.containsKey('org.matrix.msc3245.voice')), + searchFunc: (event) => { + MessageTypes.Image, + MessageTypes.Video, + }.contains(event.messageType), prevBatch: prevBatch, requestHistoryCount: 1000, limit: 32, @@ -113,10 +113,10 @@ class ChatSearchController extends State setState(() { fileStream = timeline .startSearch( - searchFunc: (event) => { - MessageTypes.Image, - MessageTypes.Video, - }.contains(event.messageType), + searchFunc: (event) => + event.messageType == MessageTypes.File || + (event.messageType == MessageTypes.Audio && + !event.content.containsKey('org.matrix.msc3245.voice')), prevBatch: prevBatch, requestHistoryCount: 1000, limit: 32, From e40e24e97dca02a3c9b938aa6a65ada8493b453d Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 5 May 2024 14:08:21 +0200 Subject: [PATCH 06/11] chore: Follow up search --- lib/pages/chat_search/chat_search_images_tab.dart | 1 + lib/pages/chat_search/chat_search_view.dart | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pages/chat_search/chat_search_images_tab.dart b/lib/pages/chat_search/chat_search_images_tab.dart index 958055b2f..9c99ade8e 100644 --- a/lib/pages/chat_search/chat_search_images_tab.dart +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -140,6 +140,7 @@ class ChatSearchImagesTab extends StatelessWidget { ], ), GridView.count( + physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, mainAxisSpacing: padding, crossAxisSpacing: padding, diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index c4c4824ee..dccdf3c66 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -57,11 +57,7 @@ class ChatSearchView extends StatelessWidget { autofocus: true, enabled: controller.tabController.index == 0, decoration: InputDecoration( - hintText: L10n.of(context)!.searchIn( - room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ), - ), + hintText: L10n.of(context)!.search, suffixIcon: const Icon(Icons.search_outlined), ), ), From b20b599ce4c3a049579801f9ec199bbe4e8ae8bd Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 5 May 2024 14:17:12 +0200 Subject: [PATCH 07/11] chore: Follow up search --- assets/l10n/intl_en.arb | 2 +- lib/pages/chat/events/message_content.dart | 5 +--- lib/pages/chat/events/video_player.dart | 28 +++++++++++-------- .../chat_search/chat_search_images_tab.dart | 20 +++++++------ lib/pages/chat_search/chat_search_view.dart | 2 +- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 09d28fe2d..cad529e89 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2540,7 +2540,7 @@ "chat": {} } }, - "photos": "Photos", + "gallery": "Gallery", "files": "Files", "databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}", "@databaseBuildErrorBody": { diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 9d8f6003c..24c768f2d 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -155,10 +155,7 @@ class MessageContent extends StatelessWidget { } return MessageDownloadContent(event, textColor); case MessageTypes.Video: - if (PlatformInfos.isMobile || PlatformInfos.isWeb) { - return EventVideoPlayer(event); - } - return MessageDownloadContent(event, textColor); + return EventVideoPlayer(event); case MessageTypes.File: return MessageDownloadContent(event, textColor); diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index 66e50b783..2cdbbc84c 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -10,9 +10,11 @@ import 'package:path_provider/path_provider.dart'; import 'package:universal_html/html.dart' as html; import 'package:video_player/video_player.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/image_bubble.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/blur_hash.dart'; import '../../../utils/error_reporter.dart'; @@ -31,6 +33,10 @@ class EventVideoPlayerState extends State { File? _tmpFile; void _downloadAction() async { + if (PlatformInfos.isDesktop) { + widget.event.saveFile(context); + return; + } setState(() => _isDownloading = true); try { final videoFile = await widget.event.downloadAndDecryptAttachment(); @@ -98,6 +104,7 @@ class EventVideoPlayerState extends State { final chewieManager = _chewieManager; return Material( color: Colors.black, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), child: SizedBox( height: 300, child: chewieManager != null @@ -114,9 +121,10 @@ class EventVideoPlayerState extends State { else BlurHash(blurhash: blurHash, width: 300, height: 300), Center( - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.surface, + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.background, ), icon: _isDownloading ? const SizedBox( @@ -126,14 +134,12 @@ class EventVideoPlayerState extends State { strokeWidth: 2, ), ) - : const Icon(Icons.download_outlined), - label: Text( - _isDownloading - ? L10n.of(context)!.loadingPleaseWait - : L10n.of(context)!.videoWithSize( - widget.event.sizeString ?? '?MB', - ), - ), + : const Icon(Icons.play_circle_outlined), + tooltip: _isDownloading + ? L10n.of(context)!.loadingPleaseWait + : L10n.of(context)!.videoWithSize( + widget.event.sizeString ?? '?MB', + ), onPressed: _isDownloading ? null : _downloadAction, ), ), diff --git a/lib/pages/chat_search/chat_search_images_tab.dart b/lib/pages/chat_search/chat_search_images_tab.dart index 9c99ade8e..e48e5c464 100644 --- a/lib/pages/chat_search/chat_search_images_tab.dart +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/events/image_bubble.dart'; +import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; class ChatSearchImagesTab extends StatelessWidget { @@ -146,14 +147,17 @@ class ChatSearchImagesTab extends StatelessWidget { crossAxisSpacing: padding, padding: const EdgeInsets.all(padding), crossAxisCount: 3, - children: monthEvents - .map( - (event) => ImageBubble( - event, - fit: BoxFit.cover, - ), - ) - .toList(), + children: monthEvents.map( + (event) { + if (event.messageType == MessageTypes.Video) { + return EventVideoPlayer(event); + } + return ImageBubble( + event, + fit: BoxFit.cover, + ); + }, + ).toList(), ), ], ); diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index dccdf3c66..e08f25a2b 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -66,7 +66,7 @@ class ChatSearchView extends StatelessWidget { controller: controller.tabController, tabs: [ Tab(child: Text(L10n.of(context)!.messages)), - Tab(child: Text(L10n.of(context)!.photos)), + Tab(child: Text(L10n.of(context)!.gallery)), Tab(child: Text(L10n.of(context)!.files)), ], ), From 18054aaa6644a557ff4f154ae1e23c4cbfa3ac5e Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 5 May 2024 14:40:24 +0200 Subject: [PATCH 08/11] chore: Follow up search --- assets/l10n/intl_en.arb | 1 + lib/pages/chat_search/chat_search_files_tab.dart | 2 +- lib/pages/chat_search/chat_search_images_tab.dart | 2 +- lib/pages/chat_search/chat_search_message_tab.dart | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index cad529e89..00f7356f5 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2540,6 +2540,7 @@ "chat": {} } }, + "searchMore": "Search more...", "gallery": "Gallery", "files": "Files", "databaseBuildErrorBody": "Unable to build the SQlite database. The app tries to use the legacy database for now. Please report this error to the developers at {url}. The error message is: {error}", diff --git a/lib/pages/chat_search/chat_search_files_tab.dart b/lib/pages/chat_search/chat_search_files_tab.dart index db85b2af3..85525ab6f 100644 --- a/lib/pages/chat_search/chat_search_files_tab.dart +++ b/lib/pages/chat_search/chat_search_files_tab.dart @@ -94,7 +94,7 @@ class ChatSearchFilesTab extends StatelessWidget { icon: const Icon( Icons.arrow_downward_outlined, ), - label: const Text('Search more...'), + label: Text(L10n.of(context)!.searchMore), ), ), ); diff --git a/lib/pages/chat_search/chat_search_images_tab.dart b/lib/pages/chat_search/chat_search_images_tab.dart index e48e5c464..9f8c1f0c9 100644 --- a/lib/pages/chat_search/chat_search_images_tab.dart +++ b/lib/pages/chat_search/chat_search_images_tab.dart @@ -103,7 +103,7 @@ class ChatSearchImagesTab extends StatelessWidget { icon: const Icon( Icons.arrow_downward_outlined, ), - label: const Text('Search more...'), + label: Text(L10n.of(context)!.searchMore), ), ), ); diff --git a/lib/pages/chat_search/chat_search_message_tab.dart b/lib/pages/chat_search/chat_search_message_tab.dart index 44f2adf00..7542d6ae9 100644 --- a/lib/pages/chat_search/chat_search_message_tab.dart +++ b/lib/pages/chat_search/chat_search_message_tab.dart @@ -91,7 +91,7 @@ class ChatSearchMessageTab extends StatelessWidget { icon: const Icon( Icons.arrow_downward_outlined, ), - label: const Text('Search more...'), + label: Text(L10n.of(context)!.searchMore), ), ), ); From 4291396f987158612eff69ade384257f0a9f5e93 Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 7 May 2024 12:36:35 +0200 Subject: [PATCH 09/11] design: Improve design of Voice Messages and add 1.25 as speed --- lib/pages/chat/events/audio_player.dart | 79 ++++++++++++++----------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 7d399e6fa..77eb26832 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -38,8 +38,8 @@ class AudioPlayerState extends State { StreamSubscription? onPlayerError; String? statusText; - int currentPosition = 0; - double maxPosition = 0; + double currentPosition = 0.0; + double maxPosition = 1.0; MatrixFile? matrixFile; File? audioFile; @@ -113,9 +113,7 @@ class AudioPlayerState extends State { setState(() { statusText = '${state.inMinutes.toString().padLeft(2, '0')}:${(state.inSeconds % 60).toString().padLeft(2, '0')}'; - currentPosition = ((state.inMilliseconds.toDouble() / maxPosition) * - AudioPlayerWidget.wavesCount) - .round(); + currentPosition = state.inMilliseconds.toDouble(); }); if (state.inMilliseconds.toDouble() == maxPosition) { audioPlayer.stop(); @@ -151,12 +149,14 @@ class AudioPlayerState extends State { return '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'; } - List _getWaveform() { + List? _getWaveform() { final eventWaveForm = widget.event.content .tryGetMap('org.matrix.msc1767.audio') ?.tryGetList('waveform'); - if (eventWaveForm == null || eventWaveForm.isEmpty) { - return List.filled(AudioPlayerWidget.wavesCount, 500); + if (eventWaveForm == null || + eventWaveForm.isEmpty || + eventWaveForm.length > 100) { + return null; } while (eventWaveForm.length < AudioPlayerWidget.wavesCount) { for (var i = 0; i < eventWaveForm.length; i = i + 2) { @@ -172,13 +172,16 @@ class AudioPlayerState extends State { return eventWaveForm.map((i) => i > 1024 ? 1024 : i).toList(); } - late final List waveform; + late final List? waveform; void _toggleSpeed() async { final audioPlayer = this.audioPlayer; if (audioPlayer == null) return; switch (audioPlayer.speed) { case 1.0: + await audioPlayer.setSpeed(1.25); + break; + case 1.25: await audioPlayer.setSpeed(1.5); break; case 1.5: @@ -205,6 +208,7 @@ class AudioPlayerState extends State { Widget build(BuildContext context) { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; + final waveform = this.waveform; return Padding( padding: const EdgeInsets.all(16), child: Row( @@ -237,42 +241,47 @@ class AudioPlayerState extends State { }, ), ), - const SizedBox(width: 8), Expanded( - child: Row( + child: Stack( children: [ - for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) - Expanded( - child: GestureDetector( - onTapDown: (_) => audioPlayer?.seek( - Duration( - milliseconds: - (maxPosition / AudioPlayerWidget.wavesCount) - .round() * - i, - ), - ), - child: Container( - height: 32, - alignment: Alignment.center, - child: Opacity( - opacity: currentPosition > i ? 1 : 0.5, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 1), - decoration: BoxDecoration( - color: widget.color, - borderRadius: BorderRadius.circular(64), + if (waveform != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + for (var i = 0; i < waveform.length; i++) + Expanded( + child: Center( + child: Container( + decoration: BoxDecoration( + color: widget.color.withAlpha(64), + borderRadius: BorderRadius.circular(2), + ), + height: 32 * (waveform[i] / 1024), + ), ), - height: 32 * (waveform[i] / 1024), ), - ), + ], + ), + ), + SizedBox( + height: 28, + child: Slider.adaptive( + value: currentPosition, + min: 0, + max: maxPosition, + onChangeStart: (_) => audioPlayer?.pause(), + onChangeEnd: (_) => audioPlayer?.play(), + onChanged: (pos) => audioPlayer?.seek( + Duration( + milliseconds: pos.round(), ), ), ), + ), ], ), ), - const SizedBox(width: 8), Container( alignment: Alignment.centerRight, width: 42, From 5a3bc23a72f88de89759344f3bbc8f9a7d5c2080 Mon Sep 17 00:00:00 2001 From: Yurt Page Date: Tue, 7 May 2024 20:34:02 +0300 Subject: [PATCH 10/11] fastlane: improve full_description.txt Make it easier to translate. Make "matrix" capitalized to "Matrix" so that translators will leave it in English. The "lovers" may be translated as "mistresses" so I changed to "your loved" --- .../fastlane/metadata/android/en-US/full_description.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/android/fastlane/metadata/android/en-US/full_description.txt b/android/fastlane/metadata/android/en-US/full_description.txt index e3b1bf5a9..64d6b1cef 100644 --- a/android/fastlane/metadata/android/en-US/full_description.txt +++ b/android/fastlane/metadata/android/en-US/full_description.txt @@ -1,4 +1,4 @@ -FluffyChat is an open, nonprofit and cute matrix messenger app for Ubuntu Touch, Android and iOS. +FluffyChat is an open, nonprofit and cute Matrix messenger app for Ubuntu Touch, Android and iOS. Open Opensource and open development where everyone can join. @@ -9,7 +9,7 @@ FluffyChat is donation funded. Cute ♥ Cute design and many theme settings including a dark mode. -One-to-one and groupchats +One-to-one and group chats Unlimited groups and direct chats. Easy @@ -22,11 +22,11 @@ Decentralized There is no "FluffyChat server" you are forced to use. Use the server you find trustworthy or host your own. Compatible -Compatible with Element, Fractal, Nheko and all matrix messengers. +Compatible with Element, Fractal, Nheko and all Matrix messengers. FluffyChat comes with a dream Imagine a world where everyone can choose the messenger they like and is still able to chat with all of their friends. -A world where there are no companies spying on you when you send selfies to friends and lovers. +A world where there are no companies spying on you when you send selfies to friends and your loved. And a world where apps are made for fluffyness and not for profit. ♥ From f0276302c719197c18a0e3967392311ee0af3663 Mon Sep 17 00:00:00 2001 From: Yurt Page Date: Tue, 7 May 2024 20:35:15 +0300 Subject: [PATCH 11/11] fastlane: i18n ru --- .../metadata/android/ru/full_description.txt | 32 +++++++++++++++++++ .../metadata/android/ru/short_description.txt | 1 + 2 files changed, 33 insertions(+) create mode 100644 android/fastlane/metadata/android/ru/full_description.txt create mode 100644 android/fastlane/metadata/android/ru/short_description.txt diff --git a/android/fastlane/metadata/android/ru/full_description.txt b/android/fastlane/metadata/android/ru/full_description.txt new file mode 100644 index 000000000..e12c29e46 --- /dev/null +++ b/android/fastlane/metadata/android/ru/full_description.txt @@ -0,0 +1,32 @@ +FluffyChat это свободный, некоммерческий и милый чат Matrix для Ubuntu Touch, Android и iOS. + +Открыть +Открытый исходный код и открытая разработка, где присоединиться может каждый. + +Некоммерческий +FluffyChat финансируется пожертвованиями. + +Милый ♥ +Симпатичный дизайн и много настроек темы, включая тёмный режим. + +Личные и групповые чаты +Неограниченные группы и прямые чаты один на один. + +Легкий +FluffyChat сделан максимально простым в использовании. + +Бесплатный +Бесплатное использование для всех без рекламы. + +Децентрализованный +Нет единого «FluffyChat сервера» который вас принуждают использовать. Используйте сервер, который вы находите надёжным или создайте свой собственный. + +Совместимый +Совместим с Element, Fractal, Nheko и всеми мессенджерами Matrix. + + +FluffyChat стремится к мечте + +Представьте себе мир где каждый может выбрать чат который ему нравится и все еще иметь возможность общаться со всеми своими друзьями. +Мир, где нет компаний, шпионящих за тобой, когда ты посылаешь селфи друзьям и любимым. +И мир, где приложения созданы для пушистости, а не для прибыли. ♥ diff --git a/android/fastlane/metadata/android/ru/short_description.txt b/android/fastlane/metadata/android/ru/short_description.txt new file mode 100644 index 000000000..012253b60 --- /dev/null +++ b/android/fastlane/metadata/android/ru/short_description.txt @@ -0,0 +1 @@ +Общайтесь с друзьями с FluffyChat \ No newline at end of file