Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: reduce number of avatar requests #10486

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
5 changes: 3 additions & 2 deletions lib/Contracts/IAvatarService.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ interface IAvatarService {
*
* @param string $email
* @param string $uid
* @return Avatar|null
* @param bool $cachedOnly
* @return Avatar|null|false the avatar if found, false if $cachedOnly is true and no value cached and null if not found
*/
public function getAvatar(string $email, string $uid): ?Avatar;
public function getAvatar(string $email, string $uid, bool $cachedOnly = false): mixed;

/**
* @param string $email
Expand Down
4 changes: 3 additions & 1 deletion lib/Contracts/IMailSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public function findMessage(Account $account,
* @param string|null $filter
* @param int|null $cursor
* @param int|null $limit
* @param string|null $userId
*
* @return Message[]
*
Expand All @@ -47,7 +48,8 @@ public function findMessages(Account $account,
string $sortOrder,
?string $filter,
?int $cursor,
?int $limit): array;
?int $limit,
?string $userId): array;

/**
* @param IUser $user
Expand Down
3 changes: 2 additions & 1 deletion lib/Controller/MessagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@
$order,
$filter === '' ? null : $filter,
$cursor,
$limit
$limit,
$this->currentUserId

Check warning on line 156 in lib/Controller/MessagesController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/MessagesController.php#L155-L156

Added lines #L155 - L156 were not covered by tests
)
);
}
Expand Down
27 changes: 27 additions & 0 deletions lib/Db/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Horde_Mail_Rfc822_Identification;
use JsonSerializable;
use OCA\Mail\AddressList;
use OCA\Mail\Service\Avatar\Avatar;
use OCP\AppFramework\Db\Entity;
use ReturnTypeWillChange;
use function in_array;
Expand Down Expand Up @@ -136,6 +137,12 @@
/** @var Tag[] */
private $tags = [];

/** @var Avatar|null */
private $avatar;

/** @var bool*/
private $fetchAvatarFromClient = false;

public function __construct() {
$this->from = new AddressList([]);
$this->to = new AddressList([]);
Expand Down Expand Up @@ -286,6 +293,24 @@
);
}
}
/**
* @param Avatar|null $avatar
* @return void
*/
public function setAvatar(?Avatar $avatar): void {
$this->avatar = $avatar;

Check warning on line 301 in lib/Db/Message.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/Message.php#L300-L301

Added lines #L300 - L301 were not covered by tests
}

public function setFetchAvatarFromClient(bool $fetchAvatarFromClient): void {
$this->fetchAvatarFromClient = $fetchAvatarFromClient;

Check warning on line 305 in lib/Db/Message.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/Message.php#L304-L305

Added lines #L304 - L305 were not covered by tests
}

/**
* @return ?Avatar
*/
public function getAvatar(): ?Avatar {
return $this->avatar;

Check warning on line 312 in lib/Db/Message.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/Message.php#L311-L312

Added lines #L311 - L312 were not covered by tests
}

#[ReturnTypeWillChange]
public function jsonSerialize() {
Expand Down Expand Up @@ -331,6 +356,8 @@
'summary' => $this->getSummary(),
'encrypted' => ($this->isEncrypted() === true),
'mentionsMe' => $this->getMentionsMe(),
'avatar' => $this->avatar?->jsonSerialize(),
'fetchAvatarFromClient' => $this->fetchAvatarFromClient,

Check warning on line 360 in lib/Db/Message.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/Message.php#L359-L360

Added lines #L359 - L360 were not covered by tests
];
}
}
27 changes: 25 additions & 2 deletions lib/IMAP/PreviewEnhancer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper as DbMapper;
use OCA\Mail\IMAP\MessageMapper as ImapMapper;
use OCA\Mail\Service\AvatarService;
use OCA\Mail\Service\Avatar\Avatar;
use Psr\Log\LoggerInterface;
use function array_key_exists;
use function array_map;
Expand All @@ -34,22 +36,27 @@
/** @var LoggerInterface */
private $logger;

