');
- linkEl.parentNode.insertBefore(img, linkEl.nextSibling);
- },
+ function ReactionsSelector(session) {
+ var available_reactions = [
+ "😂",
+ "🤣",
+ "🔥",
+ "👍",
+ "😭",
+ "🙏",
+ "❤️",
+ "😘",
+ "🥰",
+ "😍",
+ "😊",
+ ]
+
+ const rs = document.createElement("div");
+ rs.style.display = "none";
+ rs.classList.add("reactions_selector");
+ available_reactions.forEach(emoji => {
+ const em = rs.appendChild(document.createElement("em"));
+ em.textContent = emoji;
+ });
+ document.body.appendChild(rs);
- replaceAudio : function(linkEl)
- {
- var audio = chat.util.createHtmlNode('
');
- linkEl.parentNode.insertBefore(audio, linkEl.nextSibling);
- },
+ var selector = this;
+ rs.addEventListener("click", function (event) {
+ if (event.target.nodeName == "EM") {
+ session.react(selector.currentMessage, event.target.textContent);
+ event.target.parentNode.style.display = "none";
+ }
+ event.stopPropagation();
+ });
+ rs.addEventListener("mouseleave", function (event) {
+ rs.style.display = "none";
+ event.stopPropagation();
+ });
- replaceVideo : function(linkEl)
- {
- var audio = chat.util.createHtmlNode('
');
- linkEl.parentNode.insertBefore(audio, linkEl.nextSibling);
- },
+ this.show = function(messageId, nearEl, scrollEl) {
+ this.currentMessage = messageId;
+ const nbr = nearEl.getBoundingClientRect();
+ rs.style.top = (nbr.top + scrollEl.scrollTop) + "px";
+ rs.style.display = "flex";
+ const selectorRect = rs.getBoundingClientRect();
+ const scrollRect = scrollEl.getBoundingClientRect();
+ if (nbr.left + selectorRect.width > scrollRect.right) {
+ rs.style.left = (nbr.right - selectorRect.width) + "px";
+ } else {
+ rs.style.left = nbr.left + "px";
+ }
+ }
+ this.hide = function() {
+ rs.style.display = "none";
+ }
+ }
- replaceLinkAsync : function(linkEl)
- {
- chat.util.startSessionTransaction(function(tId) {
- session.getUrlHeaders(tId, linkEl.href);
- },function(result) {
- //chat.console("result ready " + chat.util.props(result, true));
- var ct = result['content-type'];
- if ((typeof(ct) == "string") && (ct != "application/octet-stream")) {
- ct = ct.split("/")[0].trim();
- switch (ct) {
- case "image":
- chat.util.replaceImage(linkEl);
- break;
- case "audio":
- chat.util.replaceAudio(linkEl);
- break;
- case "video":
- chat.util.replaceVideo(linkEl);
- break;
- }
- } else { // fallback when no content type
- //chat.console("fallback")
- var imageExts = ["png", "jpg", "jpeg", "gif", "webp"];
- var audioExts = ["mp3", "ogg", "aac", "flac", "wav", "m4a"];
- var videoExts = ["mp4", "webm", "mkv", "mov", "avi", "ogv"];
- var lpath = linkEl.pathname.toLowerCase().split('#')[0].split('?')[0];
- function checkExt(exts, replacer) {
- for (var i = 0; i < exts.length; i++) {
- if (lpath.slice(lpath.length - exts[i].length - 1) == ("." + exts[i])) {
- replacer(linkEl);
- break;
- }
+ function ContextMenu() {
+ this.items = [];
+ this.providers = [];
+
+ this.addItem = function(text, action) {
+ this.items.push({text: text, action: action});
+ };
+ this.addItemProvider = function(itemProvider) {
+ this.providers.push(itemProvider);
+ };
+ this.show = function(x, y, items) {
+ if (window.activeMenu) {
+ window.activeMenu.destroyMenu();
+ }
+ const menu = document.body.appendChild(document.createElement("div"));
+ menu.classList.add("context_menu");
+ return new Promise((resolve, reject) => {
+ for (let i = 0; i < items.length; i++) {
+ const item = menu.appendChild(document.createElement("div"));
+ item.textContent = items[i].text;
+ const action = items[i].action;
+ item.addEventListener("click", (event) => {
+ event.stopPropagation();
+ menu.destroyMenu();
+ try {
+ if (action instanceof Function) {
+ action();
}
+ } finally {
+ resolve(action);
}
- checkExt(imageExts, chat.util.replaceImage);
- checkExt(audioExts, chat.util.replaceAudio);
- checkExt(videoExts, chat.util.replaceVideo);
- }
- });
- },
-
- handleLinks : function(el)
- {
- if (!previewsEnabled)
- return;
- var links = el.querySelectorAll("a");
- var youtube = ["youtu.be", "www.youtube.com", "youtube.com", "m.youtube.com"];
- for (var li = 0; li < links.length; li++) {
- var linkEl = links[li];
- if (youtube.indexOf(linkEl.hostname) != -1) {
- chat.util.replaceYoutube(linkEl);
- } else if ((linkEl.protocol == "http:" || linkEl.protocol == "https:" || linkEl.protocol == "file:") && linkEl.hostname != "psi") {
- chat.util.replaceLinkAsync(linkEl);
- }
+ }, { "once": true });
}
- },
-
- handleShares : function(el) {
- var shares = el.querySelectorAll("share");
- for (var li = 0; li < shares.length; li++) {
- var share = shares[li];
- var info = ""; // TODO
- var source = share.getAttribute("id");
- var type = share.getAttribute("type");
- if (type.startsWith("audio")) {
- var hg = share.getAttribute("amplitudes");
- if (hg && hg.length)
- hg.split(",").forEach(v => { info += `
` });
- var playerFragment = chat.util.createHtmlNode(`
`);
- var player = playerFragment.firstChild;
- if (share.nextSibling)
- share.parentNode.insertBefore(playerFragment, share.nextSibling);
- else
- share.parentNode.appendChild(playerFragment);
- new AudioMessage(player);
- }
- else if (type.startsWith("image")) {
- let img = chat.util.createHtmlNode(`
`);
- if (share.nextSibling)
- share.parentNode.insertBefore(img, share.nextSibling);
- else
- share.parentNode.appendChild(img);
- }
+ menu.destroyMenu = () => {
+ document.body.removeEventListener("click", menu.destroyMenu);
+ menu.parentNode.removeChild(menu);
+ window.activeMenu = undefined;
+ reject();
+ };
+ document.body.addEventListener("click", menu.destroyMenu, { "once": true });
+
+ menu.style.top = y + "px";
+ menu.style.left = x + "px";
+ window.activeMenu = menu;
+ });
+ };
+
+ var menu = this;
+ document.addEventListener("contextmenu", function (event) {
+ var all_items = menu.items.slice();
+ try {
+ for (let i = 0; i < menu.providers.length; i++) {
+ all_items = all_items.concat(menu.providers[i](event));
}
- },
+ } catch(e) {
+ chat.console(e+"");
+ }
+ if (!all_items.length) {
+ return true;
+ }
- prepareContents : function(html, sender) {
- htmlSource.innerHTML = html;
- chat.util.replaceBob(htmlSource, sender);
- chat.util.handleLinks(htmlSource);
- chat.util.replaceIcons(htmlSource);
- chat.util.handleShares(htmlSource);
- },
+ event.stopPropagation();
+ event.preventDefault();
- appendHtml : function(dest, html, sender) {
- chat.util.prepareContents(html, sender);
- var firstAdded = htmlSource.firstChild;
- while (htmlSource.firstChild) dest.appendChild(htmlSource.firstChild);
- return firstAdded;
- },
+ var totalScrollY = 0;
+ var el = event.target;
+ while (el) {
+ if (el.scrollTop !== undefined) {
+ totalScrollY += el.scrollTop;
+ }
+ el = el.parentNode;
+ }
+ menu.show(event.x, event.y + totalScrollY, all_items).catch(()=>{});
+ });
+ }
- siblingHtml : function(dest, html, sourceUser) {
- chat.util.prepareContents(html, sourceUser);
- var firstAdded = htmlSource.firstChild;
- while (htmlSource.firstChild) dest.parentNode.insertBefore(htmlSource.firstChild, dest);
- return firstAdded;
- },
+ var util = {
+ console : server.console,
+ showCriticalError : function(text) {
+ var e=document.body || document.documentElement.appendChild(document.createElement("body"));
+ var er = e.appendChild(document.createElement("div"))
+ er.style.cssText = "background-color:red;color:white;border:1px solid black;padding:1em;margin:1em;font-weight:bold";
+ er.innerHTML = chat.util.escapeHtml(text).replace(/\n/, "
");
+ },
- ensureDeleted : function(id) {
- if (id) {
- var el = document.getElementById(id);
- if (el) {
- el.parentNode.removeChild(el);
- }
- }
- },
+ // just for debug
+ escapeHtml : function(html) {
+ html += ""; //hack
+ return html.split("&").join("&").split( "<").join("<").split(">").join(">");
+ },
- loadXML : function(path, callback) {
- function cb(text){
- if (!text) {
- throw new Error("File " + path + " is empty. can't parse xml");
- }
- var xml;
- try {
- xml = new DOMParser().parseFromString(text, "text/xml");
- } catch (e) {
- server.console("failed to parse xml from file " + path);
- throw e;
- }
- callback(xml);
+ // just for debug
+ props : function(e, rec) {
+ var ret='';
+ for (var i in e) {
+ var gotValue = true;
+ var val = null;
+ try {
+ val = e[i];
+ } catch(err) {
+ val = err.toString();
+ gotValue = false;
}
- if (chat.async) {
- //server.console("loading xml async: " + path);
- loader.getFileContents(path, cb);
+ if (gotValue) {
+ if (val instanceof Object && rec && val.constructor != Date) {
+ ret+=i+" = "+val.constructor.name+"{"+chat.util.props(val, rec)+"}\n";
+ } else {
+ if (val instanceof Function) {
+ ret+=i+" = Function: "+i+"\n";
+ } else {
+ ret+=i+" = "+(val === null?"null\n":val.constructor.name+"(\""+val+"\")\n");
+ }
+ }
} else {
- //server.console("loading xml sync: " + path);
- cb(loader.getFileContents(path));
+ ret+=i+" = [CAN'T GET VALUE: "+val+"]\n";
}
- },
-
- dateFormat : function(val, format) {
- return (new chat.DateTimeFormatter(format)).format(val);
- },
+ }
+ return ret;
+ },
- avatarForNick : function(nick) {
- var u = usersMap[nick];
- return u && u.avatar;
- },
+ startSessionTransaction: function(starter, finisher) {
+ var tId = "st" + (++nextServerTransaction);
+ serverTransctions[tId] = finisher;
+ starter(tId);
+ },
- nickColor : function(nick) {
- var u = usersMap[nick];
- return u && u.nickcolor || "#888";
- },
+ _remoteCallEval : function(func, args, cb) {
+ function ecb(val) { val = eval("[" + val + "][0]"); cb(val); }
- replaceableMessage : function(isMuc, isLocal, nick, msgId, text) {
- // if we have an id then this is a replacable message.
- // next weird logic is as follows:
- // - wrapping to some element may break elements flow
- // - using well known elements may break styles
- // - setting just starting mark is useless (we will never find the correct end)
- var uniqId;
- if (isMuc) {
- var u = usersMap[nick];
- if (!u) {
- return text;
- }
+ if (chat.async) {
+ args.push(ecb)
+ func.apply(this, args)
+ } else {
+ var val = func.apply(this, args);
+ ecb(val);
+ }
+ },
- uniqId = "pmr"+uniqReplId.toString(36); // pmr - psi message replace :-)
- //chat.console("Sender:"+nick);
- usersMap[nick].msgs[msgId] = uniqId;
- } else {
- var uId = isLocal?"l":"r";
- uniqId = "pmr"+uId+uniqReplId.toString(36);
- if (!usersMap[uId]) {
- usersMap[uId]={msgs:{}};
- }
- usersMap[uId].msgs[msgId] = uniqId;
- }
+ _remoteCall : function(func, args, cb) {
+ if (chat.async) {
+ args.push(cb)
+ func.apply(this, args)
+ } else {
+ var val = func.apply(this, args);
+ cb(val);
+ }
+ },
- uniqReplId++;
- // TODO better remember elements themselves instead of some id.
- return "
" + text + "
";
- },
+ psiOption : function(option, cb) { chat.util._remoteCallEval(server.psiOption, [option], cb); },
+ colorOption : function(option, cb) { chat.util._remoteCallEval(server.colorOption, [option], cb); },
+ getFont : function(cb) { chat.util._remoteCallEval(session.getFont, [], cb); },
+ getPaletteColor : function(name, cb) { chat.util._remoteCall(session.getPaletteColor, [name], cb); },
+ connectOptionChange: function(option, cb) {
+ if (typeof optionChangeHandlers[option] == 'undefined') {
+ optionChangeHandlers[option] = {value: undefined, handlers:[]};
+ }
+ optionChangeHandlers[option].handlers.push(cb);
+ },
+ rereadOptions: function() {
+ onOptionsChanged(Object.getOwnPropertyNames(optionChangeHandlers));
+ },
- replaceMessage : function(parentEl, isMuc, isLocal, nick, msgId, newId, text) {
- var u
- if (isMuc) {
- u = usersMap[nick];
- } else {
- u = usersMap[isLocal?"l":"r"];
- }
- //chat.console(isMuc + " " + isLocal + " " + nick + " " + msgId + " " + chat.util.props(u, true));
+ // replaces
+ // with
+ icon2img : function (obj) {
+ var img = document.createElement('img');
+ img.src = "/psi/icon/" + obj.getAttribute("name");
+ img.title = obj.getAttribute("text");
+ img.className = "psi-" + (obj.getAttribute("type") || "icon");
+ // ignore size attribute. it's up to css style how to size.
+ obj.parentNode.replaceChild(img, obj);
+ },
- var uniqId = u && u.msgs[msgId];
- if (!uniqId)
- return false; // replacing something we didn't use replaceableMessage for? hm.
+ // replaces all occurrence of
by function above
+ replaceIcons : function(el) {
+ var els = el.querySelectorAll("icon"); // frozen list
+ for (var i=0; i < els.length; i++) {
+ chat.util.icon2img(els[i]);
+ }
+ },
- var se =parentEl.querySelector("psims[mid='"+uniqId+"']");
- var ee =parentEl.querySelector("psime[mid='"+uniqId+"']");
-// chat.console("Replace: start: " + (se? "found, ":"not found, ") +
-// "end: " + (ee? "found, ":"not found, ") +
-// "parent match: " + ((se && ee && se.parentNode === ee.parentNode)?"yes":"no"));
- if (se && ee && se.parentNode === ee.parentNode) {
- delete u.msgs[msgId]; // no longer used. will be replaced with newId.
- while (se.nextSibling !== ee) {
- se.parentNode.removeChild(se.nextSibling);
+ replaceBob : function(el, sender) {
+ var els = el.querySelectorAll("img"); // frozen list
+ for (var i=0; i < els.length; i++) {
+ if (els[i].src.indexOf('cid:') == 0) {
+ els[i].src = "psibob/" + els[i].src.slice(4);
+ if (sender) {
+ els[i].src += "?sender=" + encodeURIComponent(sender);
}
- var node = chat.util.createHtmlNode(chat.util.replaceableMessage(isMuc, isLocal, nick, newId, text + ''));
- //chat.console(chat.util.props(node));
- chat.util.handleLinks(node);
- chat.util.replaceIcons(node);
- ee.parentNode.insertBefore(node, ee);
- se.parentNode.removeChild(se);
- ee.parentNode.removeChild(ee);
- return true;
}
- return false;
- },
-
- CSSString2CSSStyleSheet : function(css) {
- const style = document.createElement ( 'style' );
- style.innerText = css;
- document.head.appendChild ( style );
- const {sheet} = style;
- document.head.removeChild ( style );
- return sheet;
}
},
- WindowScroller : function(animate) {
- var o=this, state, timerId
- var ignoreNextScroll = false;
- o.animate = animate;
- o.atBottom = true; //just a state of aspiration
+ updateObject : function(object, update) {
+ for (var i in update) {
+ object[i] = update[i]
+ }
+ },
- var animationStep = function() {
- timerId = null;
- var before = document.body.clientHeight - (window.innerHeight+window.pageYOffset);
- var step = before;
- if (o.animate) {
- step = step>200?200:(step<8?step:Math.floor(step/1.7));
- }
- ignoreNextScroll = true;
- window.scrollTo(0, document.body.clientHeight - window.innerHeight - before + step);
- if (before>0) {
- timerId = setTimeout(animationStep, 70); //next step in 250ms even if we are already at bottom (control shot)
- }
+ findStyleSheet : function (sheet, selector) {
+ for (var i=0; i window.innerHeight) { //if we have what to scroll
- timerId = setTimeout(animationStep, 0);
+ createHtmlNode : function(html, context) {
+ var range = document.createRange();
+ range.selectNode(context || document.body);
+ return range.createContextualFragment(html);
+ },
+
+ replaceYoutube : function(linkEl) {
+ var baseLink = "https://www.youtube.com/embed/";
+ var link;
+
+ if (linkEl.hostname == "youtu.be") {
+ link = baseLink + linkEl.pathname.slice(1);
+ } else if (linkEl.pathname.indexOf("/embed/") != 0) {
+ var m = linkEl.href.match(/^.*[?&]v=([a-zA-Z0-9_-]+).*$/);
+ var code = m && m[1];
+ if (code) {
+ link = baseLink + code;
}
+ } else {
+ link = linkEl.href;
}
- var stopAnimation = function() {
- if (timerId) {
- clearTimeout(timerId);
- timerId = null;
- }
+ if (link) {
+ var iframe = chat.util.createHtmlNode('');
+ linkEl.parentNode.insertBefore(iframe, linkEl.nextSibling);
}
+ },
- // ensure we at bottom on window resize
- if (typeof ResizeObserver === 'undefined') {
-
- // next code is copied from www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/ on 7 Dec 2018
- (function(){
- var attachEvent = document.attachEvent;
- var isIE = navigator.userAgent.match(/Trident/);
- //console.log(isIE);
- var requestFrame = (function(){
- var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
- function(fn){ return window.setTimeout(fn, 20); };
- return function(fn){ return raf(fn); };
- })();
-
- var cancelFrame = (function(){
- var cancel = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame ||
- window.clearTimeout;
- return function(id){ return cancel(id); };
- })();
-
- function resizeListener(e){
- var win = e.target || e.srcElement;
- if (win.__resizeRAF__) cancelFrame(win.__resizeRAF__);
- win.__resizeRAF__ = requestFrame(function(){
- var trigger = win.__resizeTrigger__;
- trigger.__resizeListeners__.forEach(function(fn){
- fn.call(trigger, e);
- });
- });
- }
+ replaceImage : function(linkEl)
+ {
+ var img = chat.util.createHtmlNode('');
+ linkEl.parentNode.insertBefore(img, linkEl.nextSibling);
+ },
- function objectLoad(e){
- this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__;
- this.contentDocument.defaultView.addEventListener('resize', resizeListener);
- }
+ replaceAudio : function(linkEl)
+ {
+ var audio = chat.util.createHtmlNode('
');
+ linkEl.parentNode.insertBefore(audio, linkEl.nextSibling);
+ },
- window.addResizeListener = function(element, fn){
- if (!element.__resizeListeners__) {
- element.__resizeListeners__ = [];
- if (attachEvent) {
- element.__resizeTrigger__ = element;
- element.attachEvent('onresize', resizeListener);
- }
- else {
- if (getComputedStyle(element).position == 'static') element.style.position = 'relative';
- var obj = element.__resizeTrigger__ = document.createElement('object');
- obj.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
- obj.__resizeElement__ = element;
- obj.onload = objectLoad;
- obj.type = 'text/html';
- if (isIE) element.appendChild(obj);
- obj.data = 'about:blank';
- if (!isIE) element.appendChild(obj);
- }
+ replaceVideo : function(linkEl)
+ {
+ var audio = chat.util.createHtmlNode('
');
+ linkEl.parentNode.insertBefore(audio, linkEl.nextSibling);
+ },
+
+ replaceLinkAsync : function(linkEl)
+ {
+ chat.util.startSessionTransaction(function(tId) {
+ session.getUrlHeaders(tId, linkEl.href);
+ },function(result) {
+ //chat.console("result ready " + chat.util.props(result, true));
+ var ct = result['content-type'];
+ if ((typeof(ct) == "string") && (ct != "application/octet-stream")) {
+ ct = ct.split("/")[0].trim();
+ switch (ct) {
+ case "image":
+ chat.util.replaceImage(linkEl);
+ break;
+ case "audio":
+ chat.util.replaceAudio(linkEl);
+ break;
+ case "video":
+ chat.util.replaceVideo(linkEl);
+ break;
}
- element.__resizeListeners__.push(fn);
- };
-
- window.removeResizeListener = function(element, fn){
- element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
- if (!element.__resizeListeners__.length) {
- if (attachEvent) element.detachEvent('onresize', resizeListener);
- else {
- element.__resizeTrigger__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
- element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__);
- }
+ } else { // fallback when no content type
+ //chat.console("fallback")
+ var imageExts = ["png", "jpg", "jpeg", "gif", "webp"];
+ var audioExts = ["mp3", "ogg", "aac", "flac", "wav", "m4a"];
+ var videoExts = ["mp4", "webm", "mkv", "mov", "avi", "ogv"];
+ var lpath = linkEl.pathname.toLowerCase().split('#')[0].split('?')[0];
+ function checkExt(exts, replacer) {
+ for (var i = 0; i < exts.length; i++) {
+ if (lpath.slice(lpath.length - exts[i].length - 1) == ("." + exts[i])) {
+ replacer(linkEl);
+ break;
+ }
+ }
}
- }
- })();
- // end of copied code
- addResizeListener(document.body, function(){
- o.invalidate();
- });
- } else {
- const ro = new ResizeObserver(function(entries) {
- o.invalidate();
- });
-
- // Observe the scrollingElement for when the window gets resized
- ro.observe(document.scrollingElement);
- // Observe the timeline to process new messages
- // ro.observe(timeline);
+ checkExt(imageExts, chat.util.replaceImage);
+ checkExt(audioExts, chat.util.replaceAudio);
+ checkExt(videoExts, chat.util.replaceVideo);
+ }
+ });
+ },
+ handleLinks : function(el)
+ {
+ if (!previewsEnabled)
+ return;
+ var links = el.querySelectorAll("a");
+ var youtube = ["youtu.be", "www.youtube.com", "youtube.com", "m.youtube.com"];
+ for (var li = 0; li < links.length; li++) {
+ var linkEl = links[li];
+ if (youtube.indexOf(linkEl.hostname) != -1) {
+ chat.util.replaceYoutube(linkEl);
+ } else if ((linkEl.protocol == "http:" || linkEl.protocol == "https:" || linkEl.protocol == "file:") && linkEl.hostname != "psi") {
+ chat.util.replaceLinkAsync(linkEl);
+ }
}
+ },
- //let's consider scroll may happen only by user action
- window.addEventListener("scroll", function(){
- if (ignoreNextScroll) {
- ignoreNextScroll = false;
- return;
+ handleShares : function(el) {
+ var shares = el.querySelectorAll("share");
+ for (var li = 0; li < shares.length; li++) {
+ var share = shares[li];
+ var info = ""; // TODO
+ var source = share.getAttribute("id");
+ var type = share.getAttribute("type");
+ if (type.startsWith("audio")) {
+ var hg = share.getAttribute("amplitudes");
+ if (hg && hg.length)
+ hg.split(",").forEach(v => { info += `
` });
+ var playerFragment = chat.util.createHtmlNode(`
`);
+ var player = playerFragment.firstChild;
+ if (share.nextSibling)
+ share.parentNode.insertBefore(playerFragment, share.nextSibling);
+ else
+ share.parentNode.appendChild(playerFragment);
+ new AudioMessage(player);
}
- stopAnimation();
- o.atBottom = document.body.clientHeight == (window.innerHeight+window.pageYOffset);
- }, false);
-
- //EXTERNAL API
- // checks current state of scroll and wish and activates necessary actions
- o.invalidate = function() {
- if (o.atBottom) {
- startAnimation();
+ else if (type.startsWith("image")) {
+ let img = chat.util.createHtmlNode(`
`);
+ if (share.nextSibling)
+ share.parentNode.insertBefore(img, share.nextSibling);
+ else
+ share.parentNode.appendChild(img);
}
}
+ },
- o.force = function() {
- o.atBottom = true;
- o.invalidate();
- }
-
- o.cancel = stopAnimation; // stops any current in-progress autoscroll
+ prepareContents : function(html, sender) {
+ htmlSource.innerHTML = html;
+ chat.util.replaceBob(htmlSource, sender);
+ chat.util.handleLinks(htmlSource);
+ chat.util.replaceIcons(htmlSource);
+ chat.util.handleShares(htmlSource);
},
- ReactionsSelector : function(session) {
- var available_reactions = [
- "😂",
- "🤣",
- "🔥",
- "👍",
- "😭",
- "🙏",
- "❤️",
- "😘",
- "🥰",
- "😍",
- "😊",
- ]
-
- const rs = document.createElement("div");
- rs.style.display = "none";
- rs.classList.add("reactions_selector");
- available_reactions.forEach(emoji => {
- const em = rs.appendChild(document.createElement("em"));
- em.textContent = emoji;
- });
- document.body.appendChild(rs);
+ appendHtml : function(dest, html, sender) {
+ chat.util.prepareContents(html, sender);
+ var firstAdded = htmlSource.firstChild;
+ while (htmlSource.firstChild) dest.appendChild(htmlSource.firstChild);
+ return firstAdded;
+ },
- var selector = this;
- rs.addEventListener("click", function (event) {
- if (event.target.nodeName == "EM") {
- session.react(selector.currentMessage, event.target.textContent);
- event.target.parentNode.style.display = "none";
- }
- event.stopPropagation();
- });
- rs.addEventListener("mouseleave", function (event) {
- rs.style.display = "none";
- event.stopPropagation();
- });
+ siblingHtml : function(dest, html, sourceUser) {
+ chat.util.prepareContents(html, sourceUser);
+ var firstAdded = htmlSource.firstChild;
+ while (htmlSource.firstChild) dest.parentNode.insertBefore(htmlSource.firstChild, dest);
+ return firstAdded;
+ },
- this.show = function(messageId, nearEl, scrollEl) {
- this.currentMessage = messageId;
- const nbr = nearEl.getBoundingClientRect();
- rs.style.top = (nbr.top + scrollEl.scrollTop) + "px";
- rs.style.display = "flex";
- const selectorRect = rs.getBoundingClientRect();
- const scrollRect = scrollEl.getBoundingClientRect();
- if (nbr.left + selectorRect.width > scrollRect.right) {
- rs.style.left = (nbr.right - selectorRect.width) + "px";
- } else {
- rs.style.left = nbr.left + "px";
+ ensureDeleted : function(id) {
+ if (id) {
+ var el = document.getElementById(id);
+ if (el) {
+ el.parentNode.removeChild(el);
}
}
- this.hide = function() {
- rs.style.display = "none";
- }
},
- ContextMenu : function() {
- this.items = [];
- this.providers = [];
-
- this.addItem = function(text, action) {
- this.items.push({text: text, action: action});
- };
- this.addItemProvider = function(itemProvider) {
- this.providers.push(itemProvider);
- };
- this.show = function(x, y, items) {
- if (window.activeMenu) {
- window.activeMenu.destroyMenu();
+ loadXML : function(path, callback) {
+ function cb(text){
+ if (!text) {
+ throw new Error("File " + path + " is empty. can't parse xml");
}
- const menu = document.body.appendChild(document.createElement("div"));
- menu.classList.add("context_menu");
- return new Promise((resolve, reject) => {
- for (let i = 0; i < items.length; i++) {
- const item = menu.appendChild(document.createElement("div"));
- item.textContent = items[i].text;
- const action = items[i].action;
- item.addEventListener("click", (event) => {
- event.stopPropagation();
- menu.destroyMenu();
- try {
- if (action instanceof Function) {
- action();
- }
- } finally {
- resolve(action);
- }
- }, { "once": true });
- }
- menu.destroyMenu = () => {
- document.body.removeEventListener("click", menu.destroyMenu);
- menu.parentNode.removeChild(menu);
- window.activeMenu = undefined;
- reject();
- };
- document.body.addEventListener("click", menu.destroyMenu, { "once": true });
-
- menu.style.top = y + "px";
- menu.style.left = x + "px";
- window.activeMenu = menu;
- });
- };
-
- var menu = this;
- document.addEventListener("contextmenu", function (event) {
- var all_items = menu.items.slice();
+ var xml;
try {
- for (let i = 0; i < menu.providers.length; i++) {
- all_items = all_items.concat(menu.providers[i](event));
- }
- } catch(e) {
- chat.console(e+"");
- }
- if (!all_items.length) {
- return true;
+ xml = new DOMParser().parseFromString(text, "text/xml");
+ } catch (e) {
+ server.console("failed to parse xml from file " + path);
+ throw e;
}
+ callback(xml);
+ }
+ if (chat.async) {
+ //server.console("loading xml async: " + path);
+ loader.getFileContents(path, cb);
+ } else {
+ //server.console("loading xml sync: " + path);
+ cb(loader.getFileContents(path));
+ }
+ },
- event.stopPropagation();
- event.preventDefault();
+ dateFormat : function(val, format) {
+ return (new chat.DateTimeFormatter(format)).format(val);
+ },
- var totalScrollY = 0;
- var el = event.target;
- while (el) {
- if (el.scrollTop !== undefined) {
- totalScrollY += el.scrollTop;
- }
- el = el.parentNode;
- }
- menu.show(event.x, event.y + totalScrollY, all_items).catch(()=>{});
- });
+ avatarForNick : function(nick) {
+ var u = usersMap[nick];
+ return u && u.avatar;
},
- DateTimeFormatter : function(formatStr) {
- function convertToTr35(format)
- {
- var ret=""
- var i = 0;
- var m = {M: "mm", H: "HH", S: "ss", c: "EEEE', 'MMMM' 'd', 'yyyy' 'G",
- A: "EEEE", I: "hh", p: "a", Y: "yyyy"}; // if you want me, report it.
+ nickColor : function(nick) {
+ var u = usersMap[nick];
+ return u && u.nickcolor || "#888";
+ },
- var txtAcc = "";
- while (i < format.length) {
- var c;
- if (format[i] === "'" ||
- (format[i] === "%" && i < (format.length - 1) && (c = m[format[i+1]])))
- {
- if (txtAcc) {
- ret += "'" + txtAcc + "'";
- txtAcc = "";
- }
- if (format[i] === "'") {
- ret += "''";
- } else {
- ret += c;
- i++;
- }
- } else {
- txtAcc += format[i];
- }
- i++;
- }
- if (txtAcc) {
- ret += "'" + txtAcc + "'";
- txtAcc = "";
- }
- return ret;
- }
-
- function convertToMoment(format) {
- var inTxt = false;
- var i;
- var m = {j:"h"}; // sadly "j" is not supported
- var ret = "";
- for (i = 0; i < format.length; i++) {
- if (format[i] == "'") {
- ret += (inTxt? ']' : '[');
- inTxt = !inTxt;
- } else {
- var c;
- if (!inTxt && (c = m[format[i]])) {
- ret += c;
- } else {
- ret += format[i];
- }
- }
- }
- if (inTxt) {
- ret += "]";
+ replaceableMessage : function(isMuc, isLocal, nick, msgId, text) {
+ // if we have an id then this is a replacable message.
+ // next weird logic is as follows:
+ // - wrapping to some element may break elements flow
+ // - using well known elements may break styles
+ // - setting just starting mark is useless (we will never find the correct end)
+ var uniqId;
+ if (isMuc) {
+ var u = usersMap[nick];
+ if (!u) {
+ return text;
}
- ret = ret.replace("EEEE", "dddd");
- ret = ret.replace("EEE", "ddd");
-
- return ret;
+ uniqId = "pmr"+uniqReplId.toString(36); // pmr - psi message replace :-)
+ //chat.console("Sender:"+nick);
+ usersMap[nick].msgs[msgId] = uniqId;
+ } else {
+ var uId = isLocal?"l":"r";
+ uniqId = "pmr"+uId+uniqReplId.toString(36);
+ if (!usersMap[uId]) {
+ usersMap[uId]={msgs:{}};
+ }
+ usersMap[uId].msgs[msgId] = uniqId;
}
- formatStr = formatStr || "j:mm";
- if (formatStr.indexOf('%') !== -1) {
- formatStr = convertToTr35(formatStr);
+ uniqReplId++;
+ // TODO better remember elements themselves instead of some id.
+ return "
" + text + "
";
+ },
+
+ replaceMessage : function(parentEl, isMuc, isLocal, nick, msgId, newId, text) {
+ var u
+ if (isMuc) {
+ u = usersMap[nick];
+ } else {
+ u = usersMap[isLocal?"l":"r"];
}
+ //chat.console(isMuc + " " + isLocal + " " + nick + " " + msgId + " " + chat.util.props(u, true));
- formatStr = convertToMoment(formatStr);
+ var uniqId = u && u.msgs[msgId];
+ if (!uniqId)
+ return false; // replacing something we didn't use replaceableMessage for? hm.
- this.format = function(val) {
- if (val instanceof String) {
- val = Date.parse(val);
+ var se =parentEl.querySelector("psims[mid='"+uniqId+"']");
+ var ee =parentEl.querySelector("psime[mid='"+uniqId+"']");
+// chat.console("Replace: start: " + (se? "found, ":"not found, ") +
+// "end: " + (ee? "found, ":"not found, ") +
+// "parent match: " + ((se && ee && se.parentNode === ee.parentNode)?"yes":"no"));
+ if (se && ee && se.parentNode === ee.parentNode) {
+ delete u.msgs[msgId]; // no longer used. will be replaced with newId.
+ while (se.nextSibling !== ee) {
+ se.parentNode.removeChild(se.nextSibling);
}
- return moment(val).format(formatStr); // FIXME we could speedup it by keeping fomatter instances
+ var node = chat.util.createHtmlNode(chat.util.replaceableMessage(isMuc, isLocal, nick, newId, text + '
'));
+ //chat.console(chat.util.props(node));
+ chat.util.handleLinks(node);
+ chat.util.replaceIcons(node);
+ ee.parentNode.insertBefore(node, ee);
+ se.parentNode.removeChild(se);
+ ee.parentNode.removeChild(ee);
+ return true;
}
+ return false;
},
+ CSSString2CSSStyleSheet : function(css) {
+ const style = document.createElement ( 'style' );
+ style.innerText = css;
+ document.head.appendChild ( style );
+ const {sheet} = style;
+ document.head.removeChild ( style );
+ return sheet;
+ }
+ }
+
+ var chat = {
+ async : async,
+ console : server.console,
+ server : server,
+ session : session,
+ hooks: [],
+
+ util: util,
+ WindowScroller: WindowScroller,
+ LikeButton: LikeButton,
+ ReactionsSelector: ReactionsSelector,
+ ContextMenu: ContextMenu,
+ DateTimeFormatter : DateTimeFormatter,
AudioMessage : AudioMessage,
receiveObject : function(data) {