Files
steam-chat/public/app/composer.js

998 lines
28 KiB
JavaScript
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.
export function createComposerController({
buildCachedImageUrl,
buildSteamEmoticonUrl,
extractEmoticonNames,
isMobileLayout,
setStatus,
send,
createRequestId,
getConversationId,
controls,
}) {
const {
messageInput,
emoticonSuggestions,
emoticonPreviewImage,
emoticonPreviewLabel,
sendMessageButton,
attachmentButton,
attachmentMenu,
chooseImageButton,
chooseUrlButton,
imageFileInput,
urlPanel,
imageUrlInput,
confirmImageUrlButton,
cancelImageUrlButton,
attachmentPreview,
attachmentPreviewImage,
attachmentPreviewTitle,
attachmentPreviewSubtitle,
clearAttachmentButton,
uploadQueue,
uploadQueueList,
pickerButton,
pickerPanel,
pickerSearch,
pickerGrid,
pickerEmpty,
pickerTabs,
dropOverlay,
} = controls;
let pendingAttachment = null;
let dragDepth = 0;
const knownEmoticons = new Set(['steamhappy', 'steamfacepalm', 'steamthumbsup', 'steamheart', 'steamsad', 'steammocking']);
let currentSuggestions = [];
let activeSuggestionIndex = 0;
let uploadQueueItems = [];
const uploadRequestMap = new Map();
let activePickerTab = 'emoticons';
let emoticonInventory = [];
let stickerInventory = [];
function autoResizeMessageInput() {
const minHeight = isMobileLayout() ? 30 : 44;
const maxHeight = isMobileLayout() ? 72 : 160;
messageInput.style.height = 'auto';
const nextHeight = Math.max(minHeight, Math.min(messageInput.scrollHeight, maxHeight));
messageInput.style.height = nextHeight + 'px';
messageInput.style.overflowY = messageInput.scrollHeight > maxHeight ? 'auto' : 'hidden';
}
function syncMessageInputPlaceholder() {
const mobilePlaceholder = messageInput.dataset.mobilePlaceholder || '输入消息';
const desktopPlaceholder = messageInput.dataset.desktopPlaceholder || mobilePlaceholder;
messageInput.placeholder = isMobileLayout() ? mobilePlaceholder : desktopPlaceholder;
}
function categorizeEmoticon(name) {
if (/^steam/i.test(name)) {
return 'Steam';
}
return '最近使用';
}
function renderUploadQueue() {
uploadQueueList.replaceChildren();
uploadQueue.classList.toggle('active', uploadQueueItems.length > 0);
uploadQueueItems.forEach((item) => {
const row = document.createElement('div');
row.className = 'upload-queue-item';
if (item.state) {
row.classList.add('is-' + item.state);
}
const name = document.createElement('div');
name.className = 'upload-queue-name';
name.textContent = item.name;
const status = document.createElement('div');
status.className = 'upload-queue-status';
status.textContent = item.statusText;
const progress = document.createElement('div');
progress.className = 'upload-queue-progress';
const bar = document.createElement('div');
bar.className = 'upload-queue-progress-bar';
if (item.state) {
bar.classList.add('is-' + item.state);
}
bar.style.width = item.progress + '%';
progress.appendChild(bar);
row.appendChild(name);
row.appendChild(status);
row.appendChild(progress);
uploadQueueList.appendChild(row);
});
}
function updateQueueItem(id, patch) {
const item = uploadQueueItems.find((entry) => entry.id === id);
if (!item) {
return;
}
Object.assign(item, patch);
renderUploadQueue();
}
function removeQueueItem(id) {
const nextItems = uploadQueueItems.filter((entry) => entry.id !== id);
if (nextItems.length === uploadQueueItems.length) {
return;
}
uploadQueueItems = nextItems;
renderUploadQueue();
}
function scheduleQueueItemRemoval(id, delay) {
window.setTimeout(() => {
removeQueueItem(id);
}, delay || 1500);
}
function createQueueItem(name, statusText, progress) {
const queueId = 'queue-' + Date.now() + '-' + Math.random().toString(16).slice(2);
uploadQueueItems.push({
id: queueId,
name: name || '图片',
progress: Number.isFinite(progress) ? progress : 0,
statusText: statusText || '准备中',
state: 'pending',
});
renderUploadQueue();
return queueId;
}
function markQueueItemCompleted(id, statusText) {
updateQueueItem(id, {
progress: 100,
statusText: statusText || '已发送',
state: 'done',
});
scheduleQueueItemRemoval(id, 1200);
}
function markQueueItemFailed(id, statusText) {
updateQueueItem(id, {
progress: 100,
statusText: statusText || '发送失败',
state: 'error',
});
scheduleQueueItemRemoval(id, 2500);
}
function resolveUploadRequest(requestId, ok, message) {
if (!requestId || !uploadRequestMap.has(requestId)) {
return false;
}
const queueId = uploadRequestMap.get(requestId);
uploadRequestMap.delete(requestId);
if (ok) {
markQueueItemCompleted(queueId, message || '已发送');
} else {
markQueueItemFailed(queueId, message || '发送失败');
}
return true;
}
function clearPendingUploadRequests(message) {
for (const [, queueId] of uploadRequestMap.entries()) {
markQueueItemFailed(queueId, message || '发送中断');
}
uploadRequestMap.clear();
}
function closeAttachmentMenu() {
attachmentMenu.classList.remove('open');
}
function toggleAttachmentMenu() {
attachmentMenu.classList.toggle('open');
}
function closeUrlPanel() {
urlPanel.classList.remove('open');
}
function togglePickerPanel() {
const isOpen = pickerPanel.classList.toggle('open');
if (isOpen) {
closeAttachmentMenu();
pickerSearch.value = '';
renderPickerGrid();
pickerSearch.focus();
}
}
function closePickerPanel() {
pickerPanel.classList.remove('open');
}
function setPickerTab(tab) {
activePickerTab = tab;
pickerTabs.forEach((btn) => {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
pickerSearch.value = '';
renderPickerGrid();
}
function insertEmoticonAtCursor(name) {
const value = messageInput.value;
const selectionStart = messageInput.selectionStart || 0;
const selectionEnd = messageInput.selectionEnd || selectionStart;
let replaceStart = selectionStart;
let replaceEnd = selectionEnd;
if (selectionStart === selectionEnd) {
const leftSide = value.slice(0, selectionStart);
const colonIndex = leftSide.lastIndexOf(':');
if (colonIndex !== -1) {
const partialName = value.slice(colonIndex + 1, selectionStart);
if (/^[a-z0-9_\-]*$/i.test(partialName)) {
let tokenEnd = selectionStart;
while (tokenEnd < value.length && /[a-z0-9_\-]/i.test(value.charAt(tokenEnd))) {
tokenEnd += 1;
}
if (value.charAt(tokenEnd) === ':') {
tokenEnd += 1;
}
replaceStart = colonIndex;
replaceEnd = tokenEnd;
}
}
}
const before = value.slice(0, replaceStart);
const after = value.slice(replaceEnd);
const insertion = ':' + name + ': ';
messageInput.value = before + insertion + after;
const newCaret = replaceStart + insertion.length;
messageInput.setSelectionRange(newCaret, newCaret);
autoResizeMessageInput();
messageInput.focus();
knownEmoticons.add(name);
}
function sendStickerMessage(name) {
const id = getConversationId();
if (!id) {
setStatus('请先选择会话');
return;
}
const msg = '[sticker type="' + name + '" limit="0"][/sticker]';
if (send({
type: 'send_message',
requestId: createRequestId('sticker-'),
id,
msg,
})) {
setStatus('贴纸已发送');
}
}
function renderPickerGrid() {
pickerGrid.replaceChildren();
const query = (pickerSearch.value || '').trim().toLowerCase();
if (activePickerTab === 'emoticons') {
pickerGrid.className = 'picker-grid emoticon-grid';
const items = emoticonInventory
.filter((e) => !query || e.name.toLowerCase().includes(query))
.sort((a, b) => (b.use_count || 0) - (a.use_count || 0) || a.name.localeCompare(b.name));
if (!items.length) {
pickerEmpty.textContent = emoticonInventory.length ? '无匹配结果' : '加载中…';
pickerEmpty.classList.add('active');
return;
}
pickerEmpty.classList.remove('active');
items.forEach((e) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'picker-item emoticon-item';
btn.title = ':' + e.name + ':';
const img = document.createElement('img');
img.src = buildCachedImageUrl(buildSteamEmoticonUrl(e.name, true));
img.alt = e.name;
img.loading = 'lazy';
const label = document.createElement('div');
label.className = 'picker-item-name';
label.textContent = e.name;
btn.appendChild(img);
btn.appendChild(label);
btn.addEventListener('click', () => {
insertEmoticonAtCursor(e.name);
closePickerPanel();
});
pickerGrid.appendChild(btn);
});
} else {
pickerGrid.className = 'picker-grid sticker-grid';
const items = stickerInventory
.filter((s) => !query || s.name.toLowerCase().includes(query))
.sort((a, b) => (b.use_count || 0) - (a.use_count || 0) || a.name.localeCompare(b.name));
if (!items.length) {
pickerEmpty.textContent = stickerInventory.length ? '无匹配结果' : '加载中…';
pickerEmpty.classList.add('active');
return;
}
pickerEmpty.classList.remove('active');
items.forEach((s) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'picker-item sticker-item';
btn.title = s.name;
const img = document.createElement('img');
img.src = location.origin + '/proxy/sticker/' + encodeURIComponent(s.name);
img.alt = s.name;
img.loading = 'lazy';
const label = document.createElement('div');
label.className = 'picker-item-name';
label.textContent = s.name.replace(/^Sticker_/, '');
btn.appendChild(img);
btn.appendChild(label);
btn.addEventListener('click', () => {
sendStickerMessage(s.name);
closePickerPanel();
});
pickerGrid.appendChild(btn);
});
}
}
function revokeAttachmentPreviewUrl(attachment) {
if (attachment && attachment.previewUrl && String(attachment.previewUrl).startsWith('blob:')) {
URL.revokeObjectURL(attachment.previewUrl);
}
}
function revokePendingAttachmentUrl() {
revokeAttachmentPreviewUrl(pendingAttachment);
}
function clearPendingAttachment() {
revokePendingAttachmentUrl();
pendingAttachment = null;
attachmentPreview.classList.remove('active');
attachmentPreviewImage.hidden = true;
attachmentPreviewImage.removeAttribute('src');
attachmentPreviewTitle.textContent = '';
attachmentPreviewSubtitle.textContent = '';
imageFileInput.value = '';
}
function setPendingAttachment(attachment) {
if (!attachment) {
clearPendingAttachment();
return;
}
revokePendingAttachmentUrl();
pendingAttachment = attachment;
attachmentPreview.classList.add('active');
attachmentPreviewTitle.textContent = attachment.title || '待发送图片';
attachmentPreviewSubtitle.textContent = attachment.subtitle || '';
if (attachment.previewUrl) {
attachmentPreviewImage.hidden = false;
attachmentPreviewImage.src = attachment.previewUrl;
} else {
attachmentPreviewImage.hidden = true;
attachmentPreviewImage.removeAttribute('src');
}
}
function readFileAsDataUrl(file, onProgress) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onprogress = (event) => {
if (typeof onProgress === 'function') {
onProgress(event.loaded || 0, event.total || file.size || 0);
}
};
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = () => reject(reader.error || new Error('文件读取失败'));
reader.readAsDataURL(file);
});
}
function createFileAttachment(file, sourceLabel) {
if (!file) {
throw new Error('请选择图片文件');
}
return {
kind: 'file',
file,
previewUrl: URL.createObjectURL(file),
title: file.name || '待发送图片',
subtitle: sourceLabel || '本地图片',
};
}
async function resolveAttachmentPayload(attachment, onProgress) {
if (!attachment) {
throw new Error('缺少图片内容');
}
if (attachment.kind === 'url' || attachment.kind === 'base64') {
return attachment;
}
if (attachment.kind !== 'file' || !attachment.file) {
throw new Error('暂不支持的图片类型');
}
const dataUrl = await readFileAsDataUrl(attachment.file, onProgress);
const base64 = dataUrl.split(',')[1] || '';
if (!base64) {
throw new Error('图片编码失败');
}
return {
kind: 'base64',
payload: base64,
previewUrl: attachment.previewUrl,
title: attachment.title,
subtitle: attachment.subtitle,
};
}
function attachFile(file, sourceLabel) {
if (!file) {
return;
}
const attachment = createFileAttachment(file, sourceLabel);
setPendingAttachment(attachment);
setStatus((sourceLabel || '图片') + ' 已添加,点击发送即可发送');
}
async function sendFileDirectly(file, sourceLabel) {
const id = getConversationId();
if (!id) {
attachFile(file, sourceLabel);
setStatus('请先选择会话,图片已加入待发送');
return;
}
const attachment = createFileAttachment(file, sourceLabel);
if (await sendAttachmentWithQueue(id, attachment, 'drop-')) {
revokeAttachmentPreviewUrl(attachment);
setStatus((sourceLabel || '图片') + ' 已发送');
}
}
async function sendFilesDirectly(files, sourceLabel) {
const imageFiles = Array.from(files || []).filter((file) => String(file.type || '').startsWith('image/'));
if (!imageFiles.length) {
return;
}
for (let index = 0; index < imageFiles.length; index += 1) {
await sendFileDirectly(imageFiles[index], imageFiles.length > 1 ? ((sourceLabel || '拖拽图片') + ' #' + (index + 1)) : sourceLabel);
}
if (imageFiles.length > 1) {
setStatus('已连续发送 ' + imageFiles.length + ' 张图片');
}
}
function confirmImageUrl() {
const url = imageUrlInput.value.trim();
if (!url) {
setStatus('请输入图片 URL');
return;
}
setPendingAttachment({
kind: 'url',
payload: url,
previewUrl: url,
title: '待发送图片 URL',
subtitle: url,
});
closeUrlPanel();
closeAttachmentMenu();
setStatus('图片 URL 已添加,点击发送即可发送');
}
function sendAttachmentPayload(id, attachment, requestId) {
const payload = {
type: 'send_image',
requestId,
id,
};
if (attachment.kind === 'url') {
payload.url = attachment.payload;
} else {
payload.img = attachment.payload;
}
return send(payload);
}
async function sendAttachmentWithQueue(id, attachment, requestPrefix) {
if (!id) {
throw new Error('请先选择会话');
}
if (!attachment) {
return false;
}
const name = attachment.title || attachment.subtitle || '图片';
const isFileAttachment = attachment.kind === 'file' && attachment.file;
const queueId = createQueueItem(name, isFileAttachment ? '读取中 0%' : '准备发送', isFileAttachment ? 0 : 20);
let resolvedAttachment;
try {
resolvedAttachment = await resolveAttachmentPayload(attachment, (loaded, total) => {
if (!isFileAttachment) {
return;
}
const ratio = total > 0 ? loaded / total : 0;
const progress = Math.max(1, Math.min(90, Math.round(ratio * 90)));
const percent = Math.max(0, Math.min(100, Math.round(ratio * 100)));
updateQueueItem(queueId, {
progress,
statusText: '读取中 ' + percent + '%',
state: 'pending',
});
});
} catch (error) {
markQueueItemFailed(queueId, error.message || '图片读取失败');
throw error;
}
const requestId = createRequestId(requestPrefix);
updateQueueItem(queueId, {
progress: 95,
statusText: '等待发送确认',
state: 'pending',
});
uploadRequestMap.set(requestId, queueId);
if (!sendAttachmentPayload(id, resolvedAttachment, requestId)) {
uploadRequestMap.delete(requestId);
markQueueItemFailed(queueId, '发送失败');
return false;
}
return true;
}
function hideSuggestions() {
currentSuggestions = [];
activeSuggestionIndex = 0;
emoticonSuggestions.classList.remove('open');
}
function getAutocompleteContext() {
const value = messageInput.value;
const caret = messageInput.selectionStart || 0;
const textBefore = value.slice(0, caret);
const match = textBefore.match(/(^|\s):([a-z0-9_][a-z0-9_\-]*)?$/i);
if (!match) {
return null;
}
return {
start: caret - (match[2] ? match[2].length + 1 : 1),
end: caret,
query: (match[2] || '').toLowerCase(),
};
}
function refreshSuggestionHighlight() {
Array.from(emoticonSuggestions.querySelectorAll('.emoticon-option')).forEach((node, index) => {
node.classList.toggle('active', index === activeSuggestionIndex);
});
if (currentSuggestions[activeSuggestionIndex]) {
updateSuggestionPreview(currentSuggestions[activeSuggestionIndex]);
}
}
function updateSuggestionPreview(name) {
emoticonPreviewImage.src = buildSteamEmoticonUrl(name, true);
emoticonPreviewLabel.textContent = ':' + name + ': · ' + categorizeEmoticon(name);
}
function applySuggestion(index = activeSuggestionIndex) {
const suggestion = currentSuggestions[index];
const context = getAutocompleteContext();
if (!suggestion || !context) {
hideSuggestions();
return;
}
const value = messageInput.value;
const replacement = ':' + suggestion + ': ';
messageInput.value = value.slice(0, context.start) + replacement + value.slice(context.end);
const caret = context.start + replacement.length;
messageInput.setSelectionRange(caret, caret);
knownEmoticons.add(suggestion);
autoResizeMessageInput();
hideSuggestions();
messageInput.focus();
}
function renderSuggestionList(names) {
Array.from(emoticonSuggestions.querySelectorAll('.emoticon-option')).forEach((node) => node.remove());
currentSuggestions = names.slice(0, 8);
activeSuggestionIndex = 0;
if (!currentSuggestions.length) {
hideSuggestions();
return;
}
updateSuggestionPreview(currentSuggestions[0]);
currentSuggestions.forEach((name, index) => {
const option = document.createElement('button');
option.type = 'button';
option.className = 'emoticon-option' + (index === 0 ? ' active' : '');
const img = document.createElement('img');
img.src = buildSteamEmoticonUrl(name, true);
img.alt = name;
const label = document.createElement('div');
label.className = 'emoticon-option-body';
const code = document.createElement('code');
code.textContent = ':' + name + ':';
const meta = document.createElement('div');
meta.className = 'emoticon-option-meta';
meta.textContent = categorizeEmoticon(name);
label.appendChild(code);
label.appendChild(meta);
option.appendChild(img);
option.appendChild(label);
option.addEventListener('click', () => applySuggestion(index));
option.addEventListener('mouseenter', () => {
activeSuggestionIndex = index;
refreshSuggestionHighlight();
updateSuggestionPreview(name);
});
emoticonSuggestions.appendChild(option);
});
emoticonSuggestions.classList.add('open');
}
function updateEmoticonSuggestions() {
const context = getAutocompleteContext();
if (!context) {
hideSuggestions();
return;
}
const query = context.query;
const names = [...knownEmoticons]
.filter((name) => !query || name.toLowerCase().includes(query))
.sort((a, b) => {
const aLower = a.toLowerCase();
const bLower = b.toLowerCase();
const aCategory = categorizeEmoticon(a);
const bCategory = categorizeEmoticon(b);
const aStarts = query ? aLower.startsWith(query) : true;
const bStarts = query ? bLower.startsWith(query) : true;
if (aStarts !== bStarts) {
return aStarts ? -1 : 1;
}
if (aCategory !== bCategory) {
return aCategory.localeCompare(bCategory);
}
return a.localeCompare(b);
});
renderSuggestionList(names);
}
async function handlePasteImage(event) {
const items = Array.from((event.clipboardData && event.clipboardData.items) || []);
const imageItem = items.find((item) => item && item.type && item.type.startsWith('image/'));
if (!imageItem) {
return false;
}
const file = imageItem.getAsFile();
if (!file) {
return false;
}
event.preventDefault();
const attachment = createFileAttachment(file, '剪切板图片');
const id = getConversationId();
if (id && await sendAttachmentWithQueue(id, attachment, 'paste-')) {
revokeAttachmentPreviewUrl(attachment);
imageUrlInput.value = '';
setStatus('剪切板图片发送中');
return true;
}
setPendingAttachment(attachment);
setStatus('剪切板图片已添加,点击发送即可发送');
return true;
}
async function handleSendMessage() {
const id = getConversationId();
const msg = messageInput.value.trim();
if (!id || (!msg && !pendingAttachment)) {
setStatus('请输入目标 SteamID并填写消息或添加图片');
return;
}
try {
if (pendingAttachment) {
const attachment = pendingAttachment;
if (await sendAttachmentWithQueue(id, attachment, 'img-')) {
clearPendingAttachment();
imageUrlInput.value = '';
setStatus('图片发送中');
}
}
if (msg) {
if (send({
type: 'send_message',
requestId: createRequestId('msg-'),
id,
msg,
})) {
messageInput.value = '';
autoResizeMessageInput();
}
}
} catch (error) {
setStatus(error.message || '发送图片失败');
}
}
function requestEmoticonInventory() {
send({
type: 'get_emoticons',
requestId: createRequestId('emoticons-'),
});
}
function applyEmoticonInventory(data) {
emoticonInventory = (data && data.emoticons) || [];
emoticonInventory.forEach((e) => { e.name = e.name.replace(/^:+|:+$/g, ''); });
stickerInventory = (data && data.stickers) || [];
emoticonInventory.forEach((e) => knownEmoticons.add(e.name));
if (pickerPanel.classList.contains('open')) {
renderPickerGrid();
}
}
function rememberEmoticonsFromMessage(message) {
extractEmoticonNames(message).forEach((name) => knownEmoticons.add(name));
}
function bindEvents() {
sendMessageButton.addEventListener('click', () => {
handleSendMessage();
});
attachmentButton.addEventListener('click', (event) => {
event.stopPropagation();
closePickerPanel();
toggleAttachmentMenu();
});
pickerButton.addEventListener('click', (event) => {
event.stopPropagation();
togglePickerPanel();
});
pickerTabs.forEach((tab) => {
tab.addEventListener('click', () => {
setPickerTab(tab.dataset.tab);
});
});
pickerSearch.addEventListener('input', renderPickerGrid);
pickerPanel.addEventListener('click', (event) => {
event.stopPropagation();
});
chooseImageButton.addEventListener('click', () => {
closeAttachmentMenu();
closeUrlPanel();
imageFileInput.click();
});
chooseUrlButton.addEventListener('click', () => {
closeAttachmentMenu();
urlPanel.classList.add('open');
imageUrlInput.focus();
});
imageFileInput.addEventListener('change', () => {
try {
const file = imageFileInput.files && imageFileInput.files[0];
if (!file) {
setStatus('请选择图片文件');
return;
}
attachFile(file, '本地图片');
} catch (error) {
setStatus(error.message || '读取图片失败');
}
});
confirmImageUrlButton.addEventListener('click', confirmImageUrl);
cancelImageUrlButton.addEventListener('click', () => {
closeUrlPanel();
imageUrlInput.value = '';
});
clearAttachmentButton.addEventListener('click', clearPendingAttachment);
document.addEventListener('click', (event) => {
if (!attachmentMenu.contains(event.target) && event.target !== attachmentButton) {
closeAttachmentMenu();
}
if (!pickerPanel.contains(event.target) && event.target !== pickerButton) {
closePickerPanel();
}
});
messageInput.addEventListener('keydown', (event) => {
if (emoticonSuggestions.classList.contains('open')) {
if (event.key === 'ArrowDown') {
event.preventDefault();
activeSuggestionIndex = (activeSuggestionIndex + 1) % currentSuggestions.length;
refreshSuggestionHighlight();
return;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
activeSuggestionIndex = (activeSuggestionIndex - 1 + currentSuggestions.length) % currentSuggestions.length;
refreshSuggestionHighlight();
return;
}
if (event.key === 'Tab' || (event.key === 'Enter' && !event.shiftKey)) {
event.preventDefault();
applySuggestion();
return;
}
if (event.key === 'Escape') {
event.preventDefault();
hideSuggestions();
return;
}
}
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessageButton.click();
}
});
messageInput.addEventListener('input', () => {
autoResizeMessageInput();
updateEmoticonSuggestions();
});
messageInput.addEventListener('click', updateEmoticonSuggestions);
messageInput.addEventListener('blur', () => {
setTimeout(hideSuggestions, 120);
});
messageInput.addEventListener('paste', (event) => {
handlePasteImage(event).catch((error) => {
setStatus(error.message || '处理剪切板图片失败');
});
});
document.addEventListener('paste', (event) => {
if (document.activeElement === messageInput) {
return;
}
handlePasteImage(event).catch((error) => {
setStatus(error.message || '处理剪切板图片失败');
});
});
document.addEventListener('dragenter', (event) => {
const hasFile = Array.from((event.dataTransfer && event.dataTransfer.items) || []).some((item) => item.kind === 'file');
if (!hasFile) {
return;
}
dragDepth += 1;
dropOverlay.classList.add('active');
});
document.addEventListener('dragover', (event) => {
const hasFile = Array.from((event.dataTransfer && event.dataTransfer.items) || []).some((item) => item.kind === 'file');
if (!hasFile) {
return;
}
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
});
document.addEventListener('dragleave', () => {
dragDepth = Math.max(0, dragDepth - 1);
if (dragDepth === 0) {
dropOverlay.classList.remove('active');
}
});
document.addEventListener('drop', (event) => {
const files = Array.from((event.dataTransfer && event.dataTransfer.files) || []).filter((file) => String(file.type || '').startsWith('image/'));
if (!files.length) {
dragDepth = 0;
dropOverlay.classList.remove('active');
return;
}
event.preventDefault();
dragDepth = 0;
dropOverlay.classList.remove('active');
sendFilesDirectly(files, '拖拽图片').catch((error) => {
setStatus(error.message || '发送拖拽图片失败');
});
});
}
function handleViewportChange() {
syncMessageInputPlaceholder();
autoResizeMessageInput();
}
return {
applyEmoticonInventory,
bindEvents,
clearPendingUploadRequests,
handleViewportChange,
hideSuggestions,
rememberEmoticonsFromMessage,
requestEmoticonInventory,
resolveUploadRequest,
};
}