Skip to content

Commit 2d25c79

Browse files
committed
api [nfc]: Pull out Conversation
This will help support outbox messages in MessageListView later. We extracted Conversation instead of using MessageDestination because they are foundamentally different. Conversation is the identifier for the conversation that contains the message from, for example, get-messages or message events, but MessageDestination is specifically for send-message.
1 parent 87588b0 commit 2d25c79

File tree

5 files changed

+144
-78
lines changed

5 files changed

+144
-78
lines changed

lib/api/model/model.dart

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:json_annotation/json_annotation.dart';
22

3+
import '../../model/algorithms.dart';
34
import 'events.dart';
45
import 'initial_snapshot.dart';
56
import 'reaction.dart';
@@ -531,6 +532,52 @@ String? tryParseEmojiCodeToUnicode(String emojiCode) {
531532
}
532533
}
533534

535+
/// As in [StreamMessage.conversation] and [DmMessage.conversation].
536+
///
537+
/// Different from [MessageDestination], this information comes from
538+
/// [getMessages] or [getEvents], identifying the conversation that contains a
539+
/// message.
540+
sealed class Conversation {}
541+
542+
/// The conversation a stream message is in.
543+
@JsonSerializable(fieldRename: FieldRename.snake, createToJson: false)
544+
class StreamConversation extends Conversation {
545+
int streamId;
546+
547+
@JsonKey(name: 'subject')
548+
TopicName topic;
549+
550+
/// The name of the channel with ID [streamId] when the message was sent.
551+
///
552+
/// The primary reference for the name of the channel is
553+
/// the client's data structures about channels, at [streamId].
554+
/// This value may be used as a fallback when the channel is unknown.
555+
///
556+
/// This is non-null when found in a [StreamMessage] object in the API,
557+
/// but may become null in the client's data structures,
558+
/// e.g. if the message gets moved between channels.
559+
@JsonKey(required: true, disallowNullValue: true)
560+
String? displayRecipient;
561+
562+
StreamConversation(this.streamId, this.topic, {required this.displayRecipient});
563+
564+
factory StreamConversation.fromJson(Map<String, dynamic> json) =>
565+
_$StreamConversationFromJson(json);
566+
}
567+
568+
/// The conversation a DM message is in.
569+
class DmConversation extends Conversation {
570+
/// The user IDs of all users in the conversation, sorted numerically.
571+
///
572+
/// This lists the sender as well as all (other) recipients, and it
573+
/// lists each user just once. In particular the self-user is always
574+
/// included.
575+
final List<int> allRecipientIds;
576+
577+
DmConversation({required this.allRecipientIds})
578+
: assert(isSortedWithoutDuplicates(allRecipientIds.toList()));
579+
}
580+
534581
/// As in the get-messages response.
535582
///
536583
/// https://zulip.com/api/get-messages#response
@@ -720,20 +767,25 @@ class StreamMessage extends Message {
720767
@JsonKey(includeToJson: true)
721768
String get type => 'stream';
722769

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;
728-
729-
int streamId;
770+
@JsonKey(includeToJson: true)
771+
int get streamId => conversation.streamId;
730772

731773
// The topic/subject is documented to be present on DMs too, just empty.
732774
// We ignore it on DMs; if a future server introduces distinct topics in DMs,
733775
// that will need new UI that we'll design then as part of that feature,
734776
// and ignoring the topics seems as good a fallback behavior as any.
735-
@JsonKey(name: 'subject')
736-
TopicName topic;
777+
@JsonKey(name: 'subject', includeToJson: true)
778+
TopicName get topic => conversation.topic;
779+
780+
@JsonKey(includeToJson: true)
781+
String? get displayRecipient => conversation.displayRecipient;
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+
}
737789

738790
StreamMessage({
739791
required super.client,
@@ -753,9 +805,7 @@ class StreamMessage extends Message {
753805
required super.flags,
754806
required super.matchContent,
755807
required super.matchTopic,
756-
required this.displayRecipient,
757-
required this.streamId,
758-
required this.topic,
808+
required this.conversation,
759809
});
760810

761811
factory StreamMessage.fromJson(Map<String, dynamic> json) =>
@@ -781,20 +831,23 @@ class DmMessage extends Message {
781831
/// included.
782832
// TODO(server): Document that it's all users. That statement is based on
783833
// 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;
786836

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;
793839

794840
static List<Map<String, dynamic>> _allRecipientIdsToJson(List<int> allRecipientIds) {
795841
return allRecipientIds.map((element) => {'id': element}).toList();
796842
}
797843

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+
798851
DmMessage({
799852
required super.client,
800853
required super.content,
@@ -813,7 +866,7 @@ class DmMessage extends Message {
813866
required super.flags,
814867
required super.matchContent,
815868
required super.matchTopic,
816-
required this.allRecipientIds,
869+
required this.conversation,
817870
});
818871

819872
factory DmMessage.fromJson(Map<String, dynamic> json) =>

lib/api/model/model.g.dart

Lines changed: 59 additions & 52 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/message.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,14 @@ class MessageStoreImpl extends PerAccountStoreBase with MessageStore {
211211
}
212212

213213
if (newStreamId != origStreamId) {
214-
message.streamId = newStreamId;
215-
// See [StreamMessage.displayRecipient] on why the invalidation is
214+
message.conversation.streamId = newStreamId;
215+
// See [StreamConversation.displayRecipient] on why the invalidation is
216216
// needed.
217-
message.displayRecipient = null;
217+
message.conversation.displayRecipient = null;
218218
}
219219

220220
if (newTopic != origTopic) {
221-
message.topic = newTopic;
221+
message.conversation.topic = newTopic;
222222
}
223223

224224
if (!wasResolveOrUnresolve

lib/model/narrow.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ class DmNarrow extends Narrow implements SendableNarrow {
235235
/// See also:
236236
/// * [otherRecipientIds], an alternate way of identifying the conversation.
237237
/// * [DmMessage.allRecipientIds], which provides this same format.
238+
/// * [DmConversation.allRecipientIds], which also provides this same format.
238239
final List<int> allRecipientIds;
239240

240241
/// The user ID of the self-user.

test/api/model/model_checks.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ extension UserChecks on Subject<User> {
2424
extension ZulipStreamChecks on Subject<ZulipStream> {
2525
}
2626

27+
extension StreamConversationChecks on Subject<StreamConversation> {
28+
Subject<String?> get displayRecipient => has((x) => x.displayRecipient, 'displayRecipient');
29+
}
30+
2731
extension MessageChecks on Subject<Message> {
2832
Subject<String> get client => has((e) => e.client, 'client');
2933
Subject<String> get content => has((e) => e.content, 'content');
@@ -52,9 +56,10 @@ extension TopicNameChecks on Subject<TopicName> {
5256
}
5357

5458
extension StreamMessageChecks on Subject<StreamMessage> {
55-
Subject<String?> get displayRecipient => has((e) => e.displayRecipient, 'displayRecipient');
5659
Subject<int> get streamId => has((e) => e.streamId, 'streamId');
5760
Subject<TopicName> get topic => has((e) => e.topic, 'topic');
61+
Subject<String?> get displayRecipient => has((e) => e.displayRecipient, 'displayRecipient');
62+
Subject<StreamConversation> get conversation => has((e) => e.conversation, 'conversation');
5863
}
5964

6065
extension ReactionsChecks on Subject<Reactions> {

0 commit comments

Comments
 (0)