hesabixCore/webUI/src/views/chat/home.vue

2798 lines
83 KiB
Vue
Raw Normal View History

2025-08-03 16:08:15 +03:30
<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">
2025-08-04 17:01:07 +03:30
{{ channel.messageCount }} پیام {{ channel.memberCount || 0 }} عضو
2025-08-03 16:08:15 +03:30
</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">
2025-08-04 17:01:07 +03:30
{{ channel.messageCount }} پیام {{ channel.memberCount || 0 }} عضو
2025-08-03 16:08:15 +03:30
</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 -->
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
<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"
2025-08-04 17:01:07 +03:30
class="mr-1 reaction-chip"
:class="{
'like-chip': emoji === '❤️',
'dislike-chip': emoji === '👎'
}"
2025-08-03 16:08:15 +03:30
>
2025-08-04 17:01:07 +03:30
<v-icon size="14" class="mr-1">
{{ emoji === '❤️' ? 'mdi-heart' : emoji === '👎' ? 'mdi-thumb-down' : emoji }}
</v-icon>
{{ users.length }}
2025-08-03 16:08:15 +03:30
</v-chip>
</div>
</div>
2025-08-04 17:01:07 +03:30
<!-- 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>
2025-08-03 16:08:15 +03:30
</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">
2025-08-04 17:01:07 +03:30
<!-- 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>
2025-08-03 16:08:15 +03:30
<v-text-field
v-model="messageText"
2025-08-04 17:01:07 +03:30
:placeholder="quotedMessage ? 'پاسخ خود را بنویسید...' : 'پیام خود را بنویسید...'"
2025-08-03 16:08:15 +03:30
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 -->
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</v-card-title>
2025-08-04 17:01:07 +03:30
<v-card-text class="dialog-content">
<v-form ref="createChannelForm" class="mt-4">
2025-08-03 16:08:15 +03:30
<v-text-field
v-model="newChannel.name"
label="نام کانال"
2025-08-04 17:01:07 +03:30
placeholder="مثال: گروه عمومی"
2025-08-03 16:08:15 +03:30
required
:rules="[v => !!v || 'نام کانال الزامی است']"
2025-08-04 17:01:07 +03:30
variant="outlined"
density="comfortable"
prepend-inner-icon="mdi-format-title"
class="mb-4"
2025-08-03 16:08:15 +03:30
></v-text-field>
2025-08-04 17:01:07 +03:30
2025-08-03 16:08:15 +03:30
<v-textarea
v-model="newChannel.description"
2025-08-04 17:01:07 +03:30
label="توضیحات کانال"
placeholder="توضیح کوتاهی درباره هدف و موضوع کانال بنویسید..."
rows="4"
variant="outlined"
density="comfortable"
prepend-inner-icon="mdi-text"
class="mb-4"
2025-08-03 16:08:15 +03:30
></v-textarea>
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</v-form>
</v-card-text>
2025-08-04 17:01:07 +03:30
<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>
ایجاد کانال
2025-08-03 16:08:15 +03:30
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Member Management Dialog -->
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</v-card-title>
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</div>
<div v-else>
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</div>
2025-08-04 17:01:07 +03:30
<v-list class="member-list">
2025-08-03 16:08:15 +03:30
<v-list-item
v-for="member in channelMembers"
:key="member.id"
class="member-item"
2025-08-04 17:01:07 +03:30
rounded="lg"
2025-08-03 16:08:15 +03:30
>
<template v-slot:prepend>
2025-08-04 17:01:07 +03:30
<v-avatar size="48" color="primary" class="mr-3">
<span class="text-white text-h6">{{ member.fullName.charAt(0) }}</span>
2025-08-03 16:08:15 +03:30
</v-avatar>
</template>
2025-08-04 17:01:07 +03:30
<v-list-item-title class="font-weight-medium text-body-1">
2025-08-03 16:08:15 +03:30
{{ member.fullName }}
<v-chip
v-if="member.isAdmin"
size="small"
color="orange"
2025-08-04 17:01:07 +03:30
variant="elevated"
2025-08-03 16:08:15 +03:30
class="ml-2"
>
2025-08-04 17:01:07 +03:30
<v-icon start size="14">mdi-crown</v-icon>
2025-08-03 16:08:15 +03:30
مدیر
</v-chip>
</v-list-item-title>
2025-08-04 17:01:07 +03:30
<v-list-item-subtitle class="text-caption mt-1">
<v-icon size="14" class="mr-1">mdi-email</v-icon>
{{ maskEmail(member.email) }}
2025-08-03 16:08:15 +03:30
</v-list-item-subtitle>
<template v-slot:append>
<v-btn
v-if="currentChannel?.isAdmin && !member.isAdmin"
icon
size="small"
color="error"
2025-08-04 17:01:07 +03:30
variant="text"
2025-08-03 16:08:15 +03:30
@click="removeMemberFromChannel(member.id)"
2025-08-04 17:01:07 +03:30
class="remove-btn"
2025-08-03 16:08:15 +03:30
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
</v-list-item>
</v-list>
</div>
</v-card-text>
2025-08-04 17:01:07 +03:30
<v-card-actions class="dialog-actions">
<v-btn @click="showMemberDialog = false" variant="outlined" size="large">
بستن
</v-btn>
2025-08-03 16:08:15 +03:30
</v-card-actions>
</v-card>
</v-dialog>
<!-- Add Member Dialog -->
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</v-card-title>
2025-08-04 17:01:07 +03:30
<v-card-text class="dialog-content">
2025-08-03 16:08:15 +03:30
<v-text-field
v-model="memberSearch"
label="جستجوی کاربر"
placeholder="نام یا ایمیل کاربر را وارد کنید..."
prepend-inner-icon="mdi-magnify"
variant="outlined"
2025-08-04 17:01:07 +03:30
density="comfortable"
2025-08-03 16:08:15 +03:30
@input="searchUsersForMember"
2025-08-04 17:01:07 +03:30
class="mb-4"
2025-08-03 16:08:15 +03:30
></v-text-field>
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</div>
2025-08-04 17:01:07 +03:30
<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">
2025-08-03 16:08:15 +03:30
<v-list-item
v-for="user in searchResults"
:key="user.id"
@click="addMemberToChannel(user.id)"
class="search-result-item"
2025-08-04 17:01:07 +03:30
rounded="lg"
2025-08-03 16:08:15 +03:30
>
<template v-slot:prepend>
2025-08-04 17:01:07 +03:30
<v-avatar size="40" color="primary" class="mr-3">
2025-08-03 16:08:15 +03:30
<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">
2025-08-04 17:01:07 +03:30
<v-icon size="14" class="mr-1">mdi-email</v-icon>
{{ maskEmail(user.email) }}
2025-08-03 16:08:15 +03:30
</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon
size="small"
color="primary"
2025-08-04 17:01:07 +03:30
variant="text"
2025-08-03 16:08:15 +03:30
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
</v-list-item>
</v-list>
</div>
2025-08-04 17:01:07 +03:30
<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>
2025-08-03 16:08:15 +03:30
</div>
</v-card-text>
2025-08-04 17:01:07 +03:30
<v-card-actions class="dialog-actions">
<v-btn @click="showAddMemberDialog = false" variant="outlined" size="large">
لغو
</v-btn>
2025-08-03 16:08:15 +03:30
</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('');
2025-08-04 17:01:07 +03:30
// Pagination state
const pagination = ref({
limit: 30,
offset: 0,
hasMore: true,
loadingMore: false,
totalMessages: 0
});
2025-08-03 16:08:15 +03:30
// Emoji picker
const showEmojiPicker = ref(false);
const commonEmojis = ref([
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇',
'🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚',
'😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩',
'🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣',
'😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬',
'🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗',
'🤔', '🤭', '🤫', '🤥', '😶', '😐', '😑', '😯', '😦', '😧',
'😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '🤐', '🥴', '🤢',
'🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠', '💩', '👻', '💀',
'☠️', '👽', '👾', '🤖', '😺', '😸', '😹', '😻', '😼', '😽',
'🙀', '😿', '😾', '🙈', '🙉', '🙊', '💌', '💘', '💝', '💖',
'💗', '💙', '💚', '❣️', '💕', '💞', '💓', '💗', '💖', '💘',
'💝', '💟', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
'🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝',
'💟', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎'
]);
2025-08-04 17:01:07 +03:30
// 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());
2025-08-03 16:08:15 +03:30
// 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;
2025-08-04 17:01:07 +03:30
// Reset pagination when switching channels
pagination.value = {
limit: 30,
offset: 0,
hasMore: true,
loadingMore: false,
totalMessages: 0
};
2025-08-03 16:08:15 +03:30
await loadChannelMessages(channel.channelId);
2025-08-04 17:01:07 +03:30
// Update channel stats when selecting a channel
await updateChannelStats();
2025-08-03 16:08:15 +03:30
};
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();
2025-08-04 17:01:07 +03:30
// Update channel stats after joining
await updateChannelStats();
2025-08-03 16:08:15 +03:30
// 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 = [];
2025-08-04 17:01:07 +03:30
// Update channel stats after leaving
if (currentChannel.value) {
await updateChannelStats();
}
2025-08-03 16:08:15 +03:30
}
} catch (error) {
console.error('Error leaving channel:', error);
}
};
2025-08-04 17:01:07 +03:30
const loadChannelMessages = async (channelId, isAutoRefresh = false, isLoadMore = false) => {
2025-08-03 16:08:15 +03:30
try {
2025-08-04 17:01:07 +03:30
if (!isAutoRefresh && !isLoadMore) {
2025-08-03 16:08:15 +03:30
loading.value = true;
}
2025-08-04 17:01:07 +03:30
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}`);
2025-08-03 16:08:15 +03:30
if (response.data.success) {
const newMessages = response.data.data.reverse(); // Show newest first
2025-08-04 17:01:07 +03:30
const paginationInfo = response.data.pagination;
2025-08-03 16:08:15 +03:30
// 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();
}
2025-08-04 17:01:07 +03:30
} 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;
}
2025-08-03 16:08:15 +03:30
} else {
2025-08-04 17:01:07 +03:30
// Initial load
2025-08-03 16:08:15 +03:30
messages.value = newMessages;
2025-08-04 17:01:07 +03:30
pagination.value.offset = paginationInfo.offset + pagination.value.limit;
pagination.value.hasMore = paginationInfo.hasMore;
pagination.value.totalMessages = paginationInfo.total;
2025-08-03 16:08:15 +03:30
await nextTick();
scrollToBottom();
}
}
} catch (error) {
console.error('Error loading messages:', error);
} finally {
2025-08-04 17:01:07 +03:30
if (!isAutoRefresh && !isLoadMore) {
2025-08-03 16:08:15 +03:30
loading.value = false;
}
2025-08-04 17:01:07 +03:30
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();
2025-08-03 16:08:15 +03:30
}
};
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 {
2025-08-04 17:01:07 +03:30
const messageData = {
2025-08-03 16:08:15 +03:30
content: messageText.value,
messageType: 'text'
2025-08-04 17:01:07 +03:30
};
// 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);
2025-08-03 16:08:15 +03:30
if (response.data.success) {
const newMessage = response.data.data;
messages.value.push(newMessage);
messageText.value = '';
2025-08-04 17:01:07 +03:30
quotedMessage.value = null; // Clear quoted message after sending
// Update channel stats after sending message
await updateChannelStats();
2025-08-03 16:08:15 +03:30
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;
}
};
2025-08-04 17:01:07 +03:30
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);
}
};
2025-08-03 16:08:15 +03:30
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;
}
};
2025-08-04 17:01:07 +03:30
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);
}
};
2025-08-03 16:08:15 +03:30
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);
};
2025-08-04 17:01:07 +03:30
// 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('.')}`;
};
2025-08-03 16:08:15 +03:30
// 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,
2025-08-04 17:01:07 +03:30
pagination,
2025-08-03 16:08:15 +03:30
sendMessage,
createChannel,
selectChannel,
joinChannel,
leaveChannel,
searchChannels,
insertEmoji,
refreshChannels,
loadChannelMembers,
searchUsersForMember,
addMemberToChannel,
removeMemberFromChannel,
updateUserChannels,
2025-08-04 17:01:07 +03:30
updateChannelStats,
2025-08-03 16:08:15 +03:30
formatTime,
formatDate,
2025-08-04 17:01:07 +03:30
quotedMessage,
showMessageActions,
quoteMessage,
replyToMessage,
cancelQuote,
isLiked,
isDisliked,
toggleLike,
toggleDislike,
reactingMessages,
handleScroll,
loadMoreMessages,
maskEmail,
2025-08-03 16:08:15 +03:30
};
},
});
</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);
}
2025-08-04 17:01:07 +03:30
/* 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);
}
2025-08-03 16:08:15 +03:30
/* 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);
}
2025-08-04 17:01:07 +03:30
/* 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);
}
2025-08-03 16:08:15 +03:30
2025-08-04 17:01:07 +03:30
.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;
}
}
2025-08-03 16:08:15 +03:30
2025-08-04 17:01:07 +03:30
/* 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>