Skip to content

Commit aae5d72

Browse files
committed
narrow: Support MentionsNarrow.
Because of how narrow interacts with the entire app, there are test updates all over the place. The way I checked for the completeness of this change is by looking at places where CombinedFeedNarrow is referenced. Signed-off-by: Zixuan James Li <[email protected]>
1 parent e37fdec commit aae5d72

15 files changed

+190
-4
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@
344344
"@loginErrorMissingUsername": {
345345
"description": "Error message when an empty username was provided."
346346
},
347+
"mentionsPageTitle": "Mentions",
348+
"@mentionsPageTitle": {
349+
"description": "Title for the page of @-mentions."
350+
},
347351
"topicValidationErrorTooLong": "Topic length shouldn't be greater than 60 characters.",
348352
"@topicValidationErrorTooLong": {
349353
"description": "Topic validation error when topic is too long."

lib/model/autocomplete.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class MentionAutocompleteView extends ChangeNotifier {
188188
required Narrow narrow,
189189
}) {
190190
assert(narrow is! CombinedFeedNarrow);
191+
assert(narrow is! MentionsNarrow);
191192
return store.users.values.toList()
192193
..sort((userA, userB) => compareByDms(userA, userB, store: store));
193194
}

lib/model/message_list.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
370370

371371
case TopicNarrow():
372372
case DmNarrow():
373+
case MentionsNarrow():
373374
return true;
374375
}
375376
}
@@ -385,6 +386,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
385386

386387
case TopicNarrow():
387388
case DmNarrow():
389+
case MentionsNarrow():
388390
return true;
389391
}
390392
}

lib/model/narrow.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,25 @@ class DmNarrow extends Narrow implements SendableNarrow {
286286
int get hashCode => Object.hash('DmNarrow', _key);
287287
}
288288

289-
// TODO other narrow types: starred, mentioned; searches; arbitrary
289+
class MentionsNarrow extends Narrow {
290+
const MentionsNarrow();
291+
292+
@override
293+
bool containsMessage(Message message) {
294+
return message.flags.any((flag) =>
295+
flag == MessageFlag.mentioned || flag == MessageFlag.wildcardMentioned);
296+
}
297+
298+
@override
299+
ApiNarrow apiEncode() => [ApiNarrowIsMentioned()];
300+
301+
@override
302+
bool operator ==(Object other) {
303+
if (other is! MentionsNarrow) return false;
304+
// Conceptually there's only one value of this type.
305+
return true;
306+
}
307+
308+
@override
309+
int get hashCode => 'MentionedNarrow'.hashCode;
310+
}

