Notifications: Linked watch functionality to UI

Got watch system working to an initial base state.
Moved some existing logic where it makes sense.
This commit is contained in:
Dan Brown 2023-08-02 13:14:00 +01:00
parent 8cdf3203ef
commit 9d149e4d36
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
13 changed files with 161 additions and 56 deletions

View file

@ -3,6 +3,7 @@
namespace BookStack\Activity\Controllers;
use BookStack\Activity\Models\Watch;
use BookStack\Activity\Tools\UserWatchOptions;
use BookStack\App\Model;
use BookStack\Entities\Models\Entity;
use BookStack\Http\Controller;
@ -19,13 +20,12 @@ class WatchController extends Controller
]);
$watchable = $this->getValidatedModelFromRequest($request);
$newLevel = Watch::optionNameToLevel($requestData['level']);
$watchOptions = new UserWatchOptions(user());
$watchOptions->updateEntityWatchLevel($watchable, $requestData['level']);
if ($newLevel < 0) {
// TODO - Delete
} else {
// TODO - Upsert
}
$this->showSuccessNotification(trans('activities.watch_update_level_notification'));
return redirect()->back();
}
/**

View file

@ -18,13 +18,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
*/
class Watch extends Model
{
protected static array $levelByOption = [
'default' => -1,
'ignore' => 0,
'new' => 1,
'updates' => 2,
'comments' => 3,
];
protected $guarded = [];
public function watchable()
{
@ -36,17 +30,4 @@ class Watch extends Model
return $this->hasMany(JointPermission::class, 'entity_id', 'watchable_id')
->whereColumn('favourites.watchable_type', '=', 'joint_permissions.entity_type');
}
/**
* @return string[]
*/
public static function getAvailableOptionNames(): array
{
return array_keys(static::$levelByOption);
}
public static function optionNameToLevel(string $option): int
{
return static::$levelByOption[$option] ?? -1;
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace BookStack\Activity\Tools;
use BookStack\Activity\Models\Watch;
use BookStack\Entities\Models\Entity;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
class UserWatchOptions
{
protected static array $levelByName = [
'default' => -1,
'ignore' => 0,
'new' => 1,
'updates' => 2,
'comments' => 3,
];
public function __construct(
protected User $user,
) {
}
public function canWatch(): bool
{
return $this->user->can('receive-notifications') && !$this->user->isDefault();
}
public function getEntityWatchLevel(Entity $entity): string
{
$levelValue = $this->entityQuery($entity)->first(['level'])->level ?? -1;
return $this->levelValueToName($levelValue);
}
public function isWatching(Entity $entity): bool
{
return $this->entityQuery($entity)->exists();
}
public function updateEntityWatchLevel(Entity $entity, string $level): void
{
$levelValue = $this->levelNameToValue($level);
if ($levelValue < 0) {
$this->removeForEntity($entity);
return;
}
$this->updateForEntity($entity, $levelValue);
}
protected function updateForEntity(Entity $entity, int $levelValue): void
{
Watch::query()->updateOrCreate([
'watchable_id' => $entity->id,
'watchable_type' => $entity->getMorphClass(),
'user_id' => $this->user->id,
], [
'level' => $levelValue,
]);
}
protected function removeForEntity(Entity $entity): void
{
$this->entityQuery($entity)->delete();
}
protected function entityQuery(Entity $entity): Builder
{
return Watch::query()->where('watchable_id', '=', $entity->id)
->where('watchable_type', '=', $entity->getMorphClass())
->where('user_id', '=', $this->user->id);
}
/**
* @return string[]
*/
public static function getAvailableLevelNames(): array
{
return array_keys(static::$levelByName);
}
protected static function levelNameToValue(string $level): int
{
return static::$levelByName[$level] ?? -1;
}
protected static function levelValueToName(int $level): string
{
foreach (static::$levelByName as $name => $value) {
if ($level === $value) {
return $name;
}
}
return 'default';
}
}

View file

@ -5,6 +5,7 @@ namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityQueries;
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserWatchOptions;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
@ -138,6 +139,7 @@ class BookController extends Controller
'current' => $book,
'bookChildren' => $bookChildren,
'bookParentShelves' => $bookParentShelves,
'watchOptions' => new UserWatchOptions(user()),
'activity' => $activities->entityActivity($book, 20, 1),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($book),
]);

View file

