var Chat = { left: null, right: null, date: null, separator: null, edit: false, // Scroll lastHeight: null, lastScroll: null, // Intersection discussion observer discussionObserver: null, // Chat state composing: false, since: null, sent: false, // Timer typingTimer: null, // Autocomplete vars. autocompleteList: null, lastAutocomplete: null, searchAutocomplete: null, // Touch delay: 20, clientWidth: null, startX: 0, startY: 0, translateX: 0, translateY: 0, slideAuthorized: false, messageStartX: 0, messageStartY: 0, messageTranslateX: 0, messageTranslateY: 0, messageSlideAuthorized: false, // Temporary messages, for OMEMO local messages tempMessages: {}, // Last sent id, to clear the chatbox if the message is coming back by the websocket lastSentUUID: null, // Local list of the room members, used to encrypt the messages groupChatMembers: [], // Jingle types jingleTypes: ['jingle_incoming', 'jingle_outgoing', 'jingle_end'], // Keep track of replaced messages hash when loading history or refreshing replacedHash: [], init: function (date, separator, config) { var div = document.createElement('div'); Chat.clientWidth = Math.abs(document.body.clientWidth); div.innerHTML = date; Chat.date = div.firstChild.cloneNode(true); div.innerHTML = separator; Chat.separator = div.firstChild.cloneNode(true); Chat.pagination = config['pagination']; Chat.delivery_error = config['delivery_error']; Chat.action_impossible_encrypted_error = config['action_impossible_encrypted_error']; }, autocomplete: function (event, jid) { RoomsUtils_ajaxMucUsersAutocomplete(jid); }, onAutocomplete: function (usersList) { Chat.autocompleteList = usersList; usersList = Object.values(usersList); var textarea = Chat.getTextarea(); var words = textarea.value.toLowerCase().trim().split(' '); var last = words[words.length - 1].trim(); if (last == '') { // Space or nothing, so we put the first one in the list textarea.value += usersList[0] + ' '; Chat.lastAutocomplete = usersList[0]; Chat.searchAutocomplete = null; } else if (typeof Chat.lastAutocomplete === 'string' && Chat.lastAutocomplete.toLowerCase() == last && Chat.searchAutocomplete == null) { var index = (usersList.indexOf(Chat.lastAutocomplete) == usersList.length - 1) ? -1 : usersList.indexOf(Chat.lastAutocomplete); if (textarea.value.slice(-1) == ' ') textarea.value = textarea.value.trim() + ' '; // Full complete, so we iterate Chat.lastAutocomplete = usersList[index + 1]; textarea.value = textarea.value.slice(0, -last.length - 1) + Chat.lastAutocomplete + ' '; Chat.searchAutocomplete = null; } else { // Searching for nicknames starting with if (Chat.lastAutocomplete == null || last != Chat.lastAutocomplete.toLowerCase()) { Chat.searchAutocomplete = last; Chat.lastAutocomplete = null; } var start = (typeof Chat.lastAutocomplete === 'string') ? usersList.indexOf(Chat.lastAutocomplete) + 1 : start = 0; var end = false; for (var i = start; i < usersList.length; i++) { if (Chat.searchAutocomplete == usersList[i].substring(0, Chat.searchAutocomplete.length).toLowerCase()) { textarea.value = textarea.value.trim().slice(0, -last.length) + usersList[i] + ' '; Chat.lastAutocomplete = usersList[i]; end = true; break; } } if (end == false) { var found = usersList.find((user) => Chat.searchAutocomplete == user.substring(0, Chat.searchAutocomplete.length).toLowerCase()); if (found) { textarea.value = textarea.value.trim().slice(0, -last.length) + found + ' '; Chat.lastAutocomplete = found; } } } }, quoteMUC: function (nickname, add) { var textarea = Chat.getTextarea(); if (add) { if (textarea.value.search(nickname) === -1) { textarea.value = nickname + ' ' + textarea.value; } } else { textarea.value = nickname + ' '; } textarea.focus(); }, insertAtCursor: function (textToInsert) { textarea = Chat.getTextarea(); const value = textarea.value; const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = value.slice(0, start) + textToInsert + value.slice(end); textarea.selectionStart = textarea.selectionEnd = start + textToInsert.length; textarea.focus(); Chat.toggleAction(); }, get: function (jid, light) { if (MovimUtils.urlParts().page == 'chat') { if (jid != undefined) { MovimTpl.showPanel(); document.querySelector('#chat_widget').innerHTML = ''; Chats.setActive(jid); } else { Chats.clearAllActives(); MovimUtils.clearAllActivesRooms(); } Chat_ajaxGet(jid, light); } else if (MovimUtils.urlParts().page == 'space') { if (MovimUtils.isMobile()) { Chat_ajaxGet(); } else { MovimTpl.hidePanel(); } } }, getPresences: function (room) { if (!MovimUtils.isMobile()) { Chat_ajaxHttpGetPresences(room); } }, getRoom: function (jid) { MovimTpl.showPanel(); document.querySelector('#chat_widget').innerHTML = ''; MovimUtils.setActiveRoom(jid); Chat_ajaxGetRoom(jid); }, getHistory: function (tryMam) { var textarea = Chat.getTextarea(); let firstMessage = Chat.getDiscussion()?.querySelector('.message'); if (textarea) { Chat_ajaxGetHistory( textarea.dataset.jid, firstMessage ? firstMessage.dataset.published : null, Boolean(textarea.dataset.muc), true, tryMam); } }, sendMessage: function () { var textarea = Chat.getTextarea(); var text = textarea.value; var isMuc = Boolean(textarea.dataset.muc); var mucReceipts = false; var jid = textarea.dataset.jid; if (isMuc) { var counter = document.querySelector('#chat_widget header span.counter'); mucReceipts = (counter && counter.dataset.mucreceipts == 'true'); } Chat.removeSeparator(); Chat.scrollTotally(); document.querySelector('#embed').innerHTML = ''; // In case it was in edit mode textarea.parentNode.parentNode.parentNode.classList.remove('edit'); textarea.focus(); if (!Chat.sent) { Chat.enableSending(); if (Chat.edit) { Chat.edit = false; if (text == '') { Chat.disableSending(); } else { let correct = Chat_ajaxHttpDaemonCorrect(jid, textarea.dataset.mid, text); correct.then(e => { Chat.sentMessage(); }).catch(e => { Chat.failedMessage(text); }); } delete textarea.dataset.mid; } else { var reply = document.querySelector('#reply > div'); replyMid = false; if (reply) { replyMid = reply.dataset.mid; Chat_ajaxClearReply(); }; if (Chat.getOmemoState() == 'build') { var store = new ChatOmemoStorage(); store.getLocalRegistrationId().then(deviceId => { if (deviceId) { if (isMuc) { Chat.checkOMEMOState(jid, isMuc); } else { ChatOmemo_ajaxGetDevicesList(jid); } } else { Chat.disableSending(); ChatOmemo.refreshBundle(store.getOwnSessionsIds()); } }); } else if (Chat.getOmemoState() == 'yes') { if (!text) { Chat.disableSending(); return; } // Try to encrypt the message let omemo = ChatOmemo.encrypt(jid, text, isMuc); if (omemo) { // TODO, disable the other risky features omemo.then(omemoheader => { Chat.lastSentUUID = id = omemoheader.id = crypto.randomUUID(); Chat.tempMessages[id] = text; let request = Chat_ajaxHttpDaemonSendMessage(id, jid, null, isMuc, null, replyMid, mucReceipts, omemoheader); request.then(e => { Chat.sentMessage(); }).catch(e => { Chat.failedMessage(); }); }); } } else { Chat.lastSentUUID = id = crypto.randomUUID(); let request = Chat_ajaxHttpDaemonSendMessage(id, jid, text, isMuc, null, replyMid, mucReceipts); request.then(response => { response.ok ? Chat.sentMessage() : Chat.failedMessage(text); }).catch(e => { Chat.failedMessage(text); }); } } } }, sentId: function (id) { if (Chat.tempMessages[id]) { ChatOmemoDB.putMessage(id, Chat.tempMessages[id]); delete Chat.tempMessages[id]; } }, setGroupChatMembers: function (members) { Chat.groupChatMembers = members; }, checkOMEMOState: function (jid, muc) { var store = new ChatOmemoStorage(); let build = muc ? Chat.groupChatMembers.some( member => !store.isJidResolved(member) ) : !store.isJidResolved(jid); ChatOmemo.getContactState(jid).then(enabled => { let state = 'no'; if (enabled) { state = build ? 'build' : 'yes'; } else { state = 'disabled'; } Chat.setOmemoState(state); }); }, setOmemoState: function (state) { let textarea = Chat.getTextarea(); if (textarea) { textarea.dataset.encryptedstate = state; } }, getOmemoState: function () { let textarea = Chat.getTextarea(); if (textarea) { return textarea.dataset.encryptedstate; } }, enableSending: function () { Chat.sent = true; var send = document.querySelector('.chat_box'); if (send) { send.classList.add('sending'); send.classList.remove('finished'); } }, disableSending: function () { Chat.sent = false; var send = document.querySelector('.chat_box'); if (send) { send.classList.remove('sending'); } }, finishedSending: function () { Chat.sent = false; var send = document.querySelector('.chat_box'); if (send) send.classList.add('finished'); }, sentMessage: function () { Chat.disableSending(); var textarea = Chat.getTextarea(); localStorage.removeItem(textarea.dataset.jid + '_message'); Chat.clearReplace(); Chat.toggleAction(); Chat.lastSentUUID = null; }, failedMessage: function (text) { // Race condition if the Ajax request failed but the message was still handled if (Chat.lastSentUUID == null) return; Toast.send(Chat.delivery_error); Chat.disableSending(); Chat.lastSentUUID = null; // We try to put back the text in place var textarea = Chat.getTextarea(); if (textarea.value == '') { textarea.value = text; } }, clearReplace: function () { Chat.edit = false; var textarea = Chat.getTextarea(); textarea.value = localStorage.getItem(textarea.dataset.jid + '_message'); }, editPrevious: function () { var textarea = Chat.getTextarea(); if (textarea.value != '') { return; } if (!Chat.isEncrypted()) { Chat_ajaxLast(textarea.dataset.jid, Boolean(textarea.dataset.muc)); } else { Toast.send(Chat.action_impossible_encrypted_error); } }, resolveMessage: function (mid) { ChatActions_ajaxResolveMessage(mid); }, refreshMessage: function (mid) { Chat_ajaxRefreshMessage(mid); }, focus: function () { Chat.sent = false; Chat.composing = false; Chat.clearReplace(); Chat.toggleAction(); var textarea = Chat.getTextarea(); textarea.onkeydown = function (event) { if ((event.key == 'ArrowLeft' && Chat.tryPreviousEmoji()) || (event.key == 'ArrowRight' && Chat.tryNextEmoji())) { event.preventDefault(); return; } if (this.dataset.muc && textarea.value.length > 0 && event.key == 'Tab') { event.preventDefault(); if (Chat.autocompleteList == null) { Chat.autocomplete(event, this.dataset.jid); } else { Chat.onAutocomplete(Chat.autocompleteList); } return; } if (event.key == 'ArrowUp' && !Chat.isEncrypted()) { Chat.editPrevious(); } else if (event.key == 'Escape' && (this.value == '' || Chat.edit == true)) { localStorage.removeItem(textarea.dataset.jid + '_message'); textarea.parentNode.parentNode.parentNode.classList.remove('edit'); Chat.clearReplace(); } }; textarea.onkeypress = function (event) { if (event.key == 'Enter') { // An emoji was selected var emoji = document.querySelector('.chat_box .emojis img.selected'); if (emoji) { Chat.selectEmoji(emoji); event.preventDefault(); return; } if ((MovimEvents.isTouch && !event.shiftKey) || (!MovimEvents.isTouch && event.shiftKey)) { return; } Chat.composing = false; Chat.sendMessage(); return false; } else if (Chat.composing === false) { Chat.composing = true; Chat_ajaxSendComposing(this.dataset.jid, Boolean(this.dataset.muc)); Chat.since = new Date().getTime(); } }; textarea.oninput = function () { localStorage.setItem(this.dataset.jid + '_message', this.value); // A little timeout to not spam the server with composing states setTimeout(function () { if (Chat.since + 3000 < new Date().getTime()) { Chat.composing = false; } }, 3000); if (Chat.typingTimer != null) clearTimeout(Chat.typingTimer); if (this.value) { if (Chat.lastResolvedUrl != this.value) Chat.lastResolvedUrl = null; Chat.typingTimer = setTimeout(e => Chat.checkResolveUrl(this.value), 1000); } Chat.checkEmojis(this.value); Chat.scrollRestore(); Chat.toggleAction(); document.querySelector('#embed').innerHTML = ''; } textarea.addEventListener('paste', function (e) { clearTimeout(Chat.typingTimer); let clipboardData = e.clipboardData || window.clipboardData; let pastedData = clipboardData.getData('Text'); Chat.checkResolveUrl(pastedData); Chat.toggleAction(); }); if (document.documentElement.clientWidth > 1024) { textarea.focus(); } Chat.autocompleteList = null; }, checkResolveUrl(content) { let url; try { url = new URL(content); } catch (_) { return false; } if ((url.protocol === "http:" || url.protocol === "https:") && !Chat.isEncrypted() && !content.includes(' ') && Chat.lastResolvedUrl != content) { Chat.enableSending(); ChatActions_ajaxResolveUrl(content); Chat.lastResolvedUrl = content; } }, checkEmojis: function (value, reaction, noColon) { value = value.toLowerCase(); listSelector = reaction ? '.emojis_grid.results' : '.chat_box .emojis'; var emojisList = document.querySelector(listSelector); emojisList.innerHTML = ''; if (!value) return; if (noColon || value.lastIndexOf(':') > -1 && value.length > value.lastIndexOf(':') + 2) { var first = true; Object.keys(window.favoriteEmojis) .filter(key => key.includes(value.substring(value.lastIndexOf(':') + 1)) && !key.includes('type')) .slice(0, 20) .forEach(found => { var img = document.createElement('img'); img.setAttribute('src', favoriteEmojis[found]); img.classList.add('emoji'); if (reaction) img.classList.add('large'); if (first) { img.classList.add('selected'); first = false; } img.title = ':' + found + ':'; img.dataset.emoji = ':' + found + ':'; if (!reaction) { img.addEventListener('click', e => { Chat.selectEmoji(e.target); }); } emojisList.appendChild(img); }); Object.keys(window.emojis) .filter(key => key.includes(value.substring(value.lastIndexOf(':') + 1)) && !key.includes('type')) .slice(0, 30) .forEach(found => { var img = document.createElement('img'); img.setAttribute('src', BASE_URI + '/theme/img/emojis/svg/' + emojis[found].c + '.svg'); img.classList.add('emoji'); if (reaction) img.classList.add('large'); if (first) { img.classList.add('selected'); first = false; } img.title = ':' + found + ':'; img.dataset.emoji = emojis[found].e; if (!reaction) { img.addEventListener('click', e => { Chat.selectEmoji(e.target); }); } emojisList.appendChild(img); }); } }, selectEmoji: function (emoji) { var emojisList = document.querySelector('.chat_box .emojis'); var textarea = Chat.getTextarea(); textarea.value = textarea.value.substring(0, textarea.value.lastIndexOf(':')); emojisList.innerHTML = ''; Chat.insertAtCursor(emoji.dataset.emoji + ' '); }, tryNextEmoji: function () { var currentEmoji = document.querySelector('.chat_box .emojis img.selected'); if (currentEmoji) { currentEmoji.classList.remove('selected'); if (currentEmoji.nextSibling) { currentEmoji.nextSibling.classList.add('selected'); } else { document.querySelector('.chat_box .emojis img:first-child').classList.add('selected'); } return true; } return false; }, tryPreviousEmoji: function () { var currentEmoji = document.querySelector('.chat_box .emojis img.selected'); if (currentEmoji) { currentEmoji.classList.remove('selected'); if (currentEmoji.previousSibling) { currentEmoji.previousSibling.classList.add('selected'); } else { document.querySelector('.chat_box .emojis img:last-child').classList.add('selected'); } return true; } return false; }, setTextarea: function (value, mid) { Chat.edit = true; var textarea = Chat.getTextarea(); textarea.value = value; textarea.parentNode.parentNode.parentNode.classList.add('edit'); if (mid) { textarea.dataset.mid = mid; } textarea.focus(); }, setConfig(pagination, delivery_error, action_impossible_encrypted_error) { Chat.pagination = pagination; Chat.delivery_error = delivery_error; Chat.action_impossible_encrypted_error = action_impossible_encrypted_error; }, setSpecificElements: function (left, right) { var div = document.createElement('div'); div.innerHTML = left; Chat.left = div.firstChild.cloneNode(true); div.innerHTML = right; Chat.right = div.firstChild.cloneNode(true); }, setScrollBehaviour: function () { var discussion = Chat.getDiscussion(); if (!discussion) return; discussion.onscroll = function () { // Don't get more history the conversation is already empty if (discussion.querySelector('.conversation').innerHTML != '' && this.scrollTop < 10) { Chat.getHistory(true); } Chat.setScroll(); }; Chat.setScroll(); }, setScroll: function () { var discussion = Chat.getDiscussion(); if (!discussion) return; Chat.lastHeight = discussion.scrollHeight; Chat.lastScroll = discussion.scrollTop + discussion.clientHeight; var button = document.querySelector('#chat_widget #scroll_down.button.action'); if (Chat.isScrolled()) { button.classList.remove('show'); } else { button.classList.add('show'); } }, isScrolled: function () { return Chat.lastHeight - 5 <= Chat.lastScroll; }, scrollRestore: function () { var discussion = Chat.getDiscussion(); if (!discussion) return; if (Chat.isScrolled()) { Chat.scrollTotally(); } else { discussion.scrollTop = Chat.lastScroll - discussion.clientHeight; } }, scrollTotally: function () { var discussion = Chat.getDiscussion(); if (!discussion) return; discussion.scrollTop = discussion.scrollHeight; }, setObservers: function () { var options = { root: Chat.getDiscussion(), rootMargin: '0px', threshold: 1.0 }; Chat.discussionObserver = new IntersectionObserver((entries) => { entries.forEach((entrie) => { if (entrie.target.classList.contains('gif') && entrie.isIntersecting == true) { entrie.target.play(); } else { entrie.target.pause(); } }); }, options); }, scrollToSeparator: function () { var discussion = Chat.getDiscussion(); if (!discussion) return; var separator = discussion.querySelector('.separator'); if (separator) { discussion.scrollTop = separator.offsetTop - 65; Chat.setScroll(); } }, removeSeparator: function () { var separator = Chat.getDiscussion().querySelector('li.separator'); if (separator) separator.remove(); }, setReactionButtonBehaviour: function () { let reactions = document.querySelectorAll('#chat_widget span.reaction'); let i = 0; while (i < reactions.length) { reactions[i].onclick = function (e) { e.stopPropagation(); Stickers_ajaxReaction(this.dataset.mid); } i++; } }, setParentScrollBehaviour: function () { let toParents = document.querySelectorAll('#chat_widget div.parent'); let i = 0; while (i < toParents.length) { toParents[i].onclick = function (e) { var parentMsg = document.getElementById('id' + this.dataset.parentId); if (!parentMsg) { parentMsg = document.getElementById('id' + this.dataset.parentReplaceId) } if (parentMsg) { Chat.scrollAndBlinkMessage(parentMsg) } } i++; } }, scrollAndBlinkMessage: function (msg) { scrollToLi = msg.parentNode.parentNode; msg.parentNode.classList.add('scroll_blink'); setTimeout(() => { msg.parentNode.classList.remove('scroll_blink'); }, 1000); document.querySelector('#chat_widget .contained').scrollTo({ top: scrollToLi.offsetTop - 160, left: 0 }); }, scrollAndBlinkMessageMid: function (mid) { setTimeout(() => { Chat.scrollAndBlinkMessage(document.querySelector('div[data-mid="' + mid + '"')); }, 500) }, setVideoObserverBehaviour: function () { document.querySelectorAll('.file video').forEach(video => { Chat.discussionObserver.observe(video); }); }, setAudioPlayersBehaviour: function () { document.querySelectorAll('.audio_player').forEach((audioPlayer, index, audioPlayers) => { var audio = audioPlayer.querySelector('audio'); var buttonPlayPause = audioPlayer.querySelector('span.play_pause'); var progressBar = audioPlayer.querySelector('input[type=range]'); var timer = audioPlayer.querySelector('p.timer'); let mouseDownOnSlider = false; audio.onloadeddata = function () { progressBar.value = 0; timer.innerHTML = MovimUtils.cleanTime(audio.currentTime) + ' / ' + MovimUtils.cleanTime(0); } audio.ontimeupdate = function () { if (!mouseDownOnSlider) { progressBar.value = audio.currentTime / audio.duration * 100; timer.innerHTML = MovimUtils.cleanTime(audio.currentTime) + ' / ' + MovimUtils.cleanTime(Number.isFinite(audio.duration) ? audio.duration : 0); } } audio.onplay = function () { buttonPlayPause.querySelector('i').innerHTML = 'pause'; }; audio.onpause = function () { buttonPlayPause.querySelector('i').innerHTML = 'play_arrow'; }; audio.onended = function () { var maybeNext = audioPlayers[index + 1]; if (maybeNext) { maybeNext.querySelector('audio').play(); } } progressBar.onchange = function () { const pct = progressBar.value / 100; audio.currentTime = (audio.duration || 0) * pct; } progressBar.onmousedown = function () { mouseDownOnSlider = true; } progressBar.onmouseup = function () { mouseDownOnSlider = false; } buttonPlayPause.onclick = function () { if (audio.paused) { document.querySelectorAll('.audio_player:not(#' + audioPlayer.id + ') audio').forEach(otherAudio => otherAudio.pause()); audio.play(); } else { audio.pause(); } }; }); }, setActionsButtonBehaviour: function () { document.querySelectorAll('#chat_widget .contained span.reply').forEach(reply => reply.onclick = function (e) { e.stopPropagation(); Chat_ajaxHttpDaemonReply(this.dataset.mid); } ); }, setMessagePressBehaviour: function () { document.querySelectorAll('#chat_widget li div.bubble:not(.file) > div.message').forEach(message => { message.onmousedown = function (e) { setTimeout(() => { if (e.button == 0 && (e.target.classList.contains('message') || e.target.parentElement.classList.contains('message')) && !(window.getSelection().toString() != '' ) && this.dataset.mid && e.target.tagName.toLowerCase() != 'span') { ChatActions_ajaxShowMessageDialog(this.dataset.mid); } }, 200); } }); document.querySelectorAll('#chat_widget li div.bubble.file > div.message').forEach(message => { if (card = message.querySelector('ul.list.card > li:not(.ticket) > div')) { card.onclick = function (e) { ChatActions_ajaxShowMessageDialog(message.dataset.mid); }; } }); }, setMessageSwipeBehaviour: function () { if (MovimUtils.isMobile()) { document.querySelectorAll('#chat_widget li div.bubble:not(.file) > div.message').forEach(message => { message.ontouchstart = function (event) { Chat.messageStartX = event.targetTouches[0].pageX; Chat.messageStartY = event.targetTouches[0].pageY; }; message.ontouchmove = function (event) { Chat.messageTranslateX = parseInt(event.targetTouches[0].pageX - Chat.messageStartX); Chat.messageTranslateY = parseInt(event.targetTouches[0].pageY - Chat.messageStartY); if (Math.abs(Chat.messageTranslateX) > Chat.delay && Math.abs(Chat.messageTranslateX) <= Chat.clientWidth) { if (Math.abs(Chat.messageTranslateY) < Chat.delay) { Chat.messageSlideAuthorized = true; } if (Chat.messageTranslateX < -100) { message.classList.add('reply'); } else { message.classList.remove('reply'); } if (Chat.messageSlideAuthorized && Chat.messageTranslateX < 0 && Chat.messageTranslateX > -110) { message.parentNode.style.transform = 'translateX(' + (Chat.messageTranslateX + Chat.delay) + 'px)'; } } else { message.parentNode.style.transform = ''; message.classList.remove('reply'); } }; message.ontouchend = function (event) { if (Math.abs(Chat.messageTranslateX) > 100 && Chat.messageSlideAuthorized) { Chat_ajaxHttpDaemonReply(message.dataset.mid); } message.classList.remove('reply'); message.parentNode.style.transform = ''; Chat.messageSlideAuthorized = false; Chat.messageStartX = Chat.messageStartY = Chat.messageTranslateX = Chat.messageTranslateY = 0; }; }); } }, checkDiscussion: function (page) { for (var firstKey in page) break; if (page[firstKey] == null) return false; for (var firstMessageKey in page[firstKey]) break; var firstMessage = page[firstKey][firstMessageKey]; if (firstMessage == null) return false; var contactJid = firstMessage.user_id == firstMessage.jidfrom ? firstMessage.jidto : firstMessage.jidfrom; if (document.getElementById(MovimUtils.cleanupId(contactJid) + '-discussion') == null) return false; return true; }, appendMessagesWrapper: async function (page, prepend) { var discussion = Chat.getDiscussion(); if (page && Chat.checkDiscussion(page)) { if (discussion == null) return; discussion.querySelector('.placeholder.empty').classList.remove('show'); Chat.setScroll(); // Get all the messages keys var ids = []; Object.values(page).forEach(pageByDate => { Object.values(pageByDate).map(message => { if (message.omemoheader) ids.push(message.id) }); }); // Try to preload the OMEMO messages from the cache if (ids.length > 2 && OMEMO_ENABLED) { await ChatOmemoDB.loadMessagesByIds(ids); } for (date in page) { let messageDateTime = page[date][Object.keys(page[date])[0]].published; let firstMessage = Chat.getDiscussion()?.querySelector('.message') /** * We might have old messages reacted pushed by the server */ if (firstMessage && (new Date(messageDateTime) < (new Date(firstMessage.dataset.published))) && !prepend) { return; } if (prepend === undefined || prepend === false) { Chat.appendDate(date, prepend); } for (speakertime in page[date]) { Chat.appendMessage(speakertime, page[date][speakertime], prepend); } if (prepend && date) { Chat.appendDate(date, prepend); } } if (prepend) { // And we scroll where we were var scrollDiff = discussion.scrollHeight - Chat.lastHeight; discussion.scrollTop += scrollDiff; Chat.setScroll(); } else { Chat.scrollRestore(); } var chat = document.querySelector('#chat_widget'); var lastMessage = chat.querySelector('ul li:not(.oppose):last-child div.bubble > div:last-child'); var textarea = Chat.getTextarea(); if (textarea && lastMessage) { Chat_ajaxDisplayed( textarea.dataset.jid, lastMessage.id.substring(2) // 'id' is appended to the id ); } } else if (discussion !== null) { if (discussion.querySelector('ul').innerHTML === '') { discussion.querySelector('ul').classList.remove('spin'); discussion.querySelector('.placeholder.empty').classList.add('show'); } } if (discussion !== null) { if (discussion.querySelectorAll('ul li:not(.oppose)').length > 0 && discussion.querySelectorAll('ul li.oppose').length == 0 && discussion.querySelectorAll('ul li').length < 5) { discussion.querySelector('.placeholder.first_messages').classList.add('show'); } else { discussion.querySelector('.placeholder.first_messages').classList.remove('show'); } } Chat.setScrollBehaviour(); Chat.setReactionButtonBehaviour(); Chat.setActionsButtonBehaviour(); Chat.setParentScrollBehaviour(); Chat.setVideoObserverBehaviour(); Chat.setAudioPlayersBehaviour(); Chat.setMessagePressBehaviour(); Chat.setMessageSwipeBehaviour(); }, appendMessage: function (idjidtime, data, prepend) { if (data.body === null) return; // Sent message where the Ajax request somehow but the message was still handled by the server if (data.id == Chat.lastSentUUID) { Chat.sentMessage(); } var bubble = null, mergeMsg = false, msgStack, refBubble; var textarea = Chat.getTextarea(); var isMuc = Boolean(textarea.dataset.muc); var jidtime = idjidtime.substring(idjidtime.indexOf('<') + 1); if (prepend) { refBubble = document.querySelector('#chat_widget .contained section > ul > li:first-child'); msgStack = document.querySelector("[data-bubble='" + jidtime + "']"); } else { refBubble = document.querySelector("#chat_widget .contained section > ul > li:last-child"); var stack = document.querySelectorAll("[data-bubble='" + jidtime + "']"); msgStack = stack[stack.length - 1]; } if (msgStack != null && msgStack.parentNode == refBubble && data.url === false && (data.file === undefined || data.file === null) && (data.sticker === undefined || data.sticker === null) && !refBubble.querySelector('div.bubble').classList.contains('sticker') && !refBubble.querySelector('div.bubble').classList.contains('file') && Chat.jingleTypes.indexOf(data.type) < 0 ) { bubble = msgStack.parentNode; mergeMsg = true; } else { if (data.user_id == data.jidfrom || data.mine) { bubble = Chat.right.cloneNode(true); id = (data.mine) ? data.jidfrom : data.jidto; } else { bubble = Chat.left.cloneNode(true); id = data.jidfrom; } id = MovimUtils.cleanupId(id) + '-conversation'; bubble.querySelector('div.bubble').dataset.bubble = jidtime; bubble.querySelector('div.bubble').dataset.publishedprepared = data.publishedPrepared; } if (isMuc) { bubble.dataset.resource = data.resource; } if (refBubble && refBubble.dataset.resource == bubble.dataset.resource && mergeMsg == false && isMuc) { if (prepend) { refBubble.classList.add('sequel'); } else { bubble.classList.add('sequel'); } } // If there is already a msg in this bubble, create another div (next msg or replacement) var msg = (bubble.querySelector('div.bubble p') && bubble.querySelector('div.bubble p').innerHTML != '') ? Chat.right.querySelector('div.bubble > div.message').cloneNode(true) : bubble.querySelector('div.bubble > div.message'); var info = msg.querySelector('span.info'); var p = msg.getElementsByTagName('p')[0]; var reaction = msg.querySelector('span.reaction'); var reply = msg.querySelector('span.reply'); var reactions = msg.querySelector('ul.reactions'); // And we complete the message structure from the data we got... if (data.retracted) { p.classList.add('retracted'); } if (data.encrypted) { msg.classList.add('encrypted'); p.classList.add('encrypted'); } if (data.body.match(/^\/me\s/)) { p.classList.add('quote'); data.body = data.body.substring(4); } if (data.id != null) { msg.setAttribute('id', 'id' + data.id); } if (data.messageid != null) { msg.dataset.messageid = 'messageid-' + MovimUtils.hash(data.messageid + data.jidfrom); /** * If we append (during a reload) or prepend, we might have already a * replacement message displayed, in that case we just stop here */ if (this.replacedHash.includes(msg.dataset.messageid)) return; } if (data.rtl) { msg.setAttribute('dir', 'rtl'); } let omemo = false; // OMEMO handling if (data.omemoheader && data.encrypted && !data.retracted && OMEMO_ENABLED) { p.innerHTML = data.omemoheader.payload.substring(0, data.omemoheader.payload.length / 2); ChatOmemo.decrypt(data).then(plaintext => { let refreshP = document.querySelector('#id' + data.id + ' p.encrypted'); if (refreshP) { if (plaintext) { let escaped = MovimUtils.linkify(MovimUtils.htmlEscape(plaintext)); refreshP.innerHTML = ChatOmemo.searchEncryptedFile(escaped); refreshP.classList.remove('encrypted'); } else { refreshP.classList.add('error'); } } }); omemo = true; } if (data.replaceid) { msg.classList.add('edited'); } if (data.user_id == data.jidfrom || (isMuc && data.mine)) { if (data.displayed) { msg.classList.add('displayed'); } else if (data.delivered) { msg.classList.add('delivered'); } } if (data.reactionsHtml !== undefined && data.retracted == false) { reactions.innerHTML = data.reactionsHtml; } if (isMuc && data.resource) { var resourceSpan = document.createElement('span'); resourceSpan.classList.add('resource'); resourceSpan.classList.add(data.color); resourceSpan.innerText = data.resource; msg.appendChild(resourceSpan); } if (data.sticker != null && data.retracted == false) { bubble.querySelector('div.bubble').classList.add('file'); p.appendChild(Chat.getStickerHtml(data)); } else if (data.file != null && data.card === undefined && data.file.type !== 'xmpp' && data.retracted == false) { bubble.querySelector('div.bubble').classList.add('file'); msg.appendChild(Chat.getFileHtml(data.file, data)); } if (data.published) { info.title = data.published; } // Parent if (data.parent) { msg.appendChild(Chat.getParentHtml(data.parent)); } /*else if (data.parentQuote) { msg.appendChild(Chat.getSimpleParentHtml(data.parentQuote)); }*/ if (data.card) { bubble.querySelector('div.bubble').classList.add('file'); msg.appendChild(Chat.getCardHtml(data.card, data.story)); } msg.dataset.published = data.published; if (omemo == false && data.sticker === undefined) { Chat.addQuotesHtml(msg, data.body); } msg.appendChild(p); msg.appendChild(info); msg.appendChild(reactions); if (data.mid) { if ((isMuc && data.stanzaid) || (!isMuc && data.messageid)) { reaction.dataset.mid = data.mid; msg.appendChild(reaction); } if (reply) { reply.dataset.mid = data.mid; msg.appendChild(reply); } msg.dataset.mid = data.mid; } var elem; // The following commented part introduces a security issue, see https://xmpp.org/extensions/xep-0308.html#security if (data.replaceid /*&& (!isMuc || Boolean(textarea.dataset.mucGroup) == true)*/) { elem = document.querySelector("[data-messageid=messageid-" + MovimUtils.hash(data.replaceid + data.jidfrom) + "]"); msg.dataset.messageid = 'messageid-' + MovimUtils.hash(data.replaceid + data.jidfrom); this.replacedHash.push(msg.dataset.messageid); } if (!elem) { elem = document.getElementById('id' + data.id); } if (elem) { elem.parentElement.replaceChild(msg, elem); mergeMsg = true; // If the previous message was not a file or card and is replaced by it if ((data.file != null || data.card != null) && data.retracted == false) { msg.parentElement.classList.add('file'); } else { msg.parentElement.classList.remove('file'); } if (data.sticker != null && data.retracted == false) { msg.parentElement.classList.add('sticker', 'file'); } else { msg.parentElement.classList.remove('sticker'); } } else { if (prepend) { bubble.querySelector('div.bubble').insertBefore(msg, bubble.querySelector('div.bubble').firstChild); } else { bubble.querySelector('div.bubble').appendChild(msg); } } // MUC specific if (isMuc) { if (data.moderator) { bubble.querySelector('div.bubble').classList.add('moderator'); } icon = bubble.querySelector('span.primary.icon'); if (icon.querySelector('img') == undefined) { if (data.icon_url) { var img = document.createElement('img'); img.setAttribute('src', data.icon_url); img.setAttribute('loading', 'lazy'); icon.appendChild(img); } if (data.resource) { icon.dataset.resource = data.resource; } else { icon.innerHTML = ''; var i = document.createElement('i'); i.className = 'material-symbols'; i.innerText = 'notes'; icon.appendChild(i); } } if (data.quoted) { bubble.querySelector('div.bubble').classList.add('quoted'); } } if (!mergeMsg) { if (prepend) { MovimTpl.prepend('#' + id, bubble.outerHTML); } else { MovimTpl.append('#' + id, bubble.outerHTML); } } }, appendDate: function (date, prepend) { var list = document.querySelector('#chat_widget > div ul.conversation'); if (!list) return; dateNode = Chat.date.cloneNode(true); dateNode.querySelector('p').innerHTML = date; dateNode.id = MovimUtils.cleanupId(date); if (prepend) { if (list.firstChild.className == 'date') return; if (existingDate = document.getElementById(MovimUtils.cleanupId(date))) { existingDate.remove(); } list.insertBefore(dateNode, list.firstChild); } else { if (document.getElementById(MovimUtils.cleanupId(date))) { return; } list.appendChild(dateNode); } }, insertSeparator: function (counter) { separatorNode = Chat.separator.cloneNode(true); var list = document.querySelector('#chat_widget > div ul.conversation'); if (!list || list.querySelector('li.separator')) return; var messages = document.querySelectorAll('#chat_widget > div ul.conversation div.bubble p'); if (messages.length > counter && counter > 0) { var p = messages[messages.length - counter]; list.insertBefore(separatorNode, p.parentNode.parentNode.parentNode); } }, getStickerHtml: function (data) { var img = document.createElement('img'); img.classList.add('sticker'); if (data.sticker.url) { if (data.sticker.thumb) { img.setAttribute('src', data.sticker.thumb); } else { img.setAttribute('src', data.sticker.url); } if (data.sticker.width) img.setAttribute('width', data.sticker.width); if (data.sticker.height) { img.setAttribute('height', data.sticker.height); } else { img.setAttribute('height', '170'); } } return img; }, getCardHtml: function (card, story) { var ul = document.createElement('ul'); ul.innerHTML = card; ul.setAttribute('class', story ? 'list card shadow flex fourth gallery active' : 'list card middle noanim shadow active'); if (story) { ul.querySelector('li').classList.add('story'); } return ul; }, getFileHtml: function (file, data) { // Inline files if (file.disposition == 'inline' && (!file.preview || !file.preview.height) && file.type.substring(0, 5) == 'image') { var img = document.createElement('img'); img.classList.add('sticker'); img.setAttribute('src', file.url); img.setAttribute('height', '170'); return img; } var div = document.createElement('div'); div.classList.add('file'); if (file.name) { div.dataset.type = file.type; if (file.preview) { var img = document.createElement('img'); if (file.preview.url) { img.setAttribute('src', file.preview.thumb ?? file.preview.url); if (file.preview.width && file.preview.height) { img.setAttribute('width', file.preview.width); img.setAttribute('height', file.preview.height); } else if (file.thumbnail_width && file.thumbnail_height) { img.setAttribute('width', file.thumbnail_width); img.setAttribute('height', file.thumbnail_height); } } if (file.preview.title) { img.title = file.preview.title; } if (file.preview.picture) { img.classList.add('active'); img.setAttribute('onclick', 'Preview_ajaxHttpShow("' + file.preview.url + '", ' + data.mid + ')'); } if (file.preview.thumbnail_type == 'image/thumbhash' && file.preview.thumbnail_url) { try { div.style.background = `center / cover url(${thumbHashToDataURL(MovimUtils.base64ToBinary(file.preview.thumbnail_url))})`; } catch (error) { console.log('Cannot handle thumbhash hash'); } } div.appendChild(img); } if (file.type == 'audio/ogg' || file.type == 'audio/opus' || file.type == 'audio/mpeg') { div.appendChild(Chat.getAudioPlayer(file)); } else if (file.type == 'video/webm' || file.type == 'video/mp4') { div.appendChild(Chat.getVideoPlayer(file)); } var url = new URL(file.url); // Tenor implementation and inline files if (url.host && ['media.tenor.com', 'static.klipy.com'].includes(url.host) || file.type == 'audio/ogg' || file.type == 'audio/opus' || file.type == 'audio/mpeg' || file.disposition == 'inline') { return div; } if (!file.preview) { var a = document.createElement('a'); a.textContent = file.name; a.href = file.url; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.title = file.url; div.appendChild(a); if (file.host) { var host = document.createElement('span'); host.innerHTML = file.host; host.className = 'host'; a.prepend(host); } if (file.size > 0) { var span = document.createElement('span'); span.innerHTML = file.cleansize; span.className = 'size'; a.prepend(span); } } } return div; }, getAudioPlayer: function (file) { var div = document.createElement('div'); div.setAttribute('title', file.name); div.classList.add('audio_player'); div.id = 'a_' + file.message_mid; var audio = document.createElement('audio'); audio.setAttribute('src', file.url + '#t=0.01'); //audio.setAttribute('controls', ''); div.appendChild(audio); var playPauseButton = document.createElement('span'); playPauseButton.classList.add('button', 'flat', 'gray'); playPauseButton.classList.add('play_pause'); var i = document.createElement('i'); i.className = 'material-symbols'; i.innerText = 'play_arrow'; playPauseButton.appendChild(i); div.appendChild(playPauseButton); var timer = document.createElement('p'); timer.classList.add('timer'); div.appendChild(timer); var progressBar = document.createElement('input'); progressBar.type = 'range'; progressBar.value = 0; progressBar.setAttribute('min', 0); progressBar.setAttribute('max', 100); progressBar.setAttribute('step', 0.1); div.appendChild(progressBar); var downloadButton = document.createElement('a'); downloadButton.classList.add('button', 'flat', 'gray'); downloadButton.href = file.url; downloadButton.target = '_blank'; var i = document.createElement('i'); i.className = 'material-symbols'; i.innerText = 'file_download'; downloadButton.appendChild(i); div.appendChild(downloadButton); return div; }, getVideoPlayer: function (file) { var video = document.createElement('video'); video.setAttribute('src', file.url + '#t=0.01'); video.setAttribute('loop', 'loop'); if (file.thumbnail_url != null) { video.setAttribute('poster', file.thumbnail_url); video.setAttribute('width', file.thumbnail_width); video.setAttribute('height', file.thumbnail_height); } else { video.setAttribute('poster', BASE_URI + 'theme/img/poster.svg'); } var url = new URL(file.url); // Tenor implementation if ((url.host && ['media.tenor.com', 'static.klipy.com'].includes(url.host)) || file.disposition == 'inline') { video.classList.add('gif'); } else { video.setAttribute('controls', 'controls'); video.setAttribute('preload', 'metadata'); } return video; }, addQuotesHtml: function (msg, body) { let div = null; let p = null; if (!body.startsWith(' { if (line.startsWith('> ') || line == '>') { p = null; if (div == null) { div = document.createElement('div'); div.classList.add('parent'); } let divP = document.createElement('p'); let trimmed = line.substring('4'); divP.innerHTML = trimmed == '' ? ' ' : trimmed div.appendChild(divP); msg.appendChild(div); } else { div = null; if (p == null) { p = document.createElement('p'); p.innerHTML = line; } else { p.innerHTML += "\n" + line; } msg.appendChild(p); } }); return; } p = msg.getElementsByTagName('p')[0]; p.innerHTML = body; }, getParentHtml: function (parent) { var div = document.createElement('div'); div.classList.add('parent'); div.dataset.parentReplaceId = parent.replaceid div.dataset.parentId = parent.id; div.id = 'parent' + parent.id; var span = document.createElement('span'); if (parent.color) { span.classList.add('resource'); span.classList.add(parent.color); } span.classList.add('from'); span.innerHTML = parent.fromName; div.appendChild(span); var p = document.createElement('p'); div.appendChild(p); if (parent.omemoheader && OMEMO_ENABLED) { p.innerHTML = parent.omemoheader.payload.substring(0, parent.omemoheader.payload.length / 2); ChatOmemo.decrypt(parent).then(plaintext => { let refreshP = document.querySelector('#parent' + parent.id + ' p'); if (refreshP) { if (plaintext) { refreshP.innerText = plaintext; } else { refreshP.classList.add('error'); } } }); } else { p.innerHTML = parent.body; } return div; }, toggleAction: function () { var chatBox = document.querySelector('#chat_widget .chat_box'); if (chatBox) { if (Chat.getTextarea().value.length > 0) { chatBox.classList.add('compose'); Chat.toggleAttach(true); } else { chatBox.classList.remove('compose'); } } }, toggleAttach: function (forceDisabled) { var attach = document.querySelector('#chat_widget .chat_box span.attach'); if (attach) { if (forceDisabled) { attach.classList.remove('enabled'); } else { attach.classList.toggle('enabled'); } } }, getTextarea: function () { var textarea = document.querySelector('#chat_textarea'); if (textarea) return textarea; }, isEncrypted: function () { return (Chat.getTextarea() && Chat.getTextarea().dataset.encryptedstate == 'yes'); }, getDiscussion: function () { return document.querySelector('#chat_widget div.contained'); }, touchEvents: function () { var chat = document.querySelector('#chat_widget'); if (!chat) return; chat.addEventListener('touchstart', function (event) { chat.classList.remove('moving'); Chat.startX = event.targetTouches[0].pageX; Chat.startY = event.targetTouches[0].pageY; }, true); chat.addEventListener('touchmove', function (event) { Chat.translateX = parseInt(event.targetTouches[0].pageX - Chat.startX); Chat.translateY = parseInt(event.targetTouches[0].pageY - Chat.startY); if (Chat.translateX > Chat.delay && Chat.translateX <= Chat.clientWidth) { // If the horizontal movement is allowed and the vertical one is not important // we authorize the slide if (Math.abs(Chat.translateY) < Chat.delay) { Chat.slideAuthorized = true; } if (Chat.slideAuthorized) { chat.style.transform = 'translateX(' + (Chat.translateX - Chat.delay) + 'px)'; } } }, true); chat.addEventListener('touchend', function (event) { chat.classList.add('moving'); if (Chat.translateX > (Chat.clientWidth / 4) && Chat.slideAuthorized) { MovimTpl.hidePanel(); Chat.get(null, true); } chat.style.transform = ''; Chat.slideAuthorized = false; Chat.startX = Chat.translateX = Chat.startY = Chat.translateY = 0; }, true); }, isValidHttpUrl: function (string) { let url; try { url = new URL(string); } catch (_) { return false; } return url.protocol === "http:" || url.protocol === "https:"; }, getNewerMessages: function () { var jid = MovimUtils.urlParts().params[0]; let lastMessage = Chat.getDiscussion()?.querySelector('li:last-child .bubble:last-child .message:last-child'); if (jid) { Chat_ajaxGetHistory( jid, lastMessage ? lastMessage.dataset.published : null, (MovimUtils.urlParts().params[1] === 'room'), false, false ); } }, searchMembers: function (key) { var selector = '#room_nav_members > li'; document.querySelectorAll(selector) .forEach(item => item.classList.remove('found')); var founds = document.querySelectorAll( selector + '[name*="' + MovimUtils.cleanupId(key).slice(3) + '"]' ); founds.forEach(item => item.classList.add('found')); } }; MovimWebsocket.attach(function () { if (!Chat.pagination) { Chat_ajaxInit(); } var jid = MovimUtils.urlParts().params[0]; if (jid) { if (Boolean(document.getElementById(MovimUtils.cleanupId(jid) + '-conversation'))) { Chat.getNewerMessages(); } else { (MovimUtils.urlParts().params[1] === 'room') ? Chat.getRoom(jid) : Chat.get(jid); } } else { if (!MovimUtils.isMobile()) { Chat_ajaxHttpGetEmpty(); } if (MovimUtils.urlParts().page == 'chat') { Notif.current('chat'); } } }); MovimEvents.registerWindow('focus', 'chat', () => { if (MovimWebsocket.connection) { var jid = MovimUtils.urlParts().params[0]; var room = (MovimUtils.urlParts().params[1] === 'room'); if (jid) { Chat_ajaxGetHeader(jid, room); if (room) { Chat_ajaxHttpGetPresences(jid); } } } var textarea = Chat.getTextarea(); if (textarea) textarea.focus(); }); MovimEvents.registerWindow('resize', 'chat', () => Chat.scrollRestore()); MovimEvents.registerWindow('loaded', 'chat', () => { if (MovimUtils.isMobile()) Chat.touchEvents(); if (typeof Upload != 'undefined') { Upload.initiate((file) => { if (['chat', 'space'].includes(MovimUtils.urlParts().page) && (typeof (PublishStories) == 'undefined' || PublishStories.main == undefined)) { Upload.prependName = 'chat'; } }); Upload.attach((file) => { if (['chat', 'space'].includes(MovimUtils.urlParts().page) && (typeof (PublishStories) == 'undefined' || PublishStories.main == undefined)) { Chat_ajaxHttpDaemonSendMessage( Chat.getTextarea().dataset.jid, false, Boolean(Chat.getTextarea().dataset.muc), file ); } }); } // Really early panel showing in case we have a JID var parts = MovimUtils.urlParts(); if (parts.page == 'chat' && parts.params[0]) { MovimTpl.showPanel(); } }); var state = 0;