Skip to content

Commit a763545

Browse files
committed
msglist: Update narrow when propagateMode is changeAll or changeLater.
Navigation change is only intended for TopicNarrow. For StreamNarrow, propagateMode is unused. See also: zulip/zulip-mobile#5251 Fixes: #150 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 2a49166 commit a763545

File tree

7 files changed

+209
-5
lines changed

7 files changed

+209
-5
lines changed

lib/model/message.dart

+7
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ class MessageStoreImpl with MessageStore {
152152
final newStreamId = event.newStreamId; // null if topic-only move
153153
final origTopic = event.origTopic;
154154
final newTopic = event.newTopic;
155+
final propagateMode = event.propagateMode;
155156

156157
if (origTopic == null) {
157158
// There was no move.
@@ -178,6 +179,11 @@ class MessageStoreImpl with MessageStore {
178179
assert(debugLog('Malformed UpdateMessageEvent: move but no origStreamId')); // TODO(log)
179180
return;
180181
}
182+
if (propagateMode == null) {
183+
// The `propagate_mode` field (aka propagateMode) is documented to be present on moves.
184+
assert(debugLog('Malformed UpdateMessageEvent: move but no propagateMode')); // TODO(log)
185+
return;
186+
}
181187

182188
final wasResolveOrUnresolve = (newStreamId == null
183189
&& MessageEditState.topicMoveWasResolveOrUnresolve(origTopic, newTopic!));
@@ -215,6 +221,7 @@ class MessageStoreImpl with MessageStore {
215221
origTopic: origTopic,
216222
newTopic: newTopic ?? origTopic,
217223
messageIds: event.messageIds,
224+
propagateMode: propagateMode,
218225
);
219226
}
220227
}

lib/model/message_list.dart

+16-2
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
359359
}
360360

361361
final PerAccountStore store;
362-
final Narrow narrow;
362+
Narrow narrow;
363363

364364
/// Whether [message] should actually appear in this message list,
365365
/// given that it does belong to the narrow.
@@ -529,6 +529,17 @@ class MessageListView with ChangeNotifier, _MessageSequence {
529529
fetchInitial();
530530
}
531531

532+
void _handlePropagateMode(PropagateMode propagateMode, Narrow newNarrow) {
533+
switch (propagateMode) {
534+
case PropagateMode.changeAll:
535+
case PropagateMode.changeLater:
536+
narrow = newNarrow;
537+
_reset();
538+
fetchInitial();
539+
case PropagateMode.changeOne:
540+
}
541+
}
542+
532543
void _messagesMovedFromNarrow(List<int> messageIds) {
533544
if (_removeMessagesById(messageIds)) {
534545
notifyListeners();
@@ -541,6 +552,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
541552
required String origTopic,
542553
required String newTopic,
543554
required List<int> messageIds,
555+
required PropagateMode propagateMode,
544556
}) {
545557
switch (narrow) {
546558
case DmNarrow():
@@ -571,7 +583,9 @@ class MessageListView with ChangeNotifier, _MessageSequence {
571583
case (false, false): return;
572584
case (true, true ): return; // TODO(log) no-op move
573585
case (false, true ): _messagesMovedIntoNarrow();
574-
case (true, false): _messagesMovedFromNarrow(messageIds); // TODO handle propagateMode
586+
case (true, false):
587+
_messagesMovedFromNarrow(messageIds);
588+
_handlePropagateMode(propagateMode, TopicNarrow(newStreamId, newTopic));
575589
}
576590
}
577591
}

lib/widgets/message_list.dart

+14-2
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
232232
narrow = widget.initNarrow;
233233
}
234234

