- Parte 3 : Viaggi
- Chat
This commit is contained in:
509
src/modules/viaggi/pages/ChatPage.ts
Normal file
509
src/modules/viaggi/pages/ChatPage.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
// 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user