Skip to content

Commit 13685e6

Browse files
feat(contactsmenu): Sort by user status
If user_status is not enabled, fall back to sorting by contact name. Signed-off-by: Christoph Wurst <[email protected]>
1 parent 1acc7c0 commit 13685e6

File tree

8 files changed

+234
-13
lines changed

8 files changed

+234
-13
lines changed

apps/user_status/lib/ContactsMenu/StatusProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function process(array $entries): void {
4444
);
4545

4646
$statuses = $this->statusService->findByUserIds($uids);
47+
/** @var array<string, UserStatus> $indexed */
4748
$indexed = array_combine(
4849
array_map(fn(UserStatus $status) => $status->getUserId(), $statuses),
4950
$statuses
@@ -56,6 +57,7 @@ public function process(array $entries): void {
5657
$entry->setStatus(
5758
$status->getStatus(),
5859
$status->getCustomMessage(),
60+
$status->getStatusMessageTimestamp(),
5961
$status->getCustomIcon(),
6062
);
6163
}

lib/private/Contacts/ContactsMenu/ContactsStore.php

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
use OC\KnownUser\KnownUserService;
3535
use OC\Profile\ProfileManager;
36+
use OCA\UserStatus\Db\UserStatus;
37+
use OCA\UserStatus\Service\StatusService;
3638
use OCP\Contacts\ContactsMenu\IContactsStore;
3739
use OCP\Contacts\ContactsMenu\IEntry;
3840
use OCP\Contacts\IManager;
@@ -42,10 +44,17 @@
4244
use OCP\IUser;
4345
use OCP\IUserManager;
4446
use OCP\L10N\IFactory as IL10NFactory;
47+
use function array_column;
48+
use function array_fill_keys;
49+
use function array_filter;
50+
use function array_key_exists;
51+
use function array_merge;
52+
use function count;
4553

4654
class ContactsStore implements IContactsStore {
4755
public function __construct(
4856
private IManager $contactsManager,
57+
private ?StatusService $userStatusService,
4958
private IConfig $config,
5059
private ProfileManager $profileManager,
5160
private IUserManager $userManager,
@@ -70,15 +79,75 @@ public function getContacts(IUser $user, ?string $filter, ?int $limit = null, ?i
7079
if ($offset !== null) {
7180
$options['offset'] = $offset;
7281
}
82+
// Status integration only works without pagination and filters
83+
if ($offset === null && ($filter === null || $filter === '')) {
84+
$recentStatuses = $this->userStatusService?->findAllRecentStatusChanges($limit, $offset) ?? [];
85+
} else {
86+
$recentStatuses = [];
87+
}
7388

74-
$allContacts = $this->contactsManager->search(
75-
$filter ?? '',
76-
[
77-
'FN',
78-
'EMAIL'
79-
],
80-
$options
81-
);
89+
// Search by status if there is no filter and statuses are available
90+
if (!empty($recentStatuses)) {
91+
$allContacts = array_filter(array_map(function(UserStatus $userStatus) use ($options) {
92+
// UID is ambiguous with federation. We have to use the federated cloud ID to an exact match of
93+
// A local user
94+
$user = $this->userManager->get($userStatus->getUserId());
95+
if ($user === null) {
96+
return null;
97+
}
98+
99+
$contact = $this->contactsManager->search(
100+
$user->getCloudId(),
101+
[
102+
'CLOUD',
103+
],
104+
array_merge(
105+
$options,
106+
[
107+
'limit' => 1,
108+
'offset' => 0,
109+
],
110+
),
111+
)[0] ?? null;
112+
if ($contact !== null) {
113+
$contact[Entry::PROPERTY_STATUS_MESSAGE_TIMESTAMP] = $userStatus->getStatusMessageTimestamp();
114+
}
115+
return $contact;
116+
}, $recentStatuses));
117+
if ($limit !== null && count($allContacts) < $limit) {
118+
// More contacts were requested
119+
$fromContacts = $this->contactsManager->search(
120+
$filter ?? '',
121+
[
122+
'FN',
123+
'EMAIL'
124+
],
125+
array_merge(
126+
$options,
127+
[
128+
'limit' => $limit - count($allContacts),
129+
],
130+
),
131+
);
132+
133+
// Create hash map of all status contacts
134+
$existing = array_fill_keys(array_column($allContacts, 'URI'), null);
135+
// Append the ones that are new
136+
$allContacts = array_merge(
137+
$allContacts,
138+
array_filter($fromContacts, fn(array $contact): bool => !array_key_exists($contact['URI'], $existing))
139+
);
140+
}
141+
} else {
142+
$allContacts = $this->contactsManager->search(
143+
$filter ?? '',
144+
[
145+
'FN',
146+
'EMAIL'
147+
],
148+
$options
149+
);
150+
}
82151

83152
$userId = $user->getUID();
84153
$contacts = array_filter($allContacts, function ($contact) use ($userId) {

lib/private/Contacts/ContactsMenu/Entry.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
use function array_merge;
3333

3434
class Entry implements IEntry {
35+
public const PROPERTY_STATUS_MESSAGE_TIMESTAMP = 'statusMessageTimestamp';
36+
3537
/** @var string|int|null */
3638
private $id = null;
3739

@@ -53,6 +55,7 @@ class Entry implements IEntry {
5355

5456
private ?string $status = null;
5557
private ?string $statusMessage = null;
58+
private ?int $statusMessageTimestamp = null;
5659
private ?string $statusIcon = null;
5760

5861
public function setId(string $id): void {
@@ -109,9 +112,11 @@ public function addAction(IAction $action): void {
109112

110113
public function setStatus(string $status,
111114
string $statusMessage = null,
115+
int $statusMessageTimestamp = null,
112116
string $icon = null): void {
113117
$this->status = $status;
114118
$this->statusMessage = $statusMessage;
119+
$this->statusMessageTimestamp = $statusMessageTimestamp;
115120
$this->statusIcon = $icon;
116121
}
117122

@@ -159,7 +164,7 @@ public function getProperty(string $key): mixed {
159164
}
160165

161166
/**
162-
* @return array{id: int|string|null, fullName: string, avatar: string|null, topAction: mixed, actions: array, lastMessage: '', emailAddresses: string[], profileTitle: string|null, profileUrl: string|null, status: string|null, statusMessage: null|string, statusIcon: null|string, isUser: bool, uid: mixed}
167+
* @return array{id: int|string|null, fullName: string, avatar: string|null, topAction: mixed, actions: array, lastMessage: '', emailAddresses: string[], profileTitle: string|null, profileUrl: string|null, status: string|null, statusMessage: null|string, statusMessageTimestamp: null|int, statusIcon: null|string, isUser: bool, uid: mixed}
163168
*/
164169
public function jsonSerialize(): array {
165170
$topAction = !empty($this->actions) ? $this->actions[0]->jsonSerialize() : null;
@@ -179,9 +184,18 @@ public function jsonSerialize(): array {
179184
'profileUrl' => $this->profileUrl,
180185
'status' => $this->status,
181186
'statusMessage' => $this->statusMessage,
187+
'statusMessageTimestamp' => $this->statusMessageTimestamp,
182188
'statusIcon' => $this->statusIcon,
183189
'isUser' => $this->getProperty('isUser') === true,
184190
'uid' => $this->getProperty('UID'),
185191
];
186192
}
193+
194+
public function getStatusMessage(): ?string {
195+
return $this->statusMessage;
196+
}
197+
198+
public function getStatusMessageTimestamp(): ?int {
199+
return $this->statusMessageTimestamp;
200+
}
187201
}

lib/private/Contacts/ContactsMenu/Manager.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,19 @@ public function findOne(IUser $user, int $shareType, string $shareWith): ?IEntry
8282
* @return IEntry[]
8383
*/
8484
private function sortEntries(array $entries): array {
85-
usort($entries, function (IEntry $entryA, IEntry $entryB) {
86-
return strcasecmp($entryA->getFullName(), $entryB->getFullName());
85+
usort($entries, function (Entry $entryA, Entry $entryB) {
86+
$aStatusTimestamp = $entryA->getProperty(Entry::PROPERTY_STATUS_MESSAGE_TIMESTAMP);
87+
$bStatusTimestamp = $entryB->getProperty(Entry::PROPERTY_STATUS_MESSAGE_TIMESTAMP);
88+
if (!$aStatusTimestamp && !$bStatusTimestamp) {
89+
return strcasecmp($entryA->getFullName(), $entryB->getFullName());
90+
}
91+
if ($aStatusTimestamp === null) {
92+
return 1;
93+
}
94+
if ($bStatusTimestamp === null) {
95+
return -1;
96+
}
97+
return $bStatusTimestamp - $aStatusTimestamp;
8798
});
8899
return $entries;
89100
}

lib/public/Contacts/ContactsMenu/IEntry.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public function addAction(IAction $action): void;
6464
*/
6565
public function setStatus(string $status,
6666
string $statusMessage = null,
67+
int $statusMessageTimestamp = null,
6768
string $icon = null): void;
6869

6970
/**

tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<?php
2+
3+
declare(strict_types=1);
4+
25
/**
36
* @copyright 2017 Christoph Wurst <[email protected]>
47
* @copyright 2017 Lukas Reschke <[email protected]>
@@ -28,6 +31,8 @@
2831
use OC\Contacts\ContactsMenu\ContactsStore;
2932
use OC\KnownUser\KnownUserService;
3033
use OC\Profile\ProfileManager;
34+
use OCA\UserStatus\Db\UserStatus;
35+
use OCA\UserStatus\Service\StatusService;
3136
use OCP\Contacts\IManager;
3237
use OCP\IConfig;
3338
use OCP\IGroupManager;
@@ -40,6 +45,7 @@
4045

4146
class ContactsStoreTest extends TestCase {
4247
private ContactsStore $contactsStore;
48+
private StatusService|MockObject $statusService;
4349
/** @var IManager|MockObject */
4450
private $contactsManager;
4551
/** @var ProfileManager */
@@ -61,6 +67,7 @@ protected function setUp(): void {
6167
parent::setUp();
6268

6369
$this->contactsManager = $this->createMock(IManager::class);
70+
$this->statusService = $this->createMock(StatusService::class);
6471
$this->userManager = $this->createMock(IUserManager::class);
6572
$this->profileManager = $this->createMock(ProfileManager::class);
6673
$this->urlGenerator = $this->createMock(IURLGenerator::class);
@@ -70,13 +77,14 @@ protected function setUp(): void {
7077
$this->l10nFactory = $this->createMock(IL10NFactory::class);
7178
$this->contactsStore = new ContactsStore(
7279
$this->contactsManager,
80+
$this->statusService,
7381
$this->config,
7482
$this->profileManager,
7583
$this->userManager,
7684
$this->urlGenerator,
7785
$this->groupManager,
7886
$this->knownUserService,
79-
$this->l10nFactory
87+
$this->l10nFactory,
8088
);
8189
}
8290

@@ -964,4 +972,118 @@ public function testFindOneNoMatches() {
964972

965973
$this->assertEquals(null, $entry);
966974
}
975+
976+
public function testGetRecentStatusFirst(): void {
977+
$user = $this->createMock(IUser::class);
978+
$status1 = new UserStatus();
979+
$status1->setUserId('user1');
980+
$status2 = new UserStatus();
981+
$status2->setUserId('user2');
982+
$this->statusService->expects(self::once())
983+
->method('findAllRecentStatusChanges')
984+
->willReturn([
985+
$status1,
986+
$status2,
987+
]);
988+
$user1 = $this->createMock(IUser::class);
989+
$user1->method('getCloudId')->willReturn('user1@localcloud');
990+
$user2 = $this->createMock(IUser::class);
991+
$user2->method('getCloudId')->willReturn('user2@localcloud');
992+
$this->userManager->expects(self::exactly(2))
993+
->method('get')
994+
->willReturnCallback(function($uid) use ($user1, $user2) {
995+
return match($uid) {
996+
'user1' => $user1,
997+
'user2' => $user2,
998+
};
999+
});
1000+
$this->contactsManager
1001+
->expects(self::exactly(3))
1002+
->method('search')
1003+
->willReturnCallback(function($uid, $searchProps, $options) {
1004+
return match ([$uid, $options['limit'] ?? null]) {
1005+
['user1@localcloud', 1] => [
1006+
[
1007+
'UID' => 'user1',
1008+
'URI' => 'user1.vcf',
1009+
],
1010+
],
1011+
['user2@localcloud' => [], 1], // Simulate not found
1012+
['', 4] => [
1013+
[
1014+
'UID' => 'contact1',
1015+
'URI' => 'contact1.vcf',
1016+
],
1017+
[
1018+
'UID' => 'contact2',
1019+
'URI' => 'contact2.vcf',
1020+
],
1021+
],
1022+
default => [],
1023+
};
1024+
});
1025+
1026+
$contacts = $this->contactsStore->getContacts(
1027+
$user,
1028+
null,
1029+
5,
1030+
);
1031+
1032+
self::assertCount(3, $contacts);
1033+
self::assertEquals('user1', $contacts[0]->getProperty('UID'));
1034+
self::assertEquals('contact1', $contacts[1]->getProperty('UID'));
1035+
self::assertEquals('contact2', $contacts[2]->getProperty('UID'));
1036+
}
1037+
1038+
public function testPaginateRecentStatus(): void {
1039+
$user = $this->createMock(IUser::class);
1040+
$status1 = new UserStatus();
1041+
$status1->setUserId('user1');
1042+
$status2 = new UserStatus();
1043+
$status2->setUserId('user2');
1044+
$status3 = new UserStatus();
1045+
$status3->setUserId('user3');
1046+
$this->statusService->expects(self::never())
1047+
->method('findAllRecentStatusChanges');
1048+
$this->contactsManager
1049+
->expects(self::exactly(2))
1050+
->method('search')
1051+
->willReturnCallback(function($uid, $searchProps, $options) {
1052+
return match ([$uid, $options['limit'] ?? null, $options['offset'] ?? null]) {
1053+
['', 2, 0] => [
1054+
[
1055+
'UID' => 'contact1',
1056+
'URI' => 'contact1.vcf',
1057+
],
1058+
[
1059+
'UID' => 'contact2',
1060+
'URI' => 'contact2.vcf',
1061+
],
1062+
],
1063+
['', 2, 3] => [
1064+
[
1065+
'UID' => 'contact3',
1066+
'URI' => 'contact3.vcf',
1067+
],
1068+
],
1069+
default => [],
1070+
};
1071+
});
1072+
1073+
$page1 = $this->contactsStore->getContacts(
1074+
$user,
1075+
null,
1076+
2,
1077+
0,
1078+
);
1079+
$page2 = $this->contactsStore->getContacts(
1080+
$user,
1081+
null,
1082+
2,
1083+
3,
1084+
);
1085+
1086+
self::assertCount(2, $page1);
1087+
self::assertCount(1, $page2);
1088+
}
9671089
}

tests/lib/Contacts/ContactsMenu/EntryTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public function testJsonSerialize() {
105105
'profileUrl' => null,
106106
'status' => null,
107107
'statusMessage' => null,
108+
'statusMessageTimestamp' => null,
108109
'statusIcon' => null,
109110
'isUser' => false,
110111
'uid' => null,

0 commit comments

Comments
 (0)