235+
void _narrowChanged(Narrow newNarrow) {
236+
setState(() {
237+
narrow = newNarrow;
238+
});
239+
}
240+
235241
@override
236242
Widget build(BuildContext context) {
237243
final store = PerAccountStoreWidget.of(context);
@@ -289,7 +295,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
289295
removeBottom: ComposeBox.hasComposeBox(narrow),
290296

291297
child: Expanded(
292-
child: MessageList(narrow: narrow))),
298+
child: MessageList(narrow: narrow, onNarrowChanged: _narrowChanged))),
293299
ComposeBox(controllerKey: _composeBoxKey, narrow: narrow),
294300
]))));
295301
}
@@ -368,9 +374,10 @@ const _kShortMessageHeight = 80;
368374
const kFetchMessagesBufferPixels = (kMessageListFetchBatchSize / 2) * _kShortMessageHeight;
369375

370376
class MessageList extends StatefulWidget {
371-
const MessageList({super.key, required this.narrow});
377+
const MessageList({super.key, required this.narrow, required this.onNarrowChanged});
372378

373379
final Narrow narrow;
380+
final void Function(Narrow newNarrow) onNarrowChanged;
374381

375382
@override
376383
State<StatefulWidget> createState() => _MessageListState();
@@ -407,6 +414,11 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
407414
}
408415

