Notifications: added user preference UI & logic

Includes testing to cover.
Also added file missing from previous commit.
This commit is contained in:
Dan Brown 2023-07-25 17:06:48 +01:00
parent 45e75edf05
commit 100b28707c
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 210 additions and 7 deletions

View File

@ -0,0 +1,49 @@
<?php
namespace BookStack\Activity\Notifications;
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler;
use BookStack\Activity\Notifications\Handlers\NotificationHandler;
use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler;
use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler;
class NotificationManager
{
/**
* @var class-string<NotificationHandler>[]
*/
protected array $handlers = [];
public function handle(string $activityType, string|Loggable $detail): void
{
$handlersToRun = $this->handlers[$activityType] ?? [];
foreach ($handlersToRun as $handlerClass) {
/** @var NotificationHandler $handler */
$handler = app()->make($handlerClass);
$handler->handle($activityType, $detail);
}
}
/**
* @param class-string<NotificationHandler> $handlerClass
*/
public function registerHandler(string $activityType, string $handlerClass): void
{
if (!isset($this->handlers[$activityType])) {
$this->handlers[$activityType] = [];
}
if (!in_array($handlerClass, $this->handlers[$activityType])) {
$this->handlers[$activityType][] = $handlerClass;
}
}
public function loadDefaultHandlers(): void
{
$this->registerHandler(ActivityType::PAGE_CREATE, PageCreationNotificationHandler::class);
$this->registerHandler(ActivityType::PAGE_UPDATE, PageUpdateNotificationHandler::class);
$this->registerHandler(ActivityType::COMMENT_CREATE, CommentCreationNotificationHandler::class);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace BookStack\Settings;
use BookStack\Users\Models\User;
class UserNotificationPreferences
{
public function __construct(
protected User $user
) {
}
public function notifyOnOwnPageChanges(): bool
{
return $this->getNotificationSetting('own-page-changes');
}
public function notifyOnOwnPageComments(): bool
{
return $this->getNotificationSetting('own-page-comments');
}
public function notifyOnCommentReplies(): bool
{
return $this->getNotificationSetting('comment-replies');
}
public function updateFromSettingsArray(array $settings)
{
$allowList = ['own-page-changes', 'own-page-comments', 'comment-replies'];
foreach ($settings as $setting => $status) {
if (!in_array($setting, $allowList)) {
continue;
}
$value = $status === 'true' ? 'true' : 'false';
setting()->putUser($this->user, 'notifications#' . $setting, $value);
}
}
protected function getNotificationSetting(string $key): bool
{
return setting()->getUser($this->user, 'notifications#' . $key);
}
}

View File

@ -3,17 +3,16 @@
namespace BookStack\Users\Controllers; namespace BookStack\Users\Controllers;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Settings\UserNotificationPreferences;
use BookStack\Settings\UserShortcutMap; use BookStack\Settings\UserShortcutMap;
use BookStack\Users\UserRepo; use BookStack\Users\UserRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class UserPreferencesController extends Controller class UserPreferencesController extends Controller
{ {
protected UserRepo $userRepo; public function __construct(
protected UserRepo $userRepo
public function __construct(UserRepo $userRepo) ) {
{
$this->userRepo = $userRepo;
} }
/** /**
@ -47,6 +46,35 @@ class UserPreferencesController extends Controller
return redirect('/preferences/shortcuts'); return redirect('/preferences/shortcuts');
} }
/**
* Show the notification preferences for the current user.
*/
public function showNotifications()
{
$preferences = (new UserNotificationPreferences(user()));
return view('users.preferences.notifications', [
'preferences' => $preferences,
]);
}
/**
* Update the notification preferences for the current user.
*/
public function updateNotifications(Request $request)
{
$data = $this->validate($request, [
'preferences' => ['required', 'array'],
'preferences.*' => ['required', 'string'],
]);
$preferences = (new UserNotificationPreferences(user()));
$preferences->updateFromSettingsArray($data['preferences']);
$this->showSuccessNotification(trans('preferences.notifications_update_success'));
return redirect('/preferences/notifications');
}
/** /**
* Update the preferred view format for a list view of the given type. * Update the preferred view format for a list view of the given type.
*/ */
@ -123,7 +151,7 @@ class UserPreferencesController extends Controller
{ {
$validated = $this->validate($request, [ $validated = $this->validate($request, [
'language' => ['required', 'string', 'max:20'], 'language' => ['required', 'string', 'max:20'],
'active' => ['required', 'bool'], 'active' => ['required', 'bool'],
]); ]);
$currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', ''); $currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');

View File

@ -15,4 +15,12 @@ return [
'shortcuts_save' => 'Save Shortcuts', 'shortcuts_save' => 'Save Shortcuts',
'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
'shortcuts_update_success' => 'Shortcut preferences have been updated!', 'shortcuts_update_success' => 'Shortcut preferences have been updated!',
];
'notifications' => 'Notification Preferences',
'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.',
'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own',
'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own',
'notifications_opt_comment_replies' => 'Notify upon replies to my comments',
'notifications_save' => 'Save Preferences',
'notifications_update_success' => 'Notification preferences have been updated!',
];

View File

@ -0,0 +1,45 @@
@extends('layouts.simple')
@section('body')
<div class="container small my-xl">
<section class="card content-wrap auto-height">
<form action="{{ url('/preferences/notifications') }}" method="post">
{{ method_field('put') }}
{{ csrf_field() }}
<h1 class="list-heading">{{ trans('preferences.notifications') }}</h1>
<p class="text-small text-muted">{{ trans('preferences.notifications_desc') }}</p>
<div class="toggle-switch-list">
<div>
@include('form.toggle-switch', [
'name' => 'preferences[own-page-changes]',
'value' => $preferences->notifyOnOwnPageChanges(),
'label' => trans('preferences.notifications_opt_own_page_changes'),
])
</div>
<div>
@include('form.toggle-switch', [
'name' => 'preferences[own-page-comments]',
'value' => $preferences->notifyOnOwnPageComments(),
'label' => trans('preferences.notifications_opt_own_page_comments'),
])
</div>
<div>
@include('form.toggle-switch', [
'name' => 'preferences[comment-replies]',
'value' => $preferences->notifyOnCommentReplies(),
'label' => trans('preferences.notifications_opt_comment_replies'),
])
</div>
</div>
<div class="form-group text-right">
<button class="button">{{ trans('preferences.notifications_save') }}</button>
</div>
</form>
</section>
</div>
@stop

View File

@ -231,6 +231,8 @@ Route::middleware('auth')->group(function () {
Route::redirect('/preferences', '/'); Route::redirect('/preferences', '/');
Route::get('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'showShortcuts']); Route::get('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'showShortcuts']);
Route::put('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'updateShortcuts']); Route::put('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'updateShortcuts']);
Route::get('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'showNotifications']);
Route::put('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'updateNotifications']);
Route::patch('/preferences/change-view/{type}', [UserControllers\UserPreferencesController::class, 'changeView']); Route::patch('/preferences/change-view/{type}', [UserControllers\UserPreferencesController::class, 'changeView']);
Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']); Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']);
Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']); Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']);

View File

@ -45,6 +45,31 @@ class UserPreferencesTest extends TestCase
$this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]'); $this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]');
} }
public function test_notification_preferences_updating()
{
$this->asEditor();
// View preferences with defaults
$resp = $this->get('/preferences/notifications');
$resp->assertSee('Notification Preferences');
$html = $this->withHtml($resp);
$html->assertFieldHasValue('preferences[comment-replies]', 'false');
// Update preferences
$resp = $this->put('/preferences/notifications', [
'preferences' => ['comment-replies' => 'true'],
]);
$resp->assertRedirect('/preferences/notifications');
$resp->assertSessionHas('success', 'Notification preferences have been updated!');
// View updates to preferences page
$resp = $this->get('/preferences/notifications');
$html = $this->withHtml($resp);
$html->assertFieldHasValue('preferences[comment-replies]', 'true');
}
public function test_update_sort_preference() public function test_update_sort_preference()
{ {
$editor = $this->users->editor(); $editor = $this->users->editor();