lib/model/unreads.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ class Unreads extends ChangeNotifier {
193193

194194
int countInDmNarrow(DmNarrow narrow) => dms[narrow]?.length ?? 0;
195195

196+
int countInMentionsNarrow() => mentions.length;
197+
196198
int countInNarrow(Narrow narrow) {
197199
switch (narrow) {
198200
case CombinedFeedNarrow():
@@ -203,6 +205,8 @@ class Unreads extends ChangeNotifier {
203205
return countInTopicNarrow(narrow.streamId, narrow.topic);
204206
case DmNarrow():
205207
return countInDmNarrow(narrow);
208+
case MentionsNarrow():
209+
return countInMentionsNarrow();
206210
}
207211
}
208212

lib/widgets/compose_box.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,7 @@ class ComposeBox extends StatelessWidget {
961961
static bool hasComposeBox(Narrow narrow) {
962962
switch (narrow) {
963963
case CombinedFeedNarrow():
964+
case MentionsNarrow():
964965
return true;
965966

966967
case StreamNarrow():
@@ -981,6 +982,7 @@ class ComposeBox extends StatelessWidget {
981982
case DmNarrow():
982983
return _FixedDestinationComposeBox(key: controllerKey, narrow: narrow);
983984
case CombinedFeedNarrow():
985+
case MentionsNarrow():
984986
return const SizedBox.shrink();
985987
}
986988
}

lib/widgets/message_list.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
8484
bool removeAppBarBottomBorder = false;
8585
switch(widget.narrow) {
8686
case CombinedFeedNarrow():
87+
case MentionsNarrow():
8788
appBarBackgroundColor = null; // i.e., inherit
8889

8990
case StreamNarrow(:final streamId):
@@ -191,6 +192,9 @@ class MessageListAppBarTitle extends StatelessWidget {
191192
final names = otherRecipientIds.map((id) => store.users[id]?.fullName ?? '(unknown user)');
192193
return Text("DMs with ${names.join(", ")}"); // TODO show avatars
193194
}
195+
196+
case MentionsNarrow():
197+
return Text(zulipLocalizations.mentionsPageTitle);
194198
}
195199
}
196200
}
@@ -1126,5 +1130,12 @@ Future<void> _legacyMarkNarrowAsRead(BuildContext context, Narrow narrow) async
11261130
messages: unreadDms,
11271131
op: UpdateMessageFlagsOp.add,
11281132
flag: MessageFlag.read);
1133+
case MentionsNarrow():
1134+
final unreadMentions = store.unreads.mentions.toList();
1135+
if (unreadMentions.isEmpty) return;
1136+
await updateMessageFlags(connection,
1137+
messages: unreadMentions,
1138+
op: UpdateMessageFlagsOp.add,
1139+
flag: MessageFlag.read);
11291140
}
11301141
}

test/api/route/messages_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ void main() {
188188
{'operator': 'stream', 'operand': 12},
189189
{'operator': 'topic', 'operand': 'stuff'},
190190
]));
191+
checkNarrow(const MentionsNarrow().apiEncode(), jsonEncode([
192+
{'operator': 'is', 'operand': 'mentioned'},
193+
]));
191194

192195
checkNarrow([ApiNarrowDm([123, 234])], jsonEncode([
193196
{'operator': 'dm', 'operand': [123, 234]},

test/model/autocomplete_test.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,15 @@ void main() {
472472
expected: [0, 1, 2, 3, 4])
473473
).throws();
474474
});
475+
476+
test('MentionsNarrow', () async {
477+
// As we do not expect a compose box in [MentionsNarrow], it should
478+
// not proceed to show any results.
479+
await check(checkResultsIn(
480+
const MentionsNarrow(),
481+
expected: [0, 1, 2, 3, 4])
482+
).throws();
483+
});
475484
});
476485
});
477486
}

test/model/compose_test.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,13 @@ hello
299299
'#narrow/pm-with/1,2-pm/near/12345');
300300
});
301301

302-
// TODO other Narrow subclasses as we add them:
303-
// starred, mentioned; searches; arbitrary
302+
test('MentionsNarrow', () {
303+
final store = eg.store();
304+
check(narrowLink(store, const MentionsNarrow()))
305+
.equals(store.realmUrl.resolve('#narrow/is/mentioned'));
306+
check(narrowLink(store, const MentionsNarrow(), nearMessageId: 1))
307+
.equals(store.realmUrl.resolve('#narrow/is/mentioned/near/1'));
308+
});
304309
});
305310

