fix wrapping issues on event previews

This commit is contained in:
Henry Hiles 2026-10-05 11:16:11 -04:00
commit 66cfb0e2c6
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 237 additions and 115 deletions

View file

@ -23,10 +23,7 @@ class const EventPreview(final Event event, {super.key})
crossAxisAlignment: .start, crossAxisAlignment: .start,
children: [ children: [
if (event.content is MessageContent) if (event.content is MessageContent)
DefaultTextHeightBehavior( MessageDisplayname(event, shouldWrap: false),
textHeightBehavior: .new(),
child: MessageDisplayname(event),
),
EventRenderer( EventRenderer(
event.rowId, event.rowId,
roomId: event.roomId, roomId: event.roomId,

View file

@ -11,6 +11,7 @@ class const MessageDisplayname(
final Event event, { final Event event, {
final TextStyle? style, final TextStyle? style,
final bool clickable = true, final bool clickable = true,
final bool shouldWrap = true,
super.key, super.key,
}) extends ConsumerWidget { }) extends ConsumerWidget {
@override @override
@ -27,27 +28,34 @@ class const MessageDisplayname(
roomId: event.roomId, roomId: event.roomId,
) )
: null, : null,
child: Wrap( child: switch ([
spacing: 4, Text(
crossAxisAlignment: .center, value?.displayName ?? event.sender.localpart,
children: [ style:
style ?? .new(color: event.sender.colorHash, fontWeight: .bold),
maxLines: 1,
overflow: .ellipsis,
),
if (event.pmp != null)
Text( Text(
value?.displayName ?? event.sender.localpart, "(via ${event.sender})",
style: style: Theme.of(context).textTheme.labelSmall
style ?? .new(color: event.sender.colorHash, fontWeight: .bold), ?.copyWith(color: event.sender.colorHash, fontWeight: .bold),
maxLines: 1, maxLines: 1,
overflow: .ellipsis, overflow: .ellipsis,
), ),
if (event.pmp != null) ]) {
Text( final children when shouldWrap => Wrap(
"(via ${event.sender})", spacing: 4,
style: Theme.of(context).textTheme.labelSmall crossAxisAlignment: .center,
?.copyWith(color: event.sender.colorHash, fontWeight: .bold), children: children,
maxLines: 1, ),
overflow: .ellipsis, final children => Row(
), spacing: 4,
], mainAxisSize: .min,
), children: [for (final child in children) Flexible(child: child)],
),
},
), ),
}; };
} }

View file