/** @var AvatarService */
private $avatarService;

public function __construct(IMAPClientFactory $clientFactory,
ImapMapper $imapMapper,
DbMapper $dbMapper,
LoggerInterface $logger) {
LoggerInterface $logger,
AvatarService $avatarService) {
$this->clientFactory = $clientFactory;
$this->imapMapper = $imapMapper;
$this->mapper = $dbMapper;
$this->logger = $logger;
$this->avatarService = $avatarService;
}

/**
* @param Message[] $messages
*
* @return Message[]
*/
public function process(Account $account, Mailbox $mailbox, array $messages): array {
public function process(Account $account, Mailbox $mailbox, array $messages, bool $preLoadAvatars = false, ?string $userId = null): array {
$needAnalyze = array_reduce($messages, static function (array $carry, Message $message) {
if ($message->getStructureAnalyzed()) {
// Nothing to do
Expand All @@ -59,6 +66,22 @@
return array_merge($carry, [$message->getUid()]);
}, []);

if ($preLoadAvatars) {
foreach ($messages as $message) {
$from = $message->getFrom()->first();
if ($message->getAvatar() === null && $from !== null && $from->getEmail() !== null && $userId !== null) {
$avatar = $this->avatarService->getAvatar($from->getEmail(), $userId, true);
if ($avatar === false) {
$message->setFetchAvatarFromClient(true);

Check warning on line 75 in lib/IMAP/PreviewEnhancer.php

View check run for this annotation

Codecov / codecov/patch

lib/IMAP/PreviewEnhancer.php#L70-L75

Added lines #L70 - L75 were not covered by tests
}
if ($avatar instanceof Avatar) {
$message->setAvatar($avatar);

Check warning on line 78 in lib/IMAP/PreviewEnhancer.php

View check run for this annotation

Codecov / codecov/patch

lib/IMAP/PreviewEnhancer.php#L77-L78

Added lines #L77 - L78 were not covered by tests
}

}
}
}

if ($needAnalyze === []) {
// Nothing to enhance
return $messages;
Expand Down
10 changes: 8 additions & 2 deletions lib/Service/AvatarService.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@
/**
* @param string $email
* @param string $uid
* @return Avatar|null
* @param bool $cachedOnly
* @return Avatar|null|false
*/
public function getAvatar(string $email, string $uid): ?Avatar {
public function getAvatar(string $email, string $uid, bool $cachedOnly = false): mixed{
$cachedAvatar = $this->cache->get($email, $uid);
if ($cachedAvatar) {
return $cachedAvatar;
Expand All @@ -101,6 +102,11 @@
return null;
}

if ($cachedOnly){
// We want to fetch the avatar from the frontend
return false;

Check warning on line 107 in lib/Service/AvatarService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AvatarService.php#L107

Added line #L107 was not covered by tests
}

$avatar = $this->source->fetch($email, $this->avatarFactory, $this->externalAvatarsAllowed($uid));
if (is_null($avatar) || !$this->hasAllowedMime($avatar)) {
// Cannot locate any avatar -> nothing to do here
Expand All @@ -127,7 +133,7 @@
return null;
}

$cachedImage = $this->cache->getImage($avatar->getUrl(), $uid);

Check failure on line 136 in lib/Service/AvatarService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyFalseReference

lib/Service/AvatarService.php:136:50: PossiblyFalseReference: Cannot call method getUrl on possibly false value (see https://psalm.dev/105)
if (!is_null($cachedImage)) {
return [$avatar, base64_decode($cachedImage)];
}
Expand Down
7 changes: 5 additions & 2 deletions lib/Service/Search/MailSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
string $sortOrder,
?string $filter,
?int $cursor,
?int $limit): array {
?int $limit,
?string $userId): array {
if ($mailbox->hasLocks($this->timeFactory->getTime())) {
throw MailboxLockedException::from($mailbox);
}
Expand All @@ -118,7 +119,9 @@
$this->messageMapper->findByIds($account->getUserId(),
$this->getIdsLocally($account, $mailbox, $query, $sortOrder, $limit),
$sortOrder,
)
),
true,
$userId

Check warning on line 124 in lib/Service/Search/MailSearch.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/Search/MailSearch.php#L122-L124

Added lines #L122 - L124 were not covered by tests
);
}

Expand Down
28 changes: 22 additions & 6 deletions src/components/Avatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<script>
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import { generateUrl } from '@nextcloud/router'
import { fetchAvatarUrlMemoized } from '../service/AvatarService.js'
import logger from '../logger.js'

Expand All @@ -30,6 +31,14 @@ export default {
type: String,
required: true,
},
avatar: {
type: Object,
default: null,
},
fetchAvatar: {
type: Boolean,
default: false,
},
email: {
type: String,
required: true,
Expand All @@ -55,14 +64,21 @@ export default {
},
},
async mounted() {
if (this.email !== '') {
try {
this.avatarUrl = await fetchAvatarUrlMemoized(this.email)
} catch {
logger.debug('Could not fetch avatar', { email: this.email })
if (this.avatar) {
this.avatarUrl = this.avatar.isExternal
? generateUrl('/apps/mail/api/avatars/image/{email}', {
email: this.email,
})
: this.avatar.url
} else if (this.fetchAvatar) {
if (this.email !== '') {
try {
this.avatarUrl = await fetchAvatarUrlMemoized(this.email)
} catch {
logger.debug('Could not fetch avatar', { email: this.email })
}
}
}

this.loading = false
},
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/Envelope.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
:class="{ 'important-one-line': oneLineLayout, 'icon-important': !oneLineLayout }"
:data-starred="isImportant ? 'true' : 'false'"
@click.prevent="hasWriteAcl ? onToggleImportant() : false"
v-html="importantSvg" />

Check warning on line 39 in src/components/Envelope.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'v-html' directive can lead to XSS attack
<JunkIcon v-if="data.flags.$junk"
:size="18"
class="app-content-list-item-star junk-icon-style"
Expand All @@ -52,7 +52,10 @@
<CheckIcon :size="40" class="check-icon" :class="{ 'app-content-list-item-avatar-selected': selected }" />
</template>
<template v-else>
<Avatar :display-name="addresses" :email="avatarEmail" />
<Avatar :display-name="addresses"
:email="avatarEmail"
:fetch-avatar="data.fetchAvatarFromClient"
:avatar="data.avatar" />
</template>
</div>
</template>
Expand Down Expand Up @@ -481,7 +484,7 @@
window.addEventListener('resize', this.onWindowResize)
},
computed: {
...mapGetters([

Check warning on line 487 in src/components/Envelope.vue

View workflow job for this annotation

GitHub Actions / NPM lint

The "computed" property should be above the "mounted" property on line 482
'isSnoozeDisabled',
]),
messageLongDate() {
Expand Down
5 changes: 4 additions & 1 deletion src/components/OutboxMessageListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
:details="details"
@click="openModal">
<template #icon>
<Avatar :display-name="avatarDisplayName" :email="avatarEmail" />
<Avatar :display-name="avatarDisplayName"
:email="avatarEmail"
::fetch-avatar="data.fetchAvatarFromClient"
:avatar="message.avatar" />
</template>
<template #subname>
{{ subjectForSubtitle }}
Expand Down
2 changes: 2 additions & 0 deletions src/components/ThreadEnvelope.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
:display-name="envelope.from[0].label"
:disable-tooltip="true"
:size="40"
:fetch-avatar="data.fetchAvatarFromClient"
:avatar="envelope.avatar"
class="envelope__header__avatar-avatar" />
<div v-if="isImportant"
class="app-content-list-item-star icon-important"
Expand Down
Loading