+
-
+
{name}
{!isSystem &&
}
+
+ {!isSystem && unlocked && (
+
+ )}
-
- {needsKey ? (
-
- ) : (
- <>
-
- {items.length === 0 ? (
-
{t("messages.threadEmpty")}
- ) : (
- items.map((m) => {
- const mine = m.sender_id === meId;
- const text = m.kind === "system" ? m.body ?? "" : plain[m.id] ?? "…";
- return (
-
-
- {text}
-
- {relativeTime(m.created_at)}
-
-
-
- );
- })
- )}
-
-
-
- {isSystem ? (
-
{t("messages.systemReadOnly")}
- ) : (
-
-
- )}
- >
- )}
+
);
}
diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx
index 30075fb..1822a3e 100644
--- a/frontend/src/components/NavSidebar.tsx
+++ b/frontend/src/components/NavSidebar.tsx
@@ -12,6 +12,7 @@ import {
Info,
ListVideo,
LogOut,
+ MessageSquare,
Settings,
Shield,
SlidersHorizontal,
@@ -126,15 +127,14 @@ export default function NavSidebar({
// One indicator for both layers: durable server notifications + the client-side transient
// events (the former separate bell is now folded into the inbox page).
const clientUnread = useSyncExternalStore(subscribe, getUnreadCount, getUnreadCount);
- // Unread direct messages also live in the notification module, so they add to the same badge.
- // The demo account can't use messaging, so don't poll it there.
+ const unread = (unreadQuery.data?.count ?? 0) + clientUnread;
+ // Direct messages are their own module with their own unread badge (demo can't message).
const msgUnreadQuery = useLiveQuery(
["message-unread"],
api.messageUnreadCount,
{ intervalMs: 30000, enabled: !me.is_demo }
);
- const unread =
- (unreadQuery.data?.count ?? 0) + clientUnread + (msgUnreadQuery.data?.count ?? 0);
+ const msgUnread = msgUnreadQuery.data?.count ?? 0;
type NavItem = { page: Page; icon: typeof Home; label: string; badge?: number };
// User-facing content modules vs. admin/system modules, separated by a divider in the rail.
@@ -143,6 +143,10 @@ export default function NavSidebar({
{ page: "channels", icon: Tv, label: t("header.account.channels") },
{ page: "playlists", icon: ListVideo, label: t("header.account.playlists") },
{ page: "notifications", icon: Bell, label: t("inbox.navLabel"), badge: unread },
+ // Direct messaging — its own module; hidden for the shared demo account.
+ ...(me.is_demo
+ ? []
+ : [{ page: "messages" as Page, icon: MessageSquare, label: t("messages.navLabel"), badge: msgUnread }]),
// Per-user sync status + your own API usage (admins get an extra system-wide tab inside).
{ page: "stats", icon: BarChart3, label: t("header.account.stats") },
];
diff --git a/frontend/src/components/NotificationsPanel.tsx b/frontend/src/components/NotificationsPanel.tsx
index 1ef97a3..e4883b5 100644
--- a/frontend/src/components/NotificationsPanel.tsx
+++ b/frontend/src/components/NotificationsPanel.tsx
@@ -1,10 +1,9 @@
-import { useEffect, useState, useSyncExternalStore } from "react";
+import { useEffect, useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { Activity, Bell, Check, CheckCheck, ExternalLink, Eye, MessageSquare, RotateCcw, Search, Trash2, Tv, UserPlus, X } from "lucide-react";
+import { Activity, Bell, Check, CheckCheck, ExternalLink, Eye, RotateCcw, Search, Trash2, Tv, UserPlus, X } from "lucide-react";
import { api, type AppNotification, type FeedFilters } from "../lib/api";
import { useLiveQuery } from "../lib/useLiveQuery";
-import Messages from "./Messages";
import {
clearAll as clearClient,
dismiss as dismissClient,
@@ -50,26 +49,14 @@ export default function NotificationsPanel({
setFilters,
setPage,
onFocusChannel,
- meId,
- isDemo,
}: {
filters: FeedFilters;
setFilters: (f: FeedFilters) => void;
setPage: (p: Page) => void;
onFocusChannel: (name: string) => void;
- meId: number;
- isDemo: boolean;
}) {
const { t } = useTranslation();
const qc = useQueryClient();
- const [tab, setTab] = useState<"inbox" | "messages">("inbox");
- // Messages unread for the tab badge (the nav rail badge is polled separately in NavSidebar).
- // Demo can't use messaging, so don't poll it there.
- const msgUnread = useLiveQuery<{ count: number }>(
- ["message-unread"],
- api.messageUnreadCount,
- { intervalMs: POLL_MS, enabled: !isDemo }
- );
const q = useLiveQuery<{ items: AppNotification[]; total: number }>(
["notifications"],
() => api.notifications(),
@@ -139,7 +126,7 @@ export default function NotificationsPanel({
{t("inbox.title")}
{t("inbox.subtitle")}
- {tab === "inbox" && hasAny && (
+ {hasAny && (