The Messages UI trusted only the per-device IndexedDB key, so if the server's key record was gone (deleted, DB-restored, admin-reset) while a stale private key lingered in the browser, the app looked 'unlocked' but no one could be messaged and no setup was offered. Add useKeyState (server 'configured' AND local unlock): show setup when the server has no key (setup overwrites the stale local key), unlock when it has one this device hasn't opened, ready otherwise. ChatThread is now self-contained. Also fix the header title on the Messages and Notifications pages (was falling through to 'Channel manager').
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { Library, Search, User } from "lucide-react";
|
|
import type { FeedFilters, Me } from "../lib/api";
|
|
import type { Page } from "../lib/urlState";
|
|
import SyncStatus from "./SyncStatus";
|
|
|
|
// Contextual top bar. Primary navigation + account now live in the left NavSidebar; the
|
|
// header carries the global sync status, the feed's scope toggle + search (or the current
|
|
// page title), the language switcher and the notification bell.
|
|
export default function Header({
|
|
me,
|
|
filters,
|
|
setFilters,
|
|
page,
|
|
onGoToFullHistory,
|
|
}: {
|
|
me: Me;
|
|
filters: FeedFilters;
|
|
setFilters: (f: FeedFilters) => void;
|
|
page: Page;
|
|
onGoToFullHistory: () => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<header className="glass h-14 shrink-0 border-b border-border flex items-center gap-3 px-4 z-20">
|
|
<SyncStatus isAdmin={me.role === "admin"} onGoToFullHistory={onGoToFullHistory} />
|
|
|
|
{page === "feed" && (
|
|
<div
|
|
className="flex items-center rounded-full border border-border bg-card p-0.5 text-xs shrink-0"
|
|
role="group"
|
|
aria-label={t("header.scope.label")}
|
|
>
|
|
{(["my", "all"] as const).map((s) => (
|
|
<button
|
|
key={s}
|
|
onClick={() => setFilters({ ...filters, scope: s })}
|
|
title={t("header.scope." + s + "Tip")}
|
|
aria-pressed={filters.scope === s}
|
|
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full transition ${
|
|
filters.scope === s
|
|
? "bg-accent text-accent-fg"
|
|
: "text-muted hover:text-fg"
|
|
}`}
|
|
>
|
|
{s === "my" ? (
|
|
<User className="w-3.5 h-3.5" />
|
|
) : (
|
|
<Library className="w-3.5 h-3.5" />
|
|
)}
|
|
<span className="hidden sm:inline">{t("header.scope." + s)}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{page === "feed" ? (
|
|
<div className="flex-1 max-w-xl mx-auto relative">
|
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted" />
|
|
<input
|
|
value={filters.q}
|
|
onChange={(e) => setFilters({ ...filters, q: e.target.value })}
|
|
placeholder={t("header.searchPlaceholder")}
|
|
className="w-full bg-card border border-border rounded-full pl-9 pr-4 py-2 text-sm outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 text-center text-sm font-semibold">
|
|
{page === "stats"
|
|
? t("header.usageStats")
|
|
: page === "playlists"
|
|
? t("header.account.playlists")
|
|
: page === "settings"
|
|
? t("settings.title")
|
|
: page === "scheduler"
|
|
? t("header.scheduler")
|
|
: page === "config"
|
|
? t("header.configuration")
|
|
: page === "users"
|
|
? t("header.users")
|
|
: page === "notifications"
|
|
? t("inbox.navLabel")
|
|
: page === "messages"
|
|
? t("messages.navLabel")
|
|
: t("header.channelManager")}
|
|
</div>
|
|
)}
|
|
</header>
|
|
);
|
|
}
|