]> BookStack Code Mirror - bookstack/commitdiff
Notifications: Linked watch functionality to UI
authorDan Brown <redacted>
Wed, 2 Aug 2023 12:14:00 +0000 (13:14 +0100)
committerDan Brown <redacted>
Wed, 2 Aug 2023 12:14:00 +0000 (13:14 +0100)
Got watch system working to an initial base state.
Moved some existing logic where it makes sense.

13 files changed:
app/Activity/Controllers/WatchController.php
app/Activity/Models/Watch.php
app/Activity/Tools/UserWatchOptions.php [new file with mode: 0644]
app/Entities/Controllers/BookController.php
lang/en/activities.php
lang/en/entities.php
resources/icons/watch-ignore.svg [new file with mode: 0644]
resources/icons/watch.svg
resources/views/books/show.blade.php
resources/views/entities/meta.blade.php
resources/views/entities/watch-action.blade.php
resources/views/entities/watch-controls.blade.php
routes/web.php

index f9e8a4e3dc100602cf53e83681aaf6ba4f669cb0..a297aaafc33d2fd63f60d7019af2fcb5248a892f 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Activity\Controllers;
 
 use BookStack\Activity\Models\Watch;
 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;
 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);
         ]);
 
         $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();
     }
 
     /**
     }
 
     /**
index 6e0e1f787d3e384754b5aafbb1bc085d1887436e..6637c9655de10e9f9366d7ff8a037e0b48125279 100644 (file)
@@ -18,13 +18,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
  */
 class Watch extends Model
 {
  */
 class Watch extends Model
 {
-    protected static array $levelByOption = [
-        'default' => -1,
-        'ignore' => 0,
-        'new' => 1,
-        'updates' => 2,
-        'comments' => 3,
-    ];
+    protected $guarded = [];
 
     public function watchable()
     {
 
     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 $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;
-    }
 }
 }
diff --git a/app/Activity/Tools/UserWatchOptions.php b/app/Activity/Tools/UserWatchOptions.php
new file mode 100644 (file)
index 0000000..0607d60
--- /dev/null
@@ -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';
+    }
+}
index dcd1af5a187910f3f2e78013cf5eb23d0a447acd..3ce71c38aab998b6731d31d8e042fb37ce45f025 100644 (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\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;
 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,
             'current'           => $book,
             'bookChildren'      => $bookChildren,
             'bookParentShelves' => $bookParentShelves,
+            'watchOptions'      => new UserWatchOptions(user()),
             'activity'          => $activities->entityActivity($book, 20, 1),
             'referenceCount'    => $this->referenceFetcher->getPageReferenceCountToEntity($book),
         ]);
             'activity'          => $activities->entityActivity($book, 20, 1),
             'referenceCount'    => $this->referenceFetcher->getPageReferenceCountToEntity($book),
         ]);
index a96299ea7ff25951118d7e9b58f9c43f3d04f23f..d5b55c03dcb399205e0afa81853ca9c1fae32bf6 100644 (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',
 
     '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',
     // Auth
     'auth_login' => 'logged in',
     'auth_register' => 'registered as new user',
index 80b9142f5bf06ec6eed1e4b1f24bdcaa8314ce81..87c09634b749a5f6f296575027af7372ed4f7f86 100644 (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_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',
 ];
 ];
diff --git a/resources/icons/watch-ignore.svg b/resources/icons/watch-ignore.svg
new file mode 100644 (file)
index 0000000..2c6ffc2
--- /dev/null
@@ -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>
\ No newline at end of file
index c95c8875cdc36e2c9d7fe4b788628bdcf7c96c29..0be661912d73ebbe88db0a7297bc1a98e692299e 100644 (file)
@@ -1,4 +1,3 @@
 <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 <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
     <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
index 305a651321b07b6bdc104c7848740f8d1812a3d2..5c8b0a772831fa8ca8a612c35bc6d8089ded0175 100644 (file)
             @if(signedInUser())
                 @include('entities.favourite-action', ['entity' => $book])
             @endif
             @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
             @if(userCan('content-export'))
                 @include('entities.export-menu', ['entity' => $book])
             @endif
index 0bda1293828e909608b60184d0aca1414f27cd65..6783902a1552cd49bafabf52a79892f42c7bc823 100644 (file)
         </a>
     @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
 </div>
\ No newline at end of file
index dd626a0ef448f4d8f8bada90d0edcc7f7598ef91..34e287804cc62b0174058bd3c5a54b3dbe6b0a18 100644 (file)
@@ -1,8 +1,12 @@
-<form action="{{ $entity->getUrl('/') }}" method="GET">
+<form action="{{ url('/watching/update') }}" method="POST">
     {{ csrf_field() }}
     {{ csrf_field() }}
+    {{ method_field('PUT') }}
     <input type="hidden" name="type" value="{{ get_class($entity) }}">
     <input type="hidden" name="id" value="{{ $entity->id }}">
     <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>
         <span>@icon('watch')</span>
         <span>{{ trans('entities.watch') }}</span>
     </button>
index 8d6bfed006ebc23fd27f4c589aa59329ad97c10f..e02db58005b5532783af9388dbaa5feabc9303c5 100644 (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">
     {{ 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>
-                </div>
-            </button>
-        </li>
-        <li><hr class="my-none"></li>
+                </button>
+            </li>
+            <li>
+                <hr class="my-none">
+            </li>
         @endforeach
         <li>
             <a href="{{ url('/preferences/notifications') }}"
         @endforeach
         <li>
             <a href="{{ url('/preferences/notifications') }}"
index 9ea44f03c632b5068c9af968b42618fcd2598b84..27a54f8b486933ce70759083dfdda468fb63199a 100644 (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']);
 
     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']);
     // Other Pages
     Route::get('/', [HomeController::class, 'index']);
     Route::get('/home', [HomeController::class, 'index']);