diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index b969f9e55..38e27ecf2 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -784,9 +784,6 @@ 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 3bf7a4880..deee748d8 100644 --- a/surfsense_backend/app/routes/notifications_routes.py +++ b/surfsense_backend/app/routes/notifications_routes.py @@ -30,19 +30,6 @@ 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, @@ -113,41 +100,3 @@ 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/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 53f33f27b..aa7a06c81 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -2,7 +2,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; -import { Inbox, LogOut, Logs, SquareLibrary, Trash2 } from "lucide-react"; +import { Inbox, LogOut, SquareLibrary, Trash2 } from "lucide-react"; import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useTheme } from "next-themes"; @@ -87,7 +87,7 @@ export function LayoutDataProvider({ // Inbox hook const userId = user?.id ? String(user.id) : null; - const { inboxItems, unreadCount, loading: inboxLoading, markAsRead, markAllAsRead, archiveItem } = useInbox( + const { inboxItems, unreadCount, loading: inboxLoading, markAsRead, markAllAsRead } = useInbox( userId, Number(searchSpaceId) || null, null @@ -551,7 +551,6 @@ export function LayoutDataProvider({ loading={inboxLoading} markAsRead={markAsRead} markAllAsRead={markAllAsRead} - archiveItem={archiveItem} /> {/* Create Search Space Dialog */} diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index f81417a45..9e9ed2d21 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -2,7 +2,6 @@ import { AlertCircle, - Archive, AtSign, BellDot, Check, @@ -12,7 +11,6 @@ import { Inbox, ListFilter, MoreHorizontal, - RotateCcw, Search, X, } from "lucide-react"; @@ -29,7 +27,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; @@ -59,7 +56,7 @@ function getInitials(name: string | null | undefined, email: string | null | und } type InboxTab = "mentions" | "status"; -type InboxFilter = "all" | "unread" | "archived"; +type InboxFilter = "all" | "unread"; interface InboxSidebarProps { open: boolean; @@ -69,7 +66,6 @@ interface InboxSidebarProps { loading: boolean; markAsRead: (id: number) => Promise; markAllAsRead: () => Promise; - archiveItem: (id: number, archived: boolean) => Promise; onCloseMobileSidebar?: () => void; } @@ -81,7 +77,6 @@ export function InboxSidebar({ loading, markAsRead, markAllAsRead, - archiveItem, onCloseMobileSidebar, }: InboxSidebarProps) { const t = useTranslations("sidebar"); @@ -91,10 +86,9 @@ export function InboxSidebar({ const [activeTab, setActiveTab] = useState("mentions"); const [activeFilter, setActiveFilter] = useState("all"); const [mounted, setMounted] = useState(false); - // Unified dropdown state: "filter" | "options" | number (item id) | null - const [openDropdown, setOpenDropdown] = useState<"filter" | "options" | number | null>(null); + // Dropdown state for filter and options menus + const [openDropdown, setOpenDropdown] = useState<"filter" | "options" | null>(null); const [markingAsReadId, setMarkingAsReadId] = useState(null); - const [archivingItemId, setArchivingItemId] = useState(null); useEffect(() => { setMounted(true); @@ -143,16 +137,8 @@ export function InboxSidebar({ let items = currentTabItems; // Apply filter - // Note: Use `item.archived === true` to handle undefined/null as false - if (activeFilter === "all") { - // "Unread & read" shows all non-archived items - items = items.filter((item) => item.archived !== true); - } else if (activeFilter === "unread") { - // "Unread" shows only unread non-archived items - items = items.filter((item) => !item.read && item.archived !== true); - } else if (activeFilter === "archived") { - // "Archived" shows only archived items (must be explicitly true) - items = items.filter((item) => item.archived === true); + if (activeFilter === "unread") { + items = items.filter((item) => !item.read); } // Apply search query @@ -168,24 +154,14 @@ export function InboxSidebar({ return items; }, [currentTabItems, activeFilter, searchQuery]); - // Count unread items per tab (filter-aware) + // Count unread items per tab const unreadMentionsCount = useMemo(() => { - if (activeFilter === "archived") { - // In archived view, show unread archived items - return mentionItems.filter((item) => !item.read && item.archived === true).length; - } - // For "all" and "unread" filters, show unread non-archived items - return mentionItems.filter((item) => !item.read && item.archived !== true).length; - }, [mentionItems, activeFilter]); + return mentionItems.filter((item) => !item.read).length; + }, [mentionItems]); const unreadStatusCount = useMemo(() => { - if (activeFilter === "archived") { - // In archived view, show unread archived items - return statusItems.filter((item) => !item.read && item.archived === true).length; - } - // For "all" and "unread" filters, show unread non-archived items - return statusItems.filter((item) => !item.read && item.archived !== true).length; - }, [statusItems, activeFilter]); + return statusItems.filter((item) => !item.read).length; + }, [statusItems]); const handleItemClick = useCallback( async (item: InboxItem) => { @@ -217,28 +193,10 @@ export function InboxSidebar({ [markAsRead, router, onOpenChange, onCloseMobileSidebar] ); - const handleMarkAsRead = useCallback( - async (itemId: number) => { - setMarkingAsReadId(itemId); - await markAsRead(itemId); - setMarkingAsReadId(null); - }, - [markAsRead] - ); - const handleMarkAllAsRead = useCallback(async () => { await markAllAsRead(); }, [markAllAsRead]); - const handleToggleArchive = useCallback( - async (itemId: number, currentlyArchived: boolean) => { - setArchivingItemId(itemId); - await archiveItem(itemId, !currentlyArchived); - setArchivingItemId(null); - }, - [archiveItem] - ); - const handleClearSearch = useCallback(() => { setSearchQuery(""); }, []); @@ -385,7 +343,7 @@ export function InboxSidebar({ > - {t("unread_and_read") || "Unread & read"} + {t("all") || "All"} {activeFilter === "all" && } @@ -399,16 +357,6 @@ export function InboxSidebar({ {activeFilter === "unread" && } - setActiveFilter("archived")} - className="flex items-center justify-between" - > - - - {t("archived") || "Archived"} - - {activeFilter === "archived" && } - {filteredItems.map((item) => { const isMarkingAsRead = markingAsReadId === item.id; - const isArchiving = archivingItemId === item.id; - const isBusy = isMarkingAsRead || isArchiving; - const isArchived = item.archived === true; return (
@@ -528,8 +473,8 @@ export function InboxSidebar({ - )} - -
@@ -592,93 +500,13 @@ export function InboxSidebar({ - {/* Time/dot and 3-dot button container - swap on hover (desktop only) */} -
- {/* Time and unread dot - visible by default, hidden on hover or when dropdown is open */} -
- - {formatTime(item.created_at)} - - {!item.read && ( - - )} -
- - {/* 3-dot menu - hidden by default, visible on hover or when dropdown is open */} - - setOpenDropdown(isOpen ? item.id : null) - } - > - - - - - {!item.read && ( - <> - handleMarkAsRead(item.id)} - disabled={isBusy} - > - - {t("mark_as_read") || "Mark as read"} - - - - )} - handleToggleArchive(item.id, isArchived)} - disabled={isArchiving} - > - {isArchived ? ( - <> - - {t("unarchive") || "Restore"} - - ) : ( - <> - - {t("archive") || "Archive"} - - )} - - - -
- - {/* Mobile time and unread dot - always visible on mobile */} -
+ {/* Time and unread dot - fixed width to prevent content shift */} +
{formatTime(item.created_at)} {!item.read && ( - + )}
diff --git a/surfsense_web/components/ui/spinner.tsx b/surfsense_web/components/ui/spinner.tsx index 483cdf73f..eeed30a8a 100644 --- a/surfsense_web/components/ui/spinner.tsx +++ b/surfsense_web/components/ui/spinner.tsx @@ -19,11 +19,10 @@ const sizeClasses = { export function Spinner({ size = "md", hideTrack = false, className }: SpinnerProps) { return ( -
{ - try { - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/notifications/${itemId}/archive`, - { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ archived }), - } - ); - - if (!response.ok) { - const error = await response.json().catch(() => ({ detail: "Failed to update archive status" })); - throw new Error(error.detail || "Failed to update inbox item archive status"); - } - - return true; - } catch (err) { - console.error("Failed to update inbox item archive status:", err); - return false; - } - }, []); - return { inboxItems, unreadCount: totalUnreadCount, markAsRead, markAllAsRead, - archiveItem, loading, error, }; diff --git a/surfsense_web/lib/electric/client.ts b/surfsense_web/lib/electric/client.ts index 222553f32..52f46d96d 100644 --- a/surfsense_web/lib/electric/client.ts +++ b/surfsense_web/lib/electric/client.ts @@ -55,7 +55,8 @@ const pendingSyncs = new Map>(); // Version for sync state - increment this to force fresh sync when Electric config changes // v2: user-specific database architecture // v3: added archived column to notifications -const SYNC_VERSION = 3; +// v4: removed archived column from notifications +const SYNC_VERSION = 4; // Database name prefix for identifying SurfSense databases const DB_PREFIX = "surfsense-"; @@ -182,7 +183,6 @@ export async function initElectric(userId: string): Promise { title TEXT NOT NULL, message TEXT NOT NULL, read BOOLEAN NOT NULL DEFAULT FALSE, - archived BOOLEAN NOT NULL DEFAULT FALSE, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ @@ -190,7 +190,6 @@ export async function initElectric(userId: string): Promise { CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id); CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read); - CREATE INDEX IF NOT EXISTS idx_notifications_archived ON notifications(archived); `); // Create the search_source_connectors table schema in PGlite