// ChatPage.ts import { defineComponent, ref, computed, onMounted, onUnmounted, nextTick, watch, } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useQuasar } from 'quasar'; import { useChat } from '../composables/useChat'; import { useRealtimeChat } from '../composables/useRealtimeChat'; import { useAuth } from '../composables/useAuth'; import MessageBubble from '../components/chat/MessageBubble.vue'; import type { Message, RideInfo } from '../types/viaggi.types'; import { debounce } from 'quasar'; interface MessageGroup { date: string; messages: Message[]; } export default defineComponent({ name: 'ChatPage', components: { MessageBubble, }, setup() { const route = useRoute(); const router = useRouter(); const $q = useQuasar(); const { user: currentUser } = useAuth(); const { currentChat, messages, loading, loadChat, loadMessages, sendMessage: sendMessageApi, markAsRead, deleteMessage: deleteMsg, onlineUsers, typingUsers, deleteChat, fetchChats, toggleMuteChat, startPolling, // AGGIUNGI stopPolling, // AGGIUNGI } = useChat(); const { subscribeToChat, unsubscribeFromChat, sendTyping } = useRealtimeChat(); // Refs const messagesContainer = ref(); const messageInput = ref(); // State const messageText = ref(''); const sending = ref(false); const loadingMore = ref(false); const hasMoreMessages = ref(true); const showScrollButton = ref(false); const newMessagesCount = ref(0); const replyTo = ref(null); const showEmoji = ref(false); const showAttachMenu = ref(false); const showUserProfile = ref(false); const searchInChat = ref(false); const isMuted = ref(false); const lastSeen = ref(null); const commonEmojis = [ '😊', 'πŸ˜‚', '❀️', 'πŸ‘', 'πŸ™', '😍', 'πŸŽ‰', 'πŸš—', 'πŸ“', 'βœ…', '❌', '⏰', ]; // Computed const chatId = computed(() => route.params.id as string); const currentUserId = computed(() => currentUser.value?._id); const otherUser = computed((): any | undefined => { if (!currentChat.value?.participants) return undefined; return currentChat.value.participants.find((p) => p._id !== currentUserId.value); }); const rideInfo = computed((): RideInfo | undefined => { return currentChat.value?.rideInfo; }); const isOnline = computed(() => { return otherUser.value ? onlineUsers.value.includes(otherUser.value._id) : false; }); const isTyping = computed(() => { return otherUser.value ? typingUsers.value.includes(otherUser.value._id) : false; }); const groupedMessages = computed((): MessageGroup[] => { const groups: MessageGroup[] = []; let currentDate = ''; messages.value.forEach((message) => { const messageDate = formatDateHeader(new Date(message.createdAt)); if (messageDate !== currentDate) { currentDate = messageDate; groups.push({ date: messageDate, messages: [message], }); } else { groups[groups.length - 1].messages.push(message); } }); return groups; }); // Methods const goBack = () => { router.back(); }; const getInitials = (user?: User): string => { if (!user) return '?'; return `${user.name?.charAt(0) || ''}${user.surname?.charAt(0) || ''}`.toUpperCase(); }; const formatDateHeader = (date: Date): string => { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (date.toDateString() === today.toDateString()) { return 'Oggi'; } else if (date.toDateString() === yesterday.toDateString()) { return 'Ieri'; } else { return date.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long', }); } }; const formatLastSeen = (date: Date): string => { const now = new Date(); const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return 'adesso'; if (minutes < 60) return `${minutes} min fa`; if (hours < 24) return `${hours} ore fa`; if (days === 1) return 'ieri'; return `${days} giorni fa`; }; const formatRideDate = (date: string): string => { return new Date(date).toLocaleDateString('it-IT', { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', }); }; const shouldShowAvatar = (message: Message, allMessages: Message[]): boolean => { const index = allMessages.indexOf(message); if (index === allMessages.length - 1) return true; return allMessages[index + 1].senderId !== message.senderId; }; const scrollToBottom = (smooth = true) => { nextTick(() => { if (messagesContainer.value) { messagesContainer.value.scrollTo({ top: messagesContainer.value.scrollHeight, behavior: smooth ? 'smooth' : 'auto', }); newMessagesCount.value = 0; } }); }; const onScroll = () => { if (!messagesContainer.value) return; const { scrollTop, scrollHeight, clientHeight } = messagesContainer.value; const distanceFromBottom = scrollHeight - scrollTop - clientHeight; showScrollButton.value = distanceFromBottom > 200; // Load more when scrolled to top if (scrollTop < 100 && hasMoreMessages.value && !loadingMore.value) { loadMoreMessages(); } }; const loadMoreMessages = async () => { if (loadingMore.value || !hasMoreMessages.value) return; loadingMore.value = true; const oldHeight = messagesContainer.value?.scrollHeight || 0; try { const olderMessages = await loadMessages(chatId.value, { before: messages.value[0]?.createdAt, limit: 30, }); if (olderMessages.length < 30) { hasMoreMessages.value = false; } // Maintain scroll position nextTick(() => { if (messagesContainer.value) { const newHeight = messagesContainer.value.scrollHeight; messagesContainer.value.scrollTop = newHeight - oldHeight; } }); } finally { loadingMore.value = false; } }; const sendMessage = async () => { const content = messageText.value.trim(); if (!content || sending.value) return; sending.value = true; const replyToId = replyTo.value?._id; try { await sendMessageApi(chatId.value, { content, type: 'text', replyTo: replyToId, }); messageText.value = ''; replyTo.value = null; scrollToBottom(); } catch (error) { $q.notify({ type: 'negative', message: "Errore nell'invio del messaggio", }); } finally { sending.value = false; } }; const onTyping = debounce(() => { sendTyping(chatId.value); }, 500); const addEmoji = (emoji: string) => { messageText.value += emoji; showEmoji.value = false; messageInput.value?.focus(); }; const onReact = async (data: { messageId: string; emoji: string }) => { // TODO: Implementa reazione console.log('React:', data); }; const onDeleteMessage = async (messageId: string) => { $q.dialog({ title: 'Elimina messaggio', message: 'Eliminare questo messaggio?', cancel: true, }).onOk(async () => { try { await deleteMsg(chatId.value, messageId); } catch (error) { $q.notify({ type: 'negative', message: "Errore nell'eliminazione", }); } }); }; const callUser = () => { if (otherUser.value?.profile?.cell) { window.open(`tel:${otherUser.value.profile.cell}`); } else { $q.notify({ type: 'warning', message: 'Numero di telefono non disponibile', }); } }; const viewRide = () => { if (rideInfo.value?.rideId) { router.push(`/viaggi/ride/${rideInfo.value.rideId}`); } }; const viewDriverProfile = () => { if (otherUser.value) { router.push(`/viaggi/profilo/${otherUser.value._id}`); } }; const toggleMute = async () => { try { await toggleMuteChat(chatId.value, !isMuted.value); isMuted.value = !isMuted.value; $q.notify({ type: 'info', message: isMuted.value ? 'Notifiche silenziate' : 'Notifiche attivate', icon: isMuted.value ? 'notifications_off' : 'notifications', }); } catch (error) { $q.notify({ type: 'negative', message: 'Errore', }); } }; const deleteConversation = async () => { $q.dialog({ title: 'Elimina conversazione', message: 'Sei sicuro? Questa azione non Γ¨ reversibile.', cancel: true, persistent: true, }).onOk(async () => { try { await deleteChat(currentChat.value._id); $q.notify({ type: 'positive', message: 'Conversazione eliminata', }); await fetchChats(); router.push('/viaggi/chat') } catch (error) { $q.notify({ type: 'negative', message: "Errore nell'eliminazione", }); } }); }; const blockUser = () => { $q.dialog({ title: 'Blocca utente', message: `Bloccare ${otherUser.value?.name}? Non potrete piΓΉ scambiarvi messaggi.`, cancel: true, }).onOk(() => { // TODO: Implementa blocco showUserProfile.value = false; router.push('/viaggi/chat'); }); }; const attachImage = () => { showAttachMenu.value = false; // TODO: Implementa upload immagine }; const attachDocument = () => { showAttachMenu.value = false; // TODO: Implementa upload documento }; const shareLocation = () => { showAttachMenu.value = false; // TODO: Implementa condivisione posizione }; const sendRideRequest = () => { showAttachMenu.value = false; // TODO: Implementa richiesta passaggio }; const startVoiceMessage = () => { // TODO: Implementa messaggio vocale }; // Watch for new messages watch( () => messages.value.length, (newLen, oldLen) => { if (newLen > oldLen) { const lastMessage = messages.value[messages.value.length - 1]; if (lastMessage.senderId === currentUserId.value) { scrollToBottom(); } else if (showScrollButton.value) { newMessagesCount.value++; } else { scrollToBottom(); markAsRead(chatId.value); } } } ); // Lifecycle onMounted(async () => { await loadChat(chatId.value); await loadMessages(chatId.value, { limit: 50 }); scrollToBottom(false); markAsRead(chatId.value); // AVVIA POLLING startPolling(chatId.value); // subscribeToChat(chatId.value); }); // Aggiungi questa funzione nel setup() const getIsOwn = (message: Message): boolean => { const senderId = typeof message.senderId === 'object' ? (message.senderId as any)._id : message.senderId; return senderId === currentUserId.value; }; onUnmounted(() => { stopPolling(); //unsubscribeFromChat(chatId.value); }); return { // Refs messagesContainer, messageInput, // State messageText, sending, loadingMore, hasMoreMessages, showScrollButton, newMessagesCount, replyTo, showEmoji, showAttachMenu, showUserProfile, searchInChat, isMuted, lastSeen, commonEmojis, // Computed currentUserId, currentUser, otherUser, rideInfo, isOnline, isTyping, groupedMessages, // Methods goBack, getInitials, formatLastSeen, formatRideDate, shouldShowAvatar, scrollToBottom, onScroll, loadMoreMessages, sendMessage, onTyping, addEmoji, onReact, onDeleteMessage, callUser, viewRide, viewDriverProfile, toggleMute, deleteConversation, blockUser, attachImage, attachDocument, shareLocation, sendRideRequest, startVoiceMessage, getIsOwn, }; }, });