| .. | .. |
|---|
| 146 | 146 | export function ChatProvider({ children }: { children: React.ReactNode }) { |
|---|
| 147 | 147 | const [sessions, setSessions] = useState<WsSession[]>([]); |
|---|
| 148 | 148 | const [activeSessionId, setActiveSessionId] = useState<string | null>(null); |
|---|
| 149 | + const activeSessionIdRef = useRef<string | null>(null); |
|---|
| 149 | 150 | const [latestScreenshot, setLatestScreenshot] = useState<string | null>(null); |
|---|
| 150 | 151 | const needsSync = useRef(true); |
|---|
| 151 | 152 | |
|---|
| .. | .. |
|---|
| 187 | 188 | if (active) { |
|---|
| 188 | 189 | setActiveSessionId((prev) => { |
|---|
| 189 | 190 | if (prev !== active.id) { |
|---|
| 190 | | - if (prev) { |
|---|
| 191 | | - messagesMapRef.current[prev] = messages; |
|---|
| 192 | | - } |
|---|
| 191 | + // No need to save prev — messagesMapRef is kept in sync by all mutators |
|---|
| 193 | 192 | const all = messagesMapRef.current[active.id] ?? []; |
|---|
| 194 | 193 | const page = all.length > PAGE_SIZE ? all.slice(-PAGE_SIZE) : all; |
|---|
| 195 | 194 | setMessages(page); |
|---|
| .. | .. |
|---|
| 201 | 200 | return next; |
|---|
| 202 | 201 | }); |
|---|
| 203 | 202 | } |
|---|
| 203 | + activeSessionIdRef.current = active.id; |
|---|
| 204 | 204 | return active.id; |
|---|
| 205 | 205 | }); |
|---|
| 206 | 206 | } |
|---|
| 207 | | - }, [messages]); |
|---|
| 207 | + }, []); |
|---|
| 208 | 208 | |
|---|
| 209 | 209 | // On connect: ask gateway to sync sessions. If we already had a session |
|---|
| 210 | 210 | // selected, tell the gateway so it preserves our selection instead of |
|---|
| .. | .. |
|---|
| 212 | 212 | useEffect(() => { |
|---|
| 213 | 213 | if (status === "connected") { |
|---|
| 214 | 214 | needsSync.current = true; |
|---|
| 215 | | - sendCommand("sync", activeSessionId ? { activeSessionId } : undefined); |
|---|
| 215 | + const id = activeSessionIdRef.current; |
|---|
| 216 | + sendCommand("sync", id ? { activeSessionId: id } : undefined); |
|---|
| 216 | 217 | } else if (status === "disconnected") { |
|---|
| 217 | 218 | setIsTyping(false); |
|---|
| 218 | 219 | } |
|---|
| .. | .. |
|---|
| 223 | 224 | const addMessageToActive = useCallback((msg: Message) => { |
|---|
| 224 | 225 | setMessages((prev) => { |
|---|
| 225 | 226 | const next = [...prev, msg]; |
|---|
| 226 | | - setActiveSessionId((id) => { |
|---|
| 227 | | - if (id) { |
|---|
| 228 | | - messagesMapRef.current[id] = next; |
|---|
| 229 | | - debouncedSave(messagesMapRef.current); |
|---|
| 230 | | - } |
|---|
| 231 | | - return id; |
|---|
| 232 | | - }); |
|---|
| 227 | + const id = activeSessionIdRef.current; |
|---|
| 228 | + if (id) { |
|---|
| 229 | + messagesMapRef.current[id] = next; |
|---|
| 230 | + debouncedSave(messagesMapRef.current); |
|---|
| 231 | + } |
|---|
| 233 | 232 | return next; |
|---|
| 234 | 233 | }); |
|---|
| 235 | 234 | }, []); |
|---|
| 236 | 235 | |
|---|
| 237 | 236 | // Helper: add a message to a specific session (may not be active) |
|---|
| 238 | 237 | const addMessageToSession = useCallback((sessionId: string, msg: Message) => { |
|---|
| 239 | | - setActiveSessionId((currentActive) => { |
|---|
| 240 | | - if (sessionId === currentActive) { |
|---|
| 241 | | - setMessages((prev) => { |
|---|
| 242 | | - const next = [...prev, msg]; |
|---|
| 243 | | - messagesMapRef.current[sessionId] = next; |
|---|
| 244 | | - debouncedSave(messagesMapRef.current); |
|---|
| 245 | | - return next; |
|---|
| 246 | | - }); |
|---|
| 247 | | - } else { |
|---|
| 248 | | - const existing = messagesMapRef.current[sessionId] ?? []; |
|---|
| 249 | | - messagesMapRef.current[sessionId] = [...existing, msg]; |
|---|
| 238 | + const currentActive = activeSessionIdRef.current; |
|---|
| 239 | + if (sessionId === currentActive) { |
|---|
| 240 | + setMessages((prev) => { |
|---|
| 241 | + const next = [...prev, msg]; |
|---|
| 242 | + messagesMapRef.current[sessionId] = next; |
|---|
| 250 | 243 | debouncedSave(messagesMapRef.current); |
|---|
| 251 | | - setUnreadCounts((u) => ({ |
|---|
| 252 | | - ...u, |
|---|
| 253 | | - [sessionId]: (u[sessionId] ?? 0) + 1, |
|---|
| 254 | | - })); |
|---|
| 255 | | - } |
|---|
| 256 | | - return currentActive; |
|---|
| 257 | | - }); |
|---|
| 244 | + return next; |
|---|
| 245 | + }); |
|---|
| 246 | + } else { |
|---|
| 247 | + const existing = messagesMapRef.current[sessionId] ?? []; |
|---|
| 248 | + messagesMapRef.current[sessionId] = [...existing, msg]; |
|---|
| 249 | + debouncedSave(messagesMapRef.current); |
|---|
| 250 | + setUnreadCounts((u) => ({ |
|---|
| 251 | + ...u, |
|---|
| 252 | + [sessionId]: (u[sessionId] ?? 0) + 1, |
|---|
| 253 | + })); |
|---|
| 254 | + } |
|---|
| 258 | 255 | }, []); |
|---|
| 259 | 256 | |
|---|
| 260 | 257 | const updateMessageStatus = useCallback( |
|---|
| .. | .. |
|---|
| 272 | 269 | const next = prev.map((m) => |
|---|
| 273 | 270 | m.id === id ? { ...m, content } : m |
|---|
| 274 | 271 | ); |
|---|
| 275 | | - setActiveSessionId((sessId) => { |
|---|
| 276 | | - if (sessId) { |
|---|
| 277 | | - messagesMapRef.current[sessId] = next; |
|---|
| 278 | | - debouncedSave(messagesMapRef.current); |
|---|
| 279 | | - } |
|---|
| 280 | | - return sessId; |
|---|
| 281 | | - }); |
|---|
| 272 | + const sessId = activeSessionIdRef.current; |
|---|
| 273 | + if (sessId) { |
|---|
| 274 | + messagesMapRef.current[sessId] = next; |
|---|
| 275 | + debouncedSave(messagesMapRef.current); |
|---|
| 276 | + } |
|---|
| 282 | 277 | return next; |
|---|
| 283 | 278 | }); |
|---|
| 284 | 279 | }, []); |
|---|
| .. | .. |
|---|
| 324 | 319 | timestamp: Date.now(), |
|---|
| 325 | 320 | status: "sent", |
|---|
| 326 | 321 | }; |
|---|
| 322 | + const isForActive = !data.sessionId || data.sessionId === activeSessionIdRef.current; |
|---|
| 327 | 323 | if (data.sessionId) { |
|---|
| 328 | 324 | addMessageToSession(data.sessionId, msg); |
|---|
| 329 | 325 | } else { |
|---|
| 330 | 326 | addMessageToActive(msg); |
|---|
| 331 | 327 | } |
|---|
| 332 | 328 | notifyIncomingMessage("PAILot", data.content ?? "Voice message"); |
|---|
| 333 | | - if (msg.audioUri && canAutoplay()) { |
|---|
| 329 | + // Only autoplay if this voice note is for the currently viewed session |
|---|
| 330 | + if (msg.audioUri && canAutoplay() && isForActive) { |
|---|
| 334 | 331 | playAudio(msg.audioUri).catch(() => {}); |
|---|
| 335 | 332 | } |
|---|
| 336 | 333 | break; |
|---|
| .. | .. |
|---|
| 487 | 484 | const deleteMessage = useCallback((id: string) => { |
|---|
| 488 | 485 | setMessages((prev) => { |
|---|
| 489 | 486 | const next = prev.filter((m) => m.id !== id); |
|---|
| 490 | | - setActiveSessionId((sessId) => { |
|---|
| 491 | | - if (sessId) { |
|---|
| 492 | | - messagesMapRef.current[sessId] = next; |
|---|
| 493 | | - debouncedSave(messagesMapRef.current); |
|---|
| 494 | | - } |
|---|
| 495 | | - return sessId; |
|---|
| 496 | | - }); |
|---|
| 487 | + const sessId = activeSessionIdRef.current; |
|---|
| 488 | + if (sessId) { |
|---|
| 489 | + messagesMapRef.current[sessId] = next; |
|---|
| 490 | + debouncedSave(messagesMapRef.current); |
|---|
| 491 | + } |
|---|
| 497 | 492 | return next; |
|---|
| 498 | 493 | }); |
|---|
| 499 | 494 | }, []); |
|---|
| 500 | 495 | |
|---|
| 501 | 496 | const clearMessages = useCallback(() => { |
|---|
| 502 | 497 | setMessages([]); |
|---|
| 503 | | - setActiveSessionId((id) => { |
|---|
| 504 | | - if (id) { |
|---|
| 505 | | - messagesMapRef.current[id] = []; |
|---|
| 506 | | - clearPersistedMessages(id); |
|---|
| 507 | | - } |
|---|
| 508 | | - return id; |
|---|
| 509 | | - }); |
|---|
| 498 | + const id = activeSessionIdRef.current; |
|---|
| 499 | + if (id) { |
|---|
| 500 | + messagesMapRef.current[id] = []; |
|---|
| 501 | + clearPersistedMessages(id); |
|---|
| 502 | + } |
|---|
| 510 | 503 | }, []); |
|---|
| 511 | 504 | |
|---|
| 512 | 505 | // --- Session management --- |
|---|
| .. | .. |
|---|
| 516 | 509 | |
|---|
| 517 | 510 | const switchSession = useCallback( |
|---|
| 518 | 511 | (sessionId: string) => { |
|---|
| 519 | | - setActiveSessionId((prev) => { |
|---|
| 520 | | - if (prev) { |
|---|
| 521 | | - messagesMapRef.current[prev] = messages; |
|---|
| 522 | | - debouncedSave(messagesMapRef.current); |
|---|
| 523 | | - } |
|---|
| 524 | | - return prev; |
|---|
| 525 | | - }); |
|---|
| 512 | + // messagesMapRef is already kept in sync by all mutators — no need to save here |
|---|
| 526 | 513 | sendCommand("switch", { sessionId }); |
|---|
| 527 | 514 | }, |
|---|
| 528 | | - [sendCommand, messages] |
|---|
| 515 | + [sendCommand] |
|---|
| 529 | 516 | ); |
|---|
| 530 | 517 | |
|---|
| 531 | 518 | const renameSession = useCallback( |
|---|
| .. | .. |
|---|
| 559 | 546 | }, [sendCommand]); |
|---|
| 560 | 547 | |
|---|
| 561 | 548 | const loadMoreMessages = useCallback(() => { |
|---|
| 562 | | - setActiveSessionId((sessId) => { |
|---|
| 563 | | - if (!sessId) return sessId; |
|---|
| 564 | | - const all = messagesMapRef.current[sessId] ?? []; |
|---|
| 565 | | - setMessages((current) => { |
|---|
| 566 | | - if (current.length >= all.length) { |
|---|
| 567 | | - setHasMoreMessages(false); |
|---|
| 568 | | - return current; |
|---|
| 569 | | - } |
|---|
| 570 | | - const nextSize = Math.min(current.length + PAGE_SIZE, all.length); |
|---|
| 571 | | - const page = all.slice(-nextSize); |
|---|
| 572 | | - setHasMoreMessages(nextSize < all.length); |
|---|
| 573 | | - return page; |
|---|
| 574 | | - }); |
|---|
| 575 | | - return sessId; |
|---|
| 549 | + const sessId = activeSessionIdRef.current; |
|---|
| 550 | + if (!sessId) return; |
|---|
| 551 | + const all = messagesMapRef.current[sessId] ?? []; |
|---|
| 552 | + setMessages((current) => { |
|---|
| 553 | + if (current.length >= all.length) { |
|---|
| 554 | + setHasMoreMessages(false); |
|---|
| 555 | + return current; |
|---|
| 556 | + } |
|---|
| 557 | + const nextSize = Math.min(current.length + PAGE_SIZE, all.length); |
|---|
| 558 | + const page = all.slice(-nextSize); |
|---|
| 559 | + setHasMoreMessages(nextSize < all.length); |
|---|
| 560 | + return page; |
|---|
| 576 | 561 | }); |
|---|
| 577 | 562 | }, []); |
|---|
| 578 | 563 | |
|---|