diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml
index 346884c..9d9b19c 100644
--- a/src/bundle/Resources/config/services.yaml
+++ b/src/bundle/Resources/config/services.yaml
@@ -2,3 +2,4 @@ imports:
- { resource: services/mappers.yaml }
- { resource: services/papi.yaml }
- { resource: services/subscription_resolver.yaml }
+ - { resource: services/system_notifications.yaml }
diff --git a/src/bundle/Resources/config/services/system_notifications.yaml b/src/bundle/Resources/config/services/system_notifications.yaml
new file mode 100644
index 0000000..a279826
--- /dev/null
+++ b/src/bundle/Resources/config/services/system_notifications.yaml
@@ -0,0 +1,16 @@
+services:
+ _defaults:
+ autowire: true
+ autoconfigure: true
+ public: false
+
+ Ibexa\Notifications\SystemNotification\SystemNotificationChannel:
+ tags:
+ - name: notifier.channel
+ channel: ibexa
+
+ Ibexa\Notifications\SystemNotification\SystemNotificationRenderer:
+ tags:
+ - name: ibexa.notification.renderer
+ alias: system
+
diff --git a/src/bundle/Resources/views/themes/admin/.gitkeep b/src/bundle/Resources/views/themes/admin/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/bundle/Resources/views/themes/admin/notification/system_notification.html.twig b/src/bundle/Resources/views/themes/admin/notification/system_notification.html.twig
new file mode 100644
index 0000000..a605cc3
--- /dev/null
+++ b/src/bundle/Resources/views/themes/admin/notification/system_notification.html.twig
@@ -0,0 +1,26 @@
+{% extends '@ibexadesign/account/notifications/list_item.html.twig' %}
+
+{% trans_default_domain 'ibexa_notification' %}
+
+{% block icon %}
+ {% set icon = icon|default('info') %}
+
+
+
+{% endblock %}
+
+{% block notification_type %}
+
+ {{ subject|default('') }}
+
+{% endblock %}
+
+{% block message %}
+ {% embed '@ibexadesign/ui/component/table/table_body_cell.html.twig' with { class: 'ibexa-notifications-modal__description' } %}
+ {% block content %}
+
{{ content|default('') }}
+ {% endblock %}
+ {% endembed %}
+{% endblock %}
diff --git a/src/contracts/SystemNotification/SystemMessage.php b/src/contracts/SystemNotification/SystemMessage.php
new file mode 100644
index 0000000..d65c831
--- /dev/null
+++ b/src/contracts/SystemNotification/SystemMessage.php
@@ -0,0 +1,97 @@
+ */
+ private array $context;
+
+ private string $subject = '';
+
+ /**
+ * @param array $context
+ */
+ public function __construct(UserReference $user, array $context = [])
+ {
+ $this->user = $user;
+ $this->context = $context;
+ }
+
+ public function getUser(): UserReference
+ {
+ return $this->user;
+ }
+
+ public function setUser(UserReference $user): void
+ {
+ $this->user = $user;
+ }
+
+ public function getRecipientId(): ?string
+ {
+ return (string) $this->user->getUserId();
+ }
+
+ public function getSubject(): string
+ {
+ return $this->subject;
+ }
+
+ public function setSubject(string $subject): void
+ {
+ $this->subject = $subject;
+ }
+
+ public function getTransport(): ?string
+ {
+ return null;
+ }
+
+ public function getOptions(): ?MessageOptionsInterface
+ {
+ return null;
+ }
+
+ /**
+ * @return array
+ */
+ public function getContext(): array
+ {
+ return $this->context;
+ }
+
+ /**
+ * @param array $context
+ */
+ public function setContext(array $context): void
+ {
+ $this->context = $context;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function setType(string $type): void
+ {
+ $this->type = $type;
+ }
+}
diff --git a/src/contracts/SystemNotification/SystemNotification.php b/src/contracts/SystemNotification/SystemNotification.php
new file mode 100644
index 0000000..5793f5c
--- /dev/null
+++ b/src/contracts/SystemNotification/SystemNotification.php
@@ -0,0 +1,64 @@
+icon;
+ }
+
+ public function setIcon(?string $icon): void
+ {
+ $this->icon = $icon;
+ }
+
+ public function getRoute(): ?RouteReference
+ {
+ return $this->route;
+ }
+
+ public function setRoute(?RouteReference $route): void
+ {
+ $this->route = $route;
+ }
+
+ public function setContent(string $content): void
+ {
+ $this->content($content);
+ }
+
+ public function asSystemNotification(UserRecipientInterface $recipient, ?string $transport = null): SystemMessage
+ {
+ $context = [
+ 'subject' => $this->getSubject(),
+ 'content' => $this->getContent(),
+ ];
+
+ if ($this->icon !== null) {
+ $context['icon'] = $this->icon;
+ }
+
+ if ($this->route !== null) {
+ $context['route_name'] = $this->route->getRoute();
+ $context['route_params'] = $this->route->getParams();
+ }
+
+ return new SystemMessage($recipient->getUser(), $context);
+ }
+}
diff --git a/src/contracts/SystemNotification/SystemNotificationInterface.php b/src/contracts/SystemNotification/SystemNotificationInterface.php
new file mode 100644
index 0000000..e84dadb
--- /dev/null
+++ b/src/contracts/SystemNotification/SystemNotificationInterface.php
@@ -0,0 +1,19 @@
+user->email;
}
+
+ public function getUser(): UserReference
+ {
+ return $this->user;
+ }
}
diff --git a/src/contracts/Value/Recipent/UserRecipientInterface.php b/src/contracts/Value/Recipent/UserRecipientInterface.php
new file mode 100644
index 0000000..716a60a
--- /dev/null
+++ b/src/contracts/Value/Recipent/UserRecipientInterface.php
@@ -0,0 +1,17 @@
+repository = $repository;
+ $this->notificationService = $notificationService;
+ }
+
+ /**
+ * @param \Symfony\Component\Notifier\Notification\Notification&\Ibexa\Contracts\Notifications\SystemNotification\SystemNotificationInterface $notification
+ * @param \Ibexa\Contracts\Notifications\Value\Recipent\UserRecipientInterface $recipient
+ */
+ public function notify(Notification $notification, RecipientInterface $recipient, ?string $transportName = null): void
+ {
+ $message = $notification->asSystemNotification($recipient, $transportName);
+ if ($message === null) {
+ return;
+ }
+
+ $createStruct = new CreateStruct();
+ $createStruct->ownerId = $message->getUser()->getUserId();
+ $createStruct->type = $message->getType();
+ $createStruct->data = $message->getContext();
+
+ $this->repository->beginTransaction();
+ try {
+ $this->notificationService->createNotification($createStruct);
+ $this->repository->commit();
+ } catch (Throwable $e) {
+ $this->repository->rollback();
+ throw $e;
+ }
+ }
+
+ public function supports(Notification $notification, RecipientInterface $recipient): bool
+ {
+ return $notification instanceof SystemNotificationInterface && $recipient instanceof UserRecipientInterface;
+ }
+}
diff --git a/src/lib/SystemNotification/SystemNotificationRenderer.php b/src/lib/SystemNotification/SystemNotificationRenderer.php
new file mode 100644
index 0000000..c00712e
--- /dev/null
+++ b/src/lib/SystemNotification/SystemNotificationRenderer.php
@@ -0,0 +1,59 @@
+twig = $twig;
+ $this->urlGenerator = $urlGenerator;
+ }
+
+ public function render(Notification $notification): string
+ {
+ return $this->twig->render(
+ '@ibexadesign/notification/system_notification.html.twig',
+ [
+ 'notification' => $notification,
+ 'icon' => $notification->data[self::KEY_ICON] ?? null,
+ 'content' => $notification->data[self::KEY_CONTENT] ?? null,
+ 'subject' => $notification->data[self::KEY_SUBJECT] ?? null,
+ 'created_at' => $notification->created,
+ ]
+ );
+ }
+
+ public function generateUrl(Notification $notification): ?string
+ {
+ if (!isset($notification->data[self::KEY_ROUTE_NAME])) {
+ return null;
+ }
+
+ return $this->urlGenerator->generate(
+ $notification->data[self::KEY_ROUTE_NAME],
+ $notification->data[self::KEY_ROUTE_PARAMS] ?? []
+ );
+ }
+}
diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/integration/Notifications/SystemNotification/SystemNotificationTest.php b/tests/integration/Notifications/SystemNotification/SystemNotificationTest.php
new file mode 100644
index 0000000..db02764
--- /dev/null
+++ b/tests/integration/Notifications/SystemNotification/SystemNotificationTest.php
@@ -0,0 +1,47 @@
+setIcon('icon');
+ $notification->setContent('It works!');
+ $notification->setRoute(new RouteReference('example', ['foo' => 'bar', 'baz' => 'qux']));
+
+ $user = new UserReference(self::EXAMPLE_USER_ID);
+
+ $recipient = $this->createMock(UserRecipientInterface::class);
+ $recipient->method('getUser')->willReturn($user);
+
+ $systemMessage = $notification->asSystemNotification($recipient);
+
+ self::assertNotNull($systemMessage);
+ self::assertSame($user, $systemMessage->getUser());
+ self::assertEquals(SystemMessage::DEFAULT_TYPE, $systemMessage->getType());
+ self::assertEquals([
+ 'icon' => 'icon',
+ 'subject' => 'Hello World',
+ 'content' => 'It works!',
+ 'route_name' => 'example',
+ 'route_params' => ['foo' => 'bar', 'baz' => 'qux'],
+ ], $systemMessage->getContext());
+ }
+}
diff --git a/tests/lib/Channel/SystemNotificationChannelTest.php b/tests/lib/Channel/SystemNotificationChannelTest.php
new file mode 100644
index 0000000..ac257c9
--- /dev/null
+++ b/tests/lib/Channel/SystemNotificationChannelTest.php
@@ -0,0 +1,137 @@
+repository = $this->createMock(Repository::class);
+ $this->notificationService = $this->createMock(NotificationService::class);
+
+ $this->channel = new SystemNotificationChannel($this->repository, $this->notificationService);
+ }
+
+ /**
+ * @dataProvider dataProviderForTestSupports
+ */
+ public function testSupports(Notification $notification, RecipientInterface $recipient, bool $expectedResult): void
+ {
+ self::assertEquals($expectedResult, $this->channel->supports($notification, $recipient));
+ }
+
+ /**
+ * @return iterable
+ */
+ public function dataProviderForTestSupports(): iterable
+ {
+ yield 'supported' => [
+ $this->createSupportedNotification(),
+ $this->createSupportedRecipient(),
+ true,
+ ];
+
+ yield 'unsupported recipient' => [
+ $this->createSupportedNotification(),
+ $this->createMock(RecipientInterface::class),
+ false,
+ ];
+
+ yield 'unsupported notification' => [
+ $this->createMock(Notification::class),
+ $this->createSupportedRecipient(),
+ false,
+ ];
+ }
+
+ public function testNotify(): void
+ {
+ $expectedCreateStruct = new CreateStruct();
+ $expectedCreateStruct->ownerId = self::EXAMPLE_USER_ID;
+ $expectedCreateStruct->data = ['foo' => 'bar'];
+ $expectedCreateStruct->type = 'example';
+
+ $this->notificationService
+ ->expects(self::once())
+ ->method('createNotification')
+ ->with($expectedCreateStruct)
+ ->willReturn(new SystemNotification());
+
+ $user = $this->createMock(UserReference::class);
+ $user->method('getUserId')->willReturn(self::EXAMPLE_USER_ID);
+
+ $message = new SystemMessage($user, ['foo' => 'bar']);
+ $message->setType('example');
+
+ $this->channel->notify(
+ $this->createSupportedNotification($message),
+ $this->createSupportedRecipient(self::EXAMPLE_USER_ID)
+ );
+ }
+
+ /**
+ * @return \Symfony\Component\Notifier\Notification\Notification&\Ibexa\Contracts\Notifications\SystemNotification\SystemNotificationInterface
+ */
+ private function createSupportedNotification(?SystemMessage $message = null): Notification
+ {
+ return new class($message) extends Notification implements SystemNotificationInterface {
+ private ?SystemMessage $message;
+
+ public function __construct(?SystemMessage $message)
+ {
+ parent::__construct('example');
+
+ $this->message = $message;
+ }
+
+ public function asSystemNotification(
+ UserRecipientInterface $recipient,
+ ?string $transport = null
+ ): ?SystemMessage {
+ return $this->message;
+ }
+ };
+ }
+
+ private function createSupportedRecipient(?int $userId = null): UserRecipientInterface
+ {
+ $recipient = $this->createMock(UserRecipientInterface::class);
+ if ($userId !== null) {
+ $user = $this->createMock(UserReference::class);
+ $user->method('getUserId')->willReturn($userId);
+
+ $recipient->method('getUser')->willReturn($user);
+ }
+
+ return $recipient;
+ }
+}