Skip to content

Commit 995cb03

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 8313e4e commit 995cb03

17 files changed

+186
-3
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
@@ -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: 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/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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,7 @@ class ComposeBox extends StatelessWidget {
10261026
static bool hasComposeBox(Narrow narrow) {
10271027
switch (narrow) {
10281028
case CombinedFeedNarrow():
1029+
case MentionsNarrow():
10291030
return false;
10301031

10311032
case StreamNarrow():
@@ -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
@@ -83,6 +83,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
8383
bool removeAppBarBottomBorder = false;
8484
switch(widget.narrow) {
8585
case CombinedFeedNarrow():
86+
case MentionsNarrow():
8687
appBarBackgroundColor = null; // i.e., inherit
8788

8889
case StreamNarrow(:final streamId):
@@ -188,6 +189,9 @@ class MessageListAppBarTitle extends StatelessWidget {
188189
final names = otherRecipientIds.map((id) => store.users[id]?.fullName ?? '(unknown user)');
189190
return Text("DMs with ${names.join(", ")}"); // TODO show avatars
190191
}
192+
193+
case MentionsNarrow():
194+
return Text(zulipLocalizations.mentionsPageTitle);
191195
}
192196
}
193197
}
@@ -537,6 +541,7 @@ class RecipientHeader extends StatelessWidget {
537541
static bool _containsDifferentChannels(Narrow narrow) {
538542
switch (narrow) {
539543
case CombinedFeedNarrow():
544+
case MentionsNarrow():
540545
return true;
541546
case StreamNarrow():
542547
case TopicNarrow():

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
@@ -569,6 +569,13 @@ void main() {
569569
check(() => MentionAutocompleteView.init(store: store, narrow: narrow))
570570
.throws<AssertionError>();
571571
});
572+
573+
test('MentionsNarrow gives error', () async {
574+
await prepare(users: [eg.user(), eg.user()], messages: []);
575+
const narrow = MentionsNarrow();
576+
check(() => MentionAutocompleteView.init(store: store, narrow: narrow))
577+
.throws<AssertionError>();
578+
});
572579
});
573580

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

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
@@ -734,6 +734,52 @@ 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 stream1 = eg.stream(streamId: 1, name: 'stream 1');
740+
final stream2 = eg.stream(streamId: 2, name: 'stream 2');
741+
await prepare(narrow: const MentionsNarrow());
742+
await store.addStreams([stream1, stream2]);
743+
await store.addSubscription(eg.subscription(stream1));
744+
await store.addUserTopic(stream1, 'B', UserTopicVisibilityPolicy.muted);
745+
await store.addSubscription(eg.subscription(stream2, isMuted: true));
746+
await store.addUserTopic(stream2, 'C', UserTopicVisibilityPolicy.unmuted);
747+
748+
List<Message> getMessages(int startingId) => [
749+
eg.streamMessage(id: startingId,
750+
stream: stream1, topic: "A", flags: [MessageFlag.wildcardMentioned]),
751+
eg.streamMessage(id: startingId + 1,
752+
stream: stream1, topic: "B", flags: [MessageFlag.wildcardMentioned]),
753+
eg.streamMessage(id: startingId + 2,
754+
stream: stream2, topic: "C", flags: [MessageFlag.wildcardMentioned]),
755+
eg.streamMessage(id: startingId + 3,
756+
stream: stream2, topic: "D", flags: [MessageFlag.wildcardMentioned]),
757+
eg.dmMessage(id: startingId + 4,
758+
from: eg.otherUser, to: [eg.selfUser], flags: [MessageFlag.mentioned]),
759+
];
760+
761+
// Check filtering on fetchInitial…
762+
await prepareMessages(foundOldest: false, messages: getMessages(201));
763+
final expected = <int>[];
764+
check(model.messages.map((m) => m.id))
765+
.deepEquals(expected..addAll([201, 202, 203, 204, 205]));
766+
767+
// … and on fetchOlder…
768+
connection.prepare(json: olderResult(
769+
anchor: 201, foundOldest: true, messages: getMessages(101)).toJson());
770+
await model.fetchOlder();
771+
checkNotified(count: 2);
772+
check(model.messages.map((m) => m.id))
773+
.deepEquals(expected..insertAll(0, [101, 102, 103, 104, 105]));
774+
775+
// … and on MessageEvent.
776+
final messages = getMessages(301);
777+
for (var i = 0; i < 5; i += 1) {
778+
await store.handleEvent(MessageEvent(id: 0, message: messages[i]));
779+
checkNotifiedOnce();
780+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301 + i));
781+
}
782+
});
737783
});
738784

739785
test('recipient headers are maintained consistently', () async {
@@ -979,6 +1025,7 @@ void checkInvariants(MessageListView model) {
9791025
.isTrue();
9801026
case TopicNarrow():
9811027
case DmNarrow():
1028+
case MentionsNarrow():
9821029
}
9831030
}
9841031

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 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
@@ -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/actions_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
66
import 'package:flutter_test/flutter_test.dart';
77
import 'package:http/http.dart' as http;
88
import 'package:zulip/api/model/initial_snapshot.dart';
9+
import 'package:zulip/api/model/model.dart';
910
import 'package:zulip/api/model/narrow.dart';
1011
import 'package:zulip/api/route/messages.dart';
1112
import 'package:zulip/model/localizations.dart';
@@ -267,6 +268,27 @@ void main() {
267268
});
268269
});
269270

271+
testWidgets('MentionsNarrow on legacy server', (WidgetTester tester) async {
272+
const narrow = MentionsNarrow();
273+
final message = eg.streamMessage(flags: [MessageFlag.mentioned]);
274+
final unreadMsgs = eg.unreadMsgs(mentions: [message.id]);
275+
await prepare(tester, unreadMsgs: unreadMsgs);
276+
connection.zulipFeatureLevel = 154;
277+
connection.prepare(json:
278+
UpdateMessageFlagsResult(messages: [message.id]).toJson());
279+
markNarrowAsRead(context, narrow, true); // TODO move legacy-server check inside markNarrowAsRead
280+
await tester.pump(Duration.zero);
281+
check(connection.lastRequest).isA<http.Request>()
282+
..method.equals('POST')
283+
..url.path.equals('/api/v1/messages/flags')
284+
..bodyFields.deepEquals({
285+
'messages': jsonEncode([message.id]),
286+
'op': 'add',
287+
'flag': 'read',
288+
});
289+
await tester.pumpAndSettle();
290+
});
291+
270292
testWidgets('catch-all api errors', (WidgetTester tester) async {
271293
final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
272294
const narrow = CombinedFeedNarrow();

test/widgets/message_list_test.dart

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

147160
group('fetch older messages on scroll', () {
@@ -315,6 +328,15 @@ void main() {
315328
check(findInMessageList('topic name')).length.equals(1);
316329
});
317330

331+
testWidgets('show stream name in MentionsNarrow', (tester) async {
332+
await setupMessageListPage(tester,
333+
narrow: const MentionsNarrow(),
334+
messages: [message], subscriptions: [eg.subscription(stream)]);
335+
await tester.pump();
336+
check(findInMessageList('stream name')).length.equals(1);
337+
check(findInMessageList('topic name')).length.equals(1);
338+
});
339+
318340
testWidgets('do not show stream name in StreamNarrow', (tester) async {
319341
await setupMessageListPage(tester,
320342
narrow: StreamNarrow(stream.streamId),

0 commit comments

Comments
 (0)