var ChatActions = { message: null, // OMEMO messages of the open conversation: {jid, entries: [[mid, id, messageid, published]], bodies} index: null, keywords: '', setMessage: function (message) { ChatActions.message = message; }, focusSearch: function () { document.querySelector('form[name=search] input').focus(); }, // Same as utils::search_normalize on the server normalize: function (text) { return text.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replaceAll('đ', 'd'); }, setIndex: function (jid, entries) { ChatActions.index = { jid: jid, entries: entries, bodies: null }; }, // Decrypted bodies of the indexed messages, from this browser: mid → body loadBodies: async function () { let index = ChatActions.index; if (!index) return new Map(); if (index.bodies) return index.bodies; let bodies = new Map(); if (typeof ChatOmemoDB != 'undefined' && ChatOmemoDB.db) { for (const [mid, id, messageid] of index.entries) { let body = await ChatOmemoDB.getMessage(id); if (typeof body != 'string' && messageid) body = await ChatOmemoDB.getMessage(messageid); if (typeof body == 'string' && body) bodies.set(mid, body); } } index.bodies = bodies; return bodies; }, published: function (mid) { let entry = ChatActions.index?.entries.find(e => e[0] == mid); return entry ? entry[3] : ''; }, // Search: the encrypted messages here, then the server merges them with // the clear text ones search: async function (jid, keywords, muc) { ChatActions.keywords = keywords; let words = ChatActions.normalize(keywords).split(/\s+/).filter(w => w); let mids = []; if (words.length && ChatActions.index && ChatActions.index.jid == jid) { for (const [mid, body] of await ChatActions.loadBodies()) { if (typeof AESGCM_REGEX != 'undefined' && AESGCM_REGEX.test(body.trim())) continue; let normalized = ChatActions.normalize(body); if (words.every(w => normalized.includes(w))) mids.push(mid); if (mids.length >= 20) break; } } // A newer search started meanwhile if (keywords != ChatActions.keywords) return; ChatActions_ajaxSearchMessages(jid, keywords, muc, mids); }, fillEncrypted: async function () { let bodies = await ChatActions.loadBodies(); let words = ChatActions.keywords.split(/\s+/).filter(w => w); document.querySelectorAll('#chat_search p.encrypted[data-mid]').forEach(p => { let body = bodies.get(parseInt(p.dataset.mid)); if (!body) return; p.classList.remove('encrypted'); ChatActions.highlight(p, body, words); }); }, // Text with the words marked, built with the DOM (the body is not trusted) highlight: function (element, text, words) { element.textContent = ''; let escaped = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); let parts = escaped.length ? text.split(new RegExp('(' + escaped.join('|') + ')', 'gi')) : [text]; parts.forEach((part, i) => { if (i % 2 == 1) { let mark = document.createElement('mark'); mark.textContent = part; element.appendChild(mark); } else if (part) { element.appendChild(document.createTextNode(part)); } }); }, // Shared files drawer filesTab: function (tab) { document.querySelectorAll('#chat_files .tabs li').forEach(li => li.classList.toggle('active', li.dataset.tab == tab)); document.querySelectorAll('#chat_files .files_tab').forEach(div => div.classList.toggle('hide', div.dataset.tab != tab)); ChatActions.filesEmpty(); }, filesEmpty: function () { let current = document.querySelector('#chat_files .files_tab:not(.hide) [data-list]'); let empty = document.querySelector('#chat_files .files_empty'); if (current && empty) empty.classList.toggle('hide', current.children.length > 0); }, addEncryptedFiles: async function (jid) { if (typeof AESGCM_REGEX == 'undefined') return ChatActions.filesEmpty(); for (const [mid, body] of await ChatActions.loadBodies()) { let url = body.trim(); let matches = url.match(AESGCM_REGEX); if (!matches) continue; let filename = matches[2]; let type = AESGCM_TYPES[matches[3].toLowerCase()] ?? ''; let tab = type.startsWith('image/') ? 'images' : type.startsWith('video/') ? 'videos' : type.startsWith('audio/') ? 'recordings' : 'files'; let list = document.querySelector('#chat_files [data-list=' + tab + ']'); if (!list) return; let li = tab == 'images' ? ChatActions.encryptedPicture(url, filename) : ChatActions.encryptedFileItem(jid, mid, url, filename, tab, list.dataset.icon); li.dataset.published = ChatActions.published(mid); // Keep the newest first, mixed with the clear text files let older = Array.from(list.children).find(item => item.dataset.published < li.dataset.published); list.insertBefore(li, older ?? null); } ChatActions.filesEmpty(); }, encryptedPicture: function (url, filename) { let li = document.createElement('li'); let img = document.createElement('img'); img.alt = img.title = filename; li.appendChild(img); // Downloaded and decrypted when shown new IntersectionObserver((entries, observer) => { if (!entries.some(e => e.isIntersecting)) return; observer.disconnect(); ChatOmemo.decryptFile(url).then(blobUrl => { img.src = blobUrl; img.onclick = () => window.open(blobUrl, '_blank'); }).catch(e => li.classList.add('error')); }).observe(li); return li; }, encryptedFileItem: function (jid, mid, url, filename, tab, icon) { let li = document.createElement('li'); let primary = document.createElement('span'); primary.className = 'primary icon gray'; primary.innerHTML = ''; primary.firstChild.textContent = icon; let control = document.createElement('span'); control.className = 'control icon active gray'; control.innerHTML = 'chat'; control.onclick = e => { e.stopPropagation(); Chat_ajaxGetMessageContext(jid, mid); Drawer.clear(); }; let div = document.createElement('div'); let name = document.createElement('p'); name.className = 'line'; name.textContent = filename; let info = document.createElement('p'); info.className = 'line'; let published = ChatActions.published(mid); info.textContent = published ? new Date(published.replace(' ', 'T') + 'Z').toLocaleString() : ''; div.append(name, info); div.onclick = async () => { if (tab == 'recordings') { // Play it in place if (div.querySelector('audio')) return; let audio = document.createElement('audio'); audio.controls = true; audio.src = await ChatOmemo.decryptFile(url); name.after(audio); audio.play(); } else if (tab == 'videos') { window.open(await ChatOmemo.decryptFile(url), '_blank'); } else { ChatOmemo.getEncryptedFile(url); } }; li.append(primary, control, div); return li; } } MovimEvents.registerWindow('keydown', 'search_message', (e) => { if (e.key == 'f' && e.ctrlKey && MovimUtils.urlParts().page == 'chat' && Chat.getTextarea()) { e.preventDefault(); ChatActions_ajaxShowSearchDialog(Chat.getTextarea().dataset.jid, Boolean(Chat.getTextarea().dataset.muc)); } });