Skip to content

Commit 499bbfc

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 9bc5bb4 commit 499bbfc

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
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
@@ -230,6 +230,12 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
230230
super.initState();
231231
}
232232

233+
void _narrowChanged(Narrow newNarrow) {
234+
setState(() {
235+
narrow = newNarrow;
236+
});
237+
}
238+
233239
@override
234240
Widget build(BuildContext context) {
235241
final store = PerAccountStoreWidget.of(context);
@@ -288,7 +294,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
288294
removeBottom: narrow is! CombinedFeedNarrow,
289295

290296
child: Expanded(
291-
child: MessageList(narrow: narrow))),
297+
child: MessageList(narrow: narrow, onNarrowChanged: _narrowChanged))),
292298
ComposeBox(controllerKey: _composeBoxKey, narrow: narrow),
293299
]))));
294300
}
@@ -364,9 +370,10 @@ const _kShortMessageHeight = 80;
364370
const kFetchMessagesBufferPixels = (kMessageListFetchBatchSize / 2) * _kShortMessageHeight;
365371

366372
class MessageList extends StatefulWidget {
367-
const MessageList({super.key, required this.narrow});
373+
const MessageList({super.key, required this.narrow, required this.onNarrowChanged});
368374

369375
final Narrow narrow;
376+
final void Function(Narrow newNarrow) onNarrowChanged;
370377

371378
@override
372379
State<StatefulWidget> createState() => _MessageListState();
@@ -403,6 +410,11 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
403410
}
404411

405412
void _modelChanged() {
413+
if (model!.narrow != widget.narrow) {
414+
// A message move event occurred, where propagate mode is
415+
// [PropagateMode.changeAll] or [PropagateMode.changeLater].
416+
widget.onNarrowChanged(model!.narrow);
417+
}
406418
setState(() {
407419
// The actual state lives in the [MessageListView] model.
408420
// 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
@@ -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

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 channel 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(ChannelNarrow(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';
@@ -1073,4 +1074,84 @@ void main() {
10731074
});
10741075
});
10751076
});
1077+
1078+
group('Update Narrow on message move', () {
1079+
final channel = eg.stream(name: 'move test stream');
1080+
final message = eg.streamMessage(stream: channel, content: 'Message to move');
1081+
final narrow = TopicNarrow.ofMessage(message);
1082+
1083+
void prepareGetMessageResponse(List<Message> messages) {
1084+
connection.prepare(json: newestResult(
1085+
foundOldest: false, messages: messages).toJson());
1086+
}
1087+
1088+
void handleMessageMoveEvent(List<StreamMessage> messages, String newTopic) {
1089+
store.handleEvent(eg.updateMessageEventMoveFrom(
1090+
origMessages: messages,
1091+
newTopic: newTopic,
1092+
propagateMode: PropagateMode.changeAll));
1093+
}
1094+
1095+
testWidgets('compose box send message after move', (WidgetTester tester) async {
1096+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1097+
1098+
final channelContentInputFinder = find.descendant(
1099+
of: find.byType(ComposeAutocomplete),
1100+
matching: find.byType(TextField));
1101+
1102+
await tester.enterText(channelContentInputFinder, 'Some text');
1103+
prepareGetMessageResponse([message]);
1104+
handleMessageMoveEvent([message], 'new topic');
1105+
await tester.pump(const Duration(seconds: 1));
1106+
check(tester.widget<TextField>(channelContentInputFinder))
1107+
..decoration.isNotNull().hintText.equals('Message #${channel.name} > new topic')
1108+
..controller.isNotNull().text.equals('Some text');
1109+
1110+
connection.prepare(json: SendMessageResult(id: 1).toJson());
1111+
await tester.tap(find.byIcon(Icons.send));
1112+
await tester.pump();
1113+
check(connection.lastRequest).isA<http.Request>()
1114+
..method.equals('POST')
1115+
..url.path.equals('/api/v1/messages')
1116+
..bodyFields.deepEquals({
1117+
'type': 'stream',
1118+
'to': '${message.streamId}',
1119+
'topic': 'new topic',
1120+
'content': 'Some text',
1121+
'read_by_sender': 'true'});
1122+
await tester.pumpAndSettle();
1123+
});
1124+
1125+
testWidgets('Move to narrow with existing messages', (WidgetTester tester) async {
1126+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1127+
check(find.textContaining('Existing message').evaluate()).length.equals(0);
1128+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
1129+
1130+
final existingMessage = eg.streamMessage(
1131+
stream: eg.stream(), topic: 'new topic', content: 'Existing message');
1132+
prepareGetMessageResponse([existingMessage, message]);
1133+
handleMessageMoveEvent([message], 'new topic');
1134+
await tester.pump(const Duration(seconds: 1));
1135+
1136+
check(find.textContaining('Existing message').evaluate()).length.equals(1);
1137+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
1138+
});
1139+
1140+
testWidgets('show new topic in TopicNarrow after move', (tester) async {
1141+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1142+
1143+
prepareGetMessageResponse([message]);
1144+
handleMessageMoveEvent([message], 'new topic');
1145+
await tester.pump(const Duration(seconds: 1));
1146+
1147+
check(find.descendant(
1148+
of: find.byType(RecipientHeader),
1149+
matching: find.text('new topic')).evaluate()
1150+
).length.equals(1);
1151+
check(find.descendant(
1152+
of: find.byType(MessageListAppBarTitle),
1153+
matching: find.text('${channel.name} > new topic')).evaluate()
1154+
).length.equals(1);
1155+
});
1156+
});
10761157
}

0 commit comments

Comments
 (0)