1
1
import 'package:json_annotation/json_annotation.dart' ;
2
2
3
+ import '../../model/algorithms.dart' ;
3
4
import 'events.dart' ;
4
5
import 'initial_snapshot.dart' ;
5
6
import 'reaction.dart' ;
@@ -531,6 +532,111 @@ String? tryParseEmojiCodeToUnicode(String emojiCode) {
531
532
}
532
533
}
533
534
535
+ /// The name of a Zulip topic.
536
+ // TODO(dart): Can we forbid calling Object members on this extension type?
537
+ // (The lack of "implements Object" ought to do that, but doesn't.)
538
+ // In particular an interpolation "foo > $topic" is a bug we'd like to catch.
539
+ // TODO(dart): Can we forbid using this extension type as a key in a Map?
540
+ // (The lack of "implements Object" arguably should do that, but doesn't.)
541
+ // Using as a Map key is almost certainly a bug because it won't case-fold;
542
+ // see for example #739, #980, #1205.
543
+ extension type const TopicName (String _value) {
544
+ /// The canonical form of the resolved-topic prefix.
545
+ // This is RESOLVED_TOPIC_PREFIX in web:
546
+ // https://github.com/zulip/zulip/blob/1fac99733/web/shared/src/resolved_topic.ts
547
+ static const resolvedTopicPrefix = '✔ ' ;
548
+
549
+ /// Pattern for an arbitrary resolved-topic prefix.
550
+ ///
551
+ /// These always begin with [resolvedTopicPrefix]
552
+ /// but can be weird and go on longer, like "✔ ✔✔ ".
553
+ // This is RESOLVED_TOPIC_PREFIX_RE in web:
554
+ // https://github.com/zulip/zulip/blob/1fac99733/web/shared/src/resolved_topic.ts#L4-L12
555
+ static final resolvedTopicPrefixRegexp = RegExp (r'^✔ [ ✔]*' );
556
+
557
+ /// The string this topic is identified by in the Zulip API.
558
+ ///
559
+ /// This should be used in constructing HTTP requests to the server,
560
+ /// but rarely for other purposes. See [displayName] and [canonicalize] .
561
+ String get apiName => _value;
562
+
563
+ /// The string this topic is displayed as to the user in our UI.
564
+ ///
565
+ /// At the moment this always equals [apiName] .
566
+ /// In the future this will become null for the "general chat" topic (#1250),
567
+ /// so that UI code can identify when it needs to represent the topic
568
+ /// specially in the way prescribed for "general chat".
569
+ // TODO(#1250) carry out that plan
570
+ String get displayName => _value;
571
+
572
+ /// The key to use for "same topic as" comparisons.
573
+ String canonicalize () => apiName.toLowerCase ();
574
+
575
+ /// Whether the topic starts with [resolvedTopicPrefix] .
576
+ bool get isResolved => _value.startsWith (resolvedTopicPrefix);
577
+
578
+ /// This [TopicName] plus the [resolvedTopicPrefix] prefix.
579
+ TopicName resolve () => TopicName (resolvedTopicPrefix + _value);
580
+
581
+ /// A [TopicName] with [resolvedTopicPrefixRegexp] stripped if present.
582
+ TopicName unresolve () =>
583
+ TopicName (_value.replaceFirst (resolvedTopicPrefixRegexp, '' ));
584
+
585
+ /// Whether [this] and [other] have the same canonical form,
586
+ /// using [canonicalize] .
587
+ bool isSameAs (TopicName other) => canonicalize () == other.canonicalize ();
588
+
589
+ TopicName .fromJson (this ._value);
590
+
591
+ String toJson () => apiName;
592
+ }
593
+
594
+ /// As in [StreamMessage.conversation] and [DmMessage.conversation] .
595
+ ///
596
+ /// Different from [MessageDestination] , this information comes from
597
+ /// [getMessages] or [getEvents] , identifying the conversation that contains a
598
+ /// message.
599
+ sealed class Conversation {}
600
+
601
+ /// The conversation a stream message is in.
602
+ @JsonSerializable (fieldRename: FieldRename .snake, createToJson: false )
603
+ class StreamConversation extends Conversation {
604
+ StreamConversation (this .streamId, this .topic);
605
+
606
+ /// The name of the stream, found on stream message objects from the server.
607
+ ///
608
+ /// This is not updated when its name changes. Consider using [streamId]
609
+ /// instead to lookup stream name from the store.
610
+ @JsonKey (
611
+ // Make sure that this isn't nullable API-wise. If a message moves across
612
+ // channels, [displayRecipient] can still refer to the original channel
613
+ // and has to be invalidated.
614
+ required : true , disallowNullValue: true
615
+ )
616
+ String ? displayRecipient;
617
+
618
+ int streamId;
619
+
620
+ @JsonKey (name: 'subject' )
621
+ TopicName topic;
622
+
623
+ factory StreamConversation .fromJson (Map <String , dynamic > json) =>
624
+ _$StreamConversationFromJson (json);
625
+ }
626
+
627
+ /// The conversation a DM message is in.
628
+ class DmConversation extends Conversation {
629
+ DmConversation ({required this .allRecipientIds})
630
+ : assert (isSortedWithoutDuplicates (allRecipientIds.toList ()));
631
+
632
+ /// The user IDs of all users in the conversation, sorted numerically.
633
+ ///
634
+ /// This lists the sender as well as all (other) recipients, and it
635
+ /// lists each user just once. In particular the self-user is always
636
+ /// included.
637
+ final List <int > allRecipientIds;
638
+ }
639
+
534
640
/// As in the get-messages response.
535
641
///
536
642
/// https://zulip.com/api/get-messages#response
@@ -655,85 +761,31 @@ enum MessageFlag {
655
761
String toJson () => _$MessageFlagEnumMap [this ]! ;
656
762
}
657
763
658
- /// The name of a Zulip topic.
659
- // TODO(dart): Can we forbid calling Object members on this extension type?
660
- // (The lack of "implements Object" ought to do that, but doesn't.)
661
- // In particular an interpolation "foo > $topic" is a bug we'd like to catch.
662
- // TODO(dart): Can we forbid using this extension type as a key in a Map?
663
- // (The lack of "implements Object" arguably should do that, but doesn't.)
664
- // Using as a Map key is almost certainly a bug because it won't case-fold;
665
- // see for example #739, #980, #1205.
666
- extension type const TopicName (String _value) {
667
- /// The canonical form of the resolved-topic prefix.
668
- // This is RESOLVED_TOPIC_PREFIX in web:
669
- // https://github.com/zulip/zulip/blob/1fac99733/web/shared/src/resolved_topic.ts
670
- static const resolvedTopicPrefix = '✔ ' ;
671
-
672
- /// Pattern for an arbitrary resolved-topic prefix.
673
- ///
674
- /// These always begin with [resolvedTopicPrefix]
675
- /// but can be weird and go on longer, like "✔ ✔✔ ".
676
- // This is RESOLVED_TOPIC_PREFIX_RE in web:
677
- // https://github.com/zulip/zulip/blob/1fac99733/web/shared/src/resolved_topic.ts#L4-L12
678
- static final resolvedTopicPrefixRegexp = RegExp (r'^✔ [ ✔]*' );
679
-
680
- /// The string this topic is identified by in the Zulip API.
681
- ///
682
- /// This should be used in constructing HTTP requests to the server,
683
- /// but rarely for other purposes. See [displayName] and [canonicalize] .
684
- String get apiName => _value;
685
-
686
- /// The string this topic is displayed as to the user in our UI.
687
- ///
688
- /// At the moment this always equals [apiName] .
689
- /// In the future this will become null for the "general chat" topic (#1250),
690
- /// so that UI code can identify when it needs to represent the topic
691
- /// specially in the way prescribed for "general chat".
692
- // TODO(#1250) carry out that plan
693
- String get displayName => _value;
694
-
695
- /// The key to use for "same topic as" comparisons.
696
- String canonicalize () => apiName.toLowerCase ();
697
-
698
- /// Whether the topic starts with [resolvedTopicPrefix] .
699
- bool get isResolved => _value.startsWith (resolvedTopicPrefix);
700
-
701
- /// This [TopicName] plus the [resolvedTopicPrefix] prefix.
702
- TopicName resolve () => TopicName (resolvedTopicPrefix + _value);
703
-
704
- /// A [TopicName] with [resolvedTopicPrefixRegexp] stripped if present.
705
- TopicName unresolve () =>
706
- TopicName (_value.replaceFirst (resolvedTopicPrefixRegexp, '' ));
707
-
708
- /// Whether [this] and [other] have the same canonical form,
709
- /// using [canonicalize] .
710
- bool isSameAs (TopicName other) => canonicalize () == other.canonicalize ();
711
-
712
- TopicName .fromJson (this ._value);
713
-
714
- String toJson () => apiName;
715
- }
716
-
717
764
@JsonSerializable (fieldRename: FieldRename .snake)
718
765
class StreamMessage extends Message {
719
766
@override
720
767
@JsonKey (includeToJson: true )
721
768
String get type => 'stream' ;
722
769
723
- // This is not nullable API-wise, but if the message moves across channels,
724
- // [displayRecipient] still refers to the original channel and it has to be
725
- // invalidated.
726
- @JsonKey (required : true , disallowNullValue: true )
727
- String ? displayRecipient;
770
+ @JsonKey (includeToJson: true )
771
+ String ? get displayRecipient => conversation.displayRecipient;
728
772
729
- int streamId;
773
+ @JsonKey (includeToJson: true )
774
+ int get streamId => conversation.streamId;
730
775
731
776
// The topic/subject is documented to be present on DMs too, just empty.
732
777
// We ignore it on DMs; if a future server introduces distinct topics in DMs,
733
778
// that will need new UI that we'll design then as part of that feature,
734
779
// and ignoring the topics seems as good a fallback behavior as any.
735
- @JsonKey (name: 'subject' )
736
- TopicName topic;
780
+ @JsonKey (name: 'subject' , includeToJson: true )
781
+ TopicName get topic => conversation.topic;
782
+
783
+ @JsonKey (readValue: _readConversation, includeToJson: false )
784
+ StreamConversation conversation;
785
+
786
+ static Map <String , dynamic > _readConversation (Map <dynamic , dynamic > json, String key) {
787
+ return json as Map <String , dynamic >;
788
+ }
737
789
738
790
StreamMessage ({
739
791
required super .client,
@@ -753,9 +805,7 @@ class StreamMessage extends Message {
753
805
required super .flags,
754
806
required super .matchContent,
755
807
required super .matchTopic,
756
- required this .displayRecipient,
757
- required this .streamId,
758
- required this .topic,
808
+ required this .conversation,
759
809
});
760
810
761
811
factory StreamMessage .fromJson (Map <String , dynamic > json) =>
@@ -781,20 +831,23 @@ class DmMessage extends Message {
781
831
/// included.
782
832
// TODO(server): Document that it's all users. That statement is based on
783
833
// reverse-engineering notes in zulip-mobile:src/api/modelTypes.js at PmMessage.
784
- @JsonKey (name: 'display_recipient' , fromJson : _allRecipientIdsFromJson, toJson : _allRecipientIdsToJson )
785
- final List <int > allRecipientIds;
834
+ @JsonKey (name: 'display_recipient' , toJson : _allRecipientIdsToJson, includeToJson : true )
835
+ List <int > get allRecipientIds => conversation. allRecipientIds;
786
836
787
- static List <int > _allRecipientIdsFromJson (Object ? json) {
788
- return (json as List <dynamic >).map (
789
- (element) => ((element as Map <String , dynamic >)['id' ] as num ).toInt ()
790
- ).toList (growable: false )
791
- ..sort ();
792
- }
837
+ @JsonKey (name: 'display_recipient' , fromJson: _conversationFromJson, includeToJson: false )
838
+ final DmConversation conversation;
793
839
794
840
static List <Map <String , dynamic >> _allRecipientIdsToJson (List <int > allRecipientIds) {
795
841
return allRecipientIds.map ((element) => {'id' : element}).toList ();
796
842
}
797
843
844
+ static DmConversation _conversationFromJson (List <dynamic > json) {
845
+ return DmConversation (allRecipientIds: json.map (
846
+ (element) => ((element as Map <String , dynamic >)['id' ] as num ).toInt ()
847
+ ).toList (growable: false )
848
+ ..sort ());
849
+ }
850
+
798
851
DmMessage ({
799
852
required super .client,
800
853
required super .content,
@@ -813,7 +866,7 @@ class DmMessage extends Message {
813
866
required super .flags,
814
867
required super .matchContent,
815
868
required super .matchTopic,
816
- required this .allRecipientIds ,
869
+ required this .conversation ,
817
870
});
818
871
819
872
factory DmMessage .fromJson (Map <String , dynamic > json) =>
0 commit comments