Skip to content

Commit 94a4229

Browse files
committed
narrow: Add starred messages narrow.
"Starred messages narrow" is the user facing name of the narrow, thus we name the Narrow class after it. This narrow is pretty similar to MentionsNarrow. Signed-off-by: Zixuan James Li <[email protected]>
1 parent 49253cc commit 94a4229

20 files changed

+157
-3
lines changed

assets/l10n/app_en.arb

+4
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,10 @@
499499
"@mentionsPageTitle": {
500500
"description": "Title for the page of @-mentions."
501501
},
502+
"starredMessagesPageTitle": "Starred messages",
503+
"@starredMessagesPageTitle": {
504+
"description": "Title for the page of starred messages."
505+
},
502506
"notifGroupDmConversationLabel": "{senderFullName} to you and {numOthers, plural, =1{1 other} other{{numOthers} others}}",
503507
"@notifGroupDmConversationLabel": {
504508
"description": "Label for a group DM conversation notification.",

lib/model/autocomplete.dart

+1
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
364364
break;
365365
case CombinedFeedNarrow():
366366
case MentionsNarrow():
367+
case StarredMessagesNarrow():
367368
assert(false, 'No compose box, thus no autocomplete is available in ${narrow.runtimeType}.');
368369
}
369370
return (userA, userB) => _compareByRelevance(userA, userB,

lib/model/internal_link.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,11 @@ Narrow? _interpretNarrowSegments(List<String> segments, PerAccountStore store) {
199199
switch (isElementOperands.single) {
200200
case IsOperand.mentioned:
201201
return const MentionsNarrow();
202+
case IsOperand.starred:
203+
return const StarredMessagesNarrow();
202204
case IsOperand.dm:
203205
case IsOperand.private:
204206
case IsOperand.alerted:
205-
case IsOperand.starred:
206207
case IsOperand.followed:
207208
case IsOperand.resolved:
208209
case IsOperand.unread:

lib/model/message.dart

+7-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,13 @@ class MessageStoreImpl with MessageStore {
271271
for (final view in _messageListViews) {
272272
view.notifyListenersIfAnyMessagePresent(event.messages);
273273
// TODO(#818): Support MentionsNarrow live-updates when handling
274-
// @-mention flags.
274+
// @-mentioned flags.
275+
276+
// To make it easier to re-star a message, we opt-out from supporting
277+
// live-updates when starred flag is removed.
278+
//
279+
// TODO: Support StarredMessagesNarrow live-updates when starred flag
280+
// is added.
275281
}
276282
}
277283
}

lib/model/message_list.dart

+4
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
418418
case TopicNarrow():
419419
case DmNarrow():
420420
case MentionsNarrow():
421+
case StarredMessagesNarrow():
421422
return true;
422423
}
423424
}
@@ -436,6 +437,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
436437
case TopicNarrow():
437438
case DmNarrow():
438439
case MentionsNarrow():
440+
case StarredMessagesNarrow():
439441
return VisibilityEffect.none;
440442
}
441443
}
@@ -452,6 +454,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
452454
case TopicNarrow():
453455
case DmNarrow():
454456
case MentionsNarrow():
457+
case StarredMessagesNarrow():
455458
return true;
456459
}
457460
}
@@ -638,6 +641,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
638641

639642
case CombinedFeedNarrow():
640643
case MentionsNarrow():
644+
case StarredMessagesNarrow():
641645
// The messages were and remain in this narrow.
642646
// TODO(#421): … except they may have become muted or not.
643647
// We'll handle that at the same time as we handle muting itself changing.

lib/model/narrow.dart

