-
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 = {
beVerbose: false
,
playedBeep: false,
e:{
messageInjectPoint: E1('#message-inject-point'),
pageTitle: E1('head title'),
loadOlderToolbar: undefined,
inputArea: E1("#chat-input-area"),
inputLineWrapper: 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'),
viewSearch: E1('#chat-search'),
searchContent: E1('#chat-search-content'),
btnPreview: E1('#chat-button-preview'),
views: document.querySelectorAll('.chat-view'),
activeUserListWrapper: E1('#chat-user-list-wrapper'),
activeUserList: E1('#chat-user-list'),
eMsgPollError: undefined,
pollErrorMarker: document.body
},
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;
}
},
timer:{
tidPendingPoll: undefined,
tidClearPollErr: undefined,
$initialDelay: 1000,
currentDelay: 1000,
maxDelay: 60000 * 5,
minDelay: 5000,
errCount: 0,
minErrForNotify: 4,
pollTimeout: (1 && window.location.hostname.match(
"localhost"
)) ? 15000
: (+F.config.chat.pollTimeout>0
? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1)))
: 30000),
randomInterval: function(factor){
return Math.floor(Math.random() * factor);
},
incrDelay: function(){
if( this.maxDelay > this.currentDelay ){
if(this.currentDelay < this.minDelay){
this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
}else{
this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
}
}
return this.currentDelay;
},
resetDelay: function(ms=0){
return this.currentDelay = ms || this.$initialDelay;
},
isDelayed: function(){
return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
},
startPendingPollTimer: function(delay){
this.cancelPendingPollTimer().tidPendingPoll
= setTimeout( Chat.poll, delay || Chat.timer.resetDelay() );
return this;
},
cancelPendingPollTimer: function(){
if( this.tidPendingPoll ){
clearTimeout(this.tidPendingPoll);
this.tidPendingPoll = 0;
}
return this;
},
cancelReconnectCheckTimer: function(){
if( this.tidClearPollErr ){
clearTimeout(this.tidClearPollErr);
this.tidClearPollErr = 0;
}
return this;
}
},
inputValue: function(){
const e = this.inputElement();
if(arguments.length && 'boolean'!==typeof arguments[0]){
if(e.isContentEditable) e.innerText = arguments[0];
else e.value = arguments[0];
return this;
}
const rc = e.isContentEditable ? e.innerText : e.value;
if( true===arguments[0] ){
if(e.isContentEditable) e.innerText = '';
else e.value = '';
}
return rc && rc.trim();
},
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,
"beep-once": false,
"active-user-list": false,
"active-user-list-timestamps": false,
"alert-own-messages": false,
"edit-widget-x": false
}
},
playNewMessageSound: function f(){
if(f.uri){
if(!cs.pageIsActive
&& this.playedBeep && this.settings.getBool('beep-once',false)){
return;
}
try{
this.playedBeep = true;
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){
this.playedBeep = false;
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);
};
let InternalMsgId = 0;
cs.reportErrorAsMessage = function f(){
const args = argsToArray(arguments).map(function(v){
return (v instanceof Error) ? v.message : v;
});
if(Chat.beVerbose){
console.error("chat error:",args);
}
const d = new Date().toISOString(),
mw = new this.MessageWidget({
isError: true,
xfrom: undefined,
msgid: "error-"+(++InternalMsgId),
mtime: d,
lmtime: d,
xmsg: args
});
this.injectMessageElem(mw.e.body);
mw.scrollIntoView();
return mw;
};
cs.reportReconnection = function f(){
const args = argsToArray(arguments).map(function(v){
return (v instanceof Error) ? v.message : v;
});
const d = new Date().toISOString(),
mw = new this.MessageWidget({
isError: false,
xfrom: undefined,
msgid: "reconnect-"+(++InternalMsgId),
mtime: d,
lmtime: d,
xmsg: args
});
this.injectMessageElem(mw.e.body);
mw.scrollIntoView();
return mw;
};
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, silent){
var e;
if(id instanceof HTMLElement){
e = id;
id = e.dataset.msgid;
delete e.dataset.msgid;
if( e?.dataset?.alsoRemove ){
const xId = e.dataset.alsoRemove;
delete e.dataset.alsoRemove;
this.deleteMessageElem( xId );
}
}else if(id instanceof Chat.MessageWidget) {
if( this.e.eMsgPollError === e ){
this.e.eMsgPollError = undefined;
}
if(id.e?.body){
this.deleteMessageElem(id.e.body);
}
return;
} else{
e = this.getMessageElemById(id);
}
if(e && id){
D.remove(e);
if(e===this.e.newestMessage){
this.fetchLastMessageElem();
}
if( !silent ){
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){
reportConnectionOkay('chat-fetch-one');
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)){
F.fetch("chat-delete/" + id, {
responseType: 'json',
onload:(r)=>{
reportConnectionOkay('chat-delete');
this.deleteMessageElem(r);
},
onerror:(err)=>this.reportErrorAsMessage(err)
});
}else{
this.deleteMessageElem(id);
}
};
document.addEventListener('visibilitychange', function(ev){
cs.pageIsActive = ('visible' === document.visibilityState);
cs.playedBeep = false;
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 ctor = 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, longFmt=false){
const li = [];
if( longFmt ){
li.push(
d.getFullYear(),
'-', pad2(d.getMonth()+1),
'-', pad2(d.getDate()),
' ',
d.getHours(), ":",
(d.getMinutes()+100).toString().slice(1,3)
);
}else{
li.push(
d.getHours(),":",
(d.getMinutes()+100).toString().slice(1,3),
' ', dowMap[d.getDay()]
);
}
return li.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 shouldFossilRenderEmbed = 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');
}
};
ctor.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.append(D.addClass(D.span(), 'msgid'),
'#' + (m.msgid||'???')),
(m.isSearchResult ? ' ' : ' @ '),
D.append(D.addClass(D.span(), 'timestamp'),
theTime(d,!!m.isSearchResult))
);
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,false))
);
}
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 shouldFossilRender = shouldFossilRenderEmbed(m);
const downloadArgs = shouldFossilRender ? '?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, shouldFossilRender
? "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( eMsg.classList.contains('notification') ){
const btnDeletePoll = D.button("Delete /chat notifications?");
D.append(toolbar, btnDeletePoll);
btnDeletePoll.addEventListener('click', function(){
self.hide();
Chat.e.viewMessages.querySelectorAll(
'.message-widget.notification:not(.resend-message)'
).forEach(e=>Chat.deleteMessageElem(e, true));
});
}
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 ctor;
})();
Chat.SearchCtxLoader = (function(){
const nMsgContext = 5;
const zUpArrow = '\u25B2';
const zDownArrow = '\u25BC';
const ctor = function(o){
this.o = {
iFirstInTable: o.first,
iLastInTable: o.last,
iPrevId: o.previd,
iNextId: o.nextid,
bIgnoreClick: false
};
this.e = {
body:    D.addClass(D.div(), 'spacer-widget'),
up:      D.addClass(
D.button(zDownArrow+' Load '+nMsgContext+' more '+zDownArrow),
'up'
),
down:    D.addClass(
D.button(zUpArrow+' Load '+nMsgContext+' more '+zUpArrow),
'down'
),
all:     D.addClass(D.button('Load More'), 'all')
};
D.append( this.e.body, this.e.up, this.e.down, this.e.all );
const ms = this;
this.e.up.addEventListener('click', ()=>ms.load_messages(false));
this.e.down.addEventListener('click', ()=>ms.load_messages(true));
this.e.all.addEventListener('click', ()=>ms.load_messages( (ms.o.iPrevId==0) ));
this.set_button_visibility();
};
ctor.prototype = {
set_button_visibility: function() {
if( !this.e ) return;
const o = this.o;
const iPrevId = (o.iPrevId!=0) ? o.iPrevId : o.iFirstInTable-1;
const iNextId = (o.iNextId!=0) ? o.iNextId : o.iLastInTable+1;
let nDiff = (iNextId - iPrevId) - 1;
for( const x of [this.e.up, this.e.down, this.e.all] ){
if( x ) D.addClass(x, 'hidden');
}
let nVisible = 0;
if( nDiff>0 ){
if( nDiff>nMsgContext && (o.iPrevId==0 || o.iNextId==0) ){
nDiff = nMsgContext;
}
if( nDiff<=nMsgContext && o.iPrevId!=0 && o.iNextId!=0 ){
D.removeClass(this.e.all, 'hidden');
++nVisible;
this.e.all.innerText = (
zUpArrow + " Load " + nDiff + " more " + zDownArrow
);
}else{
if( o.iPrevId!=0 ){
++nVisible;
D.removeClass(this.e.up, 'hidden');
}else if( this.e.up ){
if( this.e.up.parentNode ) D.remove(this.e.up);
delete this.e.up;
}
if( o.iNextId!=0 ){
++nVisible;
D.removeClass(this.e.down, 'hidden');
}else if( this.e.down ){
if( this.e.down.parentNode ) D.remove( this.e.down );
delete this.e.down;
}
}
}
if( !nVisible ){
for( const x of [this.e.up, this.e.down, this.e.all, this.e.body] ){
if( x?.parentNode ) D.remove(x);
}
delete this.e;
}
},
load_messages: function(bDown) {
if( this.bIgnoreClick ) return;
var iFirst = 0;
var nFetch = 0;
var iEof = 0;
const e = this.e, o = this.o;
this.bIgnoreClick = true;
if( bDown ){
iFirst = this.o.iNextId - nMsgContext;
if( iFirst<this.o.iFirstInTable ){
iFirst = this.o.iFirstInTable;
}
}else{
iFirst = this.o.iPrevId+1;
}
nFetch = nMsgContext;
iEof = (this.o.iNextId > 0) ? this.o.iNextId : this.o.iLastInTable+1;
if( iFirst+nFetch>iEof ){
nFetch = iEof - iFirst;
}
const ms = this;
F.fetch("chat-query",{
urlParams:{
q: '',
n: nFetch,
i: iFirst
},
responseType: "json",
onload:function(jx){
reportConnectionOkay('chat-query');
if( bDown ) jx.msgs.reverse();
jx.msgs.forEach((m) => {
m.isSearchResult = true;
var mw = new Chat.MessageWidget(m);
if( bDown ){
const eAnchor = e.body.nextElementSibling;
if( eAnchor ) Chat.e.searchContent.insertBefore(mw.e.body, eAnchor);
else D.append(Chat.e.searchContent, mw.e.body);
}else{
Chat.e.searchContent.insertBefore(mw.e.body, e.body);
}
});
if( bDown ){
o.iNextId -= jx.msgs.length;
}else{
o.iPrevId += jx.msgs.length;
}
ms.set_button_visibility();
ms.bIgnoreClick = false;
}
});
}
};
return ctor;
})();
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[0])
});
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;
ta.setAttribute('readonly','true');
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);
}));
D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message");
};
const reportConnectionOkay = function(dbgContext, showMsg = true){
if(Chat.beVerbose){
console.warn('reportConnectionOkay', dbgContext,
'Chat.e.pollErrorMarker classes =',
Chat.e.pollErrorMarker.classList,
'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr,
'Chat.timer =',Chat.timer);
}
if( Chat.timer.errCount ){
D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
Chat.timer.errCount = 0;
}
Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer();
if( Chat.e.eMsgPollError ) {
const oldErrMsg = Chat.e.eMsgPollError;
Chat.e.eMsgPollError = undefined;
if( showMsg ){
if(Chat.beVerbose){
console.log("Poller Connection restored.");
}
const m = Chat.reportReconnection("Poller connection restored.");
if( oldErrMsg ){
D.remove(oldErrMsg.e?.body.querySelector('button.retry-now'));
}
m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
D.addClass(m.e.body,'poller-connection');
}
}
};
Chat.submitMessage = function f(){
if(!f.spaces){
f.spaces = /\s+$/;
f.markdownContinuation = /\\\s+$/;
f.spaces2 = /\s{3,}$/;
}
switch( this.e.currentView ){
case this.e.viewSearch: this.submitSearch();
return;
default: break;
}
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){
reportConnectionOkay('chat-send');
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(!text &&
(Chat.e.currentView===Chat.e.viewPreview
| Chat.e.currentView===Chat.e.viewSearch)){
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<window.innerHeight){
document.body.classList.add('my-messages-right');
}
const settingsButton = document.querySelector('#chat-button-settings');
const optionsMenu = E1('#chat-config-options');
const eToggleView = function(ev){
ev.preventDefault();
ev.stopPropagation();
Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig
? Chat.e.viewMessages : Chat.e.viewConfig);
return false;
};
D.attr(settingsButton, 'role', 'button').addEventListener('click', eToggleView, false);
Chat.e.viewConfig.querySelector('button.action-close').addEventListener('click', eToggleView, false);
const namedOptions = {
activeUsers:{
label: "Show active users list",
hint: "List users who have messages in the currently-loaded chat history.",
boolValue: 'active-user-list'
}
};
if(1){
const optAu = namedOptions.activeUsers;
optAu.theLegend = Chat.e.activeUserListWrapper.firstElementChild;
optAu.theList = optAu.theLegend.nextElementSibling;
optAu.theLegend.addEventListener('click',function(){
D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed');
if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){
Chat.animate(optAu.theList,'anim-flip-v');
}
}, false);
}
const settingsOps = [{
label: "Chat Configuration Options",
hint: F.storage.isTransient()
? "Local store is unavailable. These settings are transient."
: ["Most of these settings are persistent via ",
F.storage.storageImplName(), ": ",
F.storage.storageHelpDescription()].join('')
},{
label: "Editing Options...",
children:[{
label: "Chat-only mode",
hint: "Toggle the page between normal fossil view and chat-only view.",
boolValue: 'chat-only-mode'
},{
label: "Ctrl-enter to Send",
hint: [
"When on, only Ctrl-Enter will send messages and Enter adds ",
"blank lines. When off, both Enter and Ctrl-Enter send. ",
"When the input field has focus and is empty ",
"then Ctrl-Enter toggles this setting."
].join(''),
boolValue: 'edit-ctrl-send'
},{
label: "Compact mode",
hint: [
"Toggle between a space-saving or more spacious writing area. ",
"When the input field has focus and is empty ",
"then Shift-Enter may (depending on the current view) toggle this setting."
].join(''),
boolValue: 'edit-compact-mode'
},{
label: "Use 'contenteditable' editing mode",
boolValue: 'edit-widget-x',
hint: [
"When enabled, chat input uses a so-called 'contenteditable' ",
"field. Though generally more comfortable and modern than ",
"plain-text input fields, browser-specific quirks and bugs ",
"may lead to frustration. Ideal for mobile devices."
].join('')
},{
label: "Shift-enter to preview",
hint: ["Use shift-enter to preview being-edited messages. ",
"This is normally desirable but some software-mode ",
"keyboards misinteract with this, in which cases it can be ",
"disabled."],
boolValue: 'edit-shift-enter-preview'
}]
},{
label: "Appearance Options...",
children:[{
label: "Left-align my posts",
hint: "Default alignment of your own messages is selected "
+ "based on the window width/height ratio.",
boolValue: ()=>!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','<none>');
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: "Notify only once when away",
hint: "Notify only for the first message received after chat is hidden from view.",
boolValue: 'beep-once'
},{
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.inputLineWrapper.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 message.";
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('button.action-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){
reportConnectionOkay('ajax/preview-text');
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);
})();
(function(){
const btn = document.querySelector('#chat-button-search');
D.attr(btn, 'role', 'button').addEventListener('click', function(ev){
ev.preventDefault();
ev.stopPropagation();
const msg = Chat.inputValue();
if( Chat.e.currentView===Chat.e.viewSearch ){
if( msg ) Chat.submitSearch();
else Chat.setCurrentView(Chat.e.viewMessages);
}else{
Chat.setCurrentView(Chat.e.viewSearch);
if( msg ) Chat.submitSearch();
}
return false;
}, false);
Chat.e.viewSearch.querySelector('button.action-clear').addEventListener('click', function(ev){
ev.preventDefault();
ev.stopPropagation();
Chat.clearSearch(true);
Chat.setCurrentView(Chat.e.viewMessages);
return false;
}, false);
Chat.e.viewSearch.querySelector('button.action-close').addEventListener('click', function(ev){
ev.preventDefault();
ev.stopPropagation();
Chat.setCurrentView(Chat.e.viewMessages);
return false;
}, 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.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
if(m.xfrom && m.mtime){
const d = new Date(m.mtime);
const uls = Chat.usersLastSeen[m.xfrom];
if(!uls || uls<d){
d.$uColor = m.uclr;
Chat.usersLastSeen[m.xfrom] = d;
}
}
if( m.mdel ){
Chat.deleteMessageElem(m.mdel);
return;
}
if(!Chat._isBatchLoading
&& (Chat.me!==m.xfrom
|| Chat.settings.getBool('alert-own-messages'))){
Chat.playNewMessageSound();
}
const row = new Chat.MessageWidget(m);
Chat.injectMessageElem(row.e.body,atEnd);
if(m.isError){
Chat._gotServerError = m;
}
};
}
jx.msgs.forEach((m)=>f.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){
reportConnectionOkay('loadOldMessages()');
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<n)
|| (n===0 && gotMessages<Chat.loadMessageCount
)){
const div = Chat.e.loadOlderToolbar.querySelector('div');
D.append(D.clearElement(div), "All history has been loaded.");
D.addClass(Chat.e.loadOlderToolbar, 'all-done');
const ndx = Chat.disableDuringAjax.indexOf(Chat.e.loadOlderToolbar);
if(ndx>=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;
})();
Chat.clearSearch = function(addInstructions=false){
const e = D.clearElement( this.e.searchContent );
if(addInstructions){
D.append(e, "Enter search terms in the message field. "+
"Use #NNNNN to search for the message with ID NNNNN.");
}
return e;
};
Chat.clearSearch(true);
Chat.submitSearch = function(){
const term = this.inputValue(true);
const eMsgTgt = this.clearSearch(true);
if( !term ) return;
D.append( eMsgTgt, "Searching for ",term," ...");
const fd = new FormData();
fd.set('q', term);
F.fetch(
"chat-query", {
payload: fd,
responseType: 'json',
onerror:function(err){
Chat.setCurrentView(Chat.e.viewMessages);
Chat.reportErrorAsMessage(err);
},
onload:function(jx){
reportConnectionOkay('submitSearch()');
let previd = 0;
D.clearElement(eMsgTgt);
jx.msgs.forEach((m)=>{
m.isSearchResult = true;
const mw = new Chat.MessageWidget(m);
const spacer = new Chat.SearchCtxLoader({
first: jx.first,
last: jx.last,
previd: previd,
nextid: m.msgid
});
if( spacer.e ) D.append( eMsgTgt, spacer.e.body );
D.append( eMsgTgt, mw.e.body );
previd = m.msgid;
});
if( jx.msgs.length ){
const spacer = new Chat.SearchCtxLoader({
first: jx.first,
last: jx.last,
previd: previd,
nextid: 0
});
if( spacer.e ) D.append( eMsgTgt, spacer.e.body );
}else{
D.append( D.clearElement(eMsgTgt),
'No search results found for: ',
term );
}
}
}
);
};
const chatPollBeforeSend = function(){
if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){
Chat.timer.tidClearPollErr = setTimeout(()=>{
Chat.timer.tidClearPollErr = 0;
if( poll.running ){
reportConnectionOkay('chatPollBeforeSend', true);
}
}, Chat.timer.$initialDelay * 4 );
}
};
const afterPollFetch = function f(err){
if(true===f.isFirstCall){
f.isFirstCall = false;
Chat.ajaxEnd();
Chat.e.viewMessages.classList.remove('loading');
setTimeout(function(){
Chat.scrollMessagesTo(1);
}, 250);
}
Chat.timer.cancelPendingPollTimer();
if(Chat._gotServerError){
Chat.reportErrorAsMessage(
"Shutting down chat poller due to server-side error. ",
"Reload this page to reactivate it."
);
} else {
if( err && Chat.beVerbose ){
console.error("afterPollFetch:",err.name,err.status,err.message);
}
if( !err || 'timeout'===err.name ){
reportConnectionOkay('afterPollFetch '+err, false);
}else{
let delay;
D.addClass(Chat.e.pollErrorMarker, 'connection-error');
if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
delay = Chat.timer.resetDelay(
(Chat.timer.minDelay * Chat.timer.errCount)
+ Chat.timer.randomInterval(Chat.timer.minDelay)
);
if(Chat.beVerbose){
console.warn("Ignoring polling error #",Chat.timer.errCount,
"for another",delay,"ms" );
}
} else {
delay = Chat.timer.incrDelay();
const msg = "Poller connection error. Retrying in "+delay+ " ms.";
if( Chat.e.eMsgPollError ){
Chat.deleteMessageElem(Chat.e.eMsgPollError, false);
}
const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg);
D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection');
const btnDel = D.addClass(D.button("Retry now"), 'retry-now');
const eParent = Chat.e.eMsgPollError.e.content;
D.append(eParent, " ", btnDel);
btnDel.addEventListener('click', function(){
D.remove(btnDel);
D.append(eParent, D.text("retrying..."));
Chat.timer.cancelPendingPollTimer().currentDelay =
Chat.timer.resetDelay() +
1;
poll();
});
}
Chat.timer.startPendingPollTimer(delay);
}
}
};
afterPollFetch.isFirstCall = true;
const poll = Chat.poll = async function f(){
if(f.running) return;
f.running = true;
Chat._isBatchLoading = f.isFirstCall;
if(true===f.isFirstCall){
f.isFirstCall = false;
f.pendingOnError = undefined;
Chat.ajaxStart();
Chat.e.viewMessages.classList.add('loading');
f.delayPendingOnError = function(err){
if( f.pendingOnError ){
const x = f.pendingOnError;
f.pendingOnError = undefined;
afterPollFetch(x);
}
};
}
F.fetch("chat-poll",{
timeout: Chat.timer.pollTimeout,
urlParams:{
name: Chat.mxMsg
},
responseType: "json",
beforesend: chatPollBeforeSend,
aftersend: function(){
poll.running = false;
},
ontimeout: function(err){
f.pendingOnError = undefined;
afterPollFetch(err);
},
onerror:function(err){
Chat._isBatchLoading = false;
if(Chat.beVerbose){
console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
}
f.pendingOnError = err;
setTimeout(f.delayPendingOnError, 100);
},
onload:function(y){
reportConnectionOkay('poll.onload', true);
newcontent(y);
if(Chat._isBatchLoading){
Chat._isBatchLoading = false;
Chat.updateActiveUserList();
}
afterPollFetch();
}
});
};
poll.isFirstCall = true;
Chat._gotServerError = poll.running = false;
if( window.fossil.config.chat.fromcli ){
Chat.chatOnlyMode(true);
}
Chat.timer.startPendingPollTimer();
delete ForceResizeKludge.$disabled;
ForceResizeKludge();
Chat.animate.$disabled = false;
setTimeout( ()=>Chat.inputFocus(), 0 );
F.page.chat = Chat;
});