let { punycode } = await import(`./punycode.js?r=${revision}`); export class ui { constructor(parent) { this.parent = parent; this.inThePast = false; this.browser = this.getBrowser(); this.root = document.querySelector(':root'); this.css = getComputedStyle($("html")[0]); this.version = $("body").data("version"); this.updateAvailable = false; this.emojiCategories = { "Search": [], "People": ["Smileys & Emotion", "People & Body"], "Nature": ["Animals & Nature"], "Food": ["Food & Drink"], "Activities": ["Activities"], "Travel": ["Travel & Places"], "Objects": ["Objects"], "Symbols": ["Symbols"], "Flags": ["Flags"] } if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { $("body").addClass("desktop"); } this.preloadIcons(); } async gotoMessage(message) { let messageExists = $(`.messageRow[data-id=${message}]`).length; if (messageExists) { this.scrollToMessage(message); } else { let msg = await this.parent.getMessage(message); if (msg.message) { this.setInThePast(true); this.clear("messages"); this.messagesLoading(true); let options = { at: message } this.parent.getMessages(options); } else { return; } } setTimeout(() => { this.highlightMessage(message); }, 250); } setInThePast(bool) { if (bool) { this.inThePast = true; $("#jumpToPresent").removeClass("hidden"); } else { this.inThePast = false; $("#jumpToPresent").addClass("hidden"); } } backInPresent(body) { let lastMessage = $("#messages > .messageRow[data-id]").last(); let lastMessageID = lastMessage.data("id"); if (body.after && lastMessageID == body.latestMessage) { this.setInThePast(false); } } fetchNewMessages() { let lastMessage = $("#messages > .messageRow[data-id]").last(); let lastMessageID = lastMessage.data("id"); let lastMessageTime = lastMessage.data("time"); if (lastMessageID) { let options = { after: lastMessageID } this.parent.getMessages(options); } } getVariables() { const variables = Array.from(document.styleSheets) .filter(styleSheet => { try { return styleSheet.cssRules; } catch(e) { console.warn(e); } }) .map(styleSheet => Array.from(styleSheet.cssRules)) .flat() .filter(cssRule => cssRule.selectorText === ':root') .map(cssRule => cssRule.cssText.split('{')[1].split('}')[0].trim().split(';')) .flat() .filter(text => text !== "") .map(text => text.split(':')) .map(parts => ({key: parts[0].trim(), value: parts[1].trim() })) ; return variables; } preloadIcons() { let variables = this.getVariables(); $.each(variables, (k, data) => { let {key,value} = data; if (key.substring(key.length - 4) == "Icon") { let match = value.match(/url\((?<asset>.+)\)/); let asset = match.groups.asset.replaceAll("\\", ""); let link = `https://${this.parent.host}${asset}`; new Image().src = link; } }); } setData(e, attr, val) { $(e).data(attr, val); $(e).attr(`data-${attr}`, val); } setupSync() { let link = this.parent.syncLink(); let popover = $(".popover[data-name=syncSession]"); let qr = popover.find("#qrcode"); let input = popover.find("input[name=syncLink]"); qr.empty(); qr.html('<div id="qrlogo"><img draggable="false" src="/assets/img/handshake"></div>'); input.val(link); if (!qr.find("canvas").length) { let qrcode = new QRCode(qr[0], { text: link, width: 250, height: 250, colorLight: this.css.getPropertyValue("--tertiaryBackground"), colorDark: this.css.getPropertyValue("--primaryForeground"), correctLevel: QRCode.CorrectLevel.L }); } qr.find("img").attr("draggable", "false"); } copyToClipboard(button) { let field = button.parent().find("input")[0]; field.select(); field.setSelectionRange(0, 99999); navigator.clipboard.writeText(field.value); field.setSelectionRange(0, 0); button.addClass("copied"); setTimeout(() => { button.removeClass("copied"); }, 1000); } sizeInput() { let input = $("textarea#message"); input.css("height", "1px"); let height = input[0].scrollHeight; if (height > 200) { height = 200; } input.css("height", height+"px"); } wordForPosition(text, position) { let index = text.indexOf(position); let preText = text.substr(0, position); if (preText.indexOf(" ") > 0) { let words = preText.split(" "); return (words.length - 1) } else { return 0; } } updateCompletions() { let options = []; let closeIfNeeded = false; let input = $("textarea#message"); let text = input.val(); let words = text.split(" "); let position = input[0].selectionStart; let word = words[this.wordForPosition(text, position)]; if (word.length > 1) { $("#completions .body .list").empty(); switch (word[0]) { case "@": $("#completions .title").html("Users"); let users = this.parent.usersForConversation(this.parent.conversation); options = users.filter(user => { let match = Array.from(this.toUnicode(user.domain)).slice(0, Array.from(word).length - 1).join("").toLowerCase(); let search = word.toLowerCase(); return !user.locked && "@"+match === search; }).slice(0, 10); $.each(options, (k, option) => { let row = $(`#users .users tr[data-id="${option.id}"]`).clone(); $("#completions .body .list").append(row); }); closeIfNeeded = true; break; case "#": $("#completions .title").html("Channels"); let channels = this.parent.channels; options = channels.filter(channel => { let match = Array.from(this.toUnicode(channel.name)).slice(0, Array.from(word).length - 1).join("").toLowerCase(); let search = word.toLowerCase(); return "#"+match === search; }).slice(0, 10); $.each(options, (k, option) => { let row = $(`#conversations .channels tr[data-id="${option.id}"]`).clone(); $("#completions .body .list").append(row); }); closeIfNeeded = true; break; } } else { closeIfNeeded = true; } if (options.length) { $("#completions .list tr").first().addClass("active"); this.popover("completions"); } else { closeIfNeeded = true; } if (closeIfNeeded) { this.close(); } } updateSelectedCompletion(key) { if (!$("#completions.shown").length) { return; } let selected = $("#completions tr.active"); selected.removeClass("active"); let select; let current = selected[0]; switch (key) { case 38: if (current.previousSibling) { select = current.previousSibling; } else { select = $("#completions .list").children().last(); } break; case 40: if (current.nextSibling) { select = current.nextSibling; } else { select = $("#completions .list").children().first(); } break; } $(select).addClass("active"); } domains(array) { let domains = this.parent.sorted(array, "domain"); switch (this.parent.page) { case "chat": $(".header .domains select").empty(); $.each(domains, (k, i) => { let verify = ""; if (i.locked) { verify = " (Unverified)"; } $(".header .domains select").append(`<option value="${i.id}">${this.toUnicode(i.domain)}/${verify}</option>`); }); $(".header .domains select").append(`<optgroup label="-----------------"></optgroup>`); $(".header .domains select").append(`<option value="manageDomains">Manage Domains</option>`); $(".header .domains select").val(this.parent.domain); break; case "id": case "invite": if (!this.parent.varoLoaded() || this.parent.mobile) { $(".varo").addClass("hidden"); } $(".section#manageDomains .domains").empty(); $.each(domains, (k, i) => { let html = $(` <div class="domain" data-id="${i.id}" data-name="${i.domain}"> <div>${this.toUnicode(i.domain)}</div> <div class="actions"> <div class="icon action delete" data-action="deleteDomain"></div> </div> </div> `); if (i.locked) { html.find(".actions").prepend(`<div class="action link" data-action="verifyDomain">Verify</div>`); } $(".section#manageDomains .domains").append(html); }); if (domains.length) { $(".section#manageDomains #startChatting").removeClass("hidden"); } if (this.parent.page !== "invite") { this.showSection("manageDomains"); } break; } } removeDomain(id) { $(`.section#manageDomains .domains .domain[data-id=${id}]`).remove(); } stakedDomains(domains) { $(".section#addDomain select[name=tld]").empty(); if (this.parent.page == "invite") { $(".section[id=addDomain]").find(".button[data-action=addDomain]").addClass("hidden"); $(".section[id=addDomain]").find(".or").addClass("hidden"); let info = this.parent.stakedForName(this.parent.data); if (info) { $(".section#addDomain select[name=tld]").append(`<option value="${info.name}">${this.toUnicode(info.name)}</option>`); } else { $(".section#addDomain").empty(); $(".section#addDomain").append(`<div class="error response">This invite code isn't valid.</div>`); } this.showSection("addDomain"); } else { $.each(domains, (k, i) => { $(".section#addDomain select[name=tld]").append(`<option value="${i.name}">${this.toUnicode(i.name)}</option>`); }); } } showSection(section) { $(".section").find("input").val(''); $(".section").removeClass("shown"); $(`.section#${section}`).addClass("shown"); } updateDomainSelect() { $(".header .domains select").val(this.parent.domain); } avatarFallback(string) { let fallback; if (!this.parent.regex(/[a-zA-Z0-9]/g, string[0]).length) { $.each(sortedEmojis, (k, e) => { if (string.substring(0, e.emoji.length) == e.emoji) { fallback = e.emoji; return false; } }); } if (!fallback) { fallback = String.fromCodePoint(string.codePointAt(0)).toUpperCase(); } return fallback; } toUnicode(name) { let puny = punycode.ToUnicode(name); let zwj = nameToUnicode(puny); return zwj; } conversation(tab, data) { let name = data.name; let fallback = "#"; let user,domain; if (tab == "pms") { user = this.parent.otherUser(data.users); domain = this.parent.userForID(user); name = domain.domain; } name = this.toUnicode(name); if (tab == "pms") { fallback = this.avatarFallback(name); } let html = $(` <tr data-id="${data.id}" data-type="${tab}"> <td class="avatar"> <div class="locked"> <div class="icon lock" title="Locked"></div> </div> <div class="fallback">${fallback.toUpperCase()}</div> </td> <td class="title">${name}</td> </tr> `); if (tab == "pms") { let active = ""; if (domain.active) { active = " active"; } let avatar = $(` <div class="status${active}"></div> <div class="favicon" data-id="${user}" data-domain="${name}"></div> `); html.find(".avatar").prepend(avatar); } $(`#conversations .${tab} table`).append(html); this.updateAvatars(); } updateConversations() { let domain = this.parent.userForID(this.parent.domain); $.each($("#conversations .sections tr"), (k, c) => { let id = $(c).data("id"); let data; if (this.parent.isChannel(id)) { data = this.parent.channelForID(id); if (!data.public && data.name !== domain.tld) { $(c).addClass("locked"); } } else { data = this.parent.pmForID(id); if (data.activity) { $(c).removeClass("hidden"); } else { $(c).addClass("hidden"); } let otherUser = this.parent.otherUser(data.users); let otherUserData = this.parent.userForID(otherUser); if (otherUserData.active) { $(c).find(".avatar .status").addClass("active"); } else { $(c).find(".avatar .status").removeClass("active"); } if (otherUserData.locked || otherUserData.deleted) { $(c).addClass("locked"); } else if ($(c).hasClass("locked")) { $(c).removeClass("locked"); if (this.parent.conversation == id) { this.parent.changeConversation(id); } } } if (data.activity >= this.parent.firstLaunch) { if (data.id !== this.parent.conversation) { this.markUnread(data.id, true); } } if (this.parent.seen[data.id]) { if (this.parent.seen[data.id] >= data.activity) { this.markUnread(data.id, false); } } }); } conversationStatus() { let domain = this.parent.userForID(this.parent.domain); if (domain.locked) { return "verify"; } else if ($(`#conversations tr[data-id=${this.parent.conversation}]`).hasClass("locked")) { if (this.parent.isChannel(this.parent.conversation)) { return "permissions"; } else { let data = this.parent.pmForID(this.parent.conversation); let otherUser = this.parent.otherUser(data.users); let otherUserData = this.parent.userForID(otherUser); if (otherUserData.locked || otherUserData.deleted) { return "user"; } } } return true; } updateInputBar() { $("body").removeClass("unverified"); let status = this.conversationStatus(); switch (status) { case "user": case "verify": case "permissions": $(".inputHolder").addClass("locked"); break; default: $(".inputHolder").removeClass("locked"); break; } switch (status) { case "user": $(".inputHolder .locked").html("This user isn't available."); break; case "verify": $("body").addClass("unverified"); $(".inputHolder .locked").html("Re-verify your name to chat."); break; case "permissions": $(".inputHolder .locked").html("You don't have permissions to chat here."); break; } } setConversationTab() { $("#conversations .tabs .tab").removeClass("active"); $(`#conversations .tabs .tab[data-tab=${this.parent.tab}]`).addClass("active"); $(`#conversations .sections .section`).removeClass("shown"); $(`#conversations .sections .section.${this.parent.tab}`).addClass("shown"); } setActiveConversation() { this.setData($("#holder"), "type", this.parent.tab); $("#conversations tr").removeClass("active"); $(`#conversations tr[data-id=${this.parent.conversation}]`).addClass("active"); $(".messageHeader table").empty(); $(".messageHeader table").append($(`#conversations tr.active`).clone()); $(".messageHeader table tr").removeClass("hidden"); this.setPinnedMessage(); this.updateAvatars(); } async setPinnedMessage() { $(".pinnedMessage .delete").addClass("hidden"); $(".pinnedMessage").removeClass("shown"); let output = new Promise(resolve => { if (!this.parent.isChannel(this.parent.conversation)) { resolve(); } let channelData = this.parent.channelForID(this.parent.conversation); if (channelData.pinned) { this.parent.getMessage(channelData.pinned).then(r => { if (r.message) { let message; let decoded = he.decode(he.decode(r.message)); try { let msg = JSON.parse(decoded); message = msg.message; } catch { message = decoded; } resolve(message); } else { resolve(); } }); } else { resolve(); } }); let message = await output; if (message && message.length) { $(".pinnedMessage .message").html(message); if (this.parent.isChannel(this.parent.conversation)) { let me = this.parent.userForID(this.parent.domain); let channel = this.parent.channelForID(this.parent.conversation); if (this.isAdmin(channel, me)) { $(".pinnedMessage .delete").removeClass("hidden"); } } this.linkify($(".pinnedMessage .message")); $(".pinnedMessage").addClass("shown"); } } isAdmin(channel, user) { if ((channel.tldadmin && channel.name == user.domain) || user.admin || channel.admins.includes(user.id)) { return true; } return false; } markUnread(conversation, bool) { if (bool) { if (!$(`#conversations tr[data-id=${conversation}]`).hasClass("locked") && conversation !== this.parent.conversation) { $(`#conversations tr[data-id=${conversation}]`).addClass("unread"); } } else { $(`#conversations tr[data-id=${conversation}]`).removeClass("unread"); } this.updateNotifications(); } markMention(conversation, bool) { if (bool) { if (!$(`#conversations tr[data-id=${conversation}]`).hasClass("locked") && conversation !== this.parent.conversation) { $(`#conversations tr[data-id=${conversation}]`).addClass("mentions"); } } else { $(`#conversations tr[data-id=${conversation}]`).removeClass("mentions"); } this.updateNotifications(); } updateNotifications() { if ($("#conversations .pms tr.unread").length) { $("#conversations .tab[data-tab=pms]").addClass("notification"); } else { $("#conversations .tab[data-tab=pms]").removeClass("notification"); } if ($("#conversations .channels tr.mentions").length) { $("#conversations .tab[data-tab=channels]").addClass("notification"); } else { $("#conversations .tab[data-tab=channels]").removeClass("notification"); } if ($("#conversations .tab.notification").length) { $(".header .left").addClass("notification"); } else { $(".header .left").removeClass("notification"); } } setLoading(bool) { if (bool) { $(".connecting").removeClass("hidden"); } else { $(".connecting").addClass("hidden"); } } clear(type) { switch (type) { case "channels": case "pms": $(`#conversations .${type} table`).empty(); break; case "messages": $("#messages").empty(); $(".needSLD").remove(); break; case "input": $("textarea#message").val(''); break; } } setGatedView(data) { let channel = this.parent.channelForID(this.parent.conversation); let message = `#${this.toUnicode(channel.name)} is a private community for owners of a .${this.toUnicode(channel.name)} only.`; let names = this.parent.domains.filter(d => { return d.tld == channel.name; }); let html = $(` <div class="needSLD"> <span>${message}</span> </div> `); switch (data.resolution) { case "purchase": html.append($(` <div class="button" data-action="purchaseSLD" data-link="${data.link}">Purchase a .${this.toUnicode(channel.name)}</div> `)); break; case "create": html.append($(` <div class="button" data-action="createSLD" data-tld="${channel.name}">Create a free .${this.toUnicode(channel.name)}</div> `)); break; default: break; } names.forEach(name => { html.append($(` <div class="button" data-action="switchName" data-id="${name.id}">Switch to ${this.toUnicode(name.domain)}</div> `)); }); $("#messageHolder").append(html); } emptyUserList() { $("#users #count").html(''); $("#users .users table").empty(); } setUserList() { if (!this.parent.isChannel(this.parent.conversation)) { return; } if ($("#users .group.searching.shown").length) { return; } $("#users .users table").empty(); let users = this.parent.usersForConversation(this.parent.conversation); let sorted = this.parent.sorted(users, "domain"); let active = sorted.filter(u => { return u.active; }); let inactive = sorted.filter(u => { return !u.active; }); $.each(active, (k, user) => { this.addUserToUserlist(user); }); $.each(inactive, (k, user) => { this.addUserToUserlist(user); }); this.updateAvatars(); $("#users #count").html(users.length.toLocaleString("en-US")); } updateUserList() { if (!this.parent.isChannel(this.parent.conversation)) { return; } let users = this.parent.usersForConversation(this.parent.conversation); let sorted = this.parent.sorted(users, "domain"); let active = sorted.filter(u => { return u.active; }); let inactive = sorted.filter(u => { return !u.active; }); $.each(users, (k, user) => { let userEl = $(`#users .user[data-id=${user.id}]`); let isActive = !userEl.hasClass("inactive"); if (user.active) { if (!isActive) { let position = active.indexOf(user) - 1; let before = $($("#users .user").get(position)); userEl.remove(); userEl.removeClass("inactive"); userEl.find(".avatar .status").addClass("active"); if (position < 0) { $("#users table").prepend(userEl); } else { userEl.insertAfter(before); } } } else { if (isActive) { let position = inactive.indexOf(user) + active.length; let before = $($("#users .user").get(position)); userEl.remove(); userEl.addClass("inactive"); userEl.find(".avatar .status").removeClass("active"); if (position < 0) { $("#users table").prepend(userEl); } else { userEl.insertAfter(before); } } } }); } addUserToUserlist(user) { let name = this.toUnicode(user.domain); let html = $(` <tr class="user" data-id="${user.id}" data-name="${user.domain}"> ${this.avatar("td", user.id, user.domain, true)} <td class="title">${name}</td> </tr> `); if (!user.active) { html.addClass("inactive"); } $("#users .users table").append(html); } replaceSpecialMessage(message) { if (message.substring(0, 12) == "ACTION ") { return message.substring(12); } return message; } messageSummary(message) { let decoded = message; try { decoded = JSON.parse(message); } catch {} if (decoded.payment) { return "sent a payment"; } else if (decoded.attachment) { return "sent an attachment"; } else if (decoded.action) { return decoded.action; } else if (decoded.message) { return decoded.message; } return decoded; } async insertMessages(data, decrypt=false) { let messages = data.messages; for (let i in messages) { let message = messages[i]; if ($(`.messageRow[data-id=${message.id}]`).length) { return; } if (decrypt) { await this.parent.decryptMessageIfNeeded(this.parent.conversation, message).then(decrypted => { message.message = decrypted[0]; if (message.p_message) { message.p_message = decrypted[1]; } }); } if (message.p_message) { message.p_message = this.messageSummary(message.p_message); } let user = this.parent.userForID(message.user).domain; let messageBody = he.decode(he.decode(message.message)); messageBody = he.encode(messageBody); let isAction = false; if (messageBody.substring(0, 12) == "ACTION ") { messageBody = messageBody.substring(12); isAction = true; } let isNotice = false; let hasEffect = false; let hasStyle = false; let html = $(` <div class="messageRow" data-id="${message.id}" data-time="${message.time}" data-sender="${message.user}"> <div class="contents"> <div class="main"> ${this.avatar("div", message.user, user)} <div class="user" data-id="${message.user}">${this.toUnicode(user)}</div> <div class="holder msg"> <div class="message"> <div class="body"></div> </div> </div> </div> <div class="linkHolder"></div> <div class="holder react"> <div class="reactions"></div> </div> </div> </div> `); let decoded = he.decode(messageBody); try { let json = JSON.parse(he.decode(decoded)); if (json.hnschat) { if (json.attachment) { let link; try { let url = new URL(json.attachment); switch (url.host) { case "media.tenor.com": case "api.zora.co": link = json.attachment; break; } } catch { link = `https://${window.location.host}/uploads/${json.attachment}`; } let image = $(` <a href="${link}" target="_blank"> <img src="${link}" /> </a> `); html.find(".message").addClass("image"); html.find(".message .body").empty(); html.find(".message .body").append(image); } else if (json.payment) { let link = `https://niami.io/tx/${json.payment}`; let image = $(` <a href="${link}" target="_blank"> <div class="imageHolder"> <img src="/assets/img/icon-512x512" /> <div class="amount">${this.parent.rtrim(json.amount.toLocaleString("en-US", { minimumFractionDigits: 6 }), "0")}</div> <div class="txMessage"></div> </div> </a> `); html.find(".message").addClass("image payment"); html.find(".message .body").empty(); html.find(".message .body").append(image); } else if (json.action) { messageBody = he.encode(json.action); html.find(".message .body").html(messageBody); isAction = true; } else if (json.message) { messageBody = json.message.toString(); messageBody = he.encode(messageBody); html.find(".message .body").html(messageBody); } if (json.effect) { hasEffect = json.effect; } if (json.style) { hasStyle = json.style; } } } catch (e) { html.find(".message .body").html(messageBody); } if (isAction) { html.addClass("action"); } let firstThree = Array.from(messageBody.toString()).slice(0, 3); let isEmojis = true; $.each(firstThree, (k, char) => { if (!this.isCharEmoji(char)) { isEmojis = false; return false; } }); if (isEmojis) { html.addClass("emojis"); } let isDice = true; let chars = Array.from(messageBody.toString()); $.each(chars, (k, char) => { if (!["⚀","⚁","⚂","⚃","⚄","⚅"].includes(char)) { isDice = false; return false; } }); if (isDice) { html.addClass("emojis dice"); } if (hasStyle) { this.setData(html, "style", hasStyle); } let messageTime = new Date(message.time * 1000).format(this.parent.timeFormat); let actions = $(` <div class="hover"> <div class="time">${messageTime}</div> <div class="actions"> <div class="action icon reply" data-action="reply"></div> <div class="action icon emoji" data-action="emojis"></div> </div> </div> `) if (message.user == this.parent.domain) { html.addClass("self"); html.find(".holder.msg").prepend(actions); } else { html.find(".holder.msg").append(actions); } if (this.parent.mentionsMe(message.message)) { html.addClass("mention"); } if (message.notice) { html.addClass("notice"); } if (message.replying) { let reply = $(` <div class="reply" data-id="${message.replying}"> <div class="line"></div> <div class="contents"> <div class="user"></div> <div class="body"></div> </div> </div> `); if (message.p_user && message.p_message) { let messageReplyBody = ""; if (message.p_message.length) { messageReplyBody = he.encode(message.p_message); messageReplyBody = this.replaceSpecialMessage(messageReplyBody); } let p_user = this.parent.userForID(message.p_user).domain; this.setData(reply.find(".user"), "id", message.p_user); reply.find(".user").html(this.toUnicode(p_user)); reply.find(".body").html(messageReplyBody); if (message.p_user == this.parent.domain) { html.addClass("mention"); } } else { reply.find(".user").remove(); reply.find(".body").html("Original message was deleted."); } html.addClass("replying"); html.prepend(reply); } if (hasEffect) { let effect = $(`<div class="messageEffect action link" data-action="replayEffect" data-effect="${hasEffect}"><div class="icon replay"></div>Replay</div>`); html.find("> .contents").append(effect); } if (!message.reactions) { message.reactions = JSON.stringify({}); } if (data.before || data.at) { this.parent.messages.unshift(message); $("#messages").prepend(html); } else { this.parent.messages.push(message); $("#messages").append(html); this.fixScroll(); } if (!decrypt && hasEffect) { this.handleEffect(hasEffect); } this.updateReactions(message.id); this.stylizeMessage(html); this.updateAvatars(); } this.parent.loadingMessages = false; this.markEmptyIfNeeded(); } async handleEffect(effect) { let done = new Promise(resolve => { switch (effect) { case "confetti": startConfetti(); setTimeout(() => { stopConfetti(); resolve(); }, 3000); break; } }); return await done; } markEmptyIfNeeded() { if (!$(".messageRow").length && !$(".needSLD").length) { $("#messages").addClass("empty"); } else { $("#messages").removeClass("empty"); } } deleteMessage(id) { let message = $(`.messageRow[data-id=${id}]`); if (message.length) { let previous = message.prev(); let next = message.next(); message.remove(); if (previous.length) { this.stylizeMessage(previous); } if (next.length) { this.stylizeMessage(next); } if (this.parent.isChannel(this.parent.conversation)) { let channel = this.parent.channelForID(this.parent.conversation); if (id == channel.pinned) { channel.pinned = null; this.setPinnedMessage(); } } } } updateReactions(id) { let message = $(`.messageRow[data-id=${id}]`); message.find(".reactions").empty(); let reactions = this.parent.messages.filter(m => { return m.id == id; })[0].reactions; if (reactions) { let json = JSON.parse(reactions); if (Object.keys(json).length) { $.each(json, (r, u) => { let users = this.userString(u); let reaction = $(` <div class="reaction" data-reaction="${r}" title="${users}"> <div>${r}</div> <div class="count">${u.length}</div> </div> `); if (u.includes(this.parent.domain)) { reaction.addClass("self"); } message.find(".reactions").append(reaction); }); } } } moveConversationToTop(conversation) { let div = $(`#conversations tr[data-id=${conversation}]`); let parent = div.parent(); div.remove(); parent.prepend(div); } userString(array) { let output; let users = [...array]; $.each(users, (k, u) => { let name = this.parent.userForID(u).domain; users[k] = name; }); if (users.length == 1) { output = users[0]; } else { let last = users.pop(); let others = users.join(", "); output = `${others} and ${last}`; } return output; } isCharEmoji(char) { if (char == "\u200d") { return true; } let match = emojis.filter(emoji => { return emoji.emoji == char || emoji.emoji.replace("\ufe0f", "") == char; }); if (match.length) { return true; } return false; } stylizeMessage(message) { let messageHolder = $("#messages"); let firstMessage = $("#messages > .messageRow[data-id]").first(); let firstMessageID = firstMessage.data("id"); let lastMessage = $("#messages > .messageRow[data-id]").last(); let lastMessageID = lastMessage.data("id"); let messageID = message.data("id"); let messageTime = message.data("time"); let messageDate = new Date(messageTime * 1000).format(this.parent.dateFormat); let contents = message.find(".contents"); let messageSender = message.data("sender"); let messageUser; let previousMessage = message.prev(); let previousMessageTime = previousMessage.data("time"); let previousMessageDate = new Date(previousMessageTime * 1000).format(this.parent.dateFormat); let previousMessageSender = previousMessage.data("sender"); let nextMessage = message.next(); let nextMessageTime = nextMessage.data("time"); let nextMessageDate = new Date(nextMessageTime * 1000).format(this.parent.dateFormat); let nextMessageSender = nextMessage.data("sender"); let nextMessageUser; let isFirst = false; let isLast = false; let isReply = false; let isInformational = false; let isAction = false; let isDate = false; let before = false; let addUser = false; let addNextUser = false; let removeUser = false; let removeNextUser = false; let prependDate = false; let appendDate = false; if (firstMessageID == messageID) { isFirst = true; } if (lastMessageID == messageID) { isLast = true; } if (message.hasClass("replying")) { isReply = true; } if (message.hasClass("informational")) { isInformational = true; } if (message.hasClass("date")) { isDate = true; } if (message.hasClass("action")) { isAction = true; } if (nextMessage.length) { before = true; } if (isFirst || isReply) { addUser = true; } if (!before) { if (!isDate && previousMessage.length && messageDate !== previousMessageDate && !previousMessage.hasClass("date")) { prependDate = true; } } else { if (!isDate && nextMessage.length && messageDate !== nextMessageDate && !nextMessage.hasClass("date")) { appendDate = true; } } if (isDate) { previousMessage.addClass("last"); message.addClass("first last"); if (nextMessage.length) { addNextUser = true; } } else { let timeDifference; messageUser = this.parent.userForID(messageSender).domain; if (before) { message.addClass("first"); } else { message.addClass("last"); } if (isFirst || isReply) { message.addClass("first"); } if (isLast) { message.addClass("last"); } if (previousMessage.length) { timeDifference = messageTime - previousMessageTime; if (timeDifference >= 60 || previousMessageSender !== messageSender) { previousMessage.addClass("last"); message.addClass("first"); addUser = true; } else if (!isReply) { removeUser = true; } } if (timeDifference < 60 && previousMessageSender == messageSender && !message.hasClass("replying")) { previousMessage.removeClass("last"); } if (previousMessageSender !== messageSender) { previousMessage.addClass("last"); } if (nextMessage.length) { timeDifference = nextMessageTime - messageTime; if (timeDifference > 60) { nextMessage.addClass("first"); addNextUser = true; } if (nextMessage.hasClass("first")) { message.addClass("last"); } } if (timeDifference < 60 && nextMessageSender == messageSender && !nextMessage.hasClass("replying")) { removeNextUser = true; } if (addUser) { if (!contents.find(".user").length) { //let user = $('<div class="user" />'); //user.html(messageUser); //contents.prepend(user); //contents.prepend(messageAvatar(messageSender, messageUser)); } } if (removeUser) { message.removeClass("first"); //message.find(".contents .user").remove(); //message.find(".contents .avatar").remove(); } if (removeNextUser) { message.removeClass("last"); nextMessage.removeClass("first"); //nextMessage.find(".contents .user").remove(); //nextMessage.find(".contents .avatar").remove(); } if (prependDate) { let infoRow = $('<div class="messageRow informational date last" />'); infoRow.html(messageDate); infoRow.insertBefore(message); this.stylizeMessage(infoRow); } if (appendDate) { let infoRow = $('<div class="messageRow informational date last" />'); infoRow.html(nextMessageDate); infoRow.insertAfter(message); this.stylizeMessage(infoRow); } } if (addNextUser) { if (!nextMessage.find(".contents .user").length) { //nextMessageUser = this.parent.userForID(nextMessageSender).domain; //let user = $('<div class="user" />'); //user.html(nextMessageUser); //nextMessage.addClass("first"); //nextMessage.find(".contents").prepend(user); //nextMessage.find(".contents").prepend(messageAvatar(nextMessageSender, nextMessageUser)); } } let newLastMessage = $(".messageRow").last(); if (newLastMessage.hasClass("informational date")) { newLastMessage.remove(); } //this.codify(message.find(".contents .body")); this.linkify(message.find(".contents .body")); this.updateAvatars(); } codify(elements) { $.each(elements, (k, e) => { e = $(e); let output = e.html(); while (this.parent.regex(/\`{3}(?<code>[.\s\S]+?)\`{3}/gm, output).length) { let matches = this.parent.regex(/\`{3}(?<code>[.\s\S]+?)\`{3}/gm, output); if (matches.length) { let result = matches[0]; let full = result[0]; let code = result.groups.code; let start = result.index; let end = (start + full.length); let replace = `<pre class="multi"><code class="hljs">${code}</code></pre>`; output = this.parent.replaceRange(output, start, end, replace); } } while (this.parent.regex(/\`(?<code>[.\s\S]+?)\`/gm, output).length) { let matches = this.parent.regex(/\`(?<code>[.\s\S]+?)\`/gm, output); if (matches.length) { let result = matches[0]; let full = result[0]; let code = result.groups.code; let start = result.index; let end = (start + full.length); let replace = `<code>${code}</code>`; output = this.parent.replaceRange(output, start, end, replace); } } e.html(output); }); } linkify(elements) { $.each(elements, (k, e) => { e = $(e); let output = e.html(); let links = anchorme.list(output).reverse(); $.each(links, (k, link) => { let href = link.string; if (link.isEmail) { href = `mailto:${href}`; } else if (link.isURL && href.substring(0, 8) !== "https://" && href.substring(0, 7) !== "http://") { href = `http://${href}`; } let replace = `<a class="inline link" href="${href}" target="_blank">${link.string}</a>` output = this.parent.replaceRange(output, link.start, link.end, replace); }); let mentions = this.usersInMessage(output); $.each(mentions, (k, mention) => { let id = mention.groups.name; if (id == this.parent.domain) { e.closest(".messageRow").addClass("mention"); } }); output = this.replaceIds(output); e.html(output); this.expandLinks(e); }); } expandLinks(message) { if (message.hasClass("expanded")) { return; } message.addClass("expanded"); let links = message.find("a.inline"); if (links.length) { if (!message.parent().hasClass("message")) { return; } let link = links[0].href; let embed = this.shouldInlineLink(link); if (embed) { let div; switch (embed) { case "image": div = $(` <a href="${link}" class="previewImage" target="_blank"> <div class="preview"> <div class="media"> <img src="${link}"> </div> </div> </a> `); break; case "video": div = $(` <div class="preview"> <div class="media"> <video controls> <source src="${link}"> </video> </div> </div> `); break; } if (div) { message.closest(".messageRow").find(".linkHolder").append(div); this.fixScroll(); } } else { let data = { action: "getMetaTags", url: link } if (link.substring(0, 7) == "mailto:") { return; } this.parent.api(data).then(r => { let div = $(` <a href="${link}" class="previewLink" target="_blank"> <div class="preview"></div> <div class="info"></div> </a> `); if (r.tags) { let t = r.tags; if (t.title) { div.find(".info").append(`<div class="title">${t.title}</div>`); if (t.description) { div.find(".info").append(`<div class="subtitle">${t.description}</div>`); } /* if (t.video) { div.find(".preview").prepend(` <div class="media"> <iframe src="${t.video}" frameborder="0" allowfullscreen="" webkitallowfullscreen="true" mozallowfullscreen="true" oallowfullscreen="true" msallowfullscreen="true"> </div> `); } else */ if (t.image) { div.find(".preview").prepend(` <div class="media"> <img src="https://${window.location.host}${t.image}"> </div> `); } message.closest(".messageRow").find(".linkHolder").append(div); this.fixScroll(); } } }); } } } shouldInlineLink(link) { try { let url = new URL(link); let match; switch (url.host) { case "hns.chat": case "hnschat": case window.location.host: match = url.pathname.match(/^(\/uploads\/.{32}|\/avatar\/.{16})$/); if (match) { return "image"; } break; case "i.arxius.io": match = url.pathname.match(/^\/.{8}\.(jpeg|jpg|png|gif)$/); if (match) { return "image"; } break; case "v.arxius.io": match = url.pathname.match(/^\/.{8}$/); if (match) { return "video"; } break; case "i.imgur.com": match = url.pathname.match(/^\/.{7}\.(jpeg|jpg|png|gif)$/); if (match) { return "image"; } break; case "i.redd.it": match = url.pathname.match(/^\/.{13}\.(jpeg|jpg|png|gif)$/); if (match) { return "image"; } break; } } catch {} return false; } usersInMessage(message) { let matches = this.parent.regex(/\@(?<id>[a-zA-Z0-9]{16}(?:\b|$))/gm, message); return matches; } channelsInMessage(message) { let matches = this.parent.regex(/\@(?<id>[a-zA-Z0-9]{8}(?:\b|$))/gm, message); return matches; } replaceIds(message, link=true) { let output = message; while (this.channelsInMessage(output).length) { let channels = this.channelsInMessage(output); let result = channels[0]; let id = result.groups.id; let start = result.index; let end = (start + id.length + 1); let replace; let match = this.parent.channelForID(id); if (match) { let channel = match.name; replace = `<div class="inline channel" data-id="${id}">#${this.toUnicode(channel)}</div>`; } else { replace = `@\x00${id}`; } output = this.parent.replaceRange(output, start, end, replace); } while (this.usersInMessage(output).length) { let users = this.usersInMessage(output); let result = users[0]; let id = result.groups.id; let start = result.index; let end = (start + id.length + 1); let replace; let match = this.parent.userForID(id); if (match) { let domain = match.domain; replace = `<div class="inline nick" data-id="${id}">@${this.toUnicode(domain)}/</div>`; } else { replace = `@\x00${id}`; } output = this.parent.replaceRange(output, start, end, replace); } return output; } preventTabIfNeeded(e) { let target = $(e.target); if (this.parent.isKey(e, 9)) { if (!target.hasClass("tab")) { e.preventDefault(); } } } focusInputIfNeeded(e) { if (!$("input").is(":focus") && !$("#message").is(":focus") && !$("#blackout").is(":visible")) { if ((this.parent.keyName(e).length == 1 || this.parent.isKey(e, 9)) && !((e.ctrlKey || e.metaKey))) { if (this.parent.isKey(e, 9)) { e.preventDefault(); } $("#message").focus(); } } } removePopoverIfNeeded() { if ($("#blackout").is(":visible")) { this.close(); return true; } return false; } removeReplyingIfNeeded() { if (!$(".popover.shown").length && $("#replying.shown").length) { $(".action[data-action=removeReply]").click(); return true; } return false; } undoProfileIfNeeded() { if ($(".contextMenu[data-name=userContext].me.editing.shown").length) { this.undoProfile(); return true; } return false; } updateReplying() { if (this.parent.replying) { let name = this.toUnicode(this.parent.userForID(this.parent.replying.sender).domain); $("#replying .message .name").html(name); $("#replying").addClass("shown"); $("#holder").addClass("replying"); $(".messageRow").removeClass("selected"); $(".messageRow").removeClass("selecting"); $(`.messageRow[data-id=${this.parent.replying.message}]`).addClass("selected"); } else { $("#replying").removeClass("shown"); $("#replying .message .name").html(''); $("#holder").removeClass("replying"); $(`.messageRow`).removeClass("selected"); } } async popover(action) { $(".popover.shown").removeClass("shown"); let popover = $(`.popover[data-name=${action}]`); let output = new Promise(resolve => { switch (action) { case "syncSession": resolve(); break; case "pay": popover.find("input[name=hns]").inputmask({mask: "9{+}[.9{1,6}] HNS", greedy: false, placeholder: "0"}); popover.find(".response").html(""); resolve(); let to = getOtherUser(conversation).domain; let toID = getOtherUserID(conversation); let data = { action: "getAddress", domain: toID } api(data).then(r => { popover.find(".loading").removeClass("shown"); if (r.success) { popover.find(".subtitle").html(to+"/ is able to accept payments!"); popover.find("input[name=address]").val(r.address); popover.find(".content").addClass("shown"); popover.find("input[name=hns]").focus(); } else { popover.find(".response").addClass("error"); popover.find(".response").html(r.message); } }); break; case "settings": popover.find("input[name=avatar]").parent().addClass("hidden"); popover.find("input[name=address]").parent().addClass("hidden"); let user = this.parent.userForID(this.parent.domain); let tld = user.tld; if (["hnschat", "theshake"].includes(tld)) { popover.find("input[name=address]").parent().removeClass("hidden"); popover.find("input[name=avatar]").parent().removeClass("hidden"); popover.find("input[name=avatar]").val(user.avatar); let data = { action: "getAddress", domain: this.parent.domain } this.parent.api(data).then(r => { if (r.address) { popover.find("input[name=address]").val(r.address); } resolve(); }); } else { resolve(); } break; case "completions": case "emojis": let bottom = $(".inputHolder").outerHeight() + 10; popover.css("bottom", bottom+"px"); resolve(); break; default: resolve(); break; } }); output.then(() => { if (popover.length) { $("#blackout").addClass("shown"); $("#messageHolder").addClass("noScroll"); popover.addClass("shown"); if (action == "newConversation" && popover.find("input[name=domain]").val()) { popover.find("input[name=message]").focus(); } else { let first = popover.find("input:visible:first"); if (first.attr("type") !== "color") { if (this.isDesktop()) { if (!["syncSession"].includes(action)) { first.focus(); } } else { if (!["syncSession", "react"].includes(action)) { first.focus(); } } } } if (action === "emojis") { $(".popover[data-name=emojis] .body .grid[data-type=emojis]").scrollTop(0); } } }); } close(old=false) { $("#blackout").removeClass("shown"); $("#completions").removeClass("shown"); $(".popover.shown").find("input:not([readonly])").val(''); $(".popover.shown").find(".response").html(''); $(".popover.shown").find(".content").removeClass("shown"); $(".popover.shown").find(".loading").addClass("shown"); $(".popover.shown").find(".button").removeClass("disabled"); $(".popover[data-name=react]").find(".grid[data-type=gifs]").scrollTop(0); $(".popover.shown").removeClass("shown"); $("#messageHolder").removeClass("noScroll"); $(".popover[data-name=react] .grid[data-type=emojis] .section").removeClass("hidden"); $(".popover[data-name=react] .grid[data-type=emojis] .section[data-name=Search]").addClass("hidden"); $("#holder").removeClass("reacting"); $(".messageRow .hover.visible").removeClass("visible"); $("#users .user.selected").removeClass("selected"); $(".popover[data-name=react] .section[data-type=gifs] .column").empty(); $(".messageRow").removeClass("selecting"); if (!$("#holder").hasClass("reacting") && !$("#holder").hasClass("replying")) { $(".messageRow").removeClass("selected"); } this.updateReplying(); this.undoProfile(); this.shouldShowGifs(); } messagesLoading(bool) { if (bool) { $("#messageHolder #messages").addClass("hidden"); $("#messageHolder .loading").addClass("shown"); } else { $("#messageHolder .loading").removeClass("shown"); $("#messageHolder #messages").removeClass("hidden"); } } updateAvatars() { $.each($(".favicon:not(.loaded)"), (k, e) => { let favicon = $(e); favicon.addClass("loaded"); let d = favicon.data("domain"); let id = favicon.data("id"); let user = this.parent.userForID(id); if (user.id) { if (Object.keys(this.parent.avatars).includes(id)) { if (this.parent.avatars[id]) { let original = $("#avatars .favicon[data-id="+id+"]"); if (original.length) { let clone = original[0].cloneNode(true); let parent = favicon.parent(); favicon.remove(); parent.append(clone); //parent.find(".fallback").html(''); this.updateOtherAvatars(id); } } } else { this.parent.avatars[id] = false; let link = user.avatar; if (link) { link = `https://${window.location.host}/avatar/${user.id}`; let img = $('<img class="loading" />'); img.attr("src", link).on("load", i => { let im = $(i.target); favicon.css("background-image", "url("+link+")"); //favicon.parent().find(".fallback").html(''); im.remove(); this.parent.avatars[id] = link; let clone = favicon[0].cloneNode(true); $("#avatars").append(clone); this.updateOtherAvatars(user.id); }).on("error", r => { $(r.target).remove(); //this.parent.avatars[id] = false; }); $("html").append(img); } else { this.parent.avatars[id] = false; } } } }); } updateOtherAvatars(id) { if (this.parent.avatars[id]) { let avatars = [ $(`#conversations .section.pms .avatar .favicon[data-id=${id}].loaded`), $(`.messageHeader .avatar .favicon[data-id=${id}].loaded`), $(`#messages .messageRow .avatar .favicon[data-id=${id}].loaded`), $(`#users .user .avatar .favicon[data-id=${id}].loaded`), $(`.contextMenu[data-name=userContext] .avatar .favicon[data-id=${id}].loaded`), $(`.header .avatar .favicon[data-id=${id}].loaded`), $(`#videoInfo .users .avatar .favicon[data-id=${id}].loaded`) ]; $.each(avatars, (k, avatar) => { if (avatar.length) { $.each(avatar, (k, a) => { a = $(a); if (a.css("background-image") === "none" || !a.css("background-image")) { let original = $("#avatars .favicon[data-id="+id+"]"); if (original.length) { let clone = original[0].cloneNode(true); let parent = a.parent(); a.remove(); parent.append(clone); //parent.find(".fallback").html(''); } } }); } }); } } setContextMenuPosition(menu, e) { if (!this.isDesktop()) { return; } let hx = window.innerWidth / 2; let hy = window.innerHeight / 2; let x = e.clientX; let y = e.clientY; if (x >= hx) { x = e.clientX - menu.outerWidth(); } if (y >= hy) { y = e.clientY - menu.outerHeight(); } menu.css({ top: y, left: x }); } setCaretPosition(ctrl, pos) { if (ctrl.setSelectionRange) { ctrl.focus(); ctrl.setSelectionRange(pos, pos); } else if (ctrl.createTextRange) { let range = ctrl.createTextRange(); range.collapse(true); range.moveEnd('character', pos); range.moveStart('character', pos); range.select(); } else { var range = document.createRange(); var selection = window.getSelection(); range.setStart(ctrl[0].childNodes[0], pos); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } categoryForEmoji(emoji) { let cat = false; $.each(Object.keys(this.emojiCategories), (k, category) => { let data = this.emojiCategories[category]; if (data.includes(emoji.category)) { cat = category; return false; } }); if (cat) { return cat; } return false; } setupReactView(e, sender) { let target = $(e.target); let action = target.data("action"); let menu = $(".popover[data-name=react]"); if (!sender) { sender = ""; } this.setData(menu, "sender", sender); menu.find(".tab[data-name=categories]").addClass("hidden"); menu.find(".tab[data-name=gifs]").addClass("hidden"); if (sender) { let row = $(`.messageRow[data-id=${sender}]`); if (!row.length) { let id = target.closest(".body").find("span.message").data("id"); row = $("#messages").find(`.messageRow[data-id=${id}]`); } $("#holder").addClass("reacting"); $(`.messageRow`).removeClass("selected"); $(".messageRow").removeClass("selecting"); row.addClass("selected"); let hover = row.find(".hover"); hover.addClass("visible"); menu.addClass("react"); this.setContextMenuPosition(menu, e); } else { menu.find(".tab[data-name=gifs]").removeClass("hidden"); menu.removeClass("react"); menu.css({ top: "auto" }); } if (!menu.hasClass("loaded")) { let data = { action: "getGifCategories" } this.parent.api(data).then(r => { if (r.success) { $.each(r.categories, (k, c) => { let category = $(` <div class="category" data-term=${c.term}> <div class="title">${c.term}</div> <div class="background" style="background-image: url(${c.gif})"></div> </div> `); menu.find(".section[data-type=categories]").append(category); }); } }); $.each(Object.keys(this.emojiCategories), (k, category) => { let section = $(` <div class="section" data-name="${category}"> <div class="subtitle">${category}</div> <div class="emojis"></div> </div> `); if (k == 0) { section.addClass("hidden"); } menu.find(".body .grid[data-type=emojis]").append(section); }); $.each(emojis, (k, emoji) => { let category = this.categoryForEmoji(emoji); let item = $(`<div class="emoji" data-aliases=${JSON.stringify(emoji.aliases)}>${emoji.emoji}</div>`); menu.find(`.body .grid[data-type=emojis] .section[data-name=${category}] .emojis`).append(item); }); menu.addClass("loaded"); } this.switchReactTab(action); } switchReactTab(tab) { let menu = $(".popover[data-name=react]"); menu.find(`.tabs .tab`).removeClass("active"); menu.find(".search input[name=searchGifs]").removeClass("shown"); menu.find(".search input[name=searchEmojis]").removeClass("shown"); menu.find(`.grid`).removeClass("shown"); menu.find(`.tabs .tab[data-name=${tab}]`).addClass("active"); menu.find(`.grid[data-type=${tab}]`).addClass("shown"); switch (tab) { case "gifs": menu.find(".search input[name=searchGifs]").addClass("shown"); break; case "emojis": menu.find(".search input[name=searchEmojis]").addClass("shown"); break; } } searchGifs(query) { let menu = $(".popover[data-name=react]"); menu.find(".section[data-type=gifs] .column").empty(); if (query.length) { let data = { action: "searchGifs", query: query } this.parent.api(data).then(r => { if (r.success) { let column; let col0 = 0; let col1 = 0; $.each(r.gifs, (k, g) => { if (col0 > col1) { column = 1; col1 += g.height; } else { column = 0; col0 += g.height; } let gif = $(`<img class="gif" src="${g.preview}" data-id="${g.id}" data-full="${g.full}"></div>`); menu.find(`.section[data-type=gifs] .column[data-column=${column}]`).append(gif); }); } this.shouldShowGifs(); }); } else { this.shouldShowGifs(); } } shouldShowGifs() { let menu = $(".popover[data-name=react]"); if (menu.find(".gif").length) { menu.find(".section[data-type=categories]").addClass("hidden"); menu.find(".section[data-type=gifs]").addClass("shown"); } else { menu.find(".section[data-type=gifs]").removeClass("shown"); menu.find(".section[data-type=categories]").removeClass("hidden"); } } handleDoubleClick(target) { let id = target.data("id"); let type; if (target.hasClass("inline channel")) { type = "channel"; } else { type = "user"; } switch (type) { case "user": let name = this.parent.userForID(id).domain; let puny = `${this.toUnicode(name)}/`; $(`.popover[data-name=newConversation] input[name=domain]`).val(puny); this.popover("newConversation"); break; case "channel": $(`#conversations .channels tr[data-id=${id}]`).click(); break; } } handleRightClick(e, target) { let me = this.parent.userForID(this.parent.domain); let isChannel = this.parent.isChannel(this.parent.conversation); let channel = {}; let isAdmin = false; if (isChannel) { channel = this.parent.channelForID(this.parent.conversation); isAdmin = this.isAdmin(channel, me); } if (target.hasClass("fallback")) { target = target.parent().find(".favicon"); } if (target.hasClass("user") || target.hasClass("favicon") || target.hasClass("inline nick")) { let id = target.data("id"); let user = this.parent.userForID(id); let domain = this.toUnicode(user.domain); let joined = new Date(user.created * 1000).format(this.parent.dateFormat); let bio = null; if (user.bio) { bio = he.encode(user.bio) } let avatar = this.avatar("div", id, user.domain, true); let userList = target.closest("#users").length; if (userList) { $(`#users .user[data-id=${id}]`).addClass("selected"); } let menu = $(".popover[data-name=userContext]"); menu.find(".pic").html(avatar); this.setData(menu, "id", id); this.setData(menu, "type", user.type); menu.find("span.user").html(domain); menu.find("li.bio").addClass("hidden"); if (bio || id == this.parent.domain) { menu.find("li.bio").removeClass("hidden"); } if (id == this.parent.domain) { menu.addClass("me"); } else { menu.removeClass("me"); } menu.find("li.action.speaker").addClass("hidden"); if (channel.video && (isAdmin && user.active && !Object.keys(this.parent.currentVideoUsers()).includes(id))) { menu.find("li.action.speaker").removeClass("hidden"); } menu.find("div.bio").html(bio); menu.find("span.joined").html(joined); this.updateAvatars(); this.linkify($(".contextMenu[data-name=userContext] div.bio")); this.popover("userContext"); this.setContextMenuPosition(menu, e); } else if (target.hasClass("inline channel")) { let id = target.data("id"); let channel = this.parent.channelForID(id); let name = `#${this.toUnicode(channel.name)}`; let menu = $(".popover[data-name=channelContext]"); this.setData(menu, "id", id); menu.find("span.channel").html(name); this.popover("channelContext"); this.setContextMenuPosition(menu, e); } else if (target.closest(".messageRow").length) { let message = target.closest(".messageRow"); if (message.hasClass("informational")) { return; } $(".messageRow").removeClass("selecting"); message.addClass("selecting"); let menu = $(".popover[data-name=messageContext]"); this.setData(menu, "id", message.data("id")); this.setData(menu, "sender", message.data("sender")); menu.find("li.action.pin").addClass("hidden"); menu.find("li.action.delete").addClass("hidden"); if (isChannel) { if (isAdmin) { menu.find("li.action.pin").removeClass("hidden"); menu.find("li.action.delete").removeClass("hidden"); } } this.popover("messageContext"); this.setContextMenuPosition(menu, e); } } changeConversation(id) { $(`#conversations .sections tr[data-id=${id}]`).click(); } updateTypingView() { let typers = []; $.each(this.parent.typers, (typer, data) => { if ((this.parent.time() - data.time) <= this.parent.typingDelay) { let name = this.toUnicode(this.parent.userForID(typer).domain); if (data.to == this.parent.conversation) { typers.push(`<span>${name}</span>`); } } else { delete this.parent.typers[typer]; } }); let message; if (typers.length) { if (typers.length > 1) { if (typers.length > 5) { message = "Many users are typing..."; } else { let beginning = typers.slice(0, typers.length - 1); let last = typers.pop(); message = `${beginning.join(", ")} and ${last} are typing...`; } } else { message = `${typers[0]} is typing...`; } $("#typing .message").html(message); $("#typing").addClass("shown"); } else { $("#typing").removeClass("shown"); } } updateMentions(channels) { $.each(channels, (k, channel) => { $(`#conversations tr[data-id=${channel}]`).addClass("mentions"); }); } hasBob() { $("body").addClass("bob"); } paymentResponse(data) { let address = data.address; let popover = $(".popover[data-name=pay]"); popover.find(".loading").removeClass("shown"); popover.find(".content").addClass("shown"); if (address) { popover.find("input[name=address]").val(address); popover.find("input").removeClass("hidden"); popover.find(".button").removeClass("hidden"); } else { popover.find("input").addClass("hidden"); popover.find(".button").addClass("hidden"); popover.find(".response").addClass("error"); popover.find(".response").html(data.message); } } enableTarget(el) { el.removeClass("disabled"); } enableButton(type) { $(`.button[data-action="${type}" i]`).removeClass("disabled"); } handleSuccess(body) { switch (body.type) { case "ADDDOMAIN": case "ADDSLD": this.parent.changeDomain(body.id); this.parent.ws.send("DOMAINS"); break; case "VERIFYDOMAIN": $(`.section#manageDomains .domain[data-id=${body.id}] .action[data-action=verifyDomain]`).remove(); break; } } errorResponse(body) { if (!body.message) { body.message = "An unknown error occurred."; } switch (body.type) { case "PM": case "startConversation": $(".popover[data-name=newConversation] .response").html(body.message); break; case "ADDSLD": case "ADDDOMAIN": $(".section#addDomain .response").html(body.message); break; case "sendPayment": $(".popover[data-name=pay] .response").html(body.message); break; case "saveSettings": $(".popover[data-name=settings] .response").html(body.message); break; } } closeMenusIfNeeded() { $("#conversations").removeClass("showing"); $("#users").removeClass("showing"); $("body").removeClass("menu"); } searchUsers(bool) { if (bool) { $("#users #count").addClass("hidden"); $("#users .group.normal").addClass("hidden"); $("#users .group.searching").addClass("shown"); $("#users .group.searching input").focus(); } else { $("#users .group.searching").removeClass("shown"); $("#users .group.normal").removeClass("hidden"); $("#users tr.user").removeClass("hidden"); $("#users #count").removeClass("hidden"); $("#users input[name=search]").val(''); } } queryUsers(query) { let users = $("#users tr.user"); $.each(users, (k, user) => { let match = Array.from(this.toUnicode($(user).data("name").toString())).slice(0, Array.from(query).length).join("").toLowerCase(); let search = query.toLowerCase(); if (match == search) { $(user).removeClass("hidden"); } else { $(user).addClass("hidden"); } }); } showOrHideAttachments() { let attachments = $("#attachments"); if (!attachments.find(".attachment").length) { attachments.removeClass("shown"); } else { attachments.addClass("shown"); this.fixScroll(); } } setupNotifications() { if ('Notification' in window && navigator.serviceWorker) { if (!(Notification.permission === "granted" || Notification.permission === "blocked")) { Notification.requestPermission(e => { if (e === "granted") { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope: '/', }); } } }); } } } sendNotification(title, body, conversation) { if ('Notification' in window && navigator.serviceWorker) { if (Notification.permission == 'granted') { navigator.serviceWorker.getRegistration().then(reg => { let sound = new Audio("/assets/sound/pop"); sound.play(); var options = { body: body, icon: '/assets/img/logo.png', vibrate: [100, 50, 100], data: { dateOfArrival: this.parent.time(), primaryKey: 1 }, conversation: conversation }; var notification = new Notification(title, options); notification.onclick = () => { this.parent.changeConversation(conversation); window.focus(); }; }); } } } stripHTML(string) { var div = document.createElement("div"); div.innerHTML = string.replace("<div>", "\n"); var text = div.textContent || div.innerText || ""; return text.trim(); } clearSelection() { if (window.getSelection) { window.getSelection().removeAllRanges(); } else if (document.selection) { document.selection.empty(); } } getBrowser() { var browser = (function() { var test = function(regexp) {return regexp.test(window.navigator.userAgent)} switch (true) { case test(/edg/i): return "Microsoft Edge"; case test(/trident/i): return "Microsoft Internet Explorer"; case test(/firefox|fxios/i): return "Mozilla Firefox"; case test(/opr\//i): return "Opera"; case test(/ucbrowser/i): return "UC Browser"; case test(/samsungbrowser/i): return "Samsung Browser"; case test(/chrome|chromium|crios/i): return "Google Chrome"; case test(/safari/i): return "Apple Safari"; default: return "Other"; } })(); return browser; }; fixScroll() { switch (this.browser) { case "Apple Safari": let messageHolder = $("#messageHolder"); let scrollTop = messageHolder[0].scrollTop; if (scrollTop >= 0) { messageHolder.scrollTop(-1); messageHolder.scrollTop(0); } break; } } scrollToMessage(id) { let messageHolder = $("#messageHolder"); $(`.messageRow[data-id=${id}]`)[0].scrollIntoView({ behavior: 'smooth', block: 'start', inline: "nearest" }); } highlightMessage(id) { $(`.messageRow[data-id=${id}]`).addClass("highlighted"); setTimeout(() => { $(`.messageRow[data-id=${id}]`).removeClass("highlighted"); }, 3000); } handleHash() { let hash = this.parent.hash; if (!hash) { return; } hash = hash.toLowerCase().trim(); let split = hash.split(":"); let action = split[0]; let param = split[1]; switch (action) { case "message": if (param) { let puny = `${this.toUnicode(param)}/`; $(`.popover[data-name=newConversation] input[name=domain]`).val(puny); this.popover("newConversation"); } break; case "channel": let channel = this.parent.channelForName(param); if (channel) { this.parent.changeConversation(channel.id); } break; } localStorage.removeItem("hash"); } tabComplete() { let prefix = ""; let suffix = ""; var text = $("#message").val(); var options = []; if (text[0] === "/") { if (text.length > 1 && text[1] !== "/") { prefix = "/"; suffix = " "; options = this.parent.commands; text = text.substring(1); } } else { return; } options.sort(); let matches = options.filter(option => { option = String(option); if (option.length) { return option.substr(0, text.length).toLowerCase() == text.toLowerCase(); } return; }); if (matches.length) { $("#message").val(prefix+matches[0]+suffix); } } editProfile() { let menu = $(".contextMenu[data-name=userContext]"); menu.addClass("editing"); let bio = menu.find("div.bio"); let bioText = this.sanitizeContentEditable(bio[0]); bio.html(he.encode(bioText)); bio.attr("contenteditable", true); bio.focus(); if (bioText.length) { this.setCaretPosition(bio, bioText.length); } this.updateBioLimit(); } saveProfile() { let menu = $(".contextMenu[data-name=userContext]"); menu.removeClass("editing"); let bio = menu.find("div.bio"); let bioText = this.sanitizeContentEditable(bio[0]); bio.html(he.encode(bioText)); bio.attr("contenteditable", false); this.linkify(bio); let data = { bio: bioText } this.parent.ws.send(`SAVEPROFILE ${JSON.stringify(data)}`); } undoProfile() { let menu = $(".contextMenu[data-name=userContext].me.editing"); if (menu.length) { menu.removeClass("editing"); let bio = menu.find("div.bio"); let id = menu.data("id"); let user = this.parent.userForID(id); let bioText = user.bio; if (bioText) { bioText = he.encode(bioText); } bio.html(bioText); bio.attr("contenteditable", false); this.linkify(bio); } } updateBioLimit(e) { let menu = $(".contextMenu[data-name=userContext]"); let bio = menu.find("div.bio").text(); let save = menu.find(".action.save"); let max = 140; let string = `${bio.length} / ${max}`; let limit = menu.find(".bioHolder .limit"); limit.html(string); if (bio.length > max) { save.addClass("disabled"); limit.addClass("error"); } else { save.removeClass("disabled"); limit.removeClass("error"); } } sanitizeContentEditable(el) { let value = ""; let newLine = true; let nodes = el.childNodes; if (nodes) { nodes.forEach(node => { if (node.nodeName === "BR") { value += "\n"; newLine = true; return; } if (node.nodeName === "DIV" && newLine == false) { value += "\n"; } newLine = false; if (node.nodeType === 3 && node.textContent) { value += node.textContent; } if (node.childNodes) { value += this.sanitizeContentEditable(node); } }); } return value.trim(); } checkVersion(version) { if (!this.updateAvailable && this.version !== version) { this.updateAvailable = true; this.popover("update"); } } newTab(e) { if (e && ((e.ctrlKey || e.metaKey) || e.button == 1)) { return true; } return false; } openURL(page, e=null) { if (e && (this.newTab(e) || e.newTab)) { window.open(page, "_blank"); } else { window.location = page; } } avatar(type, id, domain, status=false) { let user = this.parent.userForID(id) let puny = this.toUnicode(domain); let active = ""; if (user.active) { active = " active"; } let statusHTML = ""; if (status) { statusHTML = `<div class="status${active}"></div>`; } return ` <${type} class="avatar"> ${statusHTML} <div class="favicon" data-id="${id}" data-domain="${domain}"></div> <div class="fallback">${this.avatarFallback(puny)}</div> </${type}> `; } changeMe(id) { let domain = this.parent.userForID(id).domain; let avatar = this.avatar("td", id, domain); $("#me").html(avatar); } chatDisplayMode(mode) { switch (mode) { case "compact": $("body").addClass("compact"); break; default: $("body").removeClass("compact"); break; } } isDesktop() { return !$(".header .icon.menu").is(":visible"); } isCompact() { return $("body").hasClass("compact"); } async scanQR() { let popover = $(".popover[data-name=qr]"); let loading = popover.find(".loading"); this.getCameraFeed($("#camera")).then(() => { loading.removeClass("shown"); this.lookForQR().then((qr) => { this.openURL(qr); }); }); } async getCameraFeed(el, device) { return new Promise(resolve => { let videoEl = el[0]; let config = { video: { facingMode: "environment" } }; navigator.mediaDevices.getUserMedia(config).then(stream => { videoEl.srcObject = stream; videoEl.play(); videoEl.addEventListener('loadedmetadata', function() { resolve(); }); }); }); } async lookForQR() { return new Promise(resolve => { let interval = setInterval(() => { let canvasElement = $("#frame")[0]; let canvas = canvasElement.getContext("2d"); let videoEl = $("#camera")[0]; let height = $(videoEl).height() * 2; let width = $(videoEl).width() * 2; canvas.drawImage(videoEl, 0, 0, canvasElement.width, canvasElement.height); var imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height); var code = jsQR(imageData.data, imageData.width, imageData.height); if (code) { clearInterval(interval); resolve(code.data); } }, 100); }); } async showVideoIfNeeded() { $("#videoContainer").removeClass("shown"); $("#videoInfo .link").removeClass("shown"); $("#videoInfo .users").empty(); $("#videoInfo .watchers").empty(); $("#videoInfo .watching").removeClass("shown"); $("#videoInfo").removeClass("shown"); if (!this.parent.isChannel(this.parent.conversation)) { return; } let channel = this.parent.channelForID(this.parent.conversation); let me = this.parent.userForID(this.parent.domain); let active = channel.video; let users = Object.keys(channel.videoUsers); let speakers = channel.videoSpeakers; let joined = users.includes(this.parent.domain); let watchers = channel.videoWatchers; let watching = channel.watching; if (active) { $("#videoInfo .title").addClass("shown"); $.each(users, (k, u) => { let avatar = this.avatar("div", u, this.parent.userForID(u).domain); $("#videoInfo .users").append(avatar); }); $.each(watchers, (k, u) => { if (!users.includes(u)) { let avatar = this.avatar("div", u, this.parent.userForID(u).domain); $("#videoInfo .watchers").append(avatar); } }); if ($("#videoInfo .watchers .avatar").length) { $("#videoInfo .watching").addClass("shown"); } this.updateAvatars(); $("#videoInfo").addClass("shown"); if (!watching) { $("#videoInfo .link[data-action=viewVideo]").addClass("shown"); } } if (this.isAdmin(channel, me)) { $("#videoInfo").addClass("shown"); if (active) { $("#videoInfo .link[data-action=startVideo]").removeClass("shown"); $("#videoInfo .link[data-action=endVideo]").addClass("shown"); if (!joined) { $("#videoInfo .link[data-action=joinVideo]").addClass("shown"); } } else { $("#videoInfo .link[data-action=startVideo]").addClass("shown"); } } else if (speakers.includes(me.id)) { $("#videoInfo").addClass("shown"); if (!joined) { $("#videoInfo .link[data-action=joinVideo]").addClass("shown"); } } let done = new Promise(resolve => { if (active) { if (joined || watching) { $("#videoInfo .link[data-action=leaveVideo]").addClass("shown"); if (joined) { $("#videoInfo .link[data-action=viewVideo]").removeClass("shown"); } } } else { $("#videoInfo .title").removeClass("shown"); } if (!watching && !joined) { $("#videoContainer").removeClass("shown"); } if (active && (joined || watching && users.length)) { if (!$("#videoContainer").hasClass("shown")) { $("#videoContainer").addClass("shown"); this.parent.stream.init().then(() => { this.updateVideoUsers(); resolve(); }); } else { this.updateVideoUsers(); resolve(); } } else { resolve(); } }); return await done; } muteAll() { this.setMute("toggleScreen", 1); this.setMute("toggleAudio", 1); this.setMute("toggleVideo", 1); } setMute(button, state) { if (state == 1) { $(`.controls .button[data-action=${button}]`).addClass("muted"); } else if (state == 0) { $(`.controls .button[data-action=${button}]`).removeClass("muted"); } else { $(`.controls .button[data-action=${button}]`).toggleClass("muted"); } } updateVideoUsers() { let users = this.parent.currentVideoUsers(); $.each(Object.keys(users), (k, u) => { this.parent.stream.addCam(u); let info = users[u]; if (info.video) { $(`.cam[data-id=${u}]`).removeClass("videoMuted"); } else { $(`.cam[data-id=${u}]`).addClass("videoMuted"); } if (info.audio) { $(`.cam[data-id=${u}]`).removeClass("audioMuted"); } else { $(`.cam[data-id=${u}]`).addClass("audioMuted"); } if (u == this.parent.domain && !this.parent.stream.sfu.webrtcStuff.myStream) { this.parent.stream.register(this.parent.stream.sfu); this.parent.stream.publish(); } }); $.each($(".cam"), (k, c) => { let id = $(c).data("id"); if (!Object.keys(users).includes(id)) { this.parent.stream.removeCam(id); } }); if (Object.keys(users).includes(this.parent.domain)) { $("#videoContainer").addClass("publishing"); } else { $("#videoContainer").removeClass("publishing"); } this.parent.stream.dish.resize(); this.parent.stream.dish.resize(); this.parent.stream.dish.resize(); } }