import { ref, computed, watch, nextTick, onMounted, defineComponent, PropType } from 'vue'; import MessageBubble from './MessageBubble.vue'; import ChatInput from './ChatInput.vue'; import type { Chat, Message, UserBasic, Ride, Coordinates } from '../../types'; interface MessageGroup { date: string; messages: Message[]; } export default defineComponent({ name: 'ChatWindow', components: { MessageBubble, ChatInput }, props: { chat: { type: Object as PropType, default: null }, messages: { type: Array as PropType, default: () => [] }, currentUserId: { type: String, required: true }, loading: { type: Boolean, default: false }, loadingMore: { type: Boolean, default: false }, sending: { type: Boolean, default: false }, hasMoreMessages: { type: Boolean, default: false }, showBackButton: { type: Boolean, default: true } }, emits: [ 'back', 'send', 'delete', 'load-more', 'user-click', 'view-profile', 'view-ride', 'share-ride', 'block' ], setup(props, { emit }) { const messagesContainer = ref(null); const replyTo = ref(null); const showScrollButton = ref(false); const newMessagesCount = ref(0); const isAtBottom = ref(true); // Computed const otherUser = computed(() => { if (!props.chat) return null; if ((props.chat as any).otherParticipant) { return (props.chat as any).otherParticipant; } if (props.chat.participants) { const other = props.chat.participants.find(p => { const id = typeof p === 'string' ? p : (p as UserBasic)._id; return id !== props.currentUserId; }); return typeof other === 'object' ? other : null; } return null; }); const userName = computed(() => { if (!otherUser.value) return 'Utente'; const user = otherUser.value as UserBasic & { profile?: { img?: string } }; if (user.name) { return `${user.name} ${user.surname?.[0] || ''}`.trim(); } return user.username || 'Utente'; }); const userInitials = computed(() => { return userName.value .split(' ') .map(n => n[0]) .join('') .toUpperCase() .slice(0, 2); }); const rideInfo = computed(() => { if (!props.chat?.rideId) return null; const ride = props.chat.rideId as Ride; if (typeof ride === 'object' && ride.departure && ride.destination) { return `${ride.departure.city} → ${ride.destination.city}`; } return null; }); const isBlocked = computed(() => { if (!props.chat) return false; return props.chat.blockedBy?.includes(props.currentUserId) || false; }); const groupedMessages = computed((): MessageGroup[] => { const groups: MessageGroup[] = []; let currentDate = ''; const sortedMessages = [...props.messages].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); sortedMessages.forEach(message => { const msgDate = formatMessageDate(message.createdAt); if (msgDate !== currentDate) { currentDate = msgDate; groups.push({ date: msgDate, messages: [message] }); } else { groups[groups.length - 1].messages.push(message); } }); return groups; }); // Methods const formatMessageDate = (date: Date | string): string => { const d = new Date(date); const now = new Date(); const diffDays = Math.floor((now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays === 0) return 'Oggi'; if (diffDays === 1) return 'Ieri'; return d.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long' }); }; const isOwnMessage = (message: Message): boolean => { const senderId = typeof message.senderId === 'string' ? message.senderId : (message.senderId as UserBasic)?._id; return senderId === props.currentUserId; }; const shouldShowAvatar = (messages: Message[], index: number): boolean => { if (index === 0) return true; const currentMsg = messages[index]; const prevMsg = messages[index - 1]; const currentSenderId = typeof currentMsg.senderId === 'string' ? currentMsg.senderId : (currentMsg.senderId as UserBasic)?._id; const prevSenderId = typeof prevMsg.senderId === 'string' ? prevMsg.senderId : (prevMsg.senderId as UserBasic)?._id; return currentSenderId !== prevSenderId; }; const getReplyMessage = (replyToId?: string | Message): Message | null => { if (!replyToId) return null; if (typeof replyToId === 'object') { return replyToId; } return props.messages.find(m => m._id === replyToId) || null; }; const setReplyTo = (message: Message) => { replyTo.value = message; }; const sendMessage = (data: { text: string; replyTo?: string }) => { emit('send', { text: data.text, replyTo: replyTo.value?._id }); replyTo.value = null; }; const deleteMessage = (message: Message) => { emit('delete', message); }; const shareLocation = (coords: Coordinates) => { emit('send', { text: '', type: 'location', metadata: { location: coords } }); }; const scrollToBottom = (smooth = true) => { if (messagesContainer.value) { messagesContainer.value.scrollTo({ top: messagesContainer.value.scrollHeight, behavior: smooth ? 'smooth' : 'auto' }); newMessagesCount.value = 0; showScrollButton.value = false; } }; const onScroll = () => { if (!messagesContainer.value) return; const { scrollTop, scrollHeight, clientHeight } = messagesContainer.value; const distanceFromBottom = scrollHeight - scrollTop - clientHeight; isAtBottom.value = distanceFromBottom < 100; showScrollButton.value = distanceFromBottom > 300; }; // Watch for new messages watch(() => props.messages.length, (newLength, oldLength) => { if (newLength > oldLength) { if (isAtBottom.value) { nextTick(() => scrollToBottom(true)); } else { newMessagesCount.value += newLength - oldLength; } } }); // Initial scroll to bottom onMounted(() => { nextTick(() => scrollToBottom(false)); }); return { messagesContainer, replyTo, showScrollButton, newMessagesCount, otherUser, userName, userInitials, rideInfo, isBlocked, groupedMessages, isOwnMessage, shouldShowAvatar, getReplyMessage, setReplyTo, sendMessage, deleteMessage, shareLocation, scrollToBottom, onScroll }; } });