From 1365cbffe5c39c3fa8131373acba018643aa18cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Ku=C3=9Fowski?= Date: Wed, 5 Nov 2025 08:50:51 +0100 Subject: [PATCH] refactor: (BREAKING) Replace Event.relationshipType and Event.relationshipEventId with Event.inReplyToEventId() for replies. This fixes the situation that an event can be a reply and in a thread. Before we have seen reply as an relationshipType but this does conflict with the concept of threads, where an event can be of relationship type "thread" but also be a reply with being a fallback or not. --- lib/src/event.dart | 77 +++++++++++++++++--------------------------- test/event_test.dart | 24 +++++++++++--- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/lib/src/event.dart b/lib/src/event.dart index cbe6548a..4317855a 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -32,7 +32,6 @@ import 'package:matrix/src/utils/markdown.dart'; import 'package:matrix/src/utils/multipart_request_progress.dart'; abstract class RelationshipTypes { - static const String reply = 'm.in_reply_to'; static const String edit = 'm.replace'; static const String reaction = 'm.annotation'; static const String reference = 'm.reference'; @@ -490,31 +489,13 @@ class Event extends MatrixEvent { /// event fallback if the relationship type is `m.thread`. /// https://spec.matrix.org/v1.14/client-server-api/#fallback-for-unthreaded-clients Future getReplyEvent(Timeline timeline) async { - switch (relationshipType) { - case RelationshipTypes.reply: - final relationshipEventId = this.relationshipEventId; - return relationshipEventId == null - ? null - : await timeline.getEventById(relationshipEventId); - - case RelationshipTypes.thread: - final relationshipContent = - content.tryGetMap('m.relates_to'); - if (relationshipContent == null) return null; - final String? relationshipEventId; - if (relationshipContent.tryGet('is_falling_back') == true) { - relationshipEventId = relationshipContent - .tryGetMap('m.in_reply_to') - ?.tryGet('event_id'); - } else { - relationshipEventId = this.relationshipEventId; - } - return relationshipEventId == null - ? null - : await timeline.getEventById(relationshipEventId); - default: - return null; - } + final relationshipEventId = content + .tryGetMap('m.relates_to') + ?.tryGetMap('m.in_reply_to') + ?.tryGet('event_id'); + return relationshipEventId == null + ? null + : await timeline.getEventById(relationshipEventId); } /// If this event is encrypted and the decryption was not successful because @@ -1021,30 +1002,30 @@ class Event extends MatrixEvent { return transactionId == search; } - /// Get the relationship type of an event. `null` if there is none - String? get relationshipType { - final mRelatesTo = content.tryGetMap('m.relates_to'); - if (mRelatesTo == null) { - return null; - } - final relType = mRelatesTo.tryGet('rel_type'); - if (relType == RelationshipTypes.thread) { - return RelationshipTypes.thread; - } + /// Get the relationship type of an event. `null` if there is none. + String? get relationshipType => content + .tryGetMap('m.relates_to') + ?.tryGet('rel_type'); - if (mRelatesTo.containsKey('m.in_reply_to')) { - return RelationshipTypes.reply; - } - return relType; - } + /// Get the event ID that this relationship will reference and `null` if there + /// is none. This could for example be the thread root, the original event for + /// an edit or the event, this is an reaction for. For replies please use + /// `Event.inReplyToEventId()` instead! + String? get relationshipEventId => content + .tryGetMap('m.relates_to') + ?.tryGet('event_id'); - /// Get the event ID that this relationship will reference. `null` if there is none - String? get relationshipEventId { - final relatesToMap = content.tryGetMap('m.relates_to'); - return relatesToMap?.tryGet('event_id') ?? - relatesToMap - ?.tryGetMap('m.in_reply_to') - ?.tryGet('event_id'); + /// If this event is in reply to another event, this returns the event ID or + /// null if this event is not a reply. + String? inReplyToEventId({bool includingFallback = true}) { + final isFallback = content + .tryGetMap('m.relates_to') + ?.tryGet('is_falling_back'); + if (isFallback == true && !includingFallback) return null; + return content + .tryGetMap('m.relates_to') + ?.tryGetMap('m.in_reply_to') + ?.tryGet('event_id'); } /// Get whether this event has aggregated events from a certain [type] diff --git a/test/event_test.dart b/test/event_test.dart index 0bfb297f..d5b17d1d 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -79,7 +79,7 @@ void main() async { expect(event.formattedText, formatted_body); expect(event.body, body); expect(event.type, EventTypes.Message); - expect(event.relationshipType, RelationshipTypes.reply); + expect(event.inReplyToEventId(), '\$1234:example.com'); jsonObj['state_key'] = ''; final state = Event.fromJson(jsonObj, room); expect(state.eventId, id); @@ -178,8 +178,8 @@ void main() async { }; event = Event.fromJson(jsonObj, room); expect(event.messageType, MessageTypes.Text); - expect(event.relationshipType, RelationshipTypes.reply); - expect(event.relationshipEventId, '1234'); + expect(event.inReplyToEventId(), '1234'); + expect(event.relationshipEventId, null); }); test('relationship types', () async { @@ -212,8 +212,22 @@ void main() async { }, }; event = Event.fromJson(jsonObj, room); - expect(event.relationshipType, RelationshipTypes.reply); - expect(event.relationshipEventId, 'def'); + expect(event.inReplyToEventId(), 'def'); + expect(event.relationshipEventId, null); + + jsonObj['content']['m.relates_to'] = { + 'rel_type': 'm.thread', + 'event_id': '\$root', + 'm.in_reply_to': { + 'event_id': '\$target', + }, + 'is_falling_back': true, + }; + event = Event.fromJson(jsonObj, room); + expect(event.relationshipType, RelationshipTypes.thread); + expect(event.inReplyToEventId(), '\$target'); + expect(event.inReplyToEventId(includingFallback: false), null); + expect(event.relationshipEventId, '\$root'); }); test('redact', () async {