Skip to content

Commit 0f3dfc7

Browse files
committed
narrow: Support MentionsNarrow.
Because of how narrow interacts with the entire app, there are test updates all over the place. To check for the completeness of this change, looking at places where CombinedFeedNarrow is referenced can help, because MentionsNarrow is similar to it in many ways. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 6c75d2c commit 0f3dfc7

17 files changed

+193
-8
lines changed

assets/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,10 @@
472472
"@combinedFeedPageTitle": {
473473
"description": "Title for the page of combined feed."
474474
},
475+
"mentionsPageTitle": "Mentions",
476+
"@mentionsPageTitle": {
477+
"description": "Title for the page of @-mentions."
478+
},
475479
"notifGroupDmConversationLabel": "{senderFullName} to you and {numOthers, plural, =1{1 other} other{{numOthers} others}}",
476480
"@notifGroupDmConversationLabel": {
477481
"description": "Label for a group DM conversation notification.",

lib/model/autocomplete.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ class MentionAutocompleteView extends ChangeNotifier {
222222
case DmNarrow():
223223
break;
224224
case CombinedFeedNarrow():
225+
case MentionsNarrow():
225226
assert(false, 'No compose box, thus no autocomplete is available in ${narrow.runtimeType}.');
226227
}
227228
return (userA, userB) => _compareByRelevance(userA, userB,

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: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,34 @@ 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+
switch (flag) {
296+
MessageFlag.mentioned => true,
297+
MessageFlag.wildcardMentioned => true,
298+
MessageFlag.read => false,
299+
MessageFlag.starred => false,
300+
MessageFlag.collapsed => false,
301+
MessageFlag.hasAlertWord => false,
302+
MessageFlag.historical => false,
303+
MessageFlag.unknown => false,
304+
});
305+
}
306+
307+
@override
308+
ApiNarrow apiEncode() => [ApiNarrowIsMentioned()];
309+
310+
@override
311+
bool operator ==(Object other) {
312+
if (other is! MentionsNarrow) return false;
313+
// Conceptually there's only one value of this type.
314+
return true;
315+
}
316+
317+
@override
318+
int get hashCode => 'MentionsNarrow'.hashCode;
319+
}

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/actions.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,12 @@ Future<void> _legacyMarkNarrowAsRead(BuildContext context, Narrow narrow) async
134134
messages: unreadDms,
135135
op: UpdateMessageFlagsOp.add,
136136
flag: MessageFlag.read);
137+
case MentionsNarrow():
138+
final unreadMentions = store.unreads.mentions.toList();
139+
if (unreadMentions.isEmpty) return;
140+
await updateMessageFlags(connection,
141+
messages: unreadMentions,
142+
op: UpdateMessageFlagsOp.add,
143+
flag: MessageFlag.read);
137144
}
138145
}

lib/widgets/compose_box.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,13 +1025,14 @@ class ComposeBox extends StatelessWidget {
10251025

10261026
static bool hasComposeBox(Narrow narrow) {
10271027
switch (narrow) {
1028-
case CombinedFeedNarrow():
1029-
return false;
1030-
10311028
case ChannelNarrow():
10321029
case TopicNarrow():
10331030
case DmNarrow():
10341031
return true;
1032+
1033+
case CombinedFeedNarrow():
1034+
case MentionsNarrow():
1035+
return false;
10351036
}
10361037
}
10371038

@@ -1046,6 +1047,7 @@ class ComposeBox extends StatelessWidget {
10461047
case DmNarrow():
10471048
return _FixedDestinationComposeBox(key: controllerKey, narrow: narrow);
10481049
case CombinedFeedNarrow():
1050+
case MentionsNarrow():
10491051
return const SizedBox.shrink();
10501052
}
10511053
}

