diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index 5c93c553b4..f8062cc343 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -584,8 +584,8 @@ class MessageEvent extends Event { // // The other difference in the server API between message objects in these // events and in the get-messages results is that `matchContent` and - // `matchSubject` are absent here. Already [Message.matchContent] and - // [Message.matchSubject] are optional, so no action is needed on that. + // `matchTopic` are absent here. Already [Message.matchContent] and + // [Message.matchTopic] are optional, so no action is needed on that. final Message message; MessageEvent({required super.id, required this.message}); @@ -617,20 +617,31 @@ class UpdateMessageEvent extends Event { final bool? renderingOnly; // TODO(server-5) final int messageId; final List messageIds; + final List flags; final int? editTimestamp; // TODO(server-5) - final String? streamName; - final int? streamId; + + // final String? streamName; // ignore + + @JsonKey(name: 'stream_id') + final int? origStreamId; final int? newStreamId; + final PropagateMode? propagateMode; - final String? origSubject; - final String? subject; + + @JsonKey(name: 'orig_subject') + final String? origTopic; + @JsonKey(name: 'subject') + final String? newTopic; + // final List topicLinks; // TODO handle + final String? origContent; final String? origRenderedContent; // final int? prevRenderedContentVersion; // deprecated final String? content; final String? renderedContent; + final bool? isMeMessage; UpdateMessageEvent({ @@ -641,12 +652,11 @@ class UpdateMessageEvent extends Event { required this.messageIds, required this.flags, required this.editTimestamp, - required this.streamName, - required this.streamId, + required this.origStreamId, required this.newStreamId, required this.propagateMode, - required this.origSubject, - required this.subject, + required this.origTopic, + required this.newTopic, required this.origContent, required this.origRenderedContent, required this.content, diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart index d20ff78d48..e31846d7fb 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -363,13 +363,12 @@ UpdateMessageEvent _$UpdateMessageEventFromJson(Map json) => .map((e) => $enumDecode(_$MessageFlagEnumMap, e)) .toList(), editTimestamp: (json['edit_timestamp'] as num?)?.toInt(), - streamName: json['stream_name'] as String?, - streamId: (json['stream_id'] as num?)?.toInt(), + origStreamId: (json['stream_id'] as num?)?.toInt(), newStreamId: (json['new_stream_id'] as num?)?.toInt(), propagateMode: $enumDecodeNullable(_$PropagateModeEnumMap, json['propagate_mode']), - origSubject: json['orig_subject'] as String?, - subject: json['subject'] as String?, + origTopic: json['orig_subject'] as String?, + newTopic: json['subject'] as String?, origContent: json['orig_content'] as String?, origRenderedContent: json['orig_rendered_content'] as String?, content: json['content'] as String?, @@ -386,12 +385,11 @@ Map _$UpdateMessageEventToJson(UpdateMessageEvent instance) => 'message_ids': instance.messageIds, 'flags': instance.flags, 'edit_timestamp': instance.editTimestamp, - 'stream_name': instance.streamName, - 'stream_id': instance.streamId, + 'stream_id': instance.origStreamId, 'new_stream_id': instance.newStreamId, 'propagate_mode': _$PropagateModeEnumMap[instance.propagateMode], - 'orig_subject': instance.origSubject, - 'subject': instance.subject, + 'orig_subject': instance.origTopic, + 'subject': instance.newTopic, 'orig_content': instance.origContent, 'orig_rendered_content': instance.origRenderedContent, 'content': instance.content, diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index 3f0f8a377d..5b24b4dd7e 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -646,7 +646,8 @@ sealed class Message { final String senderFullName; final int senderId; final String senderRealmStr; - final String subject; // TODO call it "topic" internally; also similar others + @JsonKey(name: 'subject') + final String topic; // final List submessages; // TODO handle final int timestamp; String get type; @@ -656,7 +657,8 @@ sealed class Message { @JsonKey(fromJson: _flagsFromJson) List flags; // Unrecognized flags won't roundtrip through {to,from}Json. final String? matchContent; - final String? matchSubject; + @JsonKey(name: 'match_subject') + final String? matchTopic; static Reactions? _reactionsFromJson(dynamic json) { final list = (json as List); @@ -685,11 +687,11 @@ sealed class Message { required this.senderFullName, required this.senderId, required this.senderRealmStr, - required this.subject, + required this.topic, required this.timestamp, required this.flags, required this.matchContent, - required this.matchSubject, + required this.matchTopic, }); factory Message.fromJson(Map json) { @@ -750,11 +752,11 @@ class StreamMessage extends Message { required super.senderFullName, required super.senderId, required super.senderRealmStr, - required super.subject, + required super.topic, required super.timestamp, required super.flags, required super.matchContent, - required super.matchSubject, + required super.matchTopic, required this.displayRecipient, required this.streamId, }); @@ -852,11 +854,11 @@ class DmMessage extends Message { required super.senderFullName, required super.senderId, required super.senderRealmStr, - required super.subject, + required super.topic, required super.timestamp, required super.flags, required super.matchContent, - required super.matchSubject, + required super.matchTopic, required this.displayRecipient, }); diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index 44cd4adf38..b835a7f953 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -274,11 +274,11 @@ StreamMessage _$StreamMessageFromJson(Map json) => senderFullName: json['sender_full_name'] as String, senderId: (json['sender_id'] as num).toInt(), senderRealmStr: json['sender_realm_str'] as String, - subject: json['subject'] as String, + topic: json['subject'] as String, timestamp: (json['timestamp'] as num).toInt(), flags: Message._flagsFromJson(json['flags']), matchContent: json['match_content'] as String?, - matchSubject: json['match_subject'] as String?, + matchTopic: json['match_subject'] as String?, displayRecipient: json['display_recipient'] as String, streamId: (json['stream_id'] as num).toInt(), ); @@ -297,11 +297,11 @@ Map _$StreamMessageToJson(StreamMessage instance) => 'sender_full_name': instance.senderFullName, 'sender_id': instance.senderId, 'sender_realm_str': instance.senderRealmStr, - 'subject': instance.subject, + 'subject': instance.topic, 'timestamp': instance.timestamp, 'flags': instance.flags, 'match_content': instance.matchContent, - 'match_subject': instance.matchSubject, + 'match_subject': instance.matchTopic, 'type': instance.type, 'display_recipient': instance.displayRecipient, 'stream_id': instance.streamId, @@ -333,11 +333,11 @@ DmMessage _$DmMessageFromJson(Map json) => DmMessage( senderFullName: json['sender_full_name'] as String, senderId: (json['sender_id'] as num).toInt(), senderRealmStr: json['sender_realm_str'] as String, - subject: json['subject'] as String, + topic: json['subject'] as String, timestamp: (json['timestamp'] as num).toInt(), flags: Message._flagsFromJson(json['flags']), matchContent: json['match_content'] as String?, - matchSubject: json['match_subject'] as String?, + matchTopic: json['match_subject'] as String?, displayRecipient: const DmRecipientListConverter() .fromJson(json['display_recipient'] as List), ); @@ -355,11 +355,11 @@ Map _$DmMessageToJson(DmMessage instance) => { 'sender_full_name': instance.senderFullName, 'sender_id': instance.senderId, 'sender_realm_str': instance.senderRealmStr, - 'subject': instance.subject, + 'subject': instance.topic, 'timestamp': instance.timestamp, 'flags': instance.flags, 'match_content': instance.matchContent, - 'match_subject': instance.matchSubject, + 'match_subject': instance.matchTopic, 'type': instance.type, 'display_recipient': const DmRecipientListConverter().toJson(instance.displayRecipient), diff --git a/lib/model/message_list.dart b/lib/model/message_list.dart index 09c780f35e..b5a921bed6 100644 --- a/lib/model/message_list.dart +++ b/lib/model/message_list.dart @@ -244,7 +244,7 @@ mixin _MessageSequence { bool haveSameRecipient(Message prevMessage, Message message) { if (prevMessage is StreamMessage && message is StreamMessage) { if (prevMessage.streamId != message.streamId) return false; - if (prevMessage.subject != message.subject) return false; + if (prevMessage.topic != message.topic) return false; } else if (prevMessage is DmMessage && message is DmMessage) { if (!_equalIdSequences(prevMessage.allRecipientIds, message.allRecipientIds)) { return false; @@ -335,14 +335,14 @@ class MessageListView with ChangeNotifier, _MessageSequence { case CombinedFeedNarrow(): return switch (message) { StreamMessage() => - store.isTopicVisible(message.streamId, message.subject), + store.isTopicVisible(message.streamId, message.topic), DmMessage() => true, }; case StreamNarrow(:final streamId): assert(message is StreamMessage && message.streamId == streamId); if (message is! StreamMessage) return false; - return store.isTopicVisibleInStream(streamId, message.subject); + return store.isTopicVisibleInStream(streamId, message.topic); case TopicNarrow(): case DmNarrow(): diff --git a/lib/model/narrow.dart b/lib/model/narrow.dart index d68c876190..9c8cacd06d 100644 --- a/lib/model/narrow.dart +++ b/lib/model/narrow.dart @@ -95,7 +95,7 @@ class TopicNarrow extends Narrow implements SendableNarrow { const TopicNarrow(this.streamId, this.topic); factory TopicNarrow.ofMessage(StreamMessage message) { - return TopicNarrow(message.streamId, message.subject); + return TopicNarrow(message.streamId, message.topic); } final int streamId; @@ -104,7 +104,7 @@ class TopicNarrow extends Narrow implements SendableNarrow { @override bool containsMessage(Message message) { return (message is StreamMessage - && message.streamId == streamId && message.subject == topic); + && message.streamId == streamId && message.topic == topic); } @override diff --git a/lib/model/unreads.dart b/lib/model/unreads.dart index a355dcdc2b..c1c375c657 100644 --- a/lib/model/unreads.dart +++ b/lib/model/unreads.dart @@ -214,7 +214,7 @@ class Unreads extends ChangeNotifier { switch (message) { case StreamMessage(): - _addLastInStreamTopic(message.id, message.streamId, message.subject); + _addLastInStreamTopic(message.id, message.streamId, message.topic); case DmMessage(): final narrow = DmNarrow.ofMessage(message, selfUserId: selfUserId); _addLastInDm(message.id, narrow); diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart index f185f16d03..424d666016 100644 --- a/lib/widgets/action_sheet.dart +++ b/lib/widgets/action_sheet.dart @@ -300,7 +300,7 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton { && topicController.textNormalized == kNoTopicTopic && message is StreamMessage ) { - topicController.value = TextEditingValue(text: message.subject); + topicController.value = TextEditingValue(text: message.topic); } final tag = composeBoxController.contentController .registerQuoteAndReplyStart(PerAccountStoreWidget.of(messageListContext), diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 15cf168129..77af198785 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -657,7 +657,7 @@ class StreamMessageRecipientHeader extends StatelessWidget { // https://github.com/zulip/zulip-mobile/issues/5511 final store = PerAccountStoreWidget.of(context); - final topic = message.subject; + final topic = message.topic; final subscription = store.subscriptions[message.streamId]; final Color backgroundColor; diff --git a/test/api/model/events_checks.dart b/test/api/model/events_checks.dart index cffa2b4074..0f55f68931 100644 --- a/test/api/model/events_checks.dart +++ b/test/api/model/events_checks.dart @@ -27,6 +27,25 @@ extension MessageEventChecks on Subject { Subject get message => has((e) => e.message, 'message'); } +extension UpdateMessageEventChecks on Subject { + Subject get userId => has((e) => e.userId, 'userId'); + Subject get renderingOnly => has((e) => e.renderingOnly, 'renderingOnly'); + Subject get messageId => has((e) => e.messageId, 'messageId'); + Subject> get messageIds => has((e) => e.messageIds, 'messageIds'); + Subject> get flags => has((e) => e.flags, 'flags'); + Subject get editTimestamp => has((e) => e.editTimestamp, 'editTimestamp'); + Subject get origStreamId => has((e) => e.origStreamId, 'origStreamId'); + Subject get newStreamId => has((e) => e.newStreamId, 'newStreamId'); + Subject get propagateMode => has((e) => e.propagateMode, 'propagateMode'); + Subject get origTopic => has((e) => e.origTopic, 'origTopic'); + Subject get newTopic => has((e) => e.newTopic, 'newTopic'); + Subject get origContent => has((e) => e.origContent, 'origContent'); + Subject get origRenderedContent => has((e) => e.origRenderedContent, 'origRenderedContent'); + Subject get content => has((e) => e.content, 'content'); + Subject get renderedContent => has((e) => e.renderedContent, 'renderedContent'); + Subject get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage'); +} + extension HeartbeatEventChecks on Subject { // No properties not covered by Event. } diff --git a/test/api/model/events_test.dart b/test/api/model/events_test.dart index 7d5fceaa3c..e55a0cf17d 100644 --- a/test/api/model/events_test.dart +++ b/test/api/model/events_test.dart @@ -66,6 +66,39 @@ void main() { check(mkEvent([MessageFlag.read])).message.flags.deepEquals([MessageFlag.read]); }); + group('update_message', () { + final message = eg.streamMessage(); + final baseJson = { + 'id': 1, + 'type': 'update_message', + 'user_id': eg.selfUser.userId, + 'rendering_only': false, + 'message_id': message.id, + 'message_ids': [message.id], + 'flags': [], + 'edit_timestamp': 1718741351, + 'stream_id': eg.stream().streamId, + }; + + test('stream_id -> origStreamId', () { + check(Event.fromJson({ ...baseJson, + 'stream_id': 1, + 'new_stream_id': 2, + }) as UpdateMessageEvent) + ..origStreamId.equals(1) + ..newStreamId.equals(2); + }); + + test('orig_subject -> origTopic, subject -> newTopic', () { + check(Event.fromJson({ ...baseJson, + 'orig_subject': 'foo', + 'subject': 'bar', + }) as UpdateMessageEvent) + ..origTopic.equals('foo') + ..newTopic.equals('bar'); + }); + }); + test('delete_message: require streamId and topic for stream messages', () { check(() => DeleteMessageEvent.fromJson({ 'id': 1, diff --git a/test/api/model/model_checks.dart b/test/api/model/model_checks.dart index c5ffd1f0e1..d15bca341c 100644 --- a/test/api/model/model_checks.dart +++ b/test/api/model/model_checks.dart @@ -38,14 +38,24 @@ extension StreamColorSwatchChecks on Subject { } extension MessageChecks on Subject { - Subject get id => has((e) => e.id, 'id'); + Subject get client => has((e) => e.client, 'client'); Subject get content => has((e) => e.content, 'content'); + Subject get contentType => has((e) => e.contentType, 'contentType'); + Subject get id => has((e) => e.id, 'id'); Subject get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage'); Subject get lastEditTimestamp => has((e) => e.lastEditTimestamp, 'lastEditTimestamp'); Subject get reactions => has((e) => e.reactions, 'reactions'); + Subject get recipientId => has((e) => e.recipientId, 'recipientId'); + Subject get senderEmail => has((e) => e.senderEmail, 'senderEmail'); + Subject get senderFullName => has((e) => e.senderFullName, 'senderFullName'); + Subject get senderId => has((e) => e.senderId, 'senderId'); + Subject get senderRealmStr => has((e) => e.senderRealmStr, 'senderRealmStr'); + Subject get topic => has((e) => e.topic, 'topic'); + Subject get timestamp => has((e) => e.timestamp, 'timestamp'); + Subject get type => has((e) => e.type, 'type'); Subject> get flags => has((e) => e.flags, 'flags'); - - // TODO accessors for other fields + Subject get matchContent => has((e) => e.matchContent, 'matchContent'); + Subject get matchTopic => has((e) => e.matchTopic, 'matchTopic'); } extension ReactionsChecks on Subject { diff --git a/test/api/model/model_test.dart b/test/api/model/model_test.dart index dc215e71f7..537b8988b4 100644 --- a/test/api/model/model_test.dart +++ b/test/api/model/model_test.dart @@ -551,6 +551,23 @@ void main() { }); group('Message', () { + Map baseStreamJson() => + deepToJson(eg.streamMessage()) as Map; + + test('subject -> topic', () { + check(baseStreamJson()).not((it) => it.containsKey('topic')); + check(Message.fromJson(baseStreamJson() + ..['subject'] = 'hello' + )).topic.equals('hello'); + }); + + test('match_subject -> matchTopic', () { + check(baseStreamJson()).not((it) => it.containsKey('match_topic')); + check(Message.fromJson(baseStreamJson() + ..['match_subject'] = 'yo' + )).matchTopic.equals('yo'); + }); + test('no crash on unrecognized flag', () { final m1 = Message.fromJson( (deepToJson(eg.streamMessage()) as Map) diff --git a/test/example_data.dart b/test/example_data.dart index 67ba1aaa3c..36fc60fec0 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -395,7 +395,6 @@ UpdateMessageEvent updateMessageEditEvent( int? messageId, List? flags, int? editTimestamp, - String? streamName, String? renderedContent, bool isMeMessage = false, }) { @@ -408,12 +407,11 @@ UpdateMessageEvent updateMessageEditEvent( messageIds: [messageId], flags: flags ?? origMessage.flags, editTimestamp: editTimestamp ?? 1234567890, // TODO generate timestamp - streamName: streamName, - streamId: origMessage is StreamMessage ? origMessage.streamId : null, + origStreamId: origMessage is StreamMessage ? origMessage.streamId : null, newStreamId: null, propagateMode: null, - origSubject: null, - subject: null, + origTopic: null, + newTopic: null, origContent: 'some probably-mismatched old Markdown', origRenderedContent: origMessage.content, content: 'some probably-mismatched new Markdown', @@ -441,7 +439,7 @@ UpdateMessageFlagsRemoveEvent updateMessageFlagsRemoveEvent( type: MessageType.stream, mentioned: mentioned, streamId: message.streamId, - topic: message.subject, + topic: message.topic, userIds: null, ), DmMessage() => UpdateMessageFlagsMessageDetail( diff --git a/test/model/message_list_test.dart b/test/model/message_list_test.dart index c818970c30..de0c7f32a7 100644 --- a/test/model/message_list_test.dart +++ b/test/model/message_list_test.dart @@ -873,10 +873,10 @@ void checkInvariants(MessageListView model) { if (message is! StreamMessage) continue; switch (model.narrow) { case CombinedFeedNarrow(): - check(model.store.isTopicVisible(message.streamId, message.subject)) + check(model.store.isTopicVisible(message.streamId, message.topic)) .isTrue(); case StreamNarrow(): - check(model.store.isTopicVisibleInStream(message.streamId, message.subject)) + check(model.store.isTopicVisibleInStream(message.streamId, message.topic)) .isTrue(); case TopicNarrow(): case DmNarrow(): diff --git a/test/model/narrow_test.dart b/test/model/narrow_test.dart index 5d671173b9..af75b3e6c2 100644 --- a/test/model/narrow_test.dart +++ b/test/model/narrow_test.dart @@ -27,7 +27,7 @@ void main() { final stream = eg.stream(); final message = eg.streamMessage(stream: stream); final actual = TopicNarrow.ofMessage(message); - check(actual).equals(TopicNarrow(stream.streamId, message.subject)); + check(actual).equals(TopicNarrow(stream.streamId, message.topic)); }); }); diff --git a/test/model/unreads_test.dart b/test/model/unreads_test.dart index e971d2b740..e72b3cbca4 100644 --- a/test/model/unreads_test.dart +++ b/test/model/unreads_test.dart @@ -68,7 +68,7 @@ void main() { switch (message) { case StreamMessage(): final perTopic = expectedStreams[message.streamId] ??= {}; - final messageIds = perTopic[message.subject] ??= QueueList(); + final messageIds = perTopic[message.topic] ??= QueueList(); messageIds.add(message.id); case DmMessage(): final narrow = DmNarrow.ofMessage(message, selfUserId: eg.selfUser.userId); @@ -445,7 +445,7 @@ void main() { messageType: MessageType.stream, messageIds: [message.id], streamId: message.streamId, - topic: message.subject, + topic: message.topic, ), DmMessage() => DeleteMessageEvent( id: 0, @@ -506,7 +506,7 @@ void main() { messageIds: [message.id], messageType: MessageType.stream, streamId: message.streamId, - topic: message.subject, + topic: message.topic, )); // TODO improve implementation; then: // checkNotNotified(); diff --git a/test/notifications/display_test.dart b/test/notifications/display_test.dart index 5c352123d3..b1a7183b87 100644 --- a/test/notifications/display_test.dart +++ b/test/notifications/display_test.dart @@ -181,8 +181,8 @@ void main() { final stream = eg.stream(); final message = eg.streamMessage(stream: stream); await checkNotifications(async, messageFcmMessage(message, streamName: stream.name), - expectedTitle: '#${stream.name} > ${message.subject}', - expectedTagComponent: 'stream:${message.streamId}:${message.subject}'); + expectedTitle: '#${stream.name} > ${message.topic}', + expectedTagComponent: 'stream:${message.streamId}:${message.topic}'); })); test('stream message, stream name omitted', () => awaitFakeAsync((async) async { @@ -190,8 +190,8 @@ void main() { final stream = eg.stream(); final message = eg.streamMessage(stream: stream); await checkNotifications(async, messageFcmMessage(message, streamName: null), - expectedTitle: '#(unknown channel) > ${message.subject}', - expectedTagComponent: 'stream:${message.streamId}:${message.subject}'); + expectedTitle: '#(unknown channel) > ${message.topic}', + expectedTagComponent: 'stream:${message.streamId}:${message.topic}'); })); test('group DM: 3 users', () => awaitFakeAsync((async) async { diff --git a/test/widgets/message_list_test.dart b/test/widgets/message_list_test.dart index 87054b8250..d54b7c1cd5 100644 --- a/test/widgets/message_list_test.dart +++ b/test/widgets/message_list_test.dart @@ -705,7 +705,7 @@ void main() { testWidgets('from unread to read', (WidgetTester tester) async { final message = eg.streamMessage(flags: []); final unreadMsgs = eg.unreadMsgs(streams:[ - UnreadStreamSnapshot(topic: message.subject, streamId: message.streamId, unreadMessageIds: [message.id]) + UnreadStreamSnapshot(topic: message.topic, streamId: message.streamId, unreadMessageIds: [message.id]) ]); await setupMessageListPage(tester, messages: [message], unreadMsgs: unreadMsgs); check(isMarkAsReadButtonVisible(tester)).isTrue(); @@ -723,7 +723,7 @@ void main() { testWidgets("messages don't shift position", (WidgetTester tester) async { final message = eg.streamMessage(flags: []); final unreadMsgs = eg.unreadMsgs(streams:[ - UnreadStreamSnapshot(topic: message.subject, streamId: message.streamId, + UnreadStreamSnapshot(topic: message.topic, streamId: message.streamId, unreadMessageIds: [message.id]) ]); await setupMessageListPage(tester, @@ -748,7 +748,7 @@ void main() { group('onPressed behavior', () { final message = eg.streamMessage(flags: []); final unreadMsgs = eg.unreadMsgs(streams: [ - UnreadStreamSnapshot(streamId: message.streamId, topic: message.subject, + UnreadStreamSnapshot(streamId: message.streamId, topic: message.topic, unreadMessageIds: [message.id]), ]);