+22
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,25 @@ class MentionsNarrow extends Narrow {
321321
@override
322322
int get hashCode => 'MentionsNarrow'.hashCode;
323323
}
324+
325+
class StarredMessagesNarrow extends Narrow {
326+
const StarredMessagesNarrow();
327+
328+
@override
329+
ApiNarrow apiEncode() => [ApiNarrowIs(IsOperand.starred)];
330+
331+
@override
332+
bool containsMessage(Message message) {
333+
return message.flags.contains(MessageFlag.starred);
334+
}
335+
336+
@override
337+
bool operator ==(Object other) {
338+
if (other is! StarredMessagesNarrow) return false;
339+
// Conceptually there's only one value of this type.
340+
return true;
341+
}
342+
343+
@override
344+
int get hashCode => 'StarredMessagesNarrow'.hashCode;
345+
}

lib/model/unreads.dart

+5
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ class Unreads extends ChangeNotifier {
194194

195195
int countInMentionsNarrow() => mentions.length;
196196

197+
// TODO: Implement unreads handling.
198+
int countInStarredMessagesNarrow() => 0;
199+
197200
int countInNarrow(Narrow narrow) {
198201
switch (narrow) {
199202
case CombinedFeedNarrow():
@@ -206,6 +209,8 @@ class Unreads extends ChangeNotifier {
206209
return countInDmNarrow(narrow);
207210
case MentionsNarrow():
208211
return countInMentionsNarrow();
212+
case StarredMessagesNarrow():
213+
return countInStarredMessagesNarrow();
209214
}
210215
}
211216

lib/widgets/actions.dart

+3
Original file line numberDiff line numberDiff line change
@@ -214,5 +214,8 @@ Future<void> _legacyMarkNarrowAsRead(BuildContext context, Narrow narrow) async
214214
messages: unreadMentions,
215215
op: UpdateMessageFlagsOp.add,
216216
flag: MessageFlag.read);
217+
case StarredMessagesNarrow():
218+
// TODO: Implement unreads handling.
219+
return;
217220
}
218221
}

lib/widgets/app.dart

+6
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,12 @@ class HomePage extends StatelessWidget {
283283
narrow: const MentionsNarrow())),
284284
child: Text(zulipLocalizations.mentionsPageTitle)),
285285
const SizedBox(height: 16),
286+
ElevatedButton(
287+
onPressed: () => Navigator.push(context,
288+
MessageListPage.buildRoute(context: context,
289+
narrow: const StarredMessagesNarrow())),
290+
child: Text(zulipLocalizations.starredMessagesPageTitle)),
291+
const SizedBox(height: 16),
286292
ElevatedButton(
287293
onPressed: () => Navigator.push(context,
288294
InboxPage.buildRoute(context: context)),

lib/widgets/compose_box.dart

+2
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,7 @@ class ComposeBox extends StatelessWidget {
11231123

11241124
case CombinedFeedNarrow():
11251125
case MentionsNarrow():
1126+
case StarredMessagesNarrow():
11261127
return false;
11271128
}
11281129
}
@@ -1139,6 +1140,7 @@ class ComposeBox extends StatelessWidget {
11391140
return _FixedDestinationComposeBox(key: controllerKey, narrow: narrow);
11401141
case CombinedFeedNarrow():
11411142
case MentionsNarrow():
1143+
case StarredMessagesNarrow():
11421144
return const SizedBox.shrink();
11431145
}
11441146
}

lib/widgets/message_list.dart

