From f8700dd37677278500fccaa97cd03ac272a45778 Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 1 Mar 2026 00:38:28 -0600
Subject: [PATCH 01/10] feat: add mentions column to notification preferences
schema
---
.../db/migrations/0020_concerned_toad_men.sql | 2 +
.../db/migrations/meta/0020_snapshot.json | 1211 +++++++++++++++++
.../server/db/migrations/meta/_journal.json | 7 +
src/lib/server/db/schema.ts | 1 +
4 files changed, 1221 insertions(+)
create mode 100644 src/lib/server/db/migrations/0020_concerned_toad_men.sql
create mode 100644 src/lib/server/db/migrations/meta/0020_snapshot.json
diff --git a/src/lib/server/db/migrations/0020_concerned_toad_men.sql b/src/lib/server/db/migrations/0020_concerned_toad_men.sql
new file mode 100644
index 0000000..9251222
--- /dev/null
+++ b/src/lib/server/db/migrations/0020_concerned_toad_men.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `notification_preferences` ADD `mentions` integer DEFAULT true NOT NULL;--> statement-breakpoint
+ALTER TABLE `groups` DROP COLUMN `max_duration_seconds`;
\ No newline at end of file
diff --git a/src/lib/server/db/migrations/meta/0020_snapshot.json b/src/lib/server/db/migrations/meta/0020_snapshot.json
new file mode 100644
index 0000000..bb5d0c3
--- /dev/null
+++ b/src/lib/server/db/migrations/meta/0020_snapshot.json
@@ -0,0 +1,1211 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "9beecb53-a092-49a5-b7bf-caddbb66474c",
+ "prevId": "56623836-95ff-4543-9b4b-c6687d4d9d24",
+ "tables": {
+ "clips": {
+ "name": "clips",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "added_by": {
+ "name": "added_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "original_url": {
+ "name": "original_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "video_path": {
+ "name": "video_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "thumbnail_path": {
+ "name": "thumbnail_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "platform": {
+ "name": "platform",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'downloading'"
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'video'"
+ },
+ "audio_path": {
+ "name": "audio_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "artist": {
+ "name": "artist",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "album_art": {
+ "name": "album_art",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "spotify_url": {
+ "name": "spotify_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "apple_music_url": {
+ "name": "apple_music_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "youtube_music_url": {
+ "name": "youtube_music_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "file_size_bytes": {
+ "name": "file_size_bytes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "clips_group_url": {
+ "name": "clips_group_url",
+ "columns": [
+ "group_id",
+ "original_url"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "clips_group_id_groups_id_fk": {
+ "name": "clips_group_id_groups_id_fk",
+ "tableFrom": "clips",
+ "tableTo": "groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "clips_added_by_users_id_fk": {
+ "name": "clips_added_by_users_id_fk",
+ "tableFrom": "clips",
+ "tableTo": "users",
+ "columnsFrom": [
+ "added_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "comment_hearts": {
+ "name": "comment_hearts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "comment_hearts_unique": {
+ "name": "comment_hearts_unique",
+ "columns": [
+ "comment_id",
+ "user_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "comment_hearts_comment_id_comments_id_fk": {
+ "name": "comment_hearts_comment_id_comments_id_fk",
+ "tableFrom": "comment_hearts",
+ "tableTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "comment_hearts_user_id_users_id_fk": {
+ "name": "comment_hearts_user_id_users_id_fk",
+ "tableFrom": "comment_hearts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "comment_views": {
+ "name": "comment_views",
+ "columns": {
+ "clip_id": {
+ "name": "clip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "viewed_at": {
+ "name": "viewed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "comment_views_unique": {
+ "name": "comment_views_unique",
+ "columns": [
+ "clip_id",
+ "user_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "comment_views_clip_id_clips_id_fk": {
+ "name": "comment_views_clip_id_clips_id_fk",
+ "tableFrom": "comment_views",
+ "tableTo": "clips",
+ "columnsFrom": [
+ "clip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "comment_views_user_id_users_id_fk": {
+ "name": "comment_views_user_id_users_id_fk",
+ "tableFrom": "comment_views",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "comments": {
+ "name": "comments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "clip_id": {
+ "name": "clip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "gif_url": {
+ "name": "gif_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "comments_clip_id_clips_id_fk": {
+ "name": "comments_clip_id_clips_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "clips",
+ "columnsFrom": [
+ "clip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "comments_user_id_users_id_fk": {
+ "name": "comments_user_id_users_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "favorites": {
+ "name": "favorites",
+ "columns": {
+ "clip_id": {
+ "name": "clip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "favorites_clip_id_clips_id_fk": {
+ "name": "favorites_clip_id_clips_id_fk",
+ "tableFrom": "favorites",
+ "tableTo": "clips",
+ "columnsFrom": [
+ "clip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "favorites_user_id_users_id_fk": {
+ "name": "favorites_user_id_users_id_fk",
+ "tableFrom": "favorites",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "groups": {
+ "name": "groups",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "invite_code": {
+ "name": "invite_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "retention_days": {
+ "name": "retention_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "max_storage_mb": {
+ "name": "max_storage_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "max_file_size_mb": {
+ "name": "max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 500
+ },
+ "accent_color": {
+ "name": "accent_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'coral'"
+ },
+ "download_provider": {
+ "name": "download_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "platform_filter_mode": {
+ "name": "platform_filter_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'all'"
+ },
+ "platform_filter_list": {
+ "name": "platform_filter_list",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "shortcut_token": {
+ "name": "shortcut_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "shortcut_url": {
+ "name": "shortcut_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "groups_invite_code_unique": {
+ "name": "groups_invite_code_unique",
+ "columns": [
+ "invite_code"
+ ],
+ "isUnique": true
+ },
+ "groups_shortcut_token_unique": {
+ "name": "groups_shortcut_token_unique",
+ "columns": [
+ "shortcut_token"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notification_preferences": {
+ "name": "notification_preferences",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "new_adds": {
+ "name": "new_adds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "reactions": {
+ "name": "reactions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "comments": {
+ "name": "comments",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "mentions": {
+ "name": "mentions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "daily_reminder": {
+ "name": "daily_reminder",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "notification_preferences_user_id_users_id_fk": {
+ "name": "notification_preferences_user_id_users_id_fk",
+ "tableFrom": "notification_preferences",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notifications": {
+ "name": "notifications",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "clip_id": {
+ "name": "clip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emoji": {
+ "name": "emoji",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "comment_preview": {
+ "name": "comment_preview",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "read_at": {
+ "name": "read_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "notifications_user_created": {
+ "name": "notifications_user_created",
+ "columns": [
+ "user_id",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "notifications_user_id_users_id_fk": {
+ "name": "notifications_user_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "notifications_clip_id_clips_id_fk": {
+ "name": "notifications_clip_id_clips_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "clips",
+ "columnsFrom": [
+ "clip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "notifications_actor_id_users_id_fk": {
+ "name": "notifications_actor_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "actor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "push_subscriptions": {
+ "name": "push_subscriptions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "keys_p256dh": {
+ "name": "keys_p256dh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "keys_auth": {
+ "name": "keys_auth",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "push_subscriptions_user_id_users_id_fk": {
+ "name": "push_subscriptions_user_id_users_id_fk",
+ "tableFrom": "push_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "reactions": {
+ "name": "reactions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "clip_id": {
+ "name": "clip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emoji": {
+ "name": "emoji",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "reactions_unique": {
+ "name": "reactions_unique",
+ "columns": [
+ "clip_id",
+ "user_id",
+ "emoji"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "reactions_clip_id_clips_id_fk": {
+ "name": "reactions_clip_id_clips_id_fk",
+ "tableFrom": "reactions",
+ "tableTo": "clips",
+ "columnsFrom": [
+ "clip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "reactions_user_id_users_id_fk": {
+ "name": "reactions_user_id_users_id_fk",
+ "tableFrom": "reactions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "theme_preference": {
+ "name": "theme_preference",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'system'"
+ },
+ "auto_scroll": {
+ "name": "auto_scroll",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "muted_by_default": {
+ "name": "muted_by_default",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "avatar_path": {
+ "name": "avatar_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "removed_at": {
+ "name": "removed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "users_phone_unique": {
+ "name": "users_phone_unique",
+ "columns": [
+ "phone"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "users_group_id_groups_id_fk": {
+ "name": "users_group_id_groups_id_fk",
+ "tableFrom": "users",
+ "tableTo": "groups",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "verification_codes": {
+ "name": "verification_codes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "verified_at": {
+ "name": "verified_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "verification_codes_user_id_users_id_fk": {
+ "name": "verification_codes_user_id_users_id_fk",
+ "tableFrom": "verification_codes",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "watched": {
+ "name": "watched",
+ "columns": {
+ "clip_id": {
+ "name": "clip_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "watch_percent": {
+ "name": "watch_percent",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "watched_at": {
+ "name": "watched_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "watched_clip_user": {
+ "name": "watched_clip_user",
+ "columns": [
+ "clip_id",
+ "user_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "watched_clip_id_clips_id_fk": {
+ "name": "watched_clip_id_clips_id_fk",
+ "tableFrom": "watched",
+ "tableTo": "clips",
+ "columnsFrom": [
+ "clip_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "watched_user_id_users_id_fk": {
+ "name": "watched_user_id_users_id_fk",
+ "tableFrom": "watched",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/src/lib/server/db/migrations/meta/_journal.json b/src/lib/server/db/migrations/meta/_journal.json
index 56490aa..ed74af1 100644
--- a/src/lib/server/db/migrations/meta/_journal.json
+++ b/src/lib/server/db/migrations/meta/_journal.json
@@ -141,6 +141,13 @@
"when": 1772318561461,
"tag": "0019_small_edwin_jarvis",
"breakpoints": true
+ },
+ {
+ "idx": 20,
+ "version": "6",
+ "when": 1772347099686,
+ "tag": "0020_concerned_toad_men",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
index 2bb2c00..1cbfe98 100644
--- a/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -199,5 +199,6 @@ export const notificationPreferences = sqliteTable('notification_preferences', {
newAdds: integer('new_adds', { mode: 'boolean' }).notNull().default(true),
reactions: integer('reactions', { mode: 'boolean' }).notNull().default(true),
comments: integer('comments', { mode: 'boolean' }).notNull().default(true),
+ mentions: integer('mentions', { mode: 'boolean' }).notNull().default(true),
dailyReminder: integer('daily_reminder', { mode: 'boolean' }).notNull().default(false)
});
From 72c8ce044119a2c70bb337e47aa304ec545714f0 Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 1 Mar 2026 00:34:07 -0600
Subject: [PATCH 02/10] feat: add @mention notification system with server-side
extraction
- Create mentions.ts utility with extractMentions() and notifyMentions()
- Accept message field in POST /api/clips, auto-post as first comment
- Add @mention dispatch to comments API with duplicate prevention
- Update notification preferences API to include mentions toggle
---
src/lib/server/mentions.ts | 80 +++++++++++++++++++
src/lib/server/push.ts | 2 +-
src/routes/api/clips/+server.ts | 32 +++++++-
src/routes/api/clips/[id]/comments/+server.ts | 30 ++++++-
.../api/notifications/preferences/+server.ts | 12 ++-
5 files changed, 149 insertions(+), 7 deletions(-)
create mode 100644 src/lib/server/mentions.ts
diff --git a/src/lib/server/mentions.ts b/src/lib/server/mentions.ts
new file mode 100644
index 0000000..dfad705
--- /dev/null
+++ b/src/lib/server/mentions.ts
@@ -0,0 +1,80 @@
+import { db } from '$lib/server/db';
+import { users, notificationPreferences, notifications } from '$lib/server/db/schema';
+import { eq, and, isNull } from 'drizzle-orm';
+import { sendNotification } from '$lib/server/push';
+import { v4 as uuid } from 'uuid';
+import { createLogger } from '$lib/server/logger';
+
+const log = createLogger('mentions');
+
+/**
+ * Extract @username mentions from text.
+ * Returns an array of unique lowercase usernames.
+ */
+export function extractMentions(text: string): string[] {
+ const regex = /@(\w+)/g;
+ const mentions = new Set
{n.commentPreview}
{/if} {relativeTime(n.createdAt)} diff --git a/src/lib/components/settings/NotificationSettings.svelte b/src/lib/components/settings/NotificationSettings.svelte index 99cb405..4be87a4 100644 --- a/src/lib/components/settings/NotificationSettings.svelte +++ b/src/lib/components/settings/NotificationSettings.svelte @@ -93,6 +93,22 @@ +