From 0deedbecf568ead79b680bb2b7ef2a3676adf0ea Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 23 Nov 2022 10:22:49 +0100 Subject: [PATCH] Use caching and a fetch queue to fetch avatars slowly without overloading server #115 --- plugins/avatars/avatars.js | 96 ++++++++++++++++++++++++++------------ plugins/avatars/index.php | 51 +++++++++++++++----- plugins/avatars/style.css | 19 ++++++-- 3 files changed, 122 insertions(+), 44 deletions(-) diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index f18dbdf7f..7265be123 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -1,8 +1,57 @@ (rl => { // if (rl.settings.get('Nextcloud')) const - templateId = 'MailMessageView'; -// DATA_IMAGE_USER_DOT_PIC = 'data:image/svg+xml;utf8,'; + queue = [], + avatars = new Map, + templateId = 'MailMessageView', + getAvatarUid = msg => { + let from = msg.from[0], + bimi = 'pass' == from.dkimStatus ? 1 : 0; + return `${bimi}/${from.email}`; + }, + getAvatar = msg => avatars.get(getAvatarUid(msg)), + runQueue = (() => { + let item = queue.shift(); + while (item) { + let url = getAvatar(item[0]); + if (url) { + item[1](url); + item = queue.shift(); + } else { + // TODO: fetch vCard from Nextcloud contacts +// let cfg = rl.settings.get('Nextcloud'), + + let from = item[0].from[0]; + rl.pluginRemoteRequest((iError, data) => { + if (!iError && data?.Result.type) { + url = `data:${data.Result.type};base64,${data.Result.data}`; + avatars.set(getAvatarUid(item[0]), url); + item[1](url); + } + runQueue(); + }, 'Avatar', { + bimi: 'pass' == from.dkimStatus ? 1 : 0, + email: from.email + }); + break; + } + } + }).debounce(1000); + + ko.bindingHandlers.fromPic = { + init: (element, self, dummy, msg) => { + if (msg) { + let url = getAvatar(msg), + fn = url=>{element.src = url}; + if (url) { + fn(url); + } else { + queue.push([msg, fn]); + runQueue(); + } + } + } + }; addEventListener('rl-view-model.create', e => { if (templateId === e.detail.viewModelTemplateID) { @@ -24,39 +73,28 @@ view.message.subscribe(msg => { view.viewUserPicVisible(false); if (msg) { - let from = msg.from[0], - bimi = 'pass' == from.dkimStatus ? 1 : 0; -// view.viewUserPic(`?Avatar/${bimi}/${encodeURIComponent(from.email)}`); -// view.viewUserPicVisible(true); - rl.pluginRemoteRequest((iError, data) => { - if (!iError && data?.Result.type) { - view.viewUserPic(`data:${data.Result.type};base64,${data.Result.data}`); + let url = getAvatar(msg), + fn = url => { + view.viewUserPic(url); view.viewUserPicVisible(true); - } - }, 'Avatar', { - bimi: bimi, - email: from.email - }); + }; + if (url) { + fn(url); + } else { +// let from = msg.from[0], bimi = 'pass' == from.dkimStatus ? 1 : 0; +// view.viewUserPic(`?Avatar/${bimi}/${encodeURIComponent(from.email)}`); +// view.viewUserPicVisible(true); + queue.push([msg, fn]); + runQueue(); + } } }); } -/* + if ('MailMessageList' === e.detail.viewModelTemplateID) { - const - template = document.getElementById('MailMessageList' ), - messageCheckbox = template.content.querySelector('.messageCheckbox'); - messageCheckbox.dataset.bind = 'attr:{style:$root.viewUserPic($data)}'; - e.detail.viewUserPic = msg => { - let from = msg.from[0], - bimi = 'pass' == from.dkimStatus ? 1 : 0; - return `background:no-repeat url("?Avatar/${bimi}/${encodeURIComponent(from.email)}") center / contain`; - return `background:no-repeat url("?Avatar/${bimi}/${encodeURIComponent(from.email)}") right / 32px;width:68px`; - }; - .checkboxMessage { - background: #000; - } + document.getElementById('MailMessageList').content.querySelector('.messageCheckbox') + .append(Element.fromHTML(``)); } -*/ }); })(window.rl); diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 61a8547c4..f23acc1f1 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -58,20 +58,38 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $aResult = null; - // TODO: lookup contacts vCard - $oAccount = $oActions->getAccountFromToken(); - if ($oAccount) { - $oAddressBookProvider = $oActions->AddressBookProvider($oAccount); - if ($oAddressBookProvider) { - $oContact = $oAddressBookProvider->GetContactByEmail($sEmail); - if ($oContact && $oContact->vCard && $oContact->vCard['PHOTO']) { - $aResult = [ - 'text/vcard', - $oContact->vCard - ]; + $sAsciiEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); + $sEmailId = \sha1(\strtolower($sAsciiEmail)); + + $sFile = \APP_PRIVATE_DATA . 'avatars/' . $sEmailId; + $aFiles = \glob("{$sFile}.*"); + if ($aFiles) { + $aResult = [ + \mime_content_type($aFiles[0]), + \file_get_contents($aFiles[0]) + ]; + $oActions->cacheByKey($sEmail); + return $aResult; + } + + // TODO: lookup contacts vCard and return PHOTO value + /* + if (!$aResult) { + $oAccount = $oActions->getAccountFromToken(); + if ($oAccount) { + $oAddressBookProvider = $oActions->AddressBookProvider($oAccount); + if ($oAddressBookProvider) { + $oContact = $oAddressBookProvider->GetContactByEmail($sEmail); + if ($oContact && $oContact->vCard && $oContact->vCard['PHOTO']) { + $aResult = [ + 'text/vcard', + $oContact->vCard + ]; + } } } } + */ if (!$aResult) { $sDomain = \explode('@', $sEmail); @@ -89,7 +107,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } // TODO: make Gravatar optional - $sAsciiEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); $aUrls[] = 'http://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404'; foreach ($aUrls as $sUrl) { @@ -99,6 +116,16 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } } + if ($aResult) { + if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) { + \mkdir(\APP_PRIVATE_DATA . 'avatars', 0700); + } + \file_put_contents( + $sFile . \SnappyMail\File\MimeType::toExtension($aResult[0]), + $aResult[1] + ); + } + if (!$aResult) { $aServices = [ "services/{$sDomain}", diff --git a/plugins/avatars/style.css b/plugins/avatars/style.css index dc7eeb2e3..c9fadcd86 100644 --- a/plugins/avatars/style.css +++ b/plugins/avatars/style.css @@ -3,9 +3,22 @@ float: left; width: 50px; height: 50px; - padding: 2px; margin: 0 5px 0 0; - background: #fff; - border: 1px solid #ccc; + border: 1px solid var(--border-color, #ddd); border-radius: 6px; } +/* +.checkboxMessage { + background: #000 no-repeat center / contain; + background: #000 no-repeat right / 32px; + width:68px +} +*/ +#rl-app .messageCheckbox { + padding-right:0; + white-space:nowrap; +} +.messageCheckbox .fromPic { + margin: -0.6em 0 -0.6em 0.4em; + height: 2em; +}