Skip to content

Commit 08fd062

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 fc9147f commit 08fd062

File tree

2 files changed

+184
-4
lines changed

2 files changed

+184
-4
lines changed

lib/model/autocomplete.dart

+45-4
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ class MentionAutocompleteView extends ChangeNotifier {
175175
required Narrow narrow,
176176
}) {
177177
final users = store.users.values.toList();
178-
final sortedUsers = sortByRelevance(users: users);
178+
final sortedUsers = sortByRelevance(users: users, narrow: narrow, store: store);
179179
final view = MentionAutocompleteView._(
180180
store: store,
181181
narrow: narrow,
@@ -184,9 +184,50 @@ class MentionAutocompleteView extends ChangeNotifier {
184184
store.autocompleteViewManager.registerMentionAutocomplete(view);
185185
return view;
186186
}
187-
188-
static List<User> sortByRelevance({required List<User> users}) {
189-
return users; // TODO(#228) sort for most relevant first
187+
188+
static List<User> sortByRelevance({
189+
required List<User> users,
190+
required Narrow narrow,
191+
required PerAccountStore store,
192+
}) {
193+
switch (narrow) {
194+
case StreamNarrow():
195+
case TopicNarrow():
196+
case DmNarrow():
197+
users.sort((userA, userB) => compareByRelevance(
198+
userA: userA, userB: userB, store: store));
199+
case CombinedFeedNarrow():
200+
// do nothing in this case for now
201+
}
202+
return users;
203+
}
204+
205+
static int compareByRelevance({
206+
required User userA,
207+
required User userB,
208+
required PerAccountStore store,
209+
}) {
210+
final dmPrecedence = compareByDms(userA, userB, store: store);
211+
return dmPrecedence;
212+
}
213+
214+
/// Determines which of the two users is more recent in DM conversations.
215+
///
216+
/// Returns a negative number if [userA] is more recent than [userB],
217+
/// returns a positive number if [userB] is more recent than [userA],
218+
/// and returns `0` if both [userA] and [userB] are equally recent
219+
/// or there is no DM exchanged with them whatsoever.
220+
static int compareByDms(User userA, User userB, {required PerAccountStore store}) {
221+
final recentDms = store.recentDmConversationsView;
222+
final aLatestMessageId = recentDms.latestMessagesByRecipient[userA.userId];
223+
final bLatestMessageId = recentDms.latestMessagesByRecipient[userB.userId];
224+
225+
return switch((aLatestMessageId, bLatestMessageId)) {
226+
(int a, int b) => -a.compareTo(b),
227+
(int(), _) => -1,
228+
(_, int()) => 1,
229+
_ => 0,
230+
};
190231
}
191232

192233
@override

test/model/autocomplete_test.dart

+139
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ 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';
12+
import 'package:zulip/model/store.dart';
1113
import 'package:zulip/widgets/compose_box.dart';
1214

1315
import '../example_data.dart' as eg;
@@ -318,4 +320,141 @@ void main() {
318320
doCheck('Four F', eg.user(fullName: 'Full Name Four Words'), false);
319321
});
320322
});
323+
324+
group('MentionAutocompleteView sorting users results', () {
325+
late PerAccountStore store;
326+
327+
Future<void> prepare({
328+
List<User> users = const [],
329+
List<RecentDmConversation> dmConversations = const [],
330+
}) async {
331+
store = eg.store(initialSnapshot: eg.initialSnapshot(
332+
recentPrivateConversations: dmConversations));
333+
await store.addUsers(users);
334+
}
335+
336+
group('MentionAutocompleteView.compareByDms', () {
337+
test('has DMs with userA and userB, latest with userA, prioritizes userA', () async {
338+
await prepare(
339+
dmConversations: [
340+
RecentDmConversation(userIds: [1], maxMessageId: 200),
341+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
342+
],
343+
);
344+
345+
final userA = eg.user(userId: 1);
346+
final userB = eg.user(userId: 2);
347+
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store);
348+
check(compareAB).isNegative();
349+
});
350+
351+
test('has DMs with userA and userB, latest with userB, prioritizes userB', () async {
352+
await prepare(
353+
dmConversations: [
354+
RecentDmConversation(userIds: [2], maxMessageId: 200),
355+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
356+
],
357+
);
358+
359+
final userA = eg.user(userId: 1);
360+
final userB = eg.user(userId: 2);
361+
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store);
362+
check(compareAB).isGreaterThan(0);
363+
});
364+
365+
test('has DMs with userA and userB, equally recent, prioritizes neither', () async {
366+
await prepare(
367+
dmConversations: [
368+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
369+
],
370+
);
371+
372+
final userA = eg.user(userId: 1);
373+
final userB = eg.user(userId: 2);
374+
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store);
375+
check(compareAB).equals(0);
376+
});
377+
378+
test('has DMs with userA but not userB, prioritizes userA', () async {
379+
await prepare(
380+
dmConversations: [
381+
RecentDmConversation(userIds: [1], maxMessageId: 100),
382+
],
383+
);
384+
385+
final userA = eg.user(userId: 1);
386+
final userB = eg.user(userId: 2);
387+
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store);
388+
check(compareAB).isNegative();
389+
});
390+
391+
test('has DMs with userB but not userA, prioritizes userB', () async {
392+
await prepare(
393+
dmConversations: [
394+
RecentDmConversation(userIds: [2], maxMessageId: 100),
395+
],
396+
);
397+
398+
final userA = eg.user(userId: 1);
399+
final userB = eg.user(userId: 2);
400+
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store);
401+
check(compareAB).isGreaterThan(0);
402+
});
403+
404+
test('doesn\'t have DMs with userA or userB, prioritizes neither', () async {
405+
await prepare(dmConversations: []);
406+
407+
final userA = eg.user(userId: 1);
408+
final userB = eg.user(userId: 2);
409+
final compareAB = MentionAutocompleteView.compareByDms(userA, userB, store: store);
410+
check(compareAB).equals(0);
411+
});
412+
});
413+
414+
test('autocomplete suggests relevant users in the following order: '
415+
'1. Users most recent in the DM conversations', () async {
416+
final users = [
417+
eg.user(userId: 1),
418+
eg.user(userId: 2),
419+
eg.user(userId: 3),
420+
eg.user(userId: 4),
421+
eg.user(userId: 5),
422+
];
423+
424+
final dmConversations = [
425+
RecentDmConversation(userIds: [4], maxMessageId: 300),
426+
RecentDmConversation(userIds: [1], maxMessageId: 200),
427+
RecentDmConversation(userIds: [1, 2], maxMessageId: 100),
428+
];
429+
430+
Future<void> checkResultsIn(Narrow narrow, {required List<int> expected}) async {
431+
await prepare(users: users, dmConversations: dmConversations);
432+
final view = MentionAutocompleteView.init(store: store, narrow: narrow);
433+
434+
bool done = false;
435+
view.addListener(() { done = true; });
436+
view.query = MentionAutocompleteQuery('');
437+
await Future(() {});
438+
check(done).isTrue();
439+
final results = view.results
440+
.map((e) => (e as UserMentionAutocompleteResult).userId)
441+
.toList();
442+
check(results).deepEquals(expected);
443+
}
444+
445+
const streamNarrow = StreamNarrow(1);
446+
await checkResultsIn(streamNarrow, expected: [4, 1, 2, 3, 5]);
447+
448+
const topicNarrow = TopicNarrow(1, 'topic');
449+
await checkResultsIn(topicNarrow, expected: [4, 1, 2, 3, 5]);
450+
451+
final dmNarrow = DmNarrow(allRecipientIds: [eg.selfUser.userId], selfUserId: eg.selfUser.userId);
452+
await checkResultsIn(dmNarrow, expected: [4, 1, 2, 3, 5]);
453+
454+
const allMessagesNarrow = CombinedFeedNarrow();
455+
// Results are in the original order as we do not sort them for
456+
// [CombinedFeedNarrow] because we can not access autocomplete for now.
457+
await checkResultsIn(allMessagesNarrow, expected: [1, 2, 3, 4, 5]);
458+
});
459+
});
321460
}

0 commit comments

Comments
 (0)