reroute chat

This commit is contained in:
JCEEE
2026-08-10 17:28:02 +01:00
parent 3085934fb9
commit 4a025e0ee6
5 changed files with 99 additions and 59 deletions
+25 -26
View File
@@ -115,7 +115,16 @@ class ChatStore {
return;
}
// create
if (!this.messages.some((m) => m.id === record.id)) {
// If this is our own optimistic temp (same clientId), replace it with
// the server-confirmed record instead of appending a duplicate.
const tempIdx = this.messages.findIndex(
(m) => m.clientId && record.clientId && m.clientId === record.clientId
);
if (tempIdx !== -1) {
this.messages = this.messages
.map((m, i) => (i === tempIdx ? record : m))
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
} else if (!this.messages.some((m) => m.id === record.id)) {
this.messages = [...this.messages, record].sort((a, b) =>
a.createdAt.localeCompare(b.createdAt)
);
@@ -160,52 +169,42 @@ class ChatStore {
this.typingTimers.delete(actorId);
}
// ── Writes via proxy ──
// ── Writes via SvelteKit server (forwards session/device auth) ──
private async chatFetch(method: string, path: string, body?: unknown) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (this.actorType === 'member' && this.deviceToken) {
headers['x-device-token'] = this.deviceToken;
headers['x-device-famid'] = this.famId;
}
const res = await fetch(path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined
private async serverChat(payload: Record<string, unknown>) {
const res = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `${method} ${path} failed`);
if (!res.ok) throw new Error(data.error || 'chat request failed');
return data;
}
async send(content: string) {
const text = content.trim();
if (!text || !this.famId) return;
const clientId = crypto.randomUUID();
const temp: ChatMessage = {
id: 'temp-' + Date.now(),
id: 'temp-' + clientId,
famId: this.famId,
authorType: this.actorType,
authorId: this.actorId,
authorName: this.actorName,
authorColor: this.actorColor,
content: text,
createdAt: new Date().toISOString()
createdAt: new Date().toISOString(),
clientId
};
this.messages = [...this.messages, temp];
this.lastSeenAt = Date.now();
// Signal we stopped typing (message sent).
this.setTyping(false).catch(() => {});
try {
const saved = await this.chatFetch('POST', `/api/chat/${this.famId}/messages`, {
content: text
});
// SSE may have already delivered the real record via onMessage, so
// drop both the temp and any pre-existing copy of the saved id to
// avoid duplicate keys in the keyed each block.
this.messages = this.messages
.filter((m) => m.id !== temp.id && m.id !== saved.id)
.concat([{ ...saved, authorName: temp.authorName, authorColor: temp.authorColor }])
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
// Server writes to PB; the real record arrives via SSE and
// overwrites this optimistic temp (matched by clientId) in onMessage.
await this.serverChat({ action: 'send', famId: this.famId, content: text, clientId });
} catch (e) {
this.messages = this.messages.filter((m) => m.id !== temp.id);
console.error('Chat send failed:', e);
@@ -220,7 +219,7 @@ class ChatStore {
if (typing && now - this.typingThrottle < 1500) return;
this.typingThrottle = now;
try {
await this.chatFetch('POST', `/api/chat/${this.famId}/typing`, { typing });
await this.serverChat({ action: 'typing', famId: this.famId, typing });
} catch (e) {
console.error('Chat typing failed:', e);
}
+1
View File
@@ -181,6 +181,7 @@ export interface ChatMessage {
authorColor?: string;
content: string;
createdAt: string;
clientId?: string;
}
export interface TypingRow {