306311
group('mention', () {

test/model/message_list_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,52 @@ void main() {
693693
checkNotifiedOnce();
694694
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301));
695695
});
696+
697+
test('in MentionsNarrow', () async {
698+
final stream1 = eg.stream(streamId: 1, name: 'stream 1');
699+
final stream2 = eg.stream(streamId: 2, name: 'stream 2');
700+
await prepare(narrow: const MentionsNarrow());
701+
await store.addStreams([stream1, stream2]);
702+
await store.addSubscription(eg.subscription(stream1));
703+
await store.addUserTopic(stream1, 'B', UserTopicVisibilityPolicy.muted);
704+
await store.addSubscription(eg.subscription(stream2, isMuted: true));
705+
await store.addUserTopic(stream2, 'C', UserTopicVisibilityPolicy.unmuted);
706+
707+
List<Message> getMessages(int startingId) => [
708+
eg.streamMessage(id: startingId,
709+
stream: stream1, topic: "A", flags: [MessageFlag.wildcardMentioned]),
710+
eg.streamMessage(id: startingId + 1,
711+
stream: stream1, topic: "B", flags: [MessageFlag.wildcardMentioned]),
712+
eg.streamMessage(id: startingId + 2,
713+
stream: stream2, topic: "C", flags: [MessageFlag.wildcardMentioned]),
714+
eg.streamMessage(id: startingId + 3,
715+
stream: stream2, topic: "D", flags: [MessageFlag.wildcardMentioned]),
716+
eg.dmMessage(id: startingId + 4,
717+
from: eg.otherUser, to: [eg.selfUser], flags: [MessageFlag.mentioned]),
718+
];
719+
720+
// Check filtering on fetchInitial…
721+
await prepareMessages(foundOldest: false, messages: getMessages(201));
722+
final expected = <int>[];
723+
check(model.messages.map((m) => m.id))
724+
.deepEquals(expected..addAll([201, 202, 203, 204, 205]));
725+
726+
// … and on fetchOlder…
727+
connection.prepare(json: olderResult(
728+
anchor: 201, foundOldest: true, messages: getMessages(101)).toJson());
729+
await model.fetchOlder();
730+
checkNotified(count: 2);
731+
check(model.messages.map((m) => m.id))
732+
.deepEquals(expected..insertAll(0, [101, 102, 103, 104, 105]));
733+
734+
// … and on MessageEvent.
735+
final messages = getMessages(301);
736+
for (var i = 0; i < 5; i += 1) {
737+
await store.handleEvent(MessageEvent(id: 0, message: messages[i]));
738+
checkNotifiedOnce();
739+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301 + i));
740+
}
741+
});
696742
});
697743

698744
test('recipient headers are maintained consistently', () async {
@@ -938,6 +984,7 @@ void checkInvariants(MessageListView model) {
938984
.isTrue();
939985
case TopicNarrow():
940986
case DmNarrow():
987+
case MentionsNarrow():
941988
}
942989
}
943990

test/model/narrow_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,17 @@ void main() {
149149
check(narrow123.containsMessage(dm(user3, [user1, user2]))).isTrue();
150150
});
151151
});
152+
153+
group('MentionsNarrow', () {
154+
test('containsMessage', () {
155+
const narrow = MentionsNarrow();
156+
157+
check(narrow.containsMessage(eg.streamMessage(
158+
flags: []))).isFalse();
159+
check(narrow.containsMessage(eg.streamMessage(
160+
flags:[MessageFlag.mentioned]))).isTrue();
161+
check(narrow.containsMessage(eg.streamMessage(
162+
flags: [MessageFlag.wildcardMentioned]))).isTrue();
163+
});
164+
});
152165
}

test/model/unreads_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ void main() {
215215
eg.otherUser.userId, selfUserId: eg.selfUser.userId);
216216
check(model.countInDmNarrow(narrow)).equals(5);
217217
});
218+
219+
test('countInMentionsNarrow', () async {
220+
final stream = eg.stream();
221+
prepare();
222+
await streamStore.addStream(stream);
223+
fillWithMessages([
224+
eg.streamMessage(stream: stream, flags: []),
225+
eg.streamMessage(stream: stream, flags: [MessageFlag.mentioned]),
226+
eg.streamMessage(stream: stream, flags: [MessageFlag.wildcardMentioned]),
227+
]);
228+
check(model.countInMentionsNarrow()).equals(2);
229+
});
218230
});
219231

220232
group('handleMessageEvent', () {

test/widgets/action_sheet_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,12 @@ void main() {
386386
await setupToMessageActionSheet(tester, message: message, narrow: const CombinedFeedNarrow());
387387
check(findQuoteAndReplyButton(tester)).isNull();
388388
});
389+
390+
testWidgets('not offered in MentionsNarrow (composing to reply is not yet supported)', (WidgetTester tester) async {
391+
final message = eg.streamMessage();
392+
await setupToMessageActionSheet(tester, message: message, narrow: const MentionsNarrow());
393+
check(findQuoteAndReplyButton(tester)).isNull();
394+
});
389395
});
390396

