510 lines
13 KiB
TypeScript
510 lines
13 KiB
TypeScript
// 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<HTMLElement>();
|
|
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<Message | null>(null);
|
|
const showEmoji = ref(false);
|
|
const showAttachMenu = ref(false);
|
|
const showUserProfile = ref(false);
|
|
const searchInChat = ref(false);
|
|
const isMuted = ref(false);
|
|
const lastSeen = ref<Date | null>(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,
|
|
};
|
|
},
|
|
});
|