Refine codex app controls and message navigation
This commit is contained in:
351
public/app.js
351
public/app.js
@@ -122,6 +122,7 @@
|
||||
let noteMode = false;
|
||||
let noteDraftSeq = 0;
|
||||
const pendingNotesByTarget = new Map();
|
||||
const userMessageIndex = new Map();
|
||||
|
||||
// --- DOM ---
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
@@ -149,6 +150,8 @@
|
||||
const chatAgentMenu = $('#chat-agent-menu');
|
||||
const chatRuntimeState = $('#chat-runtime-state');
|
||||
const chatCwd = $('#chat-cwd');
|
||||
const userOutlineBtn = $('#user-outline-btn');
|
||||
const userOutlinePanel = $('#user-outline-panel');
|
||||
const costDisplay = $('#cost-display');
|
||||
const attachmentTray = $('#attachment-tray');
|
||||
const pendingNotesTray = $('#pending-notes-tray');
|
||||
@@ -682,6 +685,79 @@
|
||||
return value ? value.slice(0, 8) : '';
|
||||
}
|
||||
|
||||
function shortMessagePreview(text, maxLength = 60) {
|
||||
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
||||
if (!value) return '空消息';
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
|
||||
}
|
||||
|
||||
function createLocalId(prefix = 'local') {
|
||||
if (window.crypto?.randomUUID) return `${prefix}-${window.crypto.randomUUID()}`;
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
function clearUserMessageIndex() {
|
||||
userMessageIndex.clear();
|
||||
}
|
||||
|
||||
function registerUserMessage(messageId, element, content) {
|
||||
if (!messageId || !element) return;
|
||||
userMessageIndex.set(messageId, {
|
||||
id: messageId,
|
||||
element,
|
||||
content: String(content || ''),
|
||||
});
|
||||
}
|
||||
|
||||
function buildUserOutlineItems() {
|
||||
return Array.from(userMessageIndex.values()).map((entry) => ({
|
||||
id: entry.id,
|
||||
targetMessageId: entry.element?.id || '',
|
||||
label: shortMessagePreview(entry.content, 64),
|
||||
})).filter((entry) => entry.targetMessageId);
|
||||
}
|
||||
|
||||
function updateUserOutlinePanel() {
|
||||
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||
const items = buildUserOutlineItems();
|
||||
if (items.length === 0) {
|
||||
userOutlinePanel.innerHTML = '<div class="user-outline-empty">暂无用户消息</div>';
|
||||
userOutlineBtn.disabled = true;
|
||||
} else {
|
||||
userOutlinePanel.innerHTML = items.map((item, index) => `
|
||||
<button type="button" class="user-outline-item" data-target="${escapeHtml(item.targetMessageId)}">
|
||||
<span class="user-outline-index">${index + 1}</span>
|
||||
<span class="user-outline-text">${escapeHtml(item.label)}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
userOutlineBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeUserOutlinePanel() {
|
||||
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||
userOutlinePanel.hidden = true;
|
||||
userOutlineBtn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function toggleUserOutlinePanel() {
|
||||
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||
if (userOutlinePanel.hidden) {
|
||||
updateUserOutlinePanel();
|
||||
userOutlinePanel.hidden = false;
|
||||
userOutlineBtn.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
closeUserOutlinePanel();
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToMessage(anchorId) {
|
||||
if (!anchorId) return;
|
||||
const target = document.getElementById(anchorId);
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function updateSessionIdBadge() {
|
||||
if (!chatSessionIdBtn) return;
|
||||
if (!currentSessionId) {
|
||||
@@ -2183,6 +2259,7 @@
|
||||
|
||||
function resetChatView(agent) {
|
||||
setCurrentAgent(agent);
|
||||
closeUserOutlinePanel();
|
||||
closeFileBrowser();
|
||||
currentSessionId = null;
|
||||
loadedHistorySessionId = null;
|
||||
@@ -2236,6 +2313,7 @@
|
||||
migratePendingNotesToSession(snapshot.sessionId, snapshotAgent);
|
||||
setCurrentSessionRunningState(snapshot.isRunning);
|
||||
setStatsDisplay(snapshot);
|
||||
closeUserOutlinePanel();
|
||||
currentCwd = snapshot.cwd || null;
|
||||
updateCwdBadge();
|
||||
if (snapshot.mode && MODE_LABELS[snapshot.mode]) {
|
||||
@@ -2261,6 +2339,7 @@
|
||||
const targetAgent = normalizeAgent(agent);
|
||||
const { preserveCurrent = true, loadLast = true } = options;
|
||||
setCurrentAgent(targetAgent);
|
||||
closeUserOutlinePanel();
|
||||
renderSessionList();
|
||||
|
||||
const currentMeta = currentSessionId ? getSessionMeta(currentSessionId) : null;
|
||||
@@ -2353,6 +2432,7 @@
|
||||
if (currentSessionId && currentSessionId !== sessionId) {
|
||||
send({ type: 'detach_view' });
|
||||
}
|
||||
closeUserOutlinePanel();
|
||||
clearSessionLoading();
|
||||
touchSessionCache(sessionId);
|
||||
applySessionSnapshot(snapshot, { immediate: true, suppressUnreadToast: true });
|
||||
@@ -2361,6 +2441,7 @@
|
||||
|
||||
function openSession(sessionId, options = {}) {
|
||||
if (!sessionId) return;
|
||||
closeUserOutlinePanel();
|
||||
if (options.forceSync) {
|
||||
beginSessionSwitch(sessionId, { blocking: options.blocking !== false, force: true, label: options.label });
|
||||
return;
|
||||
@@ -3111,7 +3192,12 @@
|
||||
const div = document.createElement('div');
|
||||
const isCrossConversation = role === 'user' && !!meta.crossConversation;
|
||||
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
|
||||
const resolvedMessageId = meta?.messageId || meta?.id || createLocalId('user');
|
||||
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}`;
|
||||
if (role === 'user') {
|
||||
div.id = `hapi-message-${resolvedMessageId}`;
|
||||
div.dataset.messageId = resolvedMessageId;
|
||||
}
|
||||
|
||||
if (role === 'system') {
|
||||
const bubble = document.createElement('div');
|
||||
@@ -3172,6 +3258,23 @@
|
||||
textNode.style.whiteSpace = 'pre-wrap';
|
||||
textNode.textContent = content;
|
||||
bubble.appendChild(textNode);
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.type = 'button';
|
||||
copyBtn.className = 'msg-copy-btn';
|
||||
copyBtn.title = '复制用户消息';
|
||||
copyBtn.setAttribute('aria-label', '复制用户消息');
|
||||
copyBtn.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="10" height="10" rx="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
`;
|
||||
copyBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
copyTextToClipboard(content, '用户消息已复制');
|
||||
});
|
||||
bubble.appendChild(copyBtn);
|
||||
}
|
||||
if (attachments.length > 0) {
|
||||
bubble.insertAdjacentHTML('beforeend', renderAttachmentPreviews(attachments));
|
||||
@@ -3186,6 +3289,9 @@
|
||||
hydrateAttachmentPreviews(bubble, attachments);
|
||||
div.appendChild(avatar);
|
||||
div.appendChild(bubble);
|
||||
if (role === 'user') {
|
||||
registerUserMessage(resolvedMessageId, div, content);
|
||||
}
|
||||
return div;
|
||||
}
|
||||
|
||||
@@ -3429,6 +3535,209 @@
|
||||
return section;
|
||||
}
|
||||
|
||||
function parseMaybeJsonObject(value) {
|
||||
if (value && typeof value === 'object') return value;
|
||||
if (typeof value !== 'string') return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function summarizePrompt(prompt) {
|
||||
const text = typeof prompt === 'string' ? prompt.trim().replace(/\s+/g, ' ') : '';
|
||||
if (!text) return '';
|
||||
return text.length > 140 ? `${text.slice(0, 140)}…` : text;
|
||||
}
|
||||
|
||||
function normalizeCollabAgentData(tool) {
|
||||
const inputData = effectiveObject(tool?.input);
|
||||
const resultData = effectiveObject(tool?.result);
|
||||
const merged = {
|
||||
...inputData,
|
||||
...resultData,
|
||||
agentsStates: resultData.agentsStates || inputData.agentsStates || {},
|
||||
receiverThreadIds: resultData.receiverThreadIds || inputData.receiverThreadIds || [],
|
||||
prompt: inputData.prompt || resultData.prompt || '',
|
||||
tool: inputData.tool || resultData.tool || tool?.name || '',
|
||||
status: resultData.status || inputData.status || tool?.meta?.status || null,
|
||||
};
|
||||
return merged;
|
||||
}
|
||||
|
||||
function effectiveObject(value) {
|
||||
const parsed = parseMaybeJsonObject(value);
|
||||
if (parsed && !Array.isArray(parsed)) return parsed;
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
return {};
|
||||
}
|
||||
|
||||
function collabAgentStateEntries(data) {
|
||||
const states = data?.agentsStates;
|
||||
if (!states || typeof states !== 'object') return [];
|
||||
return Object.entries(states).map(([id, value], index) => {
|
||||
const state = value && typeof value === 'object' ? value : { status: value };
|
||||
const label = String(state.label || state.title || state.nickname || state.name || `子代理 ${index + 1}`);
|
||||
const role = String(state.role || state.agent || state.agentType || '').trim();
|
||||
const status = String(state.status || state.state || 'pending').trim() || 'pending';
|
||||
const detail = String(state.summary || state.lastMessage || state.step || state.description || '').trim();
|
||||
return { id, label, role, status, detail };
|
||||
});
|
||||
}
|
||||
|
||||
function collabStateTone(statusText) {
|
||||
const normalized = String(statusText || '').toLowerCase();
|
||||
if (!normalized) return 'pending';
|
||||
if (/(done|completed|success|finished|idle)/.test(normalized)) return 'done';
|
||||
if (/(fail|error|cancel|aborted|rejected)/.test(normalized)) return 'error';
|
||||
if (/(running|working|active|inprogress|in_progress|executing)/.test(normalized)) return 'running';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
function collabStateLabel(statusText) {
|
||||
const normalized = String(statusText || '').trim();
|
||||
if (!normalized) return '等待中';
|
||||
const lower = normalized.toLowerCase();
|
||||
if (/(done|completed|success|finished)/.test(lower)) return '已完成';
|
||||
if (/(fail|error|rejected)/.test(lower)) return '失败';
|
||||
if (/(cancel|aborted)/.test(lower)) return '已取消';
|
||||
if (/(running|working|active|inprogress|in_progress|executing)/.test(lower)) return '进行中';
|
||||
if (/(idle|pending|queued|waiting)/.test(lower)) return '等待中';
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function createCollabAgentToolElement(tool) {
|
||||
const data = normalizeCollabAgentData(tool);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'tool-call-content collab-agent-content';
|
||||
|
||||
const stack = document.createElement('div');
|
||||
stack.className = 'collab-agent-stack';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'collab-agent-header';
|
||||
|
||||
const titleWrap = document.createElement('div');
|
||||
titleWrap.className = 'collab-agent-title-wrap';
|
||||
|
||||
const kicker = document.createElement('div');
|
||||
kicker.className = 'collab-agent-kicker';
|
||||
kicker.textContent = 'Codex App 子代理';
|
||||
titleWrap.appendChild(kicker);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'collab-agent-title';
|
||||
title.textContent = data.tool || '协作任务';
|
||||
titleWrap.appendChild(title);
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'collab-agent-meta';
|
||||
const threadCount = Array.isArray(data.receiverThreadIds) ? data.receiverThreadIds.length : 0;
|
||||
const agentCount = collabAgentStateEntries(data).length;
|
||||
meta.textContent = `${agentCount || threadCount || 0} 个子代理`;
|
||||
titleWrap.appendChild(meta);
|
||||
header.appendChild(titleWrap);
|
||||
|
||||
const statusChip = document.createElement('span');
|
||||
const overallTone = collabStateTone(data.status || (tool.done ? 'completed' : 'running'));
|
||||
statusChip.className = `collab-agent-overall-status ${overallTone}`;
|
||||
statusChip.textContent = collabStateLabel(data.status || (tool.done ? 'completed' : 'running'));
|
||||
header.appendChild(statusChip);
|
||||
stack.appendChild(header);
|
||||
|
||||
const promptText = summarizePrompt(data.prompt);
|
||||
if (promptText) {
|
||||
const promptBlock = document.createElement('div');
|
||||
promptBlock.className = 'collab-agent-prompt';
|
||||
promptBlock.textContent = promptText;
|
||||
stack.appendChild(promptBlock);
|
||||
}
|
||||
|
||||
const stateEntries = collabAgentStateEntries(data);
|
||||
if (stateEntries.length > 0) {
|
||||
const list = document.createElement('div');
|
||||
list.className = 'collab-agent-list';
|
||||
stateEntries.forEach((entry, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'collab-agent-item';
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'collab-agent-item-row';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'collab-agent-item-label';
|
||||
label.textContent = entry.label || `子代理 ${index + 1}`;
|
||||
row.appendChild(label);
|
||||
|
||||
const chip = document.createElement('span');
|
||||
const tone = collabStateTone(entry.status);
|
||||
chip.className = `collab-agent-item-status ${tone}`;
|
||||
chip.textContent = collabStateLabel(entry.status);
|
||||
row.appendChild(chip);
|
||||
item.appendChild(row);
|
||||
|
||||
if (entry.role) {
|
||||
const role = document.createElement('div');
|
||||
role.className = 'collab-agent-item-role';
|
||||
role.textContent = entry.role;
|
||||
item.appendChild(role);
|
||||
}
|
||||
|
||||
if (entry.detail) {
|
||||
const detail = document.createElement('div');
|
||||
detail.className = 'collab-agent-item-detail';
|
||||
detail.textContent = entry.detail;
|
||||
item.appendChild(detail);
|
||||
}
|
||||
|
||||
if (entry.id) {
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'collab-agent-item-footer';
|
||||
footer.textContent = `ID ${shortSessionId(entry.id)}`;
|
||||
footer.title = entry.id;
|
||||
footer.addEventListener('click', () => {
|
||||
copyTextToClipboard(entry.id, '子代理线程 ID 已复制');
|
||||
});
|
||||
item.appendChild(footer);
|
||||
}
|
||||
|
||||
list.appendChild(item);
|
||||
});
|
||||
stack.appendChild(list);
|
||||
}
|
||||
|
||||
if (Array.isArray(data.receiverThreadIds) && data.receiverThreadIds.length > 0) {
|
||||
const threads = document.createElement('div');
|
||||
threads.className = 'collab-agent-threads';
|
||||
data.receiverThreadIds.forEach((threadId) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'collab-agent-thread-chip';
|
||||
btn.textContent = `ID ${shortSessionId(threadId)}`;
|
||||
btn.title = `复制子代理线程 ID\n${threadId}`;
|
||||
btn.addEventListener('click', () => {
|
||||
copyTextToClipboard(threadId, '子代理线程 ID 已复制');
|
||||
});
|
||||
threads.appendChild(btn);
|
||||
});
|
||||
stack.appendChild(threads);
|
||||
}
|
||||
|
||||
if (!promptText && stateEntries.length === 0 && (!Array.isArray(data.receiverThreadIds) || data.receiverThreadIds.length === 0)) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'tool-call-empty';
|
||||
empty.textContent = tool.done ? '子代理调用已结束,未返回结构化状态。' : '等待子代理状态…';
|
||||
stack.appendChild(empty);
|
||||
}
|
||||
|
||||
wrapper.appendChild(stack);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function isGroupableToolCall(node) {
|
||||
return !!(node?.classList?.contains('tool-call') && node.dataset.toolKind !== 'todo_list');
|
||||
}
|
||||
@@ -3508,8 +3817,10 @@
|
||||
renderEpoch++;
|
||||
const epoch = renderEpoch;
|
||||
messagesDiv.innerHTML = '';
|
||||
clearUserMessageIndex();
|
||||
if (messages.length === 0) {
|
||||
messagesDiv.innerHTML = buildWelcomeMarkup(currentAgent);
|
||||
updateUserOutlinePanel();
|
||||
renderPendingNotes({ scroll: false });
|
||||
scrollToBottom();
|
||||
return;
|
||||
@@ -3518,6 +3829,7 @@
|
||||
const frag = document.createDocumentFragment();
|
||||
messages.forEach((message) => frag.appendChild(buildMsgElement(message)));
|
||||
messagesDiv.appendChild(frag);
|
||||
updateUserOutlinePanel();
|
||||
renderPendingNotes({ scroll: false });
|
||||
scrollToBottom();
|
||||
return;
|
||||
@@ -3540,6 +3852,7 @@
|
||||
const frag0 = document.createDocumentFragment();
|
||||
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i]));
|
||||
messagesDiv.appendChild(frag0);
|
||||
updateUserOutlinePanel();
|
||||
renderPendingNotes({ scroll: false });
|
||||
scrollToBottom();
|
||||
|
||||
@@ -3556,6 +3869,7 @@
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i]));
|
||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||
updateUserOutlinePanel();
|
||||
// Compensate scrollTop so visible area stays unchanged
|
||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||
updateScrollbar();
|
||||
@@ -3573,12 +3887,14 @@
|
||||
messages.forEach((m) => frag.appendChild(buildMsgElement(m)));
|
||||
if (!preserveScroll) {
|
||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||
updateUserOutlinePanel();
|
||||
if (!skipScrollbar) updateScrollbar();
|
||||
return;
|
||||
}
|
||||
const prevHeight = messagesDiv.scrollHeight;
|
||||
const prevScrollTop = messagesDiv.scrollTop;
|
||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||
updateUserOutlinePanel();
|
||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||
if (!skipScrollbar) updateScrollbar();
|
||||
}
|
||||
@@ -3754,6 +4070,10 @@
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (kind === 'collab_agent_tool_call') {
|
||||
return createCollabAgentToolElement(tool);
|
||||
}
|
||||
|
||||
if (effectiveName === 'AskUserQuestion') {
|
||||
const questions = extractAskUserQuestions(effectiveInput);
|
||||
if (questions.length > 0) {
|
||||
@@ -4691,7 +5011,11 @@
|
||||
function submitUserMessage(text, attachments = []) {
|
||||
const welcome = messagesDiv.querySelector('.welcome-msg');
|
||||
if (welcome) welcome.remove();
|
||||
messagesDiv.appendChild(createMsgElement('user', text, attachments));
|
||||
const messageId = createLocalId('user');
|
||||
const element = createMsgElement('user', text, attachments, { messageId });
|
||||
messagesDiv.appendChild(element);
|
||||
registerUserMessage(messageId, element, text);
|
||||
updateUserOutlinePanel();
|
||||
scrollToBottom();
|
||||
|
||||
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
|
||||
@@ -4730,7 +5054,11 @@
|
||||
appendError('Codex App 运行中暂不支持 slash 指令插入。');
|
||||
return;
|
||||
}
|
||||
messagesDiv.appendChild(createMsgElement('user', text, []));
|
||||
const messageId = createLocalId('user');
|
||||
const element = createMsgElement('user', text, [], { messageId });
|
||||
messagesDiv.appendChild(element);
|
||||
registerUserMessage(messageId, element, text);
|
||||
updateUserOutlinePanel();
|
||||
scrollToBottom();
|
||||
send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
|
||||
msgInput.value = '';
|
||||
@@ -4827,6 +5155,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (userOutlineBtn && userOutlinePanel) {
|
||||
userOutlineBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleUserOutlinePanel();
|
||||
});
|
||||
userOutlinePanel.addEventListener('click', (e) => {
|
||||
const target = e.target instanceof HTMLElement ? e.target.closest('.user-outline-item') : null;
|
||||
if (!target) return;
|
||||
const anchorId = target.getAttribute('data-target') || '';
|
||||
closeUserOutlinePanel();
|
||||
scrollToMessage(anchorId);
|
||||
});
|
||||
}
|
||||
|
||||
// Split new-chat button
|
||||
newChatBtn.addEventListener('click', () => showNewSessionModal());
|
||||
newChatArrow.addEventListener('click', (e) => {
|
||||
@@ -4854,6 +5196,11 @@
|
||||
e.target !== chatAgentBtn) {
|
||||
closeAgentMenu();
|
||||
}
|
||||
if (userOutlinePanel && !userOutlinePanel.hidden &&
|
||||
!userOutlinePanel.contains(e.target) &&
|
||||
e.target !== userOutlineBtn) {
|
||||
closeUserOutlinePanel();
|
||||
}
|
||||
});
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
if (noteModeBtn) {
|
||||
|
||||
Reference in New Issue
Block a user