391397
group('CopyMessageTextButton', () {

test/widgets/message_list_test.dart

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ void main() {
143143
final padding = MediaQuery.of(element).padding;
144144
check(padding).equals(EdgeInsets.zero);
145145
});
146+
147+
testWidgets('content in MentionsNarrow not asked to consume insets (including bottom)', (tester) async {
148+
const fakePadding = FakeViewPadding(left: 10, top: 10, right: 10, bottom: 10);
149+
tester.view.viewInsets = fakePadding;
150+
tester.view.padding = fakePadding;
151+
152+
await setupMessageListPage(tester, narrow: const MentionsNarrow(),
153+
messages: [eg.streamMessage(content: ContentExample.codeBlockPlain.html, flags: [MessageFlag.mentioned])]);
154+
155+
final element = tester.element(find.byType(CodeBlock));
156+
final padding = MediaQuery.of(element).padding;
157+
check(padding).equals(EdgeInsets.zero);
158+
});
146159
});
147160

148161
group('fetch older messages on scroll', () {
@@ -298,7 +311,8 @@ void main() {
298311
group('recipient headers', () {
299312
group('StreamMessageRecipientHeader', () {
300313
final stream = eg.stream(name: 'stream name');
301-
final message = eg.streamMessage(stream: stream, topic: 'topic name');
314+
final message = eg.streamMessage(
315+
stream: stream, topic: 'topic name');
302316

303317
FinderResult<Element> findInMessageList(String text) {
304318
// Stream name shows up in [AppBar] so need to avoid matching that
@@ -316,6 +330,15 @@ void main() {
316330
check(findInMessageList('topic name')).length.equals(1);
317331
});
318332

333+
testWidgets('show stream name in MentionsNarrow', (tester) async {
334+
await setupMessageListPage(tester,
335+
narrow: const MentionsNarrow(),
336+
messages: [message], subscriptions: [eg.subscription(stream)]);
337+
await tester.pump();
338+
check(findInMessageList('stream name')).length.equals(1);
339+
check(findInMessageList('topic name')).length.equals(1);
340+
});
341+
319342
testWidgets('do not show stream name in StreamNarrow', (tester) async {
320343
await setupMessageListPage(tester,
321344
narrow: StreamNarrow(stream.streamId),
@@ -1128,6 +1151,29 @@ void main() {
11281151
await tester.pumpAndSettle(); // process pending timers
11291152
});
11301153

1154+
testWidgets('MentionsNarrow on legacy server', (WidgetTester tester) async {
1155+
await setupMessageListPage(tester,
1156+
narrow: const MentionsNarrow(),
1157+
messages: [eg.streamMessage(flags: [MessageFlag.mentioned])],
1158+
unreadMsgs: eg.unreadMsgs(mentions: [message.id]),
1159+
);
1160+
check(isMarkAsReadButtonVisible(tester)).isTrue();
1161+
1162+
connection.zulipFeatureLevel = 154;
1163+
connection.prepare(json:
1164+
UpdateMessageFlagsResult(messages: [message.id]).toJson());
1165+
await tester.tap(find.byType(MarkAsReadWidget));
1166+
check(connection.lastRequest).isA<http.Request>()
1167+
..method.equals('POST')
1168+
..url.path.equals('/api/v1/messages/flags')
1169+
..bodyFields.deepEquals({
1170+
'messages': jsonEncode([message.id]),
1171+
'op': 'add',
1172+
'flag': 'read',
1173+
});
1174+
await tester.pumpAndSettle();
1175+
});
1176+
11311177
testWidgets('catch-all api errors', (WidgetTester tester) async {
11321178
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
11331179
const narrow = CombinedFeedNarrow();

0 commit comments

Comments
 (0)