| <html> |
| <!-- Empty head might be needed for setSenderImage --> |
| <head> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <meta charset=“utf-8”> |
| </head> |
| |
| <body> |
| <div id="invitation"> |
| <div id="text"> |
| </div> |
| <div id="actions"> |
| <div id="accept-btn" class="invitation-button button-green" onclick="acceptInvitation()" >Accept</div> |
| <div id="refuse-btn" class="invitation-button button-red" onclick="refuseInvitation()" >Refuse</div> |
| <div id="block-btn" class="invitation-button button-red" onclick="blockConversation()" >Block</div> |
| </div> |
| </div> |
| <div id="container"> |
| <div id="messages"></div> |
| |
| <div id="sendMessage"> |
| <textarea id="message" autofocus placeholder="Type a message" onkeyup="grow_text_area()" rows="1" disabled="false"></textarea> |
| <div class="msg-button" onclick="sendMessage()" title="Send"> |
| <svg viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"> |
| <path xmlns="http://www.w3.org/2000/svg" d="M12,11.874v4.357l7-6.69l-7-6.572v3.983c-8.775,0-11,9.732-11,9.732C3.484,12.296,7.237,11.874,12,11.874z"/> |
| </svg> |
| </div> |
| <div class="msg-button" onclick="sendFile()" title="Send File"> |
| <svg viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"> |
| <path d="M0 0h24v24H0z" fill="none" /> |
| <path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/> |
| </svg> |
| </div> |
| </div> |
| </form> |
| </div> |
| </body> |
| |
| <script> |
| |
| const avatar_size = 35; |
| var raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame; |
| var displayLinksEnabled = false; |
| var historyBufferIndex = 0; // When showing a large amount of interactions, this counter store the interaction's index to show |
| var historyBuffer = []; // Before showing a large amount of interactions, this array is used as a buffer. |
| var hoverBackButtonAllowed = true; |
| |
| /* We retrieve refs to the most used navbar and message bar elements for efficiency purposes */ |
| /* NOTE: always use getElementById when possible, way more efficient */ |
| const messageBar = document.getElementById('sendMessage'); |
| const messageBarInput = document.getElementById("message"); |
| const messages = document.getElementById("messages"); |
| const invitation = document.getElementById("invitation"); |
| const invitationText = document.getElementById('text'); |
| |
| /* States: allows us to avoid re-doing something if it isn't meaningful */ |
| var hasInvitation = false; |
| |
| messageBarInput.addEventListener("keydown", function (e) { |
| e = e || event; |
| var map = {}; |
| map[e.keyCode] = e.type == 'keydown'; |
| if (e.ctrlKey || e.shiftKey) { |
| return true; |
| } |
| if (map[13]) { |
| sendMessage(); |
| e.preventDefault(); |
| } |
| return true; |
| }); |
| |
| function grow_text_area() { |
| var is_at_bottom = messages.scrollTop === (messages.scrollHeight - messages.offsetHeight); |
| |
| var old_height = messageBarInput.style.height; |
| messageBarInput.style.height = "auto"; |
| messageBarInput.style.height = messageBarInput.scrollHeight +"px"; |
| |
| if (is_at_bottom) { |
| messages.scrollTop = messages.scrollHeight; |
| } |
| } |
| |
| /* |
| * Update timestamps messages |
| */ |
| function updateView() { |
| updateTimestamps(); |
| } |
| |
| setInterval(updateView, 60000); |
| |
| window.onresize = function(event) { |
| updateTimestamps(); |
| }; |
| |
| function setDisplayLinks(display) { |
| displayLinksEnabled = display; |
| } |
| |
| /** |
| * Transform a date to a string group like "1 hour ago". |
| */ |
| function formatDate(date) { |
| const seconds = Math.floor((new Date() - date) / 1000); |
| var interval = Math.floor(seconds / (3600 * 24)); |
| if (interval > 5) { |
| return date.toLocaleDateString(); |
| } |
| if (interval > 1) { |
| return interval + ' days ago'; |
| } |
| if (interval === 1) { |
| return interval + ' day ago'; |
| } |
| interval = Math.floor(seconds / 3600); |
| if (interval > 1) { |
| return interval + ' hours ago'; |
| } |
| if (interval === 1) { |
| return interval + ' hour ago'; |
| } |
| interval = Math.floor(seconds / 60); |
| if (interval > 1) { |
| return interval + ' minutes ago'; |
| } |
| return 'just now'; |
| } |
| |
| /** |
| * Send #sendMessage #message value |
| */ |
| function sendMessage() |
| { |
| var message = messageBarInput.value; |
| if (message.length > 0) { |
| messageBarInput.value = ''; |
| window.prompt('SEND:' + message); |
| } |
| } |
| |
| /** |
| * Disable or enable textarea |
| */ |
| function disableSendMessage(isDisabled) |
| { |
| messageBarInput.disabled = isDisabled; |
| } |
| |
| /** |
| * Change the value of the progress bar |
| */ |
| function hideMessageBar(isHidden) { |
| isHidden ? messageBar.classList.add("hiddenState") : messageBar.classList.remove("hiddenState"); |
| } |
| |
| /** |
| * Accept an invite |
| */ |
| function acceptInvitation() |
| { |
| window.prompt('ACCEPT'); |
| } |
| |
| /** |
| * Refuse an invite |
| */ |
| function refuseInvitation() |
| { |
| window.prompt('REFUSE'); |
| } |
| |
| /** |
| * Accept an invite |
| */ |
| function blockConversation() |
| { |
| window.prompt('BLOCK'); |
| } |
| |
| /** |
| * Change the content of the div invitation and show it |
| * @param contactAlias |
| */ |
| function showInvitation(contactAlias) { |
| hasInvitation = true; |
| invitationText.innerHTML = '<h1>' + contactAlias + ' sends you an invitation</h1>' |
| + 'Do you want to add them to the conversations list?<br>' |
| + 'Note: you can automatically accept this invitation by sending a message.'; |
| invitation.style.visibility = 'visible'; |
| } |
| |
| /** |
| * Hide the content of invitation |
| */ |
| function hideInvitation() { |
| if (hasInvitation) { |
| hasInvitation = false; |
| invitation.style.visibility = 'hidden'; |
| } |
| } |
| |
| /** |
| * Clears all messages |
| */ |
| function clearMessages() |
| { |
| messages.innerHTML = ""; |
| } |
| |
| /** |
| * Converts text to HTML |
| */ |
| function escapeHtml(html) |
| { |
| var text = document.createTextNode(html); |
| var div = document.createElement('div'); |
| div.appendChild(text); |
| return div.innerHTML; |
| } |
| |
| |
| /** |
| * Get the youtube video id from a URI |
| */ |
| function youtube_id(url) { |
| const regExp = /^.*(youtu\.be\/|v\/|\/u\/w|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; |
| const match = url.match(regExp); |
| return (match && match[2].length == 11) ? match[2] : null; |
| } |
| |
| /** |
| * Returns HTML message from the message text. |
| * Cleaned and linkified. |
| */ |
| function getMessageHtml(message_text) |
| { |
| const escaped_message = escapeHtml(message_text); |
| var linkified_message = linkifyHtml(escaped_message, {}); |
| |
| const textPart = document.createElement('pre'); |
| textPart.innerHTML = linkified_message; |
| |
| return textPart.outerHTML; |
| } |
| |
| /** |
| * Returns the message status, formatted for display |
| */ |
| function getMessageDeliveryStatusText(message_delivery_status) |
| { |
| var formatted_delivery_status = message_delivery_status; |
| |
| switch(message_delivery_status) |
| { |
| case "sending": |
| case "ongoing": |
| formatted_delivery_status = "Sending<svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><circle class='status_circle anim-first' cx='4' cy='12' r='1'/><circle class='status_circle anim-second' cx='8' cy='12' r='1'/><circle class='status_circle anim-third' cx='12' cy='12' r='1'/></svg>"; |
| break; |
| case "failure": |
| formatted_delivery_status = "Failure <svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><path class='status-x x-first' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M4,4 L12,12'/><path class='status-x x-second' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M12,4 L4,12'/></svg>"; |
| break; |
| case "sent": |
| case "finished": |
| case "unknown": |
| case "read": |
| formatted_delivery_status = ""; |
| break; |
| default: |
| console.log("getMessageDeliveryStatusText: unknown delivery status: " + message_delivery_status); |
| break; |
| } |
| |
| return formatted_delivery_status; |
| } |
| |
| /** |
| * Returns the message date, formatted for display |
| */ |
| function getMessageTimestampText(message_timestamp, custom_format) |
| { |
| const date = new Date(1000 * message_timestamp); |
| if(custom_format) { |
| return formatDate(date); |
| } else { |
| return date.toLocaleString(); |
| } |
| } |
| |
| /** |
| * Merge timestamps if they are from the same group (likes: "1 hour ago") |
| */ |
| function cleanPreviousTimestamps (baseNode, endIndex) { |
| // Remove previous elements with the same formatted timestamp |
| for (var c = endIndex - 1 ; c >= 0 ; --c) { |
| const child = messages.children[c]; |
| if (child.className.indexOf('timestamp') !== -1) { |
| if (child.className === baseNode.className && |
| child.innerHTML === baseNode.innerHTML) { |
| messages.removeChild(child); |
| } else { |
| break; // A different timestamp is met, we can stop here. |
| } |
| } |
| } |
| } |
| |
| /** |
| * Update timestamps |
| */ |
| function updateTimestamps() { |
| const timestamps = messages.querySelectorAll('.timestamp'); |
| const is_image_out = messages.querySelector('.message_out .sender_image') |
| const is_image_in = messages.querySelector('.message_in .sender_image') |
| if (timestamps) { |
| for ( var c = 0 ; c < messages.children.length ; ++c) { |
| const child = messages.children[c]; |
| if (child.className.indexOf('timestamp') !== -1) { |
| // Update timestamp |
| child.innerHTML = getMessageTimestampText(child.getAttribute('message_timestamp'), true) |
| |
| var desktop_margin = '25%' |
| const height = document.body.clientHeight |
| const width = document.body.clientWidth |
| if (width <= 1920 || height <= 1080) { |
| desktop_margin = '15%' |
| } |
| if (width <= 1000 || height <= 480) { |
| desktop_margin = '0px' |
| } |
| if (child.className.indexOf('timestamp_out') !== -1) { |
| const avatar_px = is_image_out ? (is_image_out.offsetHeight === avatar_size ? '60px' : '20px') : '20px' |
| child.style.paddingRight = `calc(${desktop_margin} + ${avatar_px})` |
| } else if (child.className.indexOf('timestamp_in') !== -1) { |
| const avatar_px = is_image_in ? (is_image_in.offsetHeight === avatar_size ? '60px' : '20px') : '20px' |
| child.style.paddingLeft = `calc(${desktop_margin} + ${avatar_px})` |
| } |
| // Remove previous elements with the same formatted timestamp |
| cleanPreviousTimestamps(child, c); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Convert a value in filesize |
| */ |
| function humanFileSize(bytes) { |
| var thresh = 1024; |
| if(Math.abs(bytes) < thresh) { |
| return bytes + ' B'; |
| } |
| var units = ['kB','MB','GB','TB','PB','EB','ZB','YB'] |
| var u = -1; |
| do { |
| bytes /= thresh; |
| ++u; |
| } while(Math.abs(bytes) >= thresh && u < units.length - 1); |
| return bytes.toFixed(1)+' '+units[u]; |
| } |
| |
| /** |
| * Change the value of the progress bar |
| */ |
| function updateProgressBar(progress_bar, message_object, message_delivery_status) { |
| var delivery_status = message_object["delivery_status"]; |
| if ("progress" in message_object && !isErrorStatus(delivery_status) && message_object["progress"] !== 100) { |
| var progress_percent = (100 * message_object["progress"] / message_object["totalSize"]); |
| if (progress_percent !== 100) |
| progress_bar.childNodes[0].setAttribute("style", "width: " + progress_percent + "%"); |
| else |
| progress_bar.setAttribute("style", "display: none"); |
| } else |
| progress_bar.setAttribute("style", "display: none"); |
| } |
| |
| /** |
| * Check if a status is an error status |
| */ |
| function isErrorStatus(status) { |
| return (status === 'failure' |
| || status === 'awaiting peer timeout' |
| || status === 'canceled' |
| || status === 'unjoinable peer'); |
| } |
| |
| /** |
| * Build a new file interaction |
| * @param message_id |
| */ |
| function fileInteraction(message_id) { |
| var message_wrapper = document.createElement('div') |
| message_wrapper.setAttribute('class', 'message_wrapper') |
| |
| // An file interaction contains buttons at the left of the interaction |
| // for the status or accept/refuse |
| var left_buttons = document.createElement('div') |
| left_buttons.setAttribute('class', 'left_buttons') |
| message_wrapper.appendChild(left_buttons) |
| // Also contains a bold clickable text |
| var text_div = document.createElement('div') |
| text_div.setAttribute('class', 'text') |
| text_div.addEventListener('click', function (event) { |
| // ask ring to open the file |
| const filename = document.querySelector('#message_' + message_id + ' .full').innerText |
| window.prompt('OPEN_FILE:' + filename) |
| }); |
| var full_div = document.createElement('div') |
| full_div.setAttribute('class', 'full') |
| full_div.style = 'visibility: hidden; display: none;' |
| var message_text = document.createElement('div') |
| message_text.setAttribute('class', 'filename') |
| // And some informations like size or error message. |
| var informations_div = document.createElement('div') |
| informations_div.setAttribute('class', 'informations') |
| text_div.appendChild(message_text) |
| text_div.appendChild(full_div) |
| text_div.appendChild(informations_div) |
| message_wrapper.appendChild(text_div) |
| // And finally, a progress bar |
| message_transfer_progress_bar = document.createElement('span') |
| message_transfer_progress_bar.setAttribute('class', 'message_progress_bar') |
| message_transfer_progress_completion = document.createElement('span') |
| message_transfer_progress_bar.appendChild(message_transfer_progress_completion) |
| message_wrapper.appendChild(message_transfer_progress_bar) |
| return message_wrapper |
| } |
| |
| /** |
| * Update a file interaction (icons + filename + status + progress bar) |
| * @param message_div the message to update |
| * @param message_object new informations |
| */ |
| function updateFileInteraction(message_div, message_object, forceTypeToFile = false) { |
| if (!message_div.querySelector('.informations')) return // media |
| |
| var acceptSvg = '<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>', |
| refuseSvg = '<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/><path d="M0 0h24v24H0z" fill="none"/></svg>', |
| fileSvg = '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>', |
| warningSvg = '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>' |
| var message_delivery_status = message_object["delivery_status"] |
| var message_direction = message_object["direction"] |
| var message_id = message_object["id"] |
| var message_text = message_object['text'] |
| |
| |
| if (isImage(message_text) && message_delivery_status === 'finished' && displayLinksEnabled && !forceTypeToFile) { |
| // Replace the old wrapper by the downloaded image |
| if (message_div.querySelector('.message_wrapper')) { |
| wrapper = message_div.querySelector('.message_wrapper') |
| wrapper.parentNode.removeChild(wrapper) |
| } |
| message_div.append(mediaInteraction(message_id, message_text)) |
| message_div.querySelector('img').id = message_id |
| message_div.querySelector('img').msg_obj = message_object |
| message_div.querySelector('img').onerror = function() { |
| message_div = document.getElementById('#message_' + this.id); |
| if (message_div.querySelector('.message_wrapper')) { |
| wrapper = message_div.querySelector('.message_wrapper') |
| wrapper.parentNode.removeChild(wrapper) |
| } |
| message_div.append(fileInteraction(message_id)) |
| updateFileInteraction(message_div, this.msg_obj, true) |
| } |
| return |
| } |
| |
| // Set informations text |
| var informations_div = message_div.querySelector(".informations"); |
| var informations_txt = getMessageTimestampText(message_object["timestamp"], true); |
| if (message_object["totalSize"] && message_object["progress"]) { |
| if (message_delivery_status === 'finished') { |
| informations_txt += " - " + humanFileSize(message_object["totalSize"]); |
| } else { |
| informations_txt += " - " + humanFileSize(message_object["progress"]) |
| + " / " + humanFileSize(message_object["totalSize"]); |
| } |
| } |
| informations_txt += " - " + message_delivery_status; |
| informations_div.innerText = informations_txt; |
| |
| // Update flat buttons |
| var left_buttons = message_div.querySelector(".left_buttons"); |
| left_buttons.innerHTML = ''; |
| if (message_delivery_status === 'awaiting peer' || message_delivery_status === 'awaiting host' || message_delivery_status.indexOf('ongoing') === 0) { |
| if (message_direction === 'in' && message_delivery_status.indexOf('ongoing') !== 0) { |
| // add buttons to accept or refuse a call. |
| var accept_button = document.createElement('div'); |
| accept_button.innerHTML = acceptSvg; |
| accept_button.setAttribute("title", "Accept"); |
| accept_button.setAttribute("class", "flat-button accept"); |
| accept_button.onclick = function() { |
| window.prompt('ACCEPT_FILE:' + message_id); |
| } |
| left_buttons.appendChild(accept_button); |
| } |
| var refuse_button = document.createElement('div'); |
| refuse_button.innerHTML = refuseSvg; |
| refuse_button.setAttribute("title", "Refuse"); |
| refuse_button.setAttribute("class", "flat-button refuse"); |
| refuse_button.onclick = function() { |
| window.prompt('REFUSE_FILE:' + message_id); |
| } |
| left_buttons.appendChild(refuse_button); |
| } else { |
| var status_button = document.createElement('div'); |
| var statusFile = fileSvg; |
| if (isErrorStatus(message_delivery_status)) |
| statusFile = warningSvg; |
| status_button.innerHTML = statusFile; |
| status_button.setAttribute("class", "flat-button"); |
| left_buttons.appendChild(status_button); |
| } |
| |
| message_div.querySelector('.full').innerText = message_text |
| message_div.querySelector('.filename').innerText = message_text.split('/').pop() |
| message_div.querySelector('.filename').innerText = message_text.split('/').pop() |
| updateProgressBar(message_div.querySelector('.message_progress_bar'), message_object); |
| updateProgressBar(message_div.querySelector('.message_progress_bar'), message_object); |
| } |
| |
| /** |
| * Return if a file is an image |
| * @param file |
| */ |
| function isImage(file) { |
| return file.toLowerCase().match(/\.(jpeg|jpg|gif|png)$/) !== null |
| } |
| |
| /** |
| * Return if a file is a youtube video |
| * @param file |
| */ |
| function isVideo(file) { |
| const availableProtocols = ['http:', 'https:'] |
| const videoHostname = ['youtube.com', 'www.youtube.com', 'youtu.be'] |
| const urlParser = document.createElement('a') |
| urlParser.href = file |
| return (availableProtocols.includes(urlParser.protocol) && videoHostname.includes(urlParser.hostname)) |
| } |
| |
| /** |
| * Try to show an image or a video link (youtube for now) |
| * @param message_id |
| * @param link to show |
| * @param ytid if it's a youtube video |
| */ |
| function mediaInteraction(message_id, link, ytid, noerror) { |
| // TODO promise? |
| // Try to display images. |
| const media_wrapper = document.createElement('div') |
| media_wrapper.setAttribute('class', 'media_wrapper') |
| const linkElt = document.createElement('a') |
| linkElt.href = link |
| linkElt.style = 'text-decoration: none;' |
| const imageElt = document.createElement('img') |
| // Note, here, we don't check the size of the image. |
| // in the future, we can check the content-type and content-length with a request |
| // and maybe disable svg |
| if (noerror) |
| imageElt.setAttribute('onerror', 'this.style.display=\'none\'') |
| if (ytid) { |
| imageElt.src = `http://img.youtube.com/vi/${ytid}/0.jpg` |
| } else { |
| imageElt.src = link |
| } |
| linkElt.appendChild(imageElt) |
| if (ytid) { |
| const containerElt = document.createElement('div'); |
| containerElt.setAttribute('class', 'containerVideo'); |
| const playDiv = document.createElement('div') |
| playDiv.setAttribute('class', 'playVideo') |
| playDiv.innerHTML = '<svg fill="#ffffff" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">\ |
| <path d="M8 5v14l11-7z"/>\ |
| <path d="M0 0h24v24H0z" fill="none"/>\ |
| </svg>' |
| linkElt.appendChild(playDiv) |
| containerElt.appendChild(linkElt) |
| media_wrapper.appendChild(containerElt) |
| } else { |
| media_wrapper.appendChild(linkElt) |
| } |
| return media_wrapper |
| } |
| |
| /** |
| * Build a new text interaction |
| * @param message_id |
| * @param htmlText the DOM to show |
| */ |
| function textInteraction(message_id, htmlText) { |
| const message_wrapper = document.createElement('div') |
| message_wrapper.setAttribute('class', 'message_wrapper') |
| var message_text = document.createElement('span') |
| message_text.setAttribute('class', 'message_text') |
| message_text.innerHTML = htmlText |
| message_wrapper.appendChild(message_text) |
| // TODO STATUS |
| return message_wrapper |
| } |
| |
| /** |
| * Update a text interaction (text) |
| * @param message_div the message to update |
| * @param delivery_status the status of the message |
| * @TODO retry button |
| */ |
| function updateTextInteraction(message_div, delivery_status) { |
| if (!message_div.querySelector('.message_text')) return // media |
| switch(delivery_status) |
| { |
| case "ongoing": |
| case "sending": |
| var sending_div = message_div.querySelector('.sending') |
| if (!sending_div) { |
| sending_div = document.createElement('div') |
| sending_div.setAttribute('class', 'sending') |
| sending_div.innerHTML = '<svg overflow="hidden" viewBox="0 -2 16 14" height="16px" width="16px"><circle class="status_circle anim-first" cx="4" cy="12" r="1"/><circle class="status_circle anim-second" cx="8" cy="12" r="1"/><circle class="status_circle anim-third" cx="12" cy="12" r="1"/></svg>' |
| // add sending animation to message |
| message_div.appendChild(sending_div) |
| } |
| message_div.querySelector('.message_text').style = 'color: #888' |
| break |
| case "failure": |
| // change text color to red |
| message_div.querySelector('.message_wrapper').style = 'background-color: #f3a6a6' |
| message_div.querySelector('.message_text').style = 'color: #000' |
| var failure_div = message_div.querySelector('.failure') |
| if (!failure_div) { |
| failure_div = document.createElement('div') |
| failure_div.setAttribute('class', 'failure') |
| failure_div.innerHTML = '<svg overflow="visible" viewBox="0 -2 16 14" height="16px" width="16px"><path class="status-x x-first" stroke="#AA0000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" fill="none" d="M4,4 L12,12"/><path class="status-x x-second" stroke="#AA0000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" fill="none" d="M12,4 L4,12"/></svg>' |
| // add failure animation to message |
| message_div.appendChild(failure_div) |
| } |
| var sending = message_div.querySelector('.sending') |
| if (sending) sending.style.visibility = 'hidden' |
| break |
| case "sent": |
| case "finished": |
| case "unknown": |
| case "read": |
| // change text color to black |
| message_div.querySelector('.message_text').style = 'color: #000' |
| var sending = message_div.querySelector('.sending') |
| if (sending) sending.style.visibility = 'hidden' |
| break |
| default: |
| console.log("getMessageDeliveryStatusText: unknown delivery status: " + delivery_status) |
| break |
| } |
| } |
| |
| /** |
| * Build a new interaction (call or contact) |
| * @param message_id |
| */ |
| function actionInteraction(message_id) { |
| var message_wrapper = document.createElement('div') |
| message_wrapper.setAttribute('class', 'message_wrapper') |
| |
| // An file interaction contains buttons at the left of the interaction |
| // for the status or accept/refuse |
| var left_buttons = document.createElement('div') |
| left_buttons.setAttribute('class', 'left_buttons') |
| message_wrapper.appendChild(left_buttons) |
| // Also contains a bold clickable text |
| var text_div = document.createElement('div') |
| text_div.setAttribute('class', 'text') |
| message_wrapper.appendChild(text_div) |
| return message_wrapper |
| } |
| |
| /** |
| * Update a call interaction (icon + text) |
| * @param message_div the message to update |
| * @param message_object new informations |
| */ |
| function updateCallInteraction(message_div, message_object) { |
| const outgoingCall = '<svg fill="#219d55" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5z"/></svg>' |
| const callMissed = '<svg fill="#dc2719" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.59 7L12 14.59 6.41 9H11V7H3v8h2v-4.59l7 7 9-9z"/></svg>' |
| const outgoingMissed = '<svg fill="#dc2719" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M24 24H0V0h24v24z" id="a"/></defs><clipPath id="b"><use overflow="visible" xlink:href="#a"/></clipPath><path clip-path="url(#b)" d="M3 8.41l9 9 7-7V15h2V7h-8v2h4.59L12 14.59 4.41 7 3 8.41z"/></svg>' |
| const callReceived = '<svg fill="#219d55" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 5.41L18.59 4 7 15.59V9H5v10h10v-2H8.41z"/></svg>' |
| |
| const message_text = message_object['text'] |
| const message_direction = (message_text.toLowerCase().indexOf('incoming') !== -1) ? 'in' : 'out' |
| const missed = message_text.indexOf('Missed') !== -1 |
| |
| message_div.querySelector('.text').innerText = message_text.substring(2) |
| |
| var left_buttons = message_div.querySelector('.left_buttons') |
| left_buttons.innerHTML = '' |
| var status_button = document.createElement('div') |
| var statusFile = '' |
| if (missed) |
| statusFile = (message_direction === 'in') ? callMissed : outgoingMissed |
| else |
| statusFile = (message_direction === 'in') ? callReceived : outgoingCall |
| status_button.innerHTML = statusFile |
| status_button.setAttribute('class', 'flat-button') |
| left_buttons.appendChild(status_button) |
| } |
| |
| /** |
| * Update a contact interaction (icon + text) |
| * @param message_div the message to update |
| * @param message_object new informations |
| */ |
| function updateContactInteraction(message_div, message_object) { |
| const message_text = message_object['text'] |
| |
| message_div.querySelector('.text').innerText = message_text |
| |
| var left_buttons = message_div.querySelector('.left_buttons') |
| left_buttons.innerHTML = '' |
| var status_button = document.createElement('div') |
| status_button.innerHTML = '<?xml version="1.0" ?><svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg">\ |
| <path d="M8.013766 23.107838l0.00465 -0.810382 0.033095 -0.133474c0.1495327 -0.603099 0.50107 -1.099452 1.1411917 -1.611311 0.1690725 -0.135195 0.5493165 -0.388522 0.7866284 -0.524069 1.4449359 -0.825313 3.6497829 -1.439907 5.5693029 -1.552424 0.268992 -0.01577 0.866138 -0.0085 1.131356 0.01373 1.873734 0.157215 3.884055 0.729268 5.262712 1.497544 1.031356 0.574739 1.70347 1.254543 1.937152 1.959316 0.101301 0.305518 0.100213 0.293393 0.105924 1.180132l0.0051 0.791314 -7.990878 0 -7.990878 0 0.00465 -0.810381z" fill="#000000"/>\ |
| <path d="M15.641818 15.906151c-0.512638 -0.04757 -0.970838 -0.177847 -1.424519 -0.40503 -0.534623 -0.267715 -0.972855 -0.622072 -1.344489 -1.087162 -0.500329 -0.626146 -0.797007 -1.385268 -0.858271 -2.196087 -0.01377 -0.182277 -0.0068 -0.568261 0.01341 -0.738308 0.137062 -1.155519 0.73576 -2.1602655 1.685808 -2.8291565 1.010233 -0.711264 2.333521 -0.90809 3.519055 -0.523424 0.511977 0.166119 0.9845 0.434474 1.39324 0.791249 0.103105 0.09 0.316993 0.304905 0.402137 0.404056 0.848222 0.9877635 1.16007 2.3144245 0.843044 3.5864705 -0.116312 0.466694 -0.339476 0.947863 -0.621858 1.340798 -0.695617 0.967955 -1.761735 1.568261 -2.95044 1.661324 -0.155481 0.01217 -0.501821 0.0097 -0.657113 -0.0047z" fill="#000000"/>\ |
| </svg>' |
| status_button.setAttribute('class', 'flat-button') |
| left_buttons.appendChild(status_button) |
| } |
| |
| /** |
| * Add a message to the conversation. |
| * @param message_object to treat |
| * @param new_message if it's a new message or if we need to update |
| * @param insert_after if we want the message at the end or the top of the conversation |
| */ |
| function addOrUpdateMessage(message_object, new_message, insert_after = true) { |
| const message_id = message_object['id'] |
| const message_type = message_object['type'] |
| const message_text = message_object['text'] |
| const message_direction = message_object['direction'] |
| const message_timestamp = message_object['timestamp'] |
| const delivery_status = message_object['delivery_status'] |
| const message_sender_contact_method = message_object['sender_contact_method'] |
| |
| var message_div = document.querySelector('#message_' + message_id); |
| var type = '' |
| if (new_message) { |
| // Build new message |
| var classes = [ |
| 'message', |
| `message_${message_direction}`, |
| `message_type_${message_type}` |
| ] |
| |
| message_div = document.createElement('div') |
| message_div.setAttribute('id', `message_${message_id}`) |
| message_div.setAttribute('class', classes.join(' ')) |
| |
| // Build message for each types. |
| // Add sender images if necessary (like if the interaction doesn't take the whole width) |
| const need_sender = (message_type === 'data_transfer' || message_type === 'text'); |
| if (need_sender) { |
| var message_sender_image = document.createElement('span') |
| message_sender_image.setAttribute('class', `sender_image sender_image_${message_sender_contact_method}`) |
| message_div.appendChild(message_sender_image) |
| } |
| |
| // Build main content |
| if (message_type === 'data_transfer') { |
| if (isImage(message_text) && delivery_status === 'finished' && displayLinksEnabled) { |
| message_div.append(mediaInteraction(message_id, message_text, null, false)) |
| message_div.querySelector('img').id = message_id |
| message_div.querySelector('img').msg_obj = message_object |
| message_div.querySelector('img').onerror = function() { |
| message_div = document.getElementById('#message_' + this.id); |
| if (message_div.querySelector('.message_wrapper')) { |
| wrapper = message_div.querySelector('.message_wrapper') |
| wrapper.parentNode.removeChild(wrapper) |
| } |
| message_div.append(fileInteraction(message_id)) |
| updateFileInteraction(message_div, this.msg_obj, true) |
| } |
| } else { |
| message_div.append(fileInteraction(message_id)) |
| } |
| } else if (message_type === 'text') { |
| // TODO add the possibility to update messages (remove and rebuild) |
| const htmlText = getMessageHtml(message_text) |
| if (displayLinksEnabled) { |
| const parser = new DOMParser(); |
| const DOMMsg = parser.parseFromString(htmlText, 'text/xml'); |
| const links = DOMMsg.querySelectorAll('a'); |
| if (DOMMsg.childNodes.length && links.length) { |
| var isTextToShow = (DOMMsg.childNodes[0].childNodes.length > 1) |
| const ytid = (isVideo(message_text))? youtube_id(message_text) : '' |
| if (!isTextToShow && (ytid || isImage(message_text))) { |
| type = 'media' |
| message_div.append(mediaInteraction(message_id, message_text, ytid)) |
| } |
| } |
| } |
| if (type !== 'media') { |
| type = 'text' |
| message_div.append(textInteraction(message_id, htmlText)) |
| } |
| } else if (message_type === 'call' || message_type === 'contact') { |
| message_div.append(actionInteraction(message_id)) |
| } else { |
| const temp = document.createElement('div') |
| temp.innerText = message_type |
| message_div.appendChild(temp) |
| } |
| |
| // Get timestamp to add |
| const formattedTimestamp = getMessageTimestampText(message_timestamp, true) |
| // Create the timestamp object |
| const date_elt = document.createElement('div') |
| date_elt.innerText = formattedTimestamp |
| if (message_type === 'call' || message_type === 'contact') { |
| timestamp_div_classes = [ |
| 'timestamp', |
| `timestamp_action` |
| ] |
| } else { |
| timestamp_div_classes = [ |
| 'timestamp', |
| `timestamp_${message_direction}` |
| ] |
| } |
| date_elt.setAttribute('class', timestamp_div_classes.join(' ')) |
| date_elt.setAttribute('message_timestamp', message_timestamp) |
| // Remove last timestamp if it's the same<h6></h6> |
| if (messages.querySelectorAll('.timestamp')) |
| cleanPreviousTimestamps(date_elt, messages.children.length) |
| |
| // Add message and the new timestamp |
| if (insert_after) { |
| if (message_type === 'call' || message_type === 'contact') { |
| message_div.querySelector('.message_wrapper').appendChild(date_elt) |
| messages.appendChild(message_div) |
| } else { |
| messages.appendChild(message_div) |
| messages.appendChild(date_elt) |
| } |
| } else { |
| if (message_type === 'call' || message_type === 'contact') { |
| message_div.querySelector('.message_wrapper').appendChild(date_elt) |
| messages.insertBefore(message_div, messages.firstChild) |
| } else { |
| messages.insertBefore(date_elt, messages.firstChild) |
| messages.insertBefore(message_div, messages.firstChild) |
| } |
| } |
| } |
| |
| // Update informations if needed |
| if (message_type === 'data_transfer') |
| updateFileInteraction(message_div, message_object) |
| if (message_type === 'text' && message_direction === 'out') |
| // Modify sent status if necessary |
| updateTextInteraction(message_div, delivery_status) |
| if (message_type === 'call') |
| updateCallInteraction(message_div, message_object) |
| if (message_type === 'contact') |
| updateContactInteraction(message_div, message_object) |
| |
| // Clean timestamps |
| updateTimestamps(); |
| } |
| |
| |
| /** |
| * Add a message to the buffer |
| */ |
| function addMessage(message_object) |
| { |
| var atEnd = messages.scrollTop >= messages.scrollHeight - messages.clientHeight - 5; |
| addOrUpdateMessage(message_object, true); |
| if (atEnd) { |
| var startTime = Date.now(), |
| durTime = 250., |
| scrollStartHeight = messages.scrollHeight, |
| scrollStart = messages.scrollTop, |
| scrollDiff = scrollStartHeight - messages.clientHeight - scrollStart; |
| |
| function loop() { |
| var time = Date.now() - startTime, |
| scrollHeight = messages.scrollHeight, |
| diff = messages.scrollTop - scrollStart; // If user scrolls up (diff > 0). |
| |
| if (time >= durTime || scrollHeight != scrollStartHeight || diff < 0) { |
| if (diff >= 0) { // User scrolled up, don't autoscroll. |
| messages.scrollTop = scrollHeight; |
| } |
| return false; |
| } else { |
| messages.scrollTop = scrollStart + (scrollDiff * (time/durTime)); |
| raf(loop); |
| } |
| } |
| raf(loop); // Start the animation loop |
| } |
| } |
| |
| /** |
| * Show the history in reverse order and avoid the process to consumes a lot of ressources |
| */ |
| function printHistoryPart () { |
| if(historyBuffer.length == 0) { |
| // nothing to print |
| return; |
| } |
| |
| var previousScrollHeightMinusTop = messages.scrollHeight - messages.scrollTop; |
| // Show 10 messages |
| for (var i = 0; i < 10; ++i) { |
| addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false); |
| historyBufferIndex ++; |
| if (historyBufferIndex === historyBuffer.length) |
| break; |
| } |
| // Replace the scrollbar to the wanted position |
| messages.scrollTop = messages.scrollHeight - previousScrollHeightMinusTop; |
| // Make a short pause to minimizes ressources consumption |
| // show quickly the first hundred messages |
| if (historyBufferIndex !== historyBuffer.length) |
| setTimeout(printHistoryPart, (historyBufferIndex > 100) ? 100 : 1); |
| } |
| |
| /** |
| * Show the whole history in the chatview. |
| */ |
| function printHistory(messages_array) |
| { |
| historyBuffer = messages_array; |
| historyBufferIndex = 0; |
| setTimeout(printHistoryPart, 1); |
| } |
| |
| /** |
| * Updated a message that was previously added with addMessage |
| */ |
| function updateMessage(message_object) |
| { |
| addOrUpdateMessage(message_object, false); |
| } |
| |
| /** |
| * Sets the image for a given sender |
| * set_sender_image object should contain the following keys: |
| * - sender: the name of the sender |
| * - sender_image: base64 png encoding of the sender image |
| */ |
| function setSenderImage(set_sender_image_object) |
| { |
| var sender_contact_method = set_sender_image_object['sender_contact_method'], |
| sender_image = set_sender_image_object['sender_image'], |
| sender_image_id = "sender_image_" + sender_contact_method, |
| currentSenderImage = document.getElementById("#" + sender_image_id), // Remove the currently set sender image |
| style; |
| |
| if (currentSenderImage) { |
| currentSenderImage.parentNode.removeChild(currentSenderImage); |
| } |
| |
| // Create a new style element |
| style = document.createElement('style'); |
| |
| style.type = 'text/css'; |
| style.id = sender_image_id; |
| style.innerHTML = '.' + sender_image_id + " { \n content: url(data:image/png;base64," + sender_image + "); \n height: 35px; \n width: 35px; \n }"; |
| document.head.appendChild(style); |
| } |
| |
| /** |
| * Change the send icon |
| */ |
| function setSendIcon(source) |
| { |
| sendBtn.src = "data:image/png;base64," + source; |
| } |
| |
| function clearSenderImages() |
| { |
| var styles = document.head.querySelectorAll("style"), |
| i = styles.length; |
| |
| while (i--){ |
| document.head.removeChild(styles[i]); |
| } |
| } |
| |
| function setTemporary(temporary) |
| { |
| if (temporary) { |
| messageBarInput.placeholder = "Note: an interaction will create a new contact."; |
| } else { |
| messageBarInput.placeholder = "Type a message"; |
| } |
| } |
| |
| function sendFile() |
| { |
| window.prompt('SEND_FILE'); |
| } |
| |
| </script> |
| |
| </html> |