From 6d1b2ff5a7c0ca578b937b1927aaa325cd9851f8 Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Tue, 26 Nov 2024 10:42:32 +0100
Subject: [PATCH 1/5] 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`)
---
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 | 12 +-
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 | 91 +++------
.../Notification/EntryNotificationManager.php | 24 +--
.../PostCommentNotificationManager.php | 89 +++------
.../Notification/PostNotificationManager.php | 24 +--
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, 901 insertions(+), 216 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 6f65b2c8f..233240b64 100644
--- a/assets/styles/app.scss
+++ b/assets/styles/app.scss
@@ -34,6 +34,7 @@
@use 'components/subject';
@use 'components/login';
@use 'components/modlog';
+@use 'components/notification_switch';
@use 'components/notifications';
@use 'components/messages';
@use 'components/dropdown';
diff --git a/assets/styles/components/_entry.scss b/assets/styles/components/_entry.scss
index 9d3e5ccfa..67699190d 100644
--- a/assets/styles/components/_entry.scss
+++ b/assets/styles/components/_entry.scss
@@ -335,7 +335,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 e51970261..17cd0d230 100644
--- a/assets/styles/components/_magazine.scss
+++ b/assets/styles/components/_magazine.scss
@@ -53,11 +53,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 792f66482..b11445a5a 100644
--- a/assets/styles/components/_post.scss
+++ b/assets/styles/components/_post.scss
@@ -60,7 +60,7 @@
margin-bottom: 0;
opacity: .75;
- a {
+ a:not(.notification-setting) {
color: var(--kbin-meta-link-color);
font-weight: bold;
@@ -76,7 +76,7 @@
}
}
- aside {
+ aside:not(.notification-switch) {
grid-area: vote;
}
@@ -135,13 +135,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;
}
@@ -151,7 +151,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 0694660e0..861397e83 100644
--- a/assets/styles/components/_sidebar.scss
+++ b/assets/styles/components/_sidebar.scss
@@ -247,7 +247,7 @@
}
}
- a {
+ a:not(.notification-setting) {
color: var(--kbin-meta-link-color);
}
@@ -260,6 +260,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 10a9ebe64..dd2ba87ab 100644
--- a/assets/styles/components/_user.scss
+++ b/assets/styles/components/_user.scss
@@ -5,7 +5,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 8818f5071..03f87bfc7 100644
--- a/src/Controller/Api/BaseApi.php
+++ b/src/Controller/Api/BaseApi.php
@@ -37,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;
@@ -101,6 +102,7 @@ public function __construct(
private readonly ImageRepository $imageRepository,
private readonly ReportManager $reportManager,
private readonly OAuth2ClientAccessRepository $clientAccessRepository,
+ protected readonly NotificationSettingsRepository $notificationSettingsRepository,
) {
}
@@ -302,10 +304,14 @@ protected function serializeLogItem(MagazineLog $log): array
*
* @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;
}
@@ -320,6 +326,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 d955cc260..718fd9e47 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..a5f814c3d
--- /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 7b09a7522..eaebb5681 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\Attribute\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 7381f18a8..8d7cb9575 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\Attribute\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 3b524af34..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;
@@ -50,10 +52,11 @@ public function __construct(
private readonly ImageManager $imageManager,
private readonly GenerateHtmlClassService $classService,
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 c52c415c4..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;
@@ -51,6 +52,8 @@ public function __construct(
private readonly GenerateHtmlClassService $classService,
private readonly UserManager $userManager,
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 70945cfb4..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;
@@ -50,6 +52,8 @@ public function __construct(
private readonly ImageManager $imageManager,
private readonly GenerateHtmlClassService $classService,
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 760efe697..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;
@@ -48,6 +49,8 @@ public function __construct(
private readonly ImageManager $imageManager,
private readonly GenerateHtmlClassService $classService,
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 %}
From 80714096018884a3ec2d9a783fccfca611c4380d Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Tue, 10 Dec 2024 22:12:14 +0100
Subject: [PATCH 2/5] Fix not getting notified on replies - if the magazine was
not on default (e.g.: on loud) and you had "notify on replies" enabled it
didn't work -> fix that - rename some variables for readability - add log
statement with all the variables
---
.../NotificationSettingsRepository.php | 31 ++++++++++++++-----
1 file changed, 23 insertions(+), 8 deletions(-)
diff --git a/src/Repository/NotificationSettingsRepository.php b/src/Repository/NotificationSettingsRepository.php
index b8541dcb9..30ac2a5cc 100644
--- a/src/Repository/NotificationSettingsRepository.php
+++ b/src/Repository/NotificationSettingsRepository.php
@@ -86,7 +86,7 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
if ($target instanceof Entry) {
$targetId = $target->getId();
$notifyCol = 'notify_on_new_entry';
- $activateMagazineNotifications = true;
+ $isMagazineLevel = true;
$dontNeedSubscription = false;
$dontNeedToBeAuthor = true;
$targetParentUserId = null;
@@ -99,7 +99,7 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
$notifyCol = 'notify_on_new_entry_comment_reply';
$targetParentUserId = $target->parent->user->getId();
}
- $activateMagazineNotifications = false;
+ $isMagazineLevel = false;
$dontNeedSubscription = true;
$dontNeedToBeAuthor = false;
}
@@ -108,7 +108,7 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
if ($target instanceof Post) {
$targetId = $target->getId();
$notifyCol = 'notify_on_new_post';
- $activateMagazineNotifications = true;
+ $isMagazineLevel = true;
$dontNeedSubscription = false;
$dontNeedToBeAuthor = true;
$targetParentUserId = null;
@@ -121,13 +121,14 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
$notifyCol = 'notify_on_new_post_comment_reply';
$targetParentUserId = $target->parent->user->getId();
}
- $activateMagazineNotifications = false;
+ $isMagazineLevel = false;
$dontNeedSubscription = true;
$dontNeedToBeAuthor = false;
}
}
- $activateMagazineNotificationsString = $activateMagazineNotifications ? 'true' : 'false';
+ $isMagazineLevelString = $isMagazineLevel ? 'true' : 'false';
+ $isNotMagazineLevelString = !$isMagazineLevel ? 'true' : 'false';
$dontNeedSubscriptionString = $dontNeedSubscription ? 'true' : 'false';
$dontNeedToBeAuthorString = $dontNeedToBeAuthor ? 'true' : 'false';
@@ -149,12 +150,16 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
AND COALESCE(ns_post.notification_status, :normal) = :normal
AND COALESCE(ns_mag.notification_status, :normal) = :loud
-- deactivate loud magazine notifications for comments
- AND $activateMagazineNotificationsString
+ AND $isMagazineLevelString
)
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 (
+ -- ignore the magazine level settings for comments
+ COALESCE(ns_mag.notification_status, :normal) = :normal
+ OR $isNotMagazineLevelString
+ )
AND u.$notifyCol = true
AND (
-- deactivate magazine subscription need for comments
@@ -181,7 +186,17 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
'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]);
+ $this->logger->debug('got subscribers for target {c} id {id}: {subs}, (magLevel: {ml}, notMagLevel: {nml}, targetCol: {tc}, notifyCol: {nc}, dontNeedSubs: {dns}, doneNeedAuthor: {dna})', [
+ 'c' => \get_class($target),
+ 'id' => $target->getId(),
+ 'subs' => $rows,
+ 'ml' => $isMagazineLevelString,
+ 'nml' => $isNotMagazineLevelString,
+ 'tc' => $targetCol,
+ 'nc' => $notifyCol,
+ 'dns' => $dontNeedSubscriptionString,
+ 'dna' => $dontNeedToBeAuthorString,
+ ]);
return array_map(fn (array $row) => $row['id'], $rows);
}
From b388f9e632b1312eb0e215984a445825985701bd Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Mon, 13 Jan 2025 18:03:51 +0100
Subject: [PATCH 3/5] Fix the tests and also a bug By fixing the tests I
discovered that nested comments don't trigger a notification for the author
of the entry anymore. Fix that bug and the tests
---
.../NotificationSettingsRepository.php | 56 +++++++++++++------
tests/WebTestCase.php | 8 +--
2 files changed, 42 insertions(+), 22 deletions(-)
diff --git a/src/Repository/NotificationSettingsRepository.php b/src/Repository/NotificationSettingsRepository.php
index 30ac2a5cc..e30d00752 100644
--- a/src/Repository/NotificationSettingsRepository.php
+++ b/src/Repository/NotificationSettingsRepository.php
@@ -81,6 +81,7 @@ public function setStatusByTarget(User $user, Entry|Post|User|Magazine $target,
*/
public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|PostComment $target): array
{
+ $nestedCommentPostAuthor = 'false';
if ($target instanceof Entry || $target instanceof EntryComment) {
$targetCol = 'entry_id';
if ($target instanceof Entry) {
@@ -98,6 +99,10 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
} else {
$notifyCol = 'notify_on_new_entry_comment_reply';
$targetParentUserId = $target->parent->user->getId();
+
+ $nestedCommentPostAuthor = 'u.notify_on_new_entry_reply = true
+ AND u.id = :targetParent2UserId';
+ $targetParent2UserId = $target->entry->user->getId();
}
$isMagazineLevel = false;
$dontNeedSubscription = true;
@@ -120,6 +125,10 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
} else {
$notifyCol = 'notify_on_new_post_comment_reply';
$targetParentUserId = $target->parent->user->getId();
+
+ $nestedCommentPostAuthor = 'u.notify_on_new_post_reply = true
+ AND u.id = :targetParent2UserId';
+ $targetParent2UserId = $target->post->user->getId();
}
$isMagazineLevel = false;
$dontNeedSubscription = true;
@@ -160,16 +169,22 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
COALESCE(ns_mag.notification_status, :normal) = :normal
OR $isNotMagazineLevelString
)
- 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
+ (
+ 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
+ )
+ ) OR (
+ $nestedCommentPostAuthor
+ )
)
)
)
@@ -177,16 +192,20 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
";
$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,
- ]);
+
+ $stmt->bindValue('normal', ENotificationStatus::Default->value);
+ $stmt->bindValue('loud', ENotificationStatus::Loud->value);
+ $stmt->bindValue('targetUserId', $target->user->getId());
+ $stmt->bindValue('targetId', $targetId);
+ $stmt->bindValue('magId', $target->magazine->getId());
+ $stmt->bindValue('targetParentUserId', $targetParentUserId);
+
+ if (isset($targetParent2UserId)) {
+ $stmt->bindValue('targetParent2UserId', $targetParent2UserId);
+ }
+ $result = $stmt->executeQuery();
$rows = $result->fetchAllAssociative();
- $this->logger->debug('got subscribers for target {c} id {id}: {subs}, (magLevel: {ml}, notMagLevel: {nml}, targetCol: {tc}, notifyCol: {nc}, dontNeedSubs: {dns}, doneNeedAuthor: {dna})', [
+ $this->logger->debug('got subscribers for target {c} id {id}: {subs}, (magLevel: {ml}, notMagLevel: {nml}, targetCol: {tc}, notifyCol: {nc}, dontNeedSubs: {dns}, doneNeedAuthor: {dna}, nestedComment extra condition: {nested})', [
'c' => \get_class($target),
'id' => $target->getId(),
'subs' => $rows,
@@ -196,6 +215,7 @@ public function findNotificationSubscribersByTarget(Entry|EntryComment|Post|Post
'nc' => $notifyCol,
'dns' => $dontNeedSubscriptionString,
'dna' => $dontNeedToBeAuthorString,
+ 'nested' => $nestedCommentPostAuthor,
]);
return array_map(fn (array $row) => $row['id'], $rows);
diff --git a/tests/WebTestCase.php b/tests/WebTestCase.php
index cc86ea738..0f2a1a470 100644
--- a/tests/WebTestCase.php
+++ b/tests/WebTestCase.php
@@ -69,15 +69,15 @@ abstract class WebTestCase extends BaseWebTestCase
protected const PAGINATION_KEYS = ['count', 'currentPage', 'maxPage', 'perPage'];
protected const IMAGE_KEYS = ['filePath', 'sourceUrl', 'storageUrl', 'altText', 'width', 'height', 'blurHash'];
protected const MESSAGE_RESPONSE_KEYS = ['messageId', 'threadId', 'sender', 'body', 'status', 'createdAt'];
- protected const USER_RESPONSE_KEYS = ['userId', 'username', 'about', 'avatar', 'cover', 'createdAt', 'followersCount', 'apId', 'apProfileId', 'isBot', 'isFollowedByUser', 'isFollowerOfUser', 'isBlockedByUser', 'isAdmin', 'isGlobalModerator', 'serverSoftware', 'serverSoftwareVersion'];
+ protected const USER_RESPONSE_KEYS = ['userId', 'username', 'about', 'avatar', 'cover', 'createdAt', 'followersCount', 'apId', 'apProfileId', 'isBot', 'isFollowedByUser', 'isFollowerOfUser', 'isBlockedByUser', 'isAdmin', 'isGlobalModerator', 'serverSoftware', 'serverSoftwareVersion', 'notificationStatus'];
protected const USER_SMALL_RESPONSE_KEYS = ['userId', 'username', 'isBot', 'isFollowedByUser', 'isFollowerOfUser', 'isBlockedByUser', 'avatar', 'apId', 'apProfileId', 'createdAt', 'isAdmin', 'isGlobalModerator'];
- protected const ENTRY_RESPONSE_KEYS = ['entryId', 'magazine', 'user', 'domain', 'title', 'url', 'image', 'body', 'lang', 'tags', 'badges', 'numComments', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'isOc', 'isAdult', 'isPinned', 'createdAt', 'editedAt', 'lastActive', 'visibility', 'type', 'slug', 'apId', 'canAuthUserModerate'];
+ protected const ENTRY_RESPONSE_KEYS = ['entryId', 'magazine', 'user', 'domain', 'title', 'url', 'image', 'body', 'lang', 'tags', 'badges', 'numComments', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'isOc', 'isAdult', 'isPinned', 'createdAt', 'editedAt', 'lastActive', 'visibility', 'type', 'slug', 'apId', 'canAuthUserModerate', 'notificationStatus'];
protected const ENTRY_COMMENT_RESPONSE_KEYS = ['commentId', 'magazine', 'user', 'entryId', 'parentId', 'rootId', 'image', 'body', 'lang', 'isAdult', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'visibility', 'apId', 'mentions', 'tags', 'createdAt', 'editedAt', 'lastActive', 'childCount', 'children', 'canAuthUserModerate'];
- protected const POST_RESPONSE_KEYS = ['postId', 'user', 'magazine', 'image', 'body', 'lang', 'isAdult', 'isPinned', 'comments', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'visibility', 'apId', 'tags', 'mentions', 'createdAt', 'editedAt', 'lastActive', 'slug', 'canAuthUserModerate'];
+ protected const POST_RESPONSE_KEYS = ['postId', 'user', 'magazine', 'image', 'body', 'lang', 'isAdult', 'isPinned', 'comments', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'visibility', 'apId', 'tags', 'mentions', 'createdAt', 'editedAt', 'lastActive', 'slug', 'canAuthUserModerate', 'notificationStatus'];
protected const POST_COMMENT_RESPONSE_KEYS = ['commentId', 'user', 'magazine', 'postId', 'parentId', 'rootId', 'image', 'body', 'lang', 'isAdult', 'uv', 'dv', 'favourites', 'isFavourited', 'userVote', 'visibility', 'apId', 'mentions', 'tags', 'createdAt', 'editedAt', 'lastActive', 'childCount', 'children', 'canAuthUserModerate'];
protected const BAN_RESPONSE_KEYS = ['banId', 'reason', 'expired', 'expiredAt', 'bannedUser', 'bannedBy', 'magazine'];
protected const LOG_ENTRY_KEYS = ['type', 'createdAt', 'magazine', 'moderator', 'subject'];
- protected const MAGAZINE_RESPONSE_KEYS = ['magazineId', 'owner', 'icon', 'name', 'title', 'description', 'rules', 'subscriptionsCount', 'entryCount', 'entryCommentCount', 'postCount', 'postCommentCount', 'isAdult', 'isUserSubscribed', 'isBlockedByUser', 'tags', 'badges', 'moderators', 'apId', 'apProfileId', 'serverSoftware', 'serverSoftwareVersion', 'isPostingRestrictedToMods', 'localSubscribers'];
+ protected const MAGAZINE_RESPONSE_KEYS = ['magazineId', 'owner', 'icon', 'name', 'title', 'description', 'rules', 'subscriptionsCount', 'entryCount', 'entryCommentCount', 'postCount', 'postCommentCount', 'isAdult', 'isUserSubscribed', 'isBlockedByUser', 'tags', 'badges', 'moderators', 'apId', 'apProfileId', 'serverSoftware', 'serverSoftwareVersion', 'isPostingRestrictedToMods', 'localSubscribers', 'notificationStatus'];
protected const MAGAZINE_SMALL_RESPONSE_KEYS = ['magazineId', 'name', 'icon', 'isUserSubscribed', 'isBlockedByUser', 'apId', 'apProfileId'];
protected const DOMAIN_RESPONSE_KEYS = ['domainId', 'name', 'entryCount', 'subscriptionsCount', 'isUserSubscribed', 'isBlockedByUser'];
From 00fc43eb6c4d03cf20a8e79d7938cc521ae3cbe0 Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Tue, 14 Jan 2025 14:43:10 +0100
Subject: [PATCH 4/5] Fix bookmark list callback not working anymore - the
bookmark_list component was forgotten when moving the linkCallback code into
the new html-refresh controller
---
templates/components/bookmark_list.html.twig | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/templates/components/bookmark_list.html.twig b/templates/components/bookmark_list.html.twig
index 2e075df3d..447c3fa11 100644
--- a/templates/components/bookmark_list.html.twig
+++ b/templates/components/bookmark_list.html.twig
@@ -1,16 +1,16 @@
-
{% if is_bookmarked_in_list(app.user, list, subject) %}
{{ 'bookmark_remove_from_list'|trans({'%list%': list.name}) }}
{% else %}
{{ 'bookmark_add_to_list'|trans({'%list%': list.name}) }}
From 360bb5f47a17f0df1a9a34562d5ad74e459a3dcb Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Sat, 25 Jan 2025 12:36:55 +0100
Subject: [PATCH 5/5] Only show the custom notification component when post is
in single view - the custom notification component was shown on all posts in
the timeline when viewing microblog posts
---
templates/components/post.html.twig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/templates/components/post.html.twig b/templates/components/post.html.twig
index 0f2fe0e07..d125c831c 100644
--- a/templates/components/post.html.twig
+++ b/templates/components/post.html.twig
@@ -114,7 +114,7 @@
{{ component('bookmark_standard', { subject: post }) }}
{% endif %}
{% include 'post/_menu.html.twig' %}
- {% if app.user is defined and app.user is not same as null %}
+ {% if app.user is defined and app.user is not same as null and isSingle is defined and isSingle %}
{{ component('notification_switch', {target: post}) }}
{% endif %}
-