@@ -634,6 +634,9 @@ sealed class Message {
634
634
final String contentType;
635
635
636
636
// final List<MessageEditHistory> editHistory; // TODO handle
637
+ @JsonKey (readValue: MessageEditState .readFromMessage, fromJson: Message ._messageEditStateFromJson)
638
+ MessageEditState editState;
639
+
637
640
final int id;
638
641
bool isMeMessage;
639
642
int ? lastEditTimestamp;
@@ -660,6 +663,12 @@ sealed class Message {
660
663
@JsonKey (name: 'match_subject' )
661
664
final String ? matchTopic;
662
665
666
+ static MessageEditState _messageEditStateFromJson (dynamic json) {
667
+ // The value passed here must be a MessageEditState already due to
668
+ // processing work done in [MessageEditState.readFromMessage].
669
+ return json as MessageEditState ;
670
+ }
671
+
663
672
static Reactions ? _reactionsFromJson (dynamic json) {
664
673
final list = (json as List <dynamic >);
665
674
return list.isNotEmpty ? Reactions .fromJson (list) : null ;
@@ -678,6 +687,7 @@ sealed class Message {
678
687
required this .client,
679
688
required this .content,
680
689
required this .contentType,
690
+ required this .editState,
681
691
required this .id,
682
692
required this .isMeMessage,
683
693
required this .lastEditTimestamp,
@@ -743,6 +753,7 @@ class StreamMessage extends Message {
743
753
required super .client,
744
754
required super .content,
745
755
required super .contentType,
756
+ required super .editState,
746
757
required super .id,
747
758
required super .isMeMessage,
748
759
required super .lastEditTimestamp,
@@ -845,6 +856,7 @@ class DmMessage extends Message {
845
856
required super .client,
846
857
required super .content,
847
858
required super .contentType,
859
+ required super .editState,
848
860
required super .id,
849
861
required super .isMeMessage,
850
862
required super .lastEditTimestamp,
@@ -868,3 +880,84 @@ class DmMessage extends Message {
868
880
@override
869
881
Map <String , dynamic > toJson () => _$DmMessageToJson (this );
870
882
}
883
+
884
+ enum MessageEditState {
885
+ none,
886
+ edited,
887
+ moved;
888
+
889
+ /// When a topic is resolved, the clients agree on adding a ✔ prefix to the
890
+ /// topic string. When deciding whether a message has been moved or not, two
891
+ /// topics whose only difference is the ✔ prefix are considered the same.
892
+ static bool areSameTopic (String topic, String prevTopic) {
893
+ // TODO(#744) Extract this to its own home to fully support mark as resolve
894
+
895
+ // Code adapted from the shared code: web/shared/src/resolve_topic.ts
896
+
897
+ /**
898
+ * Pattern for an arbitrary resolved-topic prefix.
899
+ *
900
+ * These always begin with the canonical prefix, but can go on longer.
901
+ */
902
+ // The class has the same characters as RESOLVED_TOPIC_PREFIX.
903
+ // It's designed to remove a weird "✔ ✔✔ " prefix, if present.
904
+ final RegExp resolvedTopicPrefixRe = RegExp ('^✔ [ ✔]*' );
905
+
906
+ // Normalize both topics so the resolved-topic prefix do not interfere.
907
+ topic = topic.replaceFirst (resolvedTopicPrefixRe, '' );
908
+ prevTopic = prevTopic.replaceFirst (resolvedTopicPrefixRe, '' );
909
+
910
+ return topic != prevTopic;
911
+ }
912
+
913
+ static MessageEditState readFromMessage (Map <dynamic , dynamic > json, String key) {
914
+ return isMoved (json['edit_history' ] as List <dynamic >? , json['last_edit_timestamp' ] as int ? );
915
+ }
916
+
917
+ static MessageEditState isMoved (List <dynamic >? editHistory, int ? lastEditTimeStamp) {
918
+ // TODO refactor this into a helper that computes this from the serialized
919
+ // MessageEditHistory.
920
+ if (editHistory == null ) {
921
+ return (lastEditTimeStamp != null )
922
+ ? MessageEditState .edited : MessageEditState .none;
923
+ }
924
+
925
+ // Edit history should never be empty whenever it is present
926
+ assert (editHistory.isNotEmpty);
927
+
928
+ bool hasEditedContent = false ;
929
+ bool hasMoved = false ;
930
+ for (final entry in editHistory) {
931
+ if (entry['prev_content' ] != null ) {
932
+ hasEditedContent = true ;
933
+ }
934
+
935
+ if (entry['prev_stream' ] != null ) {
936
+ hasMoved = true ;
937
+ }
938
+
939
+ // TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
940
+ if (entry['prev_topic' ] != null || entry['prev_subject' ] != null ) {
941
+ // TODO(server-5) pre-5.0 servers do not have the 'topic' field
942
+ if (entry['topic' ] == null ) {
943
+ hasMoved = true ;
944
+ }
945
+ else {
946
+ hasMoved = areSameTopic (
947
+ entry['topic' ] as String ,
948
+ // TODO(server-5) prev_subject was the old name of prev_topic on pre-5.0 servers
949
+ (entry['prev_topic' ] ?? entry['prev_subject' ]) as String
950
+ );
951
+ }
952
+ }
953
+ }
954
+
955
+ // Prioritize the 'edited' state over 'moved' when they both apply
956
+ if (hasEditedContent) return MessageEditState .edited;
957
+
958
+ if (hasMoved) return MessageEditState .moved;
959
+
960
+ // This can happen when a topic is resolved but nothing else has been edited
961
+ return MessageEditState .none;
962
+ }
963
+ }
0 commit comments