looood / src /hooks /use-chat.ts
looda3131's picture
Clean push without any binary history
cc276cc
'use client';
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { ref, onValue, off, push, set, serverTimestamp, runTransaction, update, query, orderByChild, startAt, remove, onChildAdded, onChildChanged, get, onChildRemoved } from "firebase/database";
import { doc, getDoc, Firestore } from "firebase/firestore";
import type { Message, ChatRecipient, User, ReplyTo, Group, Language, ActionPayload, EncryptedMessage } from '@/lib/types';
import { useAuth } from '@/contexts/auth-context';
import { useFirebase } from '@/contexts/firebase-context';
import { useGroups } from '@/contexts/groups-context';
import { useSettings } from '@/contexts/settings-context';
import { useChatUtils } from '@/contexts/chat-utils-context';
import { cryptoService } from '@/lib/crypto-service';
import { storageService } from '@/lib/storage-service';
import { useAppContext } from '@/contexts/app-context';
import { learnFromUserReply } from '@/lib/suggested-replies';
import { LocalNotifications } from '@capacitor/local-notifications';
import { Capacitor } from '@capacitor/core';
const PENDING_MESSAGE_TIMEOUT = 10000; // 10 seconds
const notifyRecipient = async (recipient: ChatRecipient, sender: User, messageText: string, chatId: string, addToast: (msg: string, options?: any) => void) => {
// Do not send a notification if the recipient is the one sending the message.
if (recipient.uid === sender.uid) {
return;
}
if (recipient.isGroup || !(recipient as User).fcmToken) {
if(!(recipient as User).fcmToken) console.log(`[FCM Notify] Skipped: Recipient ${recipient.displayName} has no FCM token.`);
return;
}
console.log(`[FCM Notify] Attempting to notify recipient: ${recipient.displayName} (UID: ${recipient.uid})`);
try {
const payload = {
recipientUid: recipient.uid,
senderUid: sender.uid,
title: sender.displayName,
body: messageText,
icon: sender.photoURL,
chatId: chatId,
};
console.log("[FCM Notify] Sending payload to /api/notify:", payload);
const isNative = Capacitor.isNativePlatform();
const baseUrl = isNative ? process.env.NEXT_PUBLIC_API_BASE_URL : '';
const apiUrl = `${baseUrl}/api/notify`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const responseData = await response.json();
if (!response.ok) {
console.error("[FCM Notify] API responded with an error:", responseData);
throw new Error(responseData.details || responseData.error || 'Failed to send notification');
}
console.log("[FCM Notify] Notification request sent successfully:", responseData);
} catch (error: any) {
const errorMessage = `[FCM] ${error.message || 'An unknown error occurred.'}`;
console.error("[FCM Notify] Failed to send notification request:", error);
// Don't toast this error to the user, it's a background task failure.
// addToast(errorMessage, { variant: 'destructive' });
}
};
export const useChat = (recipient: ChatRecipient | null) => {
const { currentUser } = useAuth();
const { rtdb, db } = useFirebase();
const { messageCache, privateReplyTo, setPrivateReplyTo } = useAppContext();
const { groups, resetGroupKeys } = useGroups();
const { addToast, playSound, stopSound, privacySettings, dataMode, language } = useSettings();
const { uploadFile, transcribeAndSet } = useChatUtils();
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(true);
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const lastPlayedSoundForMessageId = useRef<string | null>(null);
const previousTypingUsersCount = useRef(0);
const lastMessageTimestamp = useRef(0);
const uploadAbortController = useRef<AbortController | null>(null);
const chatId = useMemo(() => {
if (!currentUser || !recipient) return null;
if (recipient.isGroup) return recipient.uid;
return currentUser.uid < recipient.uid
? `private_${currentUser.uid}_${recipient.uid}`
: `private_${recipient.uid}_${currentUser.uid}`;
}, [currentUser, recipient]);
// This is a guard clause because the hook can be called before currentUser is available.
if (!currentUser) {
return {
messages: [],
setMessages: () => {},
loading: true,
sendTextMessage: async () => {},
sendMediaMessage: async () => {},
sendVoiceMessage: async () => {},
editMessage: async () => {},
toggleReaction: async () => {},
typingUsers: [],
chatId: null,
};
}
const currentGroup = useMemo<Group | undefined>(() => {
if (!recipient?.isGroup) return undefined;
return groups.find(g => g.id === recipient.uid);
}, [recipient, groups]);
// Read receipts & Delivery Acknowledgement Effect
useEffect(() => {
if (!chatId || !currentUser || messages.length === 0 || !recipient) return;
const unreadMessages = messages.filter(
msg => msg.sender !== currentUser.uid && (!msg.readBy || !msg.readBy[currentUser.uid])
);
if (unreadMessages.length > 0) {
const updates: { [key: string] : any } = {};
const localUpdates: { messageId: string, updates: Partial<Message> }[] = [];
unreadMessages.forEach(msg => {
if(msg.id) {
updates[`/chats/${chatId}/messages/${msg.id}/deliveredTo/${currentUser.uid}`] = serverTimestamp();
localUpdates.push({ messageId: msg.id, updates: { deliveredTo: { ...msg.deliveredTo, [currentUser.uid]: Date.now() } } });
if (privacySettings.readReceipts) {
updates[`/chats/${chatId}/messages/${msg.id}/readBy/${currentUser.uid}`] = serverTimestamp();
localUpdates.find(u => u.messageId === msg.id)!.updates.readBy = { ...msg.readBy, [currentUser.uid]: Date.now() };
}
}
});
if (Object.keys(updates).length > 0) {
update(ref(rtdb), updates).catch(console.error);
Promise.all(localUpdates.map(lu => storageService.updateMessage(chatId, lu.messageId, lu.updates)))
}
}
}, [messages, chatId, currentUser, rtdb, privacySettings.readReceipts, recipient]);
useEffect(() => {
if (!chatId || !currentUser || !recipient) {
setLoading(false);
setMessages([]);
setTypingUsers([]);
return;
}
let isMounted = true;
setLoading(true);
const subscriptions: (() => void)[] = [];
const handleNewMessage = (snapshot: any) => {
if (!isMounted) return;
const newMessage = { id: snapshot.key, ...snapshot.val() } as Message;
setMessages(prev => {
const existingIndex = prev.findIndex(m => m.id === newMessage.id || (m.status === 'pending' && m.text === newMessage.text));
if (existingIndex !== -1) {
// Update the pending message
const updatedMessages = [...prev];
updatedMessages[existingIndex] = { ...newMessage, status: 'sent' };
storageService.saveMessages(chatId, [updatedMessages[existingIndex]]);
return updatedMessages.sort((a, b) => a.timestamp - b.timestamp);
} else if (!prev.some(m => m.id === newMessage.id)) {
// Add the new message
storageService.saveMessages(chatId, [newMessage]);
if (newMessage.sender !== currentUser.uid && lastPlayedSoundForMessageId.current !== newMessage.id) {
playSound('receive');
lastPlayedSoundForMessageId.current = newMessage.id;
}
return [...prev, newMessage].sort((a, b) => a.timestamp - b.timestamp);
}
return prev;
});
};
const handleChangedMessage = (snapshot: any) => {
if(!isMounted) return;
const updatedMessage = { id: snapshot.key, ...snapshot.val() } as Message;
storageService.updateMessage(chatId, updatedMessage.id!, updatedMessage);
setMessages(prev => {
const newMessages = prev.map(m => m.id === updatedMessage.id ? updatedMessage : m);
messageCache.current.set(chatId, newMessages);
return newMessages;
});
};
const handleRemovedMessage = (snapshot: any) => {
if (!isMounted) return;
const messageId = snapshot.key;
storageService.deleteMessage(chatId, messageId);
setMessages(prev => {
const newMessages = prev.filter(m => m.id !== messageId);
messageCache.current.set(chatId, newMessages);
return newMessages;
});
};
const typingRef = ref(rtdb, `typing/${chatId}`);
const onTyping = onValue(typingRef, (snapshot) => {
if (!isMounted) return;
const typingData = snapshot.val() || {};
const currentTypingUsers = Object.entries(typingData)
.filter(([uid, _]) => uid !== currentUser.uid)
.map(([, displayName]) => displayName as string);
if (privacySettings.showTyping) {
if (currentTypingUsers.length > 0 && previousTypingUsersCount.current === 0) {
playSound('typing');
} else if (currentTypingUsers.length === 0 && previousTypingUsersCount.current > 0) {
stopSound('typing');
}
setTypingUsers(currentTypingUsers);
previousTypingUsersCount.current = currentTypingUsers.length;
} else {
setTypingUsers([]);
stopSound('typing');
}
});
subscriptions.push(() => off(typingRef, 'value', onTyping));
const loadAndSync = async () => {
let localMessages: Message[] = [];
if (messageCache.current.has(chatId)) {
localMessages = messageCache.current.get(chatId)!;
} else {
localMessages = await storageService.getMessages(chatId);
if (isMounted) {
messageCache.current.set(chatId, localMessages);
}
}
const now = Date.now();
const validMessages = localMessages.filter(m => !(m.status === 'pending' && (now - m.timestamp) > PENDING_MESSAGE_TIMEOUT * 2));
if(isMounted) {
setMessages(validMessages);
setLoading(false);
lastMessageTimestamp.current = validMessages.length > 0 ? validMessages[validMessages.length - 1].timestamp : 0;
}
const messagesRef = ref(rtdb, `chats/${chatId}/messages`);
const newMessagesQuery = query(messagesRef, orderByChild('timestamp'), startAt(lastMessageTimestamp.current + 1));
subscriptions.push(onChildAdded(newMessagesQuery, handleNewMessage));
subscriptions.push(onChildChanged(messagesRef, handleChangedMessage));
subscriptions.push(onChildRemoved(messagesRef, handleRemovedMessage));
};
loadAndSync();
const handleLocalDataChange = async (event: Event) => {
const customEvent = event as CustomEvent;
if (isMounted && customEvent.detail.chatId === chatId) {
const updatedMessages = await storageService.getMessages(chatId);
setMessages(updatedMessages);
messageCache.current.set(chatId, updatedMessages);
}
};
window.addEventListener('localDataChange', handleLocalDataChange);
return () => {
isMounted = false;
if (uploadAbortController.current) {
uploadAbortController.current.abort();
}
subscriptions.forEach(unsub => unsub());
window.removeEventListener('localDataChange', handleLocalDataChange);
stopSound('typing');
};
}, [chatId, currentUser, playSound, stopSound, rtdb, privacySettings.showTyping, recipient, messageCache]);
const processUrlPreview = useCallback(async (text: string, messageId: string) => {
// Disabled
}, [chatId, rtdb]);
const sendTextMessage = useCallback(async (text: string, replyTo?: ReplyTo | null) => {
if (!chatId || !currentUser || !text.trim() || !recipient) return;
learnFromUserReply(text, language);
if (recipient.isGroup) {
const group = groups.find(g => g.id === recipient.uid);
if (!group || !group.members[currentUser.uid]) {
addToast("You are no longer a member of this group.", { variant: "destructive" });
return;
}
}
let textToSend = text.trim();
const shouldEncrypt = recipient.uid !== 'public_chat';
let encryptedTextPayload: EncryptedMessage | string | null = null;
let keyVersion: number | undefined;
// Check for state-based private reply first
const isPrivateReply = !!privateReplyTo;
if (shouldEncrypt && !isPrivateReply) {
try {
if (recipient.isGroup && currentGroup) {
if (!currentGroup.keyLastRotatedAt) {
throw new Error("Group key version is missing. Cannot send message.");
}
keyVersion = currentGroup.keyLastRotatedAt;
const groupKey = await cryptoService.getGroupKey(currentGroup.id, keyVersion, currentUser.uid, db);
if (!groupKey) {
await resetGroupKeys(currentGroup.id);
throw new Error("Group key was outdated. Please try sending your message again.");
}
const encryptedString = await cryptoService.encryptGroupMessage(textToSend, groupKey);
encryptedTextPayload = encryptedString;
} else if (!recipient.isGroup) {
encryptedTextPayload = await cryptoService.encryptMessage(textToSend, recipient.uid, rtdb);
}
} catch (error: any) {
addToast(error.message || "Encryption failed.", { variant: "destructive" });
return;
}
}
const newMessageRef = push(ref(rtdb, `chats/${chatId}/messages`));
const messagePayload: Partial<Message> = {
sender: currentUser.uid,
senderDisplayName: currentUser.displayName,
senderPhotoUrl: currentUser.photoURL,
timestamp: serverTimestamp() as any,
replyTo: replyTo || null,
text: textToSend, // Keep original text for sender
encryptedText: encryptedTextPayload,
deliveredTo: { [currentUser.uid]: serverTimestamp() },
keyVersion: keyVersion,
role: 'user',
content: [{text: textToSend}],
privateFor: isPrivateReply ? { uid: privateReplyTo!.uid, displayName: privateReplyTo!.displayName } : null,
};
if (isPrivateReply) {
setPrivateReplyTo(null); // Reset after use
}
const dbMessageData = JSON.parse(JSON.stringify(messagePayload));
const localId = `pending_${Date.now()}`;
const tempMessage: Message = { ...messagePayload, id: localId, timestamp: Date.now(), status: 'pending' } as Message;
setMessages(prev => [...prev, tempMessage]);
try {
await set(newMessageRef, dbMessageData);
// Now that we have the final ID, let's update the local state correctly
setMessages(prev => prev.map(m => m.id === localId ? { ...m, status: 'sent', id: newMessageRef.key! } : m));
storageService.saveMessages(chatId, [{...tempMessage, status: 'sent', id: newMessageRef.key!, timestamp: Date.now() } as Message]);
if (!shouldEncrypt) {
processUrlPreview(text.trim(), newMessageRef.key!);
}
await notifyRecipient(recipient, currentUser, textToSend, chatId, addToast);
} catch (err: any) {
console.error('Error sending text message: ', err);
addToast(`Failed to send message: ${err.message}`, { variant: 'destructive' });
setMessages(prev => prev.map(m => (m.id === localId ? { ...m, status: 'failed' } : m)));
}
}, [chatId, currentUser, recipient, addToast, rtdb, currentGroup, db, groups, processUrlPreview, language, resetGroupKeys, privateReplyTo, setPrivateReplyTo]);
const sendMediaMessage = useCallback(async (file: File, text?: string, replyTo?: ReplyTo | null, options?: { deleteAfterDelivery?: boolean }) => {
if (!chatId || !currentUser || !recipient) {
addToast("Cannot send message. Chat or user not available.", { variant: "destructive" });
return;
}
const isNative = Capacitor.isNativePlatform();
const notificationId = Date.now();
const localId = `pending_${Date.now()}`;
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
const tempMessage: Message = {
id: localId,
chatId: chatId,
sender: currentUser.uid,
senderDisplayName: currentUser.displayName,
senderPhotoUrl: currentUser.photoURL,
timestamp: Date.now(),
status: 'pending',
uploadProgress: 0,
imageKey: isImage ? URL.createObjectURL(file) : undefined,
videoKey: isVideo ? URL.createObjectURL(file) : undefined,
fileKey: !isImage && !isVideo ? 'local' : undefined,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
text: text,
replyTo: replyTo,
role: 'user',
content: [{ text: text || 'Media attached' }],
originalSize: file.size,
compressedSize: 0,
deleteAfterDelivery: options?.deleteAfterDelivery,
};
setMessages(prev => [...prev, tempMessage]);
const onProgress = async (progress: number) => {
setMessages(prev => prev.map(m => m.id === localId ? { ...m, uploadProgress: progress } : m));
if (isNative) {
try {
await LocalNotifications.schedule({
notifications: [{
id: notificationId,
title: "Uploading Media",
body: `${file.name} (${Math.round(progress)}%)`,
ongoing: true,
autoCancel: false,
channelId: 'uploads',
progressBar: { value: progress, max: 100, indeterminate: false }
}]
});
} catch (e) {
console.warn('Failed to update notification progress', e);
}
}
};
uploadAbortController.current = new AbortController();
try {
if (isNative) {
await LocalNotifications.requestPermissions();
await LocalNotifications.createChannel({ id: 'uploads', name: 'Uploads', importance: 3, visibility: 1 });
}
await onProgress(0);
const shouldEncrypt = recipient.uid !== 'public_chat';
let fileToSend = file;
let encryptionKey: { [uid: string]: string } | { group: string } | undefined = undefined;
let keyVersion: number | undefined;
if (shouldEncrypt) {
if (recipient.isGroup && currentGroup) {
if (!currentGroup.keyLastRotatedAt) throw new Error("Group key version is missing.");
keyVersion = currentGroup.keyLastRotatedAt;
}
const result = await cryptoService.encryptFile(file, recipient, currentUser.uid, rtdb, db, currentGroup);
fileToSend = result.encryptedBlob;
encryptionKey = result.encryptionKey;
}
const { fileKey } = await uploadFile(fileToSend, onProgress);
const newMessageRef = push(ref(rtdb, `chats/${chatId}/messages`));
const messagePayload: Partial<Message> = {
sender: currentUser.uid,
senderDisplayName: currentUser.displayName,
senderPhotoUrl: currentUser.photoURL,
timestamp: serverTimestamp() as any,
replyTo: replyTo || null,
text: text || null,
deliveredTo: { [currentUser.uid]: serverTimestamp() },
readBy: {},
encryptionKey: encryptionKey as any,
keyVersion: keyVersion,
role: 'user',
content: [{text: text || `Attachment: ${file.name}`}],
originalSize: tempMessage.originalSize,
compressedSize: fileToSend.size,
deleteAfterDelivery: options?.deleteAfterDelivery,
};
if (isImage) messagePayload.imageKey = fileKey;
else if (isVideo) messagePayload.videoKey = fileKey;
else {
messagePayload.fileKey = fileKey;
messagePayload.fileName = file.name;
messagePayload.fileSize = file.size;
messagePayload.fileType = file.type;
}
const dbMessageData = JSON.parse(JSON.stringify(messagePayload));
await set(newMessageRef, dbMessageData);
setMessages(prev => prev.map(m => {
if (m.id === localId) {
const finalMessage = { ...m, ...dbMessageData, id: newMessageRef.key!, status: 'sent' as 'sent', uploadProgress: undefined };
if (finalMessage.imageKey?.startsWith('blob:')) URL.revokeObjectURL(finalMessage.imageKey);
if (finalMessage.videoKey?.startsWith('blob:')) URL.revokeObjectURL(finalMessage.videoKey);
if (isImage) finalMessage.imageKey = fileKey;
if (isVideo) finalMessage.videoKey = fileKey;
storageService.saveMessages(chatId, [finalMessage as Message]);
return finalMessage as Message;
}
return m;
}));
await notifyRecipient(recipient, currentUser, text || `📄 '${file.name}'`, chatId, addToast);
} catch (error: any) {
if (error.name !== 'AbortError') {
console.error('Failed to send media message:', error);
addToast(error.message || "Failed to send media message.", { variant: "destructive" });
setMessages(prev => prev.map(m => m.id === localId ? { ...m, status: 'failed', uploadProgress: undefined } : m));
} else {
setMessages(prev => prev.filter(m => m.id !== localId));
}
} finally {
if (isNative) {
try {
await LocalNotifications.cancel({ notifications: [{ id: notificationId }] });
} catch (e) {
console.warn('Failed to cancel notification', e);
}
}
uploadAbortController.current = null;
}
}, [chatId, currentUser, recipient, uploadFile, addToast, rtdb, db, currentGroup]);
const sendVoiceMessage = useCallback(async (file: File, duration: number) => {
if (!chatId || !currentUser || !recipient) {
addToast("Cannot send message. Chat or user not available.", { variant: "destructive" });
return Promise.reject("Chat or user not available.");
}
const isNative = Capacitor.isNativePlatform();
const notificationId = Date.now();
const localId = `pending_${Date.now()}`;
const localUrl = URL.createObjectURL(file);
const tempMessage: Message = {
id: localId,
sender: currentUser.uid,
senderDisplayName: currentUser.displayName,
senderPhotoUrl: currentUser.photoURL,
timestamp: Date.now(),
status: 'pending',
uploadProgress: 0,
audioKey: localUrl,
audioDuration: duration,
role: 'user',
content: [{ text: 'Voice message' }],
};
setMessages(prev => [...prev, tempMessage]);
const onProgress = async (progress: number) => {
setMessages(prev => prev.map(m => m.id === localId ? { ...m, uploadProgress: progress } : m));
if (isNative) {
try {
await LocalNotifications.schedule({
notifications: [{
id: notificationId,
title: "Uploading Voice Message",
body: `(${Math.round(progress)}%)`,
ongoing: true,
autoCancel: false,
channelId: 'uploads',
progressBar: { value: progress, max: 100, indeterminate: false }
}]
});
} catch (e) {
console.warn('Failed to update notification progress', e);
}
}
};
uploadAbortController.current = new AbortController();
try {
if (isNative) {
await LocalNotifications.requestPermissions();
await LocalNotifications.createChannel({ id: 'uploads', name: 'Uploads', importance: 3, visibility: 1 });
}
await onProgress(0);
const shouldEncrypt = recipient.uid !== 'public_chat';
let fileToSend = file;
let encryptionKey: { [uid: string]: string } | { group: string } | undefined = undefined;
let keyVersion: number | undefined;
if (shouldEncrypt) {
if (recipient.isGroup && currentGroup) {
if (!currentGroup.keyLastRotatedAt) throw new Error("Group key version is missing.");
keyVersion = currentGroup.keyLastRotatedAt;
}
const result = await cryptoService.encryptFile(file, recipient, currentUser.uid, rtdb, db, currentGroup);
fileToSend = result.encryptedBlob;
encryptionKey = result.encryptionKey;
}
const { fileKey } = await uploadFile(fileToSend, onProgress);
const newMessageRef = push(ref(rtdb, `chats/${chatId}/messages`));
const messagePayload: Partial<Message> = {
sender: currentUser.uid,
senderDisplayName: currentUser.displayName,
senderPhotoUrl: currentUser.photoURL,
timestamp: serverTimestamp() as any,
deliveredTo: { [currentUser.uid]: serverTimestamp() },
audioKey: fileKey,
audioDuration: duration,
encryptionKey: encryptionKey as any,
keyVersion: keyVersion,
role: 'user',
content: [{text: 'Voice message'}],
};
const dbMessageData = JSON.parse(JSON.stringify(messagePayload));
await set(newMessageRef, dbMessageData);
setMessages(prev => prev.map(m => {
if (m.id === localId) {
URL.revokeObjectURL(localUrl);
const finalMessage = { ...m, ...dbMessageData, id: newMessageRef.key!, status: 'sent' as 'sent', uploadProgress: undefined, audioKey: fileKey };
storageService.saveMessages(chatId, [finalMessage as Message]);
return finalMessage as Message;
}
return m;
}));
await notifyRecipient(recipient, currentUser, '🎤 Voice Message', chatId, addToast);
transcribeAndSet(newMessageRef.key!, file, recipient);
} catch (error: any) {
if (error.name !== 'AbortError') {
console.error('Failed to send voice message:', error);
addToast(error.message || "Failed to send voice message.", { variant: "destructive" });
setMessages(prev => prev.map(m => m.id === localId ? { ...m, status: 'failed', uploadProgress: undefined } : m));
} else {
setMessages(prev => prev.filter(m => m.id !== localId));
}
throw new Error(error.message || "Failed to upload or send voice message.");
} finally {
if (isNative) {
try {
await LocalNotifications.cancel({ notifications: [{ id: notificationId }] });
} catch (e) {
console.warn('Failed to cancel notification', e);
}
}
uploadAbortController.current = null;
}
}, [chatId, currentUser, recipient, uploadFile, addToast, rtdb, db, currentGroup, transcribeAndSet]);
const toggleReaction = useCallback(async (messageId: string, emoji: string) => {
if (!chatId || !currentUser) return;
const reactionPath = `chats/${chatId}/messages/${messageId}/reactions/${emoji}/${currentUser.uid}`;
const reactionRef = ref(rtdb, reactionPath);
playSound('touch');
try {
await runTransaction(reactionRef, (currentData) => {
return currentData ? null : { displayName: currentUser.displayName, uid: currentUser.uid };
});
} catch (error) {
console.error('Error toggling reaction:', error);
addToast("Failed to update reaction.", { variant: "destructive" });
}
}, [chatId, currentUser, addToast, rtdb, playSound]);
const editMessage = useCallback(async (messageId: string, newText: string) => {
if (!chatId || !currentUser || !recipient) return;
try {
const messageRef = ref(rtdb, `chats/${chatId}/messages/${messageId}`);
const textToSend = newText.trim();
const updates: Partial<Message> = {
text: textToSend,
editedAt: serverTimestamp() as any,
content: [{text: textToSend}],
};
const shouldEncrypt = recipient.uid !== 'public_chat';
if (shouldEncrypt) {
if (recipient.isGroup && currentGroup) {
if (!currentGroup.keyLastRotatedAt) throw new Error("Group key version is missing.");
const keyVersion = currentGroup.keyLastRotatedAt;
const groupKey = await cryptoService.getGroupKey(currentGroup.id, keyVersion, currentUser.uid, db);
if (!groupKey) throw new Error("Could not retrieve group key for encryption.");
const encryptedString = await cryptoService.encryptGroupMessage(textToSend, groupKey);
updates.encryptedText = encryptedString as any;
updates.keyVersion = keyVersion;
} else if (!recipient.isGroup) {
updates.encryptedText = await cryptoService.encryptMessage(textToSend, recipient.uid, rtdb);
}
updates.text = textToSend;
} else {
updates.encryptedText = null;
}
const dbUpdates = JSON.parse(JSON.stringify(updates));
await update(messageRef, dbUpdates);
await storageService.updateMessage(chatId, messageId, {text: textToSend, editedAt: Date.now()});
const newLocalMessages = await storageService.getMessages(chatId);
setMessages(newLocalMessages);
} catch (error) {
console.error("Error editing message:", error);
addToast("Failed to edit message.", { variant: "destructive" });
}
}, [currentUser, addToast, rtdb, currentGroup, db, chatId, recipient]);
return { messages, setMessages, loading, sendTextMessage, sendMediaMessage, sendVoiceMessage, editMessage, toggleReaction, typingUsers, chatId };
}