Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
use OCA\Talk\RoomPresets\Forced;
use OCA\Talk\RoomPresets\Parameter;
use OCA\Talk\RoomPresets\VoiceRoom;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\BanService;
use OCA\Talk\Service\BreakoutRoomService;
use OCA\Talk\Service\ChecksumVerificationService;
Expand Down Expand Up @@ -151,6 +152,7 @@ public function __construct(
private readonly ITimeFactory $timeFactory,
private readonly ChecksumVerificationService $checksumVerificationService,
private readonly RoomFormatter $roomFormatter,
private readonly AvatarService $avatarService,
private readonly Preloader $sharePreloader,
private readonly IConfig $config,
private readonly IAppConfig $appConfig,
Expand Down Expand Up @@ -1321,22 +1323,25 @@ protected function formatParticipantList(array $participants, bool $includeStatu
$results = $headers = $statuses = [];
$maxPingAge = $this->timeFactory->getTime() - Session::SESSION_TIMEOUT_KILL;

// One participant per session, so a user on several devices repeats.
$userIds = array_values(array_unique(array_filter(array_map(static function (Participant $participant): ?string {
if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
return $participant->getAttendee()->getActorId();
}
return null;
}, $participants))));

if ($this->userId !== null
&& $includeStatus
&& count($participants) < Config::USER_STATUS_INTEGRATION_LIMIT
&& $this->appManager->isEnabledForUser('user_status')) {
$userIds = array_filter(array_map(static function (Participant $participant): ?string {
if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
return $participant->getAttendee()->getActorId();
}
return null;
}, $participants));

$statuses = $this->statusManager->getUserStatuses($userIds);

$headers['X-Nextcloud-Has-User-Statuses'] = true;
}

$avatarVersions = $this->avatarService->getUserAvatarVersions($userIds);