lib/widgets/message_list.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
235235
bool removeAppBarBottomBorder = false;
236236
switch(widget.narrow) {
237237
case CombinedFeedNarrow():
238+
case MentionsNarrow():
238239
appBarBackgroundColor = null; // i.e., inherit
239240

240241
case ChannelNarrow(:final streamId):
@@ -320,6 +321,9 @@ class MessageListAppBarTitle extends StatelessWidget {
320321
case CombinedFeedNarrow():
321322
return Text(zulipLocalizations.combinedFeedPageTitle);
322323

324+
case MentionsNarrow():
325+
return Text(zulipLocalizations.mentionsPageTitle);
326+
323327
case ChannelNarrow(:var streamId):
324328
final store = PerAccountStoreWidget.of(context);
325329
final stream = store.streams[streamId];
@@ -808,6 +812,7 @@ class RecipientHeader extends StatelessWidget {
808812
static bool _containsDifferentChannels(Narrow narrow) {
809813
switch (narrow) {
810814
case CombinedFeedNarrow():
815+
case MentionsNarrow():
811816
return true;
812817

813818
case ChannelNarrow():

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,13 @@ void main() {
571571
check(() => MentionAutocompleteView.init(store: store, narrow: narrow))
572572
.throws<AssertionError>();
573573
});
574+
575+
test('MentionsNarrow gives error', () async {
576+
await prepare(users: [eg.user(), eg.user()], messages: []);
577+
const narrow = MentionsNarrow();
578+
check(() => MentionAutocompleteView.init(store: store, narrow: narrow))
579+
.throws<AssertionError>();
580+
});
574581
});
575582

576583
test('final results end-to-end', () async {

test/model/compose_test.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,14 @@ hello
231231
.equals(store.realmUrl.resolve('#narrow/near/1'));
232232
});
233233

234+
test('MentionsNarrow', () {
235+
final store = eg.store();
236+
check(narrowLink(store, const MentionsNarrow()))
237+
.equals(store.realmUrl.resolve('#narrow/is/mentioned'));
238+
check(narrowLink(store, const MentionsNarrow(), nearMessageId: 1))
239+
.equals(store.realmUrl.resolve('#narrow/is/mentioned/near/1'));
240+
});
241+
234242
test('ChannelNarrow / TopicNarrow', () {
235243
void checkNarrow(String expectedFragment, {
236244
required int streamId,
@@ -298,9 +306,6 @@ hello
298306
'#narrow/dm/1,2-dm/near/12345',
299307
'#narrow/pm-with/1,2-pm/near/12345');
300308
});
301-
302-
// TODO other Narrow subclasses as we add them:
303-
// starred, mentioned; searches; arbitrary
304309
});
305310

306311
group('mention', () {

test/model/message_list_test.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,46 @@ void main() {
734734
checkNotifiedOnce();
735735
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301));
736736
});
737+
738+
test('in MentionsNarrow', () async {
739+
final stream = eg.stream(streamId: 1, name: 'muted stream');
740+
const mutedTopic = 'muted';
741+
await prepare(narrow: const MentionsNarrow());
742+
await store.addStream(stream);
743+
await store.addUserTopic(stream, mutedTopic, UserTopicVisibilityPolicy.muted);
744+
await store.addSubscription(eg.subscription(stream, isMuted: true));
745+
746+
List<Message> getMessages(int startingId) => [
747+
eg.streamMessage(id: startingId,
748+
stream: stream, topic: mutedTopic, flags: [MessageFlag.wildcardMentioned]),
749+
eg.streamMessage(id: startingId + 1,
750+
stream: stream, topic: mutedTopic, flags: [MessageFlag.mentioned]),
751+
eg.dmMessage(id: startingId + 2,
752+
from: eg.otherUser, to: [eg.selfUser], flags: [MessageFlag.mentioned]),
753+
];
754+
755+
// Check filtering on fetchInitial…
756+
await prepareMessages(foundOldest: false, messages: getMessages(201));
757+
final expected = <int>[];
758+
check(model.messages.map((m) => m.id))
759+
.deepEquals(expected..addAll([201, 202, 203]));
760+
761+
// … and on fetchOlder…
762+
connection.prepare(json: olderResult(
763+
anchor: 201, foundOldest: true, messages: getMessages(101)).toJson());
764+
await model.fetchOlder();
765+
checkNotified(count: 2);
766+
check(model.messages.map((m) => m.id))
767+
.deepEquals(expected..insertAll(0, [101, 102, 103]));
768+
769+
// … and on MessageEvent.
770+
final messages = getMessages(301);
771+
for (var i = 0; i < 3; i += 1) {
772+
await store.handleEvent(MessageEvent(id: 0, message: messages[i]));
773+
checkNotifiedOnce();
774+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301 + i));
775+
}
776+
});
737777
});
738778

