diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index adcdad12..f6a35c1d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2949,5 +2949,6 @@ "placeholders": { "number": {} } - } + }, + "hideUnimportantStateEvents": "Hide unimportant state events" } diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index de99da9b..528777d6 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -37,6 +37,7 @@ abstract class AppConfig { static bool renderHtml = true; static bool hideRedactedEvents = false; static bool hideUnknownEvents = true; + static bool hideUnimportantStateEvents = true; static bool showDirectChatsInSpaces = true; static bool separateChatTypes = false; static bool autoplayImages = true; diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 94f4ab07..96b5ef06 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -3,6 +3,8 @@ abstract class SettingKeys { static const String renderHtml = 'chat.fluffy.renderHtml'; static const String hideRedactedEvents = 'chat.fluffy.hideRedactedEvents'; static const String hideUnknownEvents = 'chat.fluffy.hideUnknownEvents'; + static const String hideUnimportantStateEvents = + 'chat.fluffy.hideUnimportantStateEvents'; static const String showDirectChatsInSpaces = 'chat.fluffy.showDirectChatsInSpaces'; static const String separateChatTypes = 'chat.fluffy.separateChatTypes'; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 577d2aea..8da2e79d 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -28,7 +28,6 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; -import '../../utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart'; import '../../utils/matrix_sdk_extensions.dart/matrix_file_extension.dart'; import 'send_file_dialog.dart'; import 'send_location_dialog.dart'; @@ -111,8 +110,6 @@ class ChatController extends State { List selectedEvents = []; - late List filteredEvents; - final Set unfolded = {}; Event? replyEvent; @@ -184,22 +181,7 @@ class ChatController extends State { void updateView() { if (!mounted) return; - setState( - () { - filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded); - }, - ); - } - - void unfold(String eventId) { - var i = filteredEvents.indexWhere((e) => e.eventId == eventId); - setState(() { - while (i < filteredEvents.length - 1 && filteredEvents[i].isState) { - unfolded.add(filteredEvents[i].eventId); - i++; - } - filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded); - }); + setState(() {}); } Future getTimeline() async { @@ -225,7 +207,6 @@ class ChatController extends State { } }); } - filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded); timeline!.requestKeys(onlineKeyBackupOnly: false); return true; } @@ -656,7 +637,7 @@ class ChatController extends State { } void scrollToEventId(String eventId) async { - var eventIndex = filteredEvents.indexWhere((e) => e.eventId == eventId); + var eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId); if (eventIndex == -1) { // event id not found...maybe we can fetch it? // the try...finally is here to start and close the loading dialog reliably @@ -693,7 +674,7 @@ class ChatController extends State { rethrow; } eventIndex = - filteredEvents.indexWhere((e) => e.eventId == eventId); + timeline!.events.indexWhere((e) => e.eventId == eventId); } }); } diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index da4f5d00..44daa4f9 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; class ChatEventList extends StatelessWidget { @@ -26,9 +27,10 @@ class ChatEventList extends StatelessWidget { // create a map of eventId --> index to greatly improve performance of // ListView's findChildIndexCallback final thisEventsKeyMap = {}; - for (var i = 0; i < controller.filteredEvents.length; i++) { - thisEventsKeyMap[controller.filteredEvents[i].eventId] = i; + for (var i = 0; i < controller.timeline!.events.length; i++) { + thisEventsKeyMap[controller.timeline!.events[i].eventId] = i; } + return ListView.custom( padding: EdgeInsets.only( top: 16, @@ -55,7 +57,7 @@ class ChatEventList extends StatelessWidget { } // Request history button or progress indicator: - if (i == controller.filteredEvents.length + 1) { + if (i == controller.timeline!.events.length + 1) { if (controller.timeline!.isRequestingHistory) { return const Center( child: CircularProgressIndicator.adaptive(strokeWidth: 2), @@ -76,37 +78,40 @@ class ChatEventList extends StatelessWidget { } // The message at this index: + final event = controller.timeline!.events[i - 1]; + return AutoScrollTag( - key: ValueKey(controller.filteredEvents[i - 1].eventId), + key: ValueKey(event.eventId), index: i - 1, controller: controller.scrollController, - child: Message(controller.filteredEvents[i - 1], - onSwipe: (direction) => controller.replyAction( - replyTo: controller.filteredEvents[i - 1]), - onInfoTab: controller.showEventInfo, - onAvatarTab: (Event event) => showModalBottomSheet( - context: context, - builder: (c) => UserBottomSheet( - user: event.senderFromMemoryOrFallback, - outerContext: context, - onMention: () => controller.sendController.text += - '${event.senderFromMemoryOrFallback.mention} ', - ), - ), - unfold: controller.unfold, - onSelect: controller.onSelectMessage, - scrollToEventId: (String eventId) => - controller.scrollToEventId(eventId), - longPressSelect: controller.selectedEvents.isEmpty, - selected: controller.selectedEvents.any((e) => - e.eventId == controller.filteredEvents[i - 1].eventId), - timeline: controller.timeline!, - nextEvent: i < controller.filteredEvents.length - ? controller.filteredEvents[i] - : null), + child: event.isVisibleInGui + ? Message(event, + onSwipe: (direction) => + controller.replyAction(replyTo: event), + onInfoTab: controller.showEventInfo, + onAvatarTab: (Event event) => showModalBottomSheet( + context: context, + builder: (c) => UserBottomSheet( + user: event.senderFromMemoryOrFallback, + outerContext: context, + onMention: () => controller.sendController.text += + '${event.senderFromMemoryOrFallback.mention} ', + ), + ), + onSelect: controller.onSelectMessage, + scrollToEventId: (String eventId) => + controller.scrollToEventId(eventId), + longPressSelect: controller.selectedEvents.isEmpty, + selected: controller.selectedEvents + .any((e) => e.eventId == event.eventId), + timeline: controller.timeline!, + nextEvent: i < controller.timeline!.events.length + ? controller.timeline!.events[i] + : null) + : Container(), ); }, - childCount: controller.filteredEvents.length + 2, + childCount: controller.timeline!.events.length + 2, findChildIndexCallback: (key) => controller.findChildIndexCallback(key, thisEventsKeyMap), ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 5a9cf5c8..b5d5cbc3 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -22,7 +22,6 @@ class Message extends StatelessWidget { final void Function(Event)? onAvatarTab; final void Function(Event)? onInfoTab; final void Function(String)? scrollToEventId; - final void Function(String) unfold; final void Function(SwipeDirection) onSwipe; final bool longPressSelect; final bool selected; @@ -36,7 +35,6 @@ class Message extends StatelessWidget { this.onAvatarTab, this.scrollToEventId, required this.onSwipe, - required this.unfold, this.selected = false, required this.timeline, Key? key}) @@ -57,7 +55,7 @@ class Message extends StatelessWidget { if (event.type.startsWith('m.call.')) { return Container(); } - return StateMessage(event, unfold: unfold); + return StateMessage(event); } if (event.type == EventTypes.Message && diff --git a/lib/pages/chat/events/state_message.dart b/lib/pages/chat/events/state_message.dart index 6e270ae6..c4c77edb 100644 --- a/lib/pages/chat/events/state_message.dart +++ b/lib/pages/chat/events/state_message.dart @@ -8,9 +8,7 @@ import '../../../config/app_config.dart'; class StateMessage extends StatelessWidget { final Event event; - final void Function(String) unfold; - const StateMessage(this.event, {required this.unfold, Key? key}) - : super(key: key); + const StateMessage(this.event, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -25,48 +23,43 @@ class StateMessage extends StatelessWidget { vertical: 4.0, ), child: Center( - child: InkWell( - onTap: counter != 0 ? () => unfold(event.eventId) : null, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? Colors.white - : Colors.grey.shade900, - borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FutureBuilder( - future: event - .calcLocalizedBody(MatrixLocals(L10n.of(context)!)), - builder: (context, snapshot) { - return Text( - snapshot.data ?? - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!)), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14 * AppConfig.fontSizeFactor, - color: Theme.of(context).textTheme.bodyText2!.color, - decoration: event.redacted - ? TextDecoration.lineThrough - : null, - ), - ); - }), - if (counter != 0) - Text( - L10n.of(context)!.moreEvents(counter), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14 * AppConfig.fontSizeFactor, - ), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Colors.grey.shade900, + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: + event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)), + builder: (context, snapshot) { + return Text( + snapshot.data ?? + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!)), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14 * AppConfig.fontSizeFactor, + color: Theme.of(context).textTheme.bodyText2!.color, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + ); + }), + if (counter != 0) + Text( + L10n.of(context)!.moreEvents(counter), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14 * AppConfig.fontSizeFactor, ), - ], - ), + ), + ], ), ), ), diff --git a/lib/pages/chat/seen_by_row.dart b/lib/pages/chat/seen_by_row.dart index 2a3ac52a..9bd29810 100644 --- a/lib/pages/chat/seen_by_row.dart +++ b/lib/pages/chat/seen_by_row.dart @@ -12,11 +12,7 @@ class SeenByRow extends StatelessWidget { @override Widget build(BuildContext context) { - final seenByUsers = controller.room!.getSeenByUsers( - controller.timeline!, - controller.filteredEvents, - controller.unfolded, - ); + final seenByUsers = controller.room!.getSeenByUsers(controller.timeline!); const maxAvatars = 7; return Container( width: double.infinity, @@ -28,8 +24,8 @@ class SeenByRow extends StatelessWidget { duration: seenByUsers.isEmpty ? const Duration(milliseconds: 0) : const Duration(milliseconds: 300), - alignment: controller.filteredEvents.isNotEmpty && - controller.filteredEvents.first.senderId == + alignment: controller.timeline!.events.isNotEmpty && + controller.timeline!.events.first.senderId == Matrix.of(context).client.userID ? Alignment.topRight : Alignment.topLeft, diff --git a/lib/pages/chat/typing_indicators.dart b/lib/pages/chat/typing_indicators.dart index 1466040f..3250de60 100644 --- a/lib/pages/chat/typing_indicators.dart +++ b/lib/pages/chat/typing_indicators.dart @@ -26,8 +26,8 @@ class TypingIndicators extends StatelessWidget { height: typingUsers.isEmpty ? 0 : Avatar.defaultSize + bottomPadding, duration: const Duration(milliseconds: 300), curve: Curves.bounceInOut, - alignment: controller.filteredEvents.isNotEmpty && - controller.filteredEvents.first.senderId == + alignment: controller.timeline!.events.isNotEmpty && + controller.timeline!.events.first.senderId == Matrix.of(context).client.userID ? Alignment.topRight : Alignment.topLeft, diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index e62a9411..1f49aa05 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -45,6 +45,12 @@ class SettingsChatView extends StatelessWidget { storeKey: SettingKeys.hideUnknownEvents, defaultValue: AppConfig.hideUnknownEvents, ), + SettingsSwitchListTile.adaptive( + title: L10n.of(context)!.hideUnimportantStateEvents, + onChanged: (b) => AppConfig.hideUnimportantStateEvents = b, + storeKey: SettingKeys.hideUnimportantStateEvents, + defaultValue: AppConfig.hideUnimportantStateEvents, + ), if (PlatformInfos.isMobile) SettingsSwitchListTile.adaptive( title: L10n.of(context)!.autoplayImages, diff --git a/lib/pages/story/story_page.dart b/lib/pages/story/story_page.dart index ba13f34a..3d4fb411 100644 --- a/lib/pages/story/story_page.dart +++ b/lib/pages/story/story_page.dart @@ -110,8 +110,6 @@ class StoryPageController extends State { if (timeline == null || currentEvent == null) return []; return Matrix.of(context).client.getRoomById(roomId)?.getSeenByUsers( timeline, - events, - {}, eventId: currentEvent.eventId, ) ?? []; diff --git a/lib/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart index 565a07a9..76d746b6 100644 --- a/lib/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions.dart/filtered_timeline_extension.dart @@ -2,50 +2,39 @@ import 'package:matrix/matrix.dart'; import '../../config/app_config.dart'; -extension FilteredTimelineExtension on Timeline { - List getFilteredEvents({Set unfolded = const {}}) { - final filteredEvents = events - .where((e) => - // always filter out edit and reaction relationships - !{RelationshipTypes.edit, RelationshipTypes.reaction} - .contains(e.relationshipType) && - // always filter out m.key.* events - !e.type.startsWith('m.key.verification.') && - // event types to hide: redaction and reaction events - // if a reaction has been redacted we also want it to be hidden in the timeline - !{EventTypes.Reaction, EventTypes.Redaction}.contains(e.type) && - // if we enabled to hide all redacted events, don't show those - (!AppConfig.hideRedactedEvents || !e.redacted) && - // if we enabled to hide all unknown events, don't show those - (!AppConfig.hideUnknownEvents || e.isEventTypeKnown) && - // remove state events that we don't want to render - (e.isState || !AppConfig.hideAllStateEvents)) - .toList(); - - // Fold state events - var counter = 0; - for (var i = filteredEvents.length - 1; i >= 0; i--) { - if (!filteredEvents[i].isState) continue; - if (i > 0 && - filteredEvents[i - 1].isState && - !unfolded.contains(filteredEvents[i - 1].eventId)) { - counter++; - filteredEvents[i].unsigned ??= {}; - filteredEvents[i].unsigned!['im.fluffychat.collapsed_state_event'] = - true; - } else { - filteredEvents[i].unsigned!['im.fluffychat.collapsed_state_event'] = - false; - filteredEvents[i] - .unsigned!['im.fluffychat.collapsed_state_event_count'] = counter; - counter = 0; - } - } - return filteredEvents; - } -} - extension IsStateExtension on Event { + bool get isVisibleInGui => + // always filter out edit and reaction relationships + !{RelationshipTypes.edit, RelationshipTypes.reaction} + .contains(relationshipType) && + // always filter out m.key.* events + !type.startsWith('m.key.verification.') && + // event types to hide: redaction and reaction events + // if a reaction has been redacted we also want it to be hidden in the timeline + !{EventTypes.Reaction, EventTypes.Redaction}.contains(type) && + // 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 unimportant state events + (!AppConfig.hideUnimportantStateEvents || + !isState || + importantStateEvents.contains(type)) && + // hide member events in public rooms + (!AppConfig.hideUnimportantStateEvents || + type != EventTypes.RoomMember || + room.joinRules != JoinRules.public); + + static const Set importantStateEvents = { + EventTypes.Encryption, + EventTypes.RoomCreate, + EventTypes.RoomMember, + EventTypes.RoomTombstone, + EventTypes.CallInvite, + }; + bool get isState => !{ EventTypes.Message, EventTypes.Sticker, diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart index 5468010c..14b6080f 100644 --- a/lib/utils/room_status_extension.dart +++ b/lib/utils/room_status_extension.dart @@ -5,7 +5,6 @@ import 'package:matrix/matrix.dart'; import '../config/app_config.dart'; import 'date_time_extension.dart'; -import 'matrix_sdk_extensions.dart/filtered_timeline_extension.dart'; extension RoomStatusExtension on Room { CachedPresence? get directChatPresence => @@ -65,14 +64,9 @@ extension RoomStatusExtension on Room { return typingText; } - List getSeenByUsers( - Timeline timeline, List filteredEvents, Set unfolded, - {String? eventId}) { + List getSeenByUsers(Timeline timeline, {String? eventId}) { if (timeline.events.isEmpty) return []; - - final filteredEvents = timeline.getFilteredEvents(unfolded: unfolded); - if (filteredEvents.isEmpty) return []; - eventId ??= filteredEvents.first.eventId; + eventId ??= timeline.events.first.eventId; final lastReceipts = {}; // now we iterate the timeline events until we hit the first rendered event @@ -83,7 +77,7 @@ extension RoomStatusExtension on Room { } } lastReceipts.removeWhere((user) => - user.id == client.userID || user.id == filteredEvents.first.senderId); + user.id == client.userID || user.id == timeline.events.first.senderId); return lastReceipts.toList(); } }