новый функционал для редактирования сообщений
This commit is contained in:
parent
7146611dc3
commit
e147959928
@ -289,10 +289,90 @@ const deleteMessage = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @desc Редактировать сообщение
|
||||||
|
// @route PUT /api/conversations/:conversationId/messages/:messageId
|
||||||
|
// @access Private
|
||||||
|
const editMessage = async (req, res, next) => {
|
||||||
|
const { conversationId, messageId } = req.params;
|
||||||
|
const { text } = req.body;
|
||||||
|
const currentUserId = req.user._id;
|
||||||
|
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
const error = new Error('Текст сообщения не может быть пустым.');
|
||||||
|
error.statusCode = 400;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversation = await Conversation.findById(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
const error = new Error('Диалог не найден.');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conversation.participants.includes(currentUserId)) {
|
||||||
|
const error = new Error('Вы не являетесь участником этого диалога.');
|
||||||
|
error.statusCode = 403;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await Message.findById(messageId);
|
||||||
|
if (!message) {
|
||||||
|
const error = new Error('Сообщение не найдено.');
|
||||||
|
error.statusCode = 404;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.sender.equals(currentUserId)) {
|
||||||
|
const error = new Error('Вы можете редактировать только свои сообщения.');
|
||||||
|
error.statusCode = 403;
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ограничение по времени на редактирование (например, 24 часа)
|
||||||
|
// const twentyFourHours = 24 * 60 * 60 * 1000;
|
||||||
|
// if (new Date() - new Date(message.createdAt) > twentyFourHours) {
|
||||||
|
// const error = new Error('Сообщение не может быть отредактировано по истечении 24 часов.');
|
||||||
|
// error.statusCode = 403;
|
||||||
|
// return next(error);
|
||||||
|
// }
|
||||||
|
|
||||||
|
message.text = text.trim();
|
||||||
|
message.isEdited = true;
|
||||||
|
message.editedAt = new Date();
|
||||||
|
|
||||||
|
const updatedMessage = await message.save();
|
||||||
|
|
||||||
|
// Загружаем данные отправителя для ответа клиенту
|
||||||
|
await updatedMessage.populate('sender', 'name photos _id');
|
||||||
|
|
||||||
|
|
||||||
|
// Эмитим событие редактирования сообщения в комнату диалога
|
||||||
|
const io = req.app.get('io');
|
||||||
|
if (io) {
|
||||||
|
io.to(conversationId).emit('messageEdited', {
|
||||||
|
message: updatedMessage.toObject(), // Отправляем обновленное сообщение целиком
|
||||||
|
conversationId,
|
||||||
|
});
|
||||||
|
console.log(`[CONV_CTRL] Emitted messageEdited to room ${conversationId} for message ${messageId}`);
|
||||||
|
} else {
|
||||||
|
console.warn('[CONV_CTRL] Socket.io instance (io) not found on req.app. Could not emit messageEdited.');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(updatedMessage);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CONV_CTRL] Ошибка при редактировании сообщения ${messageId} из диалога ${conversationId}:`, error.message);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createOrGetConversation,
|
createOrGetConversation,
|
||||||
getUserConversations,
|
getUserConversations,
|
||||||
getMessagesForConversation,
|
getMessagesForConversation,
|
||||||
deleteMessage, // <-- Добавляем новый метод
|
deleteMessage,
|
||||||
|
editMessage, // <-- Добавляем новый метод
|
||||||
};
|
};
|
@ -26,6 +26,13 @@ const messageSchema = new mongoose.Schema(
|
|||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
ref: 'User'
|
ref: 'User'
|
||||||
}],
|
}],
|
||||||
|
isEdited: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
editedAt: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
// Или просто флаг для 1-на-1 чата
|
// Или просто флаг для 1-на-1 чата
|
||||||
// isRead: {
|
// isRead: {
|
||||||
// type: Boolean,
|
// type: Boolean,
|
||||||
|
@ -5,7 +5,8 @@ const {
|
|||||||
createOrGetConversation,
|
createOrGetConversation,
|
||||||
getUserConversations,
|
getUserConversations,
|
||||||
getMessagesForConversation,
|
getMessagesForConversation,
|
||||||
deleteMessage // Импортируем новый контроллер
|
deleteMessage, // Импортируем новый контроллер
|
||||||
|
editMessage // Импортируем контроллер редактирования
|
||||||
} = require('../controllers/conversationController');
|
} = require('../controllers/conversationController');
|
||||||
const { protect } = require('../middleware/authMiddleware');
|
const { protect } = require('../middleware/authMiddleware');
|
||||||
|
|
||||||
@ -23,4 +24,9 @@ router.route('/:conversationId/messages')
|
|||||||
router.route('/:conversationId/messages/:messageId')
|
router.route('/:conversationId/messages/:messageId')
|
||||||
.delete(deleteMessage); // DELETE /api/conversations/:conversationId/messages/:messageId (удалить сообщение)
|
.delete(deleteMessage); // DELETE /api/conversations/:conversationId/messages/:messageId (удалить сообщение)
|
||||||
|
|
||||||
|
// Маршрут для редактирования сообщения
|
||||||
|
router.route('/:conversationId/messages/:messageId')
|
||||||
|
.put(editMessage); // PUT /api/conversations/:conversationId/messages/:messageId (редактировать сообщение)
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
@ -103,5 +103,8 @@ export default {
|
|||||||
},
|
},
|
||||||
deleteMessage(conversationId, messageId) {
|
deleteMessage(conversationId, messageId) {
|
||||||
return apiClient.delete(`/conversations/${conversationId}/messages/${messageId}`);
|
return apiClient.delete(`/conversations/${conversationId}/messages/${messageId}`);
|
||||||
|
},
|
||||||
|
editMessage(conversationId, messageId, newText) {
|
||||||
|
return apiClient.put(`/conversations/${conversationId}/messages/${messageId}`, { text: newText });
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -33,6 +33,7 @@
|
|||||||
<p class="mb-0">{{ item.data.text }}</p>
|
<p class="mb-0">{{ item.data.text }}</p>
|
||||||
<small class="message-timestamp text-muted">
|
<small class="message-timestamp text-muted">
|
||||||
{{ formatMessageTimestamp(item.data.createdAt) }}
|
{{ formatMessageTimestamp(item.data.createdAt) }}
|
||||||
|
<span v-if="item.data.isEdited && item.data.sender?._id === currentUser?._id" class="ms-1 fst-italic">(изменено)</span>
|
||||||
<span v-if="item.data.sender?._id === currentUser?._id && item.data.isSending" class="ms-1 fst-italic">(отправка...)</span>
|
<span v-if="item.data.sender?._id === currentUser?._id && item.data.isSending" class="ms-1 fst-italic">(отправка...)</span>
|
||||||
<span v-if="item.data.sender?._id === currentUser?._id && !item.data.isSending" class="ms-1">
|
<span v-if="item.data.sender?._id === currentUser?._id && !item.data.isSending" class="ms-1">
|
||||||
<i :class="getMessageStatusIcon(item.data)"></i>
|
<i :class="getMessageStatusIcon(item.data)"></i>
|
||||||
@ -41,12 +42,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Кнопка удаления для ПК (появляется при наведении) -->
|
<!-- Кнопка удаления для ПК (появляется при наведении) -->
|
||||||
<button v-if="!isTouchDevice.value && hoveredMessageId === item.data._id && item.data.sender?._id === currentUser?._id"
|
<button v-if="!isTouchDevice.value && hoveredMessageId === item.data._id && item.data.sender?._id === currentUser?._id && !editingMessageId"
|
||||||
@click="confirmDeleteMessage(item.data._id)"
|
@click="confirmDeleteMessage(item.data._id)"
|
||||||
class="btn btn-sm btn-danger delete-message-btn"
|
class="btn btn-sm btn-danger delete-message-btn"
|
||||||
title="Удалить сообщение">
|
title="Удалить сообщение">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="!isTouchDevice.value && hoveredMessageId === item.data._id && item.data.sender?._id === currentUser?._id && !editingMessageId"
|
||||||
|
@click="startEditMessage(item.data._id)"
|
||||||
|
class="btn btn-sm btn-info edit-message-btn"
|
||||||
|
title="Редактировать сообщение">
|
||||||
|
<i class="bi bi-pencil-fill"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -57,6 +64,10 @@
|
|||||||
class="context-menu"
|
class="context-menu"
|
||||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }">
|
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }">
|
||||||
<ul>
|
<ul>
|
||||||
|
<li @click="startEditMessage(contextMenu.messageId)">
|
||||||
|
<i class="bi bi-pencil-fill"></i>
|
||||||
|
<span>Редактировать</span>
|
||||||
|
</li>
|
||||||
<li @click="confirmDeleteMessage(contextMenu.messageId)">
|
<li @click="confirmDeleteMessage(contextMenu.messageId)">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
<span>Удалить сообщение</span>
|
<span>Удалить сообщение</span>
|
||||||
@ -70,17 +81,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isAuthenticated" class="message-input-area p-3 border-top bg-light">
|
<div v-if="isAuthenticated" class="message-input-area p-3 border-top bg-light">
|
||||||
<form @submit.prevent="sendMessage" class="d-flex">
|
<form @submit.prevent="editingMessageId ? saveEditedMessage() : sendMessage()" class="d-flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="newMessageText"
|
v-model="newMessageText"
|
||||||
class="form-control me-2"
|
class="form-control me-2"
|
||||||
placeholder="Введите сообщение..."
|
:placeholder="editingMessageId ? 'Редактирование сообщения...' : 'Введите сообщение...'"
|
||||||
:disabled="sendingMessage"
|
:disabled="sendingMessage || savingEdit"
|
||||||
|
ref="messageInputRef"
|
||||||
/>
|
/>
|
||||||
<button type="submit" class="btn btn-primary" :disabled="sendingMessage || !newMessageText.trim()">
|
<button
|
||||||
<span v-if="sendingMessage" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
type="submit"
|
||||||
Отправить
|
class="btn btn-primary"
|
||||||
|
:disabled="sendingMessage || savingEdit || !newMessageText.trim()"
|
||||||
|
>
|
||||||
|
<span v-if="sendingMessage || savingEdit" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
{{ editingMessageId ? 'Сохранить' : 'Отправить' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="editingMessageId"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary ms-2"
|
||||||
|
@click="cancelEditMessage"
|
||||||
|
:disabled="savingEdit"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -175,6 +200,13 @@ const loadingInitialMessages = ref(true);
|
|||||||
const sendingMessage = ref(false);
|
const sendingMessage = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
|
// Для редактирования сообщений
|
||||||
|
const editingMessageId = ref(null);
|
||||||
|
const originalMessageText = ref('');
|
||||||
|
const savingEdit = ref(false);
|
||||||
|
const messageInputRef = ref(null);
|
||||||
|
|
||||||
|
|
||||||
let socket = null;
|
let socket = null;
|
||||||
const otherUserIsTyping = ref(false); // Для отображения индикатора, что собеседник печатает
|
const otherUserIsTyping = ref(false); // Для отображения индикатора, что собеседник печатает
|
||||||
let stopTypingEmitTimer = null; // Новое имя таймера для отправки stopTyping
|
let stopTypingEmitTimer = null; // Новое имя таймера для отправки stopTyping
|
||||||
@ -277,7 +309,7 @@ const getOtherParticipantName = () => getOtherParticipant()?.name;
|
|||||||
|
|
||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (!newMessageText.value.trim() || !socket || !currentUser.value || sendingMessage.value) return;
|
if (!newMessageText.value.trim() || !socket || !currentUser.value || sendingMessage.value || editingMessageId.value) return;
|
||||||
|
|
||||||
const otherParticipant = getOtherParticipant(); // Нужно получить ID получателя!
|
const otherParticipant = getOtherParticipant(); // Нужно получить ID получателя!
|
||||||
// Это слабое место, если conversationData не загружено правильно.
|
// Это слабое место, если conversationData не загружено правильно.
|
||||||
@ -317,6 +349,7 @@ const sendMessage = () => {
|
|||||||
// Пока просто сбросим через некоторое время или после получения своего же сообщения
|
// Пока просто сбросим через некоторое время или после получения своего же сообщения
|
||||||
// setTimeout(() => { sendingMessage.value = false; }, 1000);
|
// setTimeout(() => { sendingMessage.value = false; }, 1000);
|
||||||
newMessageText.value = ''; // Очищаем поле ввода
|
newMessageText.value = ''; // Очищаем поле ввода
|
||||||
|
if (messageInputRef.value) messageInputRef.value.focus(); // Возвращаем фокус в поле ввода
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNewMessage = (message) => {
|
const handleNewMessage = (message) => {
|
||||||
@ -344,6 +377,82 @@ const handleMessageDeletedBySocket = (data) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- НОВЫЕ ФУНКЦИИ И ЛОГИКА ДЛЯ РЕДАКТИРОВАНИЯ ---
|
||||||
|
const startEditMessage = (messageId) => {
|
||||||
|
const messageToEdit = messages.value.find(msg => msg._id === messageId);
|
||||||
|
if (messageToEdit && messageToEdit.sender?._id === currentUser.value?._id) {
|
||||||
|
editingMessageId.value = messageId;
|
||||||
|
originalMessageText.value = messageToEdit.text;
|
||||||
|
newMessageText.value = messageToEdit.text;
|
||||||
|
contextMenu.value.visible = false;
|
||||||
|
if (messageInputRef.value) messageInputRef.value.focus();
|
||||||
|
console.log('[ChatView] Начало редактирования сообщения:', messageId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEditMessage = () => {
|
||||||
|
editingMessageId.value = null;
|
||||||
|
newMessageText.value = '';
|
||||||
|
originalMessageText.value = '';
|
||||||
|
console.log('[ChatView] Редактирование отменено');
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEditedMessage = async () => {
|
||||||
|
if (!editingMessageId.value || !newMessageText.value.trim() || savingEdit.value) return;
|
||||||
|
|
||||||
|
const newText = newMessageText.value.trim();
|
||||||
|
if (newText === originalMessageText.value) {
|
||||||
|
// Текст не изменился, просто отменяем редактирование
|
||||||
|
cancelEditMessage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
savingEdit.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
console.log(`[ChatView] Сохранение отредактированного сообщения ${editingMessageId.value} с текстом: "${newText}"`);
|
||||||
|
// API запрос на сервер для обновления сообщения
|
||||||
|
const updatedMessage = await api.editMessage(conversationId.value, editingMessageId.value, newText);
|
||||||
|
|
||||||
|
// Обновляем сообщение в локальном массиве messages.value
|
||||||
|
// Сервер должен вернуть обновленное сообщение, включая isEdited и editedAt
|
||||||
|
const index = messages.value.findIndex(msg => msg._id === editingMessageId.value);
|
||||||
|
if (index !== -1) {
|
||||||
|
messages.value[index] = { ...messages.value[index], ...updatedMessage.data };
|
||||||
|
console.log('[ChatView] Сообщение локально обновлено:', messages.value[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelEditMessage(); // Сбрасываем состояние редактирования
|
||||||
|
// Фокус вернется в sendMessage или останется, если пользователь кликнул куда-то еще
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ChatView] Ошибка при сохранении отредактированного сообщения:', err);
|
||||||
|
error.value = err.response?.data?.message || 'Не удалось сохранить изменения.';
|
||||||
|
} finally {
|
||||||
|
savingEdit.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик события отредактированного сообщения от сокета
|
||||||
|
const handleMessageEditedBySocket = ({ message: editedMessage, conversationId: convId }) => {
|
||||||
|
console.log('[ChatView] Получено событие messageEdited от сокета:', editedMessage);
|
||||||
|
if (convId === conversationId.value) {
|
||||||
|
const index = messages.value.findIndex(msg => msg._id === editedMessage._id);
|
||||||
|
if (index !== -1) {
|
||||||
|
// Обновляем существующее сообщение новыми данными
|
||||||
|
// Важно сохранить любые локальные свойства, если они есть и не приходят с сервера
|
||||||
|
messages.value[index] = { ...messages.value[index], ...editedMessage };
|
||||||
|
console.log('[ChatView] Сообщение обновлено через сокет:', messages.value[index]);
|
||||||
|
} else {
|
||||||
|
// Если по какой-то причине сообщения нет локально, но это маловероятно для редактирования
|
||||||
|
console.warn('[ChatView] Получено отредактированное сообщение, которое не найдено локально:', editedMessage._id);
|
||||||
|
// messages.value.push(editedMessage); // Можно добавить, если такая ситуация возможна
|
||||||
|
// scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// --- КОНЕЦ ЛОГИКИ РЕДАКТИРОВАНИЯ ---
|
||||||
|
|
||||||
|
|
||||||
const confirmDeleteMessage = (messageId) => {
|
const confirmDeleteMessage = (messageId) => {
|
||||||
// Логика подтверждения удаления (например, показать модальное окно)
|
// Логика подтверждения удаления (например, показать модальное окно)
|
||||||
// А затем, если пользователь подтвердил, вызвать deleteMessage
|
// А затем, если пользователь подтвердил, вызвать deleteMessage
|
||||||
@ -378,18 +487,11 @@ const deleteMessage = async (messageId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработка события, что наши сообщения прочитаны собеседником
|
|
||||||
const handleMessagesReadByOther = ({ conversationId: readConversationId, readerId }) => {
|
const handleMessagesReadByOther = ({ conversationId: readConversationId, readerId }) => {
|
||||||
if (readConversationId === conversationId.value) {
|
if (readConversationId === conversationId.value) {
|
||||||
console.log(`[ChatView] Сообщения в диалоге ${readConversationId} прочитаны пользователем ${readerId}`);
|
console.log(`[ChatView] Сообщения в диалоге ${readConversationId} прочитаны пользователем ${readerId}`);
|
||||||
// Обновляем статус сообщений, отправленных текущим пользователем
|
|
||||||
messages.value = messages.value.map(msg => {
|
messages.value = messages.value.map(msg => {
|
||||||
if (msg.sender?._id === currentUser.value?._id) {
|
if (msg.sender?._id === currentUser.value?._id) {
|
||||||
// Если сообщение отправлено нами и еще не имеет этого readerId в readBy
|
|
||||||
// (на случай если readBy не пришло сразу с сообщением или было обновлено)
|
|
||||||
// Это упрощенный вариант. В идеале, сервер должен присылать обновленные сообщения.
|
|
||||||
// Но для отображения галочек, достаточно знать, что ОДИН получатель (собеседник) прочитал.
|
|
||||||
// Мы просто добавляем readerId в readBy для наших сообщений, если его там нет.
|
|
||||||
const updatedReadBy = msg.readBy ? [...msg.readBy] : [];
|
const updatedReadBy = msg.readBy ? [...msg.readBy] : [];
|
||||||
if (!updatedReadBy.includes(readerId)) {
|
if (!updatedReadBy.includes(readerId)) {
|
||||||
updatedReadBy.push(readerId);
|
updatedReadBy.push(readerId);
|
||||||
@ -401,7 +503,6 @@ const handleMessagesReadByOther = ({ conversationId: readConversationId, readerI
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Следим за видимостью вкладки для отметки сообщений как прочитанных
|
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
markMessagesAsReadOnServer();
|
markMessagesAsReadOnServer();
|
||||||
@ -424,34 +525,28 @@ watch(newMessageText, (newValue /*, oldValue */) => { // oldValue больше
|
|||||||
senderId: currentUser.value._id
|
senderId: currentUser.value._id
|
||||||
};
|
};
|
||||||
|
|
||||||
// Всегда очищаем таймер stopTyping при любом изменении текста
|
|
||||||
clearTimeout(stopTypingEmitTimer);
|
clearTimeout(stopTypingEmitTimer);
|
||||||
|
|
||||||
if (newValue.trim().length > 0) {
|
if (newValue.trim().length > 0) {
|
||||||
// Пользователь печатает (поле не пустое)
|
|
||||||
if (!userHasSentTypingEvent.value) {
|
if (!userHasSentTypingEvent.value) {
|
||||||
// Если событие 'typing' еще не было отправлено для этой "серии" набора
|
if (socket.connected) {
|
||||||
if (socket.connected) { // Убедимся, что сокет все еще подключен
|
|
||||||
socket.emit('typing', payload);
|
socket.emit('typing', payload);
|
||||||
userHasSentTypingEvent.value = true;
|
userHasSentTypingEvent.value = true;
|
||||||
console.log('[ChatView] Emit typing (new burst)');
|
console.log('[ChatView] Emit typing (new burst)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем таймер для отправки 'stopTyping', если пользователь перестанет печатать
|
|
||||||
stopTypingEmitTimer = setTimeout(() => {
|
stopTypingEmitTimer = setTimeout(() => {
|
||||||
if (socket.connected) { // Убедимся, что сокет все еще подключен
|
if (socket.connected) {
|
||||||
socket.emit('stopTyping', payload);
|
socket.emit('stopTyping', payload);
|
||||||
userHasSentTypingEvent.value = false; // Сбрасываем для следующей "серии" набора
|
userHasSentTypingEvent.value = false;
|
||||||
console.log('[ChatView] Emit stopTyping (timeout)');
|
console.log('[ChatView] Emit stopTyping (timeout)');
|
||||||
}
|
}
|
||||||
}, TYPING_TIMER_LENGTH);
|
}, TYPING_TIMER_LENGTH);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Пользователь очистил поле ввода или оно стало пустым
|
|
||||||
if (userHasSentTypingEvent.value) {
|
if (userHasSentTypingEvent.value) {
|
||||||
// Если событие 'typing' было активно, отправляем 'stopTyping'
|
if (socket.connected) {
|
||||||
if (socket.connected) { // Убедимся, что сокет все еще подключен
|
|
||||||
socket.emit('stopTyping', payload);
|
socket.emit('stopTyping', payload);
|
||||||
userHasSentTypingEvent.value = false;
|
userHasSentTypingEvent.value = false;
|
||||||
console.log('[ChatView] Emit stopTyping (cleared input)');
|
console.log('[ChatView] Emit stopTyping (cleared input)');
|
||||||
@ -490,6 +585,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
socket.on('messageDeleted', handleMessageDeletedBySocket);
|
socket.on('messageDeleted', handleMessageDeletedBySocket);
|
||||||
|
socket.on('messageEdited', handleMessageEditedBySocket); // <--- Подписываемся на событие редактирования
|
||||||
|
|
||||||
if (conversationId.value) {
|
if (conversationId.value) {
|
||||||
socket.emit('joinConversationRoom', conversationId.value);
|
socket.emit('joinConversationRoom', conversationId.value);
|
||||||
@ -509,6 +605,7 @@ onUnmounted(() => {
|
|||||||
socket.off('userTyping');
|
socket.off('userTyping');
|
||||||
socket.off('userStopTyping');
|
socket.off('userStopTyping');
|
||||||
socket.off('messageDeleted', handleMessageDeletedBySocket);
|
socket.off('messageDeleted', handleMessageDeletedBySocket);
|
||||||
|
socket.off('messageEdited', handleMessageEditedBySocket); // <--- Отписываемся
|
||||||
|
|
||||||
if (conversationId.value) {
|
if (conversationId.value) {
|
||||||
socket.emit('leaveConversationRoom', conversationId.value);
|
socket.emit('leaveConversationRoom', conversationId.value);
|
||||||
@ -526,23 +623,17 @@ const formatMessageTimestamp = (timestamp) => {
|
|||||||
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Иконка статуса сообщения
|
|
||||||
const getMessageStatusIcon = (message) => {
|
const getMessageStatusIcon = (message) => {
|
||||||
if (message.isSending) {
|
if (message.isSending) {
|
||||||
return 'bi bi-clock'; // Отправка (часы)
|
return 'bi bi-clock'; // Отправка (часы)
|
||||||
}
|
}
|
||||||
// Для чата 1-на-1, если есть хотя бы один ID в readBy, и он не наш, значит собеседник прочитал
|
|
||||||
const otherParticipant = getOtherParticipant();
|
const otherParticipant = getOtherParticipant();
|
||||||
if (otherParticipant && message.readBy && message.readBy.includes(otherParticipant._id)) {
|
if (otherParticipant && message.readBy && message.readBy.includes(otherParticipant._id)) {
|
||||||
return 'bi bi-check2-all'; // Прочитано (две галочки)
|
return 'bi bi-check2-all'; // Прочитано (две галочки)
|
||||||
}
|
}
|
||||||
// Если сообщение доставлено (т.е. оно есть на сервере и не isSending), но не прочитано собеседником
|
|
||||||
// (или readBy пуст/не содержит ID собеседника)
|
|
||||||
return 'bi bi-check2'; // Доставлено (одна галочка)
|
return 'bi bi-check2'; // Доставлено (одна галочка)
|
||||||
// Для групповых чатов логика была бы сложнее
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// НОВАЯ ФУНКЦИЯ для обработки нативного события contextmenu
|
|
||||||
const handleNativeContextMenu = (event, message) => {
|
const handleNativeContextMenu = (event, message) => {
|
||||||
if (message.sender?._id === currentUser.value?._id) {
|
if (message.sender?._id === currentUser.value?._id) {
|
||||||
event.preventDefault(); // Prevent native context menu for own messages to show custom one
|
event.preventDefault(); // Prevent native context menu for own messages to show custom one
|
||||||
@ -626,6 +717,10 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
order: 1; /* Кнопка справа от сообщения */
|
order: 1; /* Кнопка справа от сообщения */
|
||||||
}
|
}
|
||||||
|
.message-item-wrapper.sent-wrapper .edit-message-btn {
|
||||||
|
margin-left: 4px; /* Меньше отступ чем у кнопки удаления */
|
||||||
|
order: 0; /* Кнопка редактирования левее кнопки удаления */
|
||||||
|
}
|
||||||
|
|
||||||
.message-item-wrapper.received-wrapper {
|
.message-item-wrapper.received-wrapper {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@ -651,6 +746,26 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-message-btn {
|
||||||
|
align-self: center;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #0dcaf0; /* Bootstrap info color */
|
||||||
|
}
|
||||||
|
.edit-message-btn:hover,
|
||||||
|
.edit-message-btn:focus {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(13, 202, 240, 0.1);
|
||||||
|
}
|
||||||
|
.edit-message-btn i {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
/* Стили для контекстного меню */
|
/* Стили для контекстного меню */
|
||||||
.context-menu {
|
.context-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -690,5 +805,15 @@ const handleClickOutsideContextMenu = (event) => {
|
|||||||
color: #a71d2a; /* Darker danger color on hover */
|
color: #a71d2a; /* Darker danger color on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu li i.bi-pencil-fill {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #0d6efd; /* Bootstrap primary color */
|
||||||
|
font-size: 1.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu li:hover i.bi-pencil-fill {
|
||||||
|
color: #0a58ca; /* Darker primary color on hover */
|
||||||
|
}
|
||||||
|
|
||||||
/* ... other existing styles ... */
|
/* ... other existing styles ... */
|
||||||
</style>
|
</style>
|
Loading…
x
Reference in New Issue
Block a user