@ -116,85 +116,174 @@ class const EventRenderer(
MembershipContent content => switch (event.previousContent) { MembershipContent content => switch (event.previousContent) {
MembershipContent(:final status) => MembershipContent(:final status) =>
status == content.status ? null : MembershipRenderer(event), status == content.status
_ => MembershipRenderer(event), ? null
: MembershipRenderer(event, maxLines: maxLines),
_ => MembershipRenderer(event, maxLines: maxLines),
}, },
AvatarContent() => GenericEventRenderer(Icons.interests, [ AvatarContent() => GenericEventRenderer(
MessageDisplayname(event), Icons.interests,
Text("changed the room avatar"), maxLines: maxLines,
]), [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "changed the room avatar"),
],
),
CreateContent() => GenericEventRenderer(Icons.add, [ CreateContent() => GenericEventRenderer(
MessageDisplayname(event), Icons.add,
Text("created the room"), maxLines: maxLines,
]), [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "created the room"),
],
),
PowerLevelsContent() => GenericEventRenderer(Icons.power, [ PowerLevelsContent() => GenericEventRenderer(
MessageDisplayname(event), Icons.power,
Text("changed the room's power levels"), maxLines: maxLines,
]), [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "changed the room's power levels"),
],
),
JoinRulesContent() => GenericEventRenderer(Icons.rule, [ JoinRulesContent() => GenericEventRenderer(
MessageDisplayname(event), Icons.rule,
Text("changed the room's join rules"), maxLines: maxLines,
]), [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "changed the room's join rules"),
],
),
TopicContent() => GenericEventRenderer(Icons.description, [ TopicContent() => GenericEventRenderer(
MessageDisplayname(event), Icons.description,
Text("updated the room topic"), maxLines: maxLines,
]), [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "updated the room topic"),
],
),
HistoryVisibilityContent(:final historyVisibility) => HistoryVisibilityContent(:final historyVisibility) =>
GenericEventRenderer(Icons.history, [ GenericEventRenderer(Icons.history, maxLines: maxLines, [
MessageDisplayname(event), WidgetSpan(
Text( alignment: .middle,
"changed the room's history visibility to ${switch (historyVisibility) { child: MessageDisplayname(
.invited => "since invited", event,
.joined => "since joined", shouldWrap: maxLines == null,
.shared => "all history visible (shared)", ),
.worldReadable => "all history visible (world readable)", ),
}}", 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, [ PinnedEventsContent() => GenericEventRenderer(
MessageDisplayname(event), Icons.push_pin,
Text("pinned/unpinned some events"), maxLines: maxLines,
]), [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "pinned/unpinned some events"),
],
),
ServerACLContent() => GenericEventRenderer(Icons.list, [ ServerACLContent() => GenericEventRenderer(
MessageDisplayname(event), Icons.list,
Text("updated the server ban list"), maxLines: maxLines,
]), [
WidgetSpan(
alignment: .middle,
child: MessageDisplayname(
event,
shouldWrap: maxLines == null,
),
),
TextSpan(text: "updated the server ban list"),
],
),
CanonicalAliasContent(:final alias, :final altAliases) => CanonicalAliasContent(:final alias, :final altAliases) =>
GenericEventRenderer(Icons.numbers, [ GenericEventRenderer(Icons.numbers, maxLines: maxLines, [
MessageDisplayname(event), WidgetSpan(
Text(switch ([ alignment: .middle,
if (event.previousContent case CanonicalAliasContent( child: MessageDisplayname(
alias: final prevAlias, event,
altAliases: final prevAltAliases, shouldWrap: maxLines == null,
)) ...[ ),
if (prevAlias != alias) ),
if (alias == null) TextSpan(
"removed the room's canonical alias" text: switch ([
else if (event.previousContent case CanonicalAliasContent(
"changed the room's canonical alias to $alias", 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 if (prevAltAliases
.remove(alias ?? "") .remove(alias ?? "")
.remove(prevAlias ?? "") != .remove(prevAlias ?? "") !=
altAliases.remove(alias ?? "").remove(prevAlias ?? "")) altAliases
"changed the room's aliases", .remove(alias ?? "")
] else ...[ .remove(prevAlias ?? ""))
if (alias != null) "set the room's canonical alias", "changed the room's aliases",
if (altAliases.isNotEmpty) "set 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 "), ]) {
}), [] => "did something related to room aliases",
List prev => prev.join(" and "),
},
),
]), ]),
_ => null, _ => null,
}; };

View file

@ -2,7 +2,8 @@ import "package:material_ui/material_ui.dart";
class const GenericEventRenderer( class const GenericEventRenderer(
final IconData icon, final IconData icon,
final List<Widget> children, { final List<InlineSpan> children, {
final int? maxLines,
super.key, super.key,
}) extends StatelessWidget { }) extends StatelessWidget {
@override @override
@ -13,7 +14,20 @@ class const GenericEventRenderer(
mainAxisSize: .min, mainAxisSize: .min,
children: [ children: [
Padding(padding: .symmetric(horizontal: 4), child: Icon(icon)), 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,
),
),
], ],
), ),
); );

View file

@ -7,8 +7,11 @@ import "package:nexus/models/event.dart";
import "package:nexus/widgets/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart";
import "package:nexus/widgets/renderers/generic_event.dart"; import "package:nexus/widgets/renderers/generic_event.dart";
class const MembershipRenderer(final Event event, {super.key}) class const MembershipRenderer(
extends StatelessWidget { final Event event, {
final int? maxLines,
super.key,
}) extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert( assert(
@ -17,34 +20,45 @@ class const MembershipRenderer(final Event event, {super.key})
); );
return switch (event.content) { return switch (event.content) {
MembershipContent content => GenericEventRenderer(Icons.people, [ MembershipContent content => GenericEventRenderer(
InkWell( Icons.people,
onTap: () => context.showUserPopover( maxLines: maxLines,
content, [
event.stateKey!, WidgetSpan(
roomId: event.roomId, 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( TextSpan(
overflow: .ellipsis, text:
content.displayName ?? event.stateKey!.localpart, "${switch (content.status) {
maxLines: 1, .invite => "was invited to",
style: .new(color: event.sender.colorHash, fontWeight: .bold), .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)
Text( WidgetSpan(
overflow: .ellipsis, alignment: .middle,
maxLines: 1, child: MessageDisplayname(event, shouldWrap: maxLines == null),
"${switch (content.status) { ),
.invite => "was invited to", if (content.reason != null)
.join => "joined", TextSpan(text: "for \"${content.reason}\""),
.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}\""),
]),
_ => SizedBox.shrink(), _ => SizedBox.shrink(),
}; };
} }