@ -58,6 +58,9 @@ return [
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Watching
'watch_update_level_notification' => 'Watch preferences successfully updated',
// Auth
'auth_login' => 'logged in',
'auth_register' => 'registered as new user',

View file

@ -417,4 +417,8 @@ return [
'watch_title_comments' => 'All Page Updates & Comments',
'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',
'watch_change_default' => 'Change default notification preferences',
'watch_detail_ignore' => 'Ignoring notifications',
'watch_detail_new' => 'Watching for new pages',
'watch_detail_updates' => 'Watching new pages and updates',
'watch_detail_comments' => 'Watching new pages, updates & comments',
];

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>

After

Width:  |  Height:  |  Size: 583 B

View file

@ -1,4 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 293 B

View file

@ -142,7 +142,9 @@
@if(signedInUser())
@include('entities.favourite-action', ['entity' => $book])
@endif
@include('entities.watch-action', ['entity' => $book])
@if($watchOptions->canWatch() && !$watchOptions->isWatching($book))
@include('entities.watch-action', ['entity' => $book])
@endif
@if(userCan('content-export'))
@include('entities.export-menu', ['entity' => $book])
@endif

View file

@ -69,12 +69,17 @@
</a>
@endif
<div component="dropdown"
class="dropdown-container my-xxs">
<a refs="dropdown@toggle" href="#" class="entity-meta-item my-none">
@icon('watch')
<span>Watching with default preferences</span>
</a>
@include('entities.watch-controls', ['entity' => $entity])
</div>
@if($watchOptions?->canWatch() && $watchOptions->isWatching($entity))
@php
$watchLevel = $watchOptions->getEntityWatchLevel($entity);
@endphp
<div component="dropdown"
class="dropdown-container block my-xxs">
<a refs="dropdown@toggle" href="#" class="entity-meta-item my-none">
@icon(($watchLevel === 'ignore' ? 'watch-ignore' : 'watch'))
<span>{{ trans('entities.watch_detail_' . $watchLevel) }}</span>
</a>
@include('entities.watch-controls', ['entity' => $entity, 'watchLevel' => $watchLevel])
</div>
@endif
</div>

View file

@ -1,8 +1,12 @@
<form action="{{ $entity->getUrl('/') }}" method="GET">
<form action="{{ url('/watching/update') }}" method="POST">
{{ csrf_field() }}
{{ method_field('PUT') }}
<input type="hidden" name="type" value="{{ get_class($entity) }}">
<input type="hidden" name="id" value="{{ $entity->id }}">
<button type="submit" data-shortcut="favourite" class="icon-list-item text-link">
<button type="submit"
name="level"
value="updates"
class="icon-list-item text-link">
<span>@icon('watch')</span>
<span>{{ trans('entities.watch') }}</span>
</button>

View file

@ -1,27 +1,30 @@
<form action="{{ $entity->getUrl('/') }}" method="GET">
{{-- {{ method_field('PUT') }}--}}
<form action="{{ url('/watching/update') }}" method="POST">
{{ method_field('PUT') }}
{{ csrf_field() }}
<input type="hidden" name="type" value="{{ get_class($entity) }}">
<input type="hidden" name="id" value="{{ $entity->id }}">
<ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
@foreach(\BookStack\Activity\Models\Watch::getAvailableOptionNames() as $option)
<li>
<button name="level" value="{{ $option }}" class="icon-item">
@if(request()->query('level') === $option)
<span class="text-pos pt-m" title="{{ trans('common.status_active') }}">@icon('check-circle')</span>
@else
<span title="{{ trans('common.status_inactive') }}"></span>
@endif
<div class="break-text">
<div class="mb-xxs"><strong>{{ trans('entities.watch_title_' . $option) }}</strong></div>
<div class="text-muted text-small">
{{ trans('entities.watch_desc_' . $option) }}
@foreach(\BookStack\Activity\Tools\UserWatchOptions::getAvailableLevelNames() as $option)
<li>
<button name="level" value="{{ $option }}" class="icon-item">
@if($watchLevel === $option)
<span class="text-pos pt-m"
title="{{ trans('common.status_active') }}">@icon('check-circle')</span>
@else
<span title="{{ trans('common.status_inactive') }}"></span>
@endif
<div class="break-text">
<div class="mb-xxs"><strong>{{ trans('entities.watch_title_' . $option) }}</strong></div>
<div class="text-muted text-small">
{{ trans('entities.watch_desc_' . $option) }}
</div>
</div>
</div>
</button>
</li>
<li><hr class="my-none"></li>
</button>
</li>
<li>
<hr class="my-none">
</li>
@endforeach
<li>
<a href="{{ url('/preferences/notifications') }}"

View file

@ -194,6 +194,9 @@ Route::middleware('auth')->group(function () {
Route::post('/favourites/add', [ActivityControllers\FavouriteController::class, 'add']);
Route::post('/favourites/remove', [ActivityControllers\FavouriteController::class, 'remove']);
// Watching
Route::put('/watching/update', [ActivityControllers\WatchController::class, 'update']);
// Other Pages
Route::get('/', [HomeController::class, 'index']);
Route::get('/home', [HomeController::class, 'index']);