Skip to content

Commit 05db60b

Browse files
committed
autocomplete: Add "recent DM conversations" criterion
In @-mention autocomplete, users are suggested based on: 1. Recent DM conversations. Fixes part of: #228
1 parent 8e80db8 commit 05db60b

File tree

2 files changed

+220
-4
lines changed

2 files changed

+220
-4
lines changed

lib/model/autocomplete.dart

+43-3
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,53 @@ class MentionAutocompleteView extends ChangeNotifier {
261261

262262
List<User>? _sortedUsers;
263263

264-
List<User> sortByRelevance({required List<User> users}) {
265-
return users; // TODO(#228) sort for most relevant first
264+
/// Determines which of the two users is more recent in DM conversations.
265+
///
266+
/// Returns a negative number if [userA] is more recent than [userB],
267+
/// returns a positive number if [userB] is more recent than [userA],
268+
/// and returns `0` if both [userA] and [userB] are equally recent
269+
/// or there is no DM exchanged with them whatsoever.
270+
int compareByDms(User userA, User userB) {
271+
final recentDms = store.recentDmConversationsView;
272+
final aLatestMessageId = recentDms.latestMessagesByRecipient[userA.userId];
273+
final bLatestMessageId = recentDms.latestMessagesByRecipient[userB.userId];
274+
275+
return switch((aLatestMessageId, bLatestMessageId)) {
276+
(int a, int b) => -a.compareTo(b),
277+
(int(), _) => -1,
278+
(_, int()) => 1,
279+
_ => 0,
280+
};
281+
}
282+
283+
int compareByRelevance({
284+
required User userA,
285+
required User userB,
286+
}) {
287+
final dmPrecedence = compareByDms(userA, userB);
288+
return dmPrecedence;
289+
}
290+
291+
List<User> sortByRelevance({
292+
required List<User> users,
293+
required Narrow narrow,
294+
}) {
295+
switch (narrow) {
296+
case StreamNarrow():
297+
case TopicNarrow():
298+
case DmNarrow():
299+
users.sort((userA, userB) => compareByRelevance(
300+
userA: userA,
301+
userB: userB));
302+
case CombinedFeedNarrow():
303+
// do nothing in this case for now
304+
}
305+
return users;
266306
}
267307

268308
void _sortUsers() {
269309
final users = store.users.values.toList();
270-
_sortedUsers = sortByRelevance(users: users);
310+
_sortedUsers = sortByRelevance(users: users, narrow: narrow);
271311
}
272312

273313
Future<List<MentionAutocompleteResult>?> _computeResults(MentionAutocompleteQuery query) async {

test/model/autocomplete_test.dart

+177-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:checks/checks.dart';
55
import 'package:fake_async/fake_async.dart';
66
import 'package:flutter/cupertino.dart';
77
import 'package:test/scaffolding.dart';
8+
import 'package:zulip/api/model/initial_snapshot.dart';
89
import 'package:zulip/api/model/model.dart';
910
import 'package:zulip/model/autocomplete.dart';
1011
import 'package:zulip/model/narrow.dart';
@@ -277,12 +278,12 @@ void main() {
277278

278279
group('MentionAutocompleteView recomputes results and does it only one time', () {
279280
Future<void> doCheck({
281+
Narrow narrow = const CombinedFeedNarrow(),
280282
required List<User> users,
281283
required String rawQuery,
282284
required Future<void> Function(PerAccountStore store) act,
283285
required void Function(Iterable<MentionAutocompleteResult>) expect,
284286
}) async {
285-
const narrow = CombinedFeedNarrow();
286287
final store = eg.store();
287288
await store.addUsers(users);
288289
final view = MentionAutocompleteView.init(store: store, narrow: narrow);
@@ -382,6 +383,28 @@ void main() {
382383
expect: expect,
383384
);
384385
});
386+
387+
test('MessageEvent', () async {
388+
final users = generateUsers(count: 1500);
389+
390+
Future<void> act(PerAccountStore store) async {
391+
await store.addMessage(eg.dmMessage(from: eg.selfUser, to: [users[100]]));
392+
}
393+
394+
void expect(Iterable<MentionAutocompleteResult> results) {
395+
check(results.elementAt(0))
396+
.isA<UserMentionAutocompleteResult>()
397+
.userId.equals(100);
398+
}
399+
400+
await doCheck(
401+
narrow: DmNarrow.withUser(100, selfUserId: eg.selfUser.userId),
402+
users: users,
403+
rawQuery: '',
404+
act: act,
405+
expect: expect,
406+
);
407+
});
385408
});
386409

387410
group('MentionAutocompleteQuery.testUser', () {
@@ -428,4 +451,157 @@ void main() {
428451
doCheck('Four F', eg.user(fullName: 'Full Name Four Words'), false);
429452
});
430453
});
454+
455+
group('MentionAutocompleteView users results', () {
456+
late PerAccountStore store;
457+
late MentionAutocompleteView view;
458+
459+
Future<void> prepare({
460+
required List<User> users,
461+
required List<RecentDmConversation> dmConversations,
462+
required Narrow narrow,
463+
}) async {
464+
store = eg.store(initialSnapshot: eg.initialSnapshot(
465+
recentPrivateConversations: dmConversations));
466+
await store.addUsers(users);
467+
view = MentionAutocompleteView.init(store: store, narrow: narrow);
468+
}
469+
470+
group('MentionAutocompleteView.compareByDms', () {
471+
test('has DMs with userA and userB, latest with userA, prioritizes userA', () async {
472+
await prepare(
473+
users: [],
474+
dmConversations: [
475+
RecentDmConversation(userIds: [1], maxMessageId: 200),
476+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
477+
],
478+
narrow: const CombinedFeedNarrow(),
479+
);
480+
481+
final userA = eg.user(userId: 1);
482+
final userB = eg.user(userId: 2);
483+
final compareAB = view.compareByDms(userA, userB);
484+
check(compareAB).isNegative();
485+
});
486+
487+
test('has DMs with userA and userB, latest with userB, prioritizes userB', () async {
488+
await prepare(
489+
users: [],
490+
dmConversations: [
491+
RecentDmConversation(userIds: [2], maxMessageId: 200),
492+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
493+
],
494+
narrow: const CombinedFeedNarrow(),
495+
);
496+
497+
final userA = eg.user(userId: 1);
498+
final userB = eg.user(userId: 2);
499+
final compareAB = view.compareByDms(userA, userB);
500+
check(compareAB).isGreaterThan(0);
501+
});
502+
503+
test('has DMs with userA and userB, equally recent, prioritizes neither', () async {
504+
await prepare(
505+
users: [],
506+
dmConversations: [
507+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
508+
],
509+
narrow: const CombinedFeedNarrow(),
510+
);
511+
512+
final userA = eg.user(userId: 1);
513+
final userB = eg.user(userId: 2);
514+
final compareAB = view.compareByDms(userA, userB);
515+
check(compareAB).equals(0);
516+
});
517+
518+
test('has DMs with userA but not userB, prioritizes userA', () async {
519+
await prepare(
520+
users: [],
521+
dmConversations: [
522+
RecentDmConversation(userIds: [1], maxMessageId: 100),
523+
],
524+
narrow: const CombinedFeedNarrow(),
525+
);
526+
527+
final userA = eg.user(userId: 1);
528+
final userB = eg.user(userId: 2);
529+
final compareAB = view.compareByDms(userA, userB);
530+
check(compareAB).isNegative();
531+
});
532+
533+
test('has DMs with userB but not userA, prioritizes userB', () async {
534+
await prepare(
535+
users: [],
536+
dmConversations: [
537+
RecentDmConversation(userIds: [2], maxMessageId: 100),
538+
],
539+
narrow: const CombinedFeedNarrow(),
540+
);
541+
542+
final userA = eg.user(userId: 1);
543+
final userB = eg.user(userId: 2);
544+
final compareAB = view.compareByDms(userA, userB);
545+
check(compareAB).isGreaterThan(0);
546+
});
547+
548+
test('doesn\'t have DMs with userA or userB, prioritizes neither', () async {
549+
await prepare(
550+
users: [],
551+
dmConversations: [],
552+
narrow: const CombinedFeedNarrow(),
553+
);
554+
555+
final userA = eg.user(userId: 1);
556+
final userB = eg.user(userId: 2);
557+
final compareAB = view.compareByDms(userA, userB);
558+
check(compareAB).equals(0);
559+
});
560+
});
561+
562+
test('autocomplete suggests relevant users in the following order: '
563+
'1. Users most recent in the DM conversations', () async {
564+
final users = [
565+
eg.user(userId: 1),
566+
eg.user(userId: 2),
567+
eg.user(userId: 3),
568+
eg.user(userId: 4),
569+
eg.user(userId: 5),
570+
];
571+
572+
final dmConversations = [
573+
RecentDmConversation(userIds: [4], maxMessageId: 300),
574+
RecentDmConversation(userIds: [1], maxMessageId: 200),
575+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
576+
];
577+
578+
Future<void> checkResultsIn(Narrow narrow, {required List<int> expected}) async {
579+
await prepare(users: users, dmConversations: dmConversations, narrow: narrow);
580+
581+
bool done = false;
582+
view.addListener(() { done = true; });
583+
view.query = MentionAutocompleteQuery('');
584+
await Future(() {});
585+
check(done).isTrue();
586+
final results = view.results
587+
.map((e) => (e as UserMentionAutocompleteResult).userId)
588+
.toList();
589+
check(results).deepEquals(expected);
590+
}
591+
592+
const streamNarrow = StreamNarrow(1);
593+
await checkResultsIn(streamNarrow, expected: [4, 1, 2, 3, 5]);
594+
595+
const topicNarrow = TopicNarrow(1, 'topic');
596+
await checkResultsIn(topicNarrow, expected: [4, 1, 2, 3, 5]);
597+
598+
final dmNarrow = DmNarrow(allRecipientIds: [eg.selfUser.userId], selfUserId: eg.selfUser.userId);
599+
await checkResultsIn(dmNarrow, expected: [4, 1, 2, 3, 5]);
600+
601+
const allMessagesNarrow = CombinedFeedNarrow();
602+
// Results are in the original order as we do not sort them for
603+
// [AllMessagesNarrow] because we can not access autocomplete for now.
604+
await checkResultsIn(allMessagesNarrow, expected: [1, 2, 3, 4, 5]);
605+
});
606+
});
431607
}

0 commit comments

Comments
 (0)