409416
void _modelChanged() {
417+
if (model!.narrow != widget.narrow) {
418+
// A message move event occurred, where propagate mode is
419+
// [PropagateMode.changeAll] or [PropagateMode.changeLater].
420+
widget.onNarrowChanged(model!.narrow);
421+
}
410422
setState(() {
411423
// The actual state lives in the [MessageListView] model.
412424
// This method was called because that just changed.

test/example_data.dart

+4-1
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ UpdateMessageEvent _updateMessageMoveEvent(
472472
String? origContent,
473473
String? newContent,
474474
required List<MessageFlag> flags,
475+
PropagateMode propagateMode = PropagateMode.changeOne,
475476
}) {
476477
assert(newTopic != origTopic
477478
|| (newStreamId != null && newStreamId != origStreamId));
@@ -486,7 +487,7 @@ UpdateMessageEvent _updateMessageMoveEvent(
486487
editTimestamp: 1234567890, // TODO generate timestamp
487488
origStreamId: origStreamId,
488489
newStreamId: newStreamId,
489-
propagateMode: null,
490+
propagateMode: propagateMode,
490491
origTopic: origTopic,
491492
newTopic: newTopic,
492493
origContent: origContent,
@@ -503,6 +504,7 @@ UpdateMessageEvent updateMessageEventMoveFrom({
503504
int? newStreamId,
504505
String? newTopic,
505506
String? newContent,
507+
PropagateMode propagateMode = PropagateMode.changeOne,
506508
}) {
507509
assert(origMessages.isNotEmpty);
508510
final origMessage = origMessages.first;
@@ -516,6 +518,7 @@ UpdateMessageEvent updateMessageEventMoveFrom({
516518
origContent: origContent,
517519
newContent: newContent,
518520
flags: origMessage.flags,
521+
propagateMode: propagateMode,
519522
);
520523
}
521524

test/flutter_checks.dart

+10
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,14 @@ extension TextChecks on Subject<Text> {
6060
Subject<TextStyle?> get style => has((t) => t.style, 'style');
6161
}
6262

63+
extension TextEditingControllerChecks on Subject<TextEditingController> {
64+
Subject<String?> get text => has((t) => t.text, 'text');
65+
}
66+
6367
extension TextFieldChecks on Subject<TextField> {
6468
Subject<TextCapitalization?> get textCapitalization => has((t) => t.textCapitalization, 'textCapitalization');
69+
Subject<InputDecoration?> get decoration => has((t) => t.decoration, 'decoration');
70+
Subject<TextEditingController?> get controller => has((t) => t.controller, 'controller');
6571
}
6672

6773
extension TextStyleChecks on Subject<TextStyle> {
@@ -131,3 +137,7 @@ extension MaterialChecks on Subject<Material> {
131137
Subject<Color?> get color => has((x) => x.color, 'color');
132138
// TODO more
133139
}
140+
141+
extension InputDecorationChecks on Subject<InputDecoration> {
142+
Subject<String?> get hintText => has((x) => x.hintText, 'hintText');
143+
}

test/model/message_list_test.dart

+68
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,37 @@ void main() {
587587
checkHasMessages(initialMessages);
588588
checkNotNotified();
589589
});
590+
591+
void testMessageMove(PropagateMode propagateMode) => awaitFakeAsync((async) async {
592+
await prepareNarrow(narrow, initialMessages + movedMessages);
593+
connection.prepare(delay: const Duration(seconds: 1), json: newestResult(
594+
foundOldest: false,
595+
messages: movedMessages,
596+
).toJson());
597+
await store.handleEvent(eg.updateMessageEventMoveFrom(
598+
origMessages: movedMessages,
599+
newTopic: 'new',
600+
newStreamId: otherStream.streamId,
601+
propagateMode: propagateMode,
602+
));
603+
checkNotifiedOnce();
604+
async.elapse(const Duration(seconds: 1));
605+
checkHasMessages(initialMessages);
606+
check(model).narrow.equals(ChannelNarrow(stream.streamId));
607+
checkNotNotified();
608+
});
609+
610+
test('do not follow when propagateMode = changeOne', () {
611+
testMessageMove(PropagateMode.changeOne);
612+
});
613+
614+
test('do not follow when propagateMode = changeLater', () {
615+
testMessageMove(PropagateMode.changeLater);
616+
});
617+
618+
test('do not follow when propagateMode = changeAll', () {
619+
testMessageMove(PropagateMode.changeAll);
620+
});
590621
});
591622

592623
group('in topic narrow', () {
@@ -674,6 +705,43 @@ void main() {
674705
checkNotNotified();
675706
}));
676707
});
708+
709+
void handleMoveEvent(PropagateMode propagateMode) => awaitFakeAsync((async) async {
710+
await prepareNarrow(narrow, initialMessages + movedMessages);
711+
connection.prepare(delay: const Duration(seconds: 1), json: newestResult(
712+
foundOldest: false,
713+
messages: movedMessages,
714+
).toJson());
715+
await store.handleEvent(eg.updateMessageEventMoveFrom(
716+
origMessages: movedMessages,
717+
newTopic: 'new',
718+
newStreamId: otherStream.streamId,
719+
propagateMode: propagateMode,
720+
));
721+
checkNotifiedOnce();
722+
async.elapse(const Duration(seconds: 1));
723+
});
724+
725+
test('do not follow to the new narrow when propagateMode = changeOne', () {
726+
handleMoveEvent(PropagateMode.changeOne);
727+
checkNotNotified();
728+
checkHasMessages(initialMessages);
729+
check(model).narrow.equals(TopicNarrow(stream.streamId, 'topic'));
730+
});
731+
732+
test('follow to the new narrow when propagateMode = changeLater', () {
733+
handleMoveEvent(PropagateMode.changeLater);
734+
checkNotifiedOnce();
735+
checkHasMessages(movedMessages);
736+
check(model).narrow.equals(TopicNarrow(otherStream.streamId, 'new'));
737+
});
738+
739+
test('follow to the new narrow when propagateMode = changeAll', () {
740+
handleMoveEvent(PropagateMode.changeAll);
741+
checkNotifiedOnce();
742+
checkHasMessages(movedMessages);
743+
check(model).narrow.equals(TopicNarrow(otherStream.streamId, 'new'));
744+
});
677745
});
678746

679747
group('fetch races', () {

test/widgets/message_list_test.dart

+90
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:zulip/api/route/messages.dart';
1414
import 'package:zulip/model/localizations.dart';
1515
import 'package:zulip/model/narrow.dart';
1616
import 'package:zulip/model/store.dart';
17+
import 'package:zulip/widgets/autocomplete.dart';
1718
import 'package:zulip/widgets/content.dart';
1819
import 'package:zulip/widgets/emoji_reaction.dart';
1920
import 'package:zulip/widgets/icons.dart';
@@ -620,6 +621,95 @@ void main() {
620621
});
621622
});
622623

