291 lines
7.0 KiB
TypeScript
291 lines
7.0 KiB
TypeScript
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
|
|
};
|
|
}
|
|
});
|