diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart index 14bb52a1..16f74f06 100644 --- a/lib/components/list_items/message.dart +++ b/lib/components/list_items/message.dart @@ -16,9 +16,10 @@ import 'state_message.dart'; class Message extends StatelessWidget { final Event event; final Event nextEvent; - final Function(Event) onSelect; - final Function(Event) onAvatarTab; - final Function(String) scrollToEventId; + final void Function(Event) onSelect; + final void Function(Event) onAvatarTab; + final void Function(String) scrollToEventId; + final void Function(String) unfold; final bool longPressSelect; final bool selected; final Timeline timeline; @@ -29,6 +30,7 @@ class Message extends StatelessWidget { this.onSelect, this.onAvatarTab, this.scrollToEventId, + @required this.unfold, this.selected, this.timeline}); @@ -38,15 +40,9 @@ class Message extends StatelessWidget { @override Widget build(BuildContext context) { - if (event.type == EventTypes.RoomCreate) { - return InkWell( - onTap: () => onSelect(event), - child: StateMessage(event), - ); - } if (![EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] .contains(event.type)) { - return StateMessage(event); + return StateMessage(event, unfold: unfold); } var client = Matrix.of(context).client; diff --git a/lib/components/list_items/state_message.dart b/lib/components/list_items/state_message.dart index 4da7e4c9..a2ec03e9 100644 --- a/lib/components/list_items/state_message.dart +++ b/lib/components/list_items/state_message.dart @@ -7,31 +7,55 @@ import '../../app_config.dart'; class StateMessage extends StatelessWidget { final Event event; - const StateMessage(this.event); + final void Function(String) unfold; + const StateMessage(this.event, {@required this.unfold}); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only( - left: 8.0, - right: 8.0, - bottom: 8.0, - ), - child: Center( - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).secondaryHeaderColor, - borderRadius: BorderRadius.circular(7), - ), - child: Text( - event.getLocalizedBody(MatrixLocals(L10n.of(context))), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: Theme.of(context).textTheme.bodyText1.fontSize * - AppConfig.fontSizeFactor, - color: Theme.of(context).textTheme.bodyText2.color, - decoration: event.redacted ? TextDecoration.lineThrough : null, + if (event.unsigned['im.fluffychat.collapsed_state_event'] == true) { + return Container(); + } + final int counter = + event.unsigned['im.fluffychat.collapsed_state_event_count'] ?? 0; + return InkWell( + onTap: counter != 0 ? () => unfold(event.eventId) : null, + child: Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: 8.0, + ), + child: Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: BorderRadius.circular(7), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + event.getLocalizedBody(MatrixLocals(L10n.of(context))), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: Theme.of(context).textTheme.bodyText1.fontSize * + AppConfig.fontSizeFactor, + color: Theme.of(context).textTheme.bodyText2.color, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + ), + if (counter != 0) + Text( + counter == 1 + ? L10n.of(context).oneMoreEvent + : L10n.of(context).xMoreEvents(counter.toString()), + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], ), ), ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4c564d90..8267bc90 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -513,6 +513,18 @@ "type": "text", "placeholders": {} }, + "oneMoreEvent": "1 more event", + "@oneMoreEvent": { + "type": "text", + "placeholders": {} + }, + "xMoreEvents": "{count} more events", + "@xMoreEvents": { + "type": "text", + "placeholders": { + "count": {} + } + }, "createdTheChat": "{username} created the chat", "@createdTheChat": { "type": "text", diff --git a/lib/utils/filtered_timeline_extension.dart b/lib/utils/filtered_timeline_extension.dart index 118ebb90..c87fcde9 100644 --- a/lib/utils/filtered_timeline_extension.dart +++ b/lib/utils/filtered_timeline_extension.dart @@ -3,7 +3,7 @@ import 'package:famedlysdk/famedlysdk.dart'; import '../app_config.dart'; extension FilteredTimelineExtension on Timeline { - List getFilteredEvents({bool collapseRoomCreate = true}) { + List getFilteredEvents({Set unfolded = const {}}) { final filteredEvents = events .where((e) => // always filter out edit and reaction relationships @@ -19,23 +19,35 @@ extension FilteredTimelineExtension on Timeline { // 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 - (!{EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted} - .contains(e.type) || - !AppConfig.hideAllStateEvents)) + (e.isState || !AppConfig.hideAllStateEvents)) .toList(); - // Hide state events from the room creater right after the room created event - if (collapseRoomCreate && - filteredEvents[filteredEvents.length - 1].type == - EventTypes.RoomCreate) { - while (filteredEvents.length >= 3 && - filteredEvents[filteredEvents.length - 2].senderId == - filteredEvents[filteredEvents.length - 1].senderId && - ![EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] - .contains(filteredEvents[filteredEvents.length - 2].type)) { - filteredEvents.removeAt(filteredEvents.length - 2); + // 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['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 isState => !{ + EventTypes.Message, + EventTypes.Sticker, + EventTypes.Encrypted + }.contains(type); +} diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart index 0a5446cb..94a8c02d 100644 --- a/lib/utils/room_status_extension.dart +++ b/lib/utils/room_status_extension.dart @@ -63,11 +63,14 @@ extension RoomStatusExtension on Room { } String getLocalizedSeenByText( - BuildContext context, Timeline timeline, List filteredEvents) { + BuildContext context, + Timeline timeline, + List filteredEvents, + Set unfolded, + ) { var seenByText = ''; if (timeline.events.isNotEmpty) { - final filteredEvents = - timeline.getFilteredEvents(collapseRoomCreate: false); + final filteredEvents = timeline.getFilteredEvents(unfolded: unfolded); final lastReceipts = {}; // now we iterate the timeline events until we hit the first rendered event for (final event in timeline.events) { diff --git a/lib/views/chat.dart b/lib/views/chat.dart index fee56a2e..292e542e 100644 --- a/lib/views/chat.dart +++ b/lib/views/chat.dart @@ -72,7 +72,7 @@ class _ChatState extends State { List filteredEvents; - bool _collapseRoomCreate = true; + final Set _unfolded = {}; Event replyEvent; @@ -146,12 +146,22 @@ class _ChatState extends State { if (!mounted) return; setState( () { - filteredEvents = - timeline.getFilteredEvents(collapseRoomCreate: _collapseRoomCreate); + 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); + }); + } + Future getTimeline(BuildContext context) async { if (timeline == null) { timeline = await room.getTimeline(onUpdate: updateView); @@ -712,9 +722,7 @@ class _ChatState extends State { thisEventsKeyMap[filteredEvents[i].eventId] = i; } - return ListView.custom( - padding: EdgeInsets.symmetric( - horizontal: max( + final horizontalPadding = max( 0, (MediaQuery.of(context).size.width - FluffyThemes.columnWidth * @@ -724,7 +732,14 @@ class _ChatState extends State { null ? 4.5 : 3.5)) / - 2), + 2) + .toDouble(); + + return ListView.custom( + padding: EdgeInsets.only( + top: 16, + left: horizontalPadding, + right: horizontalPadding, ), reverse: true, controller: _scrollController, @@ -759,9 +774,11 @@ class _ChatState extends State { builder: (_, __) { final seenByText = room.getLocalizedSeenByText( - context, - timeline, - filteredEvents); + context, + timeline, + filteredEvents, + _unfolded, + ); return AnimatedContainer( height: seenByText.isEmpty ? 0 : 24, duration: seenByText.isEmpty @@ -830,13 +847,8 @@ class _ChatState extends State { '${event.senderId} ', ), ), + unfold: _unfold, onSelect: (Event event) { - if (event.type == - EventTypes.RoomCreate) { - return setState(() => - _collapseRoomCreate = - false); - } if (!event.redacted) { if (selectedEvents .contains(event)) {