diff --git a/framework/core/js/src/forum/components/NotificationList.js b/framework/core/js/src/forum/components/NotificationList.js
index 8c63fc328..abfd52423 100644
--- a/framework/core/js/src/forum/components/NotificationList.js
+++ b/framework/core/js/src/forum/components/NotificationList.js
@@ -6,6 +6,7 @@ import Link from '../../common/components/Link';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import Discussion from '../../common/models/Discussion';
import ItemList from '../../common/utils/ItemList';
+import Tooltip from '../../common/components/Tooltip';
/**
* The `NotificationList` component displays a list of the logged-in user's
@@ -34,15 +35,36 @@ export default class NotificationList extends Component {
items.add(
'mark_all_as_read',
- ,
+
+
+ ,
70
);
+ items.add(
+ 'delete_all',
+
+ ,
+ 50
+ );
+
return items;
}
diff --git a/framework/core/js/src/forum/states/NotificationListState.ts b/framework/core/js/src/forum/states/NotificationListState.ts
index 75bb07158..2b54c76a7 100644
--- a/framework/core/js/src/forum/states/NotificationListState.ts
+++ b/framework/core/js/src/forum/states/NotificationListState.ts
@@ -46,4 +46,20 @@ export default class NotificationListState extends PaginatedListState button:not(:last-of-type) {
+ margin-right: 4px;
+ }
+
&-header {
@media @tablet-up {
padding: 12px 15px;
diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml
index 010bb9cfb..3fcd8e14e 100644
--- a/framework/core/locale/core.yml
+++ b/framework/core/locale/core.yml
@@ -394,6 +394,8 @@ core:
# These translations are used by the Notifications dropdown, a.k.a. "the bell".
notifications:
+ delete_all_confirm: Are you sure you want to delete all notifications? This action is not reversable
+ delete_all_tooltip: Delete all notifications
discussion_renamed_text: "{username} changed the title"
empty_text: No Notifications
mark_all_as_read_tooltip: => core.ref.mark_all_as_read
diff --git a/framework/core/src/Api/Controller/DeleteAllNotificationsController.php b/framework/core/src/Api/Controller/DeleteAllNotificationsController.php
new file mode 100644
index 000000000..7e5d3df76
--- /dev/null
+++ b/framework/core/src/Api/Controller/DeleteAllNotificationsController.php
@@ -0,0 +1,41 @@
+bus = $bus;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function delete(ServerRequestInterface $request)
+ {
+ $this->bus->dispatch(
+ new DeleteAllNotifications(RequestUtil::getActor($request))
+ );
+ }
+}
diff --git a/framework/core/src/Api/routes.php b/framework/core/src/Api/routes.php
index 5863e4856..e21141107 100644
--- a/framework/core/src/Api/routes.php
+++ b/framework/core/src/Api/routes.php
@@ -122,6 +122,13 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\UpdateNotificationController::class)
);
+ // Delete all notifications for the current user.
+ $map->delete(
+ '/notifications',
+ 'notifications.deleteAll',
+ $route->toController(Controller\DeleteAllNotificationsController::class)
+ );
+
/*
|--------------------------------------------------------------------------
| Discussions
diff --git a/framework/core/src/Notification/Command/DeleteAllNotifications.php b/framework/core/src/Notification/Command/DeleteAllNotifications.php
new file mode 100644
index 000000000..934e60665
--- /dev/null
+++ b/framework/core/src/Notification/Command/DeleteAllNotifications.php
@@ -0,0 +1,30 @@
+actor = $actor;
+ }
+}
diff --git a/framework/core/src/Notification/Command/DeleteAllNotificationsHandler.php b/framework/core/src/Notification/Command/DeleteAllNotificationsHandler.php
new file mode 100644
index 000000000..a8d321694
--- /dev/null
+++ b/framework/core/src/Notification/Command/DeleteAllNotificationsHandler.php
@@ -0,0 +1,54 @@
+notifications = $notifications;
+ $this->events = $events;
+ }
+
+ /**
+ * @param DeleteAllNotifications $command
+ * @throws NotAuthenticatedException
+ */
+ public function handle(DeleteAllNotifications $command)
+ {
+ $actor = $command->actor;
+
+ $actor->assertRegistered();
+
+ $this->notifications->deleteAll($actor);
+
+ $this->events->dispatch(new DeletedAll($actor, Carbon::now()));
+ }
+}
diff --git a/framework/core/src/Notification/Event/DeletedAll.php b/framework/core/src/Notification/Event/DeletedAll.php
new file mode 100644
index 000000000..8d202eb7c
--- /dev/null
+++ b/framework/core/src/Notification/Event/DeletedAll.php
@@ -0,0 +1,32 @@
+user = $user;
+ $this->timestamp = $timestamp;
+ }
+}
diff --git a/framework/core/src/Notification/NotificationRepository.php b/framework/core/src/Notification/NotificationRepository.php
index dcb7acbd8..7f6b29da1 100644
--- a/framework/core/src/Notification/NotificationRepository.php
+++ b/framework/core/src/Notification/NotificationRepository.php
@@ -54,4 +54,9 @@ class NotificationRepository
->whereNull('read_at')
->update(['read_at' => Carbon::now()]);
}
+
+ public function deleteAll(User $user)
+ {
+ Notification::where('user_id', $user->id)->delete();
+ }
}
diff --git a/framework/core/tests/integration/api/notifications/DeleteTest.php b/framework/core/tests/integration/api/notifications/DeleteTest.php
new file mode 100644
index 000000000..4d4c2976b
--- /dev/null
+++ b/framework/core/tests/integration/api/notifications/DeleteTest.php
@@ -0,0 +1,72 @@
+prepareDatabase([
+ 'users' => [
+ $this->normalUser(),
+ ],
+ 'discussions' => [
+ ['id' => 1, 'title' => 'Test Discussion', 'user_id' => 2, 'comment_count' => 1],
+ ['id' => 2, 'title' => 'Test Discussion', 'user_id' => 2, 'comment_count' => 1],
+ ['id' => 3, 'title' => 'Test Discussion', 'user_id' => 1, 'comment_count' => 1],
+ ['id' => 4, 'title' => 'Test Discussion', 'user_id' => 1, 'comment_count' => 1],
+ ],
+ 'notifications' => [
+ ['id' => 1, 'user_id' => 1, 'type' => 'discussionRenamed', 'subject_id' => 1, 'from_user_id' => 2, 'read_at' => Carbon::now()],
+ ['id' => 2, 'user_id' => 1, 'type' => 'discussionRenamed', 'subject_id' => 2, 'from_user_id' => 2, 'read_at' => null],
+ ['id' => 3, 'user_id' => 2, 'type' => 'discussionRenamed', 'subject_id' => 3, 'from_user_id' => 1, 'read_at' => Carbon::now()],
+ ['id' => 4, 'user_id' => 2, 'type' => 'discussionRenamed', 'subject_id' => 4, 'from_user_id' => 1, 'read_at' => null],
+ ],
+ ]);
+ }
+
+ /**
+ * @dataProvider canDeleteAllNotifications
+ * @test
+ */
+ public function user_can_delete_all_notifications(int $authenticatedAs)
+ {
+ $this->app();
+
+ $this->assertEquals(2, User::query()->find($authenticatedAs)->notifications()->count());
+
+ $response = $this->send(
+ $this->request('DELETE', '/api/notifications', compact('authenticatedAs')),
+ );
+
+ $this->assertEquals(204, $response->getStatusCode());
+ $this->assertEquals(0, User::query()->find($authenticatedAs)->notifications()->count());
+ }
+
+ public function canDeleteAllNotifications(): array
+ {
+ return [
+ [1],
+ [2]
+ ];
+ }
+}