11import 'package:json_annotation/json_annotation.dart' ;
22
3+ import '../../model/algorithms.dart' ;
4+ import '../route/messages.dart' ;
35import 'events.dart' ;
46import 'initial_snapshot.dart' ;
57import 'reaction.dart' ;
@@ -531,10 +533,68 @@ String? tryParseEmojiCodeToUnicode(String emojiCode) {
531533 }
532534}
533535
536+ /// As in [MessageBase.recipient] .
537+ ///
538+ /// Different from [MessageDestination] , this information comes from
539+ /// [getMessages] or [getEvents] , identifying the conversation that contains a
540+ /// message.
541+ sealed class Recipient {}
542+
543+ /// The recipient of a stream message.
544+ @JsonSerializable (fieldRename: FieldRename .snake, createToJson: false )
545+ class StreamRecipient extends Recipient {
546+ StreamRecipient (this .streamId, this .topic);
547+
548+ int streamId;
549+
550+ @JsonKey (name: 'subject' )
551+ TopicName topic;
552+
553+ factory StreamRecipient .fromJson (Map <String , dynamic > json) =>
554+ _$StreamRecipientFromJson (json);
555+ }
556+
557+ /// The recipient of a DM message.
558+ class DmRecipient extends Recipient {
559+ DmRecipient ({required this .allRecipientIds})
560+ : assert (isSortedWithoutDuplicates (allRecipientIds.toList ()));
561+
562+ /// The user IDs of all users in the thread, sorted numerically.
563+ ///
564+ /// This lists the sender as well as all (other) recipients, and it
565+ /// lists each user just once. In particular the self-user is always
566+ /// included.
567+ ///
568+ /// This is required to have an efficient `length` .
569+ final List <int > allRecipientIds;
570+ }
571+
572+ /// A message or message-like object, for showing in a message list.
573+ ///
574+ /// Other than [Message] , we use this for "outbox messages",
575+ /// representing outstanding [sendMessage] requests.
576+ abstract class MessageBase <T extends Recipient > {
577+ /// The Zulip message ID.
578+ ///
579+ /// If null, the message doesn't have an ID acknowledged by the server
580+ /// (e.g.: a locally-echoed message).
581+ int ? get id;
582+
583+ int get senderId;
584+ int get timestamp;
585+
586+ /// The recipient of this message.
587+ // When implementing this, the return type should be either [StreamRecipient]
588+ // or [DmRecipient]; it should never be [Recipient], because we
589+ // expect a concrete subclass of [MessageBase] to represent either
590+ // a channel message or a DM message, not both.
591+ T get recipient;
592+ }
593+
534594/// As in the get-messages response.
535595///
536596/// https://zulip.com/api/get-messages#response
537- sealed class Message {
597+ sealed class Message < T extends Recipient > implements MessageBase < T > {
538598 // final String? avatarUrl; // Use [User.avatarUrl] instead; will live-update
539599 final String client;
540600 String content;
@@ -544,6 +604,7 @@ sealed class Message {
544604 @JsonKey (readValue: MessageEditState ._readFromMessage, fromJson: Message ._messageEditStateFromJson)
545605 MessageEditState editState;
546606
607+ @override
547608 final int id;
548609 bool isMeMessage;
549610 int ? lastEditTimestamp;
@@ -554,13 +615,15 @@ sealed class Message {
554615 final int recipientId;
555616 final String senderEmail;
556617 final String senderFullName;
618+ @override
557619 final int senderId;
558620 final String senderRealmStr;
559621
560622 /// Poll data if "submessages" describe a poll, `null` otherwise.
561623 @JsonKey (name: 'submessages' , readValue: _readPoll, fromJson: Poll .fromJson, toJson: Poll .toJson)
562624 Poll ? poll;
563625
626+ @override
564627 final int timestamp;
565628 String get type;
566629
@@ -619,6 +682,8 @@ sealed class Message {
619682 required this .matchTopic,
620683 });
621684
685+ // TODO(dart): This has to be a static method, because factories/constructors
686+ // do not support type parameters: https://github.com/dart-lang/language/issues/647
622687 static Message fromJson (Map <String , dynamic > json) {
623688 final type = json['type' ] as String ;
624689 if (type == 'stream' ) return StreamMessage .fromJson (json);
@@ -715,7 +780,7 @@ extension type const TopicName(String _value) {
715780}
716781
717782@JsonSerializable (fieldRename: FieldRename .snake)
718- class StreamMessage extends Message {
783+ class StreamMessage extends Message < StreamRecipient > {
719784 @override
720785 @JsonKey (includeToJson: true )
721786 String get type => 'stream' ;
@@ -726,14 +791,23 @@ class StreamMessage extends Message {
726791 @JsonKey (required : true , disallowNullValue: true )
727792 String ? displayRecipient;
728793
729- int streamId;
794+ @JsonKey (includeToJson: true )
795+ int get streamId => recipient.streamId;
730796
731797 // The topic/subject is documented to be present on DMs too, just empty.
732798 // We ignore it on DMs; if a future server introduces distinct topics in DMs,
733799 // that will need new UI that we'll design then as part of that feature,
734800 // and ignoring the topics seems as good a fallback behavior as any.
735- @JsonKey (name: 'subject' )
736- TopicName topic;
801+ @JsonKey (name: 'subject' , includeToJson: true )
802+ TopicName get topic => recipient.topic;
803+
804+ @override
805+ @JsonKey (readValue: _readRecipient, includeToJson: false )
806+ StreamRecipient recipient;
807+
808+ static Map <String , dynamic > _readRecipient (Map <dynamic , dynamic > json, String key) {
809+ return {'stream_id' : json['stream_id' ], 'subject' : json['subject' ]};
810+ }
737811
738812 StreamMessage ({
739813 required super .client,
@@ -754,8 +828,7 @@ class StreamMessage extends Message {
754828 required super .matchContent,
755829 required super .matchTopic,
756830 required this .displayRecipient,
757- required this .streamId,
758- required this .topic,
831+ required this .recipient,
759832 });
760833
761834 factory StreamMessage .fromJson (Map <String , dynamic > json) =>
@@ -766,7 +839,7 @@ class StreamMessage extends Message {
766839}
767840
768841@JsonSerializable (fieldRename: FieldRename .snake)
769- class DmMessage extends Message {
842+ class DmMessage extends Message < DmRecipient > {
770843 @override
771844 @JsonKey (includeToJson: true )
772845 String get type => 'private' ;
@@ -781,20 +854,24 @@ class DmMessage extends Message {
781854 /// included.
782855 // TODO(server): Document that it's all users. That statement is based on
783856 // 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;
857+ @JsonKey (name: 'display_recipient' , toJson : _allRecipientIdsToJson, includeToJson : true )
858+ List <int > get allRecipientIds => recipient. allRecipientIds;
786859
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- }
860+ @override
861+ @JsonKey (name: 'display_recipient' , fromJson: _recipientFromJson, includeToJson: false )
862+ final DmRecipient recipient;
793863
794864 static List <Map <String , dynamic >> _allRecipientIdsToJson (List <int > allRecipientIds) {
795865 return allRecipientIds.map ((element) => {'id' : element}).toList ();
796866 }
797867
868+ static DmRecipient _recipientFromJson (List <dynamic > json) {
869+ return DmRecipient (allRecipientIds: json.map (
870+ (element) => ((element as Map <String , dynamic >)['id' ] as num ).toInt ()
871+ ).toList (growable: false )
872+ ..sort ());
873+ }
874+
798875 DmMessage ({
799876 required super .client,
800877 required super .content,
@@ -813,7 +890,7 @@ class DmMessage extends Message {
813890 required super .flags,
814891 required super .matchContent,
815892 required super .matchTopic,
816- required this .allRecipientIds ,
893+ required this .recipient ,
817894 });
818895
819896 factory DmMessage .fromJson (Map <String , dynamic > json) =>
0 commit comments