]> BookStack Code Mirror - bookstack/commitdiff
Notifications: User watch list and differnt page watch options
authorDan Brown <redacted>
Mon, 14 Aug 2023 12:11:18 +0000 (13:11 +0100)
committerDan Brown <redacted>
Mon, 14 Aug 2023 12:11:18 +0000 (13:11 +0100)
- Adds option filtering and alternative text for page watch options.
- Adds "Watched & Ignored Items" list to user notification preferences
  page to show existing watched items.

app/Activity/Models/Watch.php
app/Activity/WatchLevels.php
app/Entities/Queries/TopFavourites.php
app/Users/Controllers/UserPreferencesController.php
lang/en/entities.php
lang/en/preferences.php
resources/views/entities/icon-link.blade.php [new file with mode: 0644]
resources/views/entities/watch-controls.blade.php
resources/views/settings/audit.blade.php
resources/views/users/preferences/notifications.blade.php

index 6637c9655de10e9f9366d7ff8a037e0b48125279..dfb72cc0a369598add077fbca2c3074134956e27 100644 (file)
@@ -2,10 +2,12 @@
 
 namespace BookStack\Activity\Models;
 
+use BookStack\Activity\WatchLevels;
 use BookStack\Permissions\Models\JointPermission;
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
 
 /**
  * @property int $id
@@ -20,14 +22,24 @@ class Watch extends Model
 {
     protected $guarded = [];
 
-    public function watchable()
+    public function watchable(): MorphTo
     {
-        $this->morphTo();
+        return $this->morphTo();
     }
 
     public function jointPermissions(): HasMany
     {
         return $this->hasMany(JointPermission::class, 'entity_id', 'watchable_id')
-            ->whereColumn('favourites.watchable_type', '=', 'joint_permissions.entity_type');
+            ->whereColumn('watches.watchable_type', '=', 'joint_permissions.entity_type');
+    }
+
+    public function getLevelName(): string
+    {
+        return WatchLevels::levelValueToName($this->level);
+    }
+
+    public function ignoring(): bool
+    {
+        return $this->level === WatchLevels::IGNORE;
     }
 }
index 2951bc7a8ae18b1e7e924f76cdbcb0d60ab731e3..de3c5e1228828fe2fca891ee60eaa2a92520f2ed 100644 (file)
@@ -2,6 +2,10 @@
 
 namespace BookStack\Activity;
 
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+
 class WatchLevels
 {
     /**
@@ -32,6 +36,7 @@ class WatchLevels
 
     /**
      * Get all the possible values as an option_name => value array.
+     * @returns array<string, int>
      */
     public static function all(): array
     {
@@ -43,11 +48,36 @@ class WatchLevels
         return $options;
     }
 
+    /**
+     * Get the watch options suited for the given entity.
+     * @returns array<string, int>
+     */
+    public static function allSuitedFor(Entity $entity): array
+    {
+        $options = static::all();
+
+        if ($entity instanceof Page) {
+            unset($options['new']);
+        } elseif ($entity instanceof Bookshelf) {
+            return [];
+        }
+
+        return $options;
+    }
+
+    /**
+     * Convert the given name to a level value.
+     * Defaults to default value if the level does not exist.
+     */
     public static function levelNameToValue(string $level): int
     {
-        return static::all()[$level] ?? -1;
+        return static::all()[$level] ?? static::DEFAULT;
     }
 
+    /**
+     * Convert the given int level value to a level name.
+     * Defaults to 'default' level name if not existing.
+     */
     public static function levelValueToName(int $level): string
     {
         foreach (static::all() as $name => $value) {
index 3f8d2e62e134f2da936c6054cf5f314d79522a3b..cbccf35b054eceedd863e3edde10647b7462e650 100644 (file)
@@ -10,7 +10,7 @@ class TopFavourites extends EntityQuery
     public function run(int $count, int $skip = 0)
     {
         $user = user();
-        if (is_null($user) || $user->isDefault()) {
+        if ($user->isDefault()) {
             return collect();
         }
 
index faa99629b349db365b1f39918f7d91944657bc7c..999115e7b126592b81cf915514693cf91a73983e 100644 (file)
@@ -2,7 +2,9 @@
 
 namespace BookStack\Users\Controllers;
 
+use BookStack\Activity\Models\Watch;
 use BookStack\Http\Controller;
+use BookStack\Permissions\PermissionApplicator;
 use BookStack\Settings\UserNotificationPreferences;
 use BookStack\Settings\UserShortcutMap;
 use BookStack\Users\UserRepo;
@@ -49,12 +51,17 @@ class UserPreferencesController extends Controller
     /**
      * Show the notification preferences for the current user.
      */
-    public function showNotifications()
+    public function showNotifications(PermissionApplicator $permissions)
     {
         $preferences = (new UserNotificationPreferences(user()));
 
+        $query = Watch::query()->where('user_id', '=', user()->id);
+        $query = $permissions->restrictEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');
+        $watches = $query->with('watchable')->paginate(20);
+
         return view('users.preferences.notifications', [
             'preferences' => $preferences,
+            'watches' => $watches,
         ]);
     }
 
index f1e7dcc01b6a352bf609cffa4eb7a3e5d4f13bea..b1b0e5236903a357072f2077aaf0554e27307efc 100644 (file)
@@ -414,8 +414,10 @@ return [
     'watch_desc_new' => 'Notify when any new page is created within this item.',
     'watch_title_updates' => 'All Page Updates',
     'watch_desc_updates' => 'Notify upon all new pages and page changes.',
+    'watch_desc_updates_page' => 'Notify upon all page changes.',
     'watch_title_comments' => 'All Page Updates & Comments',
     'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',
+    'watch_desc_comments_page' => 'Notify upon page changes and new comments.',
     'watch_change_default' => 'Change default notification preferences',
     'watch_detail_ignore' => 'Ignoring notifications',
     'watch_detail_new' => 'Watching for new pages',
index 2ade3a0e487fc49a70c092189690a552ddcb315b..97968f8b125e6976f186aeaa6f2b4e01a26f57a0 100644 (file)
@@ -23,4 +23,6 @@ return [
     'notifications_opt_comment_replies' => 'Notify upon replies to my comments',
     'notifications_save' => 'Save Preferences',
     'notifications_update_success' => 'Notification preferences have been updated!',
+    'notifications_watched' => 'Watched & Ignored Items',
+    'notifications_watched_desc' => ' Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',
 ];
diff --git a/resources/views/entities/icon-link.blade.php b/resources/views/entities/icon-link.blade.php
new file mode 100644 (file)
index 0000000..a3e95fd
--- /dev/null
@@ -0,0 +1,7 @@
+<a href="{{ $entity->getUrl() }}" class="flex-container-row items-center">
+    <span role="presentation"
+          class="icon flex-none text-{{$entity->getType()}}">@icon($entity->getType())</span>
+    <div class="flex text-{{ $entity->getType() }}">
+        {{ $entity->name }}
+    </div>
+</a>
\ No newline at end of file
index 4fdda53476140053165fb8d38903057c15c61457..d24e120182166aec6dc0d895ae67ade32476cf8f 100644 (file)
@@ -11,7 +11,7 @@
         <input type="hidden" name="id" value="{{ $entity->id }}">
 
         <ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
-            @foreach(\BookStack\Activity\WatchLevels::all() as $option => $value)
+            @foreach(\BookStack\Activity\WatchLevels::allSuitedFor($entity) as $option => $value)
                 <li>
                     <button name="level" value="{{ $option }}" class="icon-item">
                         @if($watchLevel === $option)
                         <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) }}
+                                @if(trans()->has('entities.watch_desc_' . $option . '_' . $entity->getMorphClass()))
+                                    {{ trans('entities.watch_desc_' . $option . '_' . $entity->getMorphClass()) }}
+                                @else
+                                    {{ trans('entities.watch_desc_' . $option) }}
+                                @endif
                             </div>
                         </div>
                     </button>
index dd0874c9226ab029de817dcb27dcc12948342034..9f0572c1ab400f7209d2d0a8f5dc48ab97145039 100644 (file)
                                 :</strong> {{ $activity->type }}</div>
                         <div class="flex-3 px-m py-xxs min-width-l">
                             @if($activity->entity)
-                                <a href="{{ $activity->entity->getUrl() }}" class="flex-container-row items-center">
-                                    <span role="presentation"
-                                          class="icon flex-none text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
-                                    <div class="flex text-{{ $activity->entity->getType() }}">
-                                        {{ $activity->entity->name }}
-                                    </div>
-                                </a>
+                                @include('entities.icon-link', ['entity' => $activity->entity])
                             @elseif($activity->detail && $activity->isForEntity())
                                 <div>
                                     {{ trans('settings.audit_deleted_item') }} <br>
index 3a301aeb6a4b879b778ed0132b50a36fa743f8be..3bbf78280e6d3c457342339b954b57e941c41e51 100644 (file)
             </form>
         </section>
 
+        <section class="card content-wrap auto-height">
+            <h2 class="list-heading">{{ trans('preferences.notifications_watched') }}</h2>
+            <p class="text-small text-muted">{{ trans('preferences.notifications_watched_desc') }}</p>
+
+            @if($watches->isEmpty())
+                <p class="text-muted italic">{{ trans('common.no_items') }}</p>
+            @else
+                <div class="item-list">
+                    @foreach($watches as $watch)
+                        <div class="flex-container-row justify-space-between item-list-row items-center wrap px-m py-s">
+                            <div class="py-xs px-s min-width-m">
+                                @include('entities.icon-link', ['entity' => $watch->watchable])
+                            </div>
+                            <div class="py-xs min-width-m text-m-right px-m">
+                                @icon('watch' . ($watch->ignoring() ? '-ignore' : ''))
+                                {{ trans('entities.watch_title_' . $watch->getLevelName()) }}
+                            </div>
+                        </div>
+                    @endforeach
+                </div>
+            @endif
+
+            <div class="my-m">{{ $watches->links() }}</div>
+        </section>
+
     </div>
 @stop