From 07863d2a7294fb8ef12b4ff2cb84235d8f450e4e Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Tue, 26 Nov 2024 10:42:32 +0100
Subject: [PATCH] Implement custom notifications You can now set custom
notification settings for users, magazine, entries and posts: `Loud`,
`Default` or `Muted`.
Move the logic of "fetch an endpoint and get html. Replace html node with selector x with the result of the fetch" to another controller, so it can be used outside of subjects (`Entry`, `EntryComment`, `Post` and `PostComment`)
# Conflicts:
# config/packages/doctrine.yaml
---
assets/controllers/html_refresh_controller.js | 46 +++++
assets/controllers/subject_controller.js | 40 ----
assets/styles/app.scss | 1 +
assets/styles/components/_entry.scss | 2 +-
assets/styles/components/_magazine.scss | 5 +-
.../components/_notification_switch.scss | 58 ++++++
assets/styles/components/_popover.scss | 4 +-
assets/styles/components/_post.scss | 10 +-
assets/styles/components/_sidebar.scss | 6 +-
assets/styles/components/_user.scss | 1 -
assets/styles/layout/_section.scss | 2 +-
config/mbin_routes/notification_settings.yaml | 6 +
.../notification_settings_api.yaml | 8 +
config/packages/doctrine.yaml | 2 +
migrations/Version20241125210454.php | 47 +++++
src/Controller/Api/BaseApi.php | 15 +-
src/Controller/Api/Entry/EntriesBaseApi.php | 1 +
.../Notification/NotificationSettingApi.php | 107 ++++++++++
src/Controller/Api/Post/PostsBaseApi.php | 1 +
.../NotificationSettingsController.php | 59 ++++++
src/DTO/EntryResponseDto.php | 3 +
src/DTO/MagazineResponseDto.php | 3 +
src/DTO/PostResponseDto.php | 3 +
src/DTO/UserResponseDto.php | 3 +
.../DBAL/Types/EnumNotificationStatus.php | 20 ++
src/Entity/NotificationSettings.php | 70 +++++++
src/Enums/ENotificationStatus.php | 36 ++++
.../NotificationSettingsRepository.php | 188 ++++++++++++++++++
.../EntryCommentNotificationManager.php | 93 +++------
.../Notification/EntryNotificationManager.php | 26 +--
.../PostCommentNotificationManager.php | 91 +++------
.../Notification/PostNotificationManager.php | 26 +--
src/Twig/Components/NotificationSwitch.php | 38 ++++
src/Twig/Extension/FrontExtension.php | 1 +
src/Twig/Runtime/FrontExtensionRuntime.php | 17 ++
templates/components/bookmark_list.html.twig | 4 +-
.../components/bookmark_standard.html.twig | 12 +-
templates/components/entry.html.twig | 7 +-
templates/components/entry_comment.html.twig | 2 +-
templates/components/magazine_box.html.twig | 7 +
.../components/notification_switch.html.twig | 32 +++
templates/components/post.html.twig | 5 +-
templates/components/post_comment.html.twig | 2 +-
templates/components/user_box.html.twig | 5 +
templates/entry/_info.html.twig | 5 +
templates/post/_info.html.twig | 5 +
templates/user/_user_popover.html.twig | 3 +
47 files changed, 907 insertions(+), 221 deletions(-)
create mode 100644 assets/controllers/html_refresh_controller.js
create mode 100644 assets/styles/components/_notification_switch.scss
create mode 100644 config/mbin_routes/notification_settings.yaml
create mode 100644 config/mbin_routes/notification_settings_api.yaml
create mode 100644 migrations/Version20241125210454.php
create mode 100644 src/Controller/Api/Notification/NotificationSettingApi.php
create mode 100644 src/Controller/NotificationSettingsController.php
create mode 100644 src/DoctrineExtensions/DBAL/Types/EnumNotificationStatus.php
create mode 100644 src/Entity/NotificationSettings.php
create mode 100644 src/Enums/ENotificationStatus.php
create mode 100644 src/Repository/NotificationSettingsRepository.php
create mode 100644 src/Twig/Components/NotificationSwitch.php
create mode 100644 templates/components/notification_switch.html.twig
diff --git a/assets/controllers/html_refresh_controller.js b/assets/controllers/html_refresh_controller.js
new file mode 100644
index 000000000..fe49d11fe
--- /dev/null
+++ b/assets/controllers/html_refresh_controller.js
@@ -0,0 +1,46 @@
+import { Controller } from "@hotwired/stimulus";
+import { fetch, ok } from "../utils/http";
+
+/* stimulusFetch: 'lazy' */
+export default class extends Controller {
+ /**
+ * Calls the address attached to the nearest link node. Replaces the outer html of the nearest `cssclass` parameter
+ * with the response from the link
+ */
+ async linkCallback(event) {
+ event.preventDefault();
+ const { cssclass: cssClass, refreshlink: refreshLink, refreshselector: refreshSelector } = event.params
+
+ const a = event.target.closest('a');
+ let subjectController = this.application.getControllerForElementAndIdentifier(this.element, 'subject')
+
+ try {
+ if (subjectController) {
+ subjectController.loadingValue = true;
+ }
+
+ let response = await fetch(a.href);
+
+ response = await ok(response);
+ response = await response.json();
+
+ event.target.closest(`.${cssClass}`).outerHTML = response.html;
+
+ const refreshElement = this.element.querySelector(refreshSelector)
+
+ if (!!refreshLink && refreshLink !== "" && !!refreshElement) {
+ let response = await fetch(refreshLink);
+
+ response = await ok(response);
+ response = await response.json();
+ refreshElement.outerHTML = response.html;
+ }
+ } catch (e) {
+ console.error(e)
+ } finally {
+ if (subjectController) {
+ subjectController.loadingValue = false;
+ }
+ }
+ }
+}
diff --git a/assets/controllers/subject_controller.js b/assets/controllers/subject_controller.js
index 093944d60..3b180aff8 100644
--- a/assets/controllers/subject_controller.js
+++ b/assets/controllers/subject_controller.js
@@ -199,46 +199,6 @@ export default class extends Controller {
}
}
- /**
- * Calls the address attached to the nearest link node. Replaces the outer html of the nearest `cssclass` parameter
- * with the response from the link
- */
- async linkCallback(event) {
- const { cssclass: cssClass, refreshlink: refreshLink, refreshselector: refreshSelector } = event.params
- event.preventDefault();
-
- const a = event.target.closest('a');
-
- try {
- this.loadingValue = true;
-
- let response = await fetch(a.href, {
- method: 'GET',
- });
-
- response = await ok(response);
- response = await response.json();
-
- event.target.closest(`.${cssClass}`).outerHTML = response.html;
-
- const refreshElement = this.element.querySelector(refreshSelector)
- console.log("linkCallback refresh stuff", refreshLink, refreshSelector, refreshElement)
-
- if (!!refreshLink && refreshLink !== "" && !!refreshElement) {
- let response = await fetch(refreshLink, {
- method: 'GET',
- });
-
- response = await ok(response);
- response = await response.json();
- refreshElement.outerHTML = response.html;
- }
- } catch (e) {
- } finally {
- this.loadingValue = false;
- }
- }
-
loadingValueChanged(val) {
const submitButton = this.containerTarget.querySelector('form button[type="submit"]');
diff --git a/assets/styles/app.scss b/assets/styles/app.scss
index 2cc77c74b..164dc71af 100644
--- a/assets/styles/app.scss
+++ b/assets/styles/app.scss
@@ -52,6 +52,7 @@
@import 'components/infinite_scroll';
@import 'components/sidebar-subscriptions';
@import 'components/settings_row';
+@import 'components/notification_switch';
@import 'pages/post_single';
@import 'pages/post_front';
@import 'pages/page_bookmarks';
diff --git a/assets/styles/components/_entry.scss b/assets/styles/components/_entry.scss
index aacbcabfb..0fd921fb8 100644
--- a/assets/styles/components/_entry.scss
+++ b/assets/styles/components/_entry.scss
@@ -331,7 +331,7 @@
text-decoration: underline;
}
- button, input[type='submit'], a {
+ button, input[type='submit'], a:not(.notification-setting) {
@include mbin-btn-link;
}
}
diff --git a/assets/styles/components/_magazine.scss b/assets/styles/components/_magazine.scss
index bbdf91a07..c17ba6afe 100644
--- a/assets/styles/components/_magazine.scss
+++ b/assets/styles/components/_magazine.scss
@@ -51,11 +51,14 @@
}
}
+ &__description {
+ margin-top: 2.5rem;
+ }
+
&__subscribe {
display: flex;
flex-direction: row;
justify-content: center;
- margin-bottom: 2.5rem;
flex-wrap: wrap;
div {
diff --git a/assets/styles/components/_notification_switch.scss b/assets/styles/components/_notification_switch.scss
new file mode 100644
index 000000000..e140bd085
--- /dev/null
+++ b/assets/styles/components/_notification_switch.scss
@@ -0,0 +1,58 @@
+.notification-switch-container .notification-switch {
+ align-items: center;
+ justify-content: center;
+}
+
+.entry-info,
+.user-main {
+ .notification-switch > * {
+ opacity: .75;
+ }
+}
+
+footer .notification-switch {
+ padding: 0.25em 0;
+ margin-top: 0;
+ line-height: 1.25em;
+}
+
+.notification-switch {
+ display: flex;
+ flex-direction: row;
+ margin-top: .25em;
+ line-height: 1.5;
+
+ >* {
+ cursor: pointer;
+ padding: .25em .375em;
+ border: var(--kbin-button-secondary-border);
+ background: var(--kbin-button-secondary-bg);
+ color: var(--kbin-button-secondary-text-color);
+
+ &:hover:not(.active) {
+ background: var(--kbin-button-secondary-hover-bg);
+ color: var(--kbin-button-secondary-text-hover-color);
+ }
+
+ &.active {
+ cursor: unset;
+ background: var(--kbin-button-primary-bg);
+ color: var(--kbin-button-primary-text-color);
+
+ &:hover {
+ background: var(--kbin-button-primary-hover-bg);
+ color: var(--kbin-button-primary-text-hover-color);
+ }
+ }
+
+ &:last-child {
+ border-radius: 0 1em 1em 0;
+ padding-right: .75em;
+ }
+
+ &:first-child {
+ border-radius: 1em 0 0 1em;
+ padding-left: .75em;
+ }
+ }
+}
diff --git a/assets/styles/components/_popover.scss b/assets/styles/components/_popover.scss
index c97cf0cd5..d5352dbbe 100644
--- a/assets/styles/components/_popover.scss
+++ b/assets/styles/components/_popover.scss
@@ -20,12 +20,12 @@
z-index: var(--z-index-popover, 25);
a {
- color: var(--kbin-meta-link-color) !important;
+ color: var(--kbin-meta-link-color);
line-height: normal;
display: inline-block;
&:hover {
- color: var(--kbin-meta-link-color-hover) !important;
+ color: var(--kbin-meta-link-color-hover);
}
}
}
diff --git a/assets/styles/components/_post.scss b/assets/styles/components/_post.scss
index 533e5f3cb..267a15508 100644
--- a/assets/styles/components/_post.scss
+++ b/assets/styles/components/_post.scss
@@ -56,7 +56,7 @@
margin-bottom: 0;
opacity: .75;
- a {
+ a:not(.notification-setting) {
color: var(--kbin-meta-link-color);
font-weight: bold;
@@ -72,7 +72,7 @@
}
}
- aside {
+ aside:not(.notification-switch) {
grid-area: vote;
}
@@ -131,13 +131,13 @@
line-height: 1rem;
}
- & > a.active,
+ & > a:not(.notification-setting).active,
& > li button.active {
text-decoration: underline;
}
button,
- a {
+ a:not(.notification-setting) {
font-size: .8rem;
@include mbin-btn-link;
}
@@ -147,7 +147,7 @@
}
}
- a {
+ a:not(.notification-setting) {
@include mbin-btn-link;
}
diff --git a/assets/styles/components/_sidebar.scss b/assets/styles/components/_sidebar.scss
index 0a5d92c89..21d7a87ae 100644
--- a/assets/styles/components/_sidebar.scss
+++ b/assets/styles/components/_sidebar.scss
@@ -244,7 +244,7 @@
}
}
- a {
+ a:not(.notification-setting) {
color: var(--kbin-meta-link-color);
}
@@ -257,6 +257,10 @@
}
}
+ .entry-info ul.info {
+ margin-top: 2.5rem;
+ }
+
.settings {
display: flex;
gap: 1rem;
diff --git a/assets/styles/components/_user.scss b/assets/styles/components/_user.scss
index 42e05f252..ff0990eb1 100644
--- a/assets/styles/components/_user.scss
+++ b/assets/styles/components/_user.scss
@@ -3,7 +3,6 @@
display: flex;
flex-direction: row;
justify-content: center;
- margin-bottom: 2.5rem;
opacity: .75;
div {
diff --git a/assets/styles/layout/_section.scss b/assets/styles/layout/_section.scss
index 47e75a8eb..6e3e77199 100644
--- a/assets/styles/layout/_section.scss
+++ b/assets/styles/layout/_section.scss
@@ -5,7 +5,7 @@
margin-bottom: .5rem;
padding: 2rem 1rem;
- a {
+ a:not(.notification-setting) {
color: var(--kbin-section-title-link-color);
overflow-wrap: anywhere;
diff --git a/config/mbin_routes/notification_settings.yaml b/config/mbin_routes/notification_settings.yaml
new file mode 100644
index 000000000..ed4b6a457
--- /dev/null
+++ b/config/mbin_routes/notification_settings.yaml
@@ -0,0 +1,6 @@
+change_notification_setting:
+ controller: App\Controller\NotificationSettingsController::changeSetting
+ path: /cns/{subject_type}/{subject_id}/{status}
+ requirements:
+ subject_type: user|magazine|entry|post
+ status: Default|Loud|Muted
diff --git a/config/mbin_routes/notification_settings_api.yaml b/config/mbin_routes/notification_settings_api.yaml
new file mode 100644
index 000000000..5107b6c3d
--- /dev/null
+++ b/config/mbin_routes/notification_settings_api.yaml
@@ -0,0 +1,8 @@
+api_notification_settings_update:
+ controller: App\Controller\Api\Notification\NotificationSettingApi::update
+ path: /api/notification/update/{targetType}/{targetId}/{setting}
+ requirements:
+ targetType: entry|post|magazine|user
+ setting: Default|Loud|Muted
+ methods: [ GET ]
+ format: json
diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml
index 809607c02..8564532dc 100644
--- a/config/packages/doctrine.yaml
+++ b/config/packages/doctrine.yaml
@@ -4,10 +4,12 @@ doctrine:
types:
citext: App\DoctrineExtensions\DBAL\Types\Citext
enumApplicationStatus: App\DoctrineExtensions\DBAL\Types\EnumApplicationStatus
+ enumNotificationStatus: App\DoctrineExtensions\DBAL\Types\EnumNotificationStatus
mapping_types:
user_type: string
citext: citext
enumApplicationStatus: string
+ enumNotificationStatus: string
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
diff --git a/migrations/Version20241125210454.php b/migrations/Version20241125210454.php
new file mode 100644
index 000000000..0a7ab28e2
--- /dev/null
+++ b/migrations/Version20241125210454.php
@@ -0,0 +1,47 @@
+addSql('CREATE TYPE enumNotificationStatus AS ENUM(\'Default\', \'Muted\', \'Loud\')');
+ $this->addSql('CREATE SEQUENCE notification_settings_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
+ $this->addSql('CREATE TABLE notification_settings (id INT NOT NULL, user_id INT NOT NULL, entry_id INT DEFAULT NULL, post_id INT DEFAULT NULL, magazine_id INT DEFAULT NULL, target_user_id INT DEFAULT NULL, notification_status enumNotificationStatus DEFAULT \'Default\' NOT NULL, PRIMARY KEY(id))');
+ $this->addSql('CREATE INDEX IDX_B0559860A76ED395 ON notification_settings (user_id)');
+ $this->addSql('CREATE INDEX IDX_B0559860BA364942 ON notification_settings (entry_id)');
+ $this->addSql('CREATE INDEX IDX_B05598604B89032C ON notification_settings (post_id)');
+ $this->addSql('CREATE INDEX IDX_B05598603EB84A1D ON notification_settings (magazine_id)');
+ $this->addSql('CREATE INDEX IDX_B05598606C066AFE ON notification_settings (target_user_id)');
+ $this->addSql('CREATE UNIQUE INDEX notification_settings_user_target ON notification_settings (user_id, entry_id, post_id, magazine_id, target_user_id)');
+ $this->addSql('COMMENT ON COLUMN notification_settings.notification_status IS \'(DC2Type:EnumNotificationStatus)\'');
+ $this->addSql('ALTER TABLE notification_settings ADD CONSTRAINT FK_B0559860A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE notification_settings ADD CONSTRAINT FK_B0559860BA364942 FOREIGN KEY (entry_id) REFERENCES entry (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE notification_settings ADD CONSTRAINT FK_B05598604B89032C FOREIGN KEY (post_id) REFERENCES post (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE notification_settings ADD CONSTRAINT FK_B05598603EB84A1D FOREIGN KEY (magazine_id) REFERENCES magazine (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('ALTER TABLE notification_settings ADD CONSTRAINT FK_B05598606C066AFE FOREIGN KEY (target_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP SEQUENCE notification_settings_id_seq CASCADE');
+ $this->addSql('ALTER TABLE notification_settings DROP CONSTRAINT FK_B0559860A76ED395');
+ $this->addSql('ALTER TABLE notification_settings DROP CONSTRAINT FK_B0559860BA364942');
+ $this->addSql('ALTER TABLE notification_settings DROP CONSTRAINT FK_B05598604B89032C');
+ $this->addSql('ALTER TABLE notification_settings DROP CONSTRAINT FK_B05598603EB84A1D');
+ $this->addSql('ALTER TABLE notification_settings DROP CONSTRAINT FK_B05598606C066AFE');
+ $this->addSql('DROP TABLE notification_settings');
+ $this->addSql('DROP TYPE enumNotificationStatus');
+ }
+}
diff --git a/src/Controller/Api/BaseApi.php b/src/Controller/Api/BaseApi.php
index fbc63903d..fe8c515ac 100644
--- a/src/Controller/Api/BaseApi.php
+++ b/src/Controller/Api/BaseApi.php
@@ -6,6 +6,7 @@
use App\Controller\AbstractController;
use App\DTO\MagazineDto;
+use App\DTO\MagazineResponseDto;
use App\DTO\ReportDto;
use App\DTO\ReportRequestDto;
use App\DTO\UserDto;
@@ -36,6 +37,7 @@
use App\Repository\EntryCommentRepository;
use App\Repository\EntryRepository;
use App\Repository\ImageRepository;
+use App\Repository\NotificationSettingsRepository;
use App\Repository\OAuth2ClientAccessRepository;
use App\Repository\PostCommentRepository;
use App\Repository\PostRepository;
@@ -98,6 +100,7 @@ public function __construct(
private readonly ImageRepository $imageRepository,
private readonly ReportManager $reportManager,
private readonly OAuth2ClientAccessRepository $clientAccessRepository,
+ protected readonly NotificationSettingsRepository $notificationSettingsRepository,
) {
}
@@ -297,12 +300,16 @@ protected function serializeLogItem(MagazineLog $log): array
*
* @param MagazineDto $dto The MagazineDto to serialize
*
- * @return array An associative array representation of the entry's safe fields, to be used as JSON
+ * @return MagazineResponseDto An associative array representation of the entry's safe fields, to be used as JSON
*/
- protected function serializeMagazine(MagazineDto $dto)
+ protected function serializeMagazine(MagazineDto $dto): MagazineResponseDto
{
$response = $this->magazineFactory->createResponseDto($dto);
+ if ($user = $this->getUser()) {
+ $response->notificationStatus = $this->notificationSettingsRepository->findOneByTarget($user, $dto);
+ }
+
return $response;
}
@@ -317,6 +324,10 @@ protected function serializeUser(UserDto $dto): UserResponseDto
{
$response = new UserResponseDto($dto);
+ if ($user = $this->getUser()) {
+ $response->notificationStatus = $this->notificationSettingsRepository->findOneByTarget($user, $dto);
+ }
+
return $response;
}
diff --git a/src/Controller/Api/Entry/EntriesBaseApi.php b/src/Controller/Api/Entry/EntriesBaseApi.php
index 8b34c280f..16de488fe 100644
--- a/src/Controller/Api/Entry/EntriesBaseApi.php
+++ b/src/Controller/Api/Entry/EntriesBaseApi.php
@@ -46,6 +46,7 @@ protected function serializeEntry(EntryDto|Entry $dto, array $tags): EntryRespon
if ($user = $this->getUser()) {
$response->canAuthUserModerate = $dto->getMagazine()->userIsModerator($user) || $user->isModerator() || $user->isAdmin();
+ $response->notificationStatus = $this->notificationSettingsRepository->findOneByTarget($user, $dto);
}
return $response;
diff --git a/src/Controller/Api/Notification/NotificationSettingApi.php b/src/Controller/Api/Notification/NotificationSettingApi.php
new file mode 100644
index 000000000..f2ce3d8b7
--- /dev/null
+++ b/src/Controller/Api/Notification/NotificationSettingApi.php
@@ -0,0 +1,107 @@
+rateLimit($apiUpdateLimiter);
+ $user = $this->getUserOrThrow();
+ $notificationSetting = ENotificationStatus::getFromString($setting);
+ if ('entry' === $targetType) {
+ $repo = $this->entityManager->getRepository(Entry::class);
+ } elseif ('post' === $targetType) {
+ $repo = $this->entityManager->getRepository(Post::class);
+ } elseif ('magazine' === $targetType) {
+ $repo = $this->entityManager->getRepository(Magazine::class);
+ } elseif ('user' === $targetType) {
+ $repo = $this->entityManager->getRepository(User::class);
+ } else {
+ throw new \LogicException();
+ }
+ $target = $repo->find($targetId);
+ if (null === $target) {
+ throw $this->createNotFoundException();
+ }
+ $this->notificationSettingsRepository->setStatusByTarget($user, $target, $notificationSetting);
+
+ return new JsonResponse();
+ }
+}
diff --git a/src/Controller/Api/Post/PostsBaseApi.php b/src/Controller/Api/Post/PostsBaseApi.php
index d1df4625b..90a0c677d 100644
--- a/src/Controller/Api/Post/PostsBaseApi.php
+++ b/src/Controller/Api/Post/PostsBaseApi.php
@@ -32,6 +32,7 @@ protected function serializePost(PostDto $dto, array $tags): PostResponseDto
if ($user = $this->getUser()) {
$response->canAuthUserModerate = $dto->getMagazine()->userIsModerator($user) || $user->isModerator() || $user->isAdmin();
+ $response->notificationStatus = $this->notificationSettingsRepository->findOneByTarget($user, $dto);
}
return $response;
diff --git a/src/Controller/NotificationSettingsController.php b/src/Controller/NotificationSettingsController.php
new file mode 100644
index 000000000..fefecdc0f
--- /dev/null
+++ b/src/Controller/NotificationSettingsController.php
@@ -0,0 +1,59 @@
+entityManager->getRepository(self::GetClassFromSubjectType($subject_type))->findOneBy(['id' => $subject_id]);
+ $user = $this->getUserOrThrow();
+ $this->repository->setStatusByTarget($user, $subject, $status);
+ if ($request->isXmlHttpRequest()) {
+ return new JsonResponse([
+ 'html' => $this->renderView('components/_ajax.html.twig', [
+ 'component' => 'notification_switch',
+ 'attributes' => [
+ 'target' => $subject,
+ ],
+ ]
+ ),
+ ]);
+ }
+
+ return $this->redirect($request->headers->get('Referer'));
+ }
+
+ protected static function GetClassFromSubjectType(string $subjectType): string
+ {
+ return match ($subjectType) {
+ 'entry' => Entry::class,
+ 'post' => Post::class,
+ 'user' => User::class,
+ 'magazine' => Magazine::class,
+ default => throw new \LogicException("cannot match type $subjectType")
+ };
+ }
+}
diff --git a/src/DTO/EntryResponseDto.php b/src/DTO/EntryResponseDto.php
index 8d761a624..7d548899d 100644
--- a/src/DTO/EntryResponseDto.php
+++ b/src/DTO/EntryResponseDto.php
@@ -6,6 +6,7 @@
use App\DTO\Contracts\VisibilityAwareDtoTrait;
use App\Entity\Entry;
+use App\Enums\ENotificationStatus;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
@@ -45,6 +46,7 @@ class EntryResponseDto implements \JsonSerializable
public ?string $slug = null;
public ?string $apId = null;
public ?bool $canAuthUserModerate = null;
+ public ?ENotificationStatus $notificationStatus = null;
public static function create(
?int $id = null,
@@ -154,6 +156,7 @@ public function jsonSerialize(): mixed
'slug' => $this->slug,
'apId' => $this->apId,
'canAuthUserModerate' => $this->canAuthUserModerate,
+ 'notificationStatus' => $this->notificationStatus,
]);
}
}
diff --git a/src/DTO/MagazineResponseDto.php b/src/DTO/MagazineResponseDto.php
index fcfa57a83..a1f80ad1b 100644
--- a/src/DTO/MagazineResponseDto.php
+++ b/src/DTO/MagazineResponseDto.php
@@ -4,6 +4,7 @@
namespace App\DTO;
+use App\Enums\ENotificationStatus;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
@@ -37,6 +38,7 @@ class MagazineResponseDto implements \JsonSerializable
public ?string $serverSoftwareVersion = null;
public bool $isPostingRestrictedToMods = false;
public ?int $localSubscribers = null;
+ public ?ENotificationStatus $notificationStatus = null;
public static function create(
?ModeratorResponseDto $owner = null,
@@ -120,6 +122,7 @@ public function jsonSerialize(): mixed
'serverSoftwareVersion' => $this->serverSoftwareVersion,
'isPostingRestrictedToMods' => $this->isPostingRestrictedToMods,
'localSubscribers' => $this->localSubscribers,
+ 'notificationStatus' => $this->notificationStatus,
];
}
}
diff --git a/src/DTO/PostResponseDto.php b/src/DTO/PostResponseDto.php
index c17d86a0a..330af2afe 100644
--- a/src/DTO/PostResponseDto.php
+++ b/src/DTO/PostResponseDto.php
@@ -5,6 +5,7 @@
namespace App\DTO;
use App\DTO\Contracts\VisibilityAwareDtoTrait;
+use App\Enums\ENotificationStatus;
use OpenApi\Attributes as OA;
#[OA\Schema()]
@@ -37,6 +38,7 @@ class PostResponseDto implements \JsonSerializable
public ?\DateTimeImmutable $editedAt = null;
public ?\DateTime $lastActive = null;
public ?bool $canAuthUserModerate = null;
+ public ?ENotificationStatus $notificationStatus = null;
public static function create(
int $id,
@@ -128,6 +130,7 @@ public function jsonSerialize(): mixed
'lastActive' => $this->lastActive?->format(\DateTimeInterface::ATOM),
'slug' => $this->slug,
'canAuthUserModerate' => $this->canAuthUserModerate,
+ 'notificationStatus' => $this->notificationStatus,
]);
}
}
diff --git a/src/DTO/UserResponseDto.php b/src/DTO/UserResponseDto.php
index a9f96308d..3f7efa44d 100644
--- a/src/DTO/UserResponseDto.php
+++ b/src/DTO/UserResponseDto.php
@@ -4,6 +4,7 @@
namespace App\DTO;
+use App\Enums\ENotificationStatus;
use OpenApi\Attributes as OA;
#[OA\Schema()]
@@ -26,6 +27,7 @@ class UserResponseDto implements \JsonSerializable
public ?int $userId = null;
public ?string $serverSoftware = null;
public ?string $serverSoftwareVersion = null;
+ public ?ENotificationStatus $notificationStatus = null;
public function __construct(UserDto $dto)
{
@@ -68,6 +70,7 @@ public function jsonSerialize(): mixed
'isBlockedByUser' => $this->isBlockedByUser,
'serverSoftware' => $this->serverSoftware,
'serverSoftwareVersion' => $this->serverSoftwareVersion,
+ 'notificationStatus' => $this->notificationStatus,
];
}
}
diff --git a/src/DoctrineExtensions/DBAL/Types/EnumNotificationStatus.php b/src/DoctrineExtensions/DBAL/Types/EnumNotificationStatus.php
new file mode 100644
index 000000000..25723ae82
--- /dev/null
+++ b/src/DoctrineExtensions/DBAL/Types/EnumNotificationStatus.php
@@ -0,0 +1,20 @@
+ ENotificationStatus::Default->value])]
+ private string $notificationStatus = ENotificationStatus::Default->value;
+
+ public function __construct(User $user, Entry|Post|User|Magazine $target, ENotificationStatus $status)
+ {
+ $this->user = $user;
+ $this->setStatus($status);
+ if ($target instanceof User) {
+ $this->targetUser = $target;
+ } elseif ($target instanceof Magazine) {
+ $this->magazine = $target;
+ } elseif ($target instanceof Entry) {
+ $this->entry = $target;
+ } elseif ($target instanceof Post) {
+ $this->post = $target;
+ }
+ }
+
+ public function setStatus(ENotificationStatus $status): void
+ {
+ $this->notificationStatus = $status->value;
+ }
+
+ public function getStatus(): ENotificationStatus
+ {
+ return ENotificationStatus::getFromString($this->notificationStatus);
+ }
+}
diff --git a/src/Enums/ENotificationStatus.php b/src/Enums/ENotificationStatus.php
new file mode 100644
index 000000000..18f177d48
--- /dev/null
+++ b/src/Enums/ENotificationStatus.php
@@ -0,0 +1,36 @@
+value => self::Default,
+ self::Muted->value => self::Muted,
+ self::Loud->value => self::Loud,
+ default => null,
+ };
+ }
+
+ public const Values = [
+ ENotificationStatus::Default->value,
+ ENotificationStatus::Muted->value,
+ ENotificationStatus::Loud->value,
+ ];
+
+ /**
+ * @return string[]
+ */
+ public static function getValues(): array
+ {
+ return self::Values;
+ }
+}
diff --git a/src/Repository/NotificationSettingsRepository.php b/src/Repository/NotificationSettingsRepository.php
new file mode 100644
index 000000000..b8541dcb9
--- /dev/null
+++ b/src/Repository/NotificationSettingsRepository.php
@@ -0,0 +1,188 @@
+createQueryBuilder('ns')
+ ->where('ns.user = :user');
+
+ if ($target instanceof User || $target instanceof UserDto) {
+ $qb->andWhere('ns.targetUser = :target');
+ } elseif ($target instanceof Magazine || $target instanceof MagazineDto) {
+ $qb->andWhere('ns.magazine = :target');
+ } elseif ($target instanceof Entry || $target instanceof EntryDto) {
+ $qb->andWhere('ns.entry = :target');
+ } elseif ($target instanceof Post || $target instanceof PostDto) {
+ $qb->andWhere('ns.post = :target');
+ }
+ $qb->setParameter('target', $target->getId());
+ $qb->setParameter('user', $user);
+
+ return $qb->getQuery()
+ ->getOneOrNullResult();
+ }
+
+ public function setStatusByTarget(User $user, Entry|Post|User|Magazine $target, ENotificationStatus $status): void
+ {
+ $setting = $this->findOneByTarget($user, $target);
+ if (null === $setting) {
+ $setting = new NotificationSettings($user, $target, $status);
+ } else {
+ $setting->setStatus($status);
+ }
+ $this->entityManager->persist($setting);
+ $this->entityManager->flush();
+ }
+
+ /**
+ * gets the users that should be notified about the created of $target. This respects user and magazine blocks
+ * as well as custom notification settings and the users default notification settings.
+ *
+ * @return int[]
+ *
+ * @throws Exception
+ */
+ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|PostComment $target): array
+ {
+ if ($target instanceof Entry || $target instanceof EntryComment) {
+ $targetCol = 'entry_id';
+ if ($target instanceof Entry) {
+ $targetId = $target->getId();
+ $notifyCol = 'notify_on_new_entry';
+ $activateMagazineNotifications = true;
+ $dontNeedSubscription = false;
+ $dontNeedToBeAuthor = true;
+ $targetParentUserId = null;
+ } else {
+ $targetId = $target->entry->getId();
+ if (null === $target->parent) {
+ $notifyCol = 'notify_on_new_entry_reply';
+ $targetParentUserId = $target->entry->user->getId();
+ } else {
+ $notifyCol = 'notify_on_new_entry_comment_reply';
+ $targetParentUserId = $target->parent->user->getId();
+ }
+ $activateMagazineNotifications = false;
+ $dontNeedSubscription = true;
+ $dontNeedToBeAuthor = false;
+ }
+ } else {
+ $targetCol = 'post_id';
+ if ($target instanceof Post) {
+ $targetId = $target->getId();
+ $notifyCol = 'notify_on_new_post';
+ $activateMagazineNotifications = true;
+ $dontNeedSubscription = false;
+ $dontNeedToBeAuthor = true;
+ $targetParentUserId = null;
+ } else {
+ $targetId = $target->post->getId();
+ if (null === $target->parent) {
+ $notifyCol = 'notify_on_new_post_reply';
+ $targetParentUserId = $target->post->user->getId();
+ } else {
+ $notifyCol = 'notify_on_new_post_comment_reply';
+ $targetParentUserId = $target->parent->user->getId();
+ }
+ $activateMagazineNotifications = false;
+ $dontNeedSubscription = true;
+ $dontNeedToBeAuthor = false;
+ }
+ }
+
+ $activateMagazineNotificationsString = $activateMagazineNotifications ? 'true' : 'false';
+ $dontNeedSubscriptionString = $dontNeedSubscription ? 'true' : 'false';
+ $dontNeedToBeAuthorString = $dontNeedToBeAuthor ? 'true' : 'false';
+
+ $sql = "SELECT u.id FROM \"user\" u
+ LEFT JOIN notification_settings ns_user ON ns_user.user_id = u.id AND ns_user.target_user_id = :targetUserId
+ LEFT JOIN notification_settings ns_post ON ns_post.user_id = u.id AND ns_post.$targetCol = :targetId
+ LEFT JOIN notification_settings ns_mag ON ns_mag.user_id = u.id AND ns_mag.magazine_id = :magId
+ WHERE
+ u.ap_id IS NULL
+ AND u.id <> :targetUserId
+ AND (
+ COALESCE(ns_user.notification_status, :normal) = :loud
+ OR (
+ COALESCE(ns_user.notification_status, :normal) = :normal
+ AND COALESCE(ns_post.notification_status, :normal) = :loud
+ )
+ OR (
+ COALESCE(ns_user.notification_status, :normal) = :normal
+ AND COALESCE(ns_post.notification_status, :normal) = :normal
+ AND COALESCE(ns_mag.notification_status, :normal) = :loud
+ -- deactivate loud magazine notifications for comments
+ AND $activateMagazineNotificationsString
+ )
+ OR (
+ COALESCE(ns_user.notification_status, :normal) = :normal
+ AND COALESCE(ns_post.notification_status, :normal) = :normal
+ AND COALESCE(ns_mag.notification_status, :normal) = :normal
+ AND u.$notifyCol = true
+ AND (
+ -- deactivate magazine subscription need for comments
+ $dontNeedSubscriptionString
+ OR EXISTS (SELECT * FROM magazine_subscription ms WHERE ms.user_id = u.id AND ms.magazine_id = :magId)
+ )
+ AND (
+ -- deactivate the need to be the author of the parent to receive notifications
+ $dontNeedToBeAuthorString
+ OR u.id = :targetParentUserId
+ )
+ )
+ )
+ AND NOT EXISTS (SELECT * FROM user_block ub WHERE ub.blocker_id = u.id AND ub.blocked_id = :targetUserId)
+ ";
+ $conn = $this->getEntityManager()->getConnection();
+ $stmt = $conn->prepare($sql);
+ $result = $stmt->executeQuery([
+ 'normal' => ENotificationStatus::Default->value,
+ 'loud' => ENotificationStatus::Loud->value,
+ 'targetUserId' => $target->user->getId(),
+ 'targetId' => $targetId,
+ 'magId' => $target->magazine->getId(),
+ 'targetParentUserId' => $targetParentUserId,
+ ]);
+ $rows = $result->fetchAllAssociative();
+ $this->logger->debug('got subscribers for target {c} id {id}: {subs}', ['c' => \get_class($target), 'id' => $target->getId(), 'subs' => $rows]);
+
+ return array_map(fn (array $row) => $row['id'], $rows);
+ }
+}
diff --git a/src/Service/Notification/EntryCommentNotificationManager.php b/src/Service/Notification/EntryCommentNotificationManager.php
index 55ca0b736..5a6c2e9f2 100644
--- a/src/Service/Notification/EntryCommentNotificationManager.php
+++ b/src/Service/Notification/EntryCommentNotificationManager.php
@@ -18,6 +18,8 @@
use App\Repository\MagazineLogRepository;
use App\Repository\MagazineSubscriptionRepository;
use App\Repository\NotificationRepository;
+use App\Repository\NotificationSettingsRepository;
+use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
@@ -49,11 +51,12 @@ public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ImageManager $imageManager,
private readonly GenerateHtmlClassService $classService,
- private readonly SettingsManager $settingsManager
+ private readonly SettingsManager $settingsManager,
+ private readonly NotificationSettingsRepository $notificationSettingsRepository,
+ private readonly UserRepository $userRepository,
) {
}
- // @todo check if author is on the block list
public function sendCreated(ContentInterface $subject): void
{
if ($subject->user->isBanned || $subject->user->isDeleted || $subject->user->isTrashed() || $subject->user->isSoftDeleted()) {
@@ -62,10 +65,30 @@ public function sendCreated(ContentInterface $subject): void
if (!$subject instanceof EntryComment) {
throw new \LogicException();
}
+ $comment = $subject;
- $users = $this->sendMentionedNotification($subject);
- $users = $this->sendUserReplyNotification($subject, $users);
- $this->sendMagazineSubscribersNotification($subject, $users);
+ $mentioned = $this->sendMentionedNotification($comment);
+
+ $this->notifyMagazine(new EntryCommentCreatedNotification($comment->user, $comment));
+
+ $userIdsToNotify = $this->notificationSettingsRepository->findNotificationSubscribersByTarget($comment);
+ $usersToNotify = $this->userRepository->findBy(['id' => $userIdsToNotify]);
+
+ if (\count($mentioned)) {
+ $usersToNotify = array_filter($usersToNotify, fn ($user) => !\in_array($user, $mentioned));
+ }
+
+ foreach ($usersToNotify as $subscriber) {
+ if (null !== $comment->parent && $comment->parent->isAuthor($subscriber)) {
+ $notification = new EntryCommentReplyNotification($subscriber, $comment);
+ } else {
+ $notification = new EntryCommentCreatedNotification($subscriber, $comment);
+ }
+ $this->entityManager->persist($notification);
+ $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
+ }
+
+ $this->entityManager->flush();
}
private function sendMentionedNotification(EntryComment $subject): array
@@ -86,41 +109,6 @@ private function sendMentionedNotification(EntryComment $subject): array
return $users;
}
- private function sendUserReplyNotification(EntryComment $comment, array $exclude): array
- {
- if (!$comment->parent || $comment->parent->isAuthor($comment->user)) {
- return $exclude;
- }
-
- if (!$comment->parent->user->notifyOnNewEntryCommentReply) {
- return $exclude;
- }
-
- if (\in_array($comment->parent->user, $exclude)) {
- return $exclude;
- }
-
- if ($comment->parent->user->apId) {
- // @todo activtypub
- $exclude[] = $comment->parent->user;
-
- return $exclude;
- }
-
- if (!$comment->parent->user->isBlocked($comment->user)) {
- $notification = new EntryCommentReplyNotification($comment->parent->user, $comment);
- $this->notifyUser($notification);
-
- $this->entityManager->persist($notification);
- $this->entityManager->flush();
- $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
-
- $exclude[] = $notification->user;
- }
-
- return $exclude;
- }
-
private function notifyUser(EntryCommentReplyNotification $notification): void
{
if (false === $this->settingsManager->get('KBIN_MERCURE_ENABLED')) {
@@ -177,31 +165,6 @@ private function getResponse(Notification $notification): string
);
}
- private function sendMagazineSubscribersNotification(EntryComment $comment, array $exclude): void
- {
- $this->notifyMagazine(new EntryCommentCreatedNotification($comment->user, $comment));
-
- $usersToNotify = []; // @todo user followers
- if ($comment->entry->user->notifyOnNewEntryReply && !$comment->isAuthor($comment->entry->user)) {
- $usersToNotify = $this->merge(
- $usersToNotify,
- [$comment->entry->user]
- );
- }
-
- if (\count($exclude)) {
- $usersToNotify = array_filter($usersToNotify, fn ($user) => !\in_array($user, $exclude));
- }
-
- foreach ($usersToNotify as $subscriber) {
- $notification = new EntryCommentCreatedNotification($subscriber, $comment);
- $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
- $this->entityManager->persist($notification);
- }
-
- $this->entityManager->flush();
- }
-
private function notifyMagazine(Notification $notification): void
{
if (false === $this->settingsManager->get('KBIN_MERCURE_ENABLED')) {
diff --git a/src/Service/Notification/EntryNotificationManager.php b/src/Service/Notification/EntryNotificationManager.php
index a7f97bb24..174a4c463 100644
--- a/src/Service/Notification/EntryNotificationManager.php
+++ b/src/Service/Notification/EntryNotificationManager.php
@@ -12,12 +12,13 @@
use App\Entity\EntryMentionedNotification;
use App\Entity\Magazine;
use App\Entity\Notification;
-use App\Entity\User;
use App\Event\NotificationCreatedEvent;
use App\Factory\MagazineFactory;
use App\Repository\MagazineLogRepository;
use App\Repository\MagazineSubscriptionRepository;
use App\Repository\NotificationRepository;
+use App\Repository\NotificationSettingsRepository;
+use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
@@ -50,7 +51,9 @@ public function __construct(
private readonly ImageManager $imageManager,
private readonly GenerateHtmlClassService $classService,
private readonly UserManager $userManager,
- private readonly SettingsManager $settingsManager
+ private readonly SettingsManager $settingsManager,
+ private readonly NotificationSettingsRepository $notificationSettingsRepository,
+ private readonly UserRepository $userRepository,
) {
}
@@ -80,20 +83,17 @@ public function sendCreated(ContentInterface $subject): void
}
// Notify subscribers
- /** @var User[] $subscribers */
- $subscribers = $this->merge(
- $this->getUsersToNotify($this->magazineRepository->findNewEntrySubscribers($subject)),
- [] // @todo user followers
- );
+ $subscriberIds = $this->notificationSettingsRepository->findNotificationSubscribersByTarget($subject);
+ $subscribers = $this->userRepository->findBy(['id' => $subscriberIds]);
- $subscribers = array_filter($subscribers, fn ($s) => !\in_array($s->username, $mentions ?? []));
+ if (\count($mentions)) {
+ $subscribers = array_filter($subscribers, fn ($s) => !\in_array($s->username, $mentions ?? []));
+ }
foreach ($subscribers as $subscriber) {
- if (!$subscriber->isBlocked($subject->user)) {
- $notification = new EntryCreatedNotification($subscriber, $subject);
- $this->entityManager->persist($notification);
- $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
- }
+ $notification = new EntryCreatedNotification($subscriber, $subject);
+ $this->entityManager->persist($notification);
+ $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
}
$this->entityManager->flush();
diff --git a/src/Service/Notification/PostCommentNotificationManager.php b/src/Service/Notification/PostCommentNotificationManager.php
index d7a8241a1..212488ead 100644
--- a/src/Service/Notification/PostCommentNotificationManager.php
+++ b/src/Service/Notification/PostCommentNotificationManager.php
@@ -18,6 +18,8 @@
use App\Repository\MagazineLogRepository;
use App\Repository\MagazineSubscriptionRepository;
use App\Repository\NotificationRepository;
+use App\Repository\NotificationSettingsRepository;
+use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
@@ -49,7 +51,9 @@ public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ImageManager $imageManager,
private readonly GenerateHtmlClassService $classService,
- private readonly SettingsManager $settingsManager
+ private readonly SettingsManager $settingsManager,
+ private readonly NotificationSettingsRepository $notificationSettingsRepository,
+ private readonly UserRepository $userRepository,
) {
}
@@ -61,10 +65,29 @@ public function sendCreated(ContentInterface $subject): void
if (!$subject instanceof PostComment) {
throw new \LogicException();
}
+ $comment = $subject;
- $users = $this->sendMentionedNotification($subject);
- $users = $this->sendUserReplyNotification($subject, $users);
- $this->sendMagazineSubscribersNotification($subject, $users);
+ $mentions = $this->sendMentionedNotification($subject);
+ $this->notifyMagazine(new PostCommentCreatedNotification($comment->user, $comment));
+
+ $userIdsToNotify = $this->notificationSettingsRepository->findNotificationSubscribersByTarget($comment);
+ $usersToNotify = $this->userRepository->findBy(['id' => $userIdsToNotify]);
+
+ if (\count($mentions)) {
+ $usersToNotify = array_filter($usersToNotify, fn ($user) => !\in_array($user, $mentions));
+ }
+
+ foreach ($usersToNotify as $subscriber) {
+ if (null !== $comment->parent && $comment->parent->isAuthor($subscriber)) {
+ $notification = new PostCommentReplyNotification($subscriber, $comment);
+ } else {
+ $notification = new PostCommentCreatedNotification($subscriber, $comment);
+ }
+ $this->entityManager->persist($notification);
+ $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
+ }
+
+ $this->entityManager->flush();
}
private function sendMentionedNotification(PostComment $subject): array
@@ -85,41 +108,6 @@ private function sendMentionedNotification(PostComment $subject): array
return $users;
}
- private function sendUserReplyNotification(PostComment $comment, array $exclude): array
- {
- if (!$comment->parent || $comment->parent->isAuthor($comment->user)) {
- return $exclude;
- }
-
- if (!$comment->parent->user->notifyOnNewPostCommentReply) {
- return $exclude;
- }
-
- if (\in_array($comment->parent->user, $exclude)) {
- return $exclude;
- }
-
- if ($comment->parent->user->apId) {
- // @todo activtypub
- $exclude[] = $comment->parent->user;
-
- return $exclude;
- }
-
- if (!$comment->parent->user->isBlocked($comment->user)) {
- $notification = new PostCommentReplyNotification($comment->parent->user, $comment);
- $this->notifyUser($notification);
-
- $this->entityManager->persist($notification);
- $this->entityManager->flush();
-
- $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
- $exclude[] = $notification->user;
- }
-
- return $exclude;
- }
-
private function notifyUser(PostCommentReplyNotification $notification): void
{
if (false === $this->settingsManager->get('KBIN_MERCURE_ENABLED')) {
@@ -175,31 +163,6 @@ private function getResponse(Notification $notification): string
);
}
- public function sendMagazineSubscribersNotification(PostComment $comment, array $exclude): void
- {
- $this->notifyMagazine(new PostCommentCreatedNotification($comment->user, $comment));
-
- $usersToNotify = []; // @todo user followers
- if ($comment->user->notifyOnNewPostReply && !$comment->isAuthor($comment->post->user)) {
- $usersToNotify = $this->merge(
- $usersToNotify,
- [$comment->post->user]
- );
- }
-
- if (\count($exclude)) {
- $usersToNotify = array_filter($usersToNotify, fn ($user) => !\in_array($user, $exclude));
- }
-
- foreach ($usersToNotify as $subscriber) {
- $notification = new PostCommentCreatedNotification($subscriber, $comment);
- $this->entityManager->persist($notification);
- $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification));
- }
-
- $this->entityManager->flush();
- }
-
private function notifyMagazine(Notification $notification): void
{
if (false === $this->settingsManager->get('KBIN_MERCURE_ENABLED')) {
diff --git a/src/Service/Notification/PostNotificationManager.php b/src/Service/Notification/PostNotificationManager.php
index 662e569c3..ff8f35767 100644
--- a/src/Service/Notification/PostNotificationManager.php
+++ b/src/Service/Notification/PostNotificationManager.php
@@ -11,12 +11,13 @@
use App\Entity\PostDeletedNotification;
use App\Entity\PostEditedNotification;
use App\Entity\PostMentionedNotification;
-use App\Entity\User;
use App\Event\NotificationCreatedEvent;
use App\Factory\MagazineFactory;
use App\Repository\MagazineLogRepository;
use App\Repository\MagazineSubscriptionRepository;
use App\Repository\NotificationRepository;
+use App\Repository\NotificationSettingsRepository;
+use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
@@ -47,7 +48,9 @@ public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ImageManager $imageManager,
private readonly GenerateHtmlClassService $classService,
- private readonly SettingsManager $settingsManager
+ private readonly SettingsManager $settingsManager,
+ private readonly NotificationSettingsRepository $notificationSettingsRepository,
+ private readonly UserRepository $userRepository,
) {
}
@@ -73,20 +76,17 @@ public function sendCreated(ContentInterface $subject): void
}
// Notify subscribers
- /** @var User[] $subscribers */
- $subscribers = $this->merge(
- $this->getUsersToNotify($this->magazineRepository->findNewPostSubscribers($subject)),
- [] // @todo user followers
- );
+ $subscriberIds = $this->notificationSettingsRepository->findNotificationSubscribersByTarget($subject);
+ $subscribers = $this->userRepository->findBy(['id' => $subscriberIds]);
- $subscribers = array_filter($subscribers, fn ($s) => !\in_array($s->username, $mentions ?? []));
+ if (\count($mentions)) {
+ $subscribers = array_filter($subscribers, fn ($s) => !\in_array($s->username, $mentions));
+ }
foreach ($subscribers as $subscriber) {
- if (!$subscriber->isBlocked($subject->user)) {
- $notification2 = new PostCreatedNotification($subscriber, $subject);
- $this->entityManager->persist($notification2);
- $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification2));
- }
+ $notification2 = new PostCreatedNotification($subscriber, $subject);
+ $this->entityManager->persist($notification2);
+ $this->eventDispatcher->dispatch(new NotificationCreatedEvent($notification2));
}
$this->entityManager->flush();
diff --git a/src/Twig/Components/NotificationSwitch.php b/src/Twig/Components/NotificationSwitch.php
new file mode 100644
index 000000000..2be49287f
--- /dev/null
+++ b/src/Twig/Components/NotificationSwitch.php
@@ -0,0 +1,38 @@
+security->getUser();
+ if ($user instanceof User) {
+ $this->status = $this->repository->findOneByTarget($user, $this->target)?->getStatus() ?? ENotificationStatus::Default;
+ }
+ }
+}
diff --git a/src/Twig/Extension/FrontExtension.php b/src/Twig/Extension/FrontExtension.php
index 8e8d81de6..c472e90e7 100644
--- a/src/Twig/Extension/FrontExtension.php
+++ b/src/Twig/Extension/FrontExtension.php
@@ -16,6 +16,7 @@ public function getFunctions(): array
new TwigFunction('front_options_url', [FrontExtensionRuntime::class, 'frontOptionsUrl']),
new TwigFunction('get_class', [FrontExtensionRuntime::class, 'getClass']),
new TwigFunction('get_subject_type', [FrontExtensionRuntime::class, 'getSubjectType']),
+ new TwigFunction('get_notification_settings_subject_type', [FrontExtensionRuntime::class, 'getNotificationSettingSubjectType']),
];
}
}
diff --git a/src/Twig/Runtime/FrontExtensionRuntime.php b/src/Twig/Runtime/FrontExtensionRuntime.php
index b52735041..7bdde2624 100644
--- a/src/Twig/Runtime/FrontExtensionRuntime.php
+++ b/src/Twig/Runtime/FrontExtensionRuntime.php
@@ -6,8 +6,10 @@
use App\Entity\Entry;
use App\Entity\EntryComment;
+use App\Entity\Magazine;
use App\Entity\Post;
use App\Entity\PostComment;
+use App\Entity\User;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Extension\RuntimeExtensionInterface;
@@ -87,4 +89,19 @@ public function getSubjectType(mixed $object): string
throw new \LogicException('unknown class '.\get_class($object));
}
}
+
+ public function getNotificationSettingSubjectType(mixed $object): string
+ {
+ if ($object instanceof Entry) {
+ return 'entry';
+ } elseif ($object instanceof Post) {
+ return 'post';
+ } elseif ($object instanceof User) {
+ return 'user';
+ } elseif ($object instanceof Magazine) {
+ return 'magazine';
+ } else {
+ throw new \LogicException('unknown class '.\get_class($object));
+ }
+ }
}
diff --git a/templates/components/bookmark_list.html.twig b/templates/components/bookmark_list.html.twig
index be65c8849..2e075df3d 100644
--- a/templates/components/bookmark_list.html.twig
+++ b/templates/components/bookmark_list.html.twig
@@ -3,7 +3,7 @@
+ data-action="html-refresh#linkCallback">
{{ 'bookmark_remove_from_list'|trans({'%list%': list.name}) }}
@@ -11,7 +11,7 @@
+ data-action="html-refresh#linkCallback">
{{ 'bookmark_add_to_list'|trans({'%list%': list.name}) }}
diff --git a/templates/components/bookmark_standard.html.twig b/templates/components/bookmark_standard.html.twig
index d8d502a9e..dd74443df 100644
--- a/templates/components/bookmark_standard.html.twig
+++ b/templates/components/bookmark_standard.html.twig
@@ -1,16 +1,16 @@
{% if is_bookmarked(app.user, subject) %}
+ data-html-refresh-cssclass-param="bookmark-standard" data-html-refresh-refreshselector-param=".bookmark-menu-list"
+ data-html-refresh-refreshlink-param="{{ path('bookmark_lists_menu_refresh_status', { 'subject_id': subject.id, 'subject_type': get_subject_type(subject) }) }}"
+ data-action="html-refresh#linkCallback">
{% else %}
+ data-html-refresh-cssclass-param="bookmark-standard" data-html-refresh-refreshselector-param=".bookmark-menu-list"
+ data-html-refresh-refreshlink-param="{{ path('bookmark_lists_menu_refresh_status', { 'subject_id': subject.id, 'subject_type': get_subject_type(subject) }) }}"
+ data-action="html-refresh#linkCallback">
{% endif %}
diff --git a/templates/components/entry.html.twig b/templates/components/entry.html.twig
index a4a402432..f26c4a0a1 100644
--- a/templates/components/entry.html.twig
+++ b/templates/components/entry.html.twig
@@ -22,7 +22,7 @@
'show-preview': SHOW_PREVIEW is same as V_TRUE and not entry.isAdult
})}).without('id') }}
id="entry-{{ entry.id }}"
- data-controller="subject preview mentions"
+ data-controller="subject preview mentions html-refresh"
data-action="notifications:Notification@window->subject#notification">
{% if entry.visibility in ['visible', 'private'] or (entry.visibility is same as 'trashed' and this.canSeeTrashed) %}
@@ -175,6 +175,11 @@
{{ component('bookmark_standard', { subject: entry }) }}
{% endif %}
{% include 'entry/_menu.html.twig' %}
+
+ {% if app.user is defined and app.user is not same as null and not showShortSentence %}
+ {{ component('notification_switch', {target: entry}) }}
+ {% endif %}
+
Loading...
diff --git a/templates/components/entry_comment.html.twig b/templates/components/entry_comment.html.twig
index 1f87f77b5..4bac7c4ac 100644
--- a/templates/components/entry_comment.html.twig
+++ b/templates/components/entry_comment.html.twig
@@ -19,7 +19,7 @@
'show-preview': SHOW_PREVIEW is same as V_TRUE and not comment.isAdult,
})}).without('id') }}
id="entry-comment-{{ comment.id }}"
- data-controller="comment subject mentions comment-collapse"
+ data-controller="comment subject mentions comment-collapse html-refresh"
data-comment-collapse-depth-value="{{ level }}"
data-subject-parent-value="{{ comment.parent ? comment.parent.id : '' }}"
data-action="{{- DYNAMIC_LISTS is same as V_TRUE ? 'notifications:Notification@window->subject#notification' : '' -}}">
diff --git a/templates/components/magazine_box.html.twig b/templates/components/magazine_box.html.twig
index d84849a53..19bc5fff9 100644
--- a/templates/components/magazine_box.html.twig
+++ b/templates/components/magazine_box.html.twig
@@ -43,6 +43,13 @@
{{ component('magazine_sub', {magazine: magazine}) }}
+
+ {% if app.user is defined and app.user is not same as null %}
+
+ {{ component('notification_switch', {target: magazine}) }}
+
+ {% endif %}
+
{% if computed.magazine.description and showDescription %}
{{ computed.magazine.description|markdown|raw }}
{% endif %}
diff --git a/templates/components/notification_switch.html.twig b/templates/components/notification_switch.html.twig
new file mode 100644
index 000000000..8d0109898
--- /dev/null
+++ b/templates/components/notification_switch.html.twig
@@ -0,0 +1,32 @@
+
diff --git a/templates/components/post.html.twig b/templates/components/post.html.twig
index d03e769ca..0f2fe0e07 100644
--- a/templates/components/post.html.twig
+++ b/templates/components/post.html.twig
@@ -15,7 +15,7 @@
'show-preview': SHOW_PREVIEW is same as V_TRUE and not post.isAdult
})}).without('id') }}
id="post-{{ post.id }}"
- data-controller="post subject mentions"
+ data-controller="post subject mentions html-refresh"
data-action="notifications:Notification@window->subject#notification">
{% if post.isAdult %}18+{% endif %}
@@ -114,6 +114,9 @@
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
{% include 'post/_menu.html.twig' %}
+ {% if app.user is defined and app.user is not same as null %}
+ {{ component('notification_switch', {target: post}) }}
+ {% endif %}
Loading...
diff --git a/templates/components/post_comment.html.twig b/templates/components/post_comment.html.twig
index 7755aceac..487c0f8a3 100644
--- a/templates/components/post_comment.html.twig
+++ b/templates/components/post_comment.html.twig
@@ -21,7 +21,7 @@
'show-preview': SHOW_PREVIEW is same as V_TRUE and not comment.isAdult,
})}).without('id') }}
id="post-comment-{{ comment.id }}"
- data-controller="comment subject mentions comment-collapse"
+ data-controller="comment subject mentions comment-collapse html-refresh"
data-comment-collapse-depth-value="{{ level }}"
data-subject-parent-value="{{ comment.parent ? comment.parent.id : '' }}"
data-action="notifications:Notification@window->subject#notification">
diff --git a/templates/components/user_box.html.twig b/templates/components/user_box.html.twig
index c74c2b07b..177e47dc3 100644
--- a/templates/components/user_box.html.twig
+++ b/templates/components/user_box.html.twig
@@ -76,6 +76,11 @@
{{ component('user_actions', {user: user}) }}
+ {% if app.user is defined and app.user is not same as null and app.user is not same as user %}
+
+ {{ component('notification_switch', {target: user}) }}
+
+ {% endif %}
{% if user.about|length %}
diff --git a/templates/entry/_info.html.twig b/templates/entry/_info.html.twig
index c4af6389e..2da5abebc 100644
--- a/templates/entry/_info.html.twig
+++ b/templates/entry/_info.html.twig
@@ -25,6 +25,11 @@
{{ component('user_actions', {user: entry.user}) }}
+ {% if app.user is defined and app.user is not same as null and app.user is not same as entry.user %}
+
+ {{ component('notification_switch', {target: entry.user}) }}
+
+ {% endif %}
- {{ 'added'|trans }}: {{ component('date', {date: entry.createdAt}) }}
{% if entry.editedAt %}
diff --git a/templates/post/_info.html.twig b/templates/post/_info.html.twig
index f2565e554..2fc2e9d4c 100644
--- a/templates/post/_info.html.twig
+++ b/templates/post/_info.html.twig
@@ -25,6 +25,11 @@
{{ component('user_actions', {user: post.user}) }}
+ {% if app.user is defined and app.user is not same as null and app.user is not same as post.user %}
+
+ {{ component('notification_switch', {target: post.user}) }}
+
+ {% endif %}
- {{ 'added'|trans }}: {{ component('date', {date: post.createdAt}) }}
- {{ 'up_votes'|trans }}:
diff --git a/templates/user/_user_popover.html.twig b/templates/user/_user_popover.html.twig
index c7900044b..21a53f126 100644
--- a/templates/user/_user_popover.html.twig
+++ b/templates/user/_user_popover.html.twig
@@ -44,6 +44,9 @@
{{ component('user_actions', {user: user}) }}
+ {% if app.user is defined and app.user is not same as null and app.user is not same as user %}
+ {{ component('notification_switch', {target: user}) }}
+ {% endif %}