Skip to content
Open
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
62 changes: 61 additions & 1 deletion lib/DigestSender.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
use OCP\Defaults;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IPreview;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
Expand All @@ -24,6 +29,14 @@
class DigestSender {
public const ACTIVITY_LIMIT = 20;

/**
* Maximum size in pixels for embedded preview thumbnails. Kept small so that
* the resulting base64-encoded payload doesn't bloat the email — a 96px PNG
* is typically a few KB, and 20 of them stay well under common message-size
* limits.
*/
private const PREVIEW_PIXELS = 96;

public function __construct(
private IConfig $config,
private Data $data,
Expand All @@ -37,6 +50,8 @@ public function __construct(
private IFactory $l10nFactory,
private IDateTimeFormatter $dateTimeFormatter,
private LoggerInterface $logger,
private IRootFolder $rootFolder,
private IPreview $previewManager,
) {
}

Expand Down Expand Up @@ -181,7 +196,8 @@ public function sendDigestForUser(IUser $user, int $now, string $timezone, strin
$l10n
);

$template->addBodyListItem($this->getHTMLSubject($event), $relativeDateTime, $event->getIcon(), $event->getParsedSubject());
$icon = $this->getPreviewDataUri($event, $uid) ?? $event->getIcon();
$template->addBodyListItem($this->getHTMLSubject($event), $relativeDateTime, $icon, $event->getParsedSubject());
}

if ($skippedCount) {
Expand Down Expand Up @@ -215,6 +231,50 @@ public function sendDigestForUser(IUser $user, int $now, string $timezone, strin
}
}

/**
* Returns a data: URI with a base64-encoded PNG preview of the file the
* activity is about, or null if no preview is available — for any reason
* (not a file event, file deleted, no read permission, preview generation
* unsupported, etc.). The caller falls back to the regular event icon.
*
* Resolved as the affected user, so the resulting access mirrors what the
* recipient could see in the web UI.
*/
private function getPreviewDataUri(IEvent $event, string $uid): ?string {
if ($event->getObjectType() !== 'files' || $event->getObjectId() <= 0) {
return null;
}

try {
$userFolder = $this->rootFolder->getUserFolder($uid);
$node = $userFolder->getFirstNodeById($event->getObjectId());
if (!$node instanceof File) {
return null;
}

if (!$this->previewManager->isAvailable($node)) {
return null;
}

$preview = $this->previewManager->getPreview($node, self::PREVIEW_PIXELS, self::PREVIEW_PIXELS, true);
$content = $preview->getContent();
if ($content === '') {
return null;
}
$mime = $preview->getMimeType() ?: 'image/png';
Comment on lines +249 to +264
Copy link
Copy Markdown
Collaborator

@miaulalala miaulalala May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does image processing for every preview - and may be quite heavy for documents such as pdfs and office documents.

return 'data:' . $mime . ';base64,' . base64_encode($content);
} catch (NotFoundException|NotPermittedException) {
return null;
} catch (\Throwable $e) {
$this->logger->debug('Could not generate digest preview', [
'app' => 'activity',
'exception' => $e,
'object_id' => $event->getObjectId(),
]);
return null;
}
}

/**
* @param IEvent $event
* @return string
Expand Down
Loading