From c23dfe16e95713e7058137308bdbc28419609a39 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 11:54:15 +0100
Subject: [PATCH] feat: typing indicator, message deletion, chain playback, autoplay guard

---
 contexts/ChatContext.tsx |   36 ++++++++++++++++++++++++++++++++++--
 1 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/contexts/ChatContext.tsx b/contexts/ChatContext.tsx
index 151f6f7..788bb31 100644
--- a/contexts/ChatContext.tsx
+++ b/contexts/ChatContext.tsx
@@ -8,7 +8,7 @@
 } from "react";
 import { Message, WsIncoming, WsSession } from "../types";
 import { useConnection } from "./ConnectionContext";
-import { playAudio, encodeAudioToBase64, saveBase64Audio } from "../services/audio";
+import { playAudio, encodeAudioToBase64, saveBase64Audio, canAutoplay } from "../services/audio";
 import { requestNotificationPermissions, notifyIncomingMessage } from "../services/notifications";
 
 function generateId(): string {
@@ -119,7 +119,9 @@
   sendTextMessage: (text: string) => void;
   sendVoiceMessage: (audioUri: string, durationMs?: number) => void;
   sendImageMessage: (imageBase64: string, caption: string, mimeType: string) => void;
+  deleteMessage: (id: string) => void;
   clearMessages: () => void;
+  isTyping: boolean;
   sessions: WsSession[];
   activeSessionId: string | null;
   requestSessions: () => void;
@@ -147,6 +149,8 @@
   const [messages, setMessages] = useState<Message[]>([]);
   // Unread counts for non-active sessions
   const [unreadCounts, setUnreadCounts] = useState<Record<string, number>>({});
+  // Typing indicator from server
+  const [isTyping, setIsTyping] = useState(false);
 
   const {
     status,
@@ -197,6 +201,8 @@
     if (status === "connected") {
       needsSync.current = true;
       sendCommand("sync", activeSessionId ? { activeSessionId } : undefined);
+    } else if (status === "disconnected") {
+      setIsTyping(false);
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps — only fire on status change
   }, [status, sendCommand]);
@@ -270,6 +276,7 @@
     onMessageReceived.current = async (data: WsIncoming) => {
       switch (data.type) {
         case "text": {
+          setIsTyping(false);
           const msg: Message = {
             id: generateId(),
             role: "assistant",
@@ -283,6 +290,7 @@
           break;
         }
         case "voice": {
+          setIsTyping(false);
           let audioUri: string | undefined;
           if (data.audioBase64) {
             try {
@@ -302,7 +310,7 @@
           };
           addMessageToActive(msg);
           notifyIncomingMessage("PAILot", data.content ?? "Voice message");
-          if (msg.audioUri) {
+          if (msg.audioUri && canAutoplay()) {
             playAudio(msg.audioUri).catch(() => {});
           }
           break;
@@ -356,6 +364,14 @@
         case "transcript": {
           // Voice → text reflection: replace voice bubble with transcribed text
           updateMessageContent(data.messageId, data.content);
+          break;
+        }
+        case "typing": {
+          setIsTyping(data.typing);
+          break;
+        }
+        case "status": {
+          // Connection status update — ignore for now
           break;
         }
         case "error": {
@@ -440,6 +456,20 @@
     [wsImageSend, addMessageToActive, updateMessageStatus]
   );
 
+  const deleteMessage = useCallback((id: string) => {
+    setMessages((prev) => {
+      const next = prev.filter((m) => m.id !== id);
+      setActiveSessionId((sessId) => {
+        if (sessId) {
+          messagesMapRef.current[sessId] = next;
+          debouncedSave(messagesMapRef.current);
+        }
+        return sessId;
+      });
+      return next;
+    });
+  }, []);
+
   const clearMessages = useCallback(() => {
     setMessages([]);
     setActiveSessionId((id) => {
@@ -515,7 +545,9 @@
         sendTextMessage,
         sendVoiceMessage,
         sendImageMessage,
+        deleteMessage,
         clearMessages,
+        isTyping,
         sessions,
         activeSessionId,
         requestSessions,

--
Gitblit v1.3.1