@@ -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,81 @@ 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
+ /// Whether two topics are equal, ignoring any resolved-topic prefix.
720
+ ///
721
+ /// When a topic is resolved, the clients agree on adding a ✔ prefix to the
722
+ /// topic string. Topics whose only difference is the ✔ prefix are considered
723
+ /// the same. This helper can be helpful when checking if a message has been
724
+ /// moved.
725
+ static bool areSameTopic (String topic, String prevTopic) {
726
+ // TODO(#744) Extract this to its own home to support "mark as resolve".
727
+
728
+ // Code adapted from the shared code: web/shared/src/resolve_topic.ts
729
+
730
+ // Pattern for an arbitrary resolved-topic prefix.
731
+ // These always begin with the canonical prefix, but can go on longer.
732
+ // It's designed to remove a weird "✔ ✔✔ " prefix, if present.
733
+ final RegExp resolvedTopicPrefixRe = RegExp ('^✔ [ ✔]*' );
734
+
735
+ topic = topic.replaceFirst (resolvedTopicPrefixRe, '' );
736
+ prevTopic = prevTopic.replaceFirst (resolvedTopicPrefixRe, '' );
737
+
738
+ return topic != prevTopic;
739
+ }
740
+
741
+ static MessageEditState readFromMessage (Map <dynamic , dynamic > json, String key) {
742
+ // TODO refactor this into a helper that computes this from the serialized
743
+ // MessageEditHistory.
744
+ final editHistory = json['edit_history' ] as List <dynamic >? ;
745
+ final lastEditTimestamp = json['last_edit_timestamp' ] as int ? ;
746
+ if (editHistory == null ) {
747
+ return (lastEditTimestamp != null )
748
+ ? MessageEditState .edited
749
+ : MessageEditState .none;
750
+ }
751
+
752
+ // Edit history should never be empty whenever it is present
753
+ assert (editHistory.isNotEmpty);
754
+
755
+ bool hasEditedContent = false ;
756
+ bool hasMoved = false ;
757
+ for (final entry in editHistory) {
758
+ if (entry['prev_content' ] != null ) {
759
+ hasEditedContent = true ;
760
+ }
761
+
762
+ if (entry['prev_stream' ] != null ) {
763
+ hasMoved = true ;
764
+ }
765
+
766
+ // TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
767
+ if (entry['prev_topic' ] != null || entry['prev_subject' ] != null ) {
768
+ // TODO(server-5) pre-5.0 servers do not have the 'topic' field
769
+ if (entry['topic' ] == null ) {
770
+ hasMoved = true ;
771
+ }
772
+ else {
773
+ hasMoved = areSameTopic (
774
+ entry['topic' ] as String ,
775
+ // TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
776
+ (entry['prev_topic' ] ?? entry['prev_subject' ]) as String
777
+ );
778
+ }
779
+ }
780
+ }
781
+
782
+ // Prioritize the 'edited' state over 'moved' when they both apply
783
+ if (hasEditedContent) return MessageEditState .edited;
784
+
785
+ if (hasMoved) return MessageEditState .moved;
786
+
787
+ // This can happen when a topic is resolved but nothing else has been edited
788
+ return MessageEditState .none;
789
+ }
790
+ }
0 commit comments