378 lines
13 KiB
Vue
378 lines
13 KiB
Vue
<!-- ChatListPage.vue -->
|
|
<template>
|
|
<q-page class="chat-list-page">
|
|
<!-- Header -->
|
|
<div class="chat-list-page__header">
|
|
<div class="chat-list-page__title-section">
|
|
<q-icon name="forum" class="chat-list-page__icon" size="32px" color="primary" />
|
|
<div>
|
|
<h1 class="chat-list-page__title">Messaggi</h1>
|
|
<p class="chat-list-page__subtitle">Le tue conversazioni</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<q-input
|
|
v-model="searchQuery"
|
|
placeholder="Cerca conversazione..."
|
|
outlined
|
|
dense
|
|
class="chat-list-page__search q-mt-md"
|
|
>
|
|
<template v-slot:prepend>
|
|
<q-icon name="search" />
|
|
</template>
|
|
<template v-if="searchQuery" v-slot:append>
|
|
<q-icon name="close" class="cursor-pointer" @click="searchQuery = ''" />
|
|
</template>
|
|
</q-input>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<q-tabs
|
|
v-model="activeTab"
|
|
class="chat-list-page__tabs"
|
|
active-color="primary"
|
|
indicator-color="primary"
|
|
align="justify"
|
|
dense
|
|
>
|
|
<q-tab name="all" label="Tutte" icon="inbox" />
|
|
<q-tab name="unread" icon="mark_email_unread">
|
|
<template v-slot:default>
|
|
<div class="chat-list-page__tab-content">
|
|
<span>Non lette</span>
|
|
<q-badge
|
|
v-if="unreadCount > 0"
|
|
color="negative"
|
|
:label="unreadCount > 99 ? '99+' : unreadCount"
|
|
class="q-ml-xs"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</q-tab>
|
|
<q-tab name="rides" label="Viaggi" icon="directions_car" />
|
|
<q-tab name="archived" label="Archiviate" icon="archive" />
|
|
</q-tabs>
|
|
|
|
<!-- Content -->
|
|
<div class="chat-list-page__content">
|
|
<!-- Loading -->
|
|
<div v-if="loading && filteredChats.length === 0" class="chat-list-page__loading">
|
|
<q-spinner-dots size="50px" color="primary" />
|
|
<p class="text-grey q-mt-md">Caricamento conversazioni...</p>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else-if="filteredChats.length === 0" class="chat-list-page__empty">
|
|
<q-icon :name="emptyStateIcon" size="80px" color="grey-4" />
|
|
<h3 class="q-mt-md q-mb-sm">{{ emptyStateTitle }}</h3>
|
|
<p class="text-grey">{{ emptyStateMessage }}</p>
|
|
<q-btn
|
|
v-if="activeTab === 'all' && !searchQuery"
|
|
color="primary"
|
|
icon="explore"
|
|
label="Esplora viaggi"
|
|
rounded
|
|
unelevated
|
|
class="q-mt-md"
|
|
to="/trasporti"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Chat List -->
|
|
<q-list v-else class="chat-list-page__list" separator>
|
|
<q-slide-item
|
|
v-for="chat in filteredChats"
|
|
:key="chat._id"
|
|
class="chat-list-page__item"
|
|
:class="{ 'chat-list-page__item--unread': (chat.unreadCount || 0) > 0 }"
|
|
@left="(details) => { details.reset(); onArchiveChat(chat); }"
|
|
@right="(details) => { details.reset(); onDeleteChat(chat); }"
|
|
>
|
|
<!-- Left Slide Action: Archive -->
|
|
<template v-slot:left>
|
|
<div class="chat-list-page__slide-action chat-list-page__slide-action--archive">
|
|
<q-icon name="archive" size="24px" />
|
|
<span class="text-caption q-mt-xs">Archivia</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Right Slide Action: Delete -->
|
|
<template v-slot:right>
|
|
<div class="chat-list-page__slide-action chat-list-page__slide-action--delete">
|
|
<q-icon name="delete" size="24px" />
|
|
<span class="text-caption q-mt-xs">Elimina</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Chat Item -->
|
|
<q-item clickable @click="openChat(chat)">
|
|
<!-- Avatar -->
|
|
<q-item-section avatar>
|
|
<div class="chat-list-page__avatar-wrapper">
|
|
<q-avatar size="56px">
|
|
<img
|
|
v-if="getOtherParticipant(chat)?.profile?.img"
|
|
:src="getOtherParticipant(chat).profile.img"
|
|
:alt="getOtherParticipant(chat)?.name"
|
|
/>
|
|
<div v-else class="chat-list-page__avatar-placeholder">
|
|
{{ getInitials(getOtherParticipant(chat)) }}
|
|
</div>
|
|
</q-avatar>
|
|
|
|
<!-- Online indicator -->
|
|
<div
|
|
v-if="isOnline(getOtherParticipant(chat)?._id)"
|
|
class="chat-list-page__online-dot"
|
|
/>
|
|
|
|
<!-- Ride type badge -->
|
|
<q-badge
|
|
v-if="chat.rideId && typeof chat.rideId === 'object' && chat.rideId.type"
|
|
:color="chat.rideId.type === 'offer' ? 'positive' : 'warning'"
|
|
floating
|
|
rounded
|
|
class="chat-list-page__ride-badge"
|
|
>
|
|
<q-icon
|
|
:name="chat.rideId.type === 'offer' ? 'directions_car' : 'hail'"
|
|
size="12px"
|
|
/>
|
|
</q-badge>
|
|
</div>
|
|
</q-item-section>
|
|
|
|
<!-- Content -->
|
|
<q-item-section>
|
|
<!-- Name -->
|
|
<q-item-label class="chat-list-page__name">
|
|
<span class="text-weight-medium">
|
|
{{ getOtherParticipant(chat)?.name || 'Utente' }}
|
|
{{ getOtherParticipant(chat)?.surname || '' }}
|
|
</span>
|
|
<q-icon
|
|
v-if="chat.pinned"
|
|
name="push_pin"
|
|
size="16px"
|
|
color="grey"
|
|
class="q-ml-xs"
|
|
/>
|
|
</q-item-label>
|
|
|
|
<!-- Ride info -->
|
|
<q-item-label
|
|
v-if="chat.rideId && typeof chat.rideId === 'object'"
|
|
caption
|
|
class="chat-list-page__ride-info"
|
|
>
|
|
<q-icon name="place" size="14px" class="q-mr-xs" />
|
|
<template v-if="chat.rideId.departure && chat.rideId.destination">
|
|
{{ typeof chat.rideId.departure === 'string'
|
|
? chat.rideId.departure
|
|
: chat.rideId.departure.city }}
|
|
→
|
|
{{ typeof chat.rideId.destination === 'string'
|
|
? chat.rideId.destination
|
|
: chat.rideId.destination.city }}
|
|
</template>
|
|
</q-item-label>
|
|
|
|
<!-- Last message -->
|
|
<q-item-label
|
|
caption
|
|
lines="1"
|
|
class="chat-list-page__last-message"
|
|
:class="{
|
|
'chat-list-page__last-message--unread': (chat.unreadCount || 0) > 0,
|
|
'text-weight-medium': (chat.unreadCount || 0) > 0
|
|
}"
|
|
>
|
|
<q-icon
|
|
v-if="chat.lastMessage && chat.lastMessage.senderId === currentUserId"
|
|
:name="getMessageStatusIcon(chat.lastMessage)"
|
|
size="14px"
|
|
:color="chat.lastMessage.readBy && chat.lastMessage.readBy.length > 1 ? 'primary' : 'grey'"
|
|
class="q-mr-xs"
|
|
/>
|
|
{{ getMessagePreview(chat.lastMessage) }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
|
|
<!-- Right side: Time & Badge -->
|
|
<q-item-section side top>
|
|
<div class="chat-list-page__meta">
|
|
<!-- Time -->
|
|
<q-item-label caption class="chat-list-page__time">
|
|
{{ formatTime(chat.lastMessage?.timestamp || chat.updatedAt) }}
|
|
</q-item-label>
|
|
|
|
<!-- Unread badge -->
|
|
<q-badge
|
|
v-if="(chat.unreadCount || 0) > 0"
|
|
color="primary"
|
|
:label="chat.unreadCount > 99 ? '99+' : chat.unreadCount"
|
|
rounded
|
|
class="chat-list-page__unread-badge q-mt-xs"
|
|
/>
|
|
|
|
<!-- Muted icon -->
|
|
<q-icon
|
|
v-else-if="chat.mutedBy && chat.mutedBy.includes(currentUserId)"
|
|
name="notifications_off"
|
|
size="18px"
|
|
color="grey"
|
|
class="q-mt-xs"
|
|
/>
|
|
</div>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-slide-item>
|
|
</q-list>
|
|
|
|
<!-- Load More -->
|
|
<div v-if="hasMore && filteredChats.length > 0" class="chat-list-page__load-more">
|
|
<q-btn
|
|
flat
|
|
color="primary"
|
|
label="Carica altre conversazioni"
|
|
icon="expand_more"
|
|
:loading="loadingMore"
|
|
@click="loadMore"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FAB for new message -->
|
|
<q-page-sticky position="bottom-right" :offset="[18, 18]">
|
|
<q-fab
|
|
icon="edit"
|
|
direction="up"
|
|
color="primary"
|
|
:disable="loading"
|
|
>
|
|
<q-fab-action
|
|
color="secondary"
|
|
icon="person_search"
|
|
label="Cerca utente"
|
|
external-label
|
|
label-position="left"
|
|
@click="showUserSearch = true"
|
|
/>
|
|
<q-fab-action
|
|
color="accent"
|
|
icon="group"
|
|
label="Nuovo gruppo"
|
|
external-label
|
|
label-position="left"
|
|
@click="showGroupCreate = true"
|
|
/>
|
|
</q-fab>
|
|
</q-page-sticky>
|
|
|
|
<!-- User Search Dialog -->
|
|
<q-dialog v-model="showUserSearch" position="top">
|
|
<q-card class="chat-list-page__search-dialog" style="width: 100%; max-width: 500px;">
|
|
<q-card-section class="row items-center q-pb-none">
|
|
<div class="text-h6">Nuova conversazione</div>
|
|
<q-space />
|
|
<q-btn icon="close" flat round dense v-close-popup />
|
|
</q-card-section>
|
|
|
|
<q-card-section>
|
|
<q-input
|
|
v-model="userSearchQuery"
|
|
placeholder="Cerca per nome o username..."
|
|
outlined
|
|
dense
|
|
autofocus
|
|
@update:model-value="searchUsers"
|
|
>
|
|
<template v-slot:prepend>
|
|
<q-icon name="search" />
|
|
</template>
|
|
<template v-if="userSearchQuery" v-slot:append>
|
|
<q-icon
|
|
name="close"
|
|
class="cursor-pointer"
|
|
@click="userSearchQuery = ''; searchedUsers = []"
|
|
/>
|
|
</template>
|
|
</q-input>
|
|
|
|
<!-- Searching spinner -->
|
|
<div v-if="searchingUsers" class="text-center q-pa-md">
|
|
<q-spinner color="primary" size="40px" />
|
|
</div>
|
|
|
|
<!-- Search results -->
|
|
<q-list v-else-if="searchedUsers.length > 0" class="q-mt-md">
|
|
<q-item
|
|
v-for="user in searchedUsers"
|
|
:key="user._id"
|
|
clickable
|
|
v-ripple
|
|
@click="startChatWith(user)"
|
|
>
|
|
<q-item-section avatar>
|
|
<q-avatar>
|
|
<img v-if="user.profile?.img" :src="user.profile.img" />
|
|
<div v-else class="chat-list-page__avatar-placeholder">
|
|
{{ getInitials(user) }}
|
|
</div>
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label>{{ user.name }} {{ user.surname }}</q-item-label>
|
|
<q-item-label caption>@{{ user.username }}</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-icon name="chevron_right" color="grey" />
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
|
|
<!-- No results -->
|
|
<div
|
|
v-else-if="userSearchQuery && userSearchQuery.length >= 2 && !searchingUsers"
|
|
class="text-center q-pa-md text-grey"
|
|
>
|
|
<q-icon name="person_off" size="48px" color="grey-4" />
|
|
<p class="q-mt-md">Nessun utente trovato</p>
|
|
</div>
|
|
|
|
<!-- Hint -->
|
|
<div
|
|
v-else-if="!userSearchQuery || userSearchQuery.length < 2"
|
|
class="text-center q-pa-md text-grey"
|
|
>
|
|
<q-icon name="search" size="48px" color="grey-4" />
|
|
<p class="q-mt-md">Digita almeno 2 caratteri per cercare</p>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<!-- Group Create Dialog (placeholder) -->
|
|
<q-dialog v-model="showGroupCreate">
|
|
<q-card style="min-width: 350px;">
|
|
<q-card-section>
|
|
<div class="text-h6">Crea gruppo</div>
|
|
</q-card-section>
|
|
|
|
<q-card-section class="q-pt-none">
|
|
<p class="text-grey">Funzionalità in arrivo...</p>
|
|
</q-card-section>
|
|
|
|
<q-card-actions align="right">
|
|
<q-btn flat label="Chiudi" color="primary" v-close-popup />
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script lang="ts" src="./ChatListPage.ts" />
|
|
<style lang="scss" src="./ChatListPage.scss" />
|