Reflex/src/views/ChatListView.vue

921 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="modern-chatlist-view">
<!-- Header Section -->
<div class="chatlist-header">
<div class="container">
<div class="header-content">
<div class="chatlist-title">
<h1 class="title-gradient">Мои диалоги</h1>
<p class="subtitle">Общение с вашими совпадениями</p>
</div>
</div>
</div>
</div>
<!-- Loading State -->
<div v-if="loading" class="loading-section">
<div class="loading-spinner">
<div class="spinner"></div>
<p>Загрузка диалогов...</p>
</div>
</div>
<!-- Error State -->
<div v-if="error" class="error-section">
<div class="container">
<div class="error-card">
<i class="bi-exclamation-triangle"></i>
<h3>Ошибка загрузки</h3>
<p>{{ error }}</p>
<button class="retry-btn" @click="fetchConversations">Попробовать снова</button>
</div>
</div>
</div>
<!-- Main Content -->
<div v-if="!loading && !error" class="main-content">
<div class="container">
<!-- Empty State -->
<div v-if="conversations.length === 0" class="empty-state">
<div class="empty-card">
<i class="bi-chat"></i>
<h3>У вас пока нет активных диалогов</h3>
<p>Лайкните кого-нибудь, чтобы начать общение!</p>
<router-link to="/swipe" class="action-btn primary">Перейти к свайпам</router-link>
</div>
</div>
<!-- Conversations List Card -->
<div v-if="conversations.length > 0" class="chat-card">
<div class="card-header">
<h3><i class="bi-chat-dots-fill"></i> Активные диалоги</h3>
<div class="header-actions">
<span class="chat-count">{{ conversations.length }} {{ getDialogsCountText(conversations.length) }}</span>
</div>
</div>
<div class="card-content">
<!-- Conversations Container -->
<div class="conversations-container">
<router-link
v-for="conversation in sortedConversations"
:key="conversation._id"
:to="{ name: 'ChatView', params: { conversationId: conversation._id } }"
class="conversation-card"
:class="{'has-unread': getUnreadCount(conversation) > 0}"
>
<!-- User Photo -->
<div class="user-photo-container">
<img
v-if="getOtherParticipant(conversation.participants)?.mainPhotoUrl"
:src="getParticipantAvatar(getOtherParticipant(conversation.participants))"
:alt="getOtherParticipant(conversation.participants)?.name || 'Фото пользователя'"
class="user-photo"
@error="onImageError($event, getOtherParticipant(conversation.participants))"
/>
<div v-else class="user-photo-placeholder">
<i class="bi-person"></i>
</div>
<span v-if="getUnreadCount(conversation) > 0" class="unread-badge">
{{ getUnreadCount(conversation) }}
</span>
</div>
<!-- Conversation Info -->
<div class="conversation-info">
<div class="conversation-header">
<h3 class="user-name">{{ getOtherParticipant(conversation.participants)?.name || 'Неизвестный пользователь' }}</h3>
<span class="timestamp">{{ formatTimestamp(conversation.lastMessage?.createdAt || conversation.updatedAt) }}</span>
</div>
<p class="last-message" :class="{'unread': getUnreadCount(conversation) > 0}">
{{ conversation.lastMessage ? formatLastMessage(conversation.lastMessage) : 'Нет сообщений' }}
</p>
</div>
<div class="conversation-action">
<i class="bi-chevron-right"></i>
</div>
</router-link>
</div>
</div>
</div>
</div>
</div>
<!-- No Data State -->
<div v-if="!isAuthenticated && !loading" class="not-authenticated-section">
<div class="container">
<div class="error-card">
<i class="bi-person-lock"></i>
<h3>Доступ ограничен</h3>
<p>Пожалуйста, войдите в систему, чтобы просматривать диалоги.</p>
<router-link to="/login" class="action-btn primary">Войти в систему</router-link>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, onUnmounted } from 'vue';
import api from '@/services/api';
import { useAuth } from '@/auth'; // Для получения ID текущего пользователя
import router from '@/router'; // Если понадобится редирект
import { getSocket, connectSocket } from '@/services/socketService';
const { user: currentUser, isAuthenticated } = useAuth(); // Получаем текущего пользователя
const conversations = ref([]);
const loading = ref(true);
const error = ref('');
// Добавляем ref для хранения количества непрочитанных сообщений
const unreadMessageCounts = ref({}); // { conversationId: count }
const defaultAvatar = '/img/default-avatar.png'; // Путь к заглушке аватара в public/img/
let socket = null; // Для прослушивания обновлений о прочтении
const fetchConversations = async () => {
loading.value = true;
error.value = '';
try {
console.log('[ChatListView] Запрос списка диалогов...');
const response = await api.getUserConversations();
conversations.value = response.data;
console.log('[ChatListView] Диалоги загружены:', conversations.value);
} catch (err) {
console.error('[ChatListView] Ошибка при загрузке диалогов:', err.response ? err.response.data : err.message);
error.value = (err.response?.data?.message) || 'Не удалось загрузить диалоги.';
if (err.response && err.response.status === 401) {
// Если 401, возможно, нужно перенаправить на логин
// const { logout } = useAuth(); // Если logout не импортирован
// await logout();
}
} finally {
loading.value = false;
}
};
// Сортируем диалоги по дате последнего сообщения/обновления (новые сверху)
const sortedConversations = computed(() => {
return [...conversations.value].sort((a, b) => {
// Бэкенд уже сортирует по updatedAt, но если lastMessageTimestamp есть, он точнее
const dateA = new Date(a.lastMessage?.createdAt || a.updatedAt);
const dateB = new Date(b.lastMessage?.createdAt || b.updatedAt);
return dateB - dateA;
});
});
// Обновленная функция для подсчета непрочитанных сообщений
const getUnreadCount = (conversation) => {
const countFromSocketLogic = unreadMessageCounts.value[conversation._id];
if (countFromSocketLogic && countFromSocketLogic > 0) {
return countFromSocketLogic;
}
// Fallback к старой логике, если новый счетчик 0 (или не инициализирован для старых чатов),
// но lastMessage не прочитано текущим пользователем.
if (currentUser.value && conversation.lastMessage &&
conversation.lastMessage.sender?._id !== currentUser.value._id &&
(!conversation.lastMessage.readBy || !conversation.lastMessage.readBy.includes(currentUser.value._id))) {
return 1; // Показываем, что есть хотя бы одно непрочитанное (последнее)
}
return 0;
};
// Получаем другого участника диалога
const getOtherParticipant = (participants) => {
if (!currentUser.value || !participants || participants.length < 2) return null;
return participants.find(p => p._id !== currentUser.value._id);
};
// Новая функция для получения URL аватара (исправленная)
const getParticipantAvatar = (participant) => {
if (participant && participant.mainPhotoUrl && typeof participant.mainPhotoUrl === 'string' && participant.mainPhotoUrl.trim() !== '') {
return participant.mainPhotoUrl.trim();
}
// Важно: если mainPhotoUrl нет или он невалиден, мы НЕ возвращаем defaultAvatar здесь,
// так как v-else в шаблоне должен показать заглушку <i class="bi bi-person-fill"></i>
// Однако, onImageError все еще может использовать defaultAvatar, если URL есть, но картинка не грузится.
return null; // или undefined, чтобы v-if="...mainPhotoUrl" работал как ожидается
};
// Форматирование последнего сообщения (кратко)
const formatLastMessage = (lastMsg) => {
if (!lastMsg || !lastMsg.text) return 'Нет сообщений';
const prefix = lastMsg.sender?._id === currentUser.value?._id ? 'Вы: ' : '';
return prefix + (lastMsg.text.length > 30 ? lastMsg.text.substring(0, 27) + '...' : lastMsg.text);
};
// Форматирование времени последнего сообщения/обновления
const formatTimestamp = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp);
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (date.toDateString() === yesterday.toDateString()) {
return 'Вчера';
} else {
return date.toLocaleDateString([], { day: '2-digit', month: 'short' });
}
};
const onImageError = (event, participantOnError) => {
console.warn("[ChatListView] Не удалось загрузить изображение:", event.target.src, "для пользователя:", participantOnError?.name);
// Устанавливаем src на заглушку из public/img, если фото не загрузилось.
// Это для случая, когда mainPhotoUrl был, но оказался битым.
event.target.src = defaultAvatar;
// Если хотим именно иконку, то нужно будет менять состояние, чтобы v-else сработало.
// Например, установить participantOnError.mainPhotoUrl = null;
// Это вызовет обновление DOM и показ блока v-else.
if (participantOnError) {
// Ищем диалог, где этот участник, и модифицируем его mainPhotoUrl,
// чтобы вызвать реактивное обновление и показ заглушки-иконки
const conversationToUpdate = conversations.value.find(conv =>
conv.participants.some(p => p._id === participantOnError._id)
);
if (conversationToUpdate) {
const participantInConversation = conversationToUpdate.participants.find(p => p._id === participantOnError._id);
if (participantInConversation) {
participantInConversation.mainPhotoUrl = null; // Это вызовет показ v-else
}
}
}
};
// Обработчик события messagesRead для обновления UI списка чатов
const handleMessagesReadInList = ({ conversationId: readConversationId, readerId }) => {
// Если текущий пользователь прочитал сообщения в каком-то чате (событие от ChatView через сервер)
// или если собеседник прочитал наши сообщения.
// Нам нужно обновить состояние `conversations` чтобы убрать индикатор непрочитанных.
const convIndex = conversations.value.findIndex(c => c._id === readConversationId);
if (convIndex > -1) {
const conv = conversations.value[convIndex];
// Если Я (currentUser) прочитал сообщения собеседника в этом диалоге
if (readerId === currentUser.value?._id && conv.lastMessage && conv.lastMessage.sender?._id !== currentUser.value?._id) {
// Очищаем счетчик непрочитанных для этого диалога
unreadMessageCounts.value[readConversationId] = 0;
if (!conv.lastMessage.readBy) conv.lastMessage.readBy = [];
if (!conv.lastMessage.readBy.includes(readerId)) {
conv.lastMessage.readBy.push(readerId);
// conversations.value[convIndex] = { ...conv }; // Вызываем реактивность
// Vue 3 должен автоматически отслеживать изменения в массиве/объекте
}
}
// Если Собеседник прочитал НАШИ сообщения (это больше для ChatView, но если хотим обновить тут что-то)
// В данном контексте (список чатов), это событие в основном означает, что МЫ прочитали,
// и если у нас был индикатор непрочитанных от собеседника, он должен исчезнуть.
// Перезапрос fetchConversations() может быть слишком тяжелым.
// Лучше, если сервер присылает обновленный объект диалога или мы мутируем локально.
console.log(`[ChatListView] Messages read in conversation ${readConversationId}, reader: ${readerId}. Updating UI.`);
// Форсируем обновление, если Vue не подхватывает изменение в глубине объекта
conversations.value = [...conversations.value];
}
};
onMounted(() => {
if (isAuthenticated.value) {
fetchConversations(); // unreadMessageCounts будет инициализирован или очищен внутри fetchConversations при необходимости
socket = getSocket();
if (!socket) {
socket = connectSocket();
}
if (socket) {
// Слушаем, если сообщения были прочитаны в каком-либо из диалогов
socket.on('messagesRead', ({ conversationId: readConversationId, readerId }) => {
// Это событие от сервера после того, как кто-то прочитал сообщения
// Нас интересует, когда ТЕКУЩИЙ пользователь прочитал сообщения в каком-то диалоге
if (readerId === currentUser.value?._id) {
console.log(`[ChatListView] Current user read messages in ${readConversationId}. Resetting unread count.`);
unreadMessageCounts.value[readConversationId] = 0;
}
// Обновляем состояние readBy для lastMessage для консистентности отображения
// и для корректной работы fallback-логики в getUnreadCount
const convIndex = conversations.value.findIndex(c => c._id === readConversationId);
if (convIndex > -1) {
const conv = conversations.value[convIndex];
if (conv.lastMessage) {
let changed = false;
if (readerId === currentUser.value?._id && conv.lastMessage.sender?._id !== currentUser.value?._id) { // Я прочитал сообщение другого
if (!conv.lastMessage.readBy) conv.lastMessage.readBy = [];
if (!conv.lastMessage.readBy.includes(readerId)) {
conv.lastMessage.readBy.push(readerId);
changed = true;
}
} else if (conv.lastMessage.sender?._id === currentUser.value?._id && readerId !== currentUser.value?._id) { // Другой прочитал мое сообщение
if (!conv.lastMessage.readBy) conv.lastMessage.readBy = [];
if (!conv.lastMessage.readBy.includes(readerId)) {
conv.lastMessage.readBy.push(readerId);
changed = true;
}
}
if (changed) {
// Форсируем обновление, если Vue не подхватывает глубокие изменения
conversations.value = [...conversations.value];
}
}
}
});
socket.on('getMessage', (newMessage) => {
const convIndex = conversations.value.findIndex(c => c._id === newMessage.conversationId);
if (convIndex > -1) {
const conversation = conversations.value[convIndex];
conversation.lastMessage = newMessage;
conversation.updatedAt = newMessage.createdAt; // Обновляем время для сортировки
// Если сообщение не от текущего пользователя и для этого диалога
if (newMessage.sender?._id !== currentUser.value?._id) {
unreadMessageCounts.value[newMessage.conversationId] =
(unreadMessageCounts.value[newMessage.conversationId] || 0) + 1;
}
// Пересортировываем массив диалогов, чтобы актуальный чат поднялся вверх
conversations.value.sort((a, b) => {
const dateA = new Date(a.lastMessage?.createdAt || a.updatedAt);
const dateB = new Date(b.lastMessage?.createdAt || b.updatedAt);
return dateB - dateA;
});
} else {
// Если пришло сообщение для НОВОГО диалога, которого еще нет в списке,
// лучшим решением будет перезапросить все диалоги, чтобы он корректно отобразился.
// Это может случиться, если диалог был создан другим пользователем только что.
console.log(`[ChatListView] Received message for a new or unknown conversation ${newMessage.conversationId}. Refetching conversations.`);
fetchConversations();
}
});
}
} else {
error.value = "Пожалуйста, войдите в систему, чтобы просматривать диалоги.";
loading.value = false;
// Можно перенаправить на логин здесь или положиться на router guard
// router.push('/login');
}
});
// Добавляем onUnmounted для отписки от событий сокета
onUnmounted(() => {
if (socket) {
socket.off('messagesRead'); // Используем то же имя события, что и в onMounted
socket.off('getMessage');
}
});
// Функция для определения правильного склонения слова "диалог"
const getDialogsCountText = (count) => {
const remainder10 = count % 10;
const remainder100 = count % 100;
if (remainder10 === 1 && remainder100 !== 11) {
return 'диалог';
} else if ([2, 3, 4].includes(remainder10) && ![12, 13, 14].includes(remainder100)) {
return 'диалога';
} else {
return 'диалогов';
}
};
</script>
<style scoped>
/* Global Reset and Base Styles */
.modern-chatlist-view {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed; /* Фиксирует фон для растяжения на весь экран */
background-size: cover;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
width: 100%;
overflow-x: hidden;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
width: 100%;
}
/* Header Section */
.chatlist-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding: 2rem 0;
color: white;
width: 100%;
margin: 0;
position: sticky;
top: 0;
z-index: 10;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.chatlist-title h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(45deg, #fff, #f0f0f0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.chatlist-title .subtitle {
margin: 0.5rem 0 0 0;
font-size: 1.1rem;
opacity: 0.9;
}
/* Loading State */
.loading-section {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
color: white;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-left: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error and Not Authenticated Sections */
.error-section, .not-authenticated-section {
padding: 4rem 0;
}
.error-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 3rem;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.error-card i {
font-size: 3rem;
color: #dc3545;
margin-bottom: 1rem;
}
.error-card h3 {
color: #495057;
margin-bottom: 1rem;
}
.retry-btn, .action-btn {
display: inline-block;
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 50px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
}
.retry-btn:hover, .action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
/* Main Content */
.main-content {
padding: 2rem 0 4rem;
}
/* Chat Card (New) */
.chat-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.card-header {
padding: 1.5rem 2rem;
background: linear-gradient(45deg, #f8f9fa, #e9ecef);
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h3 {
margin: 0;
color: #495057;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.chat-count {
font-size: 0.9rem;
color: #6c757d;
display: flex;
align-items: center;
padding: 0.3rem 0.8rem;
background: #f1f3f5;
border-radius: 30px;
}
.card-content {
padding: 1rem;
}
/* Empty State */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.empty-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 3rem;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
max-width: 500px;
width: 100%;
}
.empty-card i {
font-size: 3rem;
color: #6c757d;
margin-bottom: 1rem;
}
.empty-card h3 {
color: #495057;
margin-bottom: 1rem;
}
.empty-card p {
color: #6c757d;
margin-bottom: 1.5rem;
}
/* Conversations Container */
.conversations-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Conversation Card */
.conversation-card {
display: flex;
align-items: center;
gap: 1rem;
background: white;
border-radius: 16px;
padding: 1rem;
text-decoration: none;
color: inherit;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
position: relative;
}
.conversation-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.08);
background: #f9faff;
}
.conversation-card.has-unread {
background-color: rgba(102, 126, 234, 0.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
/* User Photo */
.user-photo-container {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border: 3px solid white;
background: linear-gradient(45deg, #e9ecef, #f8f9fa);
}
.user-photo {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.conversation-card:hover .user-photo {
transform: scale(1.1);
}
.user-photo-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #e9ecef, #f8f9fa);
transition: background 0.3s ease;
}
.conversation-card:hover .user-photo-placeholder {
background: linear-gradient(135deg, #dce1fc, #e9ecef);
}
.user-photo-placeholder i {
font-size: 1.8rem;
color: #adb5bd;
}
.unread-badge {
position: absolute;
top: -5px;
right: -5px;
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
font-size: 0.75rem;
font-weight: 700;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7);
}
70% {
box-shadow: 0 0 0 5px rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
}
}
/* Conversation Action */
.conversation-action {
margin-left: auto;
color: #ced4da;
font-size: 1.2rem;
display: flex;
align-items: center;
transition: all 0.3s ease;
}
.conversation-card:hover .conversation-action {
color: #667eea;
transform: translateX(3px);
}
/* Conversation Info */
.conversation-info {
flex-grow: 1;
min-width: 0; /* Для корректного переноса текста */
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.user-name {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 80px); /* Оставляем место для timestamp */
}
.conversation-card.has-unread .user-name {
color: #667eea;
}
.timestamp {
font-size: 0.8rem;
color: #6c757d;
font-weight: 500;
}
.last-message {
margin: 0;
font-size: 0.95rem;
color: #6c757d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.last-message.unread {
font-weight: 600;
color: #495057;
}
/* Responsive */
@media (max-width: 576px) {
.chatlist-title h1 {
font-size: 1.8rem;
}
.card-header {
padding: 1rem 1.25rem;
}
.card-content {
padding: 0.75rem;
}
.conversation-card {
padding: 0.75rem;
}
.user-photo-container {
width: 45px;
height: 45px;
}
.unread-badge {
min-width: 18px;
height: 18px;
font-size: 0.7rem;
}
.user-name {
font-size: 0.95rem;
max-width: calc(100% - 60px);
}
.timestamp {
font-size: 0.75rem;
}
.last-message {
font-size: 0.85rem;
}
.empty-chats-content p {
font-size: 0.95rem;
}
}
/* Малые телефоны */
@media (max-width: 375px) {
.chatlist-title h1 {
font-size: 1.6rem;
}
.card-header {
padding: 0.8rem 1rem;
}
.card-content {
padding: 0.5rem;
}
.conversation-card {
padding: 0.6rem;
}
.user-photo-container {
width: 40px;
height: 40px;
}
.user-name {
font-size: 0.9rem;
}
.timestamp {
font-size: 0.7rem;
}
.last-message {
font-size: 0.8rem;
}
}
/* Большие телефоны и малые планшеты */
@media (min-width: 577px) and (max-width: 767px) {
.container {
max-width: 540px;
}
.chatlist-card {
border-radius: 15px;
}
.conversation-card {
border-radius: 10px;
}
}
/* Планшеты */
@media (min-width: 768px) and (max-width: 991px) {
.container {
max-width: 720px;
}
}
/* Малые настольные */
@media (min-width: 992px) and (max-width: 1199px) {
.container {
max-width: 960px;
}
.chatlist-card {
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
}
/* Большие настольные */
@media (min-width: 1200px) {
.chatlist-card {
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
}
/* Ландшафтная ориентация на мобильных устройствах */
@media (max-height: 500px) and (orientation: landscape) {
.chatlist-header {
padding: 0.75rem 0;
}
.chatlist-title h1 {
font-size: 1.5rem;
}
.card-header {
padding: 0.75rem 1rem;
}
.conversations-list {
max-height: calc(100vh - 180px);
}
}
</style>