blob: 9f72d457a78168a671f5321c30b64f34588a05ff [file] [log] [blame]
<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 class="navbar-wrapper">
<div id="navbar">
<div id="backButton" class="nav-button non-action-button nav-left" onmouseover="addBackButtonHoverProperty()" onclick="backToWelcomeView()" title="Hide chat view">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<path d="M11.67 3.87L9.9 2.1 0 12l9.9 9.9 1.77-1.77L3.54 12z"/>
<path fill="none" d="M0 0h24v24H0z"/>
</svg>
</div>
<div id="nav-contactid" class="nav-left">
<div id="nav-contactid-alias"></div>
<div id="nav-contactid-bestId"></div>
</div>
<div style="display:none" class="deactivated nav-button action-button nav-right" onclick="moreOptions()" title="Options">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
</svg>
</div>
<div id="callButtons"> <!-- callButtons block allows more efficient hiding of placeCallButton and placeAudioCallButton -->
<div id="placeCallButton" class="nav-button action-button nav-right" onclick="placeCall()" title="Place video call">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
</svg>
</div>
<div id="placeAudioCallButton" class="nav-button action-button nav-right" onclick="placeAudioCall()" title="Place audio call">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M20.01 15.38c-1.23 0-2.42-.2-3.53-.56-.35-.12-.74-.03-1.01.24l-1.57 1.97c-2.83-1.35-5.48-3.9-6.89-6.83l1.95-1.66c.27-.28.35-.67.24-1.02-.37-1.11-.56-2.3-.56-3.53 0-.54-.45-.99-.99-.99H4.19C3.65 3 3 3.24 3 3.99 3 13.28 10.73 21 20.01 21c.71 0 .99-.63.99-1.18v-3.45c0-.54-.45-.99-.99-.99z"/>
</svg>
</div>
</div>
<div id="addToConversationsButton" class="nav-button action-button nav-right" onclick="addToConversations()" title="Add to conversations">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div>
<div id="addBannedContactButton" class="nav-button action-critical-button nav-right" onclick="addBannedContact()" title="Unban banned contact">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z"/>
<circle cx="15" cy="8" r="4"/>
<path d="M23 20v-2c0-2.3-4.1-3.7-6.9-3.9l6 5.9h.9zm-11.6-5.5C9.2 15.1 7 16.3 7 18v2h9.9l4 4 1.3-1.3-21-20.9L0 3.1l4 4V10H1v2h3v3h2v-3h2.9l2.5 2.5zM6 10v-.9l.9.9H6z"/>
</svg>
</div>
</div>
<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>
<div id="container">
<!-- messages are dynamically inserted here -->
<div id="sendMessage">
<div class="nav-button action-button" onclick="sendFile()" title="Send File">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<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>
</div>
<textarea id="message" autofocus placeholder="Type a message" onkeyup="grow_text_area()" onkeydown="process_messagebar_keydown()" rows="1" disabled="false"></textarea>
<div class="nav-button action-button" onclick="sendMessage()" title="Send">
<svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
</div>
</div>
</div>
</body>
<script>
"use strict"
/* Constants used at several places*/
const messageBarPlaceHolder = "Type a message"
const avatar_size = 35
// scrollDetectionThresh represents the number of pixels a user can scroll
// without disabling the automatic go-back-to-bottom when a new message is
// received
const scrollDetectionThresh = 200
/* Buffers */
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.
/* 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 backButton = document.getElementById("backButton")
const aliasField = document.getElementById("nav-contactid-alias")
const bestIdField = document.getElementById("nav-contactid-bestId")
const idField = document.getElementById("nav-contactid")
const messageBar = document.getElementById("sendMessage")
const messageBarInput = document.getElementById("message")
const container = document.getElementById("container")
const addToConvButton = document.getElementById("addToConversationsButton")
const invitation = document.getElementById("invitation")
const navbar = document.getElementById("navbar")
const invitationText = document.getElementById("text")
/* States: allows us to avoid re-doing something if it isn't meaningful */
var displayLinksEnabled = false
var hoverBackButtonAllowed = true
var hasInvitation = false
var isTemporary = false
var isBanned = false
var isInitialLoading = false
/**
* Generic wrapper. Execute passed function keeping scroll position identical.
*
* @param func function to execute
* @param args parameters as array
*/
function exec_keeping_scroll_position(func, args) {
const messages = document.getElementById("messages")
if (messages) {
var atEnd = messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh
func(...args)
if (atEnd) {
messages.scrollTop = messages.scrollHeight
}
} else {
// should not happen
func(...args)
}
}
/**
* Reset scrollbar at a given position.
* @param scroll position at which the scrollbar should be set.
* Here position means the number of pixels scrolled,
* i.e. scroll = 0 resets the scrollbar at the bottom.
*/
function back_to_scroll(scroll) {
const messages = document.getElementById("messages")
if (messages) {
messages.scrollTop = messages.scrollHeight - scroll
}
}
/**
* Reset scrollbar at bottom.
*/
function back_to_bottom() {
back_to_scroll(0)
}
/**
* Update common frame between conversations.
*
* Whenever the current conversation is switched, information from the navbar
* and message bar have to be updated to match new contact. This function
* handles most of the required work (except the showing/hiding the invitation,
* which is handled by showInvitation()).
*
* @param banned whether contact is banned or not
* @param temporary whether contact is temporary or not
* @param alias
* @param bestId
*/
/* exported update_chatview_frame */
function update_chatview_frame(banned, temporary, alias, bestid) {
navbar.style.display = "none"
hoverBackButtonAllowed = true
aliasField.innerHTML = (alias ? alias : bestid)
if(alias) {
bestIdField.innerHTML = bestid
idField.classList.remove("oneEntry")
} else {
idField.classList.add("oneEntry")
}
if (isBanned !== banned) {
isBanned = banned
hideMessageBar(banned)
if(banned) {
// contact is banned. update navbar and states
navbar.classList.add("onBannedState")
} else {
navbar.classList.remove("onBannedState")
}
} else if (isTemporary !== temporary) {
isTemporary = temporary
if (temporary) {
addToConvButton.style.display = "flex"
messageBarInput.placeholder = "Note: an interaction will create a new contact."
} else {
addToConvButton.style.display = ""
messageBarInput.placeholder = messageBarPlaceHolder
}
}
navbar.style.display = ""
}
/**
* Hide or show invitation.
*
* Invitation is hidden if no contactAlias/invalid alias is passed.
* Otherwise, invitation div is updated.
*
* @param contactAlias
*/
/* exported showInvitation */
function showInvitation(contactAlias) {
if (!contactAlias) {
if (hasInvitation) {
hasInvitation = false
invitation.style.visibility = ""
}
} else {
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 or show navbar, and update body top padding accordingly.
*
* @param isVisible whether navbar should be displayed or not
*/
/* exported displayNavbar */
function displayNavbar(isVisible)
{
if (isVisible) {
navbar.classList.remove("hiddenState")
document.documentElement.style.setProperty("--navbar-size", undefined)
} else {
navbar.classList.add("hiddenState")
document.documentElement.style.setProperty("--navbar-size", "0")
}
}
/**
* Hide or show message bar, and update body bottom padding accordingly.
*
* @param isHidden whether message bar should be displayed or not
*/
/* exported hideMessageBar */
function hideMessageBar(isHidden) {
if (isHidden) {
messageBar.classList.add("hiddenState")
document.body.style.setProperty("--messagebar-size", "0")
} else {
messageBar.classList.remove("hiddenState")
document.body.style.removeProperty("--messagebar-size")
}
}
/* exported setDisplayLinks */
function setDisplayLinks(display) {
displayLinksEnabled = display
}
/**
* This event handler dynamically resizes the message bar depending on the amount of
* text entered, while adjusting the body paddings so that that the message bar doesn't
* overlap messages when it grows.
*/
/* exported grow_text_area */
function grow_text_area() {
exec_keeping_scroll_position(function(){
var old_height = window.getComputedStyle(messageBar).height
messageBarInput.style.height = "auto" /* <-- necessary, no clue why */
messageBarInput.style.height = messageBarInput.scrollHeight + "px"
var new_height = window.getComputedStyle(messageBar).height
var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size")
var total_size = parseInt(msgbar_size) + parseInt(new_height) - parseInt(old_height)
document.body.style.setProperty("--messagebar-size", total_size.toString() + "px")
}, [])
}
/**
* This event handler processes keydown events from the message bar. When pressed key is
* the enter key, send the message unless shift or control was pressed too.
*
* @param key the pressed key
*/
/* exported process_messagebar_keydown */
function process_messagebar_keydown(key) {
key = key || event
var map = {}
map[key.keyCode] = key.type == "keydown"
if (key.ctrlKey || key.shiftKey) {
return true
}
if (map[13]) {
sendMessage()
key.preventDefault()
}
return true
}
/**
* Disable or enable textarea.
*
* @param isDisabled whether message bar should be enabled or disabled
*/
/* exported disableSendMessage */
function disableSendMessage(isDisabled)
{
messageBarInput.disabled = isDisabled
}
/**
* This event handler adds the hover property back to the "back to welcome view"
* button.
*
* This is a hack. It needs some explanations.
*
* Problem: Whenever the "back to welcome view" button is clicked, the webview
* freezes and the GTK ring welcome view is displayed. While the freeze
* itself is perfectly fine (probably necessary for good performances), this
* is a big problem for us when the user opens a chatview again: Since the
* chatview was freezed, the back button has «remembered» the hover state and
* still displays the blue background for a small instant. This is a very bad
* looking artefact.
*
* In order to counter this problem, we introduced the following evil mechanism:
* Whenever a user clicks on the "back to welcome view" button, the hover
* property is disabled. The hover property stays disabled until the user calls
* this event handler by hover-ing the button.
*/
/* exported addBackButtonHoverProperty */
function addBackButtonHoverProperty()
{
if(hoverBackButtonAllowed) {
backButton.classList.add("non-action-button")
}
}
/*
* Update timestamps messages.
*/
function updateView() {
const messages = document.getElementById("messages")
if (messages)
updateTimestamps(messages)
}
setInterval(updateView, 60000)
/*
* FIXME updateTimestamps has to be called when the window is resized because
* updateTimestamps also handles timestamp repositioning (responsive view). This
* task should be done in a separate function.
*/
window.onresize = function() {
updateView()
}
/* exported addBannedContact */
function addBannedContact()
{
window.prompt("UNBLOCK")
}
/* exported addToConversations */
function addToConversations()
{
window.prompt("ADD_TO_CONVERSATIONS")
}
/* exported placeCall */
function placeCall()
{
window.prompt("PLACE_CALL")
}
/* exported placeAudioCall */
function placeAudioCall()
{
window.prompt("PLACE_AUDIO_CALL")
}
/* exported backToWelcomeView */
function backToWelcomeView()
{
backButton.classList.remove("non-action-button")
hoverBackButtonAllowed = false
window.prompt("CLOSE_CHATVIEW")
}
/**
* Transform a date to a string group like "1 hour ago".
*
* @param date
*/
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 content of message bar
*/
function sendMessage()
{
var message = messageBarInput.value
if (message.length > 0) {
messageBarInput.value = ""
window.prompt("SEND:" + message)
}
}
/* exported acceptInvitation */
function acceptInvitation()
{
window.prompt("ACCEPT")
}
/* exported refuseInvitation */
function refuseInvitation()
{
window.prompt("REFUSE")
}
/* exported blockConversation */
function blockConversation()
{
window.prompt("BLOCK")
}
/* exported sendFile */
function sendFile()
{
window.prompt("SEND_FILE")
}
/**
* Clear all messages.
*/
/* exported clearMessages */
function clearMessages()
{
const messages = document.getElementById("messages")
if (messages) {
messages.parentNode.removeChild(messages)
}
}
/**
* Convert 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 URL.
* @param url
*/
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.
* @param message_text
*/
function getMessageHtml(message_text)
{
const escaped_message = escapeHtml(message_text)
var linkified_message = linkifyHtml(escaped_message, {}) // eslint-disable-line no-undef
const textPart = document.createElement("pre")
textPart.innerHTML = linkified_message
return textPart.outerHTML
}
/**
* Returns the message status, formatted for display
* @param message_delivery_status
*/
/* exported getMessageDeliveryStatusText */
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:
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. For instance, several "1 hour ago"
* timestamps will be merged into a single one.
*
* @param baseNode
* @param endIndex
* @param message_div
*/
function cleanPreviousTimestamps (baseNode, endIndex, messages_div) {
// Remove previous elements with the same formatted timestamp
for (var c = endIndex - 1 ; c >= 0 ; --c) {
const child = messages_div.children[c]
if (child.className.indexOf("timestamp") !== -1) {
if (child.className === baseNode.className &&
child.innerHTML === baseNode.innerHTML) {
messages_div.removeChild(child)
} else {
break // A different timestamp is met, we can stop here.
}
}
}
}
/**
* Update timestamps.
* @param message_div
*/
function updateTimestamps(messages_div) {
const timestamps = messages_div.querySelector(".timestamp")
const is_image_out = messages_div.querySelector(".message_out .sender_image")
const is_image_in = messages_div.querySelector(".message_in .sender_image")
if (timestamps) {
for ( var c = 0 ; c < messages_div.children.length ; ++c) {
const child = messages_div.children[c]
if (child.className.indexOf("timestamp") !== -1) {
// Update timestamp
child.innerHTML = getMessageTimestampText(child.getAttribute("message_timestamp"), true)
// FIXME this is EVIL HACK. Responsive behaviour should be handled by CSS, not JS
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, messages_div)
}
}
}
}
/**
* 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.
*
* @param progress_bar
* @param message_object
*/
function updateProgressBar(progress_bar, message_object) {
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
* @param
*/
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")
/* A file interaction contains buttons at the left for status information
or accept/refuse actions. The text is bold and clickable. */
var left_buttons = document.createElement("div")
left_buttons.setAttribute("class", "left_buttons")
message_wrapper.appendChild(left_buttons)
var text_div = document.createElement("div")
text_div.setAttribute("class", "text")
text_div.addEventListener("click", function () {
// 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
var message_transfer_progress_bar = document.createElement("span")
message_transfer_progress_bar.setAttribute("class", "message_progress_bar")
var 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
}
/**
* Build information text for passed file interaction message object
*
* @param message_object message object containing file interaction info
*/
function buildFileInformationText(message_object) {
var informations_txt = getMessageTimestampText(message_object["timestamp"], true)
if (message_object["totalSize"] && message_object["progress"]) {
if (message_object["delivery_status"] === "finished") {
informations_txt += " - " + humanFileSize(message_object["totalSize"])
} else {
informations_txt += " - " + humanFileSize(message_object["progress"])
+ " / " + humanFileSize(message_object["totalSize"])
}
}
return informations_txt + " - " + message_object["delivery_status"]
}
/**
* Update a file interaction (icons + filename + status + progress bar)
*
* @param message_div the message to update
* @param message_object new informations
* @param forceTypeToFile
*/
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")) {
var 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)
var wrapper = message_div.querySelector(".message_wrapper")
if (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")
informations_div.innerText = buildFileInformationText(message_object)
// 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()
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))
}
/**
* Build a container for passed video thumbnail
* @param linkElt video thumbnail div
*/
function buildVideoContainer(linkElt) {
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)
return containerElt
}
/**
* 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
* @param noerror
*/
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; border: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'")
imageElt.src = ytid ? `http://img.youtube.com/vi/${ytid}/0.jpg` : link
if (isInitialLoading || messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) {
/* Hack to keep the scrollbar at the bottom.
Images are loaded asynchronously and the scrollbar position is
changed each time an image is loaded and displayed. In order to
make sure the scrollbar stays at the bottom, reset scrollbar
position each time an image was loaded. */
imageElt.onload = back_to_bottom
}
linkElt.appendChild(imageElt)
if (ytid) {
media_wrapper.appendChild(buildVideoContainer(linkElt))
} 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
*/
function updateTextInteraction(message_div, delivery_status) {
if (!message_div.querySelector(".message_text")) return // media
var sending = message_div.querySelector(".sending")
switch(delivery_status)
{
case "ongoing":
case "sending":
if (!sending) {
sending = document.createElement("div")
sending.setAttribute("class", "sending")
sending.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.insertBefore(sending, message_div.querySelector(".menu_interaction"))
}
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.insertBefore(failure_div, message_div.querySelector(".menu_interaction"))
}
if (sending) sending.style.display = "none"
break
case "sent":
case "finished":
case "unknown":
case "read":
// change text color to black
message_div.querySelector(".message_text").style = "color: #000"
if (sending) sending.style.display = "none"
break
default:
break
}
}
/**
* Build a new interaction (call or contact)
*/
function actionInteraction() {
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 = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\
<path d=\"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z\"/>\
<path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>"
status_button.setAttribute("class", "flat-button")
left_buttons.appendChild(status_button)
}
/**
* Remove an interaction from the conversation
* @param interaction_id
*/
/* exported removeInteraction */
function removeInteraction(interaction_id) {
const messages = document.getElementById("messages")
var interaction = document.querySelector(`#message_${interaction_id}`)
var child = interaction
var i = 0
while( (child = child.previousSibling) != null )
i++
if (interaction) interaction.parentNode.removeChild(interaction)
if (i < messages.children.length) {
var timestampAfter = messages.children[i].classList.contains("timestamp")
var timestampBefore = messages.children[i].classList.contains("timestamp")
if (timestampAfter && timestampBefore) {
messages.children[i].parentNode.removeChild(messages.children[i])
}
}
}
/**
* Build a message div for passed message object
* @param message_object to treat
*/
function buildNewMessage(message_object) {
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 delivery_status = message_object["delivery_status"]
const message_sender_contact_method = message_object["sender_contact_method"]
var classes = [
"message",
`message_${message_direction}`,
`message_type_${message_type}`
]
var type = ""
var 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
/* FIXME in case of error during image load, we should display the standard file
transfer box. This feature is broken and should be repared. */
/* message_div.querySelector("img").onerror = function() {
message_div = .getElementById("message_" + this.id)
if (message_div.querySelector(".message_wrapper")) {
var 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())
} else {
const temp = document.createElement("div")
temp.innerText = message_type
message_div.appendChild(temp)
}
const menu_element = document.createElement("div")
menu_element.setAttribute("class", "menu_interaction")
menu_element.innerHTML = "<label for=\"showmenu\">\
<svg fill=\"#888888\" height=\"12\" viewBox=\"0 0 24 24\" width=\"12\" xmlns=\"http://www.w3.org/2000/svg\">\
<path d=\"M0 0h24v24H0z\" fill=\"none\"/>\
<path d=\"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\"/>\
</svg>\
</label>\
<input type=\"checkbox\" id=\"showmenu\" hidden />"
menu_element.onclick = function() {
const button = this.querySelector("#showmenu")
button.checked = !button.checked
}
menu_element.onmouseleave = function() {
const button = this.querySelector("#showmenu")
button.checked = false
}
const dropdown = document.createElement("div")
const dropdown_classes = [
"dropdown",
`dropdown_${message_id}`
]
dropdown.setAttribute("class", dropdown_classes.join(" "))
const remove = document.createElement("div")
remove.setAttribute("class", "delete")
remove.innerHTML = "Delete"
remove.msg_id = message_id
remove.onclick = function() {
window.prompt(`DELETE_INTERACTION:${this.msg_id}`)
}
dropdown.appendChild(remove)
menu_element.appendChild(dropdown)
if (message_type !== "call") {
message_div.appendChild(menu_element)
} else {
var wrapper = message_div.querySelector(".message_wrapper")
wrapper.insertBefore(menu_element, wrapper.firstChild)
}
return message_div
}
/**
* Build a timestamp for passed message object
* @param message_object to treat
*/
function buildNewTimestamp(message_object) {
const message_type = message_object["type"]
const message_direction = message_object["direction"]
const message_timestamp = message_object["timestamp"]
const formattedTimestamp = getMessageTimestampText(message_timestamp, true)
const date_elt = document.createElement("div")
date_elt.innerText = formattedTimestamp
var typeIsCallOrContact = (message_type === "call" || message_type === "contact")
var timestamp_div_classes = ["timestamp", typeIsCallOrContact ? "timestamp_action" : `timestamp_${message_direction}`]
date_elt.setAttribute("class", timestamp_div_classes.join(" "))
date_elt.setAttribute("message_timestamp", message_timestamp)
return date_elt
}
/**
* 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
* @param messages_div
*/
function addOrUpdateMessage(message_object, new_message, insert_after = true, messages_div) {
const message_id = message_object["id"]
const message_type = message_object["type"]
const message_direction = message_object["direction"]
const delivery_status = message_object["delivery_status"]
var message_div = messages_div.querySelector("#message_" + message_id)
if (new_message) {
message_div = buildNewMessage(message_object)
var date_elt = buildNewTimestamp(message_object)
// Remove last timestamp if it's the same
if (messages_div.querySelector(".timestamp"))
cleanPreviousTimestamps(date_elt, messages_div.children.length, messages_div)
// Add message and the new timestamp
if (message_type === "call" || message_type === "contact") {
message_div.querySelector(".message_wrapper").appendChild(date_elt)
}
if (insert_after) {
messages_div.lastChild.classList.remove("last-message")
messages_div.appendChild(message_div)
if (! (message_type === "call" || message_type === "contact")) {
messages_div.appendChild(date_elt)
}
messages_div.lastChild.classList.add("last-message")
} else {
if (! (message_type === "call" || message_type === "contact")) {
messages_div.prepend(date_elt)
}
messages_div.prepend(message_div)
}
}
if (isErrorStatus(delivery_status) && message_direction === "out") {
const dropdown = messages_div.querySelector(`.dropdown_${message_id}`)
const retry = document.createElement("div")
retry.innerHTML = "Retry"
retry.msg_id = message_id
retry.onclick = function() {
window.prompt(`RETRY_INTERACTION:${this.msg_id}`)
}
dropdown.insertBefore(retry, message_div.querySelector(".delete"))
}
// 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(messages_div)
}
/**
* Wrapper for addOrUpdateMessage.
*
* Add or update a message and make sure the scrollbar position
* is refreshed correctly
*
* @param message_object message to be added
*/
/* exported addMessage */
function addMessage(message_object)
{
const messages = document.getElementById("messages")
if (messages)
exec_keeping_scroll_position(addOrUpdateMessage, [message_object, true, undefined, messages])
}
/**
* Update a message that was previously added with addMessage and
* make sure the scrollbar position is refreshed correctly
*
* @param message_object message to be updated
*/
/* exported updateMessage */
function updateMessage(message_object)
{
const messages = document.getElementById("messages")
if (messages)
exec_keeping_scroll_position(addOrUpdateMessage, [message_object, false, undefined, messages])
}
/**
* Display history in reverse order
* This function operates on passed root div.
*
* @param messages_div root div to modify
* @param setMessages if true and #messages doesn't exist, #messages is set to the resulting messages div
*/
function printHistoryPart(messages_div, setMessages = false)
{
isInitialLoading = true
for (; historyBufferIndex < historyBuffer.length; ++historyBufferIndex) {
addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false, messages_div)
}
messages_div.lastChild.classList.add("last-message")
const messages = document.getElementById("messages")
if (!messages && setMessages) {
container.prepend(messages_div)
}
isInitialLoading = false
}
/**
* Show the whole history in the chatview.
* @param messages_array array containing history to be printed
*/
/* exported printHistory */
function printHistory(messages_array)
{
historyBuffer = messages_array
historyBufferIndex = 0
var messages_div = document.createElement("div")
messages_div.setAttribute("id", "messages")
printHistoryPart(messages_div, true)
setTimeout(back_to_bottom, 0)
}
/**
* 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
*
* @param set_sender_image_object sender image object as previously described
*/
/* exported setSenderImage */
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 + " {content: url(data:image/png;base64," + sender_image + ");height: 35px;width: 35px;}"
document.head.appendChild(style)
}
/* exported clearSenderImages */
function clearSenderImages()
{
var styles = document.head.querySelectorAll("style"),
i = styles.length
while (i--){
document.head.removeChild(styles[i])
}
}
</script>
</html>