- Implementazione TRASPORTI ! Passo 1

This commit is contained in:
Surya Paolo
2025-12-22 01:19:23 +01:00
parent 83a0cf653c
commit c9fc1a83d0
123 changed files with 27433 additions and 28 deletions

View File

@@ -0,0 +1,290 @@
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<Chat | null>,
default: null
},
messages: {
type: Array as PropType<Message[]>,
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<HTMLElement | null>(null);
const replyTo = ref<Message | null>(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
};
}
});