739779
test('recipient headers are maintained consistently', () async {
@@ -979,6 +1019,7 @@ void checkInvariants(MessageListView model) {
9791019
.isTrue();
9801020
case TopicNarrow():
9811021
case DmNarrow():
1022+
case MentionsNarrow():
9821023
}
9831024
}
9841025

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(
158+
eg.streamMessage(flags: []))).isFalse();
159+
check(narrow.containsMessage(
160+
eg.streamMessage(flags:[MessageFlag.mentioned]))).isTrue();
161+
check(narrow.containsMessage(
162+
eg.streamMessage(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 channelStore.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
@@ -377,6 +377,12 @@ void main() {
377377
await setupToMessageActionSheet(tester, message: message, narrow: const CombinedFeedNarrow());
378378
check(findQuoteAndReplyButton(tester)).isNull();
379379
});
380+
381+
testWidgets('not offered in MentionsNarrow (composing to reply is not yet supported)', (WidgetTester tester) async {
382+
final message = eg.streamMessage();
383+
await setupToMessageActionSheet(tester, message: message, narrow: const MentionsNarrow());
384+
check(findQuoteAndReplyButton(tester)).isNull();
385+
});
380386
});
381387

382388
group('CopyMessageTextButton', () {

test/widgets/actions_test.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
55
import 'package:flutter_test/flutter_test.dart';
66
import 'package:http/http.dart' as http;
77
import 'package:zulip/api/model/initial_snapshot.dart';
8+
import 'package:zulip/api/model/model.dart';
89
import 'package:zulip/api/model/narrow.dart';
910
import 'package:zulip/api/route/messages.dart';
1011
import 'package:zulip/model/localizations.dart';
@@ -258,6 +259,26 @@ void main() {
258259
});
259260
});
260261

262+
testWidgets('MentionsNarrow on legacy server', (WidgetTester tester) async {
263+
const narrow = MentionsNarrow();
264+
final message = eg.streamMessage(flags: [MessageFlag.mentioned]);
265+
final unreadMsgs = eg.unreadMsgs(mentions: [message.id]);
266+
await prepare(tester, unreadMsgs: unreadMsgs);
267+
connection.zulipFeatureLevel = 154;
268+
connection.prepare(json:
269+
UpdateMessageFlagsResult(messages: [message.id]).toJson());
270+
markNarrowAsRead(context, narrow, true); // TODO move legacy-server check inside markNarrowAsRead
271+
await tester.pump(Duration.zero);
272+
check(connection.lastRequest).isA<http.Request>()
273+
..method.equals('POST')
274+
..url.path.equals('/api/v1/messages/flags')
275+
..bodyFields.deepEquals({
276+
'messages': jsonEncode([message.id]),
277+
'op': 'add',
278+
'flag': 'read',
279+
});
280+
});
281+
261282
testWidgets('catch-all api errors', (WidgetTester tester) async {
262283
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
263284
const narrow = CombinedFeedNarrow();

test/widgets/message_list_test.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ void main() {
135135
final padding = MediaQuery.of(element).padding;
136136
check(padding).equals(EdgeInsets.zero);
137137
});
138+
139+
testWidgets('content in MentionsNarrow not asked to consume insets (including bottom)', (tester) async {
140+
const fakePadding = FakeViewPadding(left: 10, top: 10, right: 10, bottom: 10);
141+
tester.view.viewInsets = fakePadding;
142+
tester.view.padding = fakePadding;
143+
144+
await setupMessageListPage(tester, narrow: const MentionsNarrow(),
145+
messages: [eg.streamMessage(content: ContentExample.codeBlockPlain.html, flags: [MessageFlag.mentioned])]);
146+
147+
final element = tester.element(find.byType(CodeBlock));
148+
final padding = MediaQuery.of(element).padding;
149+
check(padding).equals(EdgeInsets.zero);
150+
});
138151
});
139152

140153
testWidgets('smoke test for light/dark/lerped', (tester) async {
@@ -628,7 +641,16 @@ void main() {
628641
check(findInMessageList('topic name')).length.equals(1);
629642
});
630643

631-
testWidgets('do not show stream name in ChannelNarrow', (tester) async {
644+
testWidgets('show channel name in MentionsNarrow', (tester) async {
645+
await setupMessageListPage(tester,
646+
narrow: const MentionsNarrow(),
647+
messages: [message], subscriptions: [eg.subscription(stream)]);
648+
await tester.pump();
649+
check(findInMessageList('stream name')).length.equals(1);
650+
check(findInMessageList('topic name')).length.equals(1);
651+
});
652+
653+
testWidgets('do not show channel name in ChannelNarrow', (tester) async {
632654
await setupMessageListPage(tester,
633655
narrow: ChannelNarrow(stream.streamId),
634656
messages: [message], streams: [stream]);

0 commit comments

Comments
 (0)