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

1654 lines
49 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">
{{ 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>