Got watch system working to an initial base state.
Moved some existing logic where it makes sense.
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;
]);
$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();
}
/**
*/
class Watch extends Model
{
- protected static array $levelByOption = [
- 'default' => -1,
- 'ignore' => 0,
- 'new' => 1,
- 'updates' => 2,
- 'comments' => 3,
- ];
+ protected $guarded = [];
public function watchable()
{
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;
- }
}
--- /dev/null
+<?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';
+ }
+}
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;
'current' => $book,
'bookChildren' => $bookChildren,
'bookParentShelves' => $bookParentShelves,
+ 'watchOptions' => new UserWatchOptions(user()),
'activity' => $activities->entityActivity($book, 20, 1),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($book),
]);
'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',
'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',
];
--- /dev/null
+<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>
\ No newline at end of file
<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>
\ No newline at end of file
@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
</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>
\ No newline at end of file
-<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>
-<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') }}"
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']);