diff --git a/frontend/src/components/ChatDock.tsx b/frontend/src/components/ChatDock.tsx
index ba24db7..12fff57 100644
--- a/frontend/src/components/ChatDock.tsx
+++ b/frontend/src/components/ChatDock.tsx
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { ChevronDown, ChevronUp, X } from "lucide-react";
import { closeChat, initDock, notifyIncoming, toggleMinimize, useDockChats, type DockChat } from "../lib/chatDock";
-import { useUnlocked } from "../lib/messaging";
import { onMessage } from "../lib/messagesSocket";
import * as e2ee from "../lib/e2ee";
import Avatar from "./Avatar";
@@ -15,7 +14,6 @@ import ChatThread from "./ChatThread";
export default function ChatDock({ meId }: { meId: number }) {
const qc = useQueryClient();
const chats = useDockChats();
- const unlocked = useUnlocked();
// Restore the per-device unlocked key and the persisted dock windows once, app-wide.
useEffect(() => {
@@ -45,13 +43,13 @@ export default function ChatDock({ meId }: { meId: number }) {
return (
{chats.map((c) => (
-
+
))}
);
}
-function ChatWindow({ chat, meId, unlocked }: { chat: DockChat; meId: number; unlocked: boolean }) {
+function ChatWindow({ chat, meId }: { chat: DockChat; meId: number }) {
const { t } = useTranslation();
const [flashing, setFlashing] = useState(false);
@@ -95,7 +93,7 @@ function ChatWindow({ chat, meId, unlocked }: { chat: DockChat; meId: number; un
{!chat.minimized && (
-
+
)}
diff --git a/frontend/src/components/ChatThread.tsx b/frontend/src/components/ChatThread.tsx
index bd373e7..138eddb 100644
--- a/frontend/src/components/ChatThread.tsx
+++ b/frontend/src/components/ChatThread.tsx
@@ -5,7 +5,7 @@ import { Send } from "lucide-react";
import { api, type Message, type MessageUser } from "../lib/api";
import { useLiveQuery } from "../lib/useLiveQuery";
import { relativeTime } from "../lib/format";
-import { partnerPub } from "../lib/messaging";
+import { partnerPub, useKeyState } from "../lib/messaging";
import * as e2ee from "../lib/e2ee";
import KeyGate from "./KeyGate";
@@ -13,24 +13,24 @@ const POLL_MS = 20000; // safety net; live updates arrive over the WebSocket
// The message list + composer for one conversation, shared by the full Messages page and the
// floating dock window. Handles client-side decryption and encrypt-on-send; the surrounding
-// chrome (back button / minimize / close) is the caller's.
+// chrome (back button / minimize / close) is the caller's. Self-contained key gating: prompts
+// setup/unlock (server-truth) for a user conversation until messaging is ready.
export default function ChatThread({
meId,
partnerId,
isSystem,
- unlocked,
}: {
meId: number;
partnerId: number;
isSystem: boolean;
- unlocked: boolean;
}) {
const { t } = useTranslation();
const qc = useQueryClient();
const [draft, setDraft] = useState("");
const [plain, setPlain] = useState>({});
const bottomRef = useRef(null);
- const needsKey = !isSystem && !unlocked;
+ const ready = useKeyState() === "ready";
+ const needsKey = !isSystem && !ready;
const q = useLiveQuery<{ partner: MessageUser; items: Message[] }>(
["thread", partnerId],
@@ -40,7 +40,7 @@ export default function ChatThread({
const items = q.data?.items ?? [];
useEffect(() => {
- if (isSystem || !unlocked) return;
+ if (isSystem || !ready) return;
let cancelled = false;
(async () => {
const out: Record = {};
@@ -59,7 +59,7 @@ export default function ChatThread({
return () => {
cancelled = true;
};
- }, [items, unlocked, isSystem, partnerId, t]);
+ }, [items, ready, isSystem, partnerId, t]);
// Opening / refreshing the thread marks incoming messages read, so clear the badges.
useEffect(() => {
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index b0f1b4b..a7a1c26 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -79,7 +79,11 @@ export default function Header({
? t("header.configuration")
: page === "users"
? t("header.users")
- : t("header.channelManager")}
+ : page === "notifications"
+ ? t("inbox.navLabel")
+ : page === "messages"
+ ? t("messages.navLabel")
+ : t("header.channelManager")}
)}
diff --git a/frontend/src/components/Messages.tsx b/frontend/src/components/Messages.tsx
index 9a10680..231d672 100644
--- a/frontend/src/components/Messages.tsx
+++ b/frontend/src/components/Messages.tsx
@@ -5,7 +5,7 @@ import { ArrowLeft, ShieldCheck, SquarePen, ExternalLink } from "lucide-react";
import { api, type Conversation, type MessageUser } from "../lib/api";
import { useLiveQuery } from "../lib/useLiveQuery";
import { relativeTime } from "../lib/format";
-import { partnerPub, SYSTEM_ID, useUnlocked } from "../lib/messaging";
+import { partnerPub, SYSTEM_ID, useKeyState } from "../lib/messaging";
import { openChat } from "../lib/chatDock";
import * as e2ee from "../lib/e2ee";
import Avatar from "./Avatar";
@@ -18,7 +18,6 @@ type View = "list" | "new" | { partnerId: number; name?: string; avatar?: string
export default function Messages({ meId }: { meId: number }) {
const [view, setView] = useState("list");
- const unlocked = useUnlocked();
if (typeof view === "object") {
return (
@@ -28,7 +27,6 @@ export default function Messages({ meId }: { meId: number }) {
isSystem={!!view.system}
seedName={view.name}
seedAvatar={view.avatar}
- unlocked={unlocked}
onBack={() => setView("list")}
/>
);
@@ -39,7 +37,6 @@ export default function Messages({ meId }: { meId: number }) {
return (
setView({ partnerId: c.partner.id, name: c.partner.name, avatar: c.partner.avatar_url, system: c.partner.id === SYSTEM_ID })}
onNew={() => setView("new")}
/>
@@ -48,22 +45,22 @@ export default function Messages({ meId }: { meId: number }) {
function ConversationList({
meId,
- unlocked,
onOpen,
onNew,
}: {
meId: number;
- unlocked: boolean;
onOpen: (c: Conversation) => void;
onNew: () => void;
}) {
const { t } = useTranslation();
+ const keyState = useKeyState();
+ const ready = keyState === "ready";
const q = useLiveQuery<{ items: Conversation[] }>(["conversations"], api.conversations, { intervalMs: POLL_MS });
const items = q.data?.items ?? [];
const [previews, setPreviews] = useState>({});
useEffect(() => {
- if (!unlocked) return;
+ if (!ready) return;
let cancelled = false;
(async () => {
const out: Record = {};
@@ -83,12 +80,12 @@ function ConversationList({
return () => {
cancelled = true;
};
- }, [items, unlocked]);
+ }, [items, ready]);
function previewText(c: Conversation): string {
const m = c.last_message;
if (m.kind === "system") return m.body ?? "";
- if (!unlocked) return "🔒 " + t("messages.encrypted");
+ if (!ready) return "🔒 " + t("messages.encrypted");
return previews[c.partner.id] ?? "…";
}
@@ -96,7 +93,7 @@ function ConversationList({
{t("messages.conversations")}
- {unlocked && (
+ {ready && (
- {!unlocked &&
}
+ {(keyState === "setup" || keyState === "unlock") &&
}
{q.isLoading && !q.data ? (
{t("common.loading")}
) : items.length === 0 ? (
@@ -134,7 +131,7 @@ function ConversationList({
- {!isSystem && unlocked && (
+ {!isSystem && ready && (