From 211c088df9956a91326549089188bc560440e40b Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 19 May 2026 13:37:02 -0400 Subject: [PATCH] fix extra memberships --- lib/models/event.dart | 9 +- lib/widgets/chat_page/render_event.dart | 544 ++++++++++-------- .../chat_page/wrappers/event_wrapper.dart | 1 - 3 files changed, 300 insertions(+), 254 deletions(-) diff --git a/lib/models/event.dart b/lib/models/event.dart index 3d9c0f6..51d8c9f 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -37,7 +37,8 @@ abstract class Event with _$Event { @JsonKey(name: "last_edit_rowid") int? lastEditRowId, @UnreadTypeConverter() UnreadType? unreadType, @JsonKey(fromJson: Event.pmpFromJson) Profile? pmp, - @JsonKey(fromJson: Content.fromJson) required Content content, + required Content content, + required Content? previousContent, }) = _Event; factory Event.fromJson(Map json) => @@ -46,6 +47,12 @@ abstract class Event with _$Event { json["decrypted"] ?? json["content"], json["decrypted_type"] ?? json["type"], ), + previousContent: json["unsigned"]?["prev_content"] == null + ? null + : Content.fromEventJson( + json["unsigned"]?["prev_content"], + json["decrypted_type"] ?? json["type"], + ), ); } diff --git a/lib/widgets/chat_page/render_event.dart b/lib/widgets/chat_page/render_event.dart index 3062068..b5439db 100644 --- a/lib/widgets/chat_page/render_event.dart +++ b/lib/widgets/chat_page/render_event.dart @@ -71,285 +71,325 @@ class RenderEvent extends ConsumerWidget { fontStyle: event.content is EmoteMessageContent ? FontStyle.italic : null, ); - if (event.redactedBy != null) return SizedBox.shrink(); - - final child = switch (event.content) { - Content(:final parseError?) => SelectableText( - "An error occurred while parsing this event:\n$parseError\n${parseError.stackTrace}", - style: errorStyle, - ), - MessageContent() || EncryptedContent() => Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - if (!textOnly) - if (isGrouped) - SizedBox(width: 40) - else - MessageAvatar(event, height: 40), - Expanded( - child: Column( - spacing: 4, + final child = event.redactedBy != null + ? null + : switch (event.content) { + Content(:final parseError?) => SelectableText( + "An error occurred while parsing this event:\n$parseError\n${parseError.stackTrace}", + style: errorStyle, + ), + MessageContent() || EncryptedContent() => Row( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, children: [ - if (!isGrouped && !textOnly) - Row( + if (!textOnly) + if (isGrouped) + SizedBox(width: 40) + else + MessageAvatar(event, height: 40), + Expanded( + child: Column( spacing: 4, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible( - child: MessageDisplayname( - event, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - Flexible(child: timestamp), - ], - ), - Card( - color: - ref.watch( - ClientStateController.provider.select( - (value) => value?.userId, + if (!isGrouped && !textOnly) + Row( + spacing: 4, + children: [ + Flexible( + child: MessageDisplayname( + event, + style: TextStyle(fontWeight: FontWeight.bold), + ), ), - ) == - event.sender - ? (event.eventId.startsWith("~") - ? colorScheme.onPrimary - : colorScheme.primaryContainer) - : colorScheme.surfaceContainer, + Flexible(child: timestamp), + ], + ), + Card( + color: + ref.watch( + ClientStateController.provider.select( + (value) => value?.userId, + ), + ) == + event.sender + ? (event.eventId.startsWith("~") + ? colorScheme.onPrimary + : colorScheme.primaryContainer) + : colorScheme.surfaceContainer, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Quoted( // TODO: Show replies - // EventText(replyEvent textOnly: true, maxLines: 1,) - // ), - switch (event.content) { - EncryptedContent() => Text( - "Unable to decrypt event", - style: errorStyle, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, ), - TextMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - NoticeMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - EmoteMessageContent( - :final body, - :final formattedBody, - :final format, - ) || - ImageMessageContent( - :final body, - :final formattedBody, - :final format, - ) => Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - format == "org.matrix.custom.html" && !textOnly - ? Html( - textStyle: textStyle, - formattedBody!.replaceAllMapped( - RegExp( - "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", - caseSensitive: false, - dotAll: true, - ), - (m) { - // If it's already an tag, leave it unchanged - if (m.group(1) != null) { - return m.group(1)!; - } - - // Otherwise, wrap the bare URL - final url = m.group(2)!; - return "$url"; - }, - ), - ) - : Linkify( - style: textStyle, - text: body, - maxLines: maxLines, - overflow: TextOverflow.ellipsis, - options: LinkifyOptions(humanize: false), - onOpen: (link) => ref - .watch(LaunchHelper.provider) - .launchUrl(Uri.parse(link.url)), - linkStyle: TextStyle( - color: Theme.of( - context, - ).colorScheme.primary, - ), - ), - - if (!textOnly) - if (event.content case ImageMessageContent( - :final url, - :final info, - )) - switch (url?.mxcToHttps( - ref.watch( - ClientStateController.provider.select( - (value) => value!.homeserverUrl!, - ), - ), - )) { - final url? => ConstrainedBox( - constraints: BoxConstraints.loose( - Size.fromWidth(500), - ), - child: ExpandableImage( - url.toString(), - child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(8), - ), - child: Image( - image: CachedNetworkImage( - url.toString(), - ref.watch( - CrossCacheController.provider, + // Quoted( // TODO: Show replies + // EventText(replyEvent textOnly: true, maxLines: 1,) + // ), + switch (event.content) { + EncryptedContent() => Text( + "Unable to decrypt event", + style: errorStyle, + ), + TextMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + NoticeMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + EmoteMessageContent( + :final body, + :final formattedBody, + :final format, + ) || + ImageMessageContent( + :final body, + :final formattedBody, + :final format, + ) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + format == "org.matrix.custom.html" && + !textOnly + ? Html( + textStyle: textStyle, + formattedBody!.replaceAllMapped( + RegExp( + "(]*>.*?<\\/a>)|(\\bhttps?:\\/\\/[^\\s<]+)", + caseSensitive: false, + dotAll: true, ), - headers: ref.headers, + (m) { + // If it's already an tag, leave it unchanged + if (m.group(1) != null) { + return m.group(1)!; + } + + // Otherwise, wrap the bare URL + final url = m.group(2)!; + return "$url"; + }, ), - width: info?.width, - loadingBuilder: - (_, child, loadingProgress) => - loadingProgress == null - ? child - : switch (info?.blurHash) { - final blurHash? => - SizedBox( - width: - info?.width ?? - info?.height ?? - 200, - height: - info?.height ?? - info?.width ?? - 200, - child: BlurHash( - hash: blurHash, + ) + : Linkify( + style: textStyle, + text: body, + maxLines: maxLines, + overflow: TextOverflow.ellipsis, + options: LinkifyOptions( + humanize: false, + ), + onOpen: (link) => ref + .watch(LaunchHelper.provider) + .launchUrl(Uri.parse(link.url)), + linkStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.primary, + ), + ), + + if (!textOnly) + if (event.content + case ImageMessageContent( + :final url, + :final info, + )) + switch (url?.mxcToHttps( + ref.watch( + ClientStateController.provider + .select( + (value) => + value!.homeserverUrl!, + ), + ), + )) { + final url? => ConstrainedBox( + constraints: BoxConstraints.loose( + Size.fromWidth(500), + ), + child: ExpandableImage( + url.toString(), + child: ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular(8), + ), + child: Image( + image: CachedNetworkImage( + url.toString(), + ref.watch( + CrossCacheController + .provider, + ), + headers: ref.headers, + ), + width: info?.width, + loadingBuilder: + ( + _, + child, + loadingProgress, + ) => loadingProgress == null + ? child + : switch (info?.blurHash) { + final blurHash? => + SizedBox( + width: + info?.width ?? + info?.height ?? + 200, + height: + info?.height ?? + info?.width ?? + 200, + child: BlurHash( + hash: blurHash, + ), ), + _ => Loading(), + }, + errorBuilder: + ( + context, + error, + stackTrace, + ) => Center( + child: Text( + "Image Failed to Load", + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.error, ), - _ => Loading(), - }, - errorBuilder: - (context, error, stackTrace) => - Center( - child: Text( - "Image Failed to Load", - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.error, ), ), - ), + ), + ), + ), ), - ), + _ => Text( + "Nexus currently cannot handle encrypted media", + style: errorStyle, + ), + }, + if (event.lastEditRowId != null && + !textOnly) + Text( + "(edited)", + style: theme.textTheme.labelSmall, ), - ), - _ => Text( - "Nexus currently cannot handle encrypted media", + if (linkify(body).firstWhereOrNull( + (element) => element is UrlElement, + ) + case final UrlElement link?) + LinkPreview(link.url), + ], + ), + MessageContent(:final body) => Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Unknown message type:", style: errorStyle, ), - }, - if (event.lastEditRowId != null && !textOnly) - Text( - "(edited)", - style: theme.textTheme.labelSmall, + Text(body), + ], ), - if (linkify(body).firstWhereOrNull( - (element) => element is UrlElement, - ) - case final UrlElement link?) - LinkPreview(link.url), + _ => throw Exception("This is impossible"), + }, ], ), - MessageContent(:final body) => Row( - spacing: 8, - mainAxisSize: MainAxisSize.min, - children: [ - Text("Unknown message type:", style: errorStyle), - Text(body), - ], - ), - _ => throw Exception("This is impossible"), - }, - ], - ), + ), + ), + ], ), ), ], ), - ), - ], - ), - MembershipContent content => Row( - spacing: 4, - children: [ - SizedBox(width: 4), - Icon(Icons.people), - InkWell( - onTapUp: (details) => context.showUserPopover( - content, - event.sender, - globalPosition: details.globalPosition, + MembershipContent content => + event.previousContent is MembershipContent && + (event.previousContent as MembershipContent).status == + content.status + ? null + : Row( + spacing: 4, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.people), + ), + InkWell( + onTapUp: (details) => context.showUserPopover( + content, + event.sender, + globalPosition: details.globalPosition, + ), + child: Text( + content.displayName ?? event.stateKey!.localpart, + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + "${switch (content.status) { + MembershipStatus.invite => "was invited to", + MembershipStatus.join => "joined", + MembershipStatus.leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), + MembershipStatus.ban => "was banned from", + MembershipStatus.knock => "asked to join", + }} the room${content.reason == null ? "" : "because ${content.reason}"}${event.sender == event.stateKey ? "" : " by "}", + ), + if (event.sender != event.stateKey) + MessageDisplayname( + event, + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + AvatarContent() => Row( + spacing: 4, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.numbers), + ), + MessageDisplayname( + event, + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + Text("changed the room avatar"), + ], ), - child: Text( - content.displayName ?? event.stateKey!.localpart, - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - Text( - "${switch (content.status) { - MembershipStatus.invite => "was invited to", - MembershipStatus.join => "joined", - MembershipStatus.leave => event.sender == event.stateKey ? "left" : (event.unsigned["prev_content"]?["membership"] == "ban" ? "was unbanned from" : "was kicked from"), - MembershipStatus.ban => "was banned from", - MembershipStatus.knock => "asked to join", - }} the room. ${content.reason ?? ""}", - ), - ], - ), - AvatarContent() => Row( - spacing: 4, - children: [ - SizedBox(width: 4), - Icon(Icons.numbers), - MessageDisplayname( - event, - style: TextStyle( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - Text("changed the room avatar"), - ], - ), - _ => null, - }; + _ => null, + }; - return GestureDetector( - onSecondaryTapUp: contextMenuCallback, - onLongPressStart: contextMenuCallback, - child: child == null - ? textOnly - ? Text("Unknown event type", style: errorStyle) - : SizedBox.shrink() - : Padding(padding: EdgeInsets.symmetric(vertical: 8), child: child), - ); + return child == null + ? textOnly + ? Text("Unknown event type", style: errorStyle) + : SizedBox.shrink() + : GestureDetector( + onSecondaryTapUp: contextMenuCallback, + onLongPressStart: contextMenuCallback, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: child, + ), + ); } } diff --git a/lib/widgets/chat_page/wrappers/event_wrapper.dart b/lib/widgets/chat_page/wrappers/event_wrapper.dart index fb765da..d131e66 100644 --- a/lib/widgets/chat_page/wrappers/event_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/event_wrapper.dart @@ -27,7 +27,6 @@ class EventWrapper extends StatelessWidget { duration: Duration(milliseconds: 250), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, children: [ child, if (event.sendError != null && event.sendError != "not sent")