feat(messages): auto-open/flash dock on incoming + persist dock across reload
The live-message push now carries both parties, so an incoming message opens that conversation's dock window if it's closed, or flashes it once if it's already open (expanded or minimised, without disturbing the minimised state) — only for messages from someone else, never your own echo. Dock state (open windows + minimised state) is persisted per user, so a reload restores exactly what was open, minimised, or closed.
This commit is contained in:
parent
3fd71cd316
commit
8392037e5f
4 changed files with 110 additions and 23 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
||||
import { closeChat, toggleMinimize, useDockChats, type DockChat } from "../lib/chatDock";
|
||||
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";
|
||||
|
|
@ -10,27 +10,33 @@ import Avatar from "./Avatar";
|
|||
import ChatThread from "./ChatThread";
|
||||
|
||||
// The Messenger-style floating chat dock: open conversations live here, bottom-right, across
|
||||
// page navigation. Always mounted (for human users) so it also owns the app-wide live-message
|
||||
// subscription and restores this device's unlock state on load.
|
||||
// page navigation (state persisted per user). Always mounted (for human users) so it also owns
|
||||
// the app-wide live-message subscription and restores this device's unlock state on load.
|
||||
export default function ChatDock({ meId }: { meId: number }) {
|
||||
const qc = useQueryClient();
|
||||
const chats = useDockChats();
|
||||
const unlocked = useUnlocked();
|
||||
|
||||
// Restore the per-device unlocked key once, app-wide.
|
||||
// Restore the per-device unlocked key and the persisted dock windows once, app-wide.
|
||||
useEffect(() => {
|
||||
initDock(meId);
|
||||
e2ee.loadFromDevice(meId);
|
||||
}, [meId]);
|
||||
|
||||
// Live delivery: any pushed message refreshes the conversation list, unread badge, and the
|
||||
// affected thread (re-decrypts). Drives both the dock and the Messages page.
|
||||
// Live delivery: refresh the conversation list, unread badge, and the affected thread; and
|
||||
// for an INCOMING message, open or flash that partner's dock window.
|
||||
useEffect(
|
||||
() =>
|
||||
onMessage((m) => {
|
||||
onMessage(({ message: m, from }) => {
|
||||
qc.invalidateQueries({ queryKey: ["conversations"] });
|
||||
qc.invalidateQueries({ queryKey: ["message-unread"] });
|
||||
const partner = m.sender_id === meId ? m.recipient_id : m.sender_id;
|
||||
qc.invalidateQueries({ queryKey: ["thread", partner] });
|
||||
const partnerId = m.sender_id === meId ? m.recipient_id : m.sender_id;
|
||||
qc.invalidateQueries({ queryKey: ["thread", partnerId] });
|
||||
// Auto-open/flash only for messages from someone else (not my own echo) and only for
|
||||
// real user conversations.
|
||||
if (m.kind === "user" && m.sender_id !== meId && from) {
|
||||
notifyIncoming({ id: from.id, name: from.name, avatar_url: from.avatar_url });
|
||||
}
|
||||
}),
|
||||
[qc, meId]
|
||||
);
|
||||
|
|
@ -47,8 +53,22 @@ export default function ChatDock({ meId }: { meId: number }) {
|
|||
|
||||
function ChatWindow({ chat, meId, unlocked }: { chat: DockChat; meId: number; unlocked: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const [flashing, setFlashing] = useState(false);
|
||||
|
||||
// One-shot attention flash whenever a new message bumps the counter.
|
||||
useEffect(() => {
|
||||
if (chat.flash <= 0) return;
|
||||
setFlashing(true);
|
||||
const id = setTimeout(() => setFlashing(false), 800);
|
||||
return () => clearTimeout(id);
|
||||
}, [chat.flash]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto w-80 max-w-[calc(100vw-2rem)] glass rounded-2xl shadow-2xl flex flex-col overflow-hidden">
|
||||
<div
|
||||
className={`pointer-events-auto w-80 max-w-[calc(100vw-2rem)] glass rounded-2xl shadow-2xl flex flex-col overflow-hidden transition-all duration-300 ${
|
||||
flashing ? "ring-2 ring-accent" : "ring-0"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-card/50">
|
||||
<button
|
||||
onClick={() => toggleMinimize(chat.partnerId)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue