Fossil

Check-in [852bda77bd]
Login

Check-in [852bda77bd]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:chat: refactored the messages from fieldsets to a custom widget.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | chat-widget-rework
Files: files | file ages | folders
SHA3-256: 852bda77bd5e4326144bab68ad1e7d945a33be3c5566e924df29324bcc1248e0
User & Date: stephan 2020-12-25 22:35:35.708
Context
2020-12-25
22:37
Removed some dead CSS. ... (Closed-Leaf check-in: c9ca5198be user: stephan tags: chat-widget-rework)
22:35
chat: refactored the messages from fieldsets to a custom widget. ... (check-in: 852bda77bd user: stephan tags: chat-widget-rework)
20:30
For the chat function, the server-to-client JSON uses strict ISO8601 time strings, including the "T" in the middle and the "Z" at the end. ... (check-in: 13c95f0c75 user: drh tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.js.
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
      mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
      pageIsActive: 'visible'===document.visibilityState,
      changesSincePageHidden: 0,
      notificationBubbleColor: 'white',
      totalMessageCount: 0, // total # of inbound messages
      //! Number of messages to load for the history buttons
      loadMessageCount: Math.abs(F.config.chat.initSize || 20),
      /* Alignment of 'my' messages: must be 'left' or 'right'. Note
         that 'right' is conventional for mobile chat apps but can be
         difficult to read in wide windows (desktop/tablet landscape
         mode). Can be toggled via settings popup. */
      msgMyAlign: (window.innerWidth<window.innerHeight) ? 'right' : 'left',
      ajaxInflight: 0,
      /** Gets (no args) or sets (1 arg) the current input text field value,
          taking into account single- vs multi-line input. The getter returns
          a string and the setter returns this object. */
      inputValue: function(){
        const e = this.inputElement();
        if(arguments.length){







<
<
<
<
<







29
30
31
32
33
34
35





36
37
38
39
40
41
42
      mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
      pageIsActive: 'visible'===document.visibilityState,
      changesSincePageHidden: 0,
      notificationBubbleColor: 'white',
      totalMessageCount: 0, // total # of inbound messages
      //! Number of messages to load for the history buttons
      loadMessageCount: Math.abs(F.config.chat.initSize || 20),





      ajaxInflight: 0,
      /** Gets (no args) or sets (1 arg) the current input text field value,
          taking into account single- vs multi-line input. The getter returns
          a string and the setter returns this object. */
      inputValue: function(){
        const e = this.inputElement();
        if(arguments.length){
136
137
138
139
140
141
142







143
144
145
146
147
148
149
        }
      }
    };
    Object.keys(cs.settings.defaults).forEach(function f(k){
      const v = cs.settings.get(k,f);
      if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
    });







    if(cs.settings.getBool('monospace-messages',false)){
      document.body.classList.add('monospace-messages');
    }
    cs.e.inputCurrent = cs.e.inputSingle;
    cs.pageTitleOrig = cs.e.pageTitle.innerText;
    const qs = (e)=>document.querySelector(e);
    const argsToArray = function(args){







>
>
>
>
>
>
>







131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
        }
      }
    };
    Object.keys(cs.settings.defaults).forEach(function f(k){
      const v = cs.settings.get(k,f);
      if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
    });
    if(window.innerWidth<window.innerHeight){
      /* Alignment of 'my' messages: right alignment is conventional
         for mobile chat apps but can be difficult to read in wide
         windows (desktop/tablet landscape mode). Can be toggled via
         settings popup. */
      document.body.classList.add('my-messages-right');
    }
    if(cs.settings.getBool('monospace-messages',false)){
      document.body.classList.add('monospace-messages');
    }
    cs.e.inputCurrent = cs.e.inputSingle;
    cs.pageTitleOrig = cs.e.pageTitle.innerText;
    const qs = (e)=>document.querySelector(e);
    const argsToArray = function(args){
221
222
223
224
225
226
227



















































































228
229
230
231
232
233
234
      if(cs.pageIsActive){
        cs.e.pageTitle.innerText = cs.pageTitleOrig;
      }
    }, true);
    return cs;
  })()/*Chat initialization*/;




















































































  const BlobXferState = (function(){/*drag/drop bits...*/
    /* State for paste and drag/drop */
    const bxs = {
      dropDetails: document.querySelector('#chat-drop-details'),
      blob: undefined,
      clear: function(){
        this.blob = undefined;







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
      if(cs.pageIsActive){
        cs.e.pageTitle.innerText = cs.pageTitleOrig;
      }
    }, true);
    return cs;
  })()/*Chat initialization*/;

  /**
     Custom widget type for rendering messages (one message per
     instance). These are modelled after FIELDSET elements but we
     don't use FIELDSET because of cross-browser inconsistencies in
     features of the FIELDSET/LEGEND combination, e.g. inability to
     align legends via CSS in Firefox and clicking-related
     deficiencies in Safari.
  */
  const MessageWidget = (function(){
    const cf = function(){
      this.e = {
        body: D.addClass(D.div(), 'message-widget'),
        tab: D.addClass(D.span(), 'message-widget-tab'),
        content: D.addClass(D.div(), 'message-widget-content')
      };
      D.append(this.e.body, this.e.tab);
      D.append(this.e.body, this.e.content);
      this.e.tab.setAttribute('role', 'button');
    };
    cf.prototype = {
      setLabel: function(label){
        return this;
      },
      setPopupCallback: function(callback){
        this.e.tab.addEventListener('click', callback, false);
        return this;
      },
      setMessage: function(m){
        const ds = this.e.body.dataset;
        ds.timestamp = m.mtime;
        ds.msgid = m.msgid;
        ds.xfrom = m.xfrom;
        if(m.xfrom === Chat.me){
          D.addClass(this.e.body, 'mine');
        }
        this.e.content.style.backgroundColor = m.uclr;
        this.e.tab.style.backgroundColor = m.uclr;
          
        const d = new Date(m.mtime);
        D.append(
          D.clearElement(this.e.tab), D.text(
            m.xfrom+' @ '+d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3))
        );
        var contentTarget = this.e.content;
        if( m.fsize>0 ){
          if( m.fmime
              && m.fmime.startsWith("image/")
              && Chat.settings.getBool('images-inline',true)
            ){
            contentTarget.appendChild(D.img("chat-download/" + m.msgid));
          }else{
            const a = D.a(
              window.fossil.rootPath+
                'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
              // ^^^ add m.fname to URL to cause downloaded file to have that name.
              "(" + m.fname + " " + m.fsize + " bytes)"
            )
            D.attr(a,'target','_blank');
            contentTarget.appendChild(a);
          }
          contentTarget = D.div();
        }
        if(m.xmsg){
          if(contentTarget !== this.e.content){
            D.append(this.e.content, contentTarget);
          }
          // The m.xmsg text comes from the same server as this script and
          // is guaranteed by that server to be "safe" HTML - safe in the
          // sense that it is not possible for a malefactor to inject HTML
          // or javascript or CSS.  The m.xmsg content might contain
          // hyperlinks, but otherwise it will be markup-free.  See the
          // chat_format_to_html() routine in the server for details.
          //
          // Hence, even though innerHTML is normally frowned upon, it is
          // perfectly safe to use in this context.
          contentTarget.innerHTML = m.xmsg;
        }
        return this;
      }
    };
    return cf;
  })();
  
  const BlobXferState = (function(){/*drag/drop bits...*/
    /* State for paste and drag/drop */
    const bxs = {
      dropDetails: document.querySelector('#chat-drop-details'),
      blob: undefined,
      clear: function(){
        this.blob = undefined;
411
412
413
414
415
416
417
418
419

420
421

422
423
424
425
426
427
428
429
430
431
432
433
      f.popup.hide = function(){
        delete this._eMsg;
        D.clearElement(this.e);
        return this.show(false);
      };
    }/*end static init*/
    const rect = ev.target.getBoundingClientRect();
    const eMsg = ev.target.parentNode/*the owning fieldset element*/;
    f.popup._eMsg = eMsg;

    let x = rect.left, y = rect.top - 10;
    f.popup.show(ev.target)/*so we can get its computed size*/;

    if('right'===ev.target.getAttribute('align')){
      // Shift popup to the left for right-aligned messages to avoid
      // truncation off the right edge of the page.
      const pRect = f.popup.e.getBoundingClientRect();
      x -= pRect.width/3*2;
    }
    f.popup.show(x, y);
  }/*handleLegendClicked()*/;

  (function(){/*Set up #chat-settings-button */
    const settingsButton = document.querySelector('#chat-settings-button');
    var popupSize = undefined/*placement workaround*/;







|

>
|

>
|



|







496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
      f.popup.hide = function(){
        delete this._eMsg;
        D.clearElement(this.e);
        return this.show(false);
      };
    }/*end static init*/
    const rect = ev.target.getBoundingClientRect();
    const eMsg = ev.target.parentNode/*the owning .message-widget element*/;
    f.popup._eMsg = eMsg;
    console.debug("eMsg, rect", eMsg, rect);
    let x = rect.left, y = rect.topm;
    f.popup.show(ev.target)/*so we can get its computed size*/;
    if(eMsg.dataset.xfrom===Chat.me
       && document.body.classList.contains('my-messages-right')){
      // Shift popup to the left for right-aligned messages to avoid
      // truncation off the right edge of the page.
      const pRect = f.popup.e.getBoundingClientRect();
      x = rect.right - pRect.width;
    }
    f.popup.show(x, y);
  }/*handleLegendClicked()*/;

  (function(){/*Set up #chat-settings-button */
    const settingsButton = document.querySelector('#chat-settings-button');
    var popupSize = undefined/*placement workaround*/;
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
          D.removeClass(f.elemsToToggle, 'hidden');
          D.removeClass(document.body, 'chat-only-mode');
          iws.backgroundColor = f.initialBg;
        }
      }
    },{
      label: "Left-align my posts",
      boolValue: ()=>'left'===Chat.msgMyAlign,
      callback: function f(){
        if('right'===Chat.msgMyAlign) Chat.msgMyAlign = 'left';
        else Chat.msgMyAlign = 'right';
        const msgs = Chat.e.messagesWrapper.querySelectorAll('.message-row');
        msgs.forEach(function(row){
          if(row.dataset.xfrom!==Chat.me) return;
          row.querySelector('legend').setAttribute('align', Chat.msgMyAlign);
          if('right'===Chat.msgMyAlign) row.style.justifyContent = "flex-end";
          else row.style.justifyContent = "flex-start";
        });
      }
    },{
      label: "Images inline",
      boolValue: ()=>Chat.settings.getBool('images-inline'),
      callback: function(){
        const v = Chat.settings.getBool('images-inline',true);
        Chat.settings.set('images-inline', !v);







|

<
|
<
<
<
<
<
<
<







580
581
582
583
584
585
586
587
588

589







590
591
592
593
594
595
596
          D.removeClass(f.elemsToToggle, 'hidden');
          D.removeClass(document.body, 'chat-only-mode');
          iws.backgroundColor = f.initialBg;
        }
      }
    },{
      label: "Left-align my posts",
      boolValue: ()=>!document.body.classList.contains('my-messages-right'),
      callback: function f(){

        document.body.classList.toggle('my-messages-right');







      }
    },{
      label: "Images inline",
      boolValue: ()=>Chat.settings.getBool('images-inline'),
      callback: function(){
        const v = Chat.settings.getBool('images-inline',true);
        Chat.settings.set('images-inline', !v);
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
        if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
        if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
        if( m.mdel ){
          /* A record deletion notice. */
          Chat.deleteMessageElem(m.mdel);
          return;
        }
        const eWho = D.create('legend'),
              row = D.addClass(D.fieldset(eWho), 'message-row');
        row.dataset.msgid = m.msgid;
        row.dataset.xfrom = m.xfrom;
        row.dataset.timestamp = m.mtime;
        Chat.injectMessageElem(row,atEnd);
        eWho.addEventListener('click', handleLegendClicked, false);
        eWho.setAttribute('role', 'button');
        if( m.xfrom==Chat.me ){
          eWho.setAttribute('align', Chat.msgMyAlign);
          if('right'===Chat.msgMyAlign){
            row.style.justifyContent = "flex-end";
          }else{
            row.style.justifyContent = "flex-start";
          }
        }else{
          eWho.setAttribute('align', 'left');
        }
        eWho.style.backgroundColor = m.uclr;
        eWho.classList.add('message-user');
        let whoName = m.xfrom;
        var d = new Date(m.mtime);
        if( d.getMinutes().toString()!="NaN" ){
          /* Show local time when we can compute it */
          eWho.append(D.text(whoName+' @ '+
                             d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3)
                            ))
        }else{
          /* Show UTC on systems where Date() does not work */
          eWho.append(D.text(whoName+' @ '+m.mtime.slice(11,16)))
        }
        let eContent = D.addClass(D.div(),'message-content','chat-message');
        eContent.style.backgroundColor = m.uclr;
        row.appendChild(eContent);
        if( m.fsize>0 ){
          if( m.fmime
              && m.fmime.startsWith("image/")
              && Chat.settings.getBool('images-inline',true)
            ){
            eContent.appendChild(D.img("chat-download/" + m.msgid));
          }else{
            const a = D.a(
              window.fossil.rootPath+
                'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
              // ^^^ add m.fname to URL to cause downloaded file to have that name.
              "(" + m.fname + " " + m.fsize + " bytes)"
            )
            D.attr(a,'target','_blank');
            eContent.appendChild(a);
          }
          const br = D.br();
          br.style.clear = "both";
          eContent.appendChild(br);
        }
        if(m.xmsg){
          // The m.xmsg text comes from the same server as this script and
          // is guaranteed by that server to be "safe" HTML - safe in the
          // sense that it is not possible for a malefactor to inject HTML
          // or javascript or CSS.  The m.xmsg content might contain
          // hyperlinks, but otherwise it will be markup-free.  See the
          // chat_format_to_html() routine in the server for details.
          //
          // Hence, even though innerHTML is normally frowned upon, it is
          // perfectly safe to use in this context.
          eContent.innerHTML += m.xmsg
        }
      }/*processPost()*/;
    }/*end static init*/
    jx.msgs.forEach((m)=>f.processPost(m,atEnd));
    if('visible'===document.visibilityState){
      if(Chat.changesSincePageHidden){
        Chat.changesSincePageHidden = 0;
        Chat.e.pageTitle.innerText = Chat.pageTitleOrig;







|
<
|
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







663
664
665
666
667
668
669
670

671
672

673




























































674
675
676
677
678
679
680
        if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
        if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
        if( m.mdel ){
          /* A record deletion notice. */
          Chat.deleteMessageElem(m.mdel);
          return;
        }
        const row = new MessageWidget()

        row.setMessage(m);
        row.setPopupCallback(handleLegendClicked);

        Chat.injectMessageElem(row.e.body,atEnd);




























































      }/*processPost()*/;
    }/*end static init*/
    jx.msgs.forEach((m)=>f.processPost(m,atEnd));
    if('visible'===document.visibilityState){
      if(Chat.changesSincePageHidden){
        Chat.changesSincePageHidden = 0;
        Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
Changes to src/default.css.
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487

1488



1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513






1514
1515
1516
1517
1518
1519
1520

/* Chat-related */
body.chat span.at-name { /* for @USERNAME references */
  text-decoration: underline;
  font-weight: bold;
}
/* A wrapper for a single single message (one row of the UI) */
body.chat .message-row {
  margin-bottom: 0.5em;
  border: none;
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  /*border: 1px solid rgba(0,0,0,0.2);
  border-radius: 0.25em;
  box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);*/
  border: none;

}



/* The content area of a message (the body element of a FIELDSET) */
body.chat .message-content {
  display: inline-block;
  border-radius: 0.25em;
  border: 1px solid rgba(0,0,0,0.2);
  box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
  padding: 0.25em 0.5em;
  margin-top: -0.75em/*slide it up to the base of the LEGEND*/;
  min-width: 9em /*avoid unsightly "underlap" with the user name label*/;
  white-space: pre-wrap/*needed for multi-line edits*/;
}
body.chat.monospace-messages .message-content,
body.chat.monospace-messages textarea,
body.chat.monospace-messages input[type=text]{
  font-family: monospace;  
}

/* User name for the post (a LEGEND element) */
body.chat .message-row .message-user {
  border-radius: 0.25em 0.25em 0 0;
  padding: 0 0.5em;
  /*text-align: left; Firefox requires the 'align' attribute */
  margin: 0 0.25em 0.4em 0.15em;
  padding: 0 0.5em 0em 0.5em;
  cursor: pointer;






}

body.chat .fossil-tooltip.help-buttonlet-content {
  font-size: 80%;
}

body.chat .chat-message-popup {







|
|


|
<
<
<
<

>

>
>
>

|





|



|






|



|
|

>
>
>
>
>
>







1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482




1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526

/* Chat-related */
body.chat span.at-name { /* for @USERNAME references */
  text-decoration: underline;
  font-weight: bold;
}
/* A wrapper for a single single message (one row of the UI) */
body.chat .message-widget {
  margin-bottom: 0.75em;
  border: none;
  display: flex;
  flex-direction: column;




  border: none;
  align-items: flex-start;
}
body.chat.my-messages-right .message-widget.mine {
  align-items: flex-end;
}
/* The content area of a message (the body element of a FIELDSET) */
body.chat .message-widget-content {
  display: inline-block;
  border-radius: 0.25em;
  border: 1px solid rgba(0,0,0,0.2);
  box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
  padding: 0.25em 0.5em;
  margin-top: 0;
  min-width: 9em /*avoid unsightly "underlap" with the user name label*/;
  white-space: pre-wrap/*needed for multi-line edits*/;
}
body.chat.monospace-messages .message-widget-content,
body.chat.monospace-messages textarea,
body.chat.monospace-messages input[type=text]{
  font-family: monospace;  
}

/* User name for the post (a LEGEND element) */
body.chat .message-widget .message-widget-tab {
  border-radius: 0.25em 0.25em 0 0;
  padding: 0 0.5em;
  /*text-align: left; Firefox requires the 'align' attribute */
  margin: 0 0.25em 0em 0.15em;
  padding: 0 0.5em 0.15em 0.5em;
  cursor: pointer;
}
/*body.chat.my-messages-right .message-widget-tab {
}*/
body.chat .message-widget .message-widget-tab a {
  text-decoration: none;
  color: inherit;
}

body.chat .fossil-tooltip.help-buttonlet-content {
  font-size: 80%;
}

body.chat .chat-message-popup {