window.fossil.onPageLoad(function(){ const F = window.fossil, D = F.dom; const E1 = function(selector){ const e = document.querySelector(selector); if(!e) throw new Error("missing required DOM element: "+selector); return e; }; const isEntirelyInViewport = function(e) { const rect = e.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }; const overlapsElemView = function(e,v) { const r1 = e.getBoundingClientRect(), r2 = v.getBoundingClientRect(); if(r1.top<=r2.bottom && r1.top>=r2.top) return true; else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true; return false; }; const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank'); const iso8601ish = function(d){ return d.toISOString() .replace('T',' ').replace(/\.\d+/,'') .replace('Z', ' zulu'); }; const pad2 = (x)=>('0'+x).substr(-2); const localTimeString = function ff(d){ d || (d = new Date()); return [ d.getFullYear(),'-',pad2(d.getMonth()+1), '-',pad2(d.getDate()), ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), ':',pad2(d.getSeconds()) ].join(''); }; (function(){ let dbg = document.querySelector('#debugMsg'); if(dbg){ D.append(document.body,dbg); } })(); const GetFramingElements = function() { return document.querySelectorAll([ "body > header", "body > nav.mainmenu", "body > footer", "#debugMsg" ].join(',')); }; const ForceResizeKludge = (function(){ const elemsToCount = GetFramingElements(); const contentArea = E1('div.content'); const bcl = document.body.classList; const resized = function f(){ if(f.$disabled) return; const wh = window.innerHeight, com = bcl.contains('chat-only-mode'); var ht; var extra = 0; if(com){ ht = wh; }else{ elemsToCount.forEach((e)=>e ? extra += D.effectiveHeight(e) : false); ht = wh - extra; } f.chat.e.inputX.style.maxHeight = (ht/2)+"px"; ; contentArea.style.height = contentArea.style.maxHeight = [ "calc(", (ht>=100 ? ht : 100), "px", " - 0.65em",")" ].join(''); if(false){ console.debug("resized.",wh, extra, ht, window.getComputedStyle(contentArea).maxHeight, contentArea); console.debug("Set input max height to: ", f.chat.e.inputX.style.maxHeight); } }; resized.$disabled = true; window.addEventListener('resize', F.debounce(resized, 250), false); return resized; })(); fossil.FRK = ForceResizeKludge; const Chat = ForceResizeKludge.chat = (function(){ const cs = { verboseErrors: false, e:{ messageInjectPoint: E1('#message-inject-point'), pageTitle: E1('head title'), loadOlderToolbar: undefined, inputWrapper: E1("#chat-input-area"), inputElementWrapper: E1('#chat-input-line-wrapper'), fileSelectWrapper: E1('#chat-input-file-area'), viewMessages: E1('#chat-messages-wrapper'), btnSubmit: E1('#chat-button-submit'), btnAttach: E1('#chat-button-attach'), inputX: E1('#chat-input-field-x'), input1: E1('#chat-input-field-single'), inputM: E1('#chat-input-field-multi'), inputFile: E1('#chat-input-file'), contentDiv: E1('div.content'), viewConfig: E1('#chat-config'), viewPreview: E1('#chat-preview'), previewContent: E1('#chat-preview-content'), btnPreview: E1('#chat-button-preview'), views: document.querySelectorAll('.chat-view'), activeUserListWrapper: E1('#chat-user-list-wrapper'), activeUserList: E1('#chat-user-list') }, me: F.user.name, mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, mnMsg: undefined, pageIsActive: 'visible'===document.visibilityState, changesSincePageHidden: 0, notificationBubbleColor: 'white', totalMessageCount: 0, loadMessageCount: Math.abs(F.config.chat.initSize || 20), ajaxInflight: 0, usersLastSeen:{ }, filterState:{ activeUser: undefined, match: function(uname){ return this.activeUser===uname || !this.activeUser; } }, inputValue: function(){ const e = this.inputElement(); if(arguments.length){ if(e.isContentEditable) e.innerText = arguments[0]; else e.value = arguments[0]; return this; } return e.isContentEditable ? e.innerText : e.value; }, inputFocus: function(){ this.inputElement().focus(); return this; }, inputElement: function(){ return this.e.inputFields[this.e.inputFields.$currentIndex]; }, enableAjaxComponents: function(yes){ D[yes ? 'enable' : 'disable'](this.disableDuringAjax); return this; }, ajaxStart: function(){ if(1===++this.ajaxInflight){ this.enableAjaxComponents(false); } }, ajaxEnd: function(){ if(0===--this.ajaxInflight){ this.enableAjaxComponents(true); } }, disableDuringAjax: [ ], scheduleScrollOfMsg: function(eMsg){ if(1===+eMsg.dataset.hasImage){ eMsg.querySelector('img').addEventListener( 'load', ()=>(this.e.newestMessage || eMsg).scrollIntoView(false) ); }else{ eMsg.scrollIntoView(false); } return this; }, injectMessageElem: function f(e, atEnd){ const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, holder = this.e.viewMessages, prevMessage = this.e.newestMessage; if(!this.filterState.match(e.dataset.xfrom)){ e.classList.add('hidden'); } if(atEnd){ const fe = mip.nextElementSibling; if(fe) mip.parentNode.insertBefore(e, fe); else D.append(mip.parentNode, e); }else{ D.append(holder,e); this.e.newestMessage = e; } if(!atEnd && !this._isBatchLoading && e.dataset.xfrom!==this.me && (prevMessage ? !this.messageIsInView(prevMessage) : false)){ if(!f.btnDown){ f.btnDown = D.button("⇣⇣⇣"); f.btnDown.addEventListener('click',()=>this.scrollMessagesTo(1),false); } F.toast.message(f.btnDown," New message has arrived."); }else if(!this._isBatchLoading && e.dataset.xfrom===Chat.me){ this.scheduleScrollOfMsg(e); }else if(!this._isBatchLoading){ if(1===+e.dataset.hasImage){ e.querySelector('img').addEventListener('load',()=>e.scrollIntoView()); }else if(!prevMessage || (prevMessage && isEntirelyInViewport(prevMessage))){ e.scrollIntoView(false); } } }, isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'), chatOnlyMode: function f(yes){ if(undefined === f.elemsToToggle){ f.elemsToToggle = []; GetFramingElements().forEach((e)=>f.elemsToToggle.push(e)); } if(!arguments.length) yes = true; if(yes === this.isChatOnlyMode()) return this; if(yes){ D.addClass(f.elemsToToggle, 'hidden'); D.addClass(document.body, 'chat-only-mode'); document.body.scroll(0,document.body.height); }else{ D.removeClass(f.elemsToToggle, 'hidden'); D.removeClass(document.body, 'chat-only-mode'); } ForceResizeKludge(); return this; }, scrollMessagesTo: function(where){ if(where<0){ Chat.e.viewMessages.scrollTop = 0; }else if(where>0){ Chat.e.viewMessages.scrollTop = Chat.e.viewMessages.scrollHeight; }else if(Chat.e.newestMessage){ Chat.e.newestMessage.scrollIntoView(false); } }, toggleChatOnlyMode: function(){ return this.chatOnlyMode(!this.isChatOnlyMode()); }, messageIsInView: function(e){ return e ? overlapsElemView(e, this.e.viewMessages) : false; }, settings:{ get: (k,dflt)=>F.storage.get(k,dflt), getBool: (k,dflt)=>F.storage.getBool(k,dflt), set: function(k,v){ F.storage.set(k,v); F.page.dispatchEvent('chat-setting',{key: k, value: v}); }, toggle: function(k){ const v = this.getBool(k); this.set(k, !v); return !v; }, addListener: function(setting, f){ F.page.addEventListener('chat-setting', function(ev){ if(ev.detail.key===setting) f(ev.detail); }, false); }, defaults:{ "images-inline": !!F.config.chat.imagesInline, "edit-ctrl-send": false, "edit-compact-mode": true, "edit-shift-enter-preview": F.storage.getBool('edit-shift-enter-preview', true), "monospace-messages": false, "chat-only-mode": false, "audible-alert": true, "active-user-list": false, "active-user-list-timestamps": false, "alert-own-messages": false, "edit-widget-x": false } }, playNewMessageSound: function f(){ if(f.uri){ try{ if(!f.audio) f.audio = new Audio(f.uri); f.audio.currentTime = 0; f.audio.play(); }catch(e){ console.error("Audio playblack failed.", f.uri, e); } } return this; }, setNewMessageSound: function f(uri){ delete this.playNewMessageSound.audio; this.playNewMessageSound.uri = uri; this.settings.set('audible-alert', uri); return this; }, setCurrentView: function(e){ if(e===this.e.currentView){ return e; } this.e.views.forEach(function(E){ if(e!==E) D.addClass(E,'hidden'); }); this.e.currentView = e; if(this.e.currentView.$beforeShow) this.e.currentView.$beforeShow(); D.removeClass(e,'hidden'); this.animate(this.e.currentView, 'anim-fade-in-fast'); return this.e.currentView; }, updateActiveUserList: function callee(){ if(this._isBatchLoading || this.e.activeUserListWrapper.classList.contains('hidden')){ return this; }else if(!callee.sortUsersSeen){ const self = this; callee.sortUsersSeen = function(l,r){ l = self.usersLastSeen[l]; r = self.usersLastSeen[r]; if(l && r) return r - l; else if(l) return -1; else if(r) return 1; else return 0; }; callee.addUserElem = function(u){ const uSpan = D.addClass(D.span(), 'chat-user'); const uDate = self.usersLastSeen[u]; if(self.filterState.activeUser===u){ uSpan.classList.add('selected'); } uSpan.dataset.uname = u; D.append(uSpan, u, "\n", D.append( D.addClass(D.span(),'timestamp'), localTimeString(uDate) )); if(uDate.$uColor){ uSpan.style.backgroundColor = uDate.$uColor; } D.append(self.e.activeUserList, uSpan); }; } D.remove(this.e.activeUserList.querySelectorAll('.chat-user')); Object.keys(this.usersLastSeen).sort( callee.sortUsersSeen ).forEach(callee.addUserElem); return this; }, showActiveUserList: function(yes){ if(0===arguments.length) yes = true; this.e.activeUserListWrapper.classList[ yes ? 'remove' : 'add' ]('hidden'); D.removeClass(Chat.e.activeUserListWrapper, 'collapsed'); if(Chat.e.activeUserListWrapper.classList.contains('hidden')){ Chat.setUserFilter(false); Chat.scrollMessagesTo(1); }else{ Chat.updateActiveUserList(); Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v'); } return this; }, showActiveUserTimestamps: function(yes){ if(0===arguments.length) yes = true; this.e.activeUserList.classList[yes ? 'add' : 'remove']('timestamps'); return this; }, setUserFilter: function(uname){ this.filterState.activeUser = uname; const mw = this.e.viewMessages.querySelectorAll('.message-widget'); const self = this; let eLast; if(!uname){ D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden'); }else{ mw.forEach(function(w){ if(self.filterState.match(w.dataset.xfrom)){ w.classList.remove('hidden'); eLast = w; }else{ w.classList.add('hidden'); } }); } if(eLast) eLast.scrollIntoView(false); else this.scrollMessagesTo(1); cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){ e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected'); }); return this; }, animate: function f(e,a,cb){ if(!f.$disabled){ D.addClassBriefly(e, a, 0, cb); } return this; } }; cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; cs.e.inputFields.$currentIndex = 0; cs.e.inputFields.forEach(function(e,ndx){ if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); else D.addClass(e,'hidden'); }); if(D.attr(cs.e.inputX,'contenteditable','plaintext-only').isContentEditable){ cs.$browserHasPlaintextOnly = true; }else{ cs.$browserHasPlaintextOnly = false; D.attr(cs.e.inputX,'contenteditable','true'); } cs.animate.$disabled = true; F.fetch.beforesend = ()=>cs.ajaxStart(); F.fetch.aftersend = ()=>cs.ajaxEnd(); cs.pageTitleOrig = cs.e.pageTitle.innerText; const qs = (e)=>document.querySelector(e); const argsToArray = function(args){ return Array.prototype.slice.call(args,0); }; cs.reportError = function(){ const args = argsToArray(arguments); console.error("chat error:",args); F.toast.error.apply(F.toast, args); }; cs.reportErrorAsMessage = function f(){ if(undefined === f.$msgid) f.$msgid=0; const args = argsToArray(arguments).map(function(v){ return (v instanceof Error) ? v.message : v; }); console.error("chat error:",args); const d = new Date().toISOString(), mw = new this.MessageWidget({ isError: true, xfrom: null, msgid: "error-"+(++f.$msgid), mtime: d, lmtime: d, xmsg: args }); this.injectMessageElem(mw.e.body); mw.scrollIntoView(); }; cs.getMessageElemById = function(id){ return qs('[data-msgid="'+id+'"]'); }; cs.fetchLastMessageElem = function(){ const msgs = document.querySelectorAll('.message-widget'); var rc; if(msgs.length){ rc = this.e.newestMessage = msgs[msgs.length-1]; } return rc; }; cs.deleteMessageElem = function(id){ var e; if(id instanceof HTMLElement){ e = id; id = e.dataset.msgid; }else{ e = this.getMessageElemById(id); } if(e && id){ D.remove(e); if(e===this.e.newestMessage){ this.fetchLastMessageElem(); } F.toast.message("Deleted message "+id+"."); } return !!e; }; cs.toggleTextMode = function(id){ var e; if(id instanceof HTMLElement){ e = id; id = e.dataset.msgid; }else{ e = this.getMessageElemById(id); } if(!e || !id) return false; else if(e.$isToggling) return; e.$isToggling = true; const content = e.querySelector('.content-target'); if(!content){ console.warn("Should not be possible: trying to toggle text", "mode of a message with no .content-target.", e); return; } if(!content.$elems){ content.$elems = [ content.firstElementChild, undefined ]; }else if(content.$elems[1]){ const child = ( content.firstElementChild===content.$elems[0] ? content.$elems[1] : content.$elems[0] ); D.clearElement(content); if(child===content.$elems[1]){ const cpId = 'copy-to-clipboard-'+id; const btnCp = D.attr(D.addClass(D.span(),'copy-button'), 'id', cpId); F.copyButton(btnCp, {extractText: ()=>child._xmsgRaw}); const lblCp = D.label(cpId, "Copy unformatted text"); lblCp.addEventListener('click',()=>btnCp.click(), false); D.append(content, D.append(D.addClass(D.span(), 'nobr'), btnCp, lblCp)); } delete e.$isToggling; D.append(content, child); return; } const self = this; F.fetch('chat-fetch-one',{ urlParams:{ name: id, raw: true}, responseType: 'json', onload: function(msg){ content.$elems[1] = D.append(D.pre(),msg.xmsg); content.$elems[1]._xmsgRaw = msg.xmsg; self.toggleTextMode(e); }, aftersend:function(){ delete e.$isToggling; Chat.ajaxEnd(); } }); return true; }; cs.userMayDelete = function(eMsg){ return +eMsg.dataset.msgid>0 && (this.me === eMsg.dataset.xfrom || F.user.isAdmin); }; cs._newResponseError = function(response){ return new Error([ "HTTP status ", response.status,": ",response.url,": ", response.statusText].join('')); }; cs._fetchJsonOrError = function(response){ if(response.ok) return response.json(); else throw cs._newResponseError(response); }; cs.deleteMessage = function(id){ var e; if(id instanceof HTMLElement){ e = id; id = e.dataset.msgid; }else{ e = this.getMessageElemById(id); } if(!(e instanceof HTMLElement)) return; if(this.userMayDelete(e)){ this.ajaxStart(); F.fetch("chat-delete/" + id, { responseType: 'json', onload:(r)=>this.deleteMessageElem(r), onerror:(err)=>this.reportErrorAsMessage(err) }); }else{ this.deleteMessageElem(id); } }; document.addEventListener('visibilitychange', function(ev){ cs.pageIsActive = ('visible' === document.visibilityState); if(cs.pageIsActive){ cs.e.pageTitle.innerText = cs.pageTitleOrig; if(document.activeElement!==cs.inputElement()){ setTimeout(()=>cs.inputFocus(), 0); } } }, true); cs.setCurrentView(cs.e.viewMessages); cs.e.activeUserList.addEventListener('click', function f(ev){ ev.stopPropagation(); ev.preventDefault(); let eUser = ev.target; while(eUser!==this && !eUser.classList.contains('chat-user')){ eUser = eUser.parentNode; } if(eUser==this || !eUser) return false; const uname = eUser.dataset.uname; let eLast; cs.setCurrentView(cs.e.viewMessages); if(eUser.classList.contains('selected')){ eUser.classList.remove('selected'); cs.setUserFilter(false); delete f.$eSelected; }else{ if(f.$eSelected) f.$eSelected.classList.remove('selected'); f.$eSelected = eUser; eUser.classList.add('selected'); cs.setUserFilter(uname); } return false; }, false); return cs; })(); const findMessageWidgetParent = function(e){ while( e && !e.classList.contains('message-widget')){ e = e.parentNode; } return e; }; Chat.MessageWidget = (function(){ const cf = function(){ this.e = { body: D.addClass(D.div(), 'message-widget'), tab: D.addClass(D.div(), 'message-widget-tab'), content: D.addClass(D.div(), 'message-widget-content') }; D.append(this.e.body, this.e.tab, this.e.content); this.e.tab.setAttribute('role', 'button'); if(arguments.length){ this.setMessage(arguments[0]); } }; const dowMap = { 0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday" }; const theTime = function(d){ return [ d.getHours(),":", (d.getMinutes()+100).toString().slice(1,3), ' ', dowMap[d.getDay()] ].join(''); }; const canEmbedFile = function f(msg){ if(!f.$rx){ f.$rx = /\.((html?)|(txt)|(md)|(wiki)|(pikchr))$/i; f.$specificTypes = [ 'text/plain', 'text/html', 'text/x-markdown', 'text/markdown', 'text/x-pikchr', 'text/x-fossil-wiki' ]; } if(msg.fmime){ if(msg.fmime.startsWith("image/") || f.$specificTypes.indexOf(msg.fmime)>=0){ return true; } } return (msg.fname && f.$rx.test(msg.fname)); }; const shouldWikiRenderEmbed = function f(msg){ if(!f.$rx){ f.$rx = /\.((md)|(wiki)|(pikchr))$/i; f.$specificTypes = [ 'text/x-markdown', 'text/markdown', 'text/x-pikchr', 'text/x-fossil-wiki' ]; } if(msg.fmime){ if(f.$specificTypes.indexOf(msg.fmime)>=0) return true; } return msg.fname && f.$rx.test(msg.fname); }; const adjustIFrameSize = function(msgObj){ const iframe = msgObj.e.iframe; const body = iframe.contentWindow.document.querySelector('body'); if(body && !body.style.fontSize){ body.style.fontSize = window.getComputedStyle(msgObj.e.content); } if('' === iframe.style.maxHeight){ const isHidden = iframe.classList.contains('hidden'); if(isHidden) D.removeClass(iframe, 'hidden'); iframe.style.maxHeight = iframe.style.height = iframe.contentWindow.document.documentElement.scrollHeight + 'px'; if(isHidden) D.addClass(iframe, 'hidden'); } }; cf.prototype = { scrollIntoView: function(){ this.e.content.scrollIntoView(); }, setMessage: function(m){ const ds = this.e.body.dataset; ds.timestamp = m.mtime; ds.lmtime = m.lmtime; ds.msgid = m.msgid; ds.xfrom = m.xfrom || ''; if(m.xfrom === Chat.me){ D.addClass(this.e.body, 'mine'); } if(m.uclr){ this.e.content.style.backgroundColor = m.uclr; this.e.tab.style.backgroundColor = m.uclr; } const d = new Date(m.mtime); D.clearElement(this.e.tab); var contentTarget = this.e.content; var eXFrom; if(m.xfrom){ eXFrom = D.append(D.addClass(D.span(), 'xfrom'), m.xfrom); const wrapper = D.append( D.span(), eXFrom, D.text(" #",(m.msgid||'???'),' @ ',theTime(d))) D.append(this.e.tab, wrapper); }else{ D.addClass(this.e.body, 'notification'); if(m.isError){ D.addClass([contentTarget, this.e.tab], 'error'); } D.append( this.e.tab, D.append(D.code(), 'notification @ ',theTime(d)) ); } if( m.xfrom && m.fsize>0 ){ if( m.fmime && m.fmime.startsWith("image/") && Chat.settings.getBool('images-inline',true) ){ const extension = m.fname.split('.').pop(); contentTarget.appendChild(D.img("chat-download/" + m.msgid +( extension ? ('.'+extension) : '' ))); ds.hasImage = 1; }else{ const downloadUri = window.fossil.rootPath+ 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname); const w = D.addClass(D.div(), 'attachment-link'); const a = D.a(downloadUri, "(" + m.fname + " " + m.fsize + " bytes)" ) D.attr(a,'target','_blank'); D.append(w, a); if(canEmbedFile(m)){ const shouldWikiRender = shouldWikiRenderEmbed(m); const downloadArgs = shouldWikiRender ? '?render' : ''; D.addClass(contentTarget, 'wide'); const embedTarget = this.e.content; const self = this; const btnEmbed = D.attr(D.checkbox("1", false), 'id', 'embed-'+ds.msgid); const btnLabel = D.label(btnEmbed, shouldWikiRender ? "Embed (fossil-rendered)" : "Embed"); btnEmbed.addEventListener('change',function(){ if(self.e.iframe){ if(btnEmbed.checked){ D.removeClass(self.e.iframe, 'hidden'); if(self.e.$iframeLoaded) adjustIFrameSize(self); } else D.addClass(self.e.iframe, 'hidden'); return; } const iframe = self.e.iframe = document.createElement('iframe'); D.append(embedTarget, iframe); iframe.addEventListener('load', function(){ self.e.$iframeLoaded = true; adjustIFrameSize(self); }); iframe.setAttribute('src', downloadUri + downloadArgs); }); D.append(w, btnEmbed, btnLabel); } contentTarget.appendChild(w); } } if(m.xmsg){ if(m.fsize>0){ contentTarget = D.div(); D.append(this.e.content, contentTarget); } D.addClass(contentTarget, 'content-target' ); if(m.xmsg && 'string' !== typeof m.xmsg){ D.append(contentTarget, m.xmsg); }else{ contentTarget.innerHTML = m.xmsg; contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank); if(F.pikchr){ F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr')); } } } this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false); return this; }, _handleLegendClicked: function f(ev){ if(!f.popup){ f.popup = { e: D.addClass(D.div(), 'chat-message-popup'), refresh:function(){ const eMsg = this.$eMsg; if(!eMsg) return; D.clearElement(this.e); const d = new Date(eMsg.dataset.timestamp); if(d.getMinutes().toString()!=="NaN"){ const xfrom = eMsg.dataset.xfrom || 'server'; D.append(this.e, D.append(D.span(), localTimeString(d)," ",Chat.me," time"), D.append(D.span(), iso8601ish(d))); if(eMsg.dataset.lmtime && xfrom!==Chat.me){ D.append(this.e, D.append(D.span(), localTime8601( new Date(eMsg.dataset.lmtime) ).replace('T',' ')," ",xfrom," time")); } }else{ D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," zulu")); } const toolbar = D.addClass(D.div(), 'toolbar'); D.append(this.e, toolbar); const btnDeleteLocal = D.button("Delete locally"); D.append(toolbar, btnDeleteLocal); const self = this; btnDeleteLocal.addEventListener('click', function(){ self.hide(); Chat.deleteMessageElem(eMsg); }); if(Chat.userMayDelete(eMsg)){ const btnDeleteGlobal = D.button("Delete globally"); D.append(toolbar, btnDeleteGlobal); F.confirmer(btnDeleteGlobal,{ pinSize: true, ticks: F.config.confirmerButtonTicks, confirmText: "Confirm delete?", onconfirm:function(){ self.hide(); Chat.deleteMessage(eMsg); } }); } const toolbar3 = D.addClass(D.div(), 'toolbar'); D.append(this.e, toolbar3); D.append(toolbar3, D.button( "Locally remove all previous messages", function(){ self.hide(); Chat.mnMsg = +eMsg.dataset.msgid; var e = eMsg.previousElementSibling; while(e && e.classList.contains('message-widget')){ const n = e.previousElementSibling; D.remove(e); e = n; } eMsg.scrollIntoView(); } )); const toolbar2 = D.addClass(D.div(), 'toolbar'); D.append(this.e, toolbar2); if(eMsg.querySelector('.content-target')){ D.append(toolbar2, D.button( "Toggle text mode", function(){ self.hide(); Chat.toggleTextMode(eMsg); })); } if(eMsg.dataset.xfrom){ const timelineLink = D.attr( D.a(F.repoUrl('timeline',{ u: eMsg.dataset.xfrom, y: 'a' }), "User's Timeline"), 'target', '_blank' ); D.append(toolbar2, timelineLink); if(Chat.filterState.activeUser && Chat.filterState.match(eMsg.dataset.xfrom)){ D.append( this.e, D.append( D.addClass(D.div(), 'toolbar'), D.button( "Message in context", function(){ self.hide(); Chat.setUserFilter(false); eMsg.scrollIntoView(false); Chat.animate( eMsg.firstElementChild, 'anim-flip-h' ); }) ) ); } } const tab = eMsg.querySelector('.message-widget-tab'); D.append(tab, this.e); D.removeClass(this.e, 'hidden'); Chat.animate(this.e, 'anim-fade-in-fast'); }, hide: function(){ delete this.$eMsg; D.addClass(this.e, 'hidden'); D.clearElement(this.e); }, show: function(tgtMsg){ if(tgtMsg === this.$eMsg){ this.hide(); return; } this.$eMsg = tgtMsg; this.refresh(); } }; } const theMsg = findMessageWidgetParent(ev.target); if(theMsg) f.popup.show(theMsg); } }; return cf; })(); const BlobXferState = (function(){ const bxs = { dropDetails: document.querySelector('#chat-drop-details'), blob: undefined, clear: function(){ this.blob = undefined; D.clearElement(this.dropDetails); Chat.e.inputFile.value = ""; } }; const updateDropZoneContent = bxs.updateDropZoneContent = function(blob){ const dd = bxs.dropDetails; bxs.blob = blob; D.clearElement(dd); if(!blob){ Chat.e.inputFile.value = ''; return; } D.append(dd, "Attached: ", blob.name, D.br(), "Size: ",blob.size); const btn = D.button("Cancel"); D.append(dd, D.br(), btn); btn.addEventListener('click', ()=>updateDropZoneContent(), false); if(blob.type && (blob.type.startsWith("image/") || blob.type==='BITMAP')){ const img = D.img(); D.append(dd, D.br(), img); const reader = new FileReader(); reader.onload = (e)=>img.setAttribute('src', e.target.result); reader.readAsDataURL(blob); } }; Chat.e.inputFile.addEventListener('change', function(ev){ updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined) }); const pasteListener = function(event){ const items = event.clipboardData.items, item = items[0]; if(item && item.type && ('file'===item.kind || 'BITMAP'===item.type)){ updateDropZoneContent(false); updateDropZoneContent(item.getAsFile()); event.stopPropagation(); event.preventDefault(true); return false; } }; document.addEventListener('paste', pasteListener, true); if(window.Selection && window.Range && !Chat.$browserHasPlaintextOnly){ Chat.e.inputX.addEventListener( 'paste', function(ev){ if (ev.clipboardData && ev.clipboardData.getData) { const pastedText = ev.clipboardData.getData('text/plain'); const selection = window.getSelection(); if (!selection.rangeCount) return false; selection.deleteFromDocument(); selection.getRangeAt(0).insertNode(document.createTextNode(pastedText)); selection.collapseToEnd(); ev.preventDefault(); return false; } }, false); } const noDragDropEvents = function(ev){ ev.dataTransfer.effectAllowed = 'none'; ev.dataTransfer.dropEffect = 'none'; ev.preventDefault(); ev.stopPropagation(); return false; }; ['drop','dragenter','dragleave','dragend'].forEach( (k)=>Chat.e.inputX.addEventListener(k, noDragDropEvents, false) ); return bxs; })(); const tzOffsetToString = function(off){ const hours = Math.round(off/60), min = Math.round(off % 30); return ''+(hours + (min ? '.5' : '')); }; const localTime8601 = function(d){ return [ d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()), 'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds()) ].join(''); }; const recoverFailedMessage = function(state){ const w = D.addClass(D.div(), 'failed-message'); D.append(w, D.append( D.span(),"This message was not successfully sent to the server:" )); if(state.msg){ const ta = D.textarea(); ta.value = state.msg; D.append(w,ta); } if(state.blob){ D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed"))); } const buttons = D.addClass(D.div(), 'buttons'); D.append(w, buttons); D.append(buttons, D.button("Discard message?", function(){ const theMsg = findMessageWidgetParent(w); if(theMsg) Chat.deleteMessageElem(theMsg); })); D.append(buttons, D.button("Edit message and try again?", function(){ if(state.msg) Chat.inputValue(state.msg); if(state.blob) BlobXferState.updateDropZoneContent(state.blob); const theMsg = findMessageWidgetParent(w); if(theMsg) Chat.deleteMessageElem(theMsg); })); Chat.reportErrorAsMessage(w); }; Chat.submitMessage = function f(){ if(!f.spaces){ f.spaces = /\s+$/; f.markdownContinuation = /\\\s+$/; f.spaces2 = /\s{3,}$/; } this.setCurrentView(this.e.viewMessages); const fd = new FormData(); const fallback = {msg: this.inputValue()}; var msg = fallback.msg; if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ const xmsg = msg.split('\n'); xmsg.forEach(function(line,ndx){ if(!f.markdownContinuation.test(line)){ xmsg[ndx] = line.replace(f.spaces2, ' '); } }); msg = xmsg.join('\n'); } if(msg) fd.set('msg',msg); const file = BlobXferState.blob || this.e.inputFile.files[0]; if(file) fd.set("file", file); if( !msg && !file ) return; fallback.blob = file; const self = this; fd.set("lmtime", localTime8601(new Date())); F.fetch("chat-send",{ payload: fd, responseType: 'text', onerror:function(err){ self.reportErrorAsMessage(err); recoverFailedMessage(fallback); }, onload:function(txt){ if(!txt) return; try{ const json = JSON.parse(txt); self.newContent({msgs:[json]}); }catch(e){ self.reportError(e); } recoverFailedMessage(fallback); } }); BlobXferState.clear(); Chat.inputValue("").inputFocus(); }; const inputWidgetKeydown = function f(ev){ if(!f.$toggleCtrl){ f.$toggleCtrl = function(currentMode){ currentMode = !currentMode; Chat.settings.set('edit-ctrl-send', currentMode); }; f.$toggleCompact = function(currentMode){ currentMode = !currentMode; Chat.settings.set('edit-compact-mode', currentMode); }; } if(13 !== ev.keyCode) return; const text = Chat.inputValue().trim(); const ctrlMode = Chat.settings.getBool('edit-ctrl-send', false); if(ev.shiftKey){ const compactMode = Chat.settings.getBool('edit-compact-mode', false); ev.preventDefault(); ev.stopPropagation(); if(Chat.e.currentView===Chat.e.viewPreview && !text){ Chat.setCurrentView(Chat.e.viewMessages); }else if(!text){ f.$toggleCompact(compactMode); }else if(Chat.settings.getBool('edit-shift-enter-preview', true)){ Chat.e.btnPreview.click(); } return false; } if(ev.ctrlKey && !text && !BlobXferState.blob){ ev.preventDefault(); ev.stopPropagation(); f.$toggleCtrl(ctrlMode); return false; } if(!ctrlMode && ev.ctrlKey && text){ } if((!ctrlMode && !ev.ctrlKey) || (ev.ctrlKey)){ ev.preventDefault(); ev.stopPropagation(); Chat.submitMessage(); return false; } }; Chat.e.inputFields.forEach( (e)=>e.addEventListener('keydown', inputWidgetKeydown, false) ); Chat.e.btnSubmit.addEventListener('click',(e)=>{ e.preventDefault(); Chat.submitMessage(); return false; }); Chat.e.btnAttach.addEventListener( 'click', ()=>Chat.e.inputFile.click(), false); (function(){ if(window.innerWidth!document.body.classList.contains('my-messages-right'), callback: function f(){ document.body.classList[ this.checkbox.checked ? 'remove' : 'add' ]('my-messages-right'); } },{ label: "Monospace message font", hint: "Use monospace font for message and input text.", boolValue: 'monospace-messages', callback: function(setting){ document.body.classList[ setting.value ? 'add' : 'remove' ]('monospace-messages'); } },{ label: "Show images inline", hint: "When enabled, attached images are shown inline, "+ "else they appear as a download link.", boolValue: 'images-inline' }] }]; if(1){ const selectSound = D.select(); D.option(selectSound, "", "(no audio)"); const firstSoundIndex = selectSound.options.length; F.config.chat.alerts.forEach((a)=>D.option(selectSound, a)); if(true===Chat.settings.getBool('audible-alert')){ selectSound.selectedIndex = firstSoundIndex; }else{ selectSound.value = Chat.settings.get('audible-alert',''); if(selectSound.selectedIndex<0){ selectSound.selectedIndex = firstSoundIndex; } } Chat.setNewMessageSound(selectSound.value); settingsOps.push({ label: "Sound Options...", hint: "How to enable audio playback is browser-specific!", children:[{ hint: "Audio alert", select: selectSound, callback: function(ev){ const v = ev.target.value; Chat.setNewMessageSound(v); F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); } },{ label: "Play notification for your own messages", hint: "When enabled, the audio notification will be played for all messages, "+ "including your own. When disabled only messages from other users "+ "will trigger a notification.", boolValue: 'alert-own-messages' }] }); } settingsOps.push({ label: "Active User List...", hint: [ "/chat cannot track active connections, but it can tell ", "you who has posted recently..."].join(''), children:[ namedOptions.activeUsers,{ label: "Timestamps in active users list", indent: true, hint: "Show most recent message timestamps in the active user list.", boolValue: 'active-user-list-timestamps' } ] }); settingsOps.forEach(function f(op,indentOrIndex){ const menuEntry = D.addClass(D.div(), 'menu-entry'); if(true===indentOrIndex) D.addClass(menuEntry, 'child'); const label = op.label ? D.append(D.label(),op.label) : undefined; const labelWrapper = D.addClass(D.div(), 'label-wrapper'); var hint; if(op.hint){ hint = D.append(D.addClass(D.label(),'hint'),op.hint); } if(op.hasOwnProperty('select')){ const col0 = D.addClass(D.span(), 'toggle-wrapper'); D.append(menuEntry, labelWrapper, col0); D.append(labelWrapper, op.select); if(hint) D.append(labelWrapper, hint); if(label) D.append(label); if(op.callback){ op.select.addEventListener('change', (ev)=>op.callback(ev), false); } }else if(op.hasOwnProperty('boolValue')){ if(undefined === f.$id) f.$id = 0; ++f.$id; if('string' ===typeof op.boolValue){ const key = op.boolValue; op.boolValue = ()=>Chat.settings.getBool(key); op.persistentSetting = key; } const check = op.checkbox = D.attr(D.checkbox(1, op.boolValue()), 'aria-label', op.label); const id = 'cfgopt'+f.$id; const col0 = D.addClass(D.span(), 'toggle-wrapper'); check.checked = op.boolValue(); op.checkbox = check; D.attr(check, 'id', id); if(hint) D.attr(hint, 'for', id); D.append(menuEntry, labelWrapper, col0); D.append(col0, check); if(label){ D.attr(label, 'for', id); D.append(labelWrapper, label); } if(hint) D.append(labelWrapper, hint); }else{ if(op.callback){ menuEntry.addEventListener('click', (ev)=>op.callback(ev)); } D.append(menuEntry, labelWrapper); if(label) D.append(labelWrapper, label); if(hint) D.append(labelWrapper, hint); } D.append(optionsMenu, menuEntry); if(op.persistentSetting){ Chat.settings.addListener( op.persistentSetting, function(setting){ if(op.checkbox) op.checkbox.checked = !!setting.value; else if(op.select) op.select.value = setting.value; if(op.callback) op.callback(setting); } ); if(op.checkbox){ op.checkbox.addEventListener( 'change', function(){ Chat.settings.set(op.persistentSetting, op.checkbox.checked) }, false); } }else if(op.callback && op.checkbox){ op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false); } if(op.children){ D.addClass(menuEntry, 'parent'); op.children.forEach((x)=>f(x,true)); } }); })(); (function(){ Chat.settings.addListener('monospace-messages',function(s){ document.body.classList[s.value ? 'add' : 'remove']('monospace-messages'); }) Chat.settings.addListener('active-user-list',function(s){ Chat.showActiveUserList(s.value); }); Chat.settings.addListener('active-user-list-timestamps',function(s){ Chat.showActiveUserTimestamps(s.value); }); Chat.settings.addListener('chat-only-mode',function(s){ Chat.chatOnlyMode(s.value); }); Chat.settings.addListener('edit-widget-x',function(s){ let eSelected; if(s.value){ if(Chat.e.inputX===Chat.inputElement()) return; eSelected = Chat.e.inputX; }else{ eSelected = Chat.settings.getBool('edit-compact-mode') ? Chat.e.input1 : Chat.e.inputM; } const v = Chat.inputValue(); Chat.inputValue(''); Chat.e.inputFields.forEach(function(e,ndx){ if(eSelected===e){ Chat.e.inputFields.$currentIndex = ndx; D.removeClass(e, 'hidden'); } else D.addClass(e,'hidden'); }); Chat.inputValue(v); eSelected.focus(); }); Chat.settings.addListener('edit-compact-mode',function(s){ if(Chat.e.inputX!==Chat.inputElement()){ const a = s.value ? [Chat.e.input1, Chat.e.inputM, 0] : [Chat.e.inputM, Chat.e.input1, 1]; const v = Chat.inputValue(); Chat.inputValue(''); Chat.e.inputFields.$currentIndex = a[2]; Chat.inputValue(v); D.removeClass(a[0], 'hidden'); D.addClass(a[1], 'hidden'); } Chat.e.inputElementWrapper.classList[ s.value ? 'add' : 'remove' ]('compact'); Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); }); Chat.settings.addListener('edit-ctrl-send',function(s){ const label = (s.value ? "Ctrl-" : "")+"Enter submits messages."; Chat.e.inputFields.forEach((e)=>{ const v = e.dataset.placeholder0 + " " +label; if(e.isContentEditable) e.dataset.placeholder = v; else D.attr(e,'placeholder',v); }); Chat.e.btnSubmit.title = label; }); const valueKludges = { "false": false, "true": true }; Object.keys(Chat.settings.defaults).forEach(function(k){ var v = Chat.settings.get(k,Chat); if(Chat===v) v = Chat.settings.defaults[k]; if(valueKludges.hasOwnProperty(v)) v = valueKludges[v]; Chat.settings.set(k,v) ; }); })(); (function(){ const btnPreview = Chat.e.btnPreview; Chat.setPreviewText = function(t){ this.setCurrentView(this.e.viewPreview); this.e.previewContent.innerHTML = t; this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); this.inputFocus(); }; Chat.e.viewPreview.querySelector('#chat-preview-close'). addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); let previewPending = false; const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputFields]; const submit = function(ev){ ev.preventDefault(); ev.stopPropagation(); if(previewPending) return false; const txt = Chat.inputValue(); if(!txt){ Chat.setPreviewText(''); previewPending = false; return false; } const fd = new FormData(); fd.append('content', txt); fd.append('filename','chat.md' ); fd.append('render_mode',F.page.previewModes.wiki); F.fetch('ajax/preview-text',{ payload: fd, onload: function(html){ Chat.setPreviewText(html); F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); }, onerror: function(e){ F.fetch.onerror(e); Chat.setPreviewText("ERROR: "+( e.message || 'Unknown error fetching preview!' )); }, beforesend: function(){ D.disable(elemsToEnable); Chat.ajaxStart(); previewPending = true; Chat.setPreviewText("Loading preview..."); }, aftersend:function(){ previewPending = false; Chat.ajaxEnd(); D.enable(elemsToEnable); } }); return false; }; btnPreview.addEventListener('click', submit, false); })(); const newcontent = function f(jx,atEnd){ if(!f.processPost){ f.processPost = function(m,atEnd){ ++Chat.totalMessageCount; if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid; if( !Chat.mnMsg || m.msgidf.processPost(m,atEnd)); Chat.updateActiveUserList(); if('visible'===document.visibilityState){ if(Chat.changesSincePageHidden){ Chat.changesSincePageHidden = 0; Chat.e.pageTitle.innerText = Chat.pageTitleOrig; } }else{ Chat.changesSincePageHidden += jx.msgs.length; if(jx.msgs.length){ Chat.e.pageTitle.innerText = '[*] '+Chat.pageTitleOrig; } } }; Chat.newContent = newcontent; (function(){ const loadLegend = D.legend("Load..."); const toolbar = Chat.e.loadOlderToolbar = D.attr( D.fieldset(loadLegend), "id", "load-msg-toolbar" ); Chat.disableDuringAjax.push(toolbar); const loadOldMessages = function(n){ Chat.e.viewMessages.classList.add('loading'); Chat._isBatchLoading = true; const scrollHt = Chat.e.viewMessages.scrollHeight, scrollTop = Chat.e.viewMessages.scrollTop; F.fetch("chat-poll",{ urlParams:{ before: Chat.mnMsg, n: n }, responseType: 'json', onerror:function(err){ Chat.reportErrorAsMessage(err); Chat._isBatchLoading = false; }, onload:function(x){ let gotMessages = x.msgs.length; newcontent(x,true); Chat._isBatchLoading = false; Chat.updateActiveUserList(); if(Chat._gotServerError){ Chat._gotServerError = false; return; } if(n<0 || 0===gotMessages || (n>0 && gotMessages=0) Chat.disableDuringAjax.splice(ndx,1); Chat.e.loadOlderToolbar.disabled = true; } if(gotMessages > 0){ F.toast.message("Loaded "+gotMessages+" older messages."); Chat.e.viewMessages.scrollTo( 0, Chat.e.viewMessages.scrollHeight - scrollHt + scrollTop ); } }, aftersend:function(){ Chat.e.viewMessages.classList.remove('loading'); Chat.ajaxEnd(); } }); }; const wrapper = D.div();; D.append(toolbar, wrapper); var btn = D.button("Previous "+Chat.loadMessageCount+" messages"); D.append(wrapper, btn); btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount)); btn = D.button("All previous messages"); D.append(wrapper, btn); btn.addEventListener('click',()=>loadOldMessages(-1)); D.append(Chat.e.viewMessages, toolbar); toolbar.disabled = true; })(); const afterFetch = function f(){ if(true===f.isFirstCall){ f.isFirstCall = false; Chat.ajaxEnd(); Chat.e.viewMessages.classList.remove('loading'); setTimeout(function(){ Chat.scrollMessagesTo(1); }, 250); } if(Chat._gotServerError && Chat.intervalTimer){ clearInterval(Chat.intervalTimer); Chat.reportErrorAsMessage( "Shutting down chat poller due to server-side error. ", "Reload this page to reactivate it."); delete Chat.intervalTimer; } poll.running = false; }; afterFetch.isFirstCall = true; const poll = async function f(){ if(f.running) return; f.running = true; Chat._isBatchLoading = f.isFirstCall; if(true===f.isFirstCall){ f.isFirstCall = false; Chat.ajaxStart(); Chat.e.viewMessages.classList.add('loading'); } F.fetch("chat-poll",{ timeout: 420 * 1000, urlParams:{ name: Chat.mxMsg }, responseType: "json", beforesend: function(){}, aftersend: function(){}, onerror:function(err){ Chat._isBatchLoading = false; if(Chat.verboseErrors) console.error(err); afterFetch(); }, onload:function(y){ newcontent(y); if(Chat._isBatchLoading){ Chat._isBatchLoading = false; Chat.updateActiveUserList(); } afterFetch(); } }); }; poll.isFirstCall = true; Chat._gotServerError = poll.running = false; if( window.fossil.config.chat.fromcli ){ Chat.chatOnlyMode(true); } Chat.intervalTimer = setInterval(poll, 1000); if(0){ const flip = (ev)=>Chat.animate(ev.target,'anim-flip-h'); document.querySelectorAll('#chat-buttons-wrapper .cbutton').forEach(function(e){ e.addEventListener('click',flip, false); }); } delete ForceResizeKludge.$disabled; ForceResizeKludge(); Chat.animate.$disabled = false; setTimeout( ()=>Chat.inputFocus(), 0 ); F.page.chat = Chat; });