Fossil SCM
Do not use <fieldset> and <legend> as Safari does not allow one to bind 'click' events.
Commit
6849bb0b6b5ae9ea1315fa88d43adb53c36be85fa7bc36cdffc383b786dc9fad
Parent
f5ab4888c2dc221…
2 files changed
+102
-85
+16
-16
+102
-85
| --- src/chat.js | ||
| +++ src/chat.js | ||
| @@ -31,15 +31,10 @@ | ||
| 31 | 31 | changesSincePageHidden: 0, |
| 32 | 32 | notificationBubbleColor: 'white', |
| 33 | 33 | totalMessageCount: 0, // total # of inbound messages |
| 34 | 34 | //! Number of messages to load for the history buttons |
| 35 | 35 | loadMessageCount: Math.abs(F.config.chat.initSize || 20), |
| 36 | - /* Alignment of 'my' messages: must be 'left' or 'right'. Note | |
| 37 | - that 'right' is conventional for mobile chat apps but can be | |
| 38 | - difficult to read in wide windows (desktop/tablet landscape | |
| 39 | - mode). Can be toggled via settings popup. */ | |
| 40 | - msgMyAlign: (window.innerWidth<window.innerHeight) ? 'right' : 'left', | |
| 41 | 36 | ajaxInflight: 0, |
| 42 | 37 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 43 | 38 | taking into account single- vs multi-line input. The getter returns |
| 44 | 39 | a string and the setter returns this object. */ |
| 45 | 40 | inputValue: function(){ |
| @@ -138,10 +133,17 @@ | ||
| 138 | 133 | }; |
| 139 | 134 | Object.keys(cs.settings.defaults).forEach(function f(k){ |
| 140 | 135 | const v = cs.settings.get(k,f); |
| 141 | 136 | if(f===v) cs.settings.set(k,cs.settings.defaults[k]); |
| 142 | 137 | }); |
| 138 | + if(window.innerWidth<window.innerHeight){ | |
| 139 | + /* Alignment of 'my' messages: right alignment is conventional | |
| 140 | + for mobile chat apps but can be difficult to read in wide | |
| 141 | + windows (desktop/tablet landscape mode). Can be toggled via | |
| 142 | + settings popup. */ | |
| 143 | + document.body.classList.add('my-messages-right'); | |
| 144 | + } | |
| 143 | 145 | if(cs.settings.getBool('monospace-messages',false)){ |
| 144 | 146 | document.body.classList.add('monospace-messages'); |
| 145 | 147 | } |
| 146 | 148 | cs.e.inputCurrent = cs.e.inputSingle; |
| 147 | 149 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| @@ -223,10 +225,93 @@ | ||
| 223 | 225 | } |
| 224 | 226 | }, true); |
| 225 | 227 | return cs; |
| 226 | 228 | })()/*Chat initialization*/; |
| 227 | 229 | |
| 230 | + /** | |
| 231 | + Custom widget type for rendering messages (one message per | |
| 232 | + instance). These are modelled after FIELDSET elements but we | |
| 233 | + don't use FIELDSET because of cross-browser inconsistencies in | |
| 234 | + features of the FIELDSET/LEGEND combination, e.g. inability to | |
| 235 | + align legends via CSS in Firefox and clicking-related | |
| 236 | + deficiencies in Safari. | |
| 237 | + */ | |
| 238 | + const MessageWidget = (function(){ | |
| 239 | + const cf = function(){ | |
| 240 | + this.e = { | |
| 241 | + body: D.addClass(D.div(), 'message-widget'), | |
| 242 | + tab: D.addClass(D.span(), 'message-widget-tab'), | |
| 243 | + content: D.addClass(D.div(), 'message-widget-content') | |
| 244 | + }; | |
| 245 | + D.append(this.e.body, this.e.tab); | |
| 246 | + D.append(this.e.body, this.e.content); | |
| 247 | + this.e.tab.setAttribute('role', 'button'); | |
| 248 | + }; | |
| 249 | + cf.prototype = { | |
| 250 | + setLabel: function(label){ | |
| 251 | + return this; | |
| 252 | + }, | |
| 253 | + setPopupCallback: function(callback){ | |
| 254 | + this.e.tab.addEventListener('click', callback, false); | |
| 255 | + return this; | |
| 256 | + }, | |
| 257 | + setMessage: function(m){ | |
| 258 | + const ds = this.e.body.dataset; | |
| 259 | + ds.timestamp = m.mtime; | |
| 260 | + ds.msgid = m.msgid; | |
| 261 | + ds.xfrom = m.xfrom; | |
| 262 | + if(m.xfrom === Chat.me){ | |
| 263 | + D.addClass(this.e.body, 'mine'); | |
| 264 | + } | |
| 265 | + this.e.content.style.backgroundColor = m.uclr; | |
| 266 | + this.e.tab.style.backgroundColor = m.uclr; | |
| 267 | + | |
| 268 | + const d = new Date(m.mtime); | |
| 269 | + D.append( | |
| 270 | + D.clearElement(this.e.tab), D.text( | |
| 271 | + m.xfrom+' @ '+d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3)) | |
| 272 | + ); | |
| 273 | + var contentTarget = this.e.content; | |
| 274 | + if( m.fsize>0 ){ | |
| 275 | + if( m.fmime | |
| 276 | + && m.fmime.startsWith("image/") | |
| 277 | + && Chat.settings.getBool('images-inline',true) | |
| 278 | + ){ | |
| 279 | + contentTarget.appendChild(D.img("chat-download/" + m.msgid)); | |
| 280 | + }else{ | |
| 281 | + const a = D.a( | |
| 282 | + window.fossil.rootPath+ | |
| 283 | + 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname), | |
| 284 | + // ^^^ add m.fname to URL to cause downloaded file to have that name. | |
| 285 | + "(" + m.fname + " " + m.fsize + " bytes)" | |
| 286 | + ) | |
| 287 | + D.attr(a,'target','_blank'); | |
| 288 | + contentTarget.appendChild(a); | |
| 289 | + } | |
| 290 | + contentTarget = D.div(); | |
| 291 | + } | |
| 292 | + if(m.xmsg){ | |
| 293 | + if(contentTarget !== this.e.content){ | |
| 294 | + D.append(this.e.content, contentTarget); | |
| 295 | + } | |
| 296 | + // The m.xmsg text comes from the same server as this script and | |
| 297 | + // is guaranteed by that server to be "safe" HTML - safe in the | |
| 298 | + // sense that it is not possible for a malefactor to inject HTML | |
| 299 | + // or javascript or CSS. The m.xmsg content might contain | |
| 300 | + // hyperlinks, but otherwise it will be markup-free. See the | |
| 301 | + // chat_format_to_html() routine in the server for details. | |
| 302 | + // | |
| 303 | + // Hence, even though innerHTML is normally frowned upon, it is | |
| 304 | + // perfectly safe to use in this context. | |
| 305 | + contentTarget.innerHTML = m.xmsg; | |
| 306 | + } | |
| 307 | + return this; | |
| 308 | + } | |
| 309 | + }; | |
| 310 | + return cf; | |
| 311 | + })()/*MessageWidget*/; | |
| 312 | + | |
| 228 | 313 | const BlobXferState = (function(){/*drag/drop bits...*/ |
| 229 | 314 | /* State for paste and drag/drop */ |
| 230 | 315 | const bxs = { |
| 231 | 316 | dropDetails: document.querySelector('#chat-drop-details'), |
| 232 | 317 | blob: undefined, |
| @@ -413,19 +498,21 @@ | ||
| 413 | 498 | D.clearElement(this.e); |
| 414 | 499 | return this.show(false); |
| 415 | 500 | }; |
| 416 | 501 | }/*end static init*/ |
| 417 | 502 | const rect = ev.target.getBoundingClientRect(); |
| 418 | - const eMsg = ev.target.parentNode/*the owning fieldset element*/; | |
| 503 | + const eMsg = ev.target.parentNode/*the owning .message-widget element*/; | |
| 419 | 504 | f.popup._eMsg = eMsg; |
| 420 | - let x = rect.left, y = rect.top - 10; | |
| 505 | + console.debug("eMsg, rect", eMsg, rect); | |
| 506 | + let x = rect.left, y = rect.topm; | |
| 421 | 507 | f.popup.show(ev.target)/*so we can get its computed size*/; |
| 422 | - if('right'===ev.target.getAttribute('align')){ | |
| 508 | + if(eMsg.dataset.xfrom===Chat.me | |
| 509 | + && document.body.classList.contains('my-messages-right')){ | |
| 423 | 510 | // Shift popup to the left for right-aligned messages to avoid |
| 424 | 511 | // truncation off the right edge of the page. |
| 425 | 512 | const pRect = f.popup.e.getBoundingClientRect(); |
| 426 | - x -= pRect.width/3*2; | |
| 513 | + x = rect.right - pRect.width; | |
| 427 | 514 | } |
| 428 | 515 | f.popup.show(x, y); |
| 429 | 516 | }/*handleLegendClicked()*/; |
| 430 | 517 | |
| 431 | 518 | (function(){/*Set up #chat-settings-button */ |
| @@ -495,21 +582,13 @@ | ||
| 495 | 582 | iws.backgroundColor = f.initialBg; |
| 496 | 583 | } |
| 497 | 584 | } |
| 498 | 585 | },{ |
| 499 | 586 | label: "Left-align my posts", |
| 500 | - boolValue: ()=>'left'===Chat.msgMyAlign, | |
| 587 | + boolValue: ()=>!document.body.classList.contains('my-messages-right'), | |
| 501 | 588 | callback: function f(){ |
| 502 | - if('right'===Chat.msgMyAlign) Chat.msgMyAlign = 'left'; | |
| 503 | - else Chat.msgMyAlign = 'right'; | |
| 504 | - const msgs = Chat.e.messagesWrapper.querySelectorAll('.message-row'); | |
| 505 | - msgs.forEach(function(row){ | |
| 506 | - if(row.dataset.xfrom!==Chat.me) return; | |
| 507 | - row.querySelector('legend').setAttribute('align', Chat.msgMyAlign); | |
| 508 | - if('right'===Chat.msgMyAlign) row.style.justifyContent = "flex-end"; | |
| 509 | - else row.style.justifyContent = "flex-start"; | |
| 510 | - }); | |
| 589 | + document.body.classList.toggle('my-messages-right'); | |
| 511 | 590 | } |
| 512 | 591 | },{ |
| 513 | 592 | label: "Images inline", |
| 514 | 593 | boolValue: ()=>Chat.settings.getBool('images-inline'), |
| 515 | 594 | callback: function(){ |
| @@ -586,76 +665,14 @@ | ||
| 586 | 665 | if( m.mdel ){ |
| 587 | 666 | /* A record deletion notice. */ |
| 588 | 667 | Chat.deleteMessageElem(m.mdel); |
| 589 | 668 | return; |
| 590 | 669 | } |
| 591 | - const eWho = D.create('legend'), | |
| 592 | - row = D.addClass(D.fieldset(eWho), 'message-row'); | |
| 593 | - row.dataset.msgid = m.msgid; | |
| 594 | - row.dataset.xfrom = m.xfrom; | |
| 595 | - row.dataset.timestamp = m.mtime; | |
| 596 | - Chat.injectMessageElem(row,atEnd); | |
| 597 | - eWho.addEventListener('click', handleLegendClicked, false); | |
| 598 | - eWho.setAttribute('role', 'button'); | |
| 599 | - if( m.xfrom==Chat.me ){ | |
| 600 | - eWho.setAttribute('align', Chat.msgMyAlign); | |
| 601 | - if('right'===Chat.msgMyAlign){ | |
| 602 | - row.style.justifyContent = "flex-end"; | |
| 603 | - }else{ | |
| 604 | - row.style.justifyContent = "flex-start"; | |
| 605 | - } | |
| 606 | - }else{ | |
| 607 | - eWho.setAttribute('align', 'left'); | |
| 608 | - } | |
| 609 | - eWho.style.backgroundColor = m.uclr; | |
| 610 | - eWho.classList.add('message-user'); | |
| 611 | - let whoName = m.xfrom; | |
| 612 | - var d = new Date(m.mtime); | |
| 613 | - if( d.getMinutes().toString()!="NaN" ){ | |
| 614 | - /* Show local time when we can compute it */ | |
| 615 | - eWho.append(D.text(whoName+' @ '+ | |
| 616 | - d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3) | |
| 617 | - )) | |
| 618 | - }else{ | |
| 619 | - /* Show UTC on systems where Date() does not work */ | |
| 620 | - eWho.append(D.text(whoName+' @ '+m.mtime.slice(11,16))) | |
| 621 | - } | |
| 622 | - let eContent = D.addClass(D.div(),'message-content','chat-message'); | |
| 623 | - eContent.style.backgroundColor = m.uclr; | |
| 624 | - row.appendChild(eContent); | |
| 625 | - if( m.fsize>0 ){ | |
| 626 | - if( m.fmime | |
| 627 | - && m.fmime.startsWith("image/") | |
| 628 | - && Chat.settings.getBool('images-inline',true) | |
| 629 | - ){ | |
| 630 | - eContent.appendChild(D.img("chat-download/" + m.msgid)); | |
| 631 | - }else{ | |
| 632 | - const a = D.a( | |
| 633 | - window.fossil.rootPath+ | |
| 634 | - 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname), | |
| 635 | - // ^^^ add m.fname to URL to cause downloaded file to have that name. | |
| 636 | - "(" + m.fname + " " + m.fsize + " bytes)" | |
| 637 | - ) | |
| 638 | - D.attr(a,'target','_blank'); | |
| 639 | - eContent.appendChild(a); | |
| 640 | - } | |
| 641 | - const br = D.br(); | |
| 642 | - br.style.clear = "both"; | |
| 643 | - eContent.appendChild(br); | |
| 644 | - } | |
| 645 | - if(m.xmsg){ | |
| 646 | - // The m.xmsg text comes from the same server as this script and | |
| 647 | - // is guaranteed by that server to be "safe" HTML - safe in the | |
| 648 | - // sense that it is not possible for a malefactor to inject HTML | |
| 649 | - // or javascript or CSS. The m.xmsg content might contain | |
| 650 | - // hyperlinks, but otherwise it will be markup-free. See the | |
| 651 | - // chat_format_to_html() routine in the server for details. | |
| 652 | - // | |
| 653 | - // Hence, even though innerHTML is normally frowned upon, it is | |
| 654 | - // perfectly safe to use in this context. | |
| 655 | - eContent.innerHTML += m.xmsg | |
| 656 | - } | |
| 670 | + const row = new MessageWidget() | |
| 671 | + row.setMessage(m); | |
| 672 | + row.setPopupCallback(handleLegendClicked); | |
| 673 | + Chat.injectMessageElem(row.e.body,atEnd); | |
| 657 | 674 | }/*processPost()*/; |
| 658 | 675 | }/*end static init*/ |
| 659 | 676 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 660 | 677 | if('visible'===document.visibilityState){ |
| 661 | 678 | if(Chat.changesSincePageHidden){ |
| 662 | 679 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -31,15 +31,10 @@ | |
| 31 | changesSincePageHidden: 0, |
| 32 | notificationBubbleColor: 'white', |
| 33 | totalMessageCount: 0, // total # of inbound messages |
| 34 | //! Number of messages to load for the history buttons |
| 35 | loadMessageCount: Math.abs(F.config.chat.initSize || 20), |
| 36 | /* Alignment of 'my' messages: must be 'left' or 'right'. Note |
| 37 | that 'right' is conventional for mobile chat apps but can be |
| 38 | difficult to read in wide windows (desktop/tablet landscape |
| 39 | mode). Can be toggled via settings popup. */ |
| 40 | msgMyAlign: (window.innerWidth<window.innerHeight) ? 'right' : 'left', |
| 41 | ajaxInflight: 0, |
| 42 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 43 | taking into account single- vs multi-line input. The getter returns |
| 44 | a string and the setter returns this object. */ |
| 45 | inputValue: function(){ |
| @@ -138,10 +133,17 @@ | |
| 138 | }; |
| 139 | Object.keys(cs.settings.defaults).forEach(function f(k){ |
| 140 | const v = cs.settings.get(k,f); |
| 141 | if(f===v) cs.settings.set(k,cs.settings.defaults[k]); |
| 142 | }); |
| 143 | if(cs.settings.getBool('monospace-messages',false)){ |
| 144 | document.body.classList.add('monospace-messages'); |
| 145 | } |
| 146 | cs.e.inputCurrent = cs.e.inputSingle; |
| 147 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| @@ -223,10 +225,93 @@ | |
| 223 | } |
| 224 | }, true); |
| 225 | return cs; |
| 226 | })()/*Chat initialization*/; |
| 227 | |
| 228 | const BlobXferState = (function(){/*drag/drop bits...*/ |
| 229 | /* State for paste and drag/drop */ |
| 230 | const bxs = { |
| 231 | dropDetails: document.querySelector('#chat-drop-details'), |
| 232 | blob: undefined, |
| @@ -413,19 +498,21 @@ | |
| 413 | D.clearElement(this.e); |
| 414 | return this.show(false); |
| 415 | }; |
| 416 | }/*end static init*/ |
| 417 | const rect = ev.target.getBoundingClientRect(); |
| 418 | const eMsg = ev.target.parentNode/*the owning fieldset element*/; |
| 419 | f.popup._eMsg = eMsg; |
| 420 | let x = rect.left, y = rect.top - 10; |
| 421 | f.popup.show(ev.target)/*so we can get its computed size*/; |
| 422 | if('right'===ev.target.getAttribute('align')){ |
| 423 | // Shift popup to the left for right-aligned messages to avoid |
| 424 | // truncation off the right edge of the page. |
| 425 | const pRect = f.popup.e.getBoundingClientRect(); |
| 426 | x -= pRect.width/3*2; |
| 427 | } |
| 428 | f.popup.show(x, y); |
| 429 | }/*handleLegendClicked()*/; |
| 430 | |
| 431 | (function(){/*Set up #chat-settings-button */ |
| @@ -495,21 +582,13 @@ | |
| 495 | iws.backgroundColor = f.initialBg; |
| 496 | } |
| 497 | } |
| 498 | },{ |
| 499 | label: "Left-align my posts", |
| 500 | boolValue: ()=>'left'===Chat.msgMyAlign, |
| 501 | callback: function f(){ |
| 502 | if('right'===Chat.msgMyAlign) Chat.msgMyAlign = 'left'; |
| 503 | else Chat.msgMyAlign = 'right'; |
| 504 | const msgs = Chat.e.messagesWrapper.querySelectorAll('.message-row'); |
| 505 | msgs.forEach(function(row){ |
| 506 | if(row.dataset.xfrom!==Chat.me) return; |
| 507 | row.querySelector('legend').setAttribute('align', Chat.msgMyAlign); |
| 508 | if('right'===Chat.msgMyAlign) row.style.justifyContent = "flex-end"; |
| 509 | else row.style.justifyContent = "flex-start"; |
| 510 | }); |
| 511 | } |
| 512 | },{ |
| 513 | label: "Images inline", |
| 514 | boolValue: ()=>Chat.settings.getBool('images-inline'), |
| 515 | callback: function(){ |
| @@ -586,76 +665,14 @@ | |
| 586 | if( m.mdel ){ |
| 587 | /* A record deletion notice. */ |
| 588 | Chat.deleteMessageElem(m.mdel); |
| 589 | return; |
| 590 | } |
| 591 | const eWho = D.create('legend'), |
| 592 | row = D.addClass(D.fieldset(eWho), 'message-row'); |
| 593 | row.dataset.msgid = m.msgid; |
| 594 | row.dataset.xfrom = m.xfrom; |
| 595 | row.dataset.timestamp = m.mtime; |
| 596 | Chat.injectMessageElem(row,atEnd); |
| 597 | eWho.addEventListener('click', handleLegendClicked, false); |
| 598 | eWho.setAttribute('role', 'button'); |
| 599 | if( m.xfrom==Chat.me ){ |
| 600 | eWho.setAttribute('align', Chat.msgMyAlign); |
| 601 | if('right'===Chat.msgMyAlign){ |
| 602 | row.style.justifyContent = "flex-end"; |
| 603 | }else{ |
| 604 | row.style.justifyContent = "flex-start"; |
| 605 | } |
| 606 | }else{ |
| 607 | eWho.setAttribute('align', 'left'); |
| 608 | } |
| 609 | eWho.style.backgroundColor = m.uclr; |
| 610 | eWho.classList.add('message-user'); |
| 611 | let whoName = m.xfrom; |
| 612 | var d = new Date(m.mtime); |
| 613 | if( d.getMinutes().toString()!="NaN" ){ |
| 614 | /* Show local time when we can compute it */ |
| 615 | eWho.append(D.text(whoName+' @ '+ |
| 616 | d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3) |
| 617 | )) |
| 618 | }else{ |
| 619 | /* Show UTC on systems where Date() does not work */ |
| 620 | eWho.append(D.text(whoName+' @ '+m.mtime.slice(11,16))) |
| 621 | } |
| 622 | let eContent = D.addClass(D.div(),'message-content','chat-message'); |
| 623 | eContent.style.backgroundColor = m.uclr; |
| 624 | row.appendChild(eContent); |
| 625 | if( m.fsize>0 ){ |
| 626 | if( m.fmime |
| 627 | && m.fmime.startsWith("image/") |
| 628 | && Chat.settings.getBool('images-inline',true) |
| 629 | ){ |
| 630 | eContent.appendChild(D.img("chat-download/" + m.msgid)); |
| 631 | }else{ |
| 632 | const a = D.a( |
| 633 | window.fossil.rootPath+ |
| 634 | 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname), |
| 635 | // ^^^ add m.fname to URL to cause downloaded file to have that name. |
| 636 | "(" + m.fname + " " + m.fsize + " bytes)" |
| 637 | ) |
| 638 | D.attr(a,'target','_blank'); |
| 639 | eContent.appendChild(a); |
| 640 | } |
| 641 | const br = D.br(); |
| 642 | br.style.clear = "both"; |
| 643 | eContent.appendChild(br); |
| 644 | } |
| 645 | if(m.xmsg){ |
| 646 | // The m.xmsg text comes from the same server as this script and |
| 647 | // is guaranteed by that server to be "safe" HTML - safe in the |
| 648 | // sense that it is not possible for a malefactor to inject HTML |
| 649 | // or javascript or CSS. The m.xmsg content might contain |
| 650 | // hyperlinks, but otherwise it will be markup-free. See the |
| 651 | // chat_format_to_html() routine in the server for details. |
| 652 | // |
| 653 | // Hence, even though innerHTML is normally frowned upon, it is |
| 654 | // perfectly safe to use in this context. |
| 655 | eContent.innerHTML += m.xmsg |
| 656 | } |
| 657 | }/*processPost()*/; |
| 658 | }/*end static init*/ |
| 659 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 660 | if('visible'===document.visibilityState){ |
| 661 | if(Chat.changesSincePageHidden){ |
| 662 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -31,15 +31,10 @@ | |
| 31 | changesSincePageHidden: 0, |
| 32 | notificationBubbleColor: 'white', |
| 33 | totalMessageCount: 0, // total # of inbound messages |
| 34 | //! Number of messages to load for the history buttons |
| 35 | loadMessageCount: Math.abs(F.config.chat.initSize || 20), |
| 36 | ajaxInflight: 0, |
| 37 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 38 | taking into account single- vs multi-line input. The getter returns |
| 39 | a string and the setter returns this object. */ |
| 40 | inputValue: function(){ |
| @@ -138,10 +133,17 @@ | |
| 133 | }; |
| 134 | Object.keys(cs.settings.defaults).forEach(function f(k){ |
| 135 | const v = cs.settings.get(k,f); |
| 136 | if(f===v) cs.settings.set(k,cs.settings.defaults[k]); |
| 137 | }); |
| 138 | if(window.innerWidth<window.innerHeight){ |
| 139 | /* Alignment of 'my' messages: right alignment is conventional |
| 140 | for mobile chat apps but can be difficult to read in wide |
| 141 | windows (desktop/tablet landscape mode). Can be toggled via |
| 142 | settings popup. */ |
| 143 | document.body.classList.add('my-messages-right'); |
| 144 | } |
| 145 | if(cs.settings.getBool('monospace-messages',false)){ |
| 146 | document.body.classList.add('monospace-messages'); |
| 147 | } |
| 148 | cs.e.inputCurrent = cs.e.inputSingle; |
| 149 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| @@ -223,10 +225,93 @@ | |
| 225 | } |
| 226 | }, true); |
| 227 | return cs; |
| 228 | })()/*Chat initialization*/; |
| 229 | |
| 230 | /** |
| 231 | Custom widget type for rendering messages (one message per |
| 232 | instance). These are modelled after FIELDSET elements but we |
| 233 | don't use FIELDSET because of cross-browser inconsistencies in |
| 234 | features of the FIELDSET/LEGEND combination, e.g. inability to |
| 235 | align legends via CSS in Firefox and clicking-related |
| 236 | deficiencies in Safari. |
| 237 | */ |
| 238 | const MessageWidget = (function(){ |
| 239 | const cf = function(){ |
| 240 | this.e = { |
| 241 | body: D.addClass(D.div(), 'message-widget'), |
| 242 | tab: D.addClass(D.span(), 'message-widget-tab'), |
| 243 | content: D.addClass(D.div(), 'message-widget-content') |
| 244 | }; |
| 245 | D.append(this.e.body, this.e.tab); |
| 246 | D.append(this.e.body, this.e.content); |
| 247 | this.e.tab.setAttribute('role', 'button'); |
| 248 | }; |
| 249 | cf.prototype = { |
| 250 | setLabel: function(label){ |
| 251 | return this; |
| 252 | }, |
| 253 | setPopupCallback: function(callback){ |
| 254 | this.e.tab.addEventListener('click', callback, false); |
| 255 | return this; |
| 256 | }, |
| 257 | setMessage: function(m){ |
| 258 | const ds = this.e.body.dataset; |
| 259 | ds.timestamp = m.mtime; |
| 260 | ds.msgid = m.msgid; |
| 261 | ds.xfrom = m.xfrom; |
| 262 | if(m.xfrom === Chat.me){ |
| 263 | D.addClass(this.e.body, 'mine'); |
| 264 | } |
| 265 | this.e.content.style.backgroundColor = m.uclr; |
| 266 | this.e.tab.style.backgroundColor = m.uclr; |
| 267 | |
| 268 | const d = new Date(m.mtime); |
| 269 | D.append( |
| 270 | D.clearElement(this.e.tab), D.text( |
| 271 | m.xfrom+' @ '+d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3)) |
| 272 | ); |
| 273 | var contentTarget = this.e.content; |
| 274 | if( m.fsize>0 ){ |
| 275 | if( m.fmime |
| 276 | && m.fmime.startsWith("image/") |
| 277 | && Chat.settings.getBool('images-inline',true) |
| 278 | ){ |
| 279 | contentTarget.appendChild(D.img("chat-download/" + m.msgid)); |
| 280 | }else{ |
| 281 | const a = D.a( |
| 282 | window.fossil.rootPath+ |
| 283 | 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname), |
| 284 | // ^^^ add m.fname to URL to cause downloaded file to have that name. |
| 285 | "(" + m.fname + " " + m.fsize + " bytes)" |
| 286 | ) |
| 287 | D.attr(a,'target','_blank'); |
| 288 | contentTarget.appendChild(a); |
| 289 | } |
| 290 | contentTarget = D.div(); |
| 291 | } |
| 292 | if(m.xmsg){ |
| 293 | if(contentTarget !== this.e.content){ |
| 294 | D.append(this.e.content, contentTarget); |
| 295 | } |
| 296 | // The m.xmsg text comes from the same server as this script and |
| 297 | // is guaranteed by that server to be "safe" HTML - safe in the |
| 298 | // sense that it is not possible for a malefactor to inject HTML |
| 299 | // or javascript or CSS. The m.xmsg content might contain |
| 300 | // hyperlinks, but otherwise it will be markup-free. See the |
| 301 | // chat_format_to_html() routine in the server for details. |
| 302 | // |
| 303 | // Hence, even though innerHTML is normally frowned upon, it is |
| 304 | // perfectly safe to use in this context. |
| 305 | contentTarget.innerHTML = m.xmsg; |
| 306 | } |
| 307 | return this; |
| 308 | } |
| 309 | }; |
| 310 | return cf; |
| 311 | })()/*MessageWidget*/; |
| 312 | |
| 313 | const BlobXferState = (function(){/*drag/drop bits...*/ |
| 314 | /* State for paste and drag/drop */ |
| 315 | const bxs = { |
| 316 | dropDetails: document.querySelector('#chat-drop-details'), |
| 317 | blob: undefined, |
| @@ -413,19 +498,21 @@ | |
| 498 | D.clearElement(this.e); |
| 499 | return this.show(false); |
| 500 | }; |
| 501 | }/*end static init*/ |
| 502 | const rect = ev.target.getBoundingClientRect(); |
| 503 | const eMsg = ev.target.parentNode/*the owning .message-widget element*/; |
| 504 | f.popup._eMsg = eMsg; |
| 505 | console.debug("eMsg, rect", eMsg, rect); |
| 506 | let x = rect.left, y = rect.topm; |
| 507 | f.popup.show(ev.target)/*so we can get its computed size*/; |
| 508 | if(eMsg.dataset.xfrom===Chat.me |
| 509 | && document.body.classList.contains('my-messages-right')){ |
| 510 | // Shift popup to the left for right-aligned messages to avoid |
| 511 | // truncation off the right edge of the page. |
| 512 | const pRect = f.popup.e.getBoundingClientRect(); |
| 513 | x = rect.right - pRect.width; |
| 514 | } |
| 515 | f.popup.show(x, y); |
| 516 | }/*handleLegendClicked()*/; |
| 517 | |
| 518 | (function(){/*Set up #chat-settings-button */ |
| @@ -495,21 +582,13 @@ | |
| 582 | iws.backgroundColor = f.initialBg; |
| 583 | } |
| 584 | } |
| 585 | },{ |
| 586 | label: "Left-align my posts", |
| 587 | boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 588 | callback: function f(){ |
| 589 | document.body.classList.toggle('my-messages-right'); |
| 590 | } |
| 591 | },{ |
| 592 | label: "Images inline", |
| 593 | boolValue: ()=>Chat.settings.getBool('images-inline'), |
| 594 | callback: function(){ |
| @@ -586,76 +665,14 @@ | |
| 665 | if( m.mdel ){ |
| 666 | /* A record deletion notice. */ |
| 667 | Chat.deleteMessageElem(m.mdel); |
| 668 | return; |
| 669 | } |
| 670 | const row = new MessageWidget() |
| 671 | row.setMessage(m); |
| 672 | row.setPopupCallback(handleLegendClicked); |
| 673 | Chat.injectMessageElem(row.e.body,atEnd); |
| 674 | }/*processPost()*/; |
| 675 | }/*end static init*/ |
| 676 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 677 | if('visible'===document.visibilityState){ |
| 678 | if(Chat.changesSincePageHidden){ |
| 679 |
+16
-16
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -1473,45 +1473,45 @@ | ||
| 1473 | 1473 | body.chat span.at-name { /* for @USERNAME references */ |
| 1474 | 1474 | text-decoration: underline; |
| 1475 | 1475 | font-weight: bold; |
| 1476 | 1476 | } |
| 1477 | 1477 | /* A wrapper for a single single message (one row of the UI) */ |
| 1478 | -body.chat .message-row { | |
| 1479 | - margin-bottom: 0.5em; | |
| 1480 | - border: none; | |
| 1481 | - display: flex; | |
| 1482 | - flex-direction: row; | |
| 1483 | - justify-content: flex-start; | |
| 1484 | - /*border: 1px solid rgba(0,0,0,0.2); | |
| 1485 | - border-radius: 0.25em; | |
| 1486 | - box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);*/ | |
| 1487 | - border: none; | |
| 1478 | +body.chat .message-widget { | |
| 1479 | + margin-bottom: 0.75em; | |
| 1480 | + border: none; | |
| 1481 | + display: flex; | |
| 1482 | + flex-direction: column; | |
| 1483 | + border: none; | |
| 1484 | + align-items: flex-start; | |
| 1485 | +} | |
| 1486 | +body.chat.my-messages-right .message-widget.mine { | |
| 1487 | + align-items: flex-end; | |
| 1488 | 1488 | } |
| 1489 | 1489 | /* The content area of a message (the body element of a FIELDSET) */ |
| 1490 | -body.chat .message-content { | |
| 1490 | +body.chat .message-widget-content { | |
| 1491 | 1491 | display: inline-block; |
| 1492 | 1492 | border-radius: 0.25em; |
| 1493 | 1493 | border: 1px solid rgba(0,0,0,0.2); |
| 1494 | 1494 | box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29); |
| 1495 | 1495 | padding: 0.25em 0.5em; |
| 1496 | - margin-top: -0.75em/*slide it up to the base of the LEGEND*/; | |
| 1496 | + margin-top: 0; | |
| 1497 | 1497 | min-width: 9em /*avoid unsightly "underlap" with the user name label*/; |
| 1498 | 1498 | white-space: pre-wrap/*needed for multi-line edits*/; |
| 1499 | 1499 | } |
| 1500 | -body.chat.monospace-messages .message-content, | |
| 1500 | +body.chat.monospace-messages .message-widget-content, | |
| 1501 | 1501 | body.chat.monospace-messages textarea, |
| 1502 | 1502 | body.chat.monospace-messages input[type=text]{ |
| 1503 | 1503 | font-family: monospace; |
| 1504 | 1504 | } |
| 1505 | 1505 | |
| 1506 | 1506 | /* User name for the post (a LEGEND element) */ |
| 1507 | -body.chat .message-row .message-user { | |
| 1507 | +body.chat .message-widget .message-widget-tab { | |
| 1508 | 1508 | border-radius: 0.25em 0.25em 0 0; |
| 1509 | 1509 | padding: 0 0.5em; |
| 1510 | 1510 | /*text-align: left; Firefox requires the 'align' attribute */ |
| 1511 | - margin: 0 0.25em 0.4em 0.15em; | |
| 1512 | - padding: 0 0.5em 0em 0.5em; | |
| 1511 | + margin: 0 0.25em 0em 0.15em; | |
| 1512 | + padding: 0 0.5em 0.15em 0.5em; | |
| 1513 | 1513 | cursor: pointer; |
| 1514 | 1514 | } |
| 1515 | 1515 | |
| 1516 | 1516 | body.chat .fossil-tooltip.help-buttonlet-content { |
| 1517 | 1517 | font-size: 80%; |
| 1518 | 1518 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1473,45 +1473,45 @@ | |
| 1473 | body.chat span.at-name { /* for @USERNAME references */ |
| 1474 | text-decoration: underline; |
| 1475 | font-weight: bold; |
| 1476 | } |
| 1477 | /* A wrapper for a single single message (one row of the UI) */ |
| 1478 | body.chat .message-row { |
| 1479 | margin-bottom: 0.5em; |
| 1480 | border: none; |
| 1481 | display: flex; |
| 1482 | flex-direction: row; |
| 1483 | justify-content: flex-start; |
| 1484 | /*border: 1px solid rgba(0,0,0,0.2); |
| 1485 | border-radius: 0.25em; |
| 1486 | box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);*/ |
| 1487 | border: none; |
| 1488 | } |
| 1489 | /* The content area of a message (the body element of a FIELDSET) */ |
| 1490 | body.chat .message-content { |
| 1491 | display: inline-block; |
| 1492 | border-radius: 0.25em; |
| 1493 | border: 1px solid rgba(0,0,0,0.2); |
| 1494 | box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29); |
| 1495 | padding: 0.25em 0.5em; |
| 1496 | margin-top: -0.75em/*slide it up to the base of the LEGEND*/; |
| 1497 | min-width: 9em /*avoid unsightly "underlap" with the user name label*/; |
| 1498 | white-space: pre-wrap/*needed for multi-line edits*/; |
| 1499 | } |
| 1500 | body.chat.monospace-messages .message-content, |
| 1501 | body.chat.monospace-messages textarea, |
| 1502 | body.chat.monospace-messages input[type=text]{ |
| 1503 | font-family: monospace; |
| 1504 | } |
| 1505 | |
| 1506 | /* User name for the post (a LEGEND element) */ |
| 1507 | body.chat .message-row .message-user { |
| 1508 | border-radius: 0.25em 0.25em 0 0; |
| 1509 | padding: 0 0.5em; |
| 1510 | /*text-align: left; Firefox requires the 'align' attribute */ |
| 1511 | margin: 0 0.25em 0.4em 0.15em; |
| 1512 | padding: 0 0.5em 0em 0.5em; |
| 1513 | cursor: pointer; |
| 1514 | } |
| 1515 | |
| 1516 | body.chat .fossil-tooltip.help-buttonlet-content { |
| 1517 | font-size: 80%; |
| 1518 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1473,45 +1473,45 @@ | |
| 1473 | body.chat span.at-name { /* for @USERNAME references */ |
| 1474 | text-decoration: underline; |
| 1475 | font-weight: bold; |
| 1476 | } |
| 1477 | /* A wrapper for a single single message (one row of the UI) */ |
| 1478 | body.chat .message-widget { |
| 1479 | margin-bottom: 0.75em; |
| 1480 | border: none; |
| 1481 | display: flex; |
| 1482 | flex-direction: column; |
| 1483 | border: none; |
| 1484 | align-items: flex-start; |
| 1485 | } |
| 1486 | body.chat.my-messages-right .message-widget.mine { |
| 1487 | align-items: flex-end; |
| 1488 | } |
| 1489 | /* The content area of a message (the body element of a FIELDSET) */ |
| 1490 | body.chat .message-widget-content { |
| 1491 | display: inline-block; |
| 1492 | border-radius: 0.25em; |
| 1493 | border: 1px solid rgba(0,0,0,0.2); |
| 1494 | box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29); |
| 1495 | padding: 0.25em 0.5em; |
| 1496 | margin-top: 0; |
| 1497 | min-width: 9em /*avoid unsightly "underlap" with the user name label*/; |
| 1498 | white-space: pre-wrap/*needed for multi-line edits*/; |
| 1499 | } |
| 1500 | body.chat.monospace-messages .message-widget-content, |
| 1501 | body.chat.monospace-messages textarea, |
| 1502 | body.chat.monospace-messages input[type=text]{ |
| 1503 | font-family: monospace; |
| 1504 | } |
| 1505 | |
| 1506 | /* User name for the post (a LEGEND element) */ |
| 1507 | body.chat .message-widget .message-widget-tab { |
| 1508 | border-radius: 0.25em 0.25em 0 0; |
| 1509 | padding: 0 0.5em; |
| 1510 | /*text-align: left; Firefox requires the 'align' attribute */ |
| 1511 | margin: 0 0.25em 0em 0.15em; |
| 1512 | padding: 0 0.5em 0.15em 0.5em; |
| 1513 | cursor: pointer; |
| 1514 | } |
| 1515 | |
| 1516 | body.chat .fossil-tooltip.help-buttonlet-content { |
| 1517 | font-size: 80%; |
| 1518 |