2798 lines
83 KiB
Vue
2798 lines
83 KiB
Vue
<template>
|
||
<div class="chat-home">
|
||
<!-- Header -->
|
||
<v-app-bar color="toolbar" dark elevation="0">
|
||
<v-app-bar-title class="d-flex align-center">
|
||
<v-icon start class="mr-2">mdi-chat</v-icon>
|
||
گفتوگو
|
||
</v-app-bar-title>
|
||
<v-spacer></v-spacer>
|
||
|
||
<!-- Actions -->
|
||
<div class="d-flex align-center">
|
||
<v-btn icon @click="showCreateChannel = true">
|
||
<v-icon>mdi-plus</v-icon>
|
||
</v-btn>
|
||
<v-btn icon @click="showChannelDrawer = !showChannelDrawer">
|
||
<v-icon>mdi-menu</v-icon>
|
||
</v-btn>
|
||
</div>
|
||
</v-app-bar>
|
||
|
||
<!-- Main Content -->
|
||
<div class="chat-container">
|
||
<!-- Channel Drawer -->
|
||
<v-navigation-drawer
|
||
v-model="showChannelDrawer"
|
||
:temporary="true"
|
||
width="320"
|
||
class="channel-drawer"
|
||
>
|
||
<v-card flat class="h-100">
|
||
<!-- Drawer Header -->
|
||
<div class="drawer-header pa-4">
|
||
<h3 class="text-h6 mb-3">گفتوگوها</h3>
|
||
<v-text-field
|
||
v-model="channelSearch"
|
||
placeholder="جستجو در کانالها..."
|
||
prepend-inner-icon="mdi-magnify"
|
||
variant="outlined"
|
||
density="compact"
|
||
hide-details
|
||
@input="searchChannels"
|
||
></v-text-field>
|
||
</div>
|
||
|
||
<!-- Channel Lists -->
|
||
<v-card-text class="pa-0">
|
||
<!-- My Channels -->
|
||
<div class="channel-section">
|
||
<div class="section-header pa-4">
|
||
<v-icon start class="mr-2">mdi-account-group</v-icon>
|
||
<span class="text-subtitle-2">کانالهای من</span>
|
||
<v-chip size="small" color="primary" variant="elevated" class="ml-auto count-chip">
|
||
{{ userChannels.length }}
|
||
</v-chip>
|
||
</div>
|
||
|
||
<v-list class="pa-0">
|
||
<v-list-item
|
||
v-for="channel in filteredChannels"
|
||
:key="channel.id"
|
||
@click="selectChannel(channel)"
|
||
:active="currentChannel?.id === channel.id"
|
||
class="channel-item"
|
||
rounded="lg"
|
||
:class="{ 'channel-active': currentChannel?.id === channel.id }"
|
||
>
|
||
<template v-slot:prepend>
|
||
<v-avatar size="40" :color="channel.isPublic ? 'green' : 'orange'" class="mr-3">
|
||
<v-icon :color="channel.isPublic ? 'white' : 'white'">
|
||
{{ channel.isPublic ? 'mdi-earth' : 'mdi-lock' }}
|
||
</v-icon>
|
||
</v-avatar>
|
||
</template>
|
||
|
||
<v-list-item-title class="font-weight-medium">
|
||
{{ channel.name }}
|
||
</v-list-item-title>
|
||
|
||
<v-list-item-subtitle class="text-caption">
|
||
{{ channel.messageCount }} پیام • {{ channel.memberCount || 0 }} عضو
|
||
</v-list-item-subtitle>
|
||
|
||
<template v-slot:append>
|
||
<v-chip
|
||
v-if="channel.unreadCount && channel.unreadCount > 0"
|
||
size="small"
|
||
color="error"
|
||
variant="elevated"
|
||
class="ml-2 unread-chip"
|
||
>
|
||
{{ channel.unreadCount }}
|
||
</v-chip>
|
||
</template>
|
||
</v-list-item>
|
||
</v-list>
|
||
</div>
|
||
|
||
<!-- Public Channels -->
|
||
<v-divider class="my-4"></v-divider>
|
||
|
||
<div class="channel-section">
|
||
<div class="section-header pa-4">
|
||
<v-icon start class="mr-2">mdi-earth</v-icon>
|
||
<span class="text-subtitle-2">{{ channelSearch ? 'کانالهای عمومی' : 'کانالهای محبوب' }}</span>
|
||
<v-chip size="small" color="blue" variant="elevated" class="ml-auto count-chip">
|
||
{{ publicChannels.length }}
|
||
</v-chip>
|
||
</div>
|
||
|
||
<v-list class="pa-0">
|
||
<v-list-item
|
||
v-for="channel in publicChannels"
|
||
:key="channel.id"
|
||
class="channel-item"
|
||
rounded="lg"
|
||
@click="selectChannel(channel)"
|
||
:active="currentChannel?.id === channel.id"
|
||
:class="{ 'channel-active': currentChannel?.id === channel.id }"
|
||
>
|
||
<template v-slot:prepend>
|
||
<v-avatar size="40" color="blue" class="mr-3">
|
||
<v-icon color="white">mdi-earth</v-icon>
|
||
</v-avatar>
|
||
</template>
|
||
|
||
<v-list-item-title class="font-weight-medium">
|
||
{{ channel.name }}
|
||
</v-list-item-title>
|
||
|
||
<v-list-item-subtitle class="text-caption">
|
||
{{ channel.messageCount }} پیام • {{ channel.memberCount || 0 }} عضو
|
||
</v-list-item-subtitle>
|
||
|
||
<template v-slot:append>
|
||
<v-btn
|
||
v-if="!isChannelJoined(channel.id)"
|
||
size="small"
|
||
color="primary"
|
||
variant="elevated"
|
||
class="join-btn"
|
||
@click.stop="joinChannel(channel)"
|
||
>
|
||
پیوستن
|
||
</v-btn>
|
||
<v-chip
|
||
v-else
|
||
size="small"
|
||
color="success"
|
||
variant="elevated"
|
||
class="member-chip"
|
||
>
|
||
عضو
|
||
</v-chip>
|
||
</template>
|
||
</v-list-item>
|
||
</v-list>
|
||
</div>
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-navigation-drawer>
|
||
|
||
<!-- Chat Area -->
|
||
<div class="chat-area">
|
||
<!-- Empty State -->
|
||
<div v-if="!currentChannel" class="empty-state">
|
||
<div class="empty-content">
|
||
<v-icon size="120" color="grey-lighten-1">mdi-chat-outline</v-icon>
|
||
<h1 class="text-h4 text-grey-lighten-1 mt-6 mb-4">خوش آمدید!</h1>
|
||
<p class="text-body-1 text-grey-lighten-1 mb-8 text-center">
|
||
برای شروع گفتوگو، یک کانال انتخاب کنید یا کانال جدیدی ایجاد کنید
|
||
</p>
|
||
<div class="action-buttons">
|
||
<v-btn
|
||
color="primary"
|
||
size="large"
|
||
class="mr-4"
|
||
@click="showChannelDrawer = true"
|
||
>
|
||
<v-icon start>mdi-format-list-bulleted</v-icon>
|
||
انتخاب کانال
|
||
</v-btn>
|
||
<v-btn
|
||
color="secondary"
|
||
size="large"
|
||
variant="outlined"
|
||
@click="showCreateChannel = true"
|
||
>
|
||
<v-icon start>mdi-plus</v-icon>
|
||
ایجاد کانال
|
||
</v-btn>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Content -->
|
||
<div v-else class="chat-content">
|
||
<!-- Channel Header -->
|
||
<div class="channel-header">
|
||
<div class="d-flex align-center">
|
||
<v-avatar size="48" :color="currentChannel.isPublic ? 'green' : 'orange'" class="mr-4">
|
||
<v-icon :color="currentChannel.isPublic ? 'white' : 'white'" size="24">
|
||
{{ currentChannel.isPublic ? 'mdi-earth' : 'mdi-lock' }}
|
||
</v-icon>
|
||
</v-avatar>
|
||
<div>
|
||
<h2 class="text-h5 mb-1">{{ currentChannel.name }}</h2>
|
||
<div class="d-flex align-center">
|
||
<v-icon size="16" class="mr-1">mdi-account-group</v-icon>
|
||
<span class="text-caption">{{ currentChannel.memberCount || 0 }} عضو</span>
|
||
<v-divider vertical class="mx-3"></v-divider>
|
||
<span class="text-caption">{{ currentChannel.messageCount }} پیام</span>
|
||
<v-divider vertical class="mx-3"></v-divider>
|
||
<v-chip
|
||
v-if="currentChannel.isPublic"
|
||
size="small"
|
||
:color="canSendMessage ? 'success' : 'warning'"
|
||
variant="tonal"
|
||
>
|
||
{{ canSendMessage ? 'عضو' : 'مشاهدهکننده' }}
|
||
</v-chip>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="channel-actions">
|
||
<v-menu>
|
||
<template v-slot:activator="{ props }">
|
||
<v-btn icon size="small" v-bind="props">
|
||
<v-icon>mdi-dots-vertical</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
<v-list>
|
||
<v-list-item @click="showChannelInfo = !showChannelInfo">
|
||
<v-list-item-title>اطلاعات کانال</v-list-item-title>
|
||
</v-list-item>
|
||
<v-list-item
|
||
v-if="canSendMessage"
|
||
@click="leaveChannel"
|
||
>
|
||
<v-list-item-title>ترک کانال</v-list-item-title>
|
||
</v-list-item>
|
||
<v-list-item
|
||
v-else-if="currentChannel.isPublic"
|
||
@click="joinChannel(currentChannel)"
|
||
>
|
||
<v-list-item-title>پیوستن به کانال</v-list-item-title>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-menu>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Channel Info -->
|
||
<v-expand-transition>
|
||
<div v-if="showChannelInfo" class="channel-info">
|
||
<v-card variant="outlined" class="ma-4">
|
||
<v-card-text>
|
||
<div class="d-flex align-center mb-3">
|
||
<v-icon start class="mr-2">mdi-information</v-icon>
|
||
<h3 class="text-h6">اطلاعات کانال</h3>
|
||
</div>
|
||
<p class="text-body-2 mb-4">{{ getChannelInfo?.description || 'توضیحی برای این کانال وجود ندارد.' }}</p>
|
||
<div class="channel-stats">
|
||
<div class="stat-item">
|
||
<v-icon size="20" class="mr-2">mdi-account-group</v-icon>
|
||
<span>{{ getChannelInfo?.memberCount || 0 }} عضو</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<v-icon size="20" class="mr-2">mdi-message</v-icon>
|
||
<span>{{ getChannelInfo?.messageCount || 0 }} پیام</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<v-icon size="20" class="mr-2">mdi-calendar</v-icon>
|
||
<span>{{ getChannelInfo?.createdAt ? formatDate(getChannelInfo.createdAt) : '-' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="channel-actions mt-4">
|
||
<v-btn
|
||
v-if="canSendMessage"
|
||
color="primary"
|
||
variant="outlined"
|
||
size="small"
|
||
@click="showMemberDialog = true"
|
||
>
|
||
<v-icon start>mdi-account-group</v-icon>
|
||
مشاهده اعضا
|
||
</v-btn>
|
||
<v-btn
|
||
v-if="currentChannel.isAdmin"
|
||
color="secondary"
|
||
variant="outlined"
|
||
size="small"
|
||
class="mr-2"
|
||
@click="showAddMemberDialog = true"
|
||
>
|
||
<v-icon start>mdi-account-plus</v-icon>
|
||
اضافه کردن عضو
|
||
</v-btn>
|
||
<v-btn
|
||
v-if="currentChannel.isPublic && !canSendMessage"
|
||
color="primary"
|
||
size="small"
|
||
@click="joinChannel(currentChannel)"
|
||
>
|
||
<v-icon start>mdi-account-plus</v-icon>
|
||
پیوستن به کانال
|
||
</v-btn>
|
||
</div>
|
||
</v-card-text>
|
||
</v-card>
|
||
</div>
|
||
</v-expand-transition>
|
||
|
||
<!-- Messages -->
|
||
<div class="messages-container" ref="messagesContainer" @scroll="handleScroll">
|
||
<!-- Load more indicator -->
|
||
<div v-if="pagination.loadingMore" class="load-more-indicator">
|
||
<v-progress-circular indeterminate color="primary" size="30"></v-progress-circular>
|
||
<span class="text-caption ml-2">در حال بارگذاری پیامهای بیشتر...</span>
|
||
</div>
|
||
|
||
<div v-if="loading" class="loading-state">
|
||
<v-progress-circular indeterminate color="primary" size="50"></v-progress-circular>
|
||
<p class="text-body-1 mt-4">در حال بارگذاری پیامها...</p>
|
||
</div>
|
||
|
||
<div v-else-if="messages.length === 0" class="empty-messages">
|
||
<v-icon size="80" color="grey-lighten-1">mdi-chat-outline</v-icon>
|
||
<h3 class="text-h6 text-grey-lighten-1 mt-4 mb-2">هنوز پیامی ارسال نشده است</h3>
|
||
<p class="text-grey-lighten-1">اولین پیام را ارسال کنید!</p>
|
||
</div>
|
||
|
||
<div v-else class="messages-list">
|
||
<div
|
||
v-for="message in messages"
|
||
:key="message.id"
|
||
class="message-item"
|
||
:class="{ 'message-own': message.sender.id === currentUser?.id }"
|
||
>
|
||
<div class="message-avatar">
|
||
<v-avatar size="40" color="primary">
|
||
<span class="text-white">{{ message.sender.fullName.charAt(0) }}</span>
|
||
</v-avatar>
|
||
</div>
|
||
|
||
<div class="message-content">
|
||
<div class="message-header">
|
||
<span class="message-sender">{{ message.sender.fullName }}</span>
|
||
<span class="message-time">{{ formatTime(message.sentAt) }}</span>
|
||
</div>
|
||
|
||
<!-- Quoted Message -->
|
||
<div v-if="message.quotedMessage" class="quoted-message">
|
||
<div class="quoted-content">
|
||
<span class="quoted-sender">{{ message.quotedMessage.sender }}</span>
|
||
<span class="quoted-text">{{ message.quotedMessage.content }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="message-bubble">
|
||
<div v-if="message.messageType === 'emoji'" class="emoji-message">
|
||
{{ message.content }}
|
||
</div>
|
||
<div v-else class="text-message">
|
||
{{ message.content }}
|
||
</div>
|
||
|
||
<!-- Reactions -->
|
||
<div v-if="message.reactions && Object.keys(message.reactions).length > 0" class="message-reactions">
|
||
<v-chip
|
||
v-for="(users, emoji) in message.reactions"
|
||
:key="emoji"
|
||
size="small"
|
||
variant="outlined"
|
||
class="mr-1 reaction-chip"
|
||
:class="{
|
||
'like-chip': emoji === '❤️',
|
||
'dislike-chip': emoji === '👎'
|
||
}"
|
||
>
|
||
<v-icon size="14" class="mr-1">
|
||
{{ emoji === '❤️' ? 'mdi-heart' : emoji === '👎' ? 'mdi-thumb-down' : emoji }}
|
||
</v-icon>
|
||
{{ users.length }}
|
||
</v-chip>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Message Actions -->
|
||
<div class="message-actions">
|
||
<v-tooltip text="نقل قول از این پیام" location="top">
|
||
<template v-slot:activator="{ props }">
|
||
<v-btn
|
||
icon
|
||
size="x-small"
|
||
variant="text"
|
||
@click="quoteMessage(message)"
|
||
class="action-btn"
|
||
v-bind="props"
|
||
>
|
||
<v-icon size="16">mdi-reply</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-tooltip>
|
||
<v-tooltip text="پاسخ به این پیام" location="top">
|
||
<template v-slot:activator="{ props }">
|
||
<v-btn
|
||
icon
|
||
size="x-small"
|
||
variant="text"
|
||
@click="replyToMessage(message)"
|
||
class="action-btn"
|
||
v-bind="props"
|
||
>
|
||
<v-icon size="16">mdi-reply-all</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-tooltip>
|
||
<v-tooltip :text="isLiked(message) ? 'حذف لایک' : 'لایک کردن'" location="top">
|
||
<template v-slot:activator="{ props }">
|
||
<v-btn
|
||
icon
|
||
size="x-small"
|
||
variant="text"
|
||
@click="toggleLike(message)"
|
||
class="action-btn"
|
||
:class="{ 'liked': isLiked(message) }"
|
||
:loading="reactingMessages.has(`like-${message.id}`)"
|
||
:disabled="reactingMessages.has(`like-${message.id}`) || reactingMessages.has(`dislike-${message.id}`)"
|
||
v-bind="props"
|
||
>
|
||
<v-icon size="16" :color="isLiked(message) ? 'red' : undefined">
|
||
{{ isLiked(message) ? 'mdi-heart' : 'mdi-heart-outline' }}
|
||
</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-tooltip>
|
||
<v-tooltip :text="isDisliked(message) ? 'حذف دیسلایک' : 'دیسلایک کردن'" location="top">
|
||
<template v-slot:activator="{ props }">
|
||
<v-btn
|
||
icon
|
||
size="x-small"
|
||
variant="text"
|
||
@click="toggleDislike(message)"
|
||
class="action-btn"
|
||
:class="{ 'disliked': isDisliked(message) }"
|
||
:loading="reactingMessages.has(`dislike-${message.id}`)"
|
||
:disabled="reactingMessages.has(`like-${message.id}`) || reactingMessages.has(`dislike-${message.id}`)"
|
||
v-bind="props"
|
||
>
|
||
<v-icon size="16" :color="isDisliked(message) ? 'orange' : undefined">
|
||
{{ isDisliked(message) ? 'mdi-thumb-down' : 'mdi-thumb-down-outline' }}
|
||
</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-tooltip>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Message Input -->
|
||
<div class="message-input-container">
|
||
<!-- Show join button if user is not a member -->
|
||
<div v-if="currentChannel && !canSendMessage" class="join-prompt">
|
||
<v-alert
|
||
type="info"
|
||
variant="tonal"
|
||
class="mb-4"
|
||
>
|
||
<div class="d-flex align-center">
|
||
<v-icon start class="mr-2">mdi-information</v-icon>
|
||
<span>برای ارسال پیام باید عضو این کانال باشید</span>
|
||
</div>
|
||
</v-alert>
|
||
<v-btn
|
||
color="primary"
|
||
size="large"
|
||
block
|
||
@click="joinChannel(currentChannel)"
|
||
>
|
||
<v-icon start>mdi-account-plus</v-icon>
|
||
پیوستن به کانال
|
||
</v-btn>
|
||
</div>
|
||
|
||
<!-- Show message input if user is a member -->
|
||
<div v-else-if="currentChannel && canSendMessage" class="input-wrapper">
|
||
<!-- Quoted Message Preview -->
|
||
<div v-if="quotedMessage" class="quoted-message-preview">
|
||
<div class="quoted-preview-content">
|
||
<div class="quoted-preview-header">
|
||
<v-icon size="16" class="mr-2">mdi-reply</v-icon>
|
||
<span class="text-caption">در پاسخ به {{ quotedMessage.sender.fullName }}</span>
|
||
<v-spacer></v-spacer>
|
||
<v-tooltip text="لغو نقل قول" location="top">
|
||
<template v-slot:activator="{ props }">
|
||
<v-btn
|
||
icon
|
||
size="x-small"
|
||
variant="text"
|
||
@click="cancelQuote"
|
||
v-bind="props"
|
||
>
|
||
<v-icon size="16">mdi-close</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-tooltip>
|
||
</div>
|
||
<div class="quoted-preview-text">
|
||
{{ quotedMessage.content }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<v-text-field
|
||
v-model="messageText"
|
||
:placeholder="quotedMessage ? 'پاسخ خود را بنویسید...' : 'پیام خود را بنویسید...'"
|
||
variant="outlined"
|
||
density="compact"
|
||
hide-details
|
||
@keyup.enter="sendMessage"
|
||
:disabled="loading"
|
||
class="message-input"
|
||
>
|
||
<template v-slot:prepend>
|
||
<v-btn icon size="small" @click="showEmojiPicker = !showEmojiPicker">
|
||
<v-icon>mdi-emoticon</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
|
||
<template v-slot:append>
|
||
<v-btn
|
||
icon
|
||
color="primary"
|
||
@click="sendMessage"
|
||
:disabled="!messageText.trim() || loading"
|
||
>
|
||
<v-icon>mdi-send</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-text-field>
|
||
|
||
<!-- Emoji Picker -->
|
||
<v-expand-transition>
|
||
<div v-if="showEmojiPicker" class="emoji-picker">
|
||
<div class="emoji-grid">
|
||
<v-btn
|
||
v-for="emoji in commonEmojis"
|
||
:key="emoji"
|
||
variant="text"
|
||
size="small"
|
||
@click="insertEmoji(emoji)"
|
||
>
|
||
{{ emoji }}
|
||
</v-btn>
|
||
</div>
|
||
</div>
|
||
</v-expand-transition>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Channel Dialog -->
|
||
<v-dialog v-model="showCreateChannel" max-width="600px" persistent>
|
||
<v-card class="create-channel-dialog">
|
||
<v-card-title class="dialog-header">
|
||
<div class="d-flex align-center">
|
||
<v-avatar size="48" color="primary" class="mr-4">
|
||
<v-icon size="24" color="white">mdi-plus-circle</v-icon>
|
||
</v-avatar>
|
||
<div>
|
||
<h2 class="text-h5 mb-1">ایجاد کانال جدید</h2>
|
||
<p class="text-body-2 text-grey-darken-1">کانال جدیدی برای گفتوگو ایجاد کنید</p>
|
||
</div>
|
||
</div>
|
||
</v-card-title>
|
||
|
||
<v-card-text class="dialog-content">
|
||
<v-form ref="createChannelForm" class="mt-4">
|
||
<v-text-field
|
||
v-model="newChannel.name"
|
||
label="نام کانال"
|
||
placeholder="مثال: گروه عمومی"
|
||
required
|
||
:rules="[v => !!v || 'نام کانال الزامی است']"
|
||
variant="outlined"
|
||
density="comfortable"
|
||
prepend-inner-icon="mdi-format-title"
|
||
class="mb-4"
|
||
></v-text-field>
|
||
|
||
<v-textarea
|
||
v-model="newChannel.description"
|
||
label="توضیحات کانال"
|
||
placeholder="توضیح کوتاهی درباره هدف و موضوع کانال بنویسید..."
|
||
rows="4"
|
||
variant="outlined"
|
||
density="comfortable"
|
||
prepend-inner-icon="mdi-text"
|
||
class="mb-4"
|
||
></v-textarea>
|
||
|
||
<v-card variant="outlined" class="mb-4">
|
||
<v-card-text class="pa-4">
|
||
<div class="d-flex align-center mb-3">
|
||
<v-switch
|
||
v-model="newChannel.isPublic"
|
||
color="primary"
|
||
inset
|
||
class="mr-3"
|
||
></v-switch>
|
||
<div>
|
||
<h4 class="text-subtitle-1 font-weight-medium">
|
||
{{ newChannel.isPublic ? 'کانال عمومی' : 'کانال خصوصی' }}
|
||
</h4>
|
||
<p class="text-caption text-grey-darken-1">
|
||
{{ newChannel.isPublic
|
||
? 'کانالهای عمومی برای همه قابل مشاهده و پیوستن هستند'
|
||
: 'کانالهای خصوصی فقط برای اعضای دعوت شده قابل دسترسی هستند'
|
||
}}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<v-alert
|
||
:type="newChannel.isPublic ? 'info' : 'warning'"
|
||
variant="tonal"
|
||
class="mt-3"
|
||
>
|
||
<div class="d-flex align-center">
|
||
<v-icon start class="mr-2">
|
||
{{ newChannel.isPublic ? 'mdi-earth' : 'mdi-lock' }}
|
||
</v-icon>
|
||
<span>
|
||
{{ newChannel.isPublic
|
||
? 'این کانال در لیست کانالهای عمومی نمایش داده میشود'
|
||
: 'این کانال فقط برای اعضای دعوت شده قابل دسترسی است'
|
||
}}
|
||
</span>
|
||
</div>
|
||
</v-alert>
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-form>
|
||
</v-card-text>
|
||
|
||
<v-card-actions class="dialog-actions">
|
||
<v-btn
|
||
@click="showCreateChannel = false"
|
||
variant="outlined"
|
||
size="large"
|
||
>
|
||
لغو
|
||
</v-btn>
|
||
<v-spacer></v-spacer>
|
||
<v-btn
|
||
color="primary"
|
||
@click="createChannel"
|
||
:loading="creatingChannel"
|
||
size="large"
|
||
:disabled="!newChannel.name.trim()"
|
||
>
|
||
<v-icon start>mdi-plus</v-icon>
|
||
ایجاد کانال
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<!-- Member Management Dialog -->
|
||
<v-dialog v-model="showMemberDialog" max-width="700px" persistent>
|
||
<v-card class="member-dialog">
|
||
<v-card-title class="dialog-header">
|
||
<div class="d-flex align-center">
|
||
<v-avatar size="48" color="blue" class="mr-4">
|
||
<v-icon size="24" color="white">mdi-account-group</v-icon>
|
||
</v-avatar>
|
||
<div>
|
||
<h2 class="text-h5 mb-1">مدیریت اعضای کانال</h2>
|
||
<p class="text-body-2 text-grey-darken-1">
|
||
{{ currentChannel?.name }} - {{ channelMembers.length }} عضو
|
||
</p>
|
||
</div>
|
||
<v-spacer></v-spacer>
|
||
<v-btn
|
||
v-if="currentChannel?.isAdmin"
|
||
color="primary"
|
||
size="large"
|
||
@click="showAddMemberDialog = true"
|
||
class="add-member-btn"
|
||
>
|
||
<v-icon start>mdi-plus</v-icon>
|
||
اضافه کردن عضو
|
||
</v-btn>
|
||
</div>
|
||
</v-card-title>
|
||
|
||
<v-card-text class="dialog-content">
|
||
<div v-if="loadingMembers" class="loading-members">
|
||
<v-progress-circular indeterminate color="primary" size="60"></v-progress-circular>
|
||
<p class="text-body-1 mt-4">در حال بارگذاری اعضا...</p>
|
||
</div>
|
||
|
||
<div v-else>
|
||
<div class="member-stats mb-4">
|
||
<v-row>
|
||
<v-col cols="4">
|
||
<v-card variant="outlined" class="stat-card">
|
||
<v-card-text class="text-center pa-4">
|
||
<v-icon size="32" color="primary" class="mb-2">mdi-account-group</v-icon>
|
||
<h3 class="text-h4 font-weight-bold">{{ channelMembers.length }}</h3>
|
||
<p class="text-caption text-grey-darken-1">کل اعضا</p>
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-col>
|
||
<v-col cols="4">
|
||
<v-card variant="outlined" class="stat-card">
|
||
<v-card-text class="text-center pa-4">
|
||
<v-icon size="32" color="orange" class="mb-2">mdi-crown</v-icon>
|
||
<h3 class="text-h4 font-weight-bold">
|
||
{{ channelMembers.filter(m => m.isAdmin).length }}
|
||
</h3>
|
||
<p class="text-caption text-grey-darken-1">مدیر</p>
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-col>
|
||
<v-col cols="4">
|
||
<v-card variant="outlined" class="stat-card">
|
||
<v-card-text class="text-center pa-4">
|
||
<v-icon size="32" color="green" class="mb-2">mdi-account</v-icon>
|
||
<h3 class="text-h4 font-weight-bold">
|
||
{{ channelMembers.filter(m => !m.isAdmin).length }}
|
||
</h3>
|
||
<p class="text-caption text-grey-darken-1">عضو عادی</p>
|
||
</v-card-text>
|
||
</v-card>
|
||
</v-col>
|
||
</v-row>
|
||
</div>
|
||
|
||
<v-list class="member-list">
|
||
<v-list-item
|
||
v-for="member in channelMembers"
|
||
:key="member.id"
|
||
class="member-item"
|
||
rounded="lg"
|
||
>
|
||
<template v-slot:prepend>
|
||
<v-avatar size="48" color="primary" class="mr-3">
|
||
<span class="text-white text-h6">{{ member.fullName.charAt(0) }}</span>
|
||
</v-avatar>
|
||
</template>
|
||
|
||
<v-list-item-title class="font-weight-medium text-body-1">
|
||
{{ member.fullName }}
|
||
<v-chip
|
||
v-if="member.isAdmin"
|
||
size="small"
|
||
color="orange"
|
||
variant="elevated"
|
||
class="ml-2"
|
||
>
|
||
<v-icon start size="14">mdi-crown</v-icon>
|
||
مدیر
|
||
</v-chip>
|
||
</v-list-item-title>
|
||
|
||
<v-list-item-subtitle class="text-caption mt-1">
|
||
<v-icon size="14" class="mr-1">mdi-email</v-icon>
|
||
{{ maskEmail(member.email) }}
|
||
</v-list-item-subtitle>
|
||
|
||
<template v-slot:append>
|
||
<v-btn
|
||
v-if="currentChannel?.isAdmin && !member.isAdmin"
|
||
icon
|
||
size="small"
|
||
color="error"
|
||
variant="text"
|
||
@click="removeMemberFromChannel(member.id)"
|
||
class="remove-btn"
|
||
>
|
||
<v-icon>mdi-delete</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-list-item>
|
||
</v-list>
|
||
</div>
|
||
</v-card-text>
|
||
|
||
<v-card-actions class="dialog-actions">
|
||
<v-btn @click="showMemberDialog = false" variant="outlined" size="large">
|
||
بستن
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<!-- Add Member Dialog -->
|
||
<v-dialog v-model="showAddMemberDialog" max-width="600px" persistent>
|
||
<v-card class="add-member-dialog">
|
||
<v-card-title class="dialog-header">
|
||
<div class="d-flex align-center">
|
||
<v-avatar size="48" color="green" class="mr-4">
|
||
<v-icon size="24" color="white">mdi-account-plus</v-icon>
|
||
</v-avatar>
|
||
<div>
|
||
<h2 class="text-h5 mb-1">اضافه کردن عضو جدید</h2>
|
||
<p class="text-body-2 text-grey-darken-1">
|
||
کاربران جدید را به کانال {{ currentChannel?.name }} اضافه کنید
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</v-card-title>
|
||
|
||
<v-card-text class="dialog-content">
|
||
<v-text-field
|
||
v-model="memberSearch"
|
||
label="جستجوی کاربر"
|
||
placeholder="نام یا ایمیل کاربر را وارد کنید..."
|
||
prepend-inner-icon="mdi-magnify"
|
||
variant="outlined"
|
||
density="comfortable"
|
||
@input="searchUsersForMember"
|
||
class="mb-4"
|
||
></v-text-field>
|
||
|
||
<div v-if="searchingUsers" class="searching-users">
|
||
<v-progress-circular indeterminate color="primary" size="40"></v-progress-circular>
|
||
<p class="text-body-2 mt-3">در حال جستجو...</p>
|
||
</div>
|
||
|
||
<div v-else-if="searchResults.length > 0" class="search-results">
|
||
<h4 class="text-subtitle-1 font-weight-medium mb-3">
|
||
نتایج جستجو ({{ searchResults.length }} کاربر)
|
||
</h4>
|
||
<v-list class="search-result-list">
|
||
<v-list-item
|
||
v-for="user in searchResults"
|
||
:key="user.id"
|
||
@click="addMemberToChannel(user.id)"
|
||
class="search-result-item"
|
||
rounded="lg"
|
||
>
|
||
<template v-slot:prepend>
|
||
<v-avatar size="40" color="primary" class="mr-3">
|
||
<span class="text-white">{{ user.fullName.charAt(0) }}</span>
|
||
</v-avatar>
|
||
</template>
|
||
|
||
<v-list-item-title class="font-weight-medium">
|
||
{{ user.fullName }}
|
||
</v-list-item-title>
|
||
|
||
<v-list-item-subtitle class="text-caption">
|
||
<v-icon size="14" class="mr-1">mdi-email</v-icon>
|
||
{{ maskEmail(user.email) }}
|
||
</v-list-item-subtitle>
|
||
|
||
<template v-slot:append>
|
||
<v-btn
|
||
icon
|
||
size="small"
|
||
color="primary"
|
||
variant="text"
|
||
>
|
||
<v-icon>mdi-plus</v-icon>
|
||
</v-btn>
|
||
</template>
|
||
</v-list-item>
|
||
</v-list>
|
||
</div>
|
||
|
||
<div v-else-if="memberSearch && !searchingUsers" class="no-results">
|
||
<v-icon size="64" color="grey-lighten-1" class="mb-3">mdi-account-search</v-icon>
|
||
<h4 class="text-subtitle-1 text-grey-lighten-1 mb-2">کاربری یافت نشد</h4>
|
||
<p class="text-body-2 text-grey-darken-1">
|
||
کاربری با این نام یا ایمیل در سیستم وجود ندارد
|
||
</p>
|
||
</div>
|
||
|
||
<div v-else class="search-placeholder">
|
||
<v-icon size="64" color="grey-lighten-1" class="mb-3">mdi-account-search</v-icon>
|
||
<h4 class="text-subtitle-1 text-grey-lighten-1 mb-2">جستجوی کاربر</h4>
|
||
<p class="text-body-2 text-grey-darken-1">
|
||
نام یا ایمیل کاربر مورد نظر را وارد کنید
|
||
</p>
|
||
</div>
|
||
</v-card-text>
|
||
|
||
<v-card-actions class="dialog-actions">
|
||
<v-btn @click="showAddMemberDialog = false" variant="outlined" size="large">
|
||
لغو
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { defineComponent, ref, watch, nextTick, computed, onMounted, onUnmounted } from 'vue';
|
||
import axios from 'axios';
|
||
import { useChatStore } from '@/stores/chatStore';
|
||
|
||
export default defineComponent({
|
||
name: "ChatHome",
|
||
|
||
setup() {
|
||
// Chat store
|
||
const chatStore = useChatStore();
|
||
|
||
// Reactive data
|
||
const messageText = ref('');
|
||
const loading = ref(false);
|
||
const messagesContainer = ref(null);
|
||
|
||
// Channel management
|
||
const showChannelDrawer = ref(false);
|
||
const showCreateChannel = ref(false);
|
||
const showChannelInfo = ref(false);
|
||
const userChannels = ref([]);
|
||
const publicChannels = ref([]);
|
||
const currentChannel = ref(null);
|
||
const messages = ref([]);
|
||
const creatingChannel = ref(false);
|
||
const channelSearch = ref('');
|
||
|
||
// Pagination state
|
||
const pagination = ref({
|
||
limit: 30,
|
||
offset: 0,
|
||
hasMore: true,
|
||
loadingMore: false,
|
||
totalMessages: 0
|
||
});
|
||
|
||
// Emoji picker
|
||
const showEmojiPicker = ref(false);
|
||
const commonEmojis = ref([
|
||
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇',
|
||
'🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚',
|
||
'😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩',
|
||
'🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣',
|
||
'😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬',
|
||
'🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗',
|
||
'🤔', '🤭', '🤫', '🤥', '😶', '😐', '😑', '😯', '😦', '😧',
|
||
'😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '🤐', '🥴', '🤢',
|
||
'🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠', '💩', '👻', '💀',
|
||
'☠️', '👽', '👾', '🤖', '😺', '😸', '😹', '😻', '😼', '😽',
|
||
'🙀', '😿', '😾', '🙈', '🙉', '🙊', '💌', '💘', '💝', '💖',
|
||
'💗', '💙', '💚', '❣️', '💕', '💞', '💓', '💗', '💖', '💘',
|
||
'💝', '💟', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
|
||
'🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝',
|
||
'💟', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎'
|
||
]);
|
||
|
||
// Quote/Reply functionality
|
||
const quotedMessage = ref(null);
|
||
const showMessageActions = ref(null);
|
||
|
||
// Like/Dislike functionality
|
||
const currentUser = ref({
|
||
id: 1,
|
||
fullName: 'کاربر فعلی',
|
||
email: 'user@example.com'
|
||
});
|
||
|
||
// Loading states for reactions
|
||
const reactingMessages = ref(new Set());
|
||
|
||
// Member management
|
||
const showMemberDialog = ref(false);
|
||
const showAddMemberDialog = ref(false);
|
||
const channelMembers = ref([]);
|
||
const loadingMembers = ref(false);
|
||
const memberSearch = ref('');
|
||
const searchResults = ref([]);
|
||
const searchingUsers = ref(false);
|
||
|
||
// Auto refresh
|
||
const autoRefreshInterval = ref(null);
|
||
const refreshInterval = 3000; // 3 seconds
|
||
|
||
// New channel form
|
||
const newChannel = ref({
|
||
name: '',
|
||
description: '',
|
||
isPublic: true
|
||
});
|
||
|
||
// Computed properties
|
||
const filteredChannels = computed(() => {
|
||
if (!channelSearch.value) return userChannels.value;
|
||
return userChannels.value.filter(channel =>
|
||
channel.name.toLowerCase().includes(channelSearch.value.toLowerCase())
|
||
);
|
||
});
|
||
|
||
const canSendMessage = computed(() => {
|
||
if (!currentChannel.value) return false;
|
||
// User can send message if they are a member of the channel
|
||
return userChannels.value.some(channel => channel.id === currentChannel.value.id);
|
||
});
|
||
|
||
const isCurrentChannelMember = computed(() => {
|
||
if (!currentChannel.value) return false;
|
||
return userChannels.value.some(channel => channel.id === currentChannel.value.id);
|
||
});
|
||
|
||
const isChannelJoined = (channelId) => {
|
||
return userChannels.value.some(channel => channel.id === channelId);
|
||
};
|
||
|
||
const getChannelInfo = computed(() => {
|
||
if (!currentChannel.value) return null;
|
||
|
||
return {
|
||
name: currentChannel.value.name || '',
|
||
description: currentChannel.value.description || '',
|
||
isPublic: currentChannel.value.isPublic || false,
|
||
messageCount: currentChannel.value.messageCount || 0,
|
||
memberCount: currentChannel.value.memberCount || 0,
|
||
createdAt: currentChannel.value.createdAt || null,
|
||
isAdmin: currentChannel.value.isAdmin || false
|
||
};
|
||
});
|
||
|
||
// Methods
|
||
const loadUserChannels = async () => {
|
||
try {
|
||
loading.value = true;
|
||
const response = await axios.get('/api/chat/channels');
|
||
if (response.data.success) {
|
||
userChannels.value = response.data.data;
|
||
if (userChannels.value.length > 0) {
|
||
selectChannel(userChannels.value[0]);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading channels:', error);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const updateUserChannels = async () => {
|
||
try {
|
||
const response = await axios.get('/api/chat/channels');
|
||
if (response.data.success) {
|
||
userChannels.value = response.data.data;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating user channels:', error);
|
||
}
|
||
};
|
||
|
||
const loadPublicChannels = async () => {
|
||
try {
|
||
const response = await axios.get('/api/chat/channels/search?q=');
|
||
if (response.data.success) {
|
||
// Filter out channels that user is already a member of
|
||
publicChannels.value = response.data.data.filter(channel =>
|
||
!userChannels.value.some(userChannel => userChannel.id === channel.id)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading public channels:', error);
|
||
}
|
||
};
|
||
|
||
const searchChannels = async () => {
|
||
if (channelSearch.value.trim()) {
|
||
try {
|
||
const response = await axios.get(`/api/chat/channels/search?q=${encodeURIComponent(channelSearch.value)}`);
|
||
if (response.data.success) {
|
||
publicChannels.value = response.data.data.filter(channel =>
|
||
!userChannels.value.some(userChannel => userChannel.id === channel.id)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error searching channels:', error);
|
||
}
|
||
} else {
|
||
loadPublicChannels();
|
||
}
|
||
};
|
||
|
||
const selectChannel = async (channel) => {
|
||
currentChannel.value = channel;
|
||
showChannelDrawer.value = false;
|
||
|
||
// Reset pagination when switching channels
|
||
pagination.value = {
|
||
limit: 30,
|
||
offset: 0,
|
||
hasMore: true,
|
||
loadingMore: false,
|
||
totalMessages: 0
|
||
};
|
||
|
||
await loadChannelMessages(channel.channelId);
|
||
|
||
// Update channel stats when selecting a channel
|
||
await updateChannelStats();
|
||
};
|
||
|
||
const joinChannel = async (channel) => {
|
||
try {
|
||
const response = await axios.post(`/api/chat/channels/${channel.channelId}/join`);
|
||
if (response.data.success) {
|
||
// Update user channels to get updated membership status
|
||
await updateUserChannels();
|
||
|
||
// Immediately remove the joined channel from public channels list
|
||
publicChannels.value = publicChannels.value.filter(c => c.id !== channel.id);
|
||
|
||
// If we're currently viewing this channel, update the current channel data
|
||
if (currentChannel.value && currentChannel.value.id === channel.id) {
|
||
// Find the updated channel data from userChannels
|
||
const updatedChannel = userChannels.value.find(c => c.id === channel.id);
|
||
if (updatedChannel) {
|
||
currentChannel.value = updatedChannel;
|
||
}
|
||
}
|
||
|
||
// Force reactivity update
|
||
await nextTick();
|
||
|
||
// Update channel stats after joining
|
||
await updateChannelStats();
|
||
|
||
// Show success message (optional)
|
||
console.log('Successfully joined channel:', channel.name);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error joining channel:', error);
|
||
}
|
||
};
|
||
|
||
const leaveChannel = async () => {
|
||
if (!currentChannel.value) return;
|
||
|
||
try {
|
||
const response = await axios.post(`/api/chat/channels/${currentChannel.value.channelId}/leave`);
|
||
if (response.data.success) {
|
||
await loadUserChannels();
|
||
await loadPublicChannels();
|
||
currentChannel.value = null;
|
||
messages.value = [];
|
||
|
||
// Update channel stats after leaving
|
||
if (currentChannel.value) {
|
||
await updateChannelStats();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error leaving channel:', error);
|
||
}
|
||
};
|
||
|
||
const loadChannelMessages = async (channelId, isAutoRefresh = false, isLoadMore = false) => {
|
||
try {
|
||
if (!isAutoRefresh && !isLoadMore) {
|
||
loading.value = true;
|
||
}
|
||
if (isLoadMore) {
|
||
pagination.value.loadingMore = true;
|
||
}
|
||
|
||
const params = new URLSearchParams({
|
||
limit: pagination.value.limit.toString(),
|
||
offset: pagination.value.offset.toString()
|
||
});
|
||
|
||
const response = await axios.get(`/api/chat/channels/${channelId}/messages?${params}`);
|
||
if (response.data.success) {
|
||
const newMessages = response.data.data.reverse(); // Show newest first
|
||
const paginationInfo = response.data.pagination;
|
||
|
||
// For auto-refresh, only update if there are new messages
|
||
if (isAutoRefresh) {
|
||
const currentMessageIds = messages.value.map(m => m.id);
|
||
const newMessageIds = newMessages.map(m => m.id);
|
||
const hasNewMessages = newMessageIds.some(id => !currentMessageIds.includes(id));
|
||
|
||
if (hasNewMessages) {
|
||
messages.value = newMessages;
|
||
await nextTick();
|
||
scrollToBottom();
|
||
}
|
||
} else if (isLoadMore) {
|
||
// For load more, prepend older messages to the beginning
|
||
if (newMessages.length > 0) {
|
||
const scrollPosition = messagesContainer.value?.scrollTop || 0;
|
||
const scrollHeight = messagesContainer.value?.scrollHeight || 0;
|
||
|
||
messages.value = [...newMessages, ...messages.value];
|
||
|
||
// Update pagination from server response
|
||
pagination.value.offset = paginationInfo.offset + pagination.value.limit;
|
||
pagination.value.hasMore = paginationInfo.hasMore;
|
||
pagination.value.totalMessages = paginationInfo.total;
|
||
|
||
// Maintain scroll position after adding older messages
|
||
await nextTick();
|
||
if (messagesContainer.value) {
|
||
const newScrollHeight = messagesContainer.value.scrollHeight;
|
||
const scrollDiff = newScrollHeight - scrollHeight;
|
||
messagesContainer.value.scrollTop = scrollPosition + scrollDiff;
|
||
}
|
||
} else {
|
||
pagination.value.hasMore = false;
|
||
}
|
||
} else {
|
||
// Initial load
|
||
messages.value = newMessages;
|
||
pagination.value.offset = paginationInfo.offset + pagination.value.limit;
|
||
pagination.value.hasMore = paginationInfo.hasMore;
|
||
pagination.value.totalMessages = paginationInfo.total;
|
||
|
||
await nextTick();
|
||
scrollToBottom();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading messages:', error);
|
||
} finally {
|
||
if (!isAutoRefresh && !isLoadMore) {
|
||
loading.value = false;
|
||
}
|
||
if (isLoadMore) {
|
||
pagination.value.loadingMore = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const loadMoreMessages = async () => {
|
||
if (!currentChannel.value || pagination.value.loadingMore || !pagination.value.hasMore) {
|
||
return;
|
||
}
|
||
|
||
await loadChannelMessages(currentChannel.value.channelId, false, true);
|
||
};
|
||
|
||
const handleScroll = () => {
|
||
if (!messagesContainer.value || pagination.value.loadingMore || !pagination.value.hasMore) {
|
||
return;
|
||
}
|
||
|
||
const { scrollTop } = messagesContainer.value;
|
||
|
||
// Load more messages when user scrolls to the top (or near the top)
|
||
if (scrollTop <= 100) {
|
||
loadMoreMessages();
|
||
}
|
||
};
|
||
|
||
const sendMessage = async () => {
|
||
if (!messageText.value.trim() || loading.value || !currentChannel.value) return;
|
||
|
||
// Check if user can send message
|
||
if (!canSendMessage.value) {
|
||
// Show a notification that user needs to join
|
||
console.log('User needs to join channel to send messages');
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
const messageData = {
|
||
content: messageText.value,
|
||
messageType: 'text'
|
||
};
|
||
|
||
// Add quoted message ID if there's a quoted message
|
||
if (quotedMessage.value) {
|
||
messageData.quotedMessageId = quotedMessage.value.id;
|
||
}
|
||
|
||
const response = await axios.post(`/api/chat/channels/${currentChannel.value.channelId}/messages`, messageData);
|
||
|
||
if (response.data.success) {
|
||
const newMessage = response.data.data;
|
||
messages.value.push(newMessage);
|
||
messageText.value = '';
|
||
quotedMessage.value = null; // Clear quoted message after sending
|
||
|
||
// Update channel stats after sending message
|
||
await updateChannelStats();
|
||
|
||
await nextTick();
|
||
scrollToBottom();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending message:', error);
|
||
// Handle specific error for non-members
|
||
if (error.response?.status === 403) {
|
||
console.log('User is not a member of this channel');
|
||
}
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const quoteMessage = (message) => {
|
||
quotedMessage.value = message;
|
||
// Focus on the input field
|
||
nextTick(() => {
|
||
const input = document.querySelector('.message-input input');
|
||
if (input) {
|
||
input.focus();
|
||
}
|
||
});
|
||
};
|
||
|
||
const replyToMessage = (message) => {
|
||
quotedMessage.value = message;
|
||
// Focus on the input field
|
||
nextTick(() => {
|
||
const input = document.querySelector('.message-input input');
|
||
if (input) {
|
||
input.focus();
|
||
}
|
||
});
|
||
};
|
||
|
||
const cancelQuote = () => {
|
||
quotedMessage.value = null;
|
||
};
|
||
|
||
const isLiked = (message) => {
|
||
if (!message.reactions || !message.reactions['❤️']) return false;
|
||
return message.reactions['❤️'].includes(currentUser.value.id);
|
||
};
|
||
|
||
const isDisliked = (message) => {
|
||
if (!message.reactions || !message.reactions['👎']) return false;
|
||
return message.reactions['👎'].includes(currentUser.value.id);
|
||
};
|
||
|
||
const toggleLike = async (message) => {
|
||
const loadingKey = `like-${message.id}`;
|
||
|
||
// Prevent multiple clicks
|
||
if (reactingMessages.value.has(loadingKey)) return;
|
||
|
||
try {
|
||
reactingMessages.value.add(loadingKey);
|
||
const wasLiked = isLiked(message);
|
||
|
||
// Store original state for rollback
|
||
const originalReactions = JSON.parse(JSON.stringify(message.reactions || {}));
|
||
|
||
if (wasLiked) {
|
||
// Remove like
|
||
await axios.delete(`/api/chat/messages/${message.id}/reactions`, {
|
||
data: { emoji: '❤️' }
|
||
});
|
||
|
||
// Update local state immediately
|
||
if (message.reactions && message.reactions['❤️']) {
|
||
message.reactions['❤️'] = message.reactions['❤️'].filter(id => id !== currentUser.value.id);
|
||
if (message.reactions['❤️'].length === 0) {
|
||
delete message.reactions['❤️'];
|
||
}
|
||
}
|
||
} else {
|
||
// Add like
|
||
await axios.post(`/api/chat/messages/${message.id}/reactions`, {
|
||
emoji: '❤️'
|
||
});
|
||
|
||
// Update local state immediately
|
||
if (!message.reactions) {
|
||
message.reactions = {};
|
||
}
|
||
if (!message.reactions['❤️']) {
|
||
message.reactions['❤️'] = [];
|
||
}
|
||
if (!message.reactions['❤️'].includes(currentUser.value.id)) {
|
||
message.reactions['❤️'].push(currentUser.value.id);
|
||
}
|
||
}
|
||
|
||
// Force reactivity update
|
||
messages.value = [...messages.value];
|
||
} catch (error) {
|
||
console.error('Error toggling like:', error);
|
||
// Revert local changes on error
|
||
message.reactions = originalReactions;
|
||
messages.value = [...messages.value];
|
||
// Optionally show error message to user
|
||
// You can add a toast notification here
|
||
} finally {
|
||
reactingMessages.value.delete(loadingKey);
|
||
}
|
||
};
|
||
|
||
const toggleDislike = async (message) => {
|
||
const loadingKey = `dislike-${message.id}`;
|
||
|
||
// Prevent multiple clicks
|
||
if (reactingMessages.value.has(loadingKey)) return;
|
||
|
||
try {
|
||
reactingMessages.value.add(loadingKey);
|
||
const wasDisliked = isDisliked(message);
|
||
|
||
// Store original state for rollback
|
||
const originalReactions = JSON.parse(JSON.stringify(message.reactions || {}));
|
||
|
||
if (wasDisliked) {
|
||
// Remove dislike
|
||
await axios.delete(`/api/chat/messages/${message.id}/reactions`, {
|
||
data: { emoji: '👎' }
|
||
});
|
||
|
||
// Update local state immediately
|
||
if (message.reactions && message.reactions['👎']) {
|
||
message.reactions['👎'] = message.reactions['👎'].filter(id => id !== currentUser.value.id);
|
||
if (message.reactions['👎'].length === 0) {
|
||
delete message.reactions['👎'];
|
||
}
|
||
}
|
||
} else {
|
||
// Add dislike
|
||
await axios.post(`/api/chat/messages/${message.id}/reactions`, {
|
||
emoji: '👎'
|
||
});
|
||
|
||
// Update local state immediately
|
||
if (!message.reactions) {
|
||
message.reactions = {};
|
||
}
|
||
if (!message.reactions['👎']) {
|
||
message.reactions['👎'] = [];
|
||
}
|
||
if (!message.reactions['👎'].includes(currentUser.value.id)) {
|
||
message.reactions['👎'].push(currentUser.value.id);
|
||
}
|
||
}
|
||
|
||
// Force reactivity update
|
||
messages.value = [...messages.value];
|
||
} catch (error) {
|
||
console.error('Error toggling dislike:', error);
|
||
// Revert local changes on error
|
||
message.reactions = originalReactions;
|
||
messages.value = [...messages.value];
|
||
// Optionally show error message to user
|
||
// You can add a toast notification here
|
||
} finally {
|
||
reactingMessages.value.delete(loadingKey);
|
||
}
|
||
};
|
||
|
||
const createChannel = async () => {
|
||
if (!newChannel.value.name.trim()) return;
|
||
|
||
creatingChannel.value = true;
|
||
|
||
try {
|
||
const response = await axios.post('/api/chat/channels', {
|
||
name: newChannel.value.name,
|
||
description: newChannel.value.description,
|
||
isPublic: newChannel.value.isPublic
|
||
});
|
||
|
||
if (response.data.success) {
|
||
const newChannelData = response.data.data;
|
||
userChannels.value.push(newChannelData);
|
||
selectChannel(newChannelData);
|
||
showCreateChannel.value = false;
|
||
newChannel.value = { name: '', description: '', isPublic: true };
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating channel:', error);
|
||
} finally {
|
||
creatingChannel.value = false;
|
||
}
|
||
};
|
||
|
||
const insertEmoji = (emoji) => {
|
||
messageText.value += emoji;
|
||
showEmojiPicker.value = false;
|
||
};
|
||
|
||
const scrollToBottom = () => {
|
||
if (messagesContainer.value) {
|
||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||
}
|
||
};
|
||
|
||
const updateChannelStats = async () => {
|
||
if (!currentChannel.value) return;
|
||
|
||
try {
|
||
const response = await axios.get(`/api/chat/channels/${currentChannel.value.channelId}/stats`);
|
||
if (response.data.success) {
|
||
const stats = response.data.data;
|
||
|
||
// Update current channel stats
|
||
if (currentChannel.value) {
|
||
currentChannel.value.messageCount = stats.messageCount;
|
||
currentChannel.value.memberCount = stats.memberCount;
|
||
}
|
||
|
||
// Update channel in userChannels list
|
||
const channelIndex = userChannels.value.findIndex(c => c.id === currentChannel.value.id);
|
||
if (channelIndex !== -1) {
|
||
userChannels.value[channelIndex].messageCount = stats.messageCount;
|
||
userChannels.value[channelIndex].memberCount = stats.memberCount;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating channel stats:', error);
|
||
}
|
||
};
|
||
|
||
const refreshChannels = async () => {
|
||
await loadUserChannels();
|
||
await loadPublicChannels();
|
||
};
|
||
|
||
// Member management methods
|
||
const loadChannelMembers = async () => {
|
||
if (!currentChannel.value) return;
|
||
|
||
// Only load members if user is a member of the channel
|
||
if (!canSendMessage.value) {
|
||
channelMembers.value = [];
|
||
return;
|
||
}
|
||
|
||
loadingMembers.value = true;
|
||
try {
|
||
const members = await chatStore.getChannelMembers(currentChannel.value.channelId);
|
||
channelMembers.value = members;
|
||
} catch (error) {
|
||
console.error('Error loading channel members:', error);
|
||
channelMembers.value = [];
|
||
} finally {
|
||
loadingMembers.value = false;
|
||
}
|
||
};
|
||
|
||
const searchUsersForMember = async () => {
|
||
if (!memberSearch.value.trim()) {
|
||
searchResults.value = [];
|
||
return;
|
||
}
|
||
|
||
searchingUsers.value = true;
|
||
try {
|
||
const users = await chatStore.searchUsers(memberSearch.value);
|
||
// Filter out users who are already members
|
||
const memberIds = channelMembers.value.map(m => m.id);
|
||
searchResults.value = users.filter(user => !memberIds.includes(user.id));
|
||
} catch (error) {
|
||
console.error('Error searching users:', error);
|
||
searchResults.value = [];
|
||
} finally {
|
||
searchingUsers.value = false;
|
||
}
|
||
};
|
||
|
||
const addMemberToChannel = async (userId) => {
|
||
if (!currentChannel.value) return;
|
||
|
||
try {
|
||
const success = await chatStore.addMember(currentChannel.value.channelId, userId);
|
||
if (success) {
|
||
await loadChannelMembers();
|
||
showAddMemberDialog.value = false;
|
||
memberSearch.value = '';
|
||
searchResults.value = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding member:', error);
|
||
}
|
||
};
|
||
|
||
const removeMemberFromChannel = async (userId) => {
|
||
if (!currentChannel.value) return;
|
||
|
||
try {
|
||
const success = await chatStore.removeMember(currentChannel.value.channelId, userId);
|
||
if (success) {
|
||
await loadChannelMembers();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error removing member:', error);
|
||
}
|
||
};
|
||
|
||
// Watch for channel changes to load members
|
||
watch(currentChannel, async (newChannel) => {
|
||
if (newChannel && canSendMessage.value) {
|
||
await loadChannelMembers();
|
||
} else {
|
||
channelMembers.value = [];
|
||
}
|
||
});
|
||
|
||
// Auto refresh methods
|
||
const startAutoRefresh = () => {
|
||
if (autoRefreshInterval.value) {
|
||
clearInterval(autoRefreshInterval.value);
|
||
}
|
||
|
||
autoRefreshInterval.value = setInterval(async () => {
|
||
if (currentChannel.value && !loading.value) {
|
||
await loadChannelMessages(currentChannel.value.channelId, true);
|
||
}
|
||
}, refreshInterval);
|
||
};
|
||
|
||
const stopAutoRefresh = () => {
|
||
if (autoRefreshInterval.value) {
|
||
clearInterval(autoRefreshInterval.value);
|
||
autoRefreshInterval.value = null;
|
||
}
|
||
};
|
||
|
||
// Watch current channel for auto refresh
|
||
watch(currentChannel, (newChannel) => {
|
||
if (newChannel) {
|
||
startAutoRefresh();
|
||
} else {
|
||
stopAutoRefresh();
|
||
}
|
||
});
|
||
|
||
const formatTime = (timeString) => {
|
||
if (!timeString) return '-';
|
||
|
||
const date = new Date(timeString);
|
||
|
||
// Check if date is valid
|
||
if (isNaN(date.getTime())) {
|
||
return '-';
|
||
}
|
||
|
||
return new Intl.DateTimeFormat('fa-IR', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
}).format(date);
|
||
};
|
||
|
||
const formatDate = (dateString) => {
|
||
if (!dateString) return '-';
|
||
|
||
const date = new Date(dateString);
|
||
|
||
// Check if date is valid
|
||
if (isNaN(date.getTime())) {
|
||
return '-';
|
||
}
|
||
|
||
return new Intl.DateTimeFormat('fa-IR', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric'
|
||
}).format(date);
|
||
};
|
||
|
||
// Function to mask email for security
|
||
const maskEmail = (email) => {
|
||
if (!email) return '';
|
||
|
||
const [username, domain] = email.split('@');
|
||
if (!domain) return email;
|
||
|
||
// Mask username (show first and last character)
|
||
let maskedUsername = username;
|
||
if (username.length > 2) {
|
||
maskedUsername = username.charAt(0) + '*'.repeat(username.length - 2) + username.charAt(username.length - 1);
|
||
} else if (username.length === 2) {
|
||
maskedUsername = username.charAt(0) + '*';
|
||
}
|
||
|
||
// Mask domain (show first character of each part)
|
||
const domainParts = domain.split('.');
|
||
const maskedDomainParts = domainParts.map(part => {
|
||
if (part.length > 1) {
|
||
return part.charAt(0) + '*'.repeat(part.length - 1);
|
||
}
|
||
return part;
|
||
});
|
||
|
||
return `${maskedUsername}@${maskedDomainParts.join('.')}`;
|
||
};
|
||
|
||
// Initialize
|
||
onMounted(() => {
|
||
loadUserChannels();
|
||
loadPublicChannels();
|
||
});
|
||
|
||
// Cleanup on unmount
|
||
onUnmounted(() => {
|
||
stopAutoRefresh();
|
||
});
|
||
|
||
return {
|
||
messageText,
|
||
loading,
|
||
messagesContainer,
|
||
showChannelDrawer,
|
||
showCreateChannel,
|
||
showChannelInfo,
|
||
userChannels,
|
||
publicChannels,
|
||
currentChannel,
|
||
messages,
|
||
creatingChannel,
|
||
showEmojiPicker,
|
||
commonEmojis,
|
||
currentUser,
|
||
newChannel,
|
||
channelSearch,
|
||
filteredChannels,
|
||
canSendMessage,
|
||
isCurrentChannelMember,
|
||
isChannelJoined,
|
||
getChannelInfo,
|
||
showMemberDialog,
|
||
showAddMemberDialog,
|
||
channelMembers,
|
||
loadingMembers,
|
||
memberSearch,
|
||
searchResults,
|
||
searchingUsers,
|
||
pagination,
|
||
sendMessage,
|
||
createChannel,
|
||
selectChannel,
|
||
joinChannel,
|
||
leaveChannel,
|
||
searchChannels,
|
||
insertEmoji,
|
||
refreshChannels,
|
||
loadChannelMembers,
|
||
searchUsersForMember,
|
||
addMemberToChannel,
|
||
removeMemberFromChannel,
|
||
updateUserChannels,
|
||
updateChannelStats,
|
||
formatTime,
|
||
formatDate,
|
||
quotedMessage,
|
||
showMessageActions,
|
||
quoteMessage,
|
||
replyToMessage,
|
||
cancelQuote,
|
||
isLiked,
|
||
isDisliked,
|
||
toggleLike,
|
||
toggleDislike,
|
||
reactingMessages,
|
||
handleScroll,
|
||
loadMoreMessages,
|
||
maskEmail,
|
||
};
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.chat-home {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.chat-container {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.channel-drawer {
|
||
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||
background-color: rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.drawer-header {
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||
background-color: rgba(0, 0, 0, 0.01);
|
||
}
|
||
|
||
.channel-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-header {
|
||
background-color: rgba(0, 0, 0, 0.02);
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.channel-item {
|
||
margin: 4px 8px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.channel-item:hover {
|
||
background-color: rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.channel-active {
|
||
background-color: rgba(25, 118, 210, 0.1) !important;
|
||
border-left: 3px solid #1976d2;
|
||
}
|
||
|
||
.chat-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.chat-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.channel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||
background-color: rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.channel-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.channel-info {
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||
background-color: rgba(0, 0, 0, 0.01);
|
||
}
|
||
|
||
.channel-stats {
|
||
display: flex;
|
||
gap: 24px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 0.875rem;
|
||
color: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
.messages-container {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.loading-state,
|
||
.empty-messages {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.messages-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.message-item {
|
||
display: flex;
|
||
gap: 16px;
|
||
}
|
||
|
||
.message-own {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
.message-content {
|
||
flex: 1;
|
||
max-width: 70%;
|
||
}
|
||
|
||
.message-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.message-sender {
|
||
font-weight: 600;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 0.75rem;
|
||
color: rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
.quoted-message {
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
border-left: 3px solid #1976d2;
|
||
padding: 12px;
|
||
margin-bottom: 12px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.quoted-sender {
|
||
font-weight: 600;
|
||
font-size: 0.75rem;
|
||
color: #1976d2;
|
||
}
|
||
|
||
.quoted-text {
|
||
font-size: 0.875rem;
|
||
color: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
.message-bubble {
|
||
background-color: #f5f5f5;
|
||
padding: 16px;
|
||
border-radius: 16px;
|
||
position: relative;
|
||
}
|
||
|
||
.message-own .message-bubble {
|
||
background-color: #1976d2;
|
||
color: white;
|
||
}
|
||
|
||
.emoji-message {
|
||
font-size: 2.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.text-message {
|
||
line-height: 1.6;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.message-reactions {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.message-input-container {
|
||
padding: 20px;
|
||
border-top: 1px solid rgba(0, 0, 0, 0.12);
|
||
background-color: rgba(0, 0, 0, 0.02);
|
||
position: relative;
|
||
}
|
||
|
||
.input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.message-input {
|
||
background-color: white;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.emoji-picker {
|
||
position: absolute;
|
||
bottom: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
background: white;
|
||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
max-height: 250px;
|
||
overflow-y: auto;
|
||
z-index: 1000;
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.emoji-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(8, 1fr);
|
||
gap: 6px;
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
padding: 40px;
|
||
}
|
||
|
||
.empty-content {
|
||
text-align: center;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.join-prompt {
|
||
text-align: center;
|
||
padding: 20px;
|
||
background-color: rgba(0, 0, 0, 0.02);
|
||
border-radius: 12px;
|
||
margin: 20px;
|
||
}
|
||
|
||
.v-theme--dark .join-prompt {
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
/* Custom scrollbar */
|
||
.messages-container::-webkit-scrollbar,
|
||
.channel-drawer::-webkit-scrollbar,
|
||
.channel-menu-card::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
.messages-container::-webkit-scrollbar-track,
|
||
.channel-drawer::-webkit-scrollbar-track,
|
||
.channel-menu-card::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.messages-container::-webkit-scrollbar-thumb,
|
||
.channel-drawer::-webkit-scrollbar-thumb,
|
||
.channel-menu-card::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.messages-container::-webkit-scrollbar-thumb:hover,
|
||
.channel-drawer::-webkit-scrollbar-thumb:hover,
|
||
.channel-menu-card::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
/* Dark theme support */
|
||
.v-theme--dark .channel-drawer {
|
||
border-right-color: rgba(255, 255, 255, 0.12);
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
.v-theme--dark .drawer-header {
|
||
border-bottom-color: rgba(255, 255, 255, 0.12);
|
||
background-color: rgba(255, 255, 255, 0.01);
|
||
}
|
||
|
||
.v-theme--dark .section-header {
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.v-theme--dark .channel-item:hover {
|
||
background-color: rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.v-theme--dark .message-input-container {
|
||
border-top-color: rgba(255, 255, 255, 0.12);
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
.v-theme--dark .channel-header {
|
||
border-bottom-color: rgba(255, 255, 255, 0.12);
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
}
|
||
|
||
.v-theme--dark .channel-info {
|
||
border-bottom-color: rgba(255, 255, 255, 0.12);
|
||
background-color: rgba(255, 255, 255, 0.01);
|
||
}
|
||
|
||
.v-theme--dark .message-bubble {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.v-theme--dark .quoted-message {
|
||
background-color: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.v-theme--dark .message-input {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
/* Load more indicator */
|
||
.load-more-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16px;
|
||
background-color: rgba(0, 0, 0, 0.02);
|
||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||
color: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
.v-theme--dark .load-more-indicator {
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
/* Mobile optimizations */
|
||
@media (max-width: 768px) {
|
||
.chat-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.channel-drawer {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1000;
|
||
background: white;
|
||
}
|
||
|
||
.chat-area {
|
||
width: 100%;
|
||
}
|
||
|
||
.message-content {
|
||
max-width: 85%;
|
||
}
|
||
|
||
.emoji-picker {
|
||
position: fixed;
|
||
bottom: 100px;
|
||
left: 16px;
|
||
right: 16px;
|
||
max-height: 300px;
|
||
}
|
||
|
||
.channel-header {
|
||
padding: 16px;
|
||
}
|
||
|
||
.messages-container {
|
||
padding: 16px;
|
||
}
|
||
|
||
.message-input-container {
|
||
padding: 16px;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.channel-stats {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
/* Member management styles */
|
||
.member-item {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.member-item:hover {
|
||
background-color: rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.search-result-item {
|
||
transition: all 0.2s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.search-result-item:hover {
|
||
background-color: rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
/* Join button and member chip styles */
|
||
.join-btn {
|
||
font-weight: 600;
|
||
text-transform: none;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.join-btn:hover {
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.member-chip {
|
||
font-weight: 600;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.count-chip {
|
||
font-weight: 600;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.unread-chip {
|
||
font-weight: 600;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.v-theme--dark .member-item:hover,
|
||
.v-theme--dark .search-result-item:hover {
|
||
background-color: rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.v-theme--dark .join-btn {
|
||
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.v-theme--dark .member-chip {
|
||
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.v-theme--dark .count-chip {
|
||
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.v-theme--dark .unread-chip {
|
||
box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
/* New styles for quoted message preview */
|
||
.quoted-message-preview {
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
border-left: 3px solid #1976d2;
|
||
padding: 12px;
|
||
margin-bottom: 12px;
|
||
border-radius: 8px;
|
||
position: relative;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.quoted-preview-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.quoted-preview-header {
|
||
display: flex;
|
||
align-items: center;
|
||
background-color: rgba(25, 118, 210, 0.1);
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
font-size: 0.75rem;
|
||
color: #1976d2;
|
||
}
|
||
|
||
.quoted-preview-text {
|
||
font-size: 0.875rem;
|
||
color: rgba(0, 0, 0, 0.7);
|
||
word-break: break-word;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.v-theme--dark .quoted-message-preview {
|
||
background-color: rgba(255, 255, 255, 0.05);
|
||
border-left-color: #1976d2;
|
||
}
|
||
|
||
.v-theme--dark .quoted-preview-header {
|
||
background-color: rgba(25, 118, 210, 0.2);
|
||
color: #64b5f6;
|
||
}
|
||
|
||
.v-theme--dark .quoted-preview-text {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
/* Message actions styles */
|
||
.message-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin-top: 8px;
|
||
opacity: 0;
|
||
transition: opacity 0.2s ease;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.message-item:hover .message-actions {
|
||
opacity: 1;
|
||
}
|
||
|
||
.message-own .message-actions {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.action-btn {
|
||
color: rgba(0, 0, 0, 0.6) !important;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
color: #1976d2 !important;
|
||
background-color: rgba(25, 118, 210, 0.1) !important;
|
||
}
|
||
|
||
.action-btn.liked {
|
||
color: #f44336 !important;
|
||
}
|
||
|
||
.action-btn.liked:hover {
|
||
color: #d32f2f !important;
|
||
background-color: rgba(244, 67, 54, 0.1) !important;
|
||
}
|
||
|
||
.action-btn.disliked {
|
||
color: #ff9800 !important;
|
||
}
|
||
|
||
.action-btn.disliked:hover {
|
||
color: #f57c00 !important;
|
||
background-color: rgba(255, 152, 0, 0.1) !important;
|
||
}
|
||
|
||
.v-theme--dark .action-btn {
|
||
color: rgba(255, 255, 255, 0.6) !important;
|
||
}
|
||
|
||
.v-theme--dark .action-btn:hover {
|
||
color: #64b5f6 !important;
|
||
background-color: rgba(100, 181, 246, 0.1) !important;
|
||
}
|
||
|
||
.v-theme--dark .action-btn.liked {
|
||
color: #ef5350 !important;
|
||
}
|
||
|
||
.v-theme--dark .action-btn.liked:hover {
|
||
color: #e53935 !important;
|
||
background-color: rgba(239, 83, 80, 0.1) !important;
|
||
}
|
||
|
||
.v-theme--dark .action-btn.disliked {
|
||
color: #ffb74d !important;
|
||
}
|
||
|
||
.v-theme--dark .action-btn.disliked:hover {
|
||
color: #ffa726 !important;
|
||
background-color: rgba(255, 183, 77, 0.1) !important;
|
||
}
|
||
|
||
/* Improve quoted message styling */
|
||
.quoted-message {
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
border-left: 3px solid #1976d2;
|
||
padding: 12px;
|
||
margin-bottom: 12px;
|
||
border-radius: 8px;
|
||
position: relative;
|
||
}
|
||
|
||
.quoted-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.quoted-sender {
|
||
font-weight: 600;
|
||
font-size: 0.75rem;
|
||
color: #1976d2;
|
||
}
|
||
|
||
.quoted-text {
|
||
font-size: 0.875rem;
|
||
color: rgba(0, 0, 0, 0.7);
|
||
line-height: 1.4;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.v-theme--dark .quoted-message {
|
||
background-color: rgba(255, 255, 255, 0.05);
|
||
border-left-color: #1976d2;
|
||
}
|
||
|
||
.v-theme--dark .quoted-sender {
|
||
color: #64b5f6;
|
||
}
|
||
|
||
.v-theme--dark .quoted-text {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
/* Reaction chip styles */
|
||
.reaction-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 4px 8px;
|
||
border-radius: 12px;
|
||
font-weight: 600;
|
||
font-size: 0.75rem;
|
||
color: rgba(0, 0, 0, 0.8);
|
||
background-color: rgba(0, 0, 0, 0.05);
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.reaction-chip:hover {
|
||
background-color: rgba(0, 0, 0, 0.08);
|
||
border-color: rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.reaction-chip.like-chip {
|
||
color: #f44336; /* Red for like */
|
||
background-color: rgba(244, 67, 54, 0.1);
|
||
border-color: rgba(244, 67, 54, 0.2);
|
||
}
|
||
|
||
.reaction-chip.like-chip:hover {
|
||
background-color: rgba(244, 67, 54, 0.2);
|
||
border-color: rgba(244, 67, 54, 0.3);
|
||
}
|
||
|
||
.reaction-chip.dislike-chip {
|
||
color: #ff9800; /* Orange for dislike */
|
||
background-color: rgba(255, 152, 0, 0.1);
|
||
border-color: rgba(255, 152, 0, 0.2);
|
||
}
|
||
|
||
.reaction-chip.dislike-chip:hover {
|
||
background-color: rgba(255, 152, 0, 0.2);
|
||
border-color: rgba(255, 152, 0, 0.3);
|
||
}
|
||
|
||
.v-theme--dark .reaction-chip {
|
||
color: rgba(255, 255, 255, 0.8);
|
||
background-color: rgba(255, 255, 255, 0.05);
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.v-theme--dark .reaction-chip:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
border-color: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.v-theme--dark .reaction-chip.like-chip {
|
||
color: #ef5350; /* Darker red for dark mode */
|
||
background-color: rgba(239, 83, 80, 0.1);
|
||
border-color: rgba(239, 83, 80, 0.2);
|
||
}
|
||
|
||
.v-theme--dark .reaction-chip.like-chip:hover {
|
||
background-color: rgba(239, 83, 80, 0.2);
|
||
border-color: rgba(239, 83, 80, 0.3);
|
||
}
|
||
|
||
.v-theme--dark .reaction-chip.dislike-chip {
|
||
color: #ffa726; /* Darker orange for dark mode */
|
||
background-color: rgba(255, 167, 38, 0.1);
|
||
border-color: rgba(255, 167, 38, 0.2);
|
||
}
|
||
|
||
.v-theme--dark .reaction-chip.dislike-chip:hover {
|
||
background-color: rgba(255, 167, 38, 0.2);
|
||
border-color: rgba(255, 167, 38, 0.3);
|
||
}
|
||
|
||
/* Dialog Styles */
|
||
.create-channel-dialog,
|
||
.member-dialog,
|
||
.add-member-dialog {
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.dialog-header {
|
||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||
color: white;
|
||
padding: 24px;
|
||
position: relative;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.dialog-header::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.dialog-header > div {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.dialog-header h2 {
|
||
color: white !important;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.dialog-header p {
|
||
color: rgba(255, 255, 255, 0.9) !important;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
/* Dark theme support for dialog headers */
|
||
.v-theme--dark .dialog-header {
|
||
background: linear-gradient(135deg, #1565c0 0%, #0d47a1 100%);
|
||
}
|
||
|
||
.v-theme--dark .dialog-header h2 {
|
||
color: white !important;
|
||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.v-theme--dark .dialog-header p {
|
||
color: rgba(255, 255, 255, 0.95) !important;
|
||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.dialog-content {
|
||
padding: 24px;
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.dialog-actions {
|
||
padding: 16px 24px;
|
||
background-color: rgba(0, 0, 0, 0.02);
|
||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.add-member-btn {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.add-member-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
/* Member Stats Cards */
|
||
.member-stats {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.stat-card {
|
||
border-radius: 12px;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.stat-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.stat-card .v-card-text {
|
||
padding: 20px;
|
||
}
|
||
|
||
/* Member List */
|
||
.member-list {
|
||
background: transparent;
|
||
}
|
||
|
||
.member-item {
|
||
margin-bottom: 8px;
|
||
border-radius: 12px;
|
||
transition: all 0.3s ease;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.member-item:hover {
|
||
background-color: rgba(0, 0, 0, 0.04);
|
||
border-color: rgba(0, 0, 0, 0.08);
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
.remove-btn {
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.member-item:hover .remove-btn {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Search Results */
|
||
.search-results {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.search-result-list {
|
||
background: transparent;
|
||
}
|
||
|
||
.search-result-item {
|
||
margin-bottom: 8px;
|
||
border-radius: 12px;
|
||
transition: all 0.3s ease;
|
||
border: 1px solid transparent;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.search-result-item:hover {
|
||
background-color: rgba(0, 0, 0, 0.04);
|
||
border-color: rgba(0, 0, 0, 0.08);
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
/* Loading and Empty States */
|
||
.loading-members,
|
||
.searching-users,
|
||
.no-results,
|
||
.search-placeholder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.loading-members p,
|
||
.searching-users p {
|
||
color: rgba(0, 0, 0, 0.7);
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.no-results h4,
|
||
.search-placeholder h4 {
|
||
color: rgba(0, 0, 0, 0.6);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.no-results p,
|
||
.search-placeholder p {
|
||
color: rgba(0, 0, 0, 0.5);
|
||
max-width: 300px;
|
||
}
|
||
|
||
/* Dark Theme Support for Dialogs */
|
||
.v-theme--dark .dialog-actions {
|
||
background-color: rgba(255, 255, 255, 0.02);
|
||
border-top-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.v-theme--dark .stat-card:hover {
|
||
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.v-theme--dark .member-item:hover {
|
||
background-color: rgba(255, 255, 255, 0.04);
|
||
border-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.v-theme--dark .search-result-item:hover {
|
||
background-color: rgba(255, 255, 255, 0.04);
|
||
border-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.v-theme--dark .loading-members p,
|
||
.v-theme--dark .searching-users p {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.v-theme--dark .no-results h4,
|
||
.v-theme--dark .search-placeholder h4 {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.v-theme--dark .no-results p,
|
||
.v-theme--dark .search-placeholder p {
|
||
color: rgba(255, 255, 255, 0.5);
|
||
}
|
||
|
||
/* Mobile Responsive for Dialogs */
|
||
@media (max-width: 768px) {
|
||
.create-channel-dialog,
|
||
.member-dialog,
|
||
.add-member-dialog {
|
||
margin: 16px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.dialog-header {
|
||
padding: 20px;
|
||
}
|
||
|
||
.dialog-content {
|
||
padding: 20px;
|
||
max-height: 50vh;
|
||
}
|
||
|
||
.dialog-actions {
|
||
padding: 12px 20px;
|
||
}
|
||
|
||
.member-stats .v-row {
|
||
margin: 0;
|
||
}
|
||
|
||
.member-stats .v-col {
|
||
padding: 8px;
|
||
}
|
||
|
||
.stat-card .v-card-text {
|
||
padding: 16px;
|
||
}
|
||
|
||
.add-member-btn {
|
||
font-size: 0.875rem;
|
||
padding: 8px 16px;
|
||
}
|
||
}
|
||
|
||
/* Animation for dialog entrance */
|
||
.v-dialog .v-card {
|
||
animation: dialogSlideIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes dialogSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px) scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
/* Hover effects for interactive elements */
|
||
.v-btn:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.v-chip:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* Custom scrollbar for dialog content */
|
||
.dialog-content::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.dialog-content::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.dialog-content::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.dialog-content::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.v-theme--dark .dialog-content::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.v-theme--dark .dialog-content::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
</style> |