chore: auto-commit (23 archivos)
- app.md - backend/dist/assets/index-CFDWXN9Z.js - backend/dist/index.html - backend/handlers.go - backend/main.go - backend/users.go - e2e/smoke_live.sh - frontend/src/App.tsx - frontend/src/api.ts - frontend/src/components/CardChatPanel.tsx - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,9 @@ interface Props {
|
||||
users: User[];
|
||||
currentUserId?: string;
|
||||
onMessagesChange?: (messages: CardMessage[]) => void;
|
||||
// When set, the panel scrolls the matching message into view and flashes a
|
||||
// brief highlight (~2s). Used by notification click → open card.
|
||||
highlightMessageId?: string;
|
||||
}
|
||||
|
||||
// Window for considering a peer "actively typing" after its last event.
|
||||
@@ -98,7 +101,7 @@ function renderBody(body: string, knownUsers: Map<string, User>): ReactNode {
|
||||
return out;
|
||||
}
|
||||
|
||||
export function CardChatPanel({ cardId, users, currentUserId, onMessagesChange }: Props) {
|
||||
export function CardChatPanel({ cardId, users, currentUserId, onMessagesChange, highlightMessageId }: Props) {
|
||||
const [messages, setMessages] = useState<CardMessage[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [body, setBody] = useState("");
|
||||
@@ -185,6 +188,22 @@ export function CardChatPanel({ cardId, users, currentUserId, onMessagesChange }
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
// Scroll to + briefly pulse the message that triggered an incoming
|
||||
// notification. Runs whenever the highlight id changes AND the message
|
||||
// is present in the list (it may arrive asynchronously after WS sync).
|
||||
const [pulse, setPulse] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!highlightMessageId) return;
|
||||
if (!messages.some((m) => m.id === highlightMessageId)) return;
|
||||
const el = document.querySelector(`[data-msg-id="${highlightMessageId}"]`);
|
||||
if (el && el instanceof HTMLElement) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
setPulse(highlightMessageId);
|
||||
const t = setTimeout(() => setPulse(null), 2200);
|
||||
return () => clearTimeout(t);
|
||||
}, [highlightMessageId, messages]);
|
||||
|
||||
const sendTypingPing = () => {
|
||||
const ws = wsRef.current;
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
@@ -319,13 +338,25 @@ export function CardChatPanel({ cardId, users, currentUserId, onMessagesChange }
|
||||
const author = m.author_id ? usersById.get(m.author_id) : null;
|
||||
const isMe = m.author_id && m.author_id === currentUserId;
|
||||
const label = author ? author.display_name || author.username : "Anonimo";
|
||||
const highlighted = pulse === m.id;
|
||||
return (
|
||||
<Paper
|
||||
key={m.id}
|
||||
withBorder
|
||||
p="xs"
|
||||
radius="sm"
|
||||
bg={isMe ? "var(--mantine-color-blue-light)" : undefined}
|
||||
data-msg-id={m.id}
|
||||
bg={
|
||||
highlighted
|
||||
? "var(--mantine-color-yellow-light)"
|
||||
: isMe
|
||||
? "var(--mantine-color-blue-light)"
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
transition: "background-color 600ms ease",
|
||||
boxShadow: highlighted ? "0 0 0 2px var(--mantine-color-yellow-5)" : undefined,
|
||||
}}
|
||||
>
|
||||
<Group gap={6} wrap="nowrap" align="flex-start">
|
||||
<Avatar size={22} radius="xl" color={author?.color || tagColor(label)}>
|
||||
|
||||
Reference in New Issue
Block a user