+5
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ class _MessageListPageState extends State<MessageListPage> implements MessageLis
248248
switch(narrow) {
249249
case CombinedFeedNarrow():
250250
case MentionsNarrow():
251+
case StarredMessagesNarrow():
251252
appBarBackgroundColor = null; // i.e., inherit
252253

253254
case ChannelNarrow(:final streamId):
@@ -337,6 +338,9 @@ class MessageListAppBarTitle extends StatelessWidget {
337338
case MentionsNarrow():
338339
return Text(zulipLocalizations.mentionsPageTitle);
339340

341+
case StarredMessagesNarrow():
342+
return Text(zulipLocalizations.starredMessagesPageTitle);
343+
340344
case ChannelNarrow(:var streamId):
341345
final store = PerAccountStoreWidget.of(context);
342346
final stream = store.streams[streamId];
@@ -815,6 +819,7 @@ class RecipientHeader extends StatelessWidget {
815819
switch (narrow) {
816820
case CombinedFeedNarrow():
817821
case MentionsNarrow():
822+
case StarredMessagesNarrow():
818823
return true;
819824

820825
case ChannelNarrow():

test/api/route/messages_test.dart

+3
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ void main() {
191191
checkNarrow(const MentionsNarrow().apiEncode(), jsonEncode([
192192
{'operator': 'is', 'operand': 'mentioned'},
193193
]));
194+
checkNarrow(const StarredMessagesNarrow().apiEncode(), jsonEncode([
195+
{'operator': 'is', 'operand': 'starred'},
196+
]));
194197

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

test/model/autocomplete_test.dart

+7
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,13 @@ void main() {
679679
check(() => MentionAutocompleteView.init(store: store, narrow: narrow))
680680
.throws<AssertionError>();
681681
});
682+
683+
test('StarredMessagesNarrow gives error', () async {
684+
await prepare(users: [eg.user(), eg.user()], messages: []);
685+
const narrow = StarredMessagesNarrow();
686+
check(() => MentionAutocompleteView.init(store: store, narrow: narrow))
687+
.throws<AssertionError>();
688+
});
682689
});
683690

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

test/model/compose_test.dart

+8
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@ hello
239239
.equals(store.realmUrl.resolve('#narrow/is/mentioned/near/1'));
240240
});
241241

242+
test('StarredMessagesNarrow', () {
243+
final store = eg.store();
244+
check(narrowLink(store, const StarredMessagesNarrow()))
245+
.equals(store.realmUrl.resolve('#narrow/is/starred'));
246+
check(narrowLink(store, const StarredMessagesNarrow(), nearMessageId: 1))
247+
.equals(store.realmUrl.resolve('#narrow/is/starred/near/1'));
248+
});
249+
242250
test('ChannelNarrow / TopicNarrow', () {
243251
void checkNarrow(String expectedFragment, {
244252
required int streamId,

test/model/internal_link_test.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,11 @@ void main() {
242242
switch (operand) {
243243
case IsOperand.mentioned:
244244
testCases = sharedCases(const MentionsNarrow());
245+
case IsOperand.starred:
246+
testCases = sharedCases(const StarredMessagesNarrow());
245247
case IsOperand.dm:
246248
case IsOperand.private:
247249
case IsOperand.alerted:
248-
case IsOperand.starred:
249250
case IsOperand.followed:
250251
case IsOperand.resolved:
251252
case IsOperand.unread:

test/model/message_list_test.dart

+39
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,44 @@ void main() {
14471447
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301 + i));
14481448
}
14491449
});
1450+
1451+
test('in StarredMessagesNarrow', () async {
1452+
final stream = eg.stream(streamId: 1, name: 'muted stream');
1453+
const mutedTopic = 'muted';
1454+
await prepare(narrow: const StarredMessagesNarrow());
1455+
await store.addStream(stream);
1456+
await store.addUserTopic(stream, mutedTopic, UserTopicVisibilityPolicy.muted);
1457+
await store.addSubscription(eg.subscription(stream, isMuted: true));
1458+
1459+
List<Message> getMessages(int startingId) => [
1460+
eg.streamMessage(id: startingId,
1461+
stream: stream, topic: mutedTopic, flags: [MessageFlag.starred]),
1462+
eg.dmMessage(id: startingId + 1,
1463+
from: eg.otherUser, to: [eg.selfUser], flags: [MessageFlag.starred]),
1464+
];
1465+
1466+
// Check filtering on fetchInitial…
1467+
await prepareMessages(foundOldest: false, messages: getMessages(201));
1468+
final expected = <int>[];
1469+
check(model.messages.map((m) => m.id))
1470+
.deepEquals(expected..addAll([201, 202]));
1471+
1472+
// … and on fetchOlder…
1473+
connection.prepare(json: olderResult(
1474+
anchor: 201, foundOldest: true, messages: getMessages(101)).toJson());
1475+
await model.fetchOlder();
1476+
checkNotified(count: 2);
1477+
check(model.messages.map((m) => m.id))
1478+
.deepEquals(expected..insertAll(0, [101, 102]));
1479+
1480+
// … and on MessageEvent.
1481+
final messages = getMessages(301);
1482+
for (var i = 0; i < 2; i += 1) {
1483+
await store.handleEvent(MessageEvent(id: 0, message: messages[i]));
1484+
checkNotifiedOnce();
1485+
check(model.messages.map((m) => m.id)).deepEquals(expected..add(301 + i));
1486+
}
1487+
});
14501488
});
14511489

14521490
test('recipient headers are maintained consistently', () async {
@@ -1693,6 +1731,7 @@ void checkInvariants(MessageListView model) {
16931731
case TopicNarrow():
16941732
case DmNarrow():
16951733
case MentionsNarrow():
1734+
case StarredMessagesNarrow():
16961735
}
16971736
}
16981737

test/model/narrow_test.dart

+11
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,15 @@ void main() {
162162
eg.streamMessage(flags: [MessageFlag.wildcardMentioned]))).isTrue();
163163
});
164164
});
165+
166+
group('StarredMessagesNarrow', () {
167+
test('containsMessage', () {
168+
const narrow = StarredMessagesNarrow();
169+
170+
check(narrow.containsMessage(
171+
eg.streamMessage(flags: []))).isFalse();
172+
check(narrow.containsMessage(
173+
eg.streamMessage(flags:[MessageFlag.starred]))).isTrue();
174+
});
175+
});
165176
}

test/model/unreads_test.dart

+11
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,17 @@ void main() {
227227
]);
228228
check(model.countInMentionsNarrow()).equals(2);
229229
});
230+
231+
test('countInStarredMessagesNarrow', () async {
232+
final stream = eg.stream();
233+
prepare();
234+
await channelStore.addStream(stream);
235+
fillWithMessages([
236+
eg.streamMessage(stream: stream, flags: []),
237+
eg.streamMessage(stream: stream, flags: [MessageFlag.starred]),
238+
]);
239+
check(model.countInStarredMessagesNarrow()).equals(0);
240+
});
230241
});
231242

232243
group('handleMessageEvent', () {

test/widgets/action_sheet_test.dart

+6
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,12 @@ void main() {
392392
await setupToMessageActionSheet(tester, message: message, narrow: const MentionsNarrow());
393393
check(findQuoteAndReplyButton(tester)).isNull();
394394
});
395+
396+
testWidgets('not offered in StarredMessagesNarrow (composing to reply is not yet supported)', (tester) async {
397+
final message = eg.streamMessage(flags: [MessageFlag.starred]);
398+
await setupToMessageActionSheet(tester, message: message, narrow: const StarredMessagesNarrow());
399+
check(findQuoteAndReplyButton(tester)).isNull();
400+
});
395401
});
396402

397403
group('MarkAsUnread', () {

test/widgets/message_list_test.dart

+9
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,15 @@ void main() {
722722
check(findInMessageList('topic name')).length.equals(1);
723723
});
724724

725+
testWidgets('show channel name in StarredMessagesNarrow', (tester) async {
726+
await setupMessageListPage(tester,
727+
narrow: const StarredMessagesNarrow(),
728+
messages: [message], subscriptions: [eg.subscription(stream)]);
729+
await tester.pump();
730+
check(findInMessageList('stream name')).length.equals(1);
731+
check(findInMessageList('topic name')).length.equals(1);
732+
});
733+
725734
testWidgets('do not show channel name in ChannelNarrow', (tester) async {
726735
await setupMessageListPage(tester,
727736
narrow: ChannelNarrow(stream.streamId),

0 commit comments

Comments
 (0)