Skip to content

Commit b3ddf04

Browse files
committed
msglist: Update narrow when propageMode is changeAll or changeLater.
We only implement navigation change for the TopicNarrow. For StreamNarrow, propagateMode is practically unused. See also: zulip/zulip-mobile#5251 Fixes: zulip#150. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 69d63c5 commit b3ddf04

File tree

7 files changed

+190
-7
lines changed

7 files changed

+190
-7
lines changed

lib/model/message.dart

Lines changed: 7 additions & 0 deletions
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 && newTopic != 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

Lines changed: 24 additions & 4 deletions
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.
@@ -527,8 +527,26 @@ class MessageListView with ChangeNotifier, _MessageSequence {
527527
fetchInitial();
528528
}
529529

530-
void _messagesMovedFromNarrow(List<int> messageIds) {
530+
void _handlePropagateMode(PropagateMode propagateMode, Narrow newNarrow) {
531+
switch (propagateMode) {
532+
case PropagateMode.changeAll:
533+
case PropagateMode.changeLater:
534+
narrow = newNarrow;
535+
_reset();
536+
fetchInitial();
537+
case PropagateMode.changeOne:
538+
}
539+
}
540+
541+
void _messagesMovedFromNarrow(
542+
List<int> messageIds,
543+
PropagateMode propagateMode,
544+
{TopicNarrow? newNarrow}
545+
) {
531546
if (_removeMessagesById(messageIds)) {
547+
if (newNarrow != null) {
548+
_handlePropagateMode(propagateMode, newNarrow);
549+
}
532550
notifyListeners();
533551
}
534552
}
@@ -539,6 +557,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
539557
required String origTopic,
540558
required String newTopic,
541559
required List<int> messageIds,
560+
required PropagateMode propagateMode,
542561
}) {
543562
switch (narrow) {
544563
case DmNarrow():
@@ -558,7 +577,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
558577
case (false, false): return;
559578
case (true, true ): _messagesMovedInternally(messageIds);
560579
case (false, true ): _messagesMovedIntoNarrow();
561-
case (true, false): _messagesMovedFromNarrow(messageIds);
580+
case (true, false): _messagesMovedFromNarrow(messageIds, propagateMode);
562581
}
563582

564583
case TopicNarrow(:final streamId, :final topic):
@@ -568,7 +587,8 @@ class MessageListView with ChangeNotifier, _MessageSequence {
568587
case (false, false): return;
569588
case (true, true ): return; // TODO(log) no-op move
570589
case (false, true ): _messagesMovedIntoNarrow();
571-
case (true, false): _messagesMovedFromNarrow(messageIds); // TODO handle propagateMode
590+
case (true, false): _messagesMovedFromNarrow(messageIds, propagateMode,
591+
newNarrow: TopicNarrow(newStreamId, newTopic));
572592
}
573593
}
574594
}

lib/widgets/message_list.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
8181
super.initState();
8282
}
8383

84+
void _narrowChanged(Narrow newNarrow) {
85+
setState(() {
86+
narrow = newNarrow;
87+
});
88+
}
89+
8490
@override
8591
Widget build(BuildContext context) {
8692
final store = PerAccountStoreWidget.of(context);
@@ -138,7 +144,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
138144
removeBottom: narrow is! CombinedFeedNarrow,
139145

140146
child: Expanded(
141-
child: MessageList(narrow: narrow))),
147+
child: MessageList(narrow: narrow, onNarrowChanged: _narrowChanged))),
142148
ComposeBox(controllerKey: _composeBoxKey, narrow: narrow),
143149
]))));
144150
}
@@ -214,9 +220,10 @@ const _kShortMessageHeight = 80;
214220
const kFetchMessagesBufferPixels = (kMessageListFetchBatchSize / 2) * _kShortMessageHeight;
215221

216222
class MessageList extends StatefulWidget {
217-
const MessageList({super.key, required this.narrow});
223+
const MessageList({super.key, required this.narrow, required this.onNarrowChanged});
218224

219225
final Narrow narrow;
226+
final void Function(Narrow newNarrow) onNarrowChanged;
220227

221228
@override
222229
State<StatefulWidget> createState() => _MessageListState();
@@ -253,6 +260,11 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
253260
}
254261

