Skip to content

Commit 0264de8

Browse files
committed
model: Add MessageEditState to Message.
This new field is computed from edit_history provided by the API. We create a readValue function that processes the list of edits and determine if message has been edited or moved. Some special handling was needed because topic being marked as resolved should not be considered "moved". Signed-off-by: Zixuan James Li <[email protected]>
1 parent 26dc0ae commit 0264de8

File tree

3 files changed

+202
-0
lines changed

3 files changed

+202
-0
lines changed

lib/api/model/model.dart

+89
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,9 @@ sealed class Message {
634634
final String contentType;
635635

636636
// final List<MessageEditHistory> editHistory; // TODO handle
637+
@JsonKey(readValue: MessageEditState.readFromMessage, fromJson: Message._messageEditStateFromJson)
638+
MessageEditState messageEditState;
639+
637640
final int id;
638641
bool isMeMessage;
639642
int? lastEditTimestamp;
@@ -658,6 +661,12 @@ sealed class Message {
658661
final String? matchContent;
659662
final String? matchSubject;
660663

664+
static MessageEditState _messageEditStateFromJson(dynamic json) {
665+
// The value passed here must be a MessageEditState already due to
666+
// processing work done in [MessageEditState.readFromMessage].
667+
return json as MessageEditState;
668+
}
669+
661670
static Reactions? _reactionsFromJson(dynamic json) {
662671
final list = (json as List<dynamic>);
663672
return list.isNotEmpty ? Reactions.fromJson(list) : null;
@@ -676,6 +685,7 @@ sealed class Message {
676685
required this.client,
677686
required this.content,
678687
required this.contentType,
688+
required this.messageEditState,
679689
required this.id,
680690
required this.isMeMessage,
681691
required this.lastEditTimestamp,
@@ -741,6 +751,7 @@ class StreamMessage extends Message {
741751
required super.client,
742752
required super.content,
743753
required super.contentType,
754+
required super.messageEditState,
744755
required super.id,
745756
required super.isMeMessage,
746757
required super.lastEditTimestamp,
@@ -843,6 +854,7 @@ class DmMessage extends Message {
843854
required super.client,
844855
required super.content,
845856
required super.contentType,
857+
required super.messageEditState,
846858
required super.id,
847859
required super.isMeMessage,
848860
required super.lastEditTimestamp,
@@ -866,3 +878,80 @@ class DmMessage extends Message {
866878
@override
867879
Map<String, dynamic> toJson() => _$DmMessageToJson(this);
868880
}
881+
882+
@visibleForTesting
883+
enum MessageEditState {
884+
none,
885+
edited,
886+
moved;
887+
888+
static bool isTopicMoved(String topic, String prevTopic) {
889+
// TODO(#744) Extract this to its own home to fully support mark as resolve
890+
891+
// Code adapted from the shared code: web/shared/src/resolve_topic.ts
892+
893+
/**
894+
* Pattern for an arbitrary resolved-topic prefix.
895+
*
896+
* These always begin with the canonical prefix, but can go on longer.
897+
*/
898+
// The class has the same characters as RESOLVED_TOPIC_PREFIX.
899+
// It's designed to remove a weird "✔ ✔✔ " prefix, if present.
900+
final RegExp resolvedTopicPrefixRe = RegExp('^✔ [ ✔]*');
901+
902+
// Normalize both topics so the resolved-topic prefix do not interfere.
903+
topic = topic.replaceFirst(resolvedTopicPrefixRe, '');
904+
prevTopic = prevTopic.replaceFirst(resolvedTopicPrefixRe, '');
905+
906+
return topic != prevTopic;
907+
}
908+
909+
static MessageEditState readFromMessage(Map<dynamic, dynamic> json, String key) {
910+
// TODO refactor this into a helper that computes this from the serialized
911+
// MessageEditHistory Otherwise, editHistory has to be a list
912+
final editHistory = (json['edit_history'] as List<dynamic>?);
913+
914+
if (editHistory == null) {
915+
return (json['last_edit_timestamp'] != null)
916+
? MessageEditState.edited : MessageEditState.none;
917+
}
918+
919+
// Edit history should never be empty whenever it is present
920+
assert(editHistory.isNotEmpty);
921+
922+
bool hasEditedContent = false;
923+
bool hasMoved = false;
924+
for (final entry in editHistory) {
925+
if (entry['prev_content'] != null) {
926+
hasEditedContent = true;
927+
}
928+
929+
if (entry['prev_stream'] != null) {
930+
hasMoved = true;
931+
}
932+
933+
// TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
934+
if (entry['prev_topic'] != null || entry['prev_subject'] != null) {
935+
// TODO(server-5) pre-5.0 servers do not have the 'topic' field
936+
if (entry['topic'] == null) {
937+
hasMoved = true;
938+
}
939+
else {
940+
hasMoved = isTopicMoved(
941+
entry['topic'] as String,
942+
// TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
943+
(entry['prev_topic'] ?? entry['prev_subject']) as String
944+
);
945+
}
946+
}
947+
}
948+
949+
// Prioritize the 'edited' state over 'moved' when they both apply
950+
if (hasEditedContent) return MessageEditState.edited;
951+
952+
if (hasMoved) return MessageEditState.moved;
953+
954+
// This can happen when a topic is resolved but nothing else has been edited
955+
return MessageEditState.none;
956+
}
957+
}

lib/api/model/model.g.dart

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/api/model/model_test.dart

+99
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ void main() {
564564
);
565565
check(m2).flags.deepEquals([MessageFlag.read, MessageFlag.unknown]);
566566
});
567+
568+
// Code relevant to editHistory is tested separately in the MessageEditState
569+
// group.
567570
});
568571

569572
group('DmMessage', () {
@@ -628,4 +631,100 @@ void main() {
628631
.deepEquals([2, 3, 11]);
629632
});
630633
});
634+
635+
group('MessageEditState', () {
636+
Map<String, dynamic> baseJson() => deepToJson(eg.streamMessage()) as Map<String, dynamic>;
637+
638+
test('edit history is absent', () {
639+
check(Message.fromJson(baseJson()
640+
..['edit_history'] = null
641+
).messageEditState).equals(MessageEditState.none);
642+
643+
check(Message.fromJson(baseJson()
644+
..['edit_history'] = null
645+
..['last_edit_timestamp'] = 1678139636).messageEditState
646+
).equals(MessageEditState.edited);
647+
});
648+
649+
test('edit history exists', () {
650+
// Only channel name changed: moved
651+
check(Message.fromJson(baseJson()
652+
..['edit_history'] = [{
653+
'prev_stream': 'old_stream',
654+
'stream': 'new_stream',
655+
}]
656+
).messageEditState).equals(MessageEditState.moved);
657+
658+
// Only topic name changed: moved
659+
check(Message.fromJson(baseJson()
660+
..['edit_history'] = [{
661+
'prev_topic': 'old_topic',
662+
'topic': 'new_topic',
663+
}]
664+
).messageEditState).equals(MessageEditState.moved);
665+
666+
// Both topic and content changed: edited
667+
check(Message.fromJson(baseJson()
668+
..['edit_history'] = [{
669+
'prev_topic': 'old_topic',
670+
'topic': 'new_topic',
671+
}, {
672+
'prev_content': 'old_content',
673+
}]
674+
).messageEditState).equals(MessageEditState.edited);
675+
676+
// Only content changed: edited
677+
check(Message.fromJson(baseJson()
678+
..['edit_history'] = [{
679+
'prev_content': 'old_content',
680+
}]
681+
).messageEditState).equals(MessageEditState.edited);
682+
683+
// 'prev_topic' present without the 'topic' field: moved
684+
check(Message.fromJson(baseJson()
685+
..['edit_history'] = [{
686+
'prev_topic': 'old_topic',
687+
}]
688+
).messageEditState).equals(MessageEditState.moved);
689+
});
690+
691+
test('topic resolved in edit history', () {
692+
// Topic was only resolved: none
693+
check(Message.fromJson(baseJson()
694+
..['edit_history'] = [{
695+
'prev_topic': 'old_topic',
696+
'topic': '✔ old_topic',
697+
}]
698+
).messageEditState).equals(MessageEditState.none);
699+
700+
// Topic was resolved but the content changed in the history: edited
701+
check(Message.fromJson(baseJson()
702+
..['edit_history'] = [{
703+
'prev_topic': 'old_topic',
704+
'topic': '✔ old_topic',
705+
}, {
706+
'prev_content': 'old_content',
707+
}]
708+
).messageEditState).equals(MessageEditState.edited);
709+
710+
// Topic was resolved but it also changed in the history: moved
711+
check(Message.fromJson(baseJson()
712+
..['edit_history'] = [{
713+
'prev_topic': '✔ old_topic',
714+
'topic': 'old_topic',
715+
}, {
716+
'prev_topic': 'old_topic',
717+
'topic': 'new_topic',
718+
}]
719+
).messageEditState).equals(MessageEditState.moved);
720+
721+
// Unresolving topic with a weird prefix:
722+
check(Message.fromJson(baseJson()
723+
..['edit_history'] = [{
724+
'prev_topic': '✔ ✔old_topic',
725+
'topic': 'old_topic',
726+
}]
727+
).messageEditState).equals(MessageEditState.none);
728+
});
729+
});
631730
}

0 commit comments

Comments
 (0)