624+
group('Update Narrow on message move', () {
625+
final channel = eg.stream(name: 'move test stream');
626+
final otherChannel = eg.stream(name: 'other move test stream');
627+
628+
void prepareGetMessageResponse(List<Message> messages) {
629+
connection.prepare(json: newestResult(
630+
foundOldest: false, messages: messages).toJson());
631+
}
632+
633+
void handleMessageMoveEvent(List<StreamMessage> messages, String newTopic, {int? newChannelId}) {
634+
store.handleEvent(eg.updateMessageEventMoveFrom(
635+
origMessages: messages,
636+
newTopic: newTopic,
637+
newStreamId: newChannelId,
638+
propagateMode: PropagateMode.changeAll));
639+
}
640+
641+
testWidgets('compose box send message after move', (WidgetTester tester) async {
642+
final message = eg.streamMessage(stream: channel, content: 'Message to move');
643+
final narrow = TopicNarrow.ofMessage(message);
644+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel, otherChannel]);
645+
646+
final channelContentInputFinder = find.descendant(
647+
of: find.byType(ComposeAutocomplete),
648+
matching: find.byType(TextField));
649+
650+
await tester.enterText(channelContentInputFinder, 'Some text');
651+
check(tester.widget<TextField>(channelContentInputFinder))
652+
..decoration.isNotNull().hintText.equals('Message #${channel.name} > example topic')
653+
..controller.isNotNull().text.equals('Some text');
654+
prepareGetMessageResponse([message]);
655+
handleMessageMoveEvent([message], 'new topic', newChannelId: otherChannel.streamId);
656+
await tester.pump(const Duration(seconds: 1));
657+
check(tester.widget<TextField>(channelContentInputFinder))
658+
..decoration.isNotNull().hintText.equals('Message #${otherChannel.name} > new topic')
659+
..controller.isNotNull().text.equals('Some text');
660+
661+
connection.prepare(json: SendMessageResult(id: 1).toJson());
662+
await tester.tap(find.byIcon(Icons.send));
663+
await tester.pump();
664+
check(connection.lastRequest).isA<http.Request>()
665+
..method.equals('POST')
666+
..url.path.equals('/api/v1/messages')
667+
..bodyFields.deepEquals({
668+
'type': 'stream',
669+
'to': '${otherChannel.streamId}',
670+
'topic': 'new topic',
671+
'content': 'Some text',
672+
'read_by_sender': 'true'});
673+
await tester.pumpAndSettle();
674+
});
675+
676+
testWidgets('Move to narrow with existing messages', (WidgetTester tester) async {
677+
final message = eg.streamMessage(stream: channel, content: 'Message to move');
678+
final narrow = TopicNarrow.ofMessage(message);
679+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
680+
check(find.textContaining('Existing message').evaluate()).length.equals(0);
681+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
682+
683+
final existingMessage = eg.streamMessage(
684+
stream: eg.stream(), topic: 'new topic', content: 'Existing message');
685+
prepareGetMessageResponse([existingMessage, message]);
686+
handleMessageMoveEvent([message], 'new topic');
687+
await tester.pump(const Duration(seconds: 1));
688+
689+
check(find.textContaining('Existing message').evaluate()).length.equals(1);
690+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
691+
});
692+
693+
testWidgets('show new topic in TopicNarrow after move', (tester) async {
694+
final message = eg.streamMessage(stream: channel, content: 'Message to move');
695+
final narrow = TopicNarrow.ofMessage(message);
696+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
697+
698+
prepareGetMessageResponse([message]);
699+
handleMessageMoveEvent([message], 'new topic');
700+
await tester.pump(const Duration(seconds: 1));
701+
702+
check(find.descendant(
703+
of: find.byType(RecipientHeader),
704+
matching: find.text('new topic')).evaluate()
705+
).length.equals(1);
706+
check(find.descendant(
707+
of: find.byType(MessageListAppBarTitle),
708+
matching: find.text('${channel.name} > new topic')).evaluate()
709+
).length.equals(1);
710+
});
711+
});
712+
623713
group('recipient headers', () {
624714
group('StreamMessageRecipientHeader', () {
625715
final stream = eg.stream(name: 'stream name');

0 commit comments

Comments
 (0)