255262
void _modelChanged() {
263+
if (model!.narrow != widget.narrow) {
264+
// A message move event occurred, where propagate mode is
265+
// [PropagateMode.changeAll] or [PropagateMode.changeLater].
266+
widget.onNarrowChanged(model!.narrow);
267+
}
256268
setState(() {
257269
// The actual state lives in the [MessageListView] model.
258270
// This method was called because that just changed.

test/example_data.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ UpdateMessageEvent _updateMessageMoveEvent(
471471
String? newTopic,
472472
String? origContent,
473473
String? newContent,
474+
PropagateMode propagateMode = PropagateMode.changeOne,
474475
}) {
475476
assert(messages.isNotEmpty);
476477
assert(newTopic != origTopic || newStreamId != null);
@@ -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
String? newTopic,
504505
int? newStreamId,
505506
String? newContent,
507+
PropagateMode propagateMode = PropagateMode.changeOne,
506508
}) {
507509
assert(origMessages.isNotEmpty);
508510
final origMessage = origMessages.first;
@@ -515,6 +517,7 @@ UpdateMessageEvent updateMessageEventMoveFrom({
515517
newTopic: newTopic,
516518
origContent: effectiveOrigContent,
517519
newContent: newContent,
520+
propagateMode: propagateMode,
518521
);
519522
}
520523

test/flutter_checks.dart

Lines changed: 10 additions & 0 deletions
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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,31 @@ void main() {
521521
checkHasMessages(initialMessages);
522522
checkNotifiedOnce();
523523
});
524+
525+
void testMessageMove(PropagateMode propagateMode) => awaitFakeAsync((async) async {
526+
await prepareNarrow(narrow, initialMessages + movedMessages);
527+
connection.prepare(delay: const Duration(seconds: 1), json: newestResult(
528+
foundOldest: false,
529+
messages: movedMessages,
530+
).toJson());
531+
await store.handleEvent(eg.updateMessageEventMoveFrom(
532+
origMessages: movedMessages,
533+
newTopic: 'new',
534+
newStreamId: otherStream.streamId,
535+
propagateMode: propagateMode,
536+
));
537+
checkNotifiedOnce();
538+
async.elapse(const Duration(seconds: 1));
539+
checkHasMessages(movedMessages);
540+
check(model).narrow.equals(TopicNarrow(otherStream.streamId, 'new'));
541+
checkNotifiedOnce();
542+
});
543+
544+
test('follow to the new narrow when propagateMode = changeLater', () =>
545+
testMessageMove(PropagateMode.changeLater));
546+
547+
test('follow to the new narrow when propagateMode = changeAll', () =>
548+
testMessageMove(PropagateMode.changeAll));
524549
});
525550

526551
group('in stream narrow', () {
@@ -570,6 +595,31 @@ void main() {
570595
checkHasMessages(initialMessages);
571596
checkNotifiedOnce();
572597
});
598+
599+
void testMessageMove(PropagateMode propagateMode) => awaitFakeAsync((async) async {
600+
await prepareNarrow(narrow, initialMessages + movedMessages);
601+
connection.prepare(delay: const Duration(seconds: 1), json: newestResult(
602+
foundOldest: false,
603+
messages: movedMessages,
604+
).toJson());
605+
await store.handleEvent(eg.updateMessageEventMoveFrom(
606+
origMessages: movedMessages,
607+
newTopic: 'new',
608+
newStreamId: otherStream.streamId,
609+
propagateMode: propagateMode,
610+
));
611+
checkNotifiedOnce();
612+
async.elapse(const Duration(seconds: 1));
613+
checkHasMessages(initialMessages);
614+
check(model).narrow.equals(StreamNarrow(stream.streamId));
615+
checkNotNotified();
616+
});
617+
618+
test('do not follow when propagateMode = changeLater', () =>
619+
testMessageMove(PropagateMode.changeLater));
620+
621+
test('do not follow when propagateMode = changeAll', () =>
622+
testMessageMove(PropagateMode.changeAll));
573623
});
574624

