| |
|
|
| import { Popper, css, DOMPurify } from '../lib.js'; |
| import { |
| addCopyToCodeBlocks, |
| appendMediaToMessage, |
| characters, |
| chat, |
| eventSource, |
| event_types, |
| getCurrentChatId, |
| getRequestHeaders, |
| name2, |
| reloadCurrentChat, |
| saveSettingsDebounced, |
| this_chid, |
| saveChatConditional, |
| chat_metadata, |
| neutralCharacterName, |
| updateChatMetadata, |
| system_message_types, |
| converter, |
| substituteParams, |
| getSystemMessageByType, |
| printMessages, |
| clearChat, |
| refreshSwipeButtons, |
| getMediaIndex, |
| getMediaDisplay, |
| chatElement, |
| } from '../script.js'; |
| import { selected_group } from './group-chats.js'; |
| import { power_user } from './power-user.js'; |
| import { |
| extractTextFromHTML, |
| extractTextFromMarkdown, |
| extractTextFromPDF, |
| extractTextFromEpub, |
| getBase64Async, |
| getStringHash, |
| humanFileSize, |
| saveBase64AsFile, |
| extractTextFromOffice, |
| download, |
| getFileText, |
| getFileExtension, |
| convertTextToBase64, |
| isSameFile, |
| clamp, |
| } from './utils.js'; |
| import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; |
| import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| import { ScraperManager } from './scrapers.js'; |
| import { DragAndDropHandler } from './dragdrop.js'; |
| import { renderTemplateAsync } from './templates.js'; |
| import { t } from './i18n.js'; |
| import { humanizedDateTime } from './RossAscends-mods.js'; |
| import { accountStorage } from './util/AccountStorage.js'; |
| import { MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './constants.js'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
|
|
| const fileSizeLimit = 1024 * 1024 * 350; |
| const ATTACHMENT_SOURCE = { |
| GLOBAL: 'global', |
| CHARACTER: 'character', |
| CHAT: 'chat', |
| }; |
|
|
| |
| |
| |
| const converters = { |
| 'application/pdf': extractTextFromPDF, |
| 'text/html': extractTextFromHTML, |
| 'text/markdown': extractTextFromMarkdown, |
| 'application/epub+zip': extractTextFromEpub, |
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': extractTextFromOffice, |
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': extractTextFromOffice, |
| 'application/vnd.openxmlformats-officedocument.presentationml.presentation': extractTextFromOffice, |
| 'application/vnd.oasis.opendocument.text': extractTextFromOffice, |
| 'application/vnd.oasis.opendocument.presentation': extractTextFromOffice, |
| 'application/vnd.oasis.opendocument.spreadsheet': extractTextFromOffice, |
| }; |
|
|
| |
| |
| |
| |
| |
| function findConverterKey(type) { |
| return Object.keys(converters).find((key) => { |
| |
| if (type === key) { |
| return true; |
| } |
|
|
| |
| if (key.endsWith('*')) { |
| return type.startsWith(key.substring(0, key.length - 1)); |
| } |
|
|
| return false; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| function isConvertible(type) { |
| return Boolean(findConverterKey(type)); |
| } |
|
|
| |
| |
| |
| |
| |
| function getConverter(type) { |
| const key = findConverterKey(type); |
| return key && converters[key]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function hideChatMessageRange(start, end, unhide, nameFitler = null) { |
| if (isNaN(start)) return; |
| if (!end) end = start; |
| const hide = !unhide; |
|
|
| for (let messageId = start; messageId <= end; messageId++) { |
| const message = chat[messageId]; |
| if (!message) continue; |
| if (nameFitler && message.name !== nameFitler) continue; |
|
|
| message.is_system = hide; |
|
|
| |
| const messageBlock = $(`.mes[mesid="${messageId}"]`); |
| if (!messageBlock.length) continue; |
| messageBlock.attr('is_system', String(hide)); |
| } |
|
|
| |
| refreshSwipeButtons(); |
|
|
| await saveChatConditional(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function hideChatMessage(messageId, _messageBlock) { |
| return hideChatMessageRange(messageId, messageId, false); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function unhideChatMessage(messageId, _messageBlock) { |
| return hideChatMessageRange(messageId, messageId, true); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function populateFileAttachment(message, inputId = 'file_form_input') { |
| try { |
| if (!message) return; |
| if (!message.extra || typeof message.extra !== 'object') message.extra = {}; |
| const fileInput = document.getElementById(inputId); |
| if (!(fileInput instanceof HTMLInputElement)) return; |
|
|
| for (const file of fileInput.files) { |
| const slug = getStringHash(file.name); |
| const fileNamePrefix = `${Date.now()}_${slug}`; |
| const fileBase64 = await getBase64Async(file); |
| let base64Data = fileBase64.split(',')[1]; |
| const extension = getFileExtension(file); |
|
|
| const mediaType = MEDIA_TYPE.getFromMime(file.type); |
| if (mediaType) { |
| const imageUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension); |
| if (!Array.isArray(message.extra.media)) { |
| message.extra.media = []; |
| } |
| |
| const mediaAttachment = { |
| url: imageUrl, |
| type: mediaType, |
| title: file.name, |
| source: MEDIA_SOURCE.UPLOAD, |
| }; |
| message.extra.media.push(mediaAttachment); |
| message.extra.media_index = message.extra.media.length - 1; |
| message.extra.inline_image = true; |
| } else { |
| const uniqueFileName = `${fileNamePrefix}.txt`; |
|
|
| if (isConvertible(file.type)) { |
| try { |
| const converter = getConverter(file.type); |
| const fileText = await converter(file); |
| base64Data = convertTextToBase64(fileText); |
| } catch (error) { |
| toastr.error(String(error), t`Could not convert file`); |
| console.error('Could not convert file', error); |
| } |
| } |
|
|
| const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data); |
|
|
| if (!fileUrl) { |
| continue; |
| } |
|
|
| if (!Array.isArray(message.extra.files)) { |
| message.extra.files = []; |
| } |
|
|
| message.extra.files.push({ |
| url: fileUrl, |
| size: file.size, |
| name: file.name, |
| created: Date.now(), |
| }); |
| } |
| } |
| } catch (error) { |
| console.error('Could not upload file', error); |
| toastr.error(t`Either the file is corrupted or its format is not supported.`, t`Could not upload the file`); |
| } finally { |
| $('#file_form').trigger('reset'); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function uploadFileAttachment(fileName, base64Data) { |
| try { |
| const result = await fetch('/api/files/upload', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| body: JSON.stringify({ |
| name: fileName, |
| data: base64Data, |
| }), |
| }); |
|
|
| if (!result.ok) { |
| const error = await result.text(); |
| throw new Error(error); |
| } |
|
|
| const responseData = await result.json(); |
| return responseData.path; |
| } catch (error) { |
| toastr.error(String(error), t`Could not upload file`); |
| console.error('Could not upload file', error); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export async function getFileAttachment(url) { |
| try { |
| const result = await fetch(url, { |
| method: 'GET', |
| cache: 'force-cache', |
| headers: getRequestHeaders(), |
| }); |
|
|
| if (!result.ok) { |
| const error = await result.text(); |
| throw new Error(error); |
| } |
|
|
| const text = await result.text(); |
| return text; |
| } catch (error) { |
| toastr.error(error, t`Could not download file`); |
| console.error('Could not download file', error); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async function validateFile(file) { |
| const fileText = await file.text(); |
| const isMedia = file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/'); |
| const isBinary = /^[\x00-\x08\x0E-\x1F\x7F-\xFF]*$/.test(fileText); |
|
|
| if (!isMedia && file.size > fileSizeLimit) { |
| toastr.error(t`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`); |
| return false; |
| } |
|
|
| |
| if (isBinary && !isMedia && !isConvertible(file.type)) { |
| toastr.error(t`Binary files are not supported. Select a text file or image.`); |
| return false; |
| } |
|
|
| return true; |
| } |
|
|
| export function hasPendingFileAttachment() { |
| const fileInput = document.getElementById('file_form_input'); |
| if (!(fileInput instanceof HTMLInputElement)) return false; |
| return fileInput.files.length > 0; |
| } |
|
|
| |
| |
| |
| |
| |
| async function onFileAttach(fileList) { |
| if (!fileList || fileList.length === 0) return; |
|
|
| for (const file of fileList) { |
| const isValid = await validateFile(file); |
|
|
| |
| if (!isValid) { |
| toastr.warning(t`File ${file.name} is not supported.`); |
| $('#file_form').trigger('reset'); |
| return; |
| } |
| } |
|
|
| const name = fileList.length === 1 ? fileList[0].name : t`${fileList.length} files selected`; |
| const size = [...fileList].reduce((acc, file) => acc + file.size, 0); |
| const title = [...fileList].map(x => x.name).join('\n'); |
| $('#file_form .file_name').text(name).attr('title', title); |
| $('#file_form .file_size').text(humanFileSize(size)).attr('title', size); |
| $('#file_form').removeClass('displayNone'); |
|
|
| |
| const currentChatId = getCurrentChatId(); |
| if (currentChatId) { |
| eventSource.once(event_types.CHAT_CHANGED, () => { |
| $('#file_form').trigger('reset'); |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function deleteMessageFile(messageBlock, messageId, fileIndex) { |
| if (isNaN(messageId) || isNaN(fileIndex)) { |
| console.warn('Invalid message ID or file index'); |
| return; |
| } |
|
|
| const confirm = await callGenericPopup('Are you sure you want to delete this file?', POPUP_TYPE.CONFIRM); |
|
|
| if (confirm !== POPUP_RESULT.AFFIRMATIVE) { |
| console.debug('Delete file cancelled'); |
| return; |
| } |
|
|
| const message = chat[messageId]; |
|
|
| if (!Array.isArray(message?.extra?.files)) { |
| console.debug('Message has no files'); |
| return; |
| } |
|
|
| if (fileIndex < 0 || fileIndex >= message.extra.files.length) { |
| console.warn('Invalid file index for message'); |
| return; |
| } |
|
|
| const url = message.extra.files[fileIndex]?.url; |
| message.extra.files.splice(fileIndex, 1); |
|
|
| await saveChatConditional(); |
| await deleteFileFromServer(url); |
|
|
| appendMediaToMessage(message, messageBlock, SCROLL_BEHAVIOR.KEEP); |
| } |
|
|
| |
| |
| |
| |
| |
| async function viewMessageFile(messageId, fileIndex) { |
| if (isNaN(messageId) || isNaN(fileIndex)) { |
| console.warn('Invalid message ID or file index'); |
| return; |
| } |
|
|
| const message = chat[messageId]; |
|
|
| if (!Array.isArray(message?.extra?.files)) { |
| console.debug('Message has no files'); |
| return; |
| } |
|
|
| if (fileIndex < 0 || fileIndex >= message.extra.files.length) { |
| console.warn('Invalid file index for message'); |
| return; |
| } |
|
|
| const messageFile = message.extra.files[fileIndex]; |
|
|
| if (!messageFile) { |
| console.debug('Message has no file or it is empty'); |
| return; |
| } |
|
|
| await openFilePopup(messageFile); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function embedMessageFile(messageId, messageBlock) { |
| const message = chat[messageId]; |
|
|
| if (!message) { |
| console.warn('Failed to find message with id', messageId); |
| return; |
| } |
|
|
| $('#embed_file_input') |
| .off('change') |
| .on('change', parseAndUploadEmbed) |
| .trigger('click'); |
|
|
| async function parseAndUploadEmbed( e) { |
| if (!(e.target instanceof HTMLInputElement)) return; |
| if (!e.target.files.length) return; |
|
|
| for (const file of e.target.files) { |
| const isValid = await validateFile(file); |
|
|
| if (!isValid) { |
| toastr.warning(t`File ${file.name} is not supported.`); |
| $('#file_form').trigger('reset'); |
| return; |
| } |
| } |
|
|
| await populateFileAttachment(message, 'embed_file_input'); |
| await eventSource.emit(event_types.MESSAGE_FILE_EMBEDDED, messageId); |
| appendMediaToMessage(message, messageBlock, SCROLL_BEHAVIOR.KEEP); |
| await saveChatConditional(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function appendFileContent(message, messageText) { |
| if (!message || !message.extra || typeof message.extra !== 'object') { |
| return messageText; |
| } |
| if (message.extra.fileLength >= 0) { |
| delete message.extra.fileLength; |
| } |
| if (Array.isArray(message.extra?.files) && message.extra.files.length > 0) { |
| const fileTexts = []; |
| for (const file of message.extra.files) { |
| const fileText = file.text || (await getFileAttachment(file.url)); |
| if (fileText) { |
| fileTexts.push(fileText); |
| } |
| } |
| const mergedFileTexts = fileTexts.join('\n\n') + '\n\n'; |
| message.extra.fileLength = mergedFileTexts.length; |
| return mergedFileTexts + messageText; |
| } |
| return messageText; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function encodeStyleTags(text) { |
| const styleRegex = /<style>(.+?)<\/style>/gims; |
| return text.replaceAll(styleRegex, (_, match) => { |
| return `<custom-style>${encodeURIComponent(match)}</custom-style>`; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) { |
| const styleDecodeRegex = /<custom-style>(.+?)<\/custom-style>/gms; |
| const mediaAllowed = isExternalMediaAllowed(); |
|
|
| function sanitizeRule(rule) { |
| if (Array.isArray(rule.selectors)) { |
| for (let i = 0; i < rule.selectors.length; i++) { |
| const selector = rule.selectors[i]; |
| if (selector) { |
| rule.selectors[i] = prefix + sanitizeSelector(selector); |
| } |
| } |
| } |
| if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { |
| rule.declarations = rule.declarations.filter(declaration => !declaration.value.includes('://')); |
| } |
| } |
|
|
| function sanitizeSelector(selector) { |
| |
| const pseudoClasses = ['has', 'not', 'where', 'is', 'matches', 'any']; |
| const pseudoRegex = new RegExp(`:(${pseudoClasses.join('|')})\\(([^)]+)\\)`, 'g'); |
|
|
| |
| selector = selector.replace(pseudoRegex, (match, pseudoClass, content) => { |
| |
| const sanitizedContent = sanitizeSimpleSelector(content); |
| return `:${pseudoClass}(${sanitizedContent})`; |
| }); |
|
|
| |
| return sanitizeSimpleSelector(selector); |
| } |
|
|
| function sanitizeSimpleSelector(selector) { |
| |
| return selector.split(/\s+/).map((part) => { |
| |
| return part.replace(/\.([\w-]+)/g, (match, className) => { |
| |
| if (className.startsWith('custom-')) { |
| return match; |
| } |
| return `.custom-${className}`; |
| }); |
| }).join(' '); |
| } |
|
|
| function sanitizeRuleSet(ruleSet) { |
| if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) { |
| sanitizeRule(ruleSet); |
| } |
|
|
| if (Array.isArray(ruleSet.rules)) { |
| ruleSet.rules = ruleSet.rules.filter(rule => rule.type !== 'import'); |
|
|
| for (const mediaRule of ruleSet.rules) { |
| sanitizeRuleSet(mediaRule); |
| } |
| } |
| } |
|
|
| return text.replaceAll(styleDecodeRegex, (_, style) => { |
| try { |
| let styleCleaned = decodeURIComponent(style).replaceAll(/<br\/>/g, ''); |
| const ast = css.parse(styleCleaned); |
| const sheet = ast?.stylesheet; |
| if (sheet) { |
| sanitizeRuleSet(ast.stylesheet); |
| } |
| return `<style>${css.stringify(ast)}</style>`; |
| } catch (error) { |
| return `CSS ERROR: ${error}`; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| class StylesPreference { |
| |
| |
| |
| |
| constructor(avatarId) { |
| this.avatarId = avatarId; |
| } |
|
|
| |
| |
| |
| get key() { |
| return `AllowGlobalStyles-${this.avatarId}`; |
| } |
|
|
| |
| |
| |
| |
| exists() { |
| return this.avatarId |
| ? accountStorage.getItem(this.key) !== null |
| : true; |
| } |
|
|
| |
| |
| |
| |
| get() { |
| return this.avatarId |
| ? accountStorage.getItem(this.key) === 'true' |
| : false; |
| } |
|
|
| |
| |
| |
| |
| set(allowed) { |
| if (this.avatarId) { |
| accountStorage.setItem(this.key, String(allowed)); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function formatCreatorNotes(text, avatarId) { |
| const preference = new StylesPreference(avatarId); |
| const sanitizeStyles = !preference.get(); |
| const decodeStyleParam = { prefix: sanitizeStyles ? '#creator_notes_spoiler ' : '' }; |
| |
| const config = { |
| RETURN_DOM: false, |
| RETURN_DOM_FRAGMENT: false, |
| RETURN_TRUSTED_TYPE: false, |
| MESSAGE_SANITIZE: true, |
| ADD_TAGS: ['custom-style'], |
| }; |
|
|
| let html = converter.makeHtml(substituteParams(text)); |
| html = encodeStyleTags(html); |
| html = DOMPurify.sanitize(html, config); |
| html = decodeStyleTags(html, decodeStyleParam); |
|
|
| return html; |
| } |
|
|
| async function openGlobalStylesPreferenceDialog() { |
| if (selected_group) { |
| toastr.info(t`To change the global styles preference, please select a character individually.`); |
| return; |
| } |
|
|
| const entityId = getCurrentEntityId(); |
| const preference = new StylesPreference(entityId); |
| const currentValue = preference.get(); |
|
|
| const template = $(await renderTemplateAsync('globalStylesPreference')); |
|
|
| const allowedRadio = template.find('#global_styles_allowed'); |
| const forbiddenRadio = template.find('#global_styles_forbidden'); |
|
|
| allowedRadio.on('change', () => { |
| preference.set(true); |
| allowedRadio.prop('checked', true); |
| forbiddenRadio.prop('checked', false); |
| }); |
|
|
| forbiddenRadio.on('change', () => { |
| preference.set(false); |
| allowedRadio.prop('checked', false); |
| forbiddenRadio.prop('checked', true); |
| }); |
|
|
| const currentPreferenceRadio = currentValue ? allowedRadio : forbiddenRadio; |
| template.find(currentPreferenceRadio).prop('checked', true); |
|
|
| await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: false, large: false }); |
|
|
| |
| const newValue = preference.get(); |
| if (newValue !== currentValue) { |
| $('#rm_button_selected_ch').trigger('click'); |
| setGlobalStylesButtonClass(newValue); |
| } |
| } |
|
|
| async function checkForCreatorNotesStyles() { |
| |
| if (selected_group || this_chid === undefined) { |
| return; |
| } |
|
|
| const notes = characters[this_chid].data?.creator_notes || characters[this_chid].creatorcomment; |
| const avatarId = characters[this_chid].avatar; |
| const styleContents = getStyleContentsFromMarkdown(notes); |
|
|
| if (!styleContents) { |
| setGlobalStylesButtonClass(null); |
| return; |
| } |
|
|
| const preference = new StylesPreference(avatarId); |
| const hasPreference = preference.exists(); |
| if (!hasPreference) { |
| const template = $(await renderTemplateAsync('globalStylesPopup')); |
| template.find('textarea').val(styleContents); |
| const confirmResult = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { |
| wide: false, |
| large: false, |
| okButton: t`Just to Creator's Notes`, |
| cancelButton: t`Apply to the entire app`, |
| }); |
|
|
| switch (confirmResult) { |
| case POPUP_RESULT.AFFIRMATIVE: |
| preference.set(false); |
| break; |
| case POPUP_RESULT.NEGATIVE: |
| preference.set(true); |
| break; |
| case POPUP_RESULT.CANCELLED: |
| preference.set(false); |
| break; |
| } |
|
|
| $('#rm_button_selected_ch').trigger('click'); |
| } |
|
|
| const currentPreference = preference.get(); |
| setGlobalStylesButtonClass(currentPreference); |
| } |
|
|
| |
| |
| |
| |
| function setGlobalStylesButtonClass(state) { |
| const button = $('#creators_note_styles_button'); |
| button.toggleClass('empty', state === null); |
| button.toggleClass('allowed', state === true); |
| button.toggleClass('forbidden', state === false); |
| } |
|
|
| |
| |
| |
| |
| |
| function getStyleContentsFromMarkdown(text) { |
| if (!text) { |
| return ''; |
| } |
|
|
| const html = converter.makeHtml(substituteParams(text)); |
| const parsedDocument = new DOMParser().parseFromString(html, 'text/html'); |
| const styleElements = Array.from(parsedDocument.querySelectorAll('style')); |
| return styleElements |
| .filter(s => s.textContent.trim().length > 0) |
| .map(s => s.textContent.trim()) |
| .join('\n\n'); |
| } |
|
|
| async function openExternalMediaOverridesDialog() { |
| const entityId = getCurrentEntityId(); |
|
|
| if (!entityId) { |
| toastr.info(t`No character or group selected`); |
| return; |
| } |
|
|
| const template = $(await renderTemplateAsync('forbidMedia')); |
| template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_media); |
| template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_media); |
|
|
| if (power_user.external_media_allowed_overrides.includes(entityId)) { |
| template.find('#forbid_media_override_allowed').prop('checked', true); |
| } |
| else if (power_user.external_media_forbidden_overrides.includes(entityId)) { |
| template.find('#forbid_media_override_forbidden').prop('checked', true); |
| } |
| else { |
| template.find('#forbid_media_override_global').prop('checked', true); |
| } |
|
|
| callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: false, large: false }); |
| } |
|
|
| export function getCurrentEntityId() { |
| if (selected_group) { |
| return String(selected_group); |
| } |
|
|
| return characters[this_chid]?.avatar ?? null; |
| } |
|
|
| export function isExternalMediaAllowed() { |
| const entityId = getCurrentEntityId(); |
| if (!entityId) { |
| return !power_user.forbid_external_media; |
| } |
|
|
| if (power_user.external_media_allowed_overrides.includes(entityId)) { |
| return true; |
| } |
|
|
| if (power_user.external_media_forbidden_overrides.includes(entityId)) { |
| return false; |
| } |
|
|
| return !power_user.forbid_external_media; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function expandMessageMedia(messageId, mediaIndex) { |
| if (isNaN(messageId) || isNaN(mediaIndex)) { |
| console.warn('Invalid message ID or media index'); |
| return; |
| } |
|
|
| |
| const message = chat[messageId]; |
|
|
| if (!Array.isArray(message?.extra?.media) || message.extra.media.length === 0) { |
| console.warn('Message has no media to expand'); |
| return; |
| } |
|
|
| const mediaAttachment = message.extra.media[mediaIndex]; |
| const title = mediaAttachment.title || message.extra.title || ''; |
|
|
| if (!mediaAttachment) { |
| return; |
| } |
|
|
| if (mediaAttachment.type === MEDIA_TYPE.AUDIO) { |
| console.warn('Audio media cannot be expanded'); |
| return; |
| } |
|
|
| |
| |
| |
| |
| function getMediaElement() { |
| function getImageElement() { |
| const img = document.createElement('img'); |
| img.src = mediaAttachment.url; |
| img.classList.add('img_enlarged'); |
| return img; |
| } |
|
|
| function getVideoElement() { |
| const video = document.createElement('video'); |
| video.src = mediaAttachment.url; |
| video.classList.add('img_enlarged'); |
| video.controls = true; |
| video.autoplay = true; |
| return video; |
| } |
|
|
| switch (mediaAttachment.type) { |
| case MEDIA_TYPE.IMAGE: |
| return getImageElement(); |
| case MEDIA_TYPE.VIDEO: |
| return getVideoElement(); |
| } |
|
|
| console.warn('Unsupported media type for enlargement:', mediaAttachment.type); |
| return getImageElement(); |
| } |
|
|
| const mediaElement = getMediaElement(); |
| const mediaHolder = document.createElement('div'); |
| mediaHolder.classList.add('img_enlarged_holder'); |
| mediaHolder.append(mediaElement); |
| const mediaContainer = document.createElement('div'); |
| mediaContainer.classList.add('img_enlarged_container'); |
| mediaContainer.append(mediaHolder); |
|
|
| mediaElement.addEventListener('click', event => { |
| const shouldZoom = !mediaElement.classList.contains('zoomed') && mediaElement.nodeName === 'IMG'; |
| mediaElement.classList.toggle('zoomed', shouldZoom); |
| event.stopPropagation(); |
| }); |
|
|
| if (title.trim().length > 0) { |
| const mediaTitlePre = document.createElement('pre'); |
| const mediaTitleCode = document.createElement('code'); |
| mediaTitleCode.classList.add('img_enlarged_title', 'txt'); |
| mediaTitleCode.textContent = title; |
| mediaTitlePre.append(mediaTitleCode); |
| mediaTitleCode.addEventListener('click', event => { |
| event.stopPropagation(); |
| }); |
| mediaContainer.append(mediaTitlePre); |
| addCopyToCodeBlocks(mediaContainer); |
| } |
|
|
| const popup = new Popup(mediaContainer, POPUP_TYPE.DISPLAY, '', { large: true, transparent: true }); |
|
|
| popup.dlg.style.width = 'unset'; |
| popup.dlg.style.height = 'unset'; |
| popup.dlg.addEventListener('click', () => { |
| popup.completeCancelled(); |
| }); |
|
|
| popup.show(); |
| return mediaElement; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function deleteMessageMedia(messageId, mediaIndex, messageBlock) { |
| if (isNaN(messageId) || isNaN(mediaIndex)) { |
| console.warn('Invalid message ID or media index'); |
| return; |
| } |
|
|
| const deleteUrls = []; |
| const deleteFromServerId = 'delete_media_files_checkbox'; |
| let deleteFromServer = true; |
|
|
| const value = await Popup.show.confirm(t`Delete media from message?`, t`This action can't be undone.`, { |
| okButton: t`Delete one`, |
| cancelButton: false, |
| customButtons: [ |
| { |
| text: t`Delete all`, |
| appendAtEnd: true, |
| result: POPUP_RESULT.CUSTOM1, |
| }, |
| { |
| text: t`Cancel`, |
| appendAtEnd: true, |
| result: POPUP_RESULT.CANCELLED, |
| }, |
| ], |
| customInputs: [ |
| { |
| type: 'checkbox', |
| label: t`Also delete files from server`, |
| id: deleteFromServerId, |
| defaultState: true, |
| }, |
| ], |
| onClose: (popup) => { |
| deleteFromServer = Boolean(popup.inputResults.get(deleteFromServerId) ?? false); |
| }, |
| }); |
|
|
| if (!value) { |
| return; |
| } |
|
|
| |
| const message = chat[messageId]; |
|
|
| if (!Array.isArray(message?.extra?.media)) { |
| console.debug('Message has no media'); |
| return; |
| } |
|
|
| if (mediaIndex < 0 || mediaIndex >= message.extra.media.length) { |
| console.warn('Invalid media index for message'); |
| return; |
| } |
|
|
| deleteUrls.push(message.extra.media[mediaIndex].url); |
| message.extra.media.splice(mediaIndex, 1); |
|
|
| if (message.extra.media_index === mediaIndex) { |
| const newIndex = mediaIndex > 0 ? mediaIndex - 1 : 0; |
| message.extra.media_index = clamp(newIndex, 0, message.extra.media.length - 1); |
| } |
|
|
| if (value === POPUP_RESULT.CUSTOM1) { |
| for (const media of message.extra.media) { |
| deleteUrls.push(media.url); |
| } |
| delete message.extra.media; |
| delete message.extra.inline_image; |
| delete message.extra.title; |
| delete message.extra.append_title; |
| } |
|
|
| if (deleteFromServer) { |
| for (const url of deleteUrls) { |
| if (!url) continue; |
| await deleteMediaFromServer(url, true); |
| } |
| } |
|
|
| await saveChatConditional(); |
| appendMediaToMessage(message, messageBlock, SCROLL_BEHAVIOR.KEEP); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function switchMessageMediaDisplay(messageId, messageBlock, targetDisplay) { |
| if (isNaN(messageId)) { |
| console.warn('Invalid message ID'); |
| return; |
| } |
|
|
| |
| const message = chat[messageId]; |
|
|
| if (!message) { |
| console.warn('Message not found for ID', messageId); |
| return; |
| } |
|
|
| if (!message.extra || typeof message.extra !== 'object') { |
| message.extra = {}; |
| } |
|
|
| message.extra.media_display = targetDisplay; |
| await saveChatConditional(); |
| appendMediaToMessage(message, messageBlock, SCROLL_BEHAVIOR.KEEP); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function deleteMediaFromServer(url, silent = false) { |
| try { |
| const result = await fetch('/api/images/delete', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| body: JSON.stringify({ path: url }), |
| }); |
|
|
| if (!result.ok) { |
| if (!silent) { |
| const error = await result.text(); |
| throw new Error(error); |
| } |
| return false; |
| } |
|
|
| await eventSource.emit(event_types.MEDIA_ATTACHMENT_DELETED, url); |
| return true; |
| } catch (error) { |
| toastr.error(String(error), t`Could not delete image`); |
| console.error('Could not delete image', error); |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function deleteFileFromServer(url, silent = false) { |
| try { |
| const result = await fetch('/api/files/delete', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| body: JSON.stringify({ path: url }), |
| }); |
|
|
| if (!result.ok) { |
| if (!silent) { |
| const error = await result.text(); |
| throw new Error(error); |
| } |
| return false; |
| } |
|
|
| await eventSource.emit(event_types.FILE_ATTACHMENT_DELETED, url); |
| return true; |
| } catch (error) { |
| toastr.error(String(error), t`Could not delete file`); |
| console.error('Could not delete file', error); |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| async function openFilePopup(attachment) { |
| const fileText = attachment.text || (await getFileAttachment(attachment.url)); |
|
|
| const modalTemplate = $('<div><pre><code></code></pre></div>'); |
| modalTemplate.find('code').addClass('txt').text(fileText); |
| modalTemplate.addClass('file_modal').addClass('textarea_compact').addClass('fontsize90p'); |
| addCopyToCodeBlocks(modalTemplate); |
|
|
| callGenericPopup(modalTemplate, POPUP_TYPE.TEXT, '', { wide: true, large: true }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function editAttachment(attachment, source, callback) { |
| const originalFileText = attachment.text || (await getFileAttachment(attachment.url)); |
| const template = $(await renderExtensionTemplateAsync('attachments', 'notepad')); |
|
|
| let editedFileText = originalFileText; |
| template.find('[name="notepadFileContent"]').val(editedFileText).on('input', function () { |
| editedFileText = String($(this).val()); |
| }); |
|
|
| let editedFileName = attachment.name; |
| template.find('[name="notepadFileName"]').val(editedFileName).on('input', function () { |
| editedFileName = String($(this).val()); |
| }); |
|
|
| const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: true, large: true, okButton: 'Save', cancelButton: 'Cancel' }); |
|
|
| if (result !== POPUP_RESULT.AFFIRMATIVE) { |
| return; |
| } |
|
|
| if (editedFileText === originalFileText && editedFileName === attachment.name) { |
| return; |
| } |
|
|
| const nullCallback = () => { }; |
| await deleteAttachment(attachment, source, nullCallback, false); |
| const file = new File([editedFileText], editedFileName, { type: 'text/plain' }); |
| await uploadFileAttachmentToServer(file, source); |
|
|
| callback(); |
| } |
|
|
| |
| |
| |
| |
| async function downloadAttachment(attachment) { |
| const fileText = attachment.text || (await getFileAttachment(attachment.url)); |
| const blob = new Blob([fileText], { type: 'text/plain' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = attachment.name; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } |
|
|
| |
| |
| |
| |
| |
| function enableAttachment(attachment, callback) { |
| ensureAttachmentsExist(); |
| extension_settings.disabled_attachments = extension_settings.disabled_attachments.filter(url => url !== attachment.url); |
| saveSettingsDebounced(); |
| callback(); |
| } |
|
|
| |
| |
| |
| |
| |
| function disableAttachment(attachment, callback) { |
| ensureAttachmentsExist(); |
| extension_settings.disabled_attachments.push(attachment.url); |
| saveSettingsDebounced(); |
| callback(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function moveAttachment(attachment, source, callback) { |
| let selectedTarget = source; |
| const targets = getAvailableTargets(); |
| const template = $(await renderExtensionTemplateAsync('attachments', 'move-attachment', { name: attachment.name, targets })); |
| template.find('.moveAttachmentTarget').val(source).on('input', function () { |
| selectedTarget = String($(this).val()); |
| }); |
|
|
| const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Move', cancelButton: 'Cancel' }); |
|
|
| if (result !== POPUP_RESULT.AFFIRMATIVE) { |
| console.debug('Move attachment cancelled'); |
| return; |
| } |
|
|
| if (selectedTarget === source) { |
| console.debug('Move attachment cancelled: same source and target'); |
| return; |
| } |
|
|
| const content = await getFileAttachment(attachment.url); |
| const file = new File([content], attachment.name, { type: 'text/plain' }); |
| await deleteAttachment(attachment, source, () => { }, false); |
| await uploadFileAttachmentToServer(file, selectedTarget); |
| callback(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function deleteAttachment(attachment, source, callback, confirm = true) { |
| if (confirm) { |
| const result = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM); |
|
|
| if (result !== POPUP_RESULT.AFFIRMATIVE) { |
| return; |
| } |
| } |
|
|
| ensureAttachmentsExist(); |
|
|
| switch (source) { |
| case 'global': |
| extension_settings.attachments = extension_settings.attachments.filter((a) => a.url !== attachment.url); |
| saveSettingsDebounced(); |
| break; |
| case 'chat': |
| chat_metadata.attachments = chat_metadata.attachments.filter((a) => a.url !== attachment.url); |
| saveMetadataDebounced(); |
| break; |
| case 'character': |
| extension_settings.character_attachments[characters[this_chid]?.avatar] = extension_settings.character_attachments[characters[this_chid]?.avatar].filter((a) => a.url !== attachment.url); |
| break; |
| } |
|
|
| if (Array.isArray(extension_settings.disabled_attachments) && extension_settings.disabled_attachments.includes(attachment.url)) { |
| extension_settings.disabled_attachments = extension_settings.disabled_attachments.filter(url => url !== attachment.url); |
| saveSettingsDebounced(); |
| } |
|
|
| const silent = confirm === false; |
| await deleteFileFromServer(attachment.url, silent); |
| callback(); |
| } |
|
|
| |
| |
| |
| |
| |
| function isAttachmentDisabled(attachment) { |
| return extension_settings.disabled_attachments.some(url => url === attachment?.url); |
| } |
|
|
| |
| |
| |
| async function openAttachmentManager() { |
| |
| |
| |
| |
| |
| async function renderList(attachments, source) { |
| |
| |
| |
| |
| |
| |
| function sortFn(a, b) { |
| const sortValueA = a[sortField]; |
| const sortValueB = b[sortField]; |
| if (typeof sortValueA === 'string' && typeof sortValueB === 'string') { |
| return sortValueA.localeCompare(sortValueB) * (sortOrder === 'asc' ? 1 : -1); |
| } |
| return (sortValueA - sortValueB) * (sortOrder === 'asc' ? 1 : -1); |
| } |
|
|
| |
| |
| |
| |
| |
| function filterFn(a) { |
| if (!filterString) { |
| return true; |
| } |
|
|
| return a.name.toLowerCase().includes(filterString.toLowerCase()); |
| } |
| const sources = { |
| [ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsList', |
| [ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsList', |
| [ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList', |
| }; |
|
|
| const selected = template |
| .find(sources[source]) |
| .find('.attachmentListItemCheckbox:checked') |
| .map((_, el) => $(el).closest('.attachmentListItem').attr('data-attachment-url')) |
| .get(); |
|
|
| template.find(sources[source]).empty(); |
|
|
| |
| const sortedAttachmentList = attachments.slice().filter(filterFn).sort(sortFn); |
|
|
| for (const attachment of sortedAttachmentList) { |
| const isDisabled = isAttachmentDisabled(attachment); |
| const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone(); |
| attachmentTemplate.toggleClass('disabled', isDisabled); |
| attachmentTemplate.attr('data-attachment-url', attachment.url); |
| attachmentTemplate.attr('data-attachment-source', source); |
| attachmentTemplate.find('.attachmentFileIcon').attr('title', attachment.url); |
| attachmentTemplate.find('.attachmentListItemName').text(attachment.name); |
| attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size)); |
| attachmentTemplate.find('.attachmentListItemCreated').text(new Date(attachment.created).toLocaleString()); |
| attachmentTemplate.find('.viewAttachmentButton').on('click', () => openFilePopup(attachment)); |
| attachmentTemplate.find('.editAttachmentButton').on('click', () => editAttachment(attachment, source, renderAttachments)); |
| attachmentTemplate.find('.deleteAttachmentButton').on('click', () => deleteAttachment(attachment, source, renderAttachments)); |
| attachmentTemplate.find('.downloadAttachmentButton').on('click', () => downloadAttachment(attachment)); |
| attachmentTemplate.find('.moveAttachmentButton').on('click', () => moveAttachment(attachment, source, renderAttachments)); |
| attachmentTemplate.find('.enableAttachmentButton').toggle(isDisabled).on('click', () => enableAttachment(attachment, renderAttachments)); |
| attachmentTemplate.find('.disableAttachmentButton').toggle(!isDisabled).on('click', () => disableAttachment(attachment, renderAttachments)); |
| template.find(sources[source]).append(attachmentTemplate); |
|
|
| if (selected.includes(attachment.url)) { |
| attachmentTemplate.find('.attachmentListItemCheckbox').prop('checked', true); |
| } |
| } |
| } |
|
|
| |
| |
| |
| async function renderButtons() { |
| const sources = { |
| [ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsTitle', |
| [ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsTitle', |
| [ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsTitle', |
| }; |
|
|
| const modal = template.find('.actionButtonsModal').hide(); |
| const scrapers = ScraperManager.getDataBankScrapers(); |
|
|
| for (const scraper of scrapers) { |
| const isAvailable = await ScraperManager.isScraperAvailable(scraper.id); |
| if (!isAvailable) { |
| continue; |
| } |
|
|
| const buttonTemplate = template.find('.actionButtonTemplate .actionButton').clone(); |
| if (scraper.iconAvailable) { |
| buttonTemplate.find('.actionButtonIcon').addClass(scraper.iconClass); |
| buttonTemplate.find('.actionButtonImg').remove(); |
| } else { |
| buttonTemplate.find('.actionButtonImg').attr('src', scraper.iconClass); |
| buttonTemplate.find('.actionButtonIcon').remove(); |
| } |
| buttonTemplate.find('.actionButtonText').text(scraper.name); |
| buttonTemplate.attr('title', scraper.description); |
| buttonTemplate.on('click', () => { |
| const target = modal.attr('data-attachment-manager-target'); |
| runScraper(scraper.id, target, renderAttachments); |
| }); |
| modal.append(buttonTemplate); |
| } |
|
|
| const modalButtonData = Object.entries(sources).map(entry => { |
| const [source, selector] = entry; |
| const button = template.find(selector).find('.openActionModalButton').get(0); |
|
|
| if (!button) { |
| return; |
| } |
|
|
| const bodyListener = (e) => { |
| if (modal.is(':visible') && (!$(e.target).closest('.openActionModalButton').length)) { |
| modal.hide(); |
| } |
|
|
| |
| if ($(e.target).closest('.openActionModalButton').length && !modal.is(':visible')) { |
| modal.show(); |
| } |
| }; |
| document.body.addEventListener('click', bodyListener); |
|
|
| const popper = Popper.createPopper(button, modal.get(0), { placement: 'bottom-end' }); |
| button.addEventListener('click', () => { |
| modal.attr('data-attachment-manager-target', source); |
| modal.toggle(); |
| popper.update(); |
| }); |
|
|
| return { popper, bodyListener }; |
| }).filter(Boolean); |
|
|
| return () => { |
| modalButtonData.forEach(p => { |
| const { popper, bodyListener } = p; |
| popper.destroy(); |
| document.body.removeEventListener('click', bodyListener); |
| }); |
| modal.remove(); |
| }; |
| } |
|
|
| async function renderAttachments() { |
| |
| const globalAttachments = extension_settings.attachments ?? []; |
| |
| const chatAttachments = chat_metadata.attachments ?? []; |
| |
| const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? []; |
|
|
| await renderList(globalAttachments, ATTACHMENT_SOURCE.GLOBAL); |
| await renderList(chatAttachments, ATTACHMENT_SOURCE.CHAT); |
| await renderList(characterAttachments, ATTACHMENT_SOURCE.CHARACTER); |
|
|
| const isNotCharacter = this_chid === undefined || selected_group; |
| const isNotInChat = getCurrentChatId() === undefined; |
| template.find('.characterAttachmentsBlock').toggle(!isNotCharacter); |
| template.find('.chatAttachmentsBlock').toggle(!isNotInChat); |
|
|
| const characterName = characters[this_chid]?.name || 'Anonymous'; |
| template.find('.characterAttachmentsName').text(characterName); |
|
|
| const chatName = getCurrentChatId() || 'Unnamed chat'; |
| template.find('.chatAttachmentsName').text(chatName); |
| } |
|
|
| const dragDropHandler = new DragAndDropHandler('.popup', async (files, event) => { |
| let selectedTarget = ATTACHMENT_SOURCE.GLOBAL; |
| const targets = getAvailableTargets(); |
|
|
| const targetSelectTemplate = $(await renderExtensionTemplateAsync('attachments', 'files-dropped', { count: files.length, targets: targets })); |
| targetSelectTemplate.find('.droppedFilesTarget').on('input', function () { |
| selectedTarget = String($(this).val()); |
| }); |
| const result = await callGenericPopup(targetSelectTemplate, POPUP_TYPE.CONFIRM, '', { wide: false, large: false, okButton: 'Upload', cancelButton: 'Cancel' }); |
| if (result !== POPUP_RESULT.AFFIRMATIVE) { |
| console.log('File upload cancelled'); |
| return; |
| } |
| for (const file of files) { |
| await uploadFileAttachmentToServer(file, selectedTarget); |
| } |
| renderAttachments(); |
| }); |
|
|
| let sortField = accountStorage.getItem('DataBank_sortField') || 'created'; |
| let sortOrder = accountStorage.getItem('DataBank_sortOrder') || 'desc'; |
| let filterString = ''; |
|
|
| const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {})); |
|
|
| template.find('.attachmentSearch').on('input', function () { |
| filterString = String($(this).val()); |
| renderAttachments(); |
| }); |
| template.find('.attachmentSort').on('change', function () { |
| if (!(this instanceof HTMLSelectElement) || this.selectedOptions.length === 0) { |
| return; |
| } |
|
|
| sortField = this.selectedOptions[0].dataset.sortField; |
| sortOrder = this.selectedOptions[0].dataset.sortOrder; |
| accountStorage.setItem('DataBank_sortField', sortField); |
| accountStorage.setItem('DataBank_sortOrder', sortOrder); |
| renderAttachments(); |
| }); |
| function handleBulkAction(action) { |
| return async () => { |
| const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked'); |
|
|
| if (selectedAttachments.length === 0) { |
| toastr.info(t`No attachments selected.`, t`Data Bank`); |
| return; |
| } |
|
|
| if (action.confirmMessage) { |
| const confirm = await callGenericPopup(action.confirmMessage, POPUP_TYPE.CONFIRM); |
| if (confirm !== POPUP_RESULT.AFFIRMATIVE) { |
| return; |
| } |
| } |
|
|
| const includeDisabled = true; |
| const attachments = getDataBankAttachments(includeDisabled); |
| selectedAttachments.forEach(async (checkbox) => { |
| const listItem = checkbox.closest('.attachmentListItem'); |
| if (!(listItem instanceof HTMLElement)) { |
| return; |
| } |
| const url = listItem.dataset.attachmentUrl; |
| const source = listItem.dataset.attachmentSource; |
| const attachment = attachments.find(a => a.url === url); |
| if (!attachment) { |
| return; |
| } |
| await action.perform(attachment, source); |
| }); |
|
|
| document.querySelectorAll('.attachmentListItemCheckbox, .attachmentsBulkEditCheckbox').forEach(checkbox => { |
| if (checkbox instanceof HTMLInputElement) { |
| checkbox.checked = false; |
| } |
| }); |
|
|
| await renderAttachments(); |
| }; |
| } |
|
|
| template.find('.bulkActionDisable').on('click', handleBulkAction({ |
| perform: (attachment) => disableAttachment(attachment, () => { }), |
| })); |
|
|
| template.find('.bulkActionEnable').on('click', handleBulkAction({ |
| perform: (attachment) => enableAttachment(attachment, () => { }), |
| })); |
|
|
| template.find('.bulkActionDelete').on('click', handleBulkAction({ |
| confirmMessage: 'Are you sure you want to delete the selected attachments?', |
| perform: async (attachment, source) => await deleteAttachment(attachment, source, () => { }, false), |
| })); |
|
|
| template.find('.bulkActionSelectAll').on('click', () => { |
| $('.attachmentListItemCheckbox:visible').each((_, checkbox) => { |
| if (checkbox instanceof HTMLInputElement) { |
| checkbox.checked = true; |
| } |
| }); |
| }); |
| template.find('.bulkActionSelectNone').on('click', () => { |
| $('.attachmentListItemCheckbox:visible').each((_, checkbox) => { |
| if (checkbox instanceof HTMLInputElement) { |
| checkbox.checked = false; |
| } |
| }); |
| }); |
|
|
| const cleanupFn = await renderButtons(); |
| await verifyAttachments(); |
| await renderAttachments(); |
| await callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close', allowVerticalScrolling: true }); |
|
|
| cleanupFn(); |
| dragDropHandler.destroy(); |
| } |
|
|
| |
| |
| |
| |
| function getAvailableTargets() { |
| const targets = Object.values(ATTACHMENT_SOURCE); |
|
|
| const isNotCharacter = this_chid === undefined || selected_group; |
| const isNotInChat = getCurrentChatId() === undefined; |
|
|
| if (isNotCharacter) { |
| targets.splice(targets.indexOf(ATTACHMENT_SOURCE.CHARACTER), 1); |
| } |
|
|
| if (isNotInChat) { |
| targets.splice(targets.indexOf(ATTACHMENT_SOURCE.CHAT), 1); |
| } |
|
|
| return targets; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function runScraper(scraperId, target, callback) { |
| try { |
| console.log(`Running scraper ${scraperId} for ${target}`); |
| const files = await ScraperManager.runDataBankScraper(scraperId); |
|
|
| if (!Array.isArray(files)) { |
| console.warn('Scraping returned nothing'); |
| return; |
| } |
|
|
| if (files.length === 0) { |
| console.warn('Scraping returned no files'); |
| toastr.info(t`No files were scraped.`, t`Data Bank`); |
| return; |
| } |
|
|
| for (const file of files) { |
| await uploadFileAttachmentToServer(file, target); |
| } |
|
|
| toastr.success(t`Scraped ${files.length} files from ${scraperId} to ${target}.`, t`Data Bank`); |
| callback(); |
| } |
| catch (error) { |
| console.error('Scraping failed', error); |
| toastr.error(t`Check browser console for details.`, t`Scraping failed`); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function uploadFileAttachmentToServer(file, target) { |
| const isValid = await validateFile(file); |
|
|
| if (!isValid) { |
| return; |
| } |
|
|
| let base64Data = await getBase64Async(file); |
| const slug = getStringHash(file.name); |
| const uniqueFileName = `${Date.now()}_${slug}.txt`; |
|
|
| if (isConvertible(file.type)) { |
| try { |
| const converter = getConverter(file.type); |
| const fileText = await converter(file); |
| base64Data = convertTextToBase64(fileText); |
| } catch (error) { |
| toastr.error(String(error), t`Could not convert file`); |
| console.error('Could not convert file', error); |
| } |
| } else { |
| const fileText = await file.text(); |
| base64Data = convertTextToBase64(fileText); |
| } |
|
|
| const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data); |
| const convertedSize = Math.round(base64Data.length * 0.75); |
|
|
| if (!fileUrl) { |
| return; |
| } |
|
|
| const attachment = { |
| url: fileUrl, |
| size: convertedSize, |
| name: file.name, |
| created: Date.now(), |
| }; |
|
|
| ensureAttachmentsExist(); |
|
|
| switch (target) { |
| case ATTACHMENT_SOURCE.GLOBAL: |
| extension_settings.attachments.push(attachment); |
| saveSettingsDebounced(); |
| break; |
| case ATTACHMENT_SOURCE.CHAT: |
| chat_metadata.attachments.push(attachment); |
| saveMetadataDebounced(); |
| break; |
| case ATTACHMENT_SOURCE.CHARACTER: |
| extension_settings.character_attachments[characters[this_chid]?.avatar].push(attachment); |
| saveSettingsDebounced(); |
| break; |
| } |
|
|
| return fileUrl; |
| } |
|
|
| function ensureAttachmentsExist() { |
| if (!Array.isArray(extension_settings.disabled_attachments)) { |
| extension_settings.disabled_attachments = []; |
| } |
|
|
| if (!Array.isArray(extension_settings.attachments)) { |
| extension_settings.attachments = []; |
| } |
|
|
| if (!Array.isArray(chat_metadata.attachments)) { |
| chat_metadata.attachments = []; |
| } |
|
|
| if (this_chid !== undefined && characters[this_chid]) { |
| if (!extension_settings.character_attachments) { |
| extension_settings.character_attachments = {}; |
| } |
|
|
| if (!Array.isArray(extension_settings.character_attachments[characters[this_chid].avatar])) { |
| extension_settings.character_attachments[characters[this_chid].avatar] = []; |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function getDataBankAttachments(includeDisabled = false) { |
| ensureAttachmentsExist(); |
| const globalAttachments = extension_settings.attachments ?? []; |
| const chatAttachments = chat_metadata.attachments ?? []; |
| const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? []; |
|
|
| return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => includeDisabled || !isAttachmentDisabled(x)); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function getDataBankAttachmentsForSource(source, includeDisabled = true) { |
| ensureAttachmentsExist(); |
|
|
| function getBySource() { |
| switch (source) { |
| case ATTACHMENT_SOURCE.GLOBAL: |
| return extension_settings.attachments ?? []; |
| case ATTACHMENT_SOURCE.CHAT: |
| return chat_metadata.attachments ?? []; |
| case ATTACHMENT_SOURCE.CHARACTER: |
| return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? []; |
| } |
|
|
| return []; |
| } |
|
|
| return getBySource().filter(x => includeDisabled || !isAttachmentDisabled(x)); |
| } |
|
|
| |
| |
| |
| |
| async function verifyAttachments() { |
| for (const source of Object.values(ATTACHMENT_SOURCE)) { |
| await verifyAttachmentsForSource(source); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async function verifyAttachmentsForSource(source) { |
| try { |
| const attachments = getDataBankAttachmentsForSource(source); |
| const urls = attachments.map(a => a.url); |
| const response = await fetch('/api/files/verify', { |
| method: 'POST', |
| headers: getRequestHeaders(), |
| body: JSON.stringify({ urls }), |
| }); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(error); |
| } |
|
|
| const verifiedUrls = await response.json(); |
| for (const attachment of attachments) { |
| if (verifiedUrls[attachment.url] === false) { |
| console.log('Deleting orphaned attachment', attachment); |
| await deleteAttachment(attachment, source, () => { }, false); |
| } |
| } |
| } catch (error) { |
| console.error('Attachment verification failed', error); |
| } |
| } |
|
|
| const NEUTRAL_CHAT_KEY = 'neutralChat'; |
|
|
| export function preserveNeutralChat() { |
| if (this_chid !== undefined || selected_group || name2 !== neutralCharacterName) { |
| return; |
| } |
|
|
| sessionStorage.setItem(NEUTRAL_CHAT_KEY, JSON.stringify({ chat, chat_metadata })); |
| } |
|
|
| export function restoreNeutralChat() { |
| if (this_chid !== undefined || selected_group || name2 !== neutralCharacterName) { |
| return; |
| } |
|
|
| const neutralChat = sessionStorage.getItem(NEUTRAL_CHAT_KEY); |
| if (!neutralChat) { |
| return; |
| } |
|
|
| const { chat: neutralChatData, chat_metadata: neutralChatMetadata } = JSON.parse(neutralChat); |
| chat.splice(0, chat.length, ...neutralChatData); |
| updateChatMetadata(neutralChatMetadata, true); |
| sessionStorage.removeItem(NEUTRAL_CHAT_KEY); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function registerFileConverter(mimeType, converter) { |
| if (typeof mimeType !== 'string' || typeof converter !== 'function') { |
| console.error('Invalid converter registration'); |
| return; |
| } |
|
|
| if (Object.keys(converters).includes(mimeType)) { |
| console.error('Converter already registered'); |
| return; |
| } |
|
|
| converters[mimeType] = converter; |
| } |
|
|
| export function addDOMPurifyHooks() { |
| |
| DOMPurify.addHook('afterSanitizeAttributes', function (node) { |
| if ('target' in node) { |
| node.setAttribute('target', '_blank'); |
| node.setAttribute('rel', 'noopener'); |
| } |
| }); |
|
|
| DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => { |
| if (!config['MESSAGE_SANITIZE']) { |
| return; |
| } |
|
|
| |
| const permittedNodeTypes = ['BUTTON', 'DIV']; |
| if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) { |
| return; |
| } |
|
|
| switch (data.attrName) { |
| case 'class': { |
| if (data.attrValue) { |
| data.attrValue = data.attrValue.split(' ').map((v) => { |
| if (v.startsWith('fa-') || v.startsWith('note-') || v === 'monospace') { |
| return v; |
| } |
|
|
| return 'custom-' + v; |
| }).join(' '); |
| } |
| break; |
| } |
| } |
| }); |
|
|
| DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { |
| if (!config['MESSAGE_SANITIZE']) { |
| return; |
| } |
|
|
| |
| if (node instanceof HTMLUnknownElement) { |
| node.innerHTML = node.innerHTML.trim(); |
|
|
| |
| const candidates = []; |
| const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT); |
| while (walker.nextNode()) { |
| const textNode = (walker.currentNode); |
| if (!textNode.data.includes('\n')) continue; |
|
|
| |
| if (textNode.parentElement && textNode.parentElement.closest('pre')) continue; |
|
|
| candidates.push(textNode); |
| } |
|
|
| for (const textNode of candidates) { |
| const parts = textNode.data.split('\n'); |
| const frag = document.createDocumentFragment(); |
| parts.forEach((part, idx) => { |
| if (part.length) { |
| frag.appendChild(document.createTextNode(part)); |
| } |
| if (idx < parts.length - 1) { |
| frag.appendChild(document.createElement('br')); |
| } |
| }); |
| textNode.replaceWith(frag); |
| } |
| } |
|
|
| const isMediaAllowed = isExternalMediaAllowed(); |
| if (isMediaAllowed) { |
| return; |
| } |
|
|
| if (!(node instanceof Element)) { |
| return; |
| } |
|
|
| let mediaBlocked = false; |
|
|
| switch (node.tagName) { |
| case 'AUDIO': |
| case 'VIDEO': |
| case 'SOURCE': |
| case 'TRACK': |
| case 'EMBED': |
| case 'OBJECT': |
| case 'IMG': { |
| const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin); |
| const src = node.getAttribute('src'); |
| const data = node.getAttribute('data'); |
| const srcset = node.getAttribute('srcset'); |
|
|
| if (srcset) { |
| const srcsetUrls = srcset.split(','); |
|
|
| for (const srcsetUrl of srcsetUrls) { |
| const [url] = srcsetUrl.trim().split(' '); |
|
|
| if (isExternalUrl(url)) { |
| console.warn('External media blocked', url); |
| node.remove(); |
| mediaBlocked = true; |
| break; |
| } |
| } |
| } |
|
|
| if (src && isExternalUrl(src)) { |
| console.warn('External media blocked', src); |
| mediaBlocked = true; |
| node.remove(); |
| } |
|
|
| if (data && isExternalUrl(data)) { |
| console.warn('External media blocked', data); |
| mediaBlocked = true; |
| node.remove(); |
| } |
|
|
| if (mediaBlocked && (node instanceof HTMLMediaElement)) { |
| node.autoplay = false; |
| node.pause(); |
| } |
| } |
| break; |
| } |
|
|
| if (mediaBlocked) { |
| const entityId = getCurrentEntityId(); |
| const warningShownKey = `mediaWarningShown:${entityId}`; |
|
|
| if (accountStorage.getItem(warningShownKey) === null) { |
| const warningToast = toastr.warning( |
| t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`, |
| t`External media has been blocked`, |
| { |
| timeOut: 0, |
| preventDuplicates: true, |
| onclick: () => toastr.clear(warningToast), |
| }, |
| ); |
|
|
| accountStorage.setItem(warningShownKey, 'true'); |
| } |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function onImageSwiped(messageId, element, direction) { |
| const animationClass = 'fa-fade'; |
| const messageMedia = element.find('.mes_img, .mes_video'); |
|
|
| |
| if (messageMedia.hasClass(animationClass)) { |
| return; |
| } |
|
|
| const message = chat[messageId]; |
| const media = message?.extra?.media; |
|
|
| if (!message || !Array.isArray(media) || media.length === 0) { |
| console.warn('No media found in the message'); |
| return; |
| } |
|
|
| const currentIndex = getMediaIndex(message); |
| const mediaDisplay = getMediaDisplay(message); |
|
|
| if (mediaDisplay !== MEDIA_DISPLAY.GALLERY) { |
| console.warn('Image swiping is only supported for gallery media display'); |
| return; |
| } |
|
|
| await eventSource.emit(event_types.IMAGE_SWIPED, { message, element, direction }); |
|
|
| if (media.length === 1) { |
| console.warn('Only one media item in the message, swiping is not applicable'); |
| return; |
| } |
|
|
| |
| if (direction === SWIPE_DIRECTION.LEFT) { |
| const newIndex = currentIndex === 0 ? media.length - 1 : currentIndex - 1; |
| message.extra.media_index = newIndex; |
| } |
|
|
| |
| if (direction === SWIPE_DIRECTION.RIGHT) { |
| const newIndex = currentIndex === media.length - 1 ? 0 : currentIndex + 1; |
| message.extra.media_index = newIndex >= media.length ? 0 : newIndex; |
| } |
|
|
| await saveChatConditional(); |
| appendMediaToMessage(message, element); |
| } |
|
|
| export function initChatUtilities() { |
| $(document).on('click', '.mes_hide', async function () { |
| const messageBlock = $(this).closest('.mes'); |
| const messageId = Number(messageBlock.attr('mesid')); |
| await hideChatMessageRange(messageId, messageId, false); |
| }); |
|
|
| $(document).on('click', '.mes_unhide', async function () { |
| const messageBlock = $(this).closest('.mes'); |
| const messageId = Number(messageBlock.attr('mesid')); |
| await hideChatMessageRange(messageId, messageId, true); |
| }); |
|
|
| $(document).on('click', '.mes_file_delete', async function () { |
| const messageBlock = $(this).closest('.mes'); |
| const messageId = Number(messageBlock.attr('mesid')); |
| const fileBlock = $(this).closest('.mes_file_container'); |
| const fileIndex = Number(fileBlock.attr('data-index')); |
| await deleteMessageFile(messageBlock, messageId, fileIndex); |
| }); |
|
|
| $(document).on('click', '.mes_file_open', async function () { |
| const messageBlock = $(this).closest('.mes'); |
| const messageId = Number(messageBlock.attr('mesid')); |
| const fileBlock = $(this).closest('.mes_file_container'); |
| const fileIndex = Number(fileBlock.attr('data-index')); |
| await viewMessageFile(messageId, fileIndex); |
| }); |
|
|
| $(document).on('click', '.assistant_note_export', async function (_e) { |
| |
| const chatHeader = { |
| chat_metadata: chat_metadata, |
| user_name: 'unused', |
| character_name: 'unused', |
| }; |
| const chatToSave = [ |
| chatHeader, |
| ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE), |
| ]; |
|
|
| download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json'); |
| }); |
|
|
| $(document).on('click', '.assistant_note_import', async function () { |
| const importFile = async () => { |
| const file = fileInput.files[0]; |
| if (!file) { |
| return; |
| } |
|
|
| try { |
| const text = await getFileText(file); |
| const lines = text.split('\n').filter(line => line.trim() !== ''); |
| const messages = lines.map(line => JSON.parse(line)); |
| const metadata = messages.shift()?.chat_metadata || {}; |
| messages.unshift(getSystemMessageByType(system_message_types.ASSISTANT_NOTE)); |
| await clearChat(); |
| chat.splice(0, chat.length, ...messages); |
| updateChatMetadata(metadata, true); |
| await printMessages(); |
| } catch (error) { |
| console.error('Error importing assistant chat:', error); |
| toastr.error(t`It's either corrupted or not a valid JSONL file.`, t`Failed to import chat`); |
| } |
| }; |
| const fileInput = document.createElement('input'); |
| fileInput.type = 'file'; |
| fileInput.accept = '.jsonl'; |
| fileInput.addEventListener('change', importFile); |
| fileInput.click(); |
| }); |
|
|
| const fileInput = document.getElementById('file_form_input'); |
|
|
| |
| $(document).on('click', '#attachFile', function () { |
| if (!(fileInput instanceof HTMLInputElement)) return; |
| const $fileInput = $(fileInput); |
|
|
| |
| const dataTransfer = new DataTransfer(); |
| for (const file of fileInput.files) { |
| dataTransfer.items.add(file); |
| } |
|
|
| $fileInput.off('change').on('change', async () => { |
| for (const file of fileInput.files) { |
| if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) { |
| dataTransfer.items.add(file); |
| } |
| } |
|
|
| fileInput.files = dataTransfer.files; |
| await onFileAttach(fileInput.files); |
| }); |
|
|
| $fileInput.trigger('click'); |
| }); |
|
|
| |
| $(document).on('click', '#manageAttachments', function () { |
| openAttachmentManager(); |
| }); |
|
|
| $(document).on('click', '.mes_embed', function () { |
| const messageBlock = $(this).closest('.mes'); |
| const messageId = Number(messageBlock.attr('mesid')); |
| embedMessageFile(messageId, messageBlock); |
| }); |
|
|
| $(document).on('click', '.editor_maximize', async function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
|
|
| const broId = $(this).attr('data-for'); |
| const bro = $(`#${broId}`); |
| const contentEditable = bro.is('[contenteditable]'); |
| const withTab = $(this).attr('data-tab'); |
|
|
| if (!bro.length) { |
| console.error('Could not find editor with id', broId); |
| return; |
| } |
|
|
| const wrapper = document.createElement('div'); |
| wrapper.classList.add('height100p', 'wide100p', 'flex-container'); |
| wrapper.classList.add('flexFlowColumn', 'justifyCenter', 'alignitemscenter'); |
| const textarea = document.createElement('textarea'); |
| textarea.dataset.for = broId; |
| textarea.value = String(contentEditable ? bro[0].innerText : bro.val()); |
| textarea.classList.add('height100p', 'wide100p', 'maximized_textarea'); |
| bro.hasClass('monospace') && textarea.classList.add('monospace'); |
| bro.hasClass('mdHotkeys') && textarea.classList.add('mdHotkeys'); |
| textarea.addEventListener('input', function () { |
| if (contentEditable) { |
| bro[0].innerText = textarea.value; |
| bro.trigger('input'); |
| } else { |
| bro.val(textarea.value).trigger('input'); |
| } |
| }); |
| wrapper.appendChild(textarea); |
|
|
| if (withTab) { |
| textarea.addEventListener('keydown', (evt) => { |
| if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) { |
| evt.preventDefault(); |
| const start = textarea.selectionStart; |
| const end = textarea.selectionEnd; |
| if (end - start > 0 && textarea.value.substring(start, end).includes('\n')) { |
| const lineStart = textarea.value.lastIndexOf('\n', start); |
| const count = textarea.value.substring(lineStart, end).split('\n').length - 1; |
| textarea.value = `${textarea.value.substring(0, lineStart)}${textarea.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${textarea.value.substring(end)}`; |
| textarea.selectionStart = start + 1; |
| textarea.selectionEnd = end + count; |
| } else { |
| textarea.value = `${textarea.value.substring(0, start)}\t${textarea.value.substring(end)}`; |
| textarea.selectionStart = start + 1; |
| textarea.selectionEnd = end + 1; |
| } |
| } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) { |
| evt.preventDefault(); |
| const start = textarea.selectionStart; |
| const end = textarea.selectionEnd; |
| const lineStart = textarea.value.lastIndexOf('\n', start); |
| const count = textarea.value.substring(lineStart, end).split('\n\t').length - 1; |
| textarea.value = `${textarea.value.substring(0, lineStart)}${textarea.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${textarea.value.substring(end)}`; |
| textarea.selectionStart = start - 1; |
| textarea.selectionEnd = end - count; |
| } |
| }); |
| } |
|
|
| await callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true }); |
| }); |
|
|
| $(document).on('click', 'body .mes .mes_text', function () { |
| if (!power_user.click_to_edit) return; |
| if (window.getSelection().toString()) return; |
| if ($('.edit_textarea').length) return; |
| $(this).closest('.mes').find('.mes_edit').trigger('click'); |
| }); |
|
|
| $(document).on('click', '.open_media_overrides', openExternalMediaOverridesDialog); |
| $(document).on('input', '#forbid_media_override_allowed', function () { |
| const entityId = getCurrentEntityId(); |
| if (!entityId) return; |
| power_user.external_media_allowed_overrides.push(entityId); |
| power_user.external_media_forbidden_overrides = power_user.external_media_forbidden_overrides.filter((v) => v !== entityId); |
| saveSettingsDebounced(); |
| reloadCurrentChat(); |
| }); |
| $(document).on('input', '#forbid_media_override_forbidden', function () { |
| const entityId = getCurrentEntityId(); |
| if (!entityId) return; |
| power_user.external_media_forbidden_overrides.push(entityId); |
| power_user.external_media_allowed_overrides = power_user.external_media_allowed_overrides.filter((v) => v !== entityId); |
| saveSettingsDebounced(); |
| reloadCurrentChat(); |
| }); |
| $(document).on('input', '#forbid_media_override_global', function () { |
| const entityId = getCurrentEntityId(); |
| if (!entityId) return; |
| power_user.external_media_allowed_overrides = power_user.external_media_allowed_overrides.filter((v) => v !== entityId); |
| power_user.external_media_forbidden_overrides = power_user.external_media_forbidden_overrides.filter((v) => v !== entityId); |
| saveSettingsDebounced(); |
| reloadCurrentChat(); |
| }); |
|
|
| $('#creators_note_styles_button').on('click', function (e) { |
| e.stopPropagation(); |
| openGlobalStylesPreferenceDialog(); |
| }); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function getMediaContainerInfo(containerClass = '.mes_media_container') { |
| const messageBlock = $(this).closest('.mes'); |
| const messageId = Number(messageBlock.attr('mesid')); |
| const mediaBlock = $(this).closest(containerClass); |
| const mediaIndex = Number(mediaBlock.attr('data-index')); |
| return { messageBlock, messageId, mediaBlock, mediaIndex }; |
| } |
| chatElement.on('click', '.mes_img', async function () { |
| const { messageId, mediaIndex } = getMediaContainerInfo.call(this); |
| expandMessageMedia(messageId, mediaIndex); |
| }); |
| chatElement.on('click', '.mes_media_enlarge', async function () { |
| const { messageId, mediaIndex } = getMediaContainerInfo.call(this); |
| expandMessageMedia(messageId, mediaIndex).click(); |
| }); |
| chatElement.on('click', '.mes_media_delete', async function () { |
| const { messageId, mediaIndex, messageBlock } = getMediaContainerInfo.call(this); |
| await deleteMessageMedia(messageId, mediaIndex, messageBlock); |
| }); |
| chatElement.on('click', '.mes_media_list', async function () { |
| const { messageId, messageBlock } = getMediaContainerInfo.call(this); |
| await switchMessageMediaDisplay(messageId, messageBlock, MEDIA_DISPLAY.GALLERY); |
| }); |
| chatElement.on('click', '.mes_media_gallery', async function () { |
| const { messageId, messageBlock } = getMediaContainerInfo.call(this); |
| await switchMessageMediaDisplay(messageId, messageBlock, MEDIA_DISPLAY.LIST); |
| }); |
| chatElement.on('click', '.mes_img_swipe_left', async function () { |
| const { messageId, messageBlock } = getMediaContainerInfo.call(this); |
| await onImageSwiped(messageId, messageBlock, SWIPE_DIRECTION.LEFT); |
| }); |
| chatElement.on('click', '.mes_img_swipe_right', async function () { |
| const { messageId, messageBlock } = getMediaContainerInfo.call(this); |
| await onImageSwiped(messageId, messageBlock, SWIPE_DIRECTION.RIGHT); |
| }); |
|
|
| $('#file_form').on('reset', function () { |
| $('#file_form').addClass('displayNone'); |
| }); |
|
|
| document.getElementById('send_textarea').addEventListener('paste', async function (event) { |
| if (event.clipboardData.files.length === 0) { |
| return; |
| } |
|
|
| event.preventDefault(); |
| event.stopPropagation(); |
|
|
| await handleFileAttach(Array.from(event.clipboardData.files)); |
| }); |
|
|
| new DragAndDropHandler('#form_sheld', async (files) => { |
| await handleFileAttach(files); |
| }); |
|
|
| |
| |
| |
| |
| |
| async function handleFileAttach(files) { |
| if (!(fileInput instanceof HTMLInputElement)) return; |
|
|
| |
| const dataTransfer = new DataTransfer(); |
| for (const file of fileInput.files) { |
| dataTransfer.items.add(file); |
| } |
|
|
| |
| for (const file of files) { |
| if (!Array.from(dataTransfer.files).some(f => isSameFile(f, file))) { |
| dataTransfer.items.add(file); |
| } |
| } |
|
|
| fileInput.files = dataTransfer.files; |
| await onFileAttach(fileInput.files); |
| } |
|
|
| eventSource.on(event_types.CHAT_CHANGED, checkForCreatorNotesStyles); |
| } |
|
|