From 103cb8328dbabda1169bbef30cef2b16b78a0322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Sat, 21 Jun 2025 11:15:28 +0200 Subject: [PATCH] feat: Collapse all state events by default --- lib/config/app_config.dart | 2 - lib/l10n/intl_en.arb | 3 +- lib/pages/chat/chat.dart | 18 ++++ lib/pages/chat/chat_event_list.dart | 22 ++++- lib/pages/chat/events/message.dart | 6 +- lib/pages/chat/events/state_message.dart | 90 +++++++++++++------ .../settings_chat/settings_chat_view.dart | 7 -- .../filtered_timeline_extension.dart | 48 +++------- lib/widgets/matrix.dart | 4 - 9 files changed, 121 insertions(+), 79 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 11f9e8f21..a80d16e14 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -53,7 +53,6 @@ abstract class AppConfig { static bool renderHtml = true; static bool hideRedactedEvents = false; static bool hideUnknownEvents = true; - static bool hideUnimportantStateEvents = true; static bool separateChatTypes = false; static bool autoplayImages = true; static bool sendTypingNotifications = true; @@ -64,7 +63,6 @@ abstract class AppConfig { static bool displayNavigationRail = false; static bool experimentalVoip = false; static const bool hideTypingUsernames = false; - static const bool hideAllStateEvents = false; static const String inviteLinkPrefix = 'https://matrix.to/#/'; static const String deepLinkPrefix = 'im.fluffychat://chat/'; static const String schemePrefix = 'matrix:'; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 52d2c76f0..863fcc0f3 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3240,5 +3240,6 @@ "commandHint_logout": "Logout your current device", "commandHint_logoutall": "Logout all active devices", "displayNavigationRail": "Show navigation rail on mobile", - "customReaction": "Custom reaction" + "customReaction": "Custom reaction", + "moreEvents": "More events" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 308ac2b1f..13238a6a8 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -339,6 +339,24 @@ class ChatController extends State } } + final Set expandedEventIds = {}; + + void expandEventsFrom(Event event, bool expand) { + final events = timeline!.events.filterByVisibleInGui(); + final start = events.indexOf(event); + setState(() { + for (var i = start; i < events.length; i++) { + final event = events[i]; + if (!event.isCollapsedState) return; + if (expand) { + expandedEventIds.add(event.eventId); + } else { + expandedEventIds.remove(event.eventId); + } + } + }); + } + void _tryLoadTimeline() async { final initialEventId = widget.eventId; loadTimelineFuture = _getTimeline(); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 50a1c262a..7e11588a4 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -119,6 +119,17 @@ class ChatEventList extends StatelessWidget { timeline.events.length > animateInEventIndex && event == timeline.events[animateInEventIndex]; + final nextEvent = i + 1 < events.length ? events[i + 1] : null; + final previousEvent = i > 0 ? events[i - 1] : null; + + // Collapsed state event + final canExpand = event.isCollapsedState && + nextEvent?.isCollapsedState == true && + previousEvent?.isCollapsedState != true; + final isCollapsed = event.isCollapsedState && + previousEvent?.isCollapsedState == true && + !controller.expandedEventIds.contains(event.eventId); + return AutoScrollTag( key: ValueKey(event.eventId), index: i, @@ -148,11 +159,18 @@ class ChatEventList extends StatelessWidget { timeline: timeline, displayReadMarker: i > 0 && controller.readMarkerEventId == event.eventId, - nextEvent: i + 1 < events.length ? events[i + 1] : null, - previousEvent: i > 0 ? events[i - 1] : null, + nextEvent: nextEvent, + previousEvent: previousEvent, wallpaperMode: hasWallpaper, scrollController: controller.scrollController, colors: colors, + isCollapsed: isCollapsed, + onExpand: canExpand + ? () => controller.expandEventsFrom( + event, + !controller.expandedEventIds.contains(event.eventId), + ) + : null, ), ); }, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 279ea439f..fab82c91b 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -44,6 +44,8 @@ class Message extends StatelessWidget { final bool wallpaperMode; final ScrollController scrollController; final List colors; + final void Function()? onExpand; + final bool isCollapsed; const Message( this.event, { @@ -66,6 +68,8 @@ class Message extends StatelessWidget { required this.onMention, required this.scrollController, required this.colors, + this.onExpand, + this.isCollapsed = false, super.key, }); @@ -85,7 +89,7 @@ class Message extends StatelessWidget { if (event.type == EventTypes.RoomCreate) { return RoomCreationStateEvent(event: event); } - return StateMessage(event); + return StateMessage(event, onExpand: onExpand, isCollapsed: isCollapsed); } if (event.type == EventTypes.Message && diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index 41f011305..7d6be428c 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -1,46 +1,84 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import '../../../config/app_config.dart'; class StateMessage extends StatelessWidget { final Event event; - const StateMessage(this.event, {super.key}); + final void Function()? onExpand; + final bool isCollapsed; + const StateMessage( + this.event, { + this.onExpand, + this.isCollapsed = false, + super.key, + }); @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Center( - child: Padding( - padding: const EdgeInsets.all(4), - child: Material( - color: theme.colorScheme.surface.withAlpha(128), - borderRadius: BorderRadius.circular(AppConfig.borderRadius / 3), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Text( - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)), - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12 * AppConfig.fontSizeFactor, - decoration: - event.redacted ? TextDecoration.lineThrough : null, + return AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: isCollapsed + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Center( + child: Padding( + padding: const EdgeInsets.all(4), + child: Material( + color: theme.colorScheme.surface.withAlpha(128), + borderRadius: + BorderRadius.circular(AppConfig.borderRadius / 3), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)), + ), + ), + if (onExpand != null) ...[ + const TextSpan( + text: ' + ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + style: TextStyle( + color: theme.colorScheme.primary, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = onExpand, + text: L10n.of(context).moreEvents, + ), + ], + ], + ), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12 * AppConfig.fontSizeFactor, + decoration: event.redacted + ? TextDecoration.lineThrough + : null, + ), + ), + ), + ), ), ), ), - ), - ), - ), ); } } diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 449ab6a75..d3526a28e 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -38,13 +38,6 @@ class SettingsChatView extends StatelessWidget { storeKey: SettingKeys.renderHtml, defaultValue: AppConfig.renderHtml, ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context).hideMemberChangesInPublicChats, - subtitle: L10n.of(context).hideMemberChangesInPublicChatsBody, - onChanged: (b) => AppConfig.hideUnimportantStateEvents = b, - storeKey: SettingKeys.hideUnimportantStateEvents, - defaultValue: AppConfig.hideUnimportantStateEvents, - ), SettingsSwitchListTile.adaptive( title: L10n.of(context).hideRedactedMessages, subtitle: L10n.of(context).hideRedactedMessagesBody, diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index cd9d223ee..055ce5011 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -3,33 +3,9 @@ import 'package:matrix/matrix.dart'; import '../../config/app_config.dart'; extension VisibleInGuiExtension on List { - List filterByVisibleInGui({String? exceptionEventId}) { - final visibleEvents = - where((e) => e.isVisibleInGui || e.eventId == exceptionEventId) - .toList(); - - // Hide creation state events: - if (visibleEvents.isNotEmpty && - visibleEvents.last.type == EventTypes.RoomCreate) { - var i = visibleEvents.length - 2; - while (i > 0) { - final event = visibleEvents[i]; - if (!event.isState) break; - if (event.type == EventTypes.Encryption) { - i--; - continue; - } - if (event.type == EventTypes.RoomMember && - event.roomMemberChangeType == RoomMemberChangeType.acceptInvite) { - i--; - continue; - } - visibleEvents.removeAt(i); - i--; - } - } - return visibleEvents; - } + List filterByVisibleInGui({String? exceptionEventId}) => where( + (event) => event.isVisibleInGui || event.eventId == exceptionEventId, + ).toList(); } extension IsStateExtension on Event { @@ -45,19 +21,19 @@ extension IsStateExtension on Event { // if we enabled to hide all redacted events, don't show those (!AppConfig.hideRedactedEvents || !redacted) && // if we enabled to hide all unknown events, don't show those - (!AppConfig.hideUnknownEvents || isEventTypeKnown) && - // remove state events that we don't want to render - (isState || !AppConfig.hideAllStateEvents) && - // hide simple join/leave member events in public rooms - (!AppConfig.hideUnimportantStateEvents || - type != EventTypes.RoomMember || - room.joinRules != JoinRules.public || - content.tryGet('membership') == 'ban' || - stateKey != senderId); + (!AppConfig.hideUnknownEvents || isEventTypeKnown); bool get isState => !{ EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted, }.contains(type); + + bool get isCollapsedState => !{ + EventTypes.Message, + EventTypes.Sticker, + EventTypes.Encrypted, + EventTypes.RoomCreate, + EventTypes.RoomTombstone, + }.contains(type); } diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 1115e8756..2b7a1797b 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -411,10 +411,6 @@ class MatrixState extends State with WidgetsBindingObserver { store.getBool(SettingKeys.hideUnknownEvents) ?? AppConfig.hideUnknownEvents; - AppConfig.hideUnimportantStateEvents = - store.getBool(SettingKeys.hideUnimportantStateEvents) ?? - AppConfig.hideUnimportantStateEvents; - AppConfig.separateChatTypes = store.getBool(SettingKeys.separateChatTypes) ?? AppConfig.separateChatTypes;