575625
group('in combined feed narrow', () {

test/widgets/message_list_test.dart

Lines changed: 81 additions & 0 deletions
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';
@@ -1032,4 +1033,84 @@ void main() {
10321033
});
10331034
});
10341035
});
1036+
1037+
group('Update Narrow on message move', () {
1038+
final channel = eg.stream(name: 'move test stream');
1039+
final message = eg.streamMessage(stream: channel, content: 'Message to move');
1040+
final narrow = TopicNarrow.ofMessage(message);
1041+
1042+
void prepareGetMessageResponse(List<Message> messages) {
1043+
connection.prepare(json: newestResult(
1044+
foundOldest: false, messages: messages).toJson());
1045+
}
1046+
1047+
void handleMessageMoveEvent(List<StreamMessage> messages, String newTopic) {
1048+
store.handleEvent(eg.updateMessageEventMoveFrom(
1049+
origMessages: messages,
1050+
newTopic: newTopic,
1051+
propagateMode: PropagateMode.changeAll));
1052+
}
1053+
1054+
testWidgets('compose box send message after move', (WidgetTester tester) async {
1055+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1056+
1057+
final channelContentInputFinder = find.descendant(
1058+
of: find.byType(ComposeAutocomplete),
1059+
matching: find.byType(TextField));
1060+
1061+
await tester.enterText(channelContentInputFinder, 'Some text');
1062+
prepareGetMessageResponse([message]);
1063+
handleMessageMoveEvent([message], 'new topic');
1064+
await tester.pump(const Duration(seconds: 1));
1065+
check(tester.widget<TextField>(channelContentInputFinder))
1066+
..decoration.isNotNull().hintText.equals('Message #${channel.name} > new topic')
1067+
..controller.isNotNull().text.equals('Some text');
1068+
1069+
connection.prepare(json: SendMessageResult(id: 1).toJson());
1070+
await tester.tap(find.byIcon(Icons.send));
1071+
await tester.pump();
1072+
check(connection.lastRequest).isA<http.Request>()
1073+
..method.equals('POST')
1074+
..url.path.equals('/api/v1/messages')
1075+
..bodyFields.deepEquals({
1076+
'type': 'stream',
1077+
'to': '${message.streamId}',
1078+
'topic': 'new topic',
1079+
'content': 'Some text',
1080+
'read_by_sender': 'true'});
1081+
await tester.pumpAndSettle();
1082+
});
1083+
1084+
testWidgets('Move to narrow with existing messages', (WidgetTester tester) async {
1085+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1086+
check(find.textContaining('Existing message').evaluate()).length.equals(0);
1087+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
1088+
1089+
final existingMessage = eg.streamMessage(
1090+
stream: eg.stream(), topic: 'new topic', content: 'Existing message');
1091+
prepareGetMessageResponse([existingMessage, message]);
1092+
handleMessageMoveEvent([message], 'new topic');
1093+
await tester.pump(const Duration(seconds: 1));
1094+
1095+
check(find.textContaining('Existing message').evaluate()).length.equals(1);
1096+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
1097+
});
1098+
1099+
testWidgets('show new topic in TopicNarrow after move', (tester) async {
1100+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1101+
1102+
prepareGetMessageResponse([message]);
1103+
handleMessageMoveEvent([message], 'new topic');
1104+
await tester.pump(const Duration(seconds: 1));
1105+
1106+
check(find.descendant(
1107+
of: find.byType(RecipientHeader),
1108+
matching: find.text('new topic')).evaluate()
1109+
).length.equals(1);
1110+
check(find.descendant(
1111+
of: find.byType(MessageListAppBarTitle),
1112+
matching: find.text('${channel.name} > new topic')).evaluate()
1113+
).length.equals(1);
1114+
});
1115+
});
10351116
}

0 commit comments

Comments
 (0)