@@ -464,6 +464,9 @@ sealed class Message {
464
464
final String contentType;
465
465
466
466
// final List<MessageEditHistory> editHistory; // TODO handle
467
+ @JsonKey (readValue: MessageEditState .readFromMessage, fromJson: Message ._messageEditStateFromJson)
468
+ MessageEditState editState;
469
+
467
470
final int id;
468
471
bool isMeMessage;
469
472
int ? lastEditTimestamp;
@@ -490,6 +493,12 @@ sealed class Message {
490
493
@JsonKey (name: 'match_subject' )
491
494
final String ? matchTopic;
492
495
496
+ static MessageEditState _messageEditStateFromJson (dynamic json) {
497
+ // The value passed here must be a MessageEditState already due to
498
+ // processing work done in [MessageEditState.readFromMessage].
499
+ return json as MessageEditState ;
500
+ }
501
+
493
502
static Reactions ? _reactionsFromJson (dynamic json) {
494
503
final list = (json as List <dynamic >);
495
504
return list.isNotEmpty ? Reactions .fromJson (list) : null ;
@@ -508,6 +517,7 @@ sealed class Message {
508
517
required this .client,
509
518
required this .content,
510
519
required this .contentType,
520
+ required this .editState,
511
521
required this .id,
512
522
required this .isMeMessage,
513
523
required this .lastEditTimestamp,
@@ -573,6 +583,7 @@ class StreamMessage extends Message {
573
583
required super .client,
574
584
required super .content,
575
585
required super .contentType,
586
+ required super .editState,
576
587
required super .id,
577
588
required super .isMeMessage,
578
589
required super .lastEditTimestamp,
@@ -675,6 +686,7 @@ class DmMessage extends Message {
675
686
required super .client,
676
687
required super .content,
677
688
required super .contentType,
689
+ required super .editState,
678
690
required super .id,
679
691
required super .isMeMessage,
680
692
required super .lastEditTimestamp,
@@ -698,3 +710,78 @@ class DmMessage extends Message {
698
710
@override
699
711
Map <String , dynamic > toJson () => _$DmMessageToJson (this );
700
712
}
713
+
714
+ enum MessageEditState {
715
+ none,
716
+ edited,
717
+ moved;
718
+
719
+ // Code adapted from the shared code: web/shared/src/resolve_topic.ts
720
+ // Pattern for an arbitrary resolved-topic prefix.
721
+ // These always begin with the canonical prefix, but can go on longer.
722
+ // It's designed to remove a weird "✔ ✔✔ " prefix, if present.
723
+ static RegExp resolvedTopicPrefixRe = RegExp ('^✔ [ ✔]*' );
724
+
725
+ /// Whether two topics are equal, ignoring any resolved-topic prefix.
726
+ ///
727
+ /// When a topic is resolved, the clients agree on adding a ✔ prefix to the
728
+ /// topic string. Topics whose only difference is the ✔ prefix are considered
729
+ /// the same. This helper can be helpful when checking if a message has been
730
+ /// moved.
731
+ static bool areSameTopic (String topic, String prevTopic) {
732
+ // TODO(#744) Extract this to its own home to support "mark as resolve".
733
+ topic = topic.replaceFirst (resolvedTopicPrefixRe, '' );
734
+ prevTopic = prevTopic.replaceFirst (resolvedTopicPrefixRe, '' );
735
+
736
+ return topic == prevTopic;
737
+ }
738
+
739
+ static MessageEditState readFromMessage (Map <dynamic , dynamic > json, String key) {
740
+ // TODO refactor this into a helper that computes this from the serialized
741
+ // MessageEditHistory.
742
+ final editHistory = json['edit_history' ] as List <dynamic >? ;
743
+ final lastEditTimestamp = json['last_edit_timestamp' ] as int ? ;
744
+ if (editHistory == null ) {
745
+ return (lastEditTimestamp != null )
746
+ ? MessageEditState .edited
747
+ : MessageEditState .none;
748
+ }
749
+
750
+ // Edit history should never be empty whenever it is present
751
+ assert (editHistory.isNotEmpty);
752
+
753
+ bool hasEditedContent = false ;
754
+ bool hasMoved = false ;
755
+ for (final entry in editHistory) {
756
+ if (entry['prev_content' ] != null ) {
757
+ hasEditedContent = true ;
758
+ }
759
+
760
+ if (entry['prev_stream' ] != null ) {
761
+ hasMoved = true ;
762
+ }
763
+
764
+ // TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
765
+ if (entry['prev_topic' ] != null || entry['prev_subject' ] != null ) {
766
+ // TODO(server-5) pre-5.0 servers do not have the 'topic' field
767
+ if (entry['topic' ] == null ) {
768
+ hasMoved = true ;
769
+ } else {
770
+ hasMoved = ! areSameTopic (
771
+ entry['topic' ] as String ,
772
+ // TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
773
+ (entry['prev_topic' ] ?? entry['prev_subject' ]) as String
774
+ );
775
+ }
776
+ }
777
+ }
778
+
779
+ // Prioritize the 'edited' state over 'moved' when they both apply
780
+ if (hasEditedContent) return MessageEditState .edited;
781
+
782
+ if (hasMoved) return MessageEditState .moved;
783
+
784
+ // This can happen when a topic is resolved but nothing else has been edited
785
+ return MessageEditState .none;
786
+ }
787
+ }
0 commit comments