1654 lines
49 KiB
Vue
1654 lines
49 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 }} پیام
|
|||
|
|
</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 }} پیام
|
|||
|
|
</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">
|
|||
|
|
<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"
|
|||
|
|
>
|
|||
|
|
{{ emoji }} {{ users.length }}
|
|||
|
|
</v-chip>
|
|||
|
|
</div>
|
|||
|
|
</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">
|
|||
|
|
<v-text-field
|
|||
|
|
v-model="messageText"
|
|||
|
|
placeholder="پیام خود را بنویسید..."
|
|||
|
|
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="500px">
|
|||
|
|
<v-card>
|
|||
|
|
<v-card-title class="d-flex align-center">
|
|||
|
|
<v-icon start class="mr-2">mdi-plus-circle</v-icon>
|
|||
|
|
ایجاد کانال جدید
|
|||
|
|
</v-card-title>
|
|||
|
|
<v-card-text>
|
|||
|
|
<v-form ref="createChannelForm">
|
|||
|
|
<v-text-field
|
|||
|
|
v-model="newChannel.name"
|
|||
|
|
label="نام کانال"
|
|||
|
|
required
|
|||
|
|
:rules="[v => !!v || 'نام کانال الزامی است']"
|
|||
|
|
></v-text-field>
|
|||
|
|
<v-textarea
|
|||
|
|
v-model="newChannel.description"
|
|||
|
|
label="توضیحات"
|
|||
|
|
rows="3"
|
|||
|
|
placeholder="توضیح کوتاهی درباره کانال بنویسید..."
|
|||
|
|
></v-textarea>
|
|||
|
|
<v-switch
|
|||
|
|
v-model="newChannel.isPublic"
|
|||
|
|
label="کانال عمومی"
|
|||
|
|
color="primary"
|
|||
|
|
inset
|
|||
|
|
></v-switch>
|
|||
|
|
<div class="text-caption text-grey-darken-1 mt-2">
|
|||
|
|
کانالهای عمومی برای همه قابل مشاهده و پیوستن هستند
|
|||
|
|
</div>
|
|||
|
|
</v-form>
|
|||
|
|
</v-card-text>
|
|||
|
|
<v-card-actions>
|
|||
|
|
<v-btn color="primary" @click="createChannel" :loading="creatingChannel">
|
|||
|
|
ایجاد
|
|||
|
|
</v-btn>
|
|||
|
|
<v-btn @click="showCreateChannel = false">لغو</v-btn>
|
|||
|
|
</v-card-actions>
|
|||
|
|
</v-card>
|
|||
|
|
</v-dialog>
|
|||
|
|
|
|||
|
|
<!-- Member Management Dialog -->
|
|||
|
|
<v-dialog v-model="showMemberDialog" max-width="600px">
|
|||
|
|
<v-card>
|
|||
|
|
<v-card-title class="d-flex align-center">
|
|||
|
|
<v-icon start class="mr-2">mdi-account-group</v-icon>
|
|||
|
|
مدیریت اعضای کانال
|
|||
|
|
<v-spacer></v-spacer>
|
|||
|
|
<v-btn
|
|||
|
|
v-if="currentChannel?.isAdmin"
|
|||
|
|
color="primary"
|
|||
|
|
size="small"
|
|||
|
|
@click="showAddMemberDialog = true"
|
|||
|
|
>
|
|||
|
|
<v-icon start>mdi-plus</v-icon>
|
|||
|
|
اضافه کردن عضو
|
|||
|
|
</v-btn>
|
|||
|
|
</v-card-title>
|
|||
|
|
<v-card-text>
|
|||
|
|
<div v-if="loadingMembers" class="text-center pa-4">
|
|||
|
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
|||
|
|
<p class="mt-2">در حال بارگذاری اعضا...</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else>
|
|||
|
|
<div class="d-flex align-center mb-4">
|
|||
|
|
<v-icon class="mr-2">mdi-account-group</v-icon>
|
|||
|
|
<span class="text-subtitle-1">{{ channelMembers.length }} عضو</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<v-list>
|
|||
|
|
<v-list-item
|
|||
|
|
v-for="member in channelMembers"
|
|||
|
|
:key="member.id"
|
|||
|
|
class="member-item"
|
|||
|
|
>
|
|||
|
|
<template v-slot:prepend>
|
|||
|
|
<v-avatar size="40" color="primary" class="mr-3">
|
|||
|
|
<span class="text-white">{{ member.fullName.charAt(0) }}</span>
|
|||
|
|
</v-avatar>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<v-list-item-title class="font-weight-medium">
|
|||
|
|
{{ member.fullName }}
|
|||
|
|
<v-chip
|
|||
|
|
v-if="member.isAdmin"
|
|||
|
|
size="small"
|
|||
|
|
color="orange"
|
|||
|
|
class="ml-2"
|
|||
|
|
>
|
|||
|
|
مدیر
|
|||
|
|
</v-chip>
|
|||
|
|
</v-list-item-title>
|
|||
|
|
|
|||
|
|
<v-list-item-subtitle class="text-caption">
|
|||
|
|
{{ member.email }}
|
|||
|
|
</v-list-item-subtitle>
|
|||
|
|
|
|||
|
|
<template v-slot:append>
|
|||
|
|
<v-btn
|
|||
|
|
v-if="currentChannel?.isAdmin && !member.isAdmin"
|
|||
|
|
icon
|
|||
|
|
size="small"
|
|||
|
|
color="error"
|
|||
|
|
@click="removeMemberFromChannel(member.id)"
|
|||
|
|
>
|
|||
|
|
<v-icon>mdi-delete</v-icon>
|
|||
|
|
</v-btn>
|
|||
|
|
</template>
|
|||
|
|
</v-list-item>
|
|||
|
|
</v-list>
|
|||
|
|
</div>
|
|||
|
|
</v-card-text>
|
|||
|
|
<v-card-actions>
|
|||
|
|
<v-btn @click="showMemberDialog = false">بستن</v-btn>
|
|||
|
|
</v-card-actions>
|
|||
|
|
</v-card>
|
|||
|
|
</v-dialog>
|
|||
|
|
|
|||
|
|
<!-- Add Member Dialog -->
|
|||
|
|
<v-dialog v-model="showAddMemberDialog" max-width="500px">
|
|||
|
|
<v-card>
|
|||
|
|
<v-card-title class="d-flex align-center">
|
|||
|
|
<v-icon start class="mr-2">mdi-account-plus</v-icon>
|
|||
|
|
اضافه کردن عضو جدید
|
|||
|
|
</v-card-title>
|
|||
|
|
<v-card-text>
|
|||
|
|
<v-text-field
|
|||
|
|
v-model="memberSearch"
|
|||
|
|
label="جستجوی کاربر"
|
|||
|
|
placeholder="نام یا ایمیل کاربر را وارد کنید..."
|
|||
|
|
prepend-inner-icon="mdi-magnify"
|
|||
|
|
variant="outlined"
|
|||
|
|
@input="searchUsersForMember"
|
|||
|
|
></v-text-field>
|
|||
|
|
|
|||
|
|
<div v-if="searchingUsers" class="text-center pa-4">
|
|||
|
|
<v-progress-circular indeterminate color="primary" size="20"></v-progress-circular>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="searchResults.length > 0" class="mt-4">
|
|||
|
|
<v-list>
|
|||
|
|
<v-list-item
|
|||
|
|
v-for="user in searchResults"
|
|||
|
|
:key="user.id"
|
|||
|
|
@click="addMemberToChannel(user.id)"
|
|||
|
|
class="search-result-item"
|
|||
|
|
>
|
|||
|
|
<template v-slot:prepend>
|
|||
|
|
<v-avatar size="36" 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">
|
|||
|
|
{{ user.email }}
|
|||
|
|
</v-list-item-subtitle>
|
|||
|
|
|
|||
|
|
<template v-slot:append>
|
|||
|
|
<v-btn
|
|||
|
|
icon
|
|||
|
|
size="small"
|
|||
|
|
color="primary"
|
|||
|
|
>
|
|||
|
|
<v-icon>mdi-plus</v-icon>
|
|||
|
|
</v-btn>
|
|||
|
|
</template>
|
|||
|
|
</v-list-item>
|
|||
|
|
</v-list>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="memberSearch && !searchingUsers" class="text-center pa-4">
|
|||
|
|
<p class="text-grey-lighten-1">کاربری یافت نشد</p>
|
|||
|
|
</div>
|
|||
|
|
</v-card-text>
|
|||
|
|
<v-card-actions>
|
|||
|
|
<v-btn @click="showAddMemberDialog = false">لغو</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('');
|
|||
|
|
|
|||
|
|
// Emoji picker
|
|||
|
|
const showEmojiPicker = ref(false);
|
|||
|
|
const commonEmojis = ref([
|
|||
|
|
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇',
|
|||
|
|
'🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚',
|
|||
|
|
'😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩',
|
|||
|
|
'🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣',
|
|||
|
|
'😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬',
|
|||
|
|
'🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗',
|
|||
|
|
'🤔', '🤭', '🤫', '🤥', '😶', '😐', '😑', '😯', '😦', '😧',
|
|||
|
|
'😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '🤐', '🥴', '🤢',
|
|||
|
|
'🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠', '💩', '👻', '💀',
|
|||
|
|
'☠️', '👽', '👾', '🤖', '😺', '😸', '😹', '😻', '😼', '😽',
|
|||
|
|
'🙀', '😿', '😾', '🙈', '🙉', '🙊', '💌', '💘', '💝', '💖',
|
|||
|
|
'💗', '💙', '💚', '❣️', '💕', '💞', '💓', '💗', '💖', '💘',
|
|||
|
|
'💝', '💟', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
|
|||
|
|
'🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝',
|
|||
|
|
'💟', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎'
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// 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
|
|||
|
|
|
|||
|
|
// Current user (mock for now)
|
|||
|
|
const currentUser = ref({
|
|||
|
|
id: 1,
|
|||
|
|
fullName: 'کاربر فعلی',
|
|||
|
|
email: 'user@example.com'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 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;
|
|||
|
|
await loadChannelMessages(channel.channelId);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
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();
|
|||
|
|
|
|||
|
|
// 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 = [];
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error leaving channel:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const loadChannelMessages = async (channelId, isAutoRefresh = false) => {
|
|||
|
|
try {
|
|||
|
|
if (!isAutoRefresh) {
|
|||
|
|
loading.value = true;
|
|||
|
|
}
|
|||
|
|
const response = await axios.get(`/api/chat/channels/${channelId}/messages`);
|
|||
|
|
if (response.data.success) {
|
|||
|
|
const newMessages = response.data.data.reverse(); // Show newest first
|
|||
|
|
|
|||
|
|
// 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 {
|
|||
|
|
messages.value = newMessages;
|
|||
|
|
await nextTick();
|
|||
|
|
scrollToBottom();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error loading messages:', error);
|
|||
|
|
} finally {
|
|||
|
|
if (!isAutoRefresh) {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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 response = await axios.post(`/api/chat/channels/${currentChannel.value.channelId}/messages`, {
|
|||
|
|
content: messageText.value,
|
|||
|
|
messageType: 'text'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (response.data.success) {
|
|||
|
|
const newMessage = response.data.data;
|
|||
|
|
messages.value.push(newMessage);
|
|||
|
|
messageText.value = '';
|
|||
|
|
|
|||
|
|
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 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 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);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 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,
|
|||
|
|
sendMessage,
|
|||
|
|
createChannel,
|
|||
|
|
selectChannel,
|
|||
|
|
joinChannel,
|
|||
|
|
leaveChannel,
|
|||
|
|
searchChannels,
|
|||
|
|
insertEmoji,
|
|||
|
|
refreshChannels,
|
|||
|
|
loadChannelMembers,
|
|||
|
|
searchUsersForMember,
|
|||
|
|
addMemberToChannel,
|
|||
|
|
removeMemberFromChannel,
|
|||
|
|
updateUserChannels,
|
|||
|
|
formatTime,
|
|||
|
|
formatDate,
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
</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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
</style>
|
|||
|
|
|