fix(messages): gate on server key state, not just local unlock

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').
This commit is contained in:
npeter83 2026-06-25 22:58:29 +02:00
parent 8392037e5f
commit 963afa33a6
5 changed files with 42 additions and 29 deletions

View file

@ -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<Record<number, string>>({});
const bottomRef = useRef<HTMLDivElement>(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<number, string> = {};
@ -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(() => {