Skip to content

Commit cf3737d

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 ecdb164 commit cf3737d

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', () {
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);
@@ -380,6 +381,28 @@ void main() {
380381
expect: expect,
381382
);
382383
});
384+
385+
test('MessageEvent', () async {
386+
final users = generateUsers(count: 1500);
387+
388+
Future<void> act(PerAccountStore store) async {
389+
await store.addMessage(eg.dmMessage(from: eg.selfUser, to: [users[100]]));
390+
}
391+
392+
void expect(Iterable<MentionAutocompleteResult> results) {
393+
check(results.elementAt(0))
394+
.isA<UserMentionAutocompleteResult>()
395+
.userId.equals(100);
396+
}
397+
398+
await doCheck(
399+
narrow: DmNarrow.withUser(100, selfUserId: eg.selfUser.userId),
400+
users: users,
401+
rawQuery: '',
402+
act: act,
403+
expect: expect,
404+
);
405+
});
383406
});
384407

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

0 commit comments

Comments
 (0)