Loading app/Database/Migrations/2022-07-26-091451_AddNotifications.php 0 → 100644 +75 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * Class AddNotifications Creates notifications table in database * * @copyright 2021 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; class AddNotifications extends Migration { public function up(): void { $this->forge->addField([ 'id' => [ 'type' => 'INT', 'unsigned' => true, 'auto_increment' => true, ], 'actor_id' => [ 'type' => 'INT', 'unsigned' => true, ], 'target_actor_id' => [ 'type' => 'INT', 'unsigned' => true, ], 'post_id' => [ 'type' => 'BINARY', 'constraint' => 16, 'null' => true, ], 'activity_id' => [ 'type' => 'BINARY', 'constraint' => 16, ], 'type' => [ 'type' => 'ENUM', 'constraint' => ['like', 'follow', 'share', 'reply'], ], 'read_at' => [ 'type' => 'DATETIME', 'null' => true, ], 'created_at' => [ 'type' => 'DATETIME', ], 'updated_at' => [ 'type' => 'DATETIME', ], ]); $tablesPrefix = config('Fediverse') ->tablesPrefix; $this->forge->addPrimaryKey('id'); $this->forge->addForeignKey('actor_id', $tablesPrefix . 'actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('target_actor_id', $tablesPrefix . 'actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('post_id', $tablesPrefix . 'posts', 'id', '', 'CASCADE'); $this->forge->addForeignKey('activity_id', $tablesPrefix . 'activities', 'id', '', 'CASCADE'); $this->forge->createTable('notifications'); } public function down(): void { $this->forge->dropTable('notifications'); } } app/Database/Migrations/2022-07-28-143030_AddActivitiesTrigger.php 0 → 100644 +62 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * Class AddActivitiesTrigger Creates activities trigger in database * * @copyright 2020 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; class AddNotificationsTrigger extends Migration { public function up(): void { $activitiesTable = $this->db->prefixTable(config('Fediverse')->tablesPrefix . 'activities'); $notificationsTable = $this->db->prefixTable('notifications'); $createQuery = <<<CODE_SAMPLE CREATE TRIGGER `{$activitiesTable}_after_insert` AFTER INSERT ON `{$activitiesTable}` FOR EACH ROW BEGIN -- only create notification if new incoming activity with NULL status is created IF NEW.target_actor_id AND NEW.target_actor_id != NEW.actor_id AND NEW.status IS NULL THEN IF NEW.type IN ( 'Create', 'Like', 'Announce', 'Follow' ) THEN SET @type = (CASE WHEN NEW.type = 'Create' THEN 'reply' WHEN NEW.type = 'Like' THEN 'like' WHEN NEW.type = 'Announce' THEN 'share' WHEN NEW.type = 'Follow' THEN 'follow' END); INSERT INTO `{$notificationsTable}` (`actor_id`, `target_actor_id`, `post_id`, `activity_id`, `type`, `created_at`, `updated_at`) VALUES (NEW.actor_id, NEW.target_actor_id, NEW.post_id, NEW.id, @type, NEW.created_at, NEW.created_at); ELSE DELETE FROM `{$notificationsTable}` WHERE `actor_id` = NEW.actor_id AND `target_actor_id` = NEW.target_actor_id AND ((`type` = (CASE WHEN NEW.type = 'Undo_Follow' THEN 'follow' END) AND `post_id` IS NULL) OR (`type` = (CASE WHEN NEW.type = 'Delete' THEN 'reply' WHEN NEW.type = 'Undo_Like' THEN 'like' WHEN NEW.type = 'Undo_Announce' THEN 'share' END) AND `post_id` = NEW.post_id)); END IF; END IF; END CODE_SAMPLE; $this->db->query($createQuery); } public function down(): void { $activitiesTable = $this->db->prefixTable(config('Fediverse')->tablesPrefix . 'activities'); $this->db->query("DROP TRIGGER IF EXISTS `{$activitiesTable}_after_insert`"); } } app/Entities/Notification.php 0 → 100644 +106 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * @copyright 2020 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Entities; use Michalsn\Uuid\UuidEntity; use Modules\Fediverse\Entities\Activity; use Modules\Fediverse\Entities\Actor; use Modules\Fediverse\Entities\Post; use Modules\Fediverse\Models\ActorModel; use Modules\Fediverse\Models\PostModel; use RuntimeException; /** * @property int $id * @property int $actor_id * @property Actor $actor * @property int $target_actor_id * @property Actor $target_actor * @property string|null $post_id * @property Post $post * @property string $activity_id * @property Activity $activity * @property 'like'|'follow'|'share'|'reply' $type * @property Time|null $read_at * @property Time $created_at * @property Time $updated_at */ class Notification extends UuidEntity { protected ?Actor $actor = null; protected ?Actor $target_actor = null; protected ?Post $post = null; protected ?Activity $activity = null; /** * @var string[] */ protected $uuids = ['post_id', 'activity_id']; /** * @var string[] */ protected $dates = ['read_at', 'created_at', 'updated_at']; /** * @var array<string, string> */ protected $casts = [ 'id' => 'integer', 'actor_id' => 'integer', 'target_actor_id' => 'integer', 'post_id' => '?string', 'activity_id' => 'string', 'type' => 'string', ]; public function getActor(): ?Actor { if ($this->actor_id === null) { throw new RuntimeException('Notification must have an actor_id before getting actor.'); } if (! $this->actor instanceof Actor) { $this->actor = (new ActorModel())->getActorById($this->actor_id); } return $this->actor; } public function getTargetActor(): ?Actor { if ($this->target_actor_id === null) { throw new RuntimeException('Notification must have a target_actor_id before getting target actor.'); } if (! $this->target_actor instanceof Actor) { $this->target_actor = (new ActorModel())->getActorById($this->target_actor_id); } return $this->target_actor; } public function getPost(): ?Post { if ($this->post_id === null) { throw new RuntimeException('Notification must have a post_id before getting post.'); } if (! $this->post instanceof Post) { $this->post = (new PostModel())->getPostById($this->post_id); } return $this->post; } } app/Models/NotificationModel.php 0 → 100644 +47 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * @copyright 2021 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Models; use App\Entities\Notification; use Michalsn\Uuid\UuidModel; class NotificationModel extends UuidModel { /** * @var string */ protected $table = 'notifications'; /** * @var string */ protected $primaryKey = 'id'; /** * @var string */ protected $returnType = Notification::class; /** * @var bool */ protected $useTimestamps = true; /** * @var string[] */ protected $uuidFields = ['post_id', 'activity_id']; /** * @var string[] */ protected $allowedFields = ['read_at']; } app/Resources/icons/notification-bell.svg 0 → 100644 +6 −0 Original line number Diff line number Diff line <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <g> <path fill="none" d="M0 0h24v24H0z"/> <path d="M20 17h2v2H2v-2h2v-7a8 8 0 1 1 16 0v7zM9 21h6v2H9v-2z"/> </g> </svg> No newline at end of file Loading
app/Database/Migrations/2022-07-26-091451_AddNotifications.php 0 → 100644 +75 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * Class AddNotifications Creates notifications table in database * * @copyright 2021 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; class AddNotifications extends Migration { public function up(): void { $this->forge->addField([ 'id' => [ 'type' => 'INT', 'unsigned' => true, 'auto_increment' => true, ], 'actor_id' => [ 'type' => 'INT', 'unsigned' => true, ], 'target_actor_id' => [ 'type' => 'INT', 'unsigned' => true, ], 'post_id' => [ 'type' => 'BINARY', 'constraint' => 16, 'null' => true, ], 'activity_id' => [ 'type' => 'BINARY', 'constraint' => 16, ], 'type' => [ 'type' => 'ENUM', 'constraint' => ['like', 'follow', 'share', 'reply'], ], 'read_at' => [ 'type' => 'DATETIME', 'null' => true, ], 'created_at' => [ 'type' => 'DATETIME', ], 'updated_at' => [ 'type' => 'DATETIME', ], ]); $tablesPrefix = config('Fediverse') ->tablesPrefix; $this->forge->addPrimaryKey('id'); $this->forge->addForeignKey('actor_id', $tablesPrefix . 'actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('target_actor_id', $tablesPrefix . 'actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('post_id', $tablesPrefix . 'posts', 'id', '', 'CASCADE'); $this->forge->addForeignKey('activity_id', $tablesPrefix . 'activities', 'id', '', 'CASCADE'); $this->forge->createTable('notifications'); } public function down(): void { $this->forge->dropTable('notifications'); } }
app/Database/Migrations/2022-07-28-143030_AddActivitiesTrigger.php 0 → 100644 +62 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * Class AddActivitiesTrigger Creates activities trigger in database * * @copyright 2020 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; class AddNotificationsTrigger extends Migration { public function up(): void { $activitiesTable = $this->db->prefixTable(config('Fediverse')->tablesPrefix . 'activities'); $notificationsTable = $this->db->prefixTable('notifications'); $createQuery = <<<CODE_SAMPLE CREATE TRIGGER `{$activitiesTable}_after_insert` AFTER INSERT ON `{$activitiesTable}` FOR EACH ROW BEGIN -- only create notification if new incoming activity with NULL status is created IF NEW.target_actor_id AND NEW.target_actor_id != NEW.actor_id AND NEW.status IS NULL THEN IF NEW.type IN ( 'Create', 'Like', 'Announce', 'Follow' ) THEN SET @type = (CASE WHEN NEW.type = 'Create' THEN 'reply' WHEN NEW.type = 'Like' THEN 'like' WHEN NEW.type = 'Announce' THEN 'share' WHEN NEW.type = 'Follow' THEN 'follow' END); INSERT INTO `{$notificationsTable}` (`actor_id`, `target_actor_id`, `post_id`, `activity_id`, `type`, `created_at`, `updated_at`) VALUES (NEW.actor_id, NEW.target_actor_id, NEW.post_id, NEW.id, @type, NEW.created_at, NEW.created_at); ELSE DELETE FROM `{$notificationsTable}` WHERE `actor_id` = NEW.actor_id AND `target_actor_id` = NEW.target_actor_id AND ((`type` = (CASE WHEN NEW.type = 'Undo_Follow' THEN 'follow' END) AND `post_id` IS NULL) OR (`type` = (CASE WHEN NEW.type = 'Delete' THEN 'reply' WHEN NEW.type = 'Undo_Like' THEN 'like' WHEN NEW.type = 'Undo_Announce' THEN 'share' END) AND `post_id` = NEW.post_id)); END IF; END IF; END CODE_SAMPLE; $this->db->query($createQuery); } public function down(): void { $activitiesTable = $this->db->prefixTable(config('Fediverse')->tablesPrefix . 'activities'); $this->db->query("DROP TRIGGER IF EXISTS `{$activitiesTable}_after_insert`"); } }
app/Entities/Notification.php 0 → 100644 +106 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * @copyright 2020 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Entities; use Michalsn\Uuid\UuidEntity; use Modules\Fediverse\Entities\Activity; use Modules\Fediverse\Entities\Actor; use Modules\Fediverse\Entities\Post; use Modules\Fediverse\Models\ActorModel; use Modules\Fediverse\Models\PostModel; use RuntimeException; /** * @property int $id * @property int $actor_id * @property Actor $actor * @property int $target_actor_id * @property Actor $target_actor * @property string|null $post_id * @property Post $post * @property string $activity_id * @property Activity $activity * @property 'like'|'follow'|'share'|'reply' $type * @property Time|null $read_at * @property Time $created_at * @property Time $updated_at */ class Notification extends UuidEntity { protected ?Actor $actor = null; protected ?Actor $target_actor = null; protected ?Post $post = null; protected ?Activity $activity = null; /** * @var string[] */ protected $uuids = ['post_id', 'activity_id']; /** * @var string[] */ protected $dates = ['read_at', 'created_at', 'updated_at']; /** * @var array<string, string> */ protected $casts = [ 'id' => 'integer', 'actor_id' => 'integer', 'target_actor_id' => 'integer', 'post_id' => '?string', 'activity_id' => 'string', 'type' => 'string', ]; public function getActor(): ?Actor { if ($this->actor_id === null) { throw new RuntimeException('Notification must have an actor_id before getting actor.'); } if (! $this->actor instanceof Actor) { $this->actor = (new ActorModel())->getActorById($this->actor_id); } return $this->actor; } public function getTargetActor(): ?Actor { if ($this->target_actor_id === null) { throw new RuntimeException('Notification must have a target_actor_id before getting target actor.'); } if (! $this->target_actor instanceof Actor) { $this->target_actor = (new ActorModel())->getActorById($this->target_actor_id); } return $this->target_actor; } public function getPost(): ?Post { if ($this->post_id === null) { throw new RuntimeException('Notification must have a post_id before getting post.'); } if (! $this->post instanceof Post) { $this->post = (new PostModel())->getPostById($this->post_id); } return $this->post; } }
app/Models/NotificationModel.php 0 → 100644 +47 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); /** * @copyright 2021 Ad Aures * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ */ namespace App\Models; use App\Entities\Notification; use Michalsn\Uuid\UuidModel; class NotificationModel extends UuidModel { /** * @var string */ protected $table = 'notifications'; /** * @var string */ protected $primaryKey = 'id'; /** * @var string */ protected $returnType = Notification::class; /** * @var bool */ protected $useTimestamps = true; /** * @var string[] */ protected $uuidFields = ['post_id', 'activity_id']; /** * @var string[] */ protected $allowedFields = ['read_at']; }
app/Resources/icons/notification-bell.svg 0 → 100644 +6 −0 Original line number Diff line number Diff line <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <g> <path fill="none" d="M0 0h24v24H0z"/> <path d="M20 17h2v2H2v-2h2v-7a8 8 0 1 1 16 0v7zM9 21h6v2H9v-2z"/> </g> </svg> No newline at end of file