vietnamese
This commit is contained in:
@@ -1,11 +1,198 @@
|
||||
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 = '<i class="material-symbols"></i>';
|
||||
primary.firstChild.textContent = icon;
|
||||
|
||||
let control = document.createElement('span');
|
||||
control.className = 'control icon active gray';
|
||||
control.innerHTML = '<i class="material-symbols">chat</i>';
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user