$currentUser = null;
if ($this->userId !== null) {
$currentUser = $this->userManager->get($this->userId);
Expand Down Expand Up @@ -1409,6 +1414,9 @@ protected function formatParticipantList(array $participants, bool $includeStatu
$this->participantService->leaveRoomAsSession($this->room, $participant);
}

if (isset($avatarVersions[$userId])) {
$result['actorAvatarVersion'] = $avatarVersions[$userId];
}
$result['displayName'] = $participant->getAttendee()->getDisplayName();
if (!$result['displayName']) {
$userDisplayName = $this->userManager->getDisplayName($userId);
Expand Down
2 changes: 2 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@
* roomToken: string,
* // Array of session ids, each are up to 512 character long strings, or empty if no session
* sessionIds: list<string>,
* // Version of the actor's avatar, only for users. Passing it to the avatar endpoint allows a much longer cache lifetime
* actorAvatarVersion?: string,
* // Only available with `includeStatus=true`, for users with a set status and when there are less than 100 participants in the conversation
* status?: string,
* // Only available with `includeStatus=true`, for users with a set status and when there are less than 100 participants in the conversation
Expand Down
26 changes: 26 additions & 0 deletions lib/Service/AvatarService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use InvalidArgumentException;
use OCA\Talk\Room;
use OCA\Talk\RoomAttributes;
use OCP\Config\IUserConfig;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\InMemoryFile;
Expand All @@ -30,6 +31,7 @@ class AvatarService {

public function __construct(
private readonly IAppData $appData,
private readonly IUserConfig $userConfig,
private readonly IL10N $l,
private readonly IURLGenerator $url,
private readonly ISecureRandom $random,
Expand Down Expand Up @@ -314,6 +316,30 @@ public function getAvatarUrl(Room $room): string {
return $this->url->linkToOCSRouteAbsolute('spreed.Avatar.getAvatar', $arguments);
}

/**
* Users who never set an avatar have no stored version and get "0", which is
* their real version and becomes 1 the first time they upload one.
*
* Issues one query per 50 ids.
*
* @param list<string> $userIds
* @return array<string, string>
*/
public function getUserAvatarVersions(array $userIds): array {
if ($userIds === []) {
return [];
}

$versions = $this->userConfig->getValuesByUsers('avatar', 'version', userIds: $userIds);

$result = [];
foreach ($userIds as $userId) {
$result[$userId] = (string)($versions[$userId] ?? 0);
}

return $result;
}

public function getAvatarVersion(Room $room): string {
$avatarVersion = $room->getAvatar();
if ($avatarVersion) {
Expand Down
4 changes: 4 additions & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,10 @@
"type": "string"
}
},
"actorAvatarVersion": {
"type": "string",
"description": "Version of the actor's avatar, only for users. Passing it to the avatar endpoint allows a much longer cache lifetime"
},
"status": {
"type": "string",
"description": "Only available with `includeStatus=true`, for users with a set status and when there are less than 100 participants in the conversation"
Expand Down
4 changes: 4 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,10 @@
"type": "string"
}
},
"actorAvatarVersion": {
"type": "string",
"description": "Version of the actor's avatar, only for users. Passing it to the avatar endpoint allows a much longer cache lifetime"
},
"status": {
"type": "string",
"description": "Only available with `includeStatus=true`, for users with a set status and when there are less than 100 participants in the conversation"
Expand Down
9 changes: 9 additions & 0 deletions src/components/AvatarWrapper/AvatarWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
:hideStatus="!showUserStatus"
:verboseStatus="!showUserStatusCompact"
:preloadedUserStatus="preloadedUserStatus"
:version="version"
:size="size" />
<!-- Override user status for federated users -->
<span
Expand Down Expand Up @@ -100,6 +101,14 @@ export default {
default: AVATAR.SIZE.DEFAULT,
},

/**
* The actor's avatar version, as returned with the participant list.
*/
version: {
type: [String, Number],
default: null,
},

condensed: {
type: Boolean,
default: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
:source="participant.actorType"
disableTooltip
:showUserStatus="showUserStatus"
:version="participant.actorAvatarVersion"
:preloadedUserStatus="preloadedUserStatus"
:highlighted="isSpeakingStatusAvailable && isParticipantSpeaking"
:offline="isOffline" />
Expand Down
2 changes: 2 additions & 0 deletions src/types/openapi/openapi-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3421,6 +3421,8 @@ export type components = {
roomToken: string;
/** @description Array of session ids, each are up to 512 character long strings, or empty if no session */
sessionIds: string[];
/** @description Version of the actor's avatar, only for users. Passing it to the avatar endpoint allows a much longer cache lifetime */
actorAvatarVersion?: string;
/** @description Only available with `includeStatus=true`, for users with a set status and when there are less than 100 participants in the conversation */
status?: string;
/**
Expand Down
2 changes: 2 additions & 0 deletions src/types/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2798,6 +2798,8 @@ export type components = {
roomToken: string;
/** @description Array of session ids, each are up to 512 character long strings, or empty if no session */
sessionIds: string[];
/** @description Version of the actor's avatar, only for users. Passing it to the avatar endpoint allows a much longer cache lifetime */
actorAvatarVersion?: string;
/** @description Only available with `includeStatus=true`, for users with a set status and when there are less than 100 participants in the conversation */
status?: string;
/**
Expand Down
21 changes: 21 additions & 0 deletions tests/php/Service/AvatarServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\EmojiService;
use OCA\Talk\Service\RoomService;
use OCP\Config\IUserConfig;
use OCP\Files\IAppData;
use OCP\Files\IFilenameValidator;
use OCP\IAvatarManager;
Expand All @@ -27,6 +28,7 @@
#[Group('DB')]
class AvatarServiceTest extends TestCase {
protected IAppData&MockObject $appData;
protected IUserConfig&MockObject $userConfig;
protected IL10N&MockObject $l;
protected IURLGenerator&MockObject $url;
protected ISecureRandom&MockObject $random;
Expand All @@ -40,6 +42,7 @@ public function setUp(): void {
parent::setUp();

$this->appData = $this->createMock(IAppData::class);
$this->userConfig = $this->createMock(IUserConfig::class);
$this->l = $this->createMock(IL10N::class);
$this->url = $this->createMock(IURLGenerator::class);
$this->random = $this->createMock(ISecureRandom::class);
Expand All @@ -49,6 +52,7 @@ public function setUp(): void {
$this->filenameValidator = Server::get(IFilenameValidator::class);
$this->service = new AvatarService(
$this->appData,
$this->userConfig,
$this->l,
$this->url,
$this->random,
Expand All @@ -59,6 +63,23 @@ public function setUp(): void {
);
}

public function testGetUserAvatarVersionsFillsInUsersWithoutAVersion(): void {
$this->userConfig->method('getValuesByUsers')
->with('avatar', 'version', null, ['alice', 'bob'])
->willReturn(['alice' => 5]);

$this->assertSame(
['alice' => '5', 'bob' => '0'],
$this->service->getUserAvatarVersions(['alice', 'bob']),
);
}

public function testGetUserAvatarVersionsSkipsAnEmptyList(): void {
$this->userConfig->expects($this->never())->method('getValuesByUsers');

$this->assertSame([], $this->service->getUserAvatarVersions([]));
}

public static function dataGetAvatarVersion(): array {
return [
['', 'STRING WITH 8 CHARS'],
Expand Down
Loading