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:
2026-05-21 18:22:44 +02:00
parent 2524340759
commit c9e15513c7
22 changed files with 2380 additions and 179 deletions
+33 -2
View File
@@ -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)}>