Added DB and started controller method.
--- /dev/null
+<?php
+
+namespace BookStack\Activity\Controllers;
+
+use BookStack\Activity\Models\Watch;
+use BookStack\App\Model;
+use BookStack\Entities\Models\Entity;
+use BookStack\Http\Controller;
+use Exception;
+use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
+
+class WatchController extends Controller
+{
+ public function update(Request $request)
+ {
+ $requestData = $this->validate($request, [
+ 'level' => ['required', 'string'],
+ ]);
+
+ $watchable = $this->getValidatedModelFromRequest($request);
+ $newLevel = Watch::optionNameToLevel($requestData['level']);
+
+ if ($newLevel < 0) {
+ // TODO - Delete
+ } else {
+ // TODO - Upsert
+ }
+ }
+
+ /**
+ * @throws ValidationException
+ * @throws Exception
+ */
+ protected function getValidatedModelFromRequest(Request $request): Entity
+ {
+ $modelInfo = $this->validate($request, [
+ 'type' => ['required', 'string'],
+ 'id' => ['required', 'integer'],
+ ]);
+
+ if (!class_exists($modelInfo['type'])) {
+ throw new Exception('Model not found');
+ }
+
+ /** @var Model $model */
+ $model = new $modelInfo['type']();
+ if (!$model instanceof Entity) {
+ throw new Exception('Model not an entity');
+ }
+
+ $modelInstance = $model->newQuery()
+ ->where('id', '=', $modelInfo['id'])
+ ->first(['id', 'name', 'owned_by']);
+
+ $inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
+ if (is_null($modelInstance) || $inaccessibleEntity) {
+ throw new Exception('Model instance not found');
+ }
+
+ return $modelInstance;
+ }
+}
--- /dev/null
+<?php
+
+namespace BookStack\Activity\Models;
+
+use BookStack\Permissions\Models\JointPermission;
+use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+
+/**
+ * @property int $id
+ * @property int $user_id
+ * @property int $watchable_id
+ * @property string $watchable_type
+ * @property int $level
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ */
+class Watch extends Model
+{
+ protected static array $levelByOption = [
+ 'default' => -1,
+ 'ignore' => 0,
+ 'new' => 1,
+ 'updates' => 2,
+ 'comments' => 3,
+ ];
+
+ public function watchable()
+ {
+ $this->morphTo();
+ }
+
+ public function jointPermissions(): HasMany
+ {
+ 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\Users;
-
-class UserWatchOptions
-{
- protected static array $levelByOption = [
- 'default' => -1,
- 'ignore' => 0,
- 'new' => 1,
- 'updates' => 2,
- 'comments' => 3,
- ];
-
- /**
- * @return string[]
- */
- public static function getAvailableOptionNames(): array
- {
- return array_keys(static::$levelByOption);
- }
-}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('watches', function (Blueprint $table) {
+ $table->increments('id');
+ $table->integer('user_id')->index();
+ $table->integer('watchable_id');
+ $table->string('watchable_type', 100);
+ $table->tinyInteger('level', false, true)->index();
+ $table->timestamps();
+
+ $table->index(['watchable_id', 'watchable_type'], 'watchable_index');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('watches');
+ }
+};
<input type="hidden" name="id" value="{{ $entity->id }}">
<ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
- @foreach(\BookStack\Users\UserWatchOptions::getAvailableOptionNames() as $option)
+ @foreach(\BookStack\Activity\Models\Watch::getAvailableOptionNames() as $option)
<li>
<button name="level" value="{{ $option }}" class="icon-item">
@if(request()->query('level') === $option)