From 66cfb0e2c65ba250bde07a790a477c775034b439 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 5 Oct 2026 11:16:11 -0400 Subject: [PATCH] fix wrapping issues on event previews --- lib/widgets/event_preview.dart | 5 +- .../lazy_loading/message_displayname.dart | 42 ++-- lib/widgets/renderers/event.dart | 217 ++++++++++++------ lib/widgets/renderers/generic_event.dart | 18 +- lib/widgets/renderers/membership.dart | 70 +++--- 5 files changed, 237 insertions(+), 115 deletions(-) diff --git a/lib/widgets/event_preview.dart b/lib/widgets/event_preview.dart index ee9011b..1b88fa8 100644 --- a/lib/widgets/event_preview.dart +++ b/lib/widgets/event_preview.dart @@ -23,10 +23,7 @@ class const EventPreview(final Event event, {super.key}) crossAxisAlignment: .start, children: [ if (event.content is MessageContent) - DefaultTextHeightBehavior( - textHeightBehavior: .new(), - child: MessageDisplayname(event), - ), + MessageDisplayname(event, shouldWrap: false), EventRenderer( event.rowId, roomId: event.roomId, diff --git a/lib/widgets/lazy_loading/message_displayname.dart b/lib/widgets/lazy_loading/message_displayname.dart index 12c15c2..593e755 100644 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ b/lib/widgets/lazy_loading/message_displayname.dart @@ -11,6 +11,7 @@ class const MessageDisplayname( final Event event, { final TextStyle? style, final bool clickable = true, + final bool shouldWrap = true, super.key, }) extends ConsumerWidget { @override @@ -27,27 +28,34 @@ class const MessageDisplayname( roomId: event.roomId, ) : null, - child: Wrap( - spacing: 4, - crossAxisAlignment: .center, - children: [ + child: switch ([ + Text( + value?.displayName ?? event.sender.localpart, + style: + style ?? .new(color: event.sender.colorHash, fontWeight: .bold), + maxLines: 1, + overflow: .ellipsis, + ), + if (event.pmp != null) Text( - value?.displayName ?? event.sender.localpart, - style: - style ?? .new(color: event.sender.colorHash, fontWeight: .bold), + "(via ${event.sender})", + style: Theme.of(context).textTheme.labelSmall + ?.copyWith(color: event.sender.colorHash, fontWeight: .bold), maxLines: 1, overflow: .ellipsis, ), - if (event.pmp != null) - Text( - "(via ${event.sender})", - style: Theme.of(context).textTheme.labelSmall - ?.copyWith(color: event.sender.colorHash, fontWeight: .bold), - maxLines: 1, - overflow: .ellipsis, - ), - ], - ), + ]) { + final children when shouldWrap => Wrap( + spacing: 4, + crossAxisAlignment: .center, + children: children, + ), + final children => Row( + spacing: 4, + mainAxisSize: .min, + children: [for (final child in children) Flexible(child: child)], + ), + }, ), }; } diff --git a/lib/widgets/renderers/event.dart b/lib/widgets/renderers/event.dart index 0879c1c..af4cef4 100644 --- a/lib/widgets/renderers/event.dart +++ b/lib/widgets/renderers/event.dart @@ -116,85 +116,174 @@ class const EventRenderer( MembershipContent content => switch (event.previousContent) { MembershipContent(:final status) => - status == content.status ? null : MembershipRenderer(event), - _ => MembershipRenderer(event), + status == content.status + ? null + : MembershipRenderer(event, maxLines: maxLines), + _ => MembershipRenderer(event, maxLines: maxLines), }, - AvatarContent() => GenericEventRenderer(Icons.interests, [ - MessageDisplayname(event), - Text("changed the room avatar"), - ]), + AvatarContent() => GenericEventRenderer( + Icons.interests, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan(text: "changed the room avatar"), + ], + ), - CreateContent() => GenericEventRenderer(Icons.add, [ - MessageDisplayname(event), - Text("created the room"), - ]), + CreateContent() => GenericEventRenderer( + Icons.add, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan(text: "created the room"), + ], + ), - PowerLevelsContent() => GenericEventRenderer(Icons.power, [ - MessageDisplayname(event), - Text("changed the room's power levels"), - ]), + PowerLevelsContent() => GenericEventRenderer( + Icons.power, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan(text: "changed the room's power levels"), + ], + ), - JoinRulesContent() => GenericEventRenderer(Icons.rule, [ - MessageDisplayname(event), - Text("changed the room's join rules"), - ]), + JoinRulesContent() => GenericEventRenderer( + Icons.rule, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan(text: "changed the room's join rules"), + ], + ), - TopicContent() => GenericEventRenderer(Icons.description, [ - MessageDisplayname(event), - Text("updated the room topic"), - ]), + TopicContent() => GenericEventRenderer( + Icons.description, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan(text: "updated the room topic"), + ], + ), HistoryVisibilityContent(:final historyVisibility) => - GenericEventRenderer(Icons.history, [ - MessageDisplayname(event), - Text( - "changed the room's history visibility to ${switch (historyVisibility) { - .invited => "since invited", - .joined => "since joined", - .shared => "all history visible (shared)", - .worldReadable => "all history visible (world readable)", - }}", + GenericEventRenderer(Icons.history, maxLines: maxLines, [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan( + text: + "changed the room's history visibility to ${switch (historyVisibility) { + .invited => "since invited", + .joined => "since joined", + .shared => "all history visible (shared)", + .worldReadable => "all history visible (world readable)", + }}", ), ]), - PinnedEventsContent() => GenericEventRenderer(Icons.push_pin, [ - MessageDisplayname(event), - Text("pinned/unpinned some events"), - ]), + PinnedEventsContent() => GenericEventRenderer( + Icons.push_pin, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan(text: "pinned/unpinned some events"), + ], + ), - ServerACLContent() => GenericEventRenderer(Icons.list, [ - MessageDisplayname(event), - Text("updated the server ban list"), - ]), + ServerACLContent() => GenericEventRenderer( + Icons.list, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan(text: "updated the server ban list"), + ], + ), CanonicalAliasContent(:final alias, :final altAliases) => - GenericEventRenderer(Icons.numbers, [ - MessageDisplayname(event), - Text(switch ([ - if (event.previousContent case CanonicalAliasContent( - alias: final prevAlias, - altAliases: final prevAltAliases, - )) ...[ - if (prevAlias != alias) - if (alias == null) - "removed the room's canonical alias" - else - "changed the room's canonical alias to $alias", + GenericEventRenderer(Icons.numbers, maxLines: maxLines, [ + WidgetSpan( + alignment: .middle, + child: MessageDisplayname( + event, + shouldWrap: maxLines == null, + ), + ), + TextSpan( + text: switch ([ + if (event.previousContent case CanonicalAliasContent( + alias: final prevAlias, + altAliases: final prevAltAliases, + )) ...[ + if (prevAlias != alias) + if (alias == null) + "removed the room's canonical alias" + else + "changed the room's canonical alias to $alias", - if (prevAltAliases - .remove(alias ?? "") - .remove(prevAlias ?? "") != - altAliases.remove(alias ?? "").remove(prevAlias ?? "")) - "changed the room's aliases", - ] else ...[ - if (alias != null) "set the room's canonical alias", - if (altAliases.isNotEmpty) "set the room's aliases", - ], - ]) { - [] => "did something related to room aliases", - List prev => prev.join(" and "), - }), + if (prevAltAliases + .remove(alias ?? "") + .remove(prevAlias ?? "") != + altAliases + .remove(alias ?? "") + .remove(prevAlias ?? "")) + "changed the room's aliases", + ] else ...[ + if (alias != null) "set the room's canonical alias", + if (altAliases.isNotEmpty) "set the room's aliases", + ], + ]) { + [] => "did something related to room aliases", + List prev => prev.join(" and "), + }, + ), ]), _ => null, }; diff --git a/lib/widgets/renderers/generic_event.dart b/lib/widgets/renderers/generic_event.dart index eb5b29b..c5aef0b 100644 --- a/lib/widgets/renderers/generic_event.dart +++ b/lib/widgets/renderers/generic_event.dart @@ -2,7 +2,8 @@ import "package:material_ui/material_ui.dart"; class const GenericEventRenderer( final IconData icon, - final List children, { + final List children, { + final int? maxLines, super.key, }) extends StatelessWidget { @override @@ -13,7 +14,20 @@ class const GenericEventRenderer( mainAxisSize: .min, children: [ Padding(padding: .symmetric(horizontal: 4), child: Icon(icon)), - Flexible(child: Wrap(spacing: 4, children: children)), + Flexible( + child: Text.rich( + TextSpan( + children: [ + for (final (index, child) in children.indexed) ...[ + if (index > 0) TextSpan(text: " "), + child, + ], + ], + ), + maxLines: maxLines ?? 9007199254740991, + overflow: .ellipsis, + ), + ), ], ), ); diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index 5ce5d68..c57200c 100644 --- a/lib/widgets/renderers/membership.dart +++ b/lib/widgets/renderers/membership.dart @@ -7,8 +7,11 @@ import "package:nexus/models/event.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/renderers/generic_event.dart"; -class const MembershipRenderer(final Event event, {super.key}) - extends StatelessWidget { +class const MembershipRenderer( + final Event event, { + final int? maxLines, + super.key, +}) extends StatelessWidget { @override Widget build(BuildContext context) { assert( @@ -17,34 +20,45 @@ class const MembershipRenderer(final Event event, {super.key}) ); return switch (event.content) { - MembershipContent content => GenericEventRenderer(Icons.people, [ - InkWell( - onTap: () => context.showUserPopover( - content, - event.stateKey!, - roomId: event.roomId, + MembershipContent content => GenericEventRenderer( + Icons.people, + maxLines: maxLines, + [ + WidgetSpan( + alignment: .middle, + child: InkWell( + onTap: () => context.showUserPopover( + content, + event.stateKey!, + roomId: event.roomId, + ), + child: Text( + overflow: .ellipsis, + content.displayName ?? event.stateKey!.localpart, + maxLines: 1, + style: .new(color: event.sender.colorHash, fontWeight: .bold), + ), + ), ), - child: Text( - overflow: .ellipsis, - content.displayName ?? event.stateKey!.localpart, - maxLines: 1, - style: .new(color: event.sender.colorHash, fontWeight: .bold), + TextSpan( + text: + "${switch (content.status) { + .invite => "was invited to", + .join => "joined", + .leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), + .ban => "was banned from", + .knock => "asked to join", + }} the room${event.sender == event.stateKey ? "" : " by"}", ), - ), - Text( - overflow: .ellipsis, - maxLines: 1, - "${switch (content.status) { - .invite => "was invited to", - .join => "joined", - .leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), - .ban => "was banned from", - .knock => "asked to join", - }} the room${event.sender == event.stateKey ? "" : " by "}", - ), - if (event.sender != event.stateKey) MessageDisplayname(event), - if (content.reason != null) Text("for \"${content.reason}\""), - ]), + if (event.sender != event.stateKey) + WidgetSpan( + alignment: .middle, + child: MessageDisplayname(event, shouldWrap: maxLines == null), + ), + if (content.reason != null) + TextSpan(text: "for \"${content.reason}\""), + ], + ), _ => SizedBox.shrink(), }; }