feat(chat): ответы на сообщения с цитатой и переходом к оригиналу
Кнопка «Ответить» у каждого сообщения: цитата показывается в новом сообщении и в поле ввода, отмена — ✕ или Escape, переключение треда сбрасывает ответ. Клик по цитате прокручивает ленту к исходному сообщению и подсвечивает его. Цитата повторяет вложение оригинала: имя файла с иконкой, для картинок — миниатюра WebP через существующий /api/chat/files/:token. - chat_messages.reply_to_id: FK на саму таблицу с ON DELETE SET NULL, миграция и ensureChatTables, индекс - POST .../messages принимает reply_to_id и проверяет, что родитель в этом же треде и не удалён; вложения при ошибке подчищаются - цитата едет в списке, при отправке и в SSE правки/удаления, так что у собеседника она обновляется по живому каналу - бэкап: reply_to_id в normalizeRestoreData, при restore ссылка принимается только если родитель восстановился, иначе NULL - shorts: скрывать поле «Резидент» при добавлении без разбора строк
This commit is contained in:
@@ -490,6 +490,7 @@ function normalizeRestoreData(data) {
|
|||||||
body: optText(x.body, 5000),
|
body: optText(x.body, 5000),
|
||||||
edited_at: optTs(x.edited_at),
|
edited_at: optTs(x.edited_at),
|
||||||
deleted_at: optTs(x.deleted_at),
|
deleted_at: optTs(x.deleted_at),
|
||||||
|
reply_to_id: optInt(x.reply_to_id, 1, 2147483647),
|
||||||
created_at: optTs(x.created_at),
|
created_at: optTs(x.created_at),
|
||||||
}));
|
}));
|
||||||
const chat_attachments = (data.chat_attachments || []).map(x => ({
|
const chat_attachments = (data.chat_attachments || []).map(x => ({
|
||||||
|
|||||||
@@ -364,10 +364,12 @@ CREATE TABLE IF NOT EXISTS chat_messages (
|
|||||||
body TEXT NOT NULL DEFAULT '',
|
body TEXT NOT NULL DEFAULT '',
|
||||||
edited_at TIMESTAMPTZ,
|
edited_at TIMESTAMPTZ,
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
|
reply_to_id INT REFERENCES chat_messages(id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, id);
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_reply ON chat_messages(reply_to_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_attachments (
|
CREATE TABLE IF NOT EXISTS chat_attachments (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
|||||||
@@ -337,13 +337,16 @@ CREATE TABLE IF NOT EXISTS chat_messages (
|
|||||||
body TEXT NOT NULL DEFAULT '',
|
body TEXT NOT NULL DEFAULT '',
|
||||||
edited_at TIMESTAMPTZ,
|
edited_at TIMESTAMPTZ,
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
|
reply_to_id INT REFERENCES chat_messages(id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, id);
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_reply ON chat_messages(reply_to_id);
|
||||||
|
|
||||||
ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS edited_at TIMESTAMPTZ;
|
ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS edited_at TIMESTAMPTZ;
|
||||||
ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS reply_to_id INT REFERENCES chat_messages(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat_attachments (
|
CREATE TABLE IF NOT EXISTS chat_attachments (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
|||||||
+24
-1
@@ -982,7 +982,18 @@ body.report-card-open .report-fields{display:flex}
|
|||||||
.msg-act:hover{color:var(--accent);background:var(--bg)}
|
.msg-act:hover{color:var(--accent);background:var(--bg)}
|
||||||
.msg-act-del:hover{color:#ef4444}
|
.msg-act-del:hover{color:#ef4444}
|
||||||
.msg-act svg{width:13px;height:13px}
|
.msg-act svg{width:13px;height:13px}
|
||||||
.msg-deleted-body{font-style:italic;color:var(--muted)}
|
.msg-reply{display:flex;flex-direction:column;gap:1px;border-left:2px solid var(--accent);background:var(--bg);border-radius:0 7px 7px 0;padding:4px 8px;margin-bottom:2px;min-width:0;text-align:left;font:inherit;cursor:pointer;position:relative}
|
||||||
|
.msg-reply:hover{background:var(--card);border-left-color:var(--accent)}
|
||||||
|
.msg-reply-img{display:block;width:100%;max-width:280px;max-height:150px;object-fit:cover;border-radius:6px;background:var(--card);margin-bottom:4px}
|
||||||
|
.msg-reply-who{font-size:.68rem;color:var(--accent);font-weight:600}
|
||||||
|
.msg-reply-text{font-size:.75rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:280px}
|
||||||
|
.msg-reply-text[hidden]{display:none}
|
||||||
|
.msg-reply-file{display:inline-flex;align-items:center;gap:5px;font-size:.75rem;color:var(--muted);max-width:280px;min-width:0}
|
||||||
|
.msg-reply-file[hidden]{display:none}
|
||||||
|
.msg-reply-file svg{width:13px;height:13px;flex-shrink:0}
|
||||||
|
.msg-reply-file span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.msg-flash{animation:msgFlash 1.4s ease-out}
|
||||||
|
@keyframes msgFlash{0%,100%{box-shadow:none}12%,55%{box-shadow:0 0 0 3px rgba(37,99,235,.35)}}
|
||||||
.chat-edit-modal{max-width:560px}
|
.chat-edit-modal{max-width:560px}
|
||||||
.chat-edit-modal textarea{min-height:110px;max-height:40dvh}
|
.chat-edit-modal textarea{min-height:110px;max-height:40dvh}
|
||||||
.chat-edit-modal textarea:focus{border-color:var(--accent)}
|
.chat-edit-modal textarea:focus{border-color:var(--accent)}
|
||||||
@@ -997,6 +1008,18 @@ body.report-card-open .report-fields{display:flex}
|
|||||||
.msg-file-img img{width:100%;height:110px;object-fit:cover;border-radius:6px;display:block;background:var(--bg)}
|
.msg-file-img img{width:100%;height:110px;object-fit:cover;border-radius:6px;display:block;background:var(--bg)}
|
||||||
.msg-file-img .msg-file-name{font-size:.7rem;color:var(--muted)}
|
.msg-file-img .msg-file-name{font-size:.7rem;color:var(--muted)}
|
||||||
.chat-composer{border-top:1px solid var(--border);padding:11px 14px 13px;display:flex;flex-direction:column;gap:8px}
|
.chat-composer{border-top:1px solid var(--border);padding:11px 14px 13px;display:flex;flex-direction:column;gap:8px}
|
||||||
|
.chat-reply-bar{display:flex;align-items:center;gap:8px;background:var(--bg);border:1px solid var(--accent);border-radius:9px;padding:6px 9px;font-size:.78rem}
|
||||||
|
.chat-reply-bar[hidden]{display:none}
|
||||||
|
.chat-reply-bar svg{width:14px;height:14px;color:var(--accent);flex-shrink:0}
|
||||||
|
.chat-reply-meta{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
|
||||||
|
.chat-reply-who{color:var(--accent);font-weight:600;font-size:.7rem}
|
||||||
|
.chat-reply-text{color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.chat-reply-text[hidden]{display:none}
|
||||||
|
.chat-reply-file{display:inline-flex;align-items:center;gap:5px;color:var(--muted);min-width:0}
|
||||||
|
.chat-reply-file[hidden]{display:none}
|
||||||
|
.chat-reply-img{display:block;height:44px;width:44px;object-fit:cover;border-radius:6px;background:var(--card);flex-shrink:0}
|
||||||
|
.chat-reply-file svg{width:13px;height:13px;flex-shrink:0}
|
||||||
|
.chat-reply-file span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
.chat-attach-list{display:flex;flex-wrap:wrap;gap:7px}
|
.chat-attach-list{display:flex;flex-wrap:wrap;gap:7px}
|
||||||
.chat-attach-list[hidden]{display:none}
|
.chat-attach-list[hidden]{display:none}
|
||||||
.chat-attach-chip{display:inline-flex;align-items:center;gap:6px;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:5px 9px;font-size:.75rem;max-width:250px}
|
.chat-attach-chip{display:inline-flex;align-items:center;gap:6px;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:5px 9px;font-size:.75rem;max-width:250px}
|
||||||
|
|||||||
@@ -49,6 +49,15 @@
|
|||||||
<div class="chat-messages" id="chatMessages"></div>
|
<div class="chat-messages" id="chatMessages"></div>
|
||||||
|
|
||||||
<div class="chat-composer">
|
<div class="chat-composer">
|
||||||
|
<div class="chat-reply-bar" id="chatReplyBar" hidden>
|
||||||
|
<i data-lucide="corner-up-left"></i>
|
||||||
|
<span class="chat-reply-meta">
|
||||||
|
<span class="chat-reply-who" id="chatReplyWho"></span>
|
||||||
|
<span class="chat-reply-text" id="chatReplyText"></span>
|
||||||
|
<span class="chat-reply-file" id="chatReplyFile" hidden></span>
|
||||||
|
</span>
|
||||||
|
<button type="button" class="chat-attach-del" id="chatReplyCancel" title="Отменить ответ">✕</button>
|
||||||
|
</div>
|
||||||
<div class="chat-attach-list" id="chatAttachList" hidden></div>
|
<div class="chat-attach-list" id="chatAttachList" hidden></div>
|
||||||
<div class="chat-compose-row">
|
<div class="chat-compose-row">
|
||||||
<button class="chat-attach-btn" id="chatAttachBtn" type="button" title="Прикрепить файл или фото"><i data-lucide="paperclip"></i></button>
|
<button class="chat-attach-btn" id="chatAttachBtn" type="button" title="Прикрепить файл или фото"><i data-lucide="paperclip"></i></button>
|
||||||
|
|||||||
+127
-16
@@ -17,6 +17,7 @@ let chatPendingFiles = [];
|
|||||||
let chatLoading = false;
|
let chatLoading = false;
|
||||||
let chatSending = false;
|
let chatSending = false;
|
||||||
let chatEditId = 0;
|
let chatEditId = 0;
|
||||||
|
let chatReplyTo = null;
|
||||||
let chatHiddenMode = false;
|
let chatHiddenMode = false;
|
||||||
let chatHiddenCount = 0;
|
let chatHiddenCount = 0;
|
||||||
|
|
||||||
@@ -125,17 +126,7 @@ function chatMessageHtml(m) {
|
|||||||
const mine = Number(m.sender_id) === Number(currentUser?.id);
|
const mine = Number(m.sender_id) === Number(currentUser?.id);
|
||||||
const who = mine ? 'Вы' : (m.sender_name || 'Тьютор');
|
const who = mine ? 'Вы' : (m.sender_name || 'Тьютор');
|
||||||
const role = mine ? '' : (m.sender_role === 'admin' ? 'администратор' : 'тьютор');
|
const role = mine ? '' : (m.sender_role === 'admin' ? 'администратор' : 'тьютор');
|
||||||
if (m.deleted_at) {
|
if (m.deleted_at) return '';
|
||||||
return `<div class="msg msg-deleted ${mine ? 'msg-mine' : ''}" data-msg="${m.id}">
|
|
||||||
<div class="msg-head">
|
|
||||||
<span class="msg-who">${esc(who)}</span>
|
|
||||||
${role ? `<span class="msg-role">${esc(role)}</span>` : ''}
|
|
||||||
<span class="grow"></span>
|
|
||||||
<span class="msg-time">${esc(chatMessageTime(m.created_at))}</span>
|
|
||||||
</div>
|
|
||||||
<div class="msg-body msg-deleted-body">Сообщение удалено</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
const canDelete = mine || isAdmin();
|
const canDelete = mine || isAdmin();
|
||||||
return `<div class="msg ${mine ? 'msg-mine' : ''}" data-msg="${m.id}">
|
return `<div class="msg ${mine ? 'msg-mine' : ''}" data-msg="${m.id}">
|
||||||
<div class="msg-head">
|
<div class="msg-head">
|
||||||
@@ -144,16 +135,38 @@ function chatMessageHtml(m) {
|
|||||||
${m.edited_at ? `<span class="msg-edited" title="Изменено ${esc(chatMessageTime(m.edited_at))}">изменено</span>` : ''}
|
${m.edited_at ? `<span class="msg-edited" title="Изменено ${esc(chatMessageTime(m.edited_at))}">изменено</span>` : ''}
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
<span class="msg-time">${esc(chatMessageTime(m.created_at))}</span>
|
<span class="msg-time">${esc(chatMessageTime(m.created_at))}</span>
|
||||||
${canDelete ? `<span class="msg-actions">
|
<span class="msg-actions">
|
||||||
|
<button type="button" class="msg-act" data-msg-reply="${m.id}" title="Ответить"><i data-lucide="corner-up-left"></i></button>
|
||||||
${mine ? `<button type="button" class="msg-act" data-msg-edit="${m.id}" title="Редактировать"><i data-lucide="pencil"></i></button>` : ''}
|
${mine ? `<button type="button" class="msg-act" data-msg-edit="${m.id}" title="Редактировать"><i data-lucide="pencil"></i></button>` : ''}
|
||||||
<button type="button" class="msg-act msg-act-del" data-msg-del="${m.id}" title="Удалить"><i data-lucide="trash-2"></i></button>
|
${canDelete ? `<button type="button" class="msg-act msg-act-del" data-msg-del="${m.id}" title="Удалить"><i data-lucide="trash-2"></i></button>` : ''}
|
||||||
</span>` : ''}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
${chatReplyHtml(m)}
|
||||||
${m.body ? `<div class="msg-body">${esc(m.body)}</div>` : ''}
|
${m.body ? `<div class="msg-body">${esc(m.body)}</div>` : ''}
|
||||||
${chatFilesHtml(m.files)}
|
${chatFilesHtml(m.files)}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chatReplyHtml(m) {
|
||||||
|
if (!m.reply_to_id) return '';
|
||||||
|
const who = m.reply_sender_name || 'Тьютор';
|
||||||
|
const gone = !!m.reply_deleted_at;
|
||||||
|
const body = String(m.reply_body || '').trim();
|
||||||
|
const file = m.reply_file_name ? String(m.reply_file_name) : '';
|
||||||
|
const img = !gone && file && CHAT_IMAGE_EXT.test(file) && m.reply_file_token
|
||||||
|
? `<img class="msg-reply-img" src="${chatFileThumbUrl({ token: m.reply_file_token })}" alt="" loading="lazy">`
|
||||||
|
: '';
|
||||||
|
const fileLine = !gone && file
|
||||||
|
? `<span class="msg-reply-file"><i data-lucide="${chatFileIcon(file)}"></i><span title="${esc(file)}">${esc(file)}</span></span>`
|
||||||
|
: '';
|
||||||
|
const head = `<span class="msg-reply-who">${esc(who)}</span>`;
|
||||||
|
const inner = gone
|
||||||
|
? `${head}<span class="msg-reply-text">Сообщение удалено</span>`
|
||||||
|
: `${head}${body ? `<span class="msg-reply-text">${esc(body)}</span>` : ''}${fileLine}`;
|
||||||
|
if (gone) return `<div class="msg-reply">${inner}</div>`;
|
||||||
|
return `<button type="button" class="msg-reply" data-msg-jump="${m.reply_to_id}" title="Перейти к сообщению">${img}${inner}</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
function addChatMessage(m) {
|
function addChatMessage(m) {
|
||||||
if (!m || m.id === undefined || m.id === null) return false;
|
if (!m || m.id === undefined || m.id === null) return false;
|
||||||
if (chatMessages.some(x => Number(x.id) === Number(m.id))) return false;
|
if (chatMessages.some(x => Number(x.id) === Number(m.id))) return false;
|
||||||
@@ -161,14 +174,19 @@ function addChatMessage(m) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chatVisibleMessages() {
|
||||||
|
return chatMessages.filter(m => !m.deleted_at);
|
||||||
|
}
|
||||||
|
|
||||||
function renderChatMessages() {
|
function renderChatMessages() {
|
||||||
const box = document.getElementById('chatMessages');
|
const box = document.getElementById('chatMessages');
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
if (!chatMessages.length) {
|
const visible = chatVisibleMessages();
|
||||||
|
if (!visible.length) {
|
||||||
box.innerHTML = '<div class="chat-msgs-empty">Сообщений пока нет. Напишите первым.</div>';
|
box.innerHTML = '<div class="chat-msgs-empty">Сообщений пока нет. Напишите первым.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
box.innerHTML = chatMessages.map(chatMessageHtml).join('');
|
box.innerHTML = visible.map(chatMessageHtml).join('');
|
||||||
renderIcons();
|
renderIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +195,15 @@ function scrollChatDown() {
|
|||||||
if (box) box.scrollTop = box.scrollHeight;
|
if (box) box.scrollTop = box.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollChatToMessage(messageId) {
|
||||||
|
const box = document.getElementById('chatMessages');
|
||||||
|
const node = box && box.querySelector(`.msg[data-msg="${messageId}"]`);
|
||||||
|
if (!node) return;
|
||||||
|
node.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||||
|
node.classList.add('msg-flash');
|
||||||
|
setTimeout(() => node.classList.remove('msg-flash'), 1400);
|
||||||
|
}
|
||||||
|
|
||||||
function chatAtBottom() {
|
function chatAtBottom() {
|
||||||
const box = document.getElementById('chatMessages');
|
const box = document.getElementById('chatMessages');
|
||||||
if (!box) return true;
|
if (!box) return true;
|
||||||
@@ -416,8 +443,59 @@ function autoGrowChatInput(el) {
|
|||||||
input.style.height = `${Math.min(input.scrollHeight, 160)}px`;
|
input.style.height = `${Math.min(input.scrollHeight, 160)}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderChatReplyBar() {
|
||||||
|
const bar = document.getElementById('chatReplyBar');
|
||||||
|
if (!bar) return;
|
||||||
|
if (!chatReplyTo) {
|
||||||
|
bar.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const who = document.getElementById('chatReplyWho');
|
||||||
|
const text = document.getElementById('chatReplyText');
|
||||||
|
const file = document.getElementById('chatReplyFile');
|
||||||
|
if (who) who.textContent = chatReplyTo.sender_name || 'Тьютор';
|
||||||
|
if (text) {
|
||||||
|
const body = String(chatReplyTo.body || '').trim();
|
||||||
|
text.textContent = body || (chatReplyTo.file_name ? '' : 'Вложение');
|
||||||
|
text.hidden = !text.textContent;
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
const fn = chatReplyTo.file_name;
|
||||||
|
file.innerHTML = !fn
|
||||||
|
? ''
|
||||||
|
: CHAT_IMAGE_EXT.test(fn) && chatReplyTo.file_token
|
||||||
|
? `<img class="chat-reply-img" src="${chatFileThumbUrl({ token: chatReplyTo.file_token })}" alt="">`
|
||||||
|
: `<i data-lucide="${chatFileIcon(fn)}"></i><span title="${esc(fn)}">${esc(fn)}</span>`;
|
||||||
|
file.hidden = !fn;
|
||||||
|
}
|
||||||
|
bar.hidden = false;
|
||||||
|
renderIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setChatReply(messageId) {
|
||||||
|
const msg = chatMessages.find(m => Number(m.id) === Number(messageId));
|
||||||
|
if (!msg || msg.deleted_at) return;
|
||||||
|
const files = msg.files || [];
|
||||||
|
chatReplyTo = {
|
||||||
|
id: Number(msg.id),
|
||||||
|
sender_name: msg.sender_name,
|
||||||
|
body: msg.body,
|
||||||
|
file_name: files.length ? files[0].name : '',
|
||||||
|
file_token: files.length ? files[0].token : '',
|
||||||
|
};
|
||||||
|
renderChatReplyBar();
|
||||||
|
const input = document.getElementById('chatInput');
|
||||||
|
if (input) input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearChatReply() {
|
||||||
|
chatReplyTo = null;
|
||||||
|
renderChatReplyBar();
|
||||||
|
}
|
||||||
|
|
||||||
function selectChatThread(id) {
|
function selectChatThread(id) {
|
||||||
chatActiveId = Number(id) || 0;
|
chatActiveId = Number(id) || 0;
|
||||||
|
clearChatReply();
|
||||||
renderChatThreads();
|
renderChatThreads();
|
||||||
loadChatMessages();
|
loadChatMessages();
|
||||||
}
|
}
|
||||||
@@ -464,6 +542,14 @@ function onChatMessageUpdate(p) {
|
|||||||
} else {
|
} else {
|
||||||
chatMessages[idx] = { ...chatMessages[idx], ...m };
|
chatMessages[idx] = { ...chatMessages[idx], ...m };
|
||||||
}
|
}
|
||||||
|
for (const x of chatMessages) {
|
||||||
|
if (Number(x.reply_to_id) === Number(m.id)) {
|
||||||
|
x.reply_body = m.body || '';
|
||||||
|
x.reply_sender_name = m.sender_name || x.reply_sender_name;
|
||||||
|
x.reply_file_name = m.files && m.files.length ? m.files[0].name : null;
|
||||||
|
x.reply_file_token = m.files && m.files.length ? m.files[0].token : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
renderChatMessages();
|
renderChatMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +560,15 @@ function onChatMessageDelete(p) {
|
|||||||
const m = p.message;
|
const m = p.message;
|
||||||
const idx = chatMessages.findIndex(x => Number(x.id) === Number(m.id));
|
const idx = chatMessages.findIndex(x => Number(x.id) === Number(m.id));
|
||||||
if (idx >= 0) chatMessages[idx] = { ...chatMessages[idx], ...m, body: '', files: [], edited_at: null };
|
if (idx >= 0) chatMessages[idx] = { ...chatMessages[idx], ...m, body: '', files: [], edited_at: null };
|
||||||
|
for (const x of chatMessages) {
|
||||||
|
if (Number(x.reply_to_id) === Number(m.id)) {
|
||||||
|
x.reply_body = '';
|
||||||
|
x.reply_file_name = null;
|
||||||
|
x.reply_file_token = null;
|
||||||
|
x.reply_deleted_at = m.deleted_at || new Date().toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chatReplyTo && Number(chatReplyTo.id) === Number(m.id)) clearChatReply();
|
||||||
renderChatMessages();
|
renderChatMessages();
|
||||||
if (Number(m.sender_id) !== Number(currentUser?.id)) markChatRead();
|
if (Number(m.sender_id) !== Number(currentUser?.id)) markChatRead();
|
||||||
}
|
}
|
||||||
@@ -552,6 +647,7 @@ async function deleteChatMessage(messageId) {
|
|||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
chatMessages[idx] = { ...chatMessages[idx], body: '', files: [], edited_at: null, deleted_at: new Date().toISOString() };
|
chatMessages[idx] = { ...chatMessages[idx], body: '', files: [], edited_at: null, deleted_at: new Date().toISOString() };
|
||||||
}
|
}
|
||||||
|
if (chatReplyTo && Number(chatReplyTo.id) === Number(messageId)) clearChatReply();
|
||||||
renderChatMessages();
|
renderChatMessages();
|
||||||
loadChatThreads();
|
loadChatThreads();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -590,10 +686,12 @@ async function sendChatMessage() {
|
|||||||
try {
|
try {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('body', body);
|
fd.append('body', body);
|
||||||
|
if (chatReplyTo) fd.append('reply_to_id', String(chatReplyTo.id));
|
||||||
for (const f of files) fd.append('files', f, f.name);
|
for (const f of files) fd.append('files', f, f.name);
|
||||||
const data = await chatApi(`/api/chat/threads/${chatActiveId}/messages`, { method: 'POST', headers: hdr(), body: fd });
|
const data = await chatApi(`/api/chat/threads/${chatActiveId}/messages`, { method: 'POST', headers: hdr(), body: fd });
|
||||||
chatPendingFiles = [];
|
chatPendingFiles = [];
|
||||||
renderChatAttach();
|
renderChatAttach();
|
||||||
|
clearChatReply();
|
||||||
input.value = '';
|
input.value = '';
|
||||||
autoGrowChatInput();
|
autoGrowChatInput();
|
||||||
const stick = chatAtBottom();
|
const stick = chatAtBottom();
|
||||||
@@ -672,6 +770,16 @@ function initChatPage() {
|
|||||||
|
|
||||||
if (box) {
|
if (box) {
|
||||||
box.addEventListener('click', e => {
|
box.addEventListener('click', e => {
|
||||||
|
const jumpBtn = e.target.closest('[data-msg-jump]');
|
||||||
|
if (jumpBtn) {
|
||||||
|
scrollChatToMessage(Number(jumpBtn.dataset.msgJump));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const replyBtn = e.target.closest('[data-msg-reply]');
|
||||||
|
if (replyBtn) {
|
||||||
|
setChatReply(Number(replyBtn.dataset.msgReply));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const editBtn = e.target.closest('[data-msg-edit]');
|
const editBtn = e.target.closest('[data-msg-edit]');
|
||||||
if (editBtn) {
|
if (editBtn) {
|
||||||
openChatEditModal(Number(editBtn.dataset.msgEdit));
|
openChatEditModal(Number(editBtn.dataset.msgEdit));
|
||||||
@@ -681,6 +789,8 @@ function initChatPage() {
|
|||||||
if (delBtn) deleteChatMessage(Number(delBtn.dataset.msgDel));
|
if (delBtn) deleteChatMessage(Number(delBtn.dataset.msgDel));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const replyCancel = document.getElementById('chatReplyCancel');
|
||||||
|
if (replyCancel) replyCancel.addEventListener('click', clearChatReply);
|
||||||
if (editCancel) editCancel.addEventListener('click', closeChatEditModal);
|
if (editCancel) editCancel.addEventListener('click', closeChatEditModal);
|
||||||
if (editSave) editSave.addEventListener('click', submitChatEdit);
|
if (editSave) editSave.addEventListener('click', submitChatEdit);
|
||||||
if (editModal) {
|
if (editModal) {
|
||||||
@@ -780,6 +890,7 @@ function initChatPage() {
|
|||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key !== 'Escape') return;
|
if (e.key !== 'Escape') return;
|
||||||
if (editModal && editModal.classList.contains('open')) { closeChatEditModal(); return; }
|
if (editModal && editModal.classList.contains('open')) { closeChatEditModal(); return; }
|
||||||
|
if (chatReplyTo) { clearChatReply(); return; }
|
||||||
if (imgModal && imgModal.classList.contains('open')) { imgModal.classList.remove('open'); return; }
|
if (imgModal && imgModal.classList.contains('open')) { imgModal.classList.remove('open'); return; }
|
||||||
if (newModal && newModal.classList.contains('open')) newModal.classList.remove('open');
|
if (newModal && newModal.classList.contains('open')) newModal.classList.remove('open');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ function openShortsModal(row) {
|
|||||||
document.getElementById('shortsDate').value = row ? row.lesson_date : todayIso();
|
document.getElementById('shortsDate').value = row ? row.lesson_date : todayIso();
|
||||||
document.getElementById('shortsTopic').value = row ? (row.topic || '') : '';
|
document.getElementById('shortsTopic').value = row ? (row.topic || '') : '';
|
||||||
document.getElementById('shortsResident').value = row ? (row.resident || '') : '';
|
document.getElementById('shortsResident').value = row ? (row.resident || '') : '';
|
||||||
|
document.getElementById('shortsResidentField').style.display = row ? '' : 'none';
|
||||||
document.getElementById('shortsUsed').checked = row ? !!row.used : false;
|
document.getElementById('shortsUsed').checked = row ? !!row.used : false;
|
||||||
document.getElementById('shortsUsedAt').value = row && row.used_at ? String(row.used_at).slice(0, 10) : '';
|
document.getElementById('shortsUsedAt').value = row && row.used_at ? String(row.used_at).slice(0, 10) : '';
|
||||||
const usedDisplay = row ? '' : 'none';
|
const usedDisplay = row ? '' : 'none';
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<textarea id="shortsLines" class="settings-input" rows="5" placeholder="Иванов И.И. - Отличная работа на занятии Петров П.П. — Продолжили изучение Photoshop"></textarea>
|
<textarea id="shortsLines" class="settings-input" rows="5" placeholder="Иванов И.И. - Отличная работа на занятии Петров П.П. — Продолжили изучение Photoshop"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field">
|
<div class="settings-field" id="shortsResidentField" style="display:none">
|
||||||
<label>Резидент (для добавления без разбора строк)</label>
|
<label>Резидент (для добавления без разбора строк)</label>
|
||||||
<input type="text" id="shortsResident" class="settings-input" placeholder="Иванов И.И.">
|
<input type="text" id="shortsResident" class="settings-input" placeholder="Иванов И.И.">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -463,11 +463,14 @@ async function ensureChatTables() {
|
|||||||
body TEXT NOT NULL DEFAULT '',
|
body TEXT NOT NULL DEFAULT '',
|
||||||
edited_at TIMESTAMPTZ,
|
edited_at TIMESTAMPTZ,
|
||||||
deleted_at TIMESTAMPTZ,
|
deleted_at TIMESTAMPTZ,
|
||||||
|
reply_to_id INT REFERENCES chat_messages(id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
)`);
|
)`);
|
||||||
await pool.query('ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS edited_at TIMESTAMPTZ');
|
await pool.query('ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS edited_at TIMESTAMPTZ');
|
||||||
await pool.query('ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ');
|
await pool.query('ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ');
|
||||||
|
await pool.query('ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS reply_to_id INT REFERENCES chat_messages(id) ON DELETE SET NULL');
|
||||||
await pool.query(`CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, id)`);
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id, id)`);
|
||||||
|
await pool.query(`CREATE INDEX IF NOT EXISTS idx_chat_messages_reply ON chat_messages(reply_to_id)`);
|
||||||
await pool.query(`CREATE TABLE IF NOT EXISTS chat_attachments (
|
await pool.query(`CREATE TABLE IF NOT EXISTS chat_attachments (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
message_id INT NOT NULL REFERENCES chat_messages(id) ON DELETE CASCADE,
|
message_id INT NOT NULL REFERENCES chat_messages(id) ON DELETE CASCADE,
|
||||||
@@ -2029,7 +2032,17 @@ async function chatMessageFor(user, threadId, messageId) {
|
|||||||
const mid = parseInt(messageId, 10);
|
const mid = parseInt(messageId, 10);
|
||||||
if (!Number.isInteger(mid) || mid < 1) return { error: 400, message: 'Invalid message' };
|
if (!Number.isInteger(mid) || mid < 1) return { error: 400, message: 'Invalid message' };
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT id, thread_id, sender_id, body, edited_at, deleted_at, created_at
|
`SELECT id, thread_id, sender_id, body, edited_at, deleted_at, reply_to_id, created_at,
|
||||||
|
(SELECT rm.body FROM chat_messages rm WHERE rm.id = chat_messages.reply_to_id) AS reply_body,
|
||||||
|
(SELECT rm.deleted_at FROM chat_messages rm WHERE rm.id = chat_messages.reply_to_id) AS reply_deleted_at,
|
||||||
|
(SELECT a.name FROM chat_attachments a
|
||||||
|
WHERE a.message_id = chat_messages.reply_to_id ORDER BY a.id LIMIT 1) AS reply_file_name,
|
||||||
|
(SELECT a.token FROM chat_attachments a
|
||||||
|
WHERE a.message_id = chat_messages.reply_to_id ORDER BY a.id LIMIT 1) AS reply_file_token,
|
||||||
|
(SELECT ru.name FROM chat_messages rm JOIN users ru ON ru.id = rm.sender_id
|
||||||
|
WHERE rm.id = chat_messages.reply_to_id) AS reply_name,
|
||||||
|
(SELECT ru.username FROM chat_messages rm JOIN users ru ON ru.id = rm.sender_id
|
||||||
|
WHERE rm.id = chat_messages.reply_to_id) AS reply_username
|
||||||
FROM chat_messages WHERE id = $1 AND thread_id = $2`,
|
FROM chat_messages WHERE id = $1 AND thread_id = $2`,
|
||||||
[mid, acc.thread.id]
|
[mid, acc.thread.id]
|
||||||
);
|
);
|
||||||
@@ -2156,8 +2169,15 @@ app.get('/api/chat/threads/:id/messages', requireAuth, chatLimiter, async (req,
|
|||||||
extra = ` AND m.id < $${params.length}`;
|
extra = ` AND m.id < $${params.length}`;
|
||||||
}
|
}
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT m.id, m.sender_id, m.body, m.edited_at, m.deleted_at, m.created_at, u.username, u.name, u.role
|
`SELECT m.id, m.sender_id, m.body, m.edited_at, m.deleted_at, m.created_at, u.username, u.name, u.role,
|
||||||
FROM chat_messages m JOIN users u ON u.id = m.sender_id
|
m.reply_to_id, rm.body AS reply_body, rm.deleted_at AS reply_deleted_at,
|
||||||
|
ru.name AS reply_name, ru.username AS reply_username,
|
||||||
|
(SELECT a.name FROM chat_attachments a WHERE a.message_id = rm.id ORDER BY a.id LIMIT 1) AS reply_file_name,
|
||||||
|
(SELECT a.token FROM chat_attachments a WHERE a.message_id = rm.id ORDER BY a.id LIMIT 1) AS reply_file_token
|
||||||
|
FROM chat_messages m
|
||||||
|
JOIN users u ON u.id = m.sender_id
|
||||||
|
LEFT JOIN chat_messages rm ON rm.id = m.reply_to_id
|
||||||
|
LEFT JOIN users ru ON ru.id = rm.sender_id
|
||||||
WHERE m.thread_id = $1${extra}
|
WHERE m.thread_id = $1${extra}
|
||||||
ORDER BY m.id DESC
|
ORDER BY m.id DESC
|
||||||
LIMIT $${params.length + 1}`,
|
LIMIT $${params.length + 1}`,
|
||||||
@@ -2188,6 +2208,12 @@ app.get('/api/chat/threads/:id/messages', requireAuth, chatLimiter, async (req,
|
|||||||
edited_at: m.edited_at,
|
edited_at: m.edited_at,
|
||||||
deleted_at: m.deleted_at,
|
deleted_at: m.deleted_at,
|
||||||
created_at: m.created_at,
|
created_at: m.created_at,
|
||||||
|
reply_to_id: m.reply_to_id,
|
||||||
|
reply_body: m.reply_deleted_at ? '' : m.reply_body || '',
|
||||||
|
reply_deleted_at: m.reply_deleted_at,
|
||||||
|
reply_sender_name: m.reply_deleted_at ? '' : (m.reply_name || m.reply_username || 'Тьютор'),
|
||||||
|
reply_file_name: m.reply_deleted_at ? null : (m.reply_file_name || null),
|
||||||
|
reply_file_token: m.reply_deleted_at ? null : (m.reply_file_token || null),
|
||||||
files: m.deleted_at ? [] : (byMessage.get(m.id) || []),
|
files: m.deleted_at ? [] : (byMessage.get(m.id) || []),
|
||||||
}));
|
}));
|
||||||
const unreadCol = req.user.role === 'admin' ? acc.thread.admin_unread : acc.thread.tutor_unread;
|
const unreadCol = req.user.role === 'admin' ? acc.thread.admin_unread : acc.thread.tutor_unread;
|
||||||
@@ -2284,7 +2310,27 @@ app.post('/api/chat/threads/:id/messages', requireAuth, chatLimiter, (req, res,
|
|||||||
return res.status(acc.error).json({ error: acc.message });
|
return res.status(acc.error).json({ error: acc.message });
|
||||||
}
|
}
|
||||||
const body = String(req.body?.body || '').trim().slice(0, CHAT_BODY_MAX);
|
const body = String(req.body?.body || '').trim().slice(0, CHAT_BODY_MAX);
|
||||||
if (!body && !files.length) return res.status(400).json({ error: 'Пустое сообщение' });
|
if (!body && !files.length) {
|
||||||
|
files.forEach(removeUpload);
|
||||||
|
return res.status(400).json({ error: 'Пустое сообщение' });
|
||||||
|
}
|
||||||
|
let replyToId = null;
|
||||||
|
const rawReply = req.body?.reply_to_id;
|
||||||
|
if (rawReply !== undefined && rawReply !== null && rawReply !== '') {
|
||||||
|
replyToId = parseInt(rawReply, 10);
|
||||||
|
if (!Number.isInteger(replyToId) || replyToId < 1) {
|
||||||
|
files.forEach(removeUpload);
|
||||||
|
return res.status(400).json({ error: 'Некорректное сообщение для ответа' });
|
||||||
|
}
|
||||||
|
const rr = await pool.query(
|
||||||
|
'SELECT id FROM chat_messages WHERE id = $1 AND thread_id = $2 AND deleted_at IS NULL',
|
||||||
|
[replyToId, acc.thread.id]
|
||||||
|
);
|
||||||
|
if (!rr.rows.length) {
|
||||||
|
files.forEach(removeUpload);
|
||||||
|
return res.status(404).json({ error: 'Сообщение для ответа не найдено' });
|
||||||
|
}
|
||||||
|
}
|
||||||
const totalBytes = files.reduce((s, f) => s + (f.size || 0), 0);
|
const totalBytes = files.reduce((s, f) => s + (f.size || 0), 0);
|
||||||
if (totalBytes > MAX_TOTAL_UPLOAD_BYTES) {
|
if (totalBytes > MAX_TOTAL_UPLOAD_BYTES) {
|
||||||
files.forEach(removeUpload);
|
files.forEach(removeUpload);
|
||||||
@@ -2297,8 +2343,9 @@ app.post('/api/chat/threads/:id/messages', requireAuth, chatLimiter, (req, res,
|
|||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
const mr = await client.query(
|
const mr = await client.query(
|
||||||
'INSERT INTO chat_messages (thread_id, sender_id, body) VALUES ($1, $2, $3) RETURNING id, sender_id, body, created_at',
|
`INSERT INTO chat_messages (thread_id, sender_id, body, reply_to_id)
|
||||||
[acc.thread.id, req.user.id, body]
|
VALUES ($1, $2, $3, $4) RETURNING id, sender_id, body, reply_to_id, created_at`,
|
||||||
|
[acc.thread.id, req.user.id, body, replyToId]
|
||||||
);
|
);
|
||||||
created = mr.rows[0];
|
created = mr.rows[0];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
@@ -2333,6 +2380,26 @@ app.post('/api/chat/threads/:id/messages', requireAuth, chatLimiter, (req, res,
|
|||||||
'SELECT id, token, name, size FROM chat_attachments WHERE message_id = $1 ORDER BY id',
|
'SELECT id, token, name, size FROM chat_attachments WHERE message_id = $1 ORDER BY id',
|
||||||
[created.id]
|
[created.id]
|
||||||
);
|
);
|
||||||
|
let replyBody = '';
|
||||||
|
let replySenderName = '';
|
||||||
|
let replyFileName = null;
|
||||||
|
let replyFileToken = null;
|
||||||
|
if (replyToId) {
|
||||||
|
const { rows: rrows } = await pool.query(
|
||||||
|
`SELECT m.body, m.deleted_at, u.name, u.username,
|
||||||
|
(SELECT a.name FROM chat_attachments a WHERE a.message_id = m.id ORDER BY a.id LIMIT 1) AS file_name,
|
||||||
|
(SELECT a.token FROM chat_attachments a WHERE a.message_id = m.id ORDER BY a.id LIMIT 1) AS file_token
|
||||||
|
FROM chat_messages m JOIN users u ON u.id = m.sender_id
|
||||||
|
WHERE m.id = $1`,
|
||||||
|
[replyToId]
|
||||||
|
);
|
||||||
|
if (rrows.length && !rrows[0].deleted_at) {
|
||||||
|
replyBody = rrows[0].body || '';
|
||||||
|
replySenderName = rrows[0].name || rrows[0].username || 'Тьютор';
|
||||||
|
replyFileName = rrows[0].file_name || null;
|
||||||
|
replyFileToken = rrows[0].file_token || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const message = {
|
const message = {
|
||||||
id: created.id,
|
id: created.id,
|
||||||
sender_id: created.sender_id,
|
sender_id: created.sender_id,
|
||||||
@@ -2343,6 +2410,12 @@ app.post('/api/chat/threads/:id/messages', requireAuth, chatLimiter, (req, res,
|
|||||||
edited_at: null,
|
edited_at: null,
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
created_at: created.created_at,
|
created_at: created.created_at,
|
||||||
|
reply_to_id: created.reply_to_id,
|
||||||
|
reply_body: replyBody,
|
||||||
|
reply_deleted_at: null,
|
||||||
|
reply_sender_name: replySenderName,
|
||||||
|
reply_file_name: replyFileName,
|
||||||
|
reply_file_token: replyFileToken,
|
||||||
files: frows.map(f => ({ id: f.id, token: f.token, name: f.name, size: f.size })),
|
files: frows.map(f => ({ id: f.id, token: f.token, name: f.name, size: f.size })),
|
||||||
};
|
};
|
||||||
publishChat({ type: 'message', thread_id: acc.thread.id, tutor_id: acc.thread.tutor_id, message });
|
publishChat({ type: 'message', thread_id: acc.thread.id, tutor_id: acc.thread.tutor_id, message });
|
||||||
@@ -2388,6 +2461,12 @@ app.put('/api/chat/threads/:id/messages/:messageId', requireAuth, chatLimiter, a
|
|||||||
edited_at: updated.edited_at,
|
edited_at: updated.edited_at,
|
||||||
deleted_at: updated.deleted_at,
|
deleted_at: updated.deleted_at,
|
||||||
created_at: updated.created_at,
|
created_at: updated.created_at,
|
||||||
|
reply_to_id: updated.reply_to_id,
|
||||||
|
reply_body: message.reply_body || '',
|
||||||
|
reply_deleted_at: message.reply_deleted_at || null,
|
||||||
|
reply_sender_name: message.reply_sender_name || '',
|
||||||
|
reply_file_name: message.reply_file_name || null,
|
||||||
|
reply_file_token: message.reply_file_token || null,
|
||||||
files: frows.map(f => ({ id: f.id, token: f.token, name: f.name, size: f.size })),
|
files: frows.map(f => ({ id: f.id, token: f.token, name: f.name, size: f.size })),
|
||||||
};
|
};
|
||||||
publishChat({
|
publishChat({
|
||||||
@@ -3812,15 +3891,18 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
|||||||
[x.id, x.tutor_id, x.last_message_at, x.last_message_text, x.tutor_unread || 0, x.admin_unread || 0, x.deleted_at || null, x.purge_at || null, x.created_at]
|
[x.id, x.tutor_id, x.last_message_at, x.last_message_text, x.tutor_unread || 0, x.admin_unread || 0, x.deleted_at || null, x.purge_at || null, x.created_at]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const restoredMsgIds = new Set();
|
||||||
for (const x of ndata.chat_messages) {
|
for (const x of ndata.chat_messages) {
|
||||||
const ex = await client.query('SELECT 1 FROM chat_threads WHERE id = $1', [x.thread_id]);
|
const ex = await client.query('SELECT 1 FROM chat_threads WHERE id = $1', [x.thread_id]);
|
||||||
if (!ex.rowCount) continue;
|
if (!ex.rowCount) continue;
|
||||||
const eu = await client.query('SELECT 1 FROM users WHERE id = $1', [x.sender_id]);
|
const eu = await client.query('SELECT 1 FROM users WHERE id = $1', [x.sender_id]);
|
||||||
if (!eu.rowCount) continue;
|
if (!eu.rowCount) continue;
|
||||||
|
const replyTo = x.reply_to_id && restoredMsgIds.has(x.reply_to_id) ? x.reply_to_id : null;
|
||||||
await client.query(
|
await client.query(
|
||||||
'INSERT INTO chat_messages (id, thread_id, sender_id, body, edited_at, deleted_at, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
'INSERT INTO chat_messages (id, thread_id, sender_id, body, edited_at, deleted_at, reply_to_id, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)',
|
||||||
[x.id, x.thread_id, x.sender_id, x.body, x.edited_at, x.deleted_at, x.created_at]
|
[x.id, x.thread_id, x.sender_id, x.body, x.edited_at, x.deleted_at, replyTo, x.created_at]
|
||||||
);
|
);
|
||||||
|
restoredMsgIds.add(x.id);
|
||||||
}
|
}
|
||||||
for (const x of ndata.chat_attachments) {
|
for (const x of ndata.chat_attachments) {
|
||||||
const ex = await client.query('SELECT 1 FROM chat_messages WHERE id = $1', [x.message_id]);
|
const ex = await client.query('SELECT 1 FROM chat_messages WHERE id = $1', [x.message_id]);
|
||||||
|
|||||||
Reference in New Issue
Block a user