From 22b2d6e400412296b4e3821bf02e0b3de4cb0367 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:34:58 +0530 Subject: [PATCH] feat: add archived column to notifications and implement archiving functionality - Introduced an archived boolean column in the notifications table to allow users to archive inbox items without deletion. - Updated Notification model to include the archived field with default value. - Added ArchiveRequest and ArchiveResponse models for handling archive/unarchive operations. - Implemented API endpoint to archive or unarchive notifications, ensuring real-time updates with Electric SQL. - Enhanced InboxSidebar to filter and display archived notifications appropriately. --- ...73_add_archived_column_to_notifications.py | 51 ++++++++++++++ surfsense_backend/app/db.py | 3 + .../app/routes/notifications_routes.py | 51 ++++++++++++++ .../ui/sidebar/AllPrivateChatsSidebar.tsx | 17 +---- .../ui/sidebar/AllSharedChatsSidebar.tsx | 17 +---- .../layout/ui/sidebar/InboxSidebar.tsx | 70 ++++++++++++++++--- surfsense_web/contracts/types/inbox.types.ts | 1 + surfsense_web/lib/electric/client.ts | 7 +- 8 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 surfsense_backend/alembic/versions/73_add_archived_column_to_notifications.py diff --git a/surfsense_backend/alembic/versions/73_add_archived_column_to_notifications.py b/surfsense_backend/alembic/versions/73_add_archived_column_to_notifications.py new file mode 100644 index 000000000..99962e501 --- /dev/null +++ b/surfsense_backend/alembic/versions/73_add_archived_column_to_notifications.py @@ -0,0 +1,51 @@ +"""Add archived column to notifications table + +Revision ID: 73 +Revises: 72 + +Adds an archived boolean column to the notifications table to allow users +to archive inbox items without deleting them. + +NOTE: Electric SQL automatically picks up schema changes when REPLICA IDENTITY FULL +is set (which was done in migration 66). We re-affirm it here to ensure replication +continues to work after adding the new column. +""" + +from collections.abc import Sequence + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "73" +down_revision: str | None = "72" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Add archived column to notifications table.""" + # Add the archived column with a default value + op.execute( + """ + ALTER TABLE notifications + ADD COLUMN IF NOT EXISTS archived BOOLEAN NOT NULL DEFAULT FALSE; + """ + ) + + # Create index for archived column + op.execute( + "CREATE INDEX IF NOT EXISTS ix_notifications_archived ON notifications (archived);" + ) + + # Re-affirm REPLICA IDENTITY FULL for Electric SQL after schema change + # This ensures Electric SQL continues to replicate all columns including the new one + op.execute("ALTER TABLE notifications REPLICA IDENTITY FULL;") + + +def downgrade() -> None: + """Remove archived column from notifications table.""" + op.execute("DROP INDEX IF EXISTS ix_notifications_archived;") + op.execute("ALTER TABLE notifications DROP COLUMN IF EXISTS archived;") + # Re-affirm REPLICA IDENTITY FULL after removing the column + op.execute("ALTER TABLE notifications REPLICA IDENTITY FULL;") + diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 38e27ecf2..b969f9e55 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -784,6 +784,9 @@ class Notification(BaseModel, TimestampMixin): read = Column( Boolean, nullable=False, default=False, server_default=text("false"), index=True ) + archived = Column( + Boolean, nullable=False, default=False, server_default=text("false"), index=True + ) notification_metadata = Column("metadata", JSONB, nullable=True, default={}) updated_at = Column( TIMESTAMP(timezone=True), diff --git a/surfsense_backend/app/routes/notifications_routes.py b/surfsense_backend/app/routes/notifications_routes.py index deee748d8..3bf7a4880 100644 --- a/surfsense_backend/app/routes/notifications_routes.py +++ b/surfsense_backend/app/routes/notifications_routes.py @@ -30,6 +30,19 @@ class MarkAllReadResponse(BaseModel): updated_count: int +class ArchiveRequest(BaseModel): + """Request body for archive/unarchive operations.""" + + archived: bool + + +class ArchiveResponse(BaseModel): + """Response for archive operations.""" + + success: bool + message: str + + @router.patch("/{notification_id}/read", response_model=MarkReadResponse) async def mark_notification_as_read( notification_id: int, @@ -100,3 +113,41 @@ async def mark_all_notifications_as_read( message=f"Marked {updated_count} notification(s) as read", updated_count=updated_count, ) + + +@router.patch("/{notification_id}/archive", response_model=ArchiveResponse) +async def archive_notification( + notification_id: int, + request: ArchiveRequest, + user: User = Depends(current_active_user), + session: AsyncSession = Depends(get_async_session), +) -> ArchiveResponse: + """ + Archive or unarchive a notification. + + Electric SQL will automatically sync this change to all connected clients. + """ + # Verify the notification belongs to the user + result = await session.execute( + select(Notification).where( + Notification.id == notification_id, + Notification.user_id == user.id, + ) + ) + notification = result.scalar_one_or_none() + + if not notification: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Notification not found", + ) + + # Update the notification + notification.archived = request.archived + await session.commit() + + action = "archived" if request.archived else "unarchived" + return ArchiveResponse( + success=True, + message=f"Notification {action}", + ) diff --git a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx index fb1a6ed0d..6be4809cf 100644 --- a/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/AllPrivateChatsSidebar.tsx @@ -238,20 +238,9 @@ export function AllPrivateChatsSidebar({ aria-label={t("chats") || "Private Chats"} >