Skip to content

Commit 1b319ab

Browse files
committed
msglist: Monitor and handle narrow updates from model.
See also: zulip/zulip-mobile#5251 Fixes: #150. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 7821601 commit 1b319ab

File tree

3 files changed

+105
-2
lines changed

3 files changed

+105
-2
lines changed

lib/widgets/message_list.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ class _MessageListPageState extends State<MessageListPage> {
6363
super.initState();
6464
}
6565

66+
void _narrowChanged(Narrow newNarrow) {
67+
setState(() {
68+
narrow = newNarrow;
69+
});
70+
}
71+
6672
@override
6773
Widget build(BuildContext context) {
6874
final store = PerAccountStoreWidget.of(context);
@@ -120,7 +126,7 @@ class _MessageListPageState extends State<MessageListPage> {
120126
removeBottom: narrow is! CombinedFeedNarrow,
121127

122128
child: Expanded(
123-
child: MessageList(narrow: narrow!))),
129+
child: MessageList(narrow: narrow!, onNarrowChanged: _narrowChanged))),
124130
ComposeBox(controllerKey: _composeBoxKey, narrow: narrow!),
125131
]))));
126132
}
@@ -196,9 +202,10 @@ const _kShortMessageHeight = 80;
196202
const kFetchMessagesBufferPixels = (kMessageListFetchBatchSize / 2) * _kShortMessageHeight;
197203

198204
class MessageList extends StatefulWidget {
199-
const MessageList({super.key, required this.narrow});
205+
const MessageList({super.key, required this.narrow, required this.onNarrowChanged});
200206

201207
final Narrow narrow;
208+
final void Function(Narrow newNarrow) onNarrowChanged;
202209

203210
@override
204211
State<StatefulWidget> createState() => _MessageListState();
@@ -235,6 +242,11 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
235242
}
236243

237244
void _modelChanged() {
245+
if (model!.narrow != widget.narrow) {
246+
// A message move event occurred, where propagate mode is
247+
// [PropagateMode.changeAll] or [PropagateMode.changeLater].
248+
widget.onNarrowChanged(model!.narrow);
249+
}
238250
setState(() {
239251
// The actual state lives in the [MessageListView] model.
240252
// This method was called because that just changed.

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> {
@@ -111,3 +117,7 @@ extension TypographyChecks on Subject<Typography> {
111117
extension InlineSpanChecks on Subject<InlineSpan> {
112118
Subject<TextStyle?> get style => has((x) => x.style, 'style');
113119
}
120+
121+
extension InputDecorationChecks on Subject<InputDecoration> {
122+
Subject<String?> get hintText => has((x) => x.hintText, 'hintText');
123+
}

test/widgets/message_list_test.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:zulip/api/route/messages.dart';
1515
import 'package:zulip/model/localizations.dart';
1616
import 'package:zulip/model/narrow.dart';
1717
import 'package:zulip/model/store.dart';
18+
import 'package:zulip/widgets/autocomplete.dart';
1819
import 'package:zulip/widgets/content.dart';
1920
import 'package:zulip/widgets/icons.dart';
2021
import 'package:zulip/widgets/message_list.dart';
@@ -1009,4 +1010,84 @@ void main() {
10091010
});
10101011
});
10111012
});
1013+
1014+
group('Update Narrow on message move', () {
1015+
final channel = eg.stream(name: 'move test stream');
1016+
final message = eg.streamMessage(stream: channel, content: 'Message to move');
1017+
final narrow = TopicNarrow.ofMessage(message);
1018+
1019+
void prepareGetMessageResponse(List<Message> messages) {
1020+
connection.prepare(json: newestResult(
1021+
foundOldest: false, messages: messages).toJson());
1022+
}
1023+
1024+
void handleMessageMoveEvent(List<Message> messages, String newTopic) {
1025+
store.handleEvent(eg.updateMessageMoveEvent(messages,
1026+
origTopic: messages[0].topic,
1027+
newTopic: newTopic,
1028+
propagateMode: PropagateMode.changeAll));
1029+
}
1030+
1031+
testWidgets('compose box send message after move', (WidgetTester tester) async {
1032+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1033+
1034+
final channelContentInputFinder = find.descendant(
1035+
of: find.byType(ComposeAutocomplete),
1036+
matching: find.byType(TextField));
1037+
1038+
await tester.enterText(channelContentInputFinder, 'Some text');
1039+
prepareGetMessageResponse([message]);
1040+
handleMessageMoveEvent([message], 'new topic');
1041+
await tester.pump(const Duration(seconds: 1));
1042+
check(tester.widget<TextField>(channelContentInputFinder))
1043+
..decoration.isNotNull().hintText.equals('Message #${channel.name} > new topic')
1044+
..controller.isNotNull().text.equals('Some text');
1045+
1046+
connection.prepare(json: SendMessageResult(id: 1).toJson());
1047+
await tester.tap(find.byIcon(Icons.send));
1048+
await tester.pump();
1049+
check(connection.lastRequest).isA<http.Request>()
1050+
..method.equals('POST')
1051+
..url.path.equals('/api/v1/messages')
1052+
..bodyFields.deepEquals({
1053+
'type': 'stream',
1054+
'to': '${message.streamId}',
1055+
'topic': 'new topic',
1056+
'content': 'Some text',
1057+
'read_by_sender': 'true'});
1058+
await tester.pumpAndSettle();
1059+
});
1060+
1061+
testWidgets('Move to narrow with existing messages', (WidgetTester tester) async {
1062+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1063+
check(find.textContaining('Existing message').evaluate()).length.equals(0);
1064+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
1065+
1066+
final existingMessage = eg.streamMessage(
1067+
stream: eg.stream(), topic: 'new topic', content: 'Existing message');
1068+
prepareGetMessageResponse([existingMessage, message]);
1069+
handleMessageMoveEvent([message], 'new topic');
1070+
await tester.pump(const Duration(seconds: 1));
1071+
1072+
check(find.textContaining('Existing message').evaluate()).length.equals(1);
1073+
check(find.textContaining('Message to move').evaluate()).length.equals(1);
1074+
});
1075+
1076+
testWidgets('show new topic name in TopicNarrow after move', (tester) async {
1077+
await setupMessageListPage(tester, narrow: narrow, messages: [message], streams: [channel]);
1078+
1079+
prepareGetMessageResponse([message]);
1080+
handleMessageMoveEvent([message], 'new topic');
1081+
await tester.pump(const Duration(seconds: 1));
1082+
1083+
check(find.descendant(
1084+
of: find.byType(RecipientHeader),
1085+
matching: find.text('new topic')).evaluate()
1086+
).length.equals(1);
1087+
check(find.descendant(
1088+
of: find.byType(MessageListAppBarTitle),
1089+
matching: find.text('${channel.name} > new topic')).evaluate()
1090+
).length.equals(1);
1091+
});
1092+
});
10121093
}

0 commit comments

Comments
 (0)