diff --git a/app/Database/Migrations/2022-07-26-091451_AddNotifications.php b/app/Database/Migrations/2022-07-26-091451_AddNotifications.php
new file mode 100644
index 0000000000000000000000000000000000000000..cddfcdc97ae3ad8a746707c999d1eae6d0815941
--- /dev/null
+++ b/app/Database/Migrations/2022-07-26-091451_AddNotifications.php
@@ -0,0 +1,75 @@
+<?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');
+    }
+}
diff --git a/app/Database/Migrations/2022-07-28-143030_AddActivitiesTrigger.php b/app/Database/Migrations/2022-07-28-143030_AddActivitiesTrigger.php
new file mode 100644
index 0000000000000000000000000000000000000000..1024ad7b7ffae6ef10ff647e16b765db31d308b3
--- /dev/null
+++ b/app/Database/Migrations/2022-07-28-143030_AddActivitiesTrigger.php
@@ -0,0 +1,62 @@
+<?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`");
+    }
+}
diff --git a/app/Entities/Notification.php b/app/Entities/Notification.php
new file mode 100644
index 0000000000000000000000000000000000000000..b878228fa861227bd5a1f9eb9c5c9b02ee93fd04
--- /dev/null
+++ b/app/Entities/Notification.php
@@ -0,0 +1,106 @@
+<?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;
+    }
+}
diff --git a/app/Models/NotificationModel.php b/app/Models/NotificationModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..10ce851ade76334ce54767f9c621145d00f18f32
--- /dev/null
+++ b/app/Models/NotificationModel.php
@@ -0,0 +1,47 @@
+<?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'];
+}
diff --git a/app/Resources/icons/notification-bell.svg b/app/Resources/icons/notification-bell.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ea792a4d93f140b764d4709b1f549f61e51da316
--- /dev/null
+++ b/app/Resources/icons/notification-bell.svg
@@ -0,0 +1,6 @@
+<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
diff --git a/app/Resources/icons/user-follow.svg b/app/Resources/icons/user-follow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e8892f2823138e863a2c759c6f9d8aaba76f8ea3
--- /dev/null
+++ b/app/Resources/icons/user-follow.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <g>
+        <path fill="none" d="M0 0h24v24H0z"/>
+        <path d="M13 14.062V22H4a8 8 0 0 1 9-7.938zM12 13c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm5.793 6.914l3.535-3.535 1.415 1.414-4.95 4.95-3.536-3.536 1.415-1.414 2.12 2.121z"/>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php
index d6c58357a576a3a26fa7e71814c2e620652049b5..8a2f0611fbf4bd399705e086bbd88668076d9b7d 100644
--- a/modules/Admin/Config/Routes.php
+++ b/modules/Admin/Config/Routes.php
@@ -626,6 +626,19 @@ $routes->group(
                         ],
                     );
                 });
+
+                // Podcast notifications
+                $routes->group('notifications', function ($routes): void {
+                    $routes->get('/', 'NotificationController::list/$1', [
+                        'as' => 'notification-list',
+                    ]);
+                    $routes->get('(:num)/mark-as-read', 'NotificationController::markAsRead/$1/$2', [
+                        'as' => 'notification-mark-as-read',
+                    ]);
+                    $routes->get('mark-all-as-read', 'NotificationController::markAllAsRead/$1', [
+                        'as' => 'notification-mark-all-as-read',
+                    ]);
+                });
             });
         });
 
diff --git a/modules/Admin/Controllers/NotificationController.php b/modules/Admin/Controllers/NotificationController.php
new file mode 100644
index 0000000000000000000000000000000000000000..c3191c840ef6e866262749608681a4be446cb579
--- /dev/null
+++ b/modules/Admin/Controllers/NotificationController.php
@@ -0,0 +1,107 @@
+<?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 Modules\Admin\Controllers;
+
+use App\Entities\Notification;
+use App\Entities\Podcast;
+use App\Models\NotificationModel;
+use App\Models\PodcastModel;
+use CodeIgniter\Exceptions\PageNotFoundException;
+use CodeIgniter\HTTP\RedirectResponse;
+use CodeIgniter\I18n\Time;
+use Modules\Fediverse\Models\PostModel;
+
+class NotificationController extends BaseController
+{
+    protected Podcast $podcast;
+
+    protected Notification $notification;
+
+    public function _remap(string $method, string ...$params): mixed
+    {
+        if (
+            ($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null
+        ) {
+            throw PageNotFoundException::forPageNotFound();
+        }
+
+        $this->podcast = $podcast;
+
+        if (count($params) > 1) {
+            if (
+                ! ($notification = (new NotificationModel())
+                    ->where([
+                        'id' => $params[1],
+                    ])
+                    ->first())
+            ) {
+                throw PageNotFoundException::forPageNotFound();
+            }
+
+            $this->notification = $notification;
+
+            unset($params[1]);
+            unset($params[0]);
+        }
+
+        return $this->{$method}(...$params);
+    }
+
+    public function list(): string
+    {
+        $notifications = (new NotificationModel())->where('target_actor_id', $this->podcast->actor_id)
+            ->orderBy('created_at', 'desc');
+
+        $data = [
+            'podcast' => $this->podcast,
+            'notifications' => $notifications->paginate(10),
+            'pager' => $notifications->pager,
+        ];
+
+        replace_breadcrumb_params([
+            0 => $this->podcast->title,
+        ]);
+
+        return view('podcast/notifications', $data);
+    }
+
+    public function markAsRead(): RedirectResponse
+    {
+        $this->notification->read_at = new Time('now');
+        $notificationModel = new NotificationModel();
+        $notificationModel->update($this->notification->id, $this->notification);
+
+        if ($this->notification->post_id === null) {
+            return redirect()->route('podcast-activity', [esc($this->podcast->handle)]);
+        }
+
+        $post = (new PostModel())->getPostById($this->notification->post_id);
+
+        return redirect()->route(
+            'post',
+            [esc((new PodcastModel())->getPodcastByActorId($this->notification->actor_id)->handle), $post->id]
+        );
+    }
+
+    public function markAllAsRead(): RedirectResponse
+    {
+        $notifications = (new NotificationModel())->where('target_actor_id', $this->podcast->actor_id)
+            ->where('read_at', null)
+            ->findAll();
+
+        foreach ($notifications as $notification) {
+            $notification->read_at = new Time('now');
+            (new NotificationModel())->update($notification->id, $notification);
+        }
+
+        return redirect()->back();
+    }
+}
diff --git a/modules/Admin/Language/en/Breadcrumb.php b/modules/Admin/Language/en/Breadcrumb.php
index d9400ca786ad22820c4bda166f6f11e61a0cb199..24bece0140b8e7cbd6edf835d096f76a43f97e70 100644
--- a/modules/Admin/Language/en/Breadcrumb.php
+++ b/modules/Admin/Language/en/Breadcrumb.php
@@ -45,4 +45,5 @@ return [
     'soundbites' => 'soundbites',
     'video-clips' => 'video clips',
     'embed' => 'embeddable player',
+    'notifications' => 'notifications',
 ];
diff --git a/modules/Admin/Language/en/Notifications.php b/modules/Admin/Language/en/Notifications.php
new file mode 100644
index 0000000000000000000000000000000000000000..1772ba76b290dc6ec713315c062d70e95dcde28a
--- /dev/null
+++ b/modules/Admin/Language/en/Notifications.php
@@ -0,0 +1,19 @@
+<?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/
+ */
+
+return [
+    'title' => 'Notifications',
+    'reply' => '{actor_username} replied to your post',
+    'favourite' => '{actor_username} favourited your post',
+    'reblog' => '{actor_username} shared your post',
+    'follow' => '{actor_username} started following {target_actor_username}',
+    'no_notifications' => 'No notifications',
+    'mark_all_as_read' => 'Mark all as read',
+];
diff --git a/modules/Auth/Entities/User.php b/modules/Auth/Entities/User.php
index 37805b20f0b0c781ab735591488621c4bc5c1165..c94e23f603cc07c428245476667c988e435495de 100644
--- a/modules/Auth/Entities/User.php
+++ b/modules/Auth/Entities/User.php
@@ -11,6 +11,7 @@ declare(strict_types=1);
 namespace Modules\Auth\Entities;
 
 use App\Entities\Podcast;
+use App\Models\NotificationModel;
 use App\Models\PodcastModel;
 use Myth\Auth\Entities\User as MythAuthUser;
 use RuntimeException;
@@ -26,6 +27,7 @@ use RuntimeException;
  * @property string|null $podcast_role
  *
  * @property Podcast[] $podcasts All podcasts the user is contributing to
+ * @property int[] $actorIdsWithUnreadNotifications Ids of the user's actors that have unread notifications
  */
 class User extends MythAuthUser
 {
@@ -34,6 +36,11 @@ class User extends MythAuthUser
      */
     protected ?array $podcasts = null;
 
+    /**
+     * @var int[]|null
+     */
+    protected ?array $actorIdsWithUnreadNotifications = null;
+
     /**
      * Array of field names and the type of value to cast them as when they are accessed.
      *
@@ -64,4 +71,25 @@ class User extends MythAuthUser
 
         return $this->podcasts;
     }
+
+    /**
+     * Returns the ids of the user's actors that have unread notifications
+     *
+     * @return int[]
+     */
+    public function getActorIdsWithUnreadNotifications(): array
+    {
+        if ($this->getPodcasts() === []) {
+            return [];
+        }
+
+        $unreadNotifications = (new NotificationModel())->whereIn(
+            'target_actor_id',
+            array_column($this->getPodcasts(), 'actor_id')
+        )
+            ->where('read_at', null)
+            ->findAll();
+
+        return array_column($unreadNotifications, 'target_actor_id');
+    }
 }
diff --git a/modules/Fediverse/Models/ActivityModel.php b/modules/Fediverse/Models/ActivityModel.php
index fd74f7edd988b276583c22a7c1daa56e09ac3e83..31b9659ead5a25d1a145db987af305825422ea17 100644
--- a/modules/Fediverse/Models/ActivityModel.php
+++ b/modules/Fediverse/Models/ActivityModel.php
@@ -97,7 +97,7 @@ class ActivityModel extends BaseUuidModel
                 'actor_id' => $actorId,
                 'target_actor_id' => $targetActorId,
                 'post_id' => $postId,
-                'type' => $type,
+                'type' => $type === 'Undo' ? $type . '_' . (json_decode($payload, true))['object']['type'] : $type,
                 'payload' => $payload,
                 'scheduled_at' => $scheduledAt,
                 'status' => $taskStatus,
diff --git a/themes/cp_admin/_partials/_nav_header.php b/themes/cp_admin/_partials/_nav_header.php
index e054fd0a855def499dd7af7614d088bae19ff78b..b3711aa956ab4f8b2dc778a5d70d9ef1227e730d 100644
--- a/themes/cp_admin/_partials/_nav_header.php
+++ b/themes/cp_admin/_partials/_nav_header.php
@@ -15,20 +15,73 @@
                 <?= icon('external-link', 'ml-1 opacity-60') ?>
         </a>
     </div>
-    <button
-        type="button"
-        class="inline-flex items-center h-full px-3 ml-auto text-sm font-semibold focus:ring-inset focus:ring-accent gap-x-2"
-        id="my-account-dropdown"
-        data-dropdown="button"
-        data-dropdown-target="my-account-dropdown-menu"
-        aria-haspopup="true"
-        aria-expanded="false"><div class="relative mr-1">
-            <?= icon('account-circle', 'text-3xl opacity-60') ?>
-            <?= user()
-                ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-navigation-bg" loading="lazy" />' ?>
-        </div>
-        <?= esc(user()->username) ?>
-        <?= icon('caret-down', 'ml-auto text-2xl') ?></button>
+    <div class="inline-flex items-center h-full ml-auto">
+        <button type="button" class="relative h-full px-2 focus:ring-accent focus:ring-inset" id="notifications-dropdown" data-dropdown="button" data-dropdown-target="notifications-dropdown-menu" aria-haspopup="true" aria-expanded="false">
+            <?= icon('notification-bell', 'text-2xl opacity-80') ?>
+            <?php if (user()->actorIdsWithUnreadNotifications !== []): ?>
+                <span class="absolute top-2 right-2 w-2.5 h-2.5 bg-red-500 rounded-full border border-navigation-bg"></span>
+            <?php endif ?>
+        </button>
+        <?php
+            $notificationsTitle = lang('Notifications.title');
+
+            $items = [
+                [
+                    'type' => 'html',
+                    'content' => esc(<<<CODE_SAMPLE
+                        <span class="px-4 my-2 text-xs font-semibold tracking-wider uppercase text-skin-muted">{$notificationsTitle}</span>
+                        CODE_SAMPLE),
+                ],
+            ];
+
+            if (user()->podcasts !== []) {
+                foreach (user()->podcasts as $userPodcast) {
+                    $userPodcastTitle = esc($userPodcast->title);
+
+                    $unreadNotificationDotDisplayClass = in_array($userPodcast->actor_id, user()->actorIdsWithUnreadNotifications, true) ? '' : 'hidden';
+
+                    $items[] = [
+                        'type' => 'link',
+                        'title' => <<<CODE_SAMPLE
+                            <div class="inline-flex items-center flex-1 text-sm align-middle">
+                                <div class="relative">
+                                    <img src="{$userPodcast->cover->tiny_url}" class="w-6 h-6 mr-2 rounded-full" loading="lazy" />
+                                    <span class="absolute top-0 right-1 w-2.5 h-2.5 bg-red-500 rounded-full border border-background-elevated {$unreadNotificationDotDisplayClass}"></span>
+                                </div>
+                                <span class="max-w-xs truncate">{$userPodcastTitle}</span>
+                            </div>
+                        CODE_SAMPLE
+                        ,
+                        'uri' => route_to('notification-list', $userPodcast->id),
+                    ];
+                }
+            } else {
+                $noNotificationsText = lang('Notifications.no_notifications');
+                $items[] = [
+                    'type' => 'html',
+                    'content' => esc(<<<CODE_SAMPLE
+                        <span class="mx-4 my-2 text-sm italic text-center text-skin-muted">{$noNotificationsText}</span>
+                    CODE_SAMPLE),
+                ];
+            }
+        ?>
+        <DropdownMenu id="notifications-dropdown-menu" labelledby="notifications-dropdown" items="<?= esc(json_encode($items)) ?>" placement="bottom"/>
+    
+        <button
+            type="button"
+            class="inline-flex items-center h-full px-3 text-sm font-semibold focus:ring-inset focus:ring-accent gap-x-2"
+            id="my-account-dropdown"
+            data-dropdown="button"
+            data-dropdown-target="my-account-dropdown-menu"
+            aria-haspopup="true"
+            aria-expanded="false"><div class="relative mr-1">
+                <?= icon('account-circle', 'text-3xl opacity-60') ?>
+                <?= user()
+                    ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-navigation-bg" loading="lazy" />' ?>
+            </div>
+            <?= esc(user()->username) ?>
+            <?= icon('caret-down', 'ml-auto text-2xl') ?></button>
+    </div>
     <?php
         $interactButtons = '';
         foreach (user()->podcasts as $userPodcast) {
diff --git a/themes/cp_admin/podcast/notifications.php b/themes/cp_admin/podcast/notifications.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5a4454e561f686cd3400b4d89ec166e9fc0580a
--- /dev/null
+++ b/themes/cp_admin/podcast/notifications.php
@@ -0,0 +1,104 @@
+<?= $this->extend('_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Notifications.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Notifications.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('headerRight') ?>
+<Button uri="<?= route_to('notification-mark-all-as-read', $podcast->actor_id) ?>" variant="primary"><?= lang('Notifications.mark_all_as_read') ?></Button>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+<?php if ($notifications === []): ?>
+    <div class="text-sm italic text-center text-skin-muted"><?= lang('Notifications.no_notifications') ?></div>
+<?php else: ?>        
+    <div class="-mx-2 -mt-8 border-b divide-y md:-mx-12">
+        <?php
+            foreach ($notifications as $notification):
+            $backgroundColor = $notification->read_at === null ? 'bg-heading-background' : 'bg-base';
+        ?>
+            <div class="py-3 hover:bg-white px-4 <?= $backgroundColor ?> group">
+                <?php
+                    $post = $notification->post_id !== null ? $notification->post : null;
+
+                    $actorUsername = '@' . esc($notification->actor
+                        ->username) .
+                                ($notification->actor->is_local
+                                    ? ''
+                                    : '@' . esc($notification->actor->domain));
+
+                    $actorUsernameHtml = <<<CODE_SAMPLE
+                    <strong class="break-all">{$actorUsername}</strong>
+                    CODE_SAMPLE;
+
+                    $targetActorUsername = '@' . esc($notification->target_actor->username);
+
+                    $targetActorUsernameHtml = <<<CODE_SAMPLE
+                    <strong class="break-all">{$targetActorUsername}</strong>
+                    CODE_SAMPLE;
+
+                    $notificationTitle = match ($notification->type) {
+                        'reply' => lang('Notifications.reply', [
+                            'actor_username' => $actorUsernameHtml,
+                        ], null, false),
+                        'like' => lang('Notifications.favourite', [
+                            'actor_username' => $actorUsernameHtml,
+                        ], null, false),
+                        'share' => lang('Notifications.reblog', [
+                            'actor_username' => $actorUsernameHtml,
+                        ], null, false),
+                        'follow' => lang('Notifications.follow', [
+                            'actor_username' => $actorUsernameHtml,
+                            'target_actor_username' => $targetActorUsernameHtml,
+                        ], null, false),
+                        default => '',
+                    };
+                    $notificationContent = $post !== null ? $post->message_html : null;
+
+                    $postLink = $post !== null ? route_to('post', esc($podcast->handle), $post->id) : route_to('podcast-activity', esc($podcast->handle));
+                    $link = $notification->read_at !== null ? $postLink : route_to('notification-mark-as-read', $podcast->id, $notification->id);
+                ?>
+                <a href="<?= $link ?>">
+                    <div class="flex items-start md:items-center">
+                        <div class="flex items-center shrink-0">
+                            <span class="w-2 h-2 bg-red-500 rounded-full <?= $notification->read_at === null ? '' : 'invisible' ?>"></span>
+                            <div class="relative ml-4">
+                                <img src="<?= $notification->actor->avatar_image_url ?>" alt="<?= esc($notification->actor->display_name) ?>" class="rounded-full shadow-inner w-14 h-14 aspect-square" loading="lazy" />
+                                <span class="absolute bottom-0 w-6 h-6 rounded-full -right-2.5 flex justify-center items-center <?= $backgroundColor ?> group-hover:bg-white">
+                                    <?php
+                                        $icon = match ($notification->type) {
+                                            'reply' => icon('chat', 'text-sky-500 text-base'),
+                                            'like' => icon('heart', 'text-rose-500 text-base'),
+                                            'share' => icon('repeat', 'text-green-500 text-base'),
+                                            'follow' => icon('user-follow', 'text-violet-500 text-base'),
+                                            default => '',
+                                        };
+                                    ?>
+                                    <?= $icon ?>
+                                </span>
+                            </div>
+                        </div>
+                        <div class="ml-5 md:flex md:items-center grow">
+                            <div class="grow">
+                                <?= $notificationTitle ?>
+                                <?php if ($notificationContent !== null): ?>
+                                    <p class="overflow-y-hidden text-skin-muted line-clamp-2 md:line-clamp-1"><?= $notificationContent ?></p>
+                                <?php endif; ?>
+                            </div>
+                            <span class="text-xs text-skin-muted md:ml-auto md:mr-4 whitespace-nowrap"><?= relative_time($notification->created_at) ?></span>
+                        </div>
+                    </div>
+                </a>
+            </div>
+        <?php endforeach; ?>
+    </div>
+
+    <div class="mt-6"><?= $pager->links() ?></div>
+
+<?php endif ?>
+
+<?= $this->endsection() ?>
diff --git a/themes/cp_app/_admin_navbar.php b/themes/cp_app/_admin_navbar.php
index da1def26d02c4a360f1941ef7db0f94b06503531..15f542f4456fa93260a55a79c97c47386f2e4597 100644
--- a/themes/cp_app/_admin_navbar.php
+++ b/themes/cp_app/_admin_navbar.php
@@ -10,20 +10,71 @@
         </div>
 
         <div class="inline-flex items-center h-full">
-        <button
-            type="button"
-            class="inline-flex items-center h-full px-3 text-sm font-semibold focus:ring-inset focus:ring-accent gap-x-2"
-            id="my-account-dropdown"
-            data-dropdown="button"
-            data-dropdown-target="my-account-dropdown-menu"
-            aria-haspopup="true"
-            aria-expanded="false"><div class="relative mr-1">
-                <?= icon('account-circle', 'text-3xl opacity-60') ?>
-                <?= user()
-                    ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-navigation-bg" loading="lazy" />' ?>
-            </div>
-            <?= esc(user()->username) ?>
-            <?= icon('caret-down', 'ml-auto text-2xl') ?></button>
+            <button type="button" class="relative h-full px-2 focus:ring-accent focus:ring-inset" id="notifications-dropdown" data-dropdown="button" data-dropdown-target="notifications-dropdown-menu" aria-haspopup="true" aria-expanded="false">
+                <?= icon('notification-bell', 'text-2xl opacity-80') ?>
+                <?php if (user()->actorIdsWithUnreadNotifications !== []): ?>
+                    <span class="absolute top-2 right-2 w-2.5 h-2.5 bg-red-500 rounded-full border border-navigation-bg"></span>
+                <?php endif ?>
+            </button>
+            <?php
+                $notificationsTitle = lang('Notifications.title');
+
+                $items = [
+                    [
+                        'type' => 'html',
+                        'content' => esc(<<<CODE_SAMPLE
+                            <span class="px-4 my-2 text-xs font-semibold tracking-wider uppercase text-skin-muted">{$notificationsTitle}</span>
+                            CODE_SAMPLE),
+                    ],
+                ];
+
+                if (user()->podcasts !== []) {
+                    foreach (user()->podcasts as $userPodcast) {
+                        $userPodcastTitle = esc($userPodcast->title);
+
+                        $unreadNotificationDotDisplayClass = in_array($userPodcast->actor_id, user()->actorIdsWithUnreadNotifications, true) ? '' : 'hidden';
+
+                        $items[] = [
+                            'type' => 'link',
+                            'title' => <<<CODE_SAMPLE
+                                <div class="inline-flex items-center flex-1 text-sm align-middle">
+                                    <div class="relative">
+                                        <img src="{$userPodcast->cover->tiny_url}" class="w-6 h-6 mr-2 rounded-full" loading="lazy" />
+                                        <span class="absolute top-0 right-1 w-2.5 h-2.5 bg-red-500 rounded-full border border-background-elevated {$unreadNotificationDotDisplayClass}"></span>
+                                    </div>
+                                    <span class="max-w-xs truncate">{$userPodcastTitle}</span>
+                                </div>
+                            CODE_SAMPLE
+                            ,
+                            'uri' => route_to('notification-list', $userPodcast->id),
+                        ];
+                    }
+                } else {
+                    $noNotificationsText = lang('Notifications.no_notifications');
+                    $items[] = [
+                        'type' => 'html',
+                        'content' => esc(<<<CODE_SAMPLE
+                            <span class="mx-4 my-2 text-sm italic text-center text-skin-muted">{$noNotificationsText}</span>
+                        CODE_SAMPLE),
+                    ];
+                }
+            ?>
+            <DropdownMenu id="notifications-dropdown-menu" labelledby="notifications-dropdown" items="<?= esc(json_encode($items)) ?>" placement="bottom"/>
+            
+            <button
+                type="button"
+                class="inline-flex items-center h-full px-3 text-sm font-semibold focus:ring-inset focus:ring-accent gap-x-2"
+                id="my-account-dropdown"
+                data-dropdown="button"
+                data-dropdown-target="my-account-dropdown-menu"
+                aria-haspopup="true"
+                aria-expanded="false"><div class="relative mr-1">
+                    <?= icon('account-circle', 'text-3xl opacity-60') ?>
+                    <?= user()
+                        ->podcasts === [] ? '' : '<img src="' . interact_as_actor()->avatar_image_url . '" class="absolute bottom-0 w-4 h-4 border rounded-full -right-1 border-navigation-bg" loading="lazy" />' ?>
+                </div>
+                <?= esc(user()->username) ?>
+                <?= icon('caret-down', 'ml-auto text-2xl') ?></button>
         <?php
             $interactButtons = '';
             foreach (user()->podcasts as $userPodcast) {