- Implementazione TRASPORTI ! Passo 1
This commit is contained in:
290
src/modules/trasporti/components/chat/ChatWindow.ts
Normal file
290
src/modules/trasporti/components/chat/ChatWindow.ts
Normal 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
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user