Fossil

Check-in [dfc20f4297]
Login

Check-in [dfc20f4297]

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

Overview
Comment:Implement bottom-up and top-down chat layouts in chat-only mode and normal mode. There is a minor scroll-on-new-message quirk or two to resolve, but it otherwise seems to work.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | chat-mode-bottom-up
Files: files | file ages | folders
SHA3-256: dfc20f429713e4c46e26a6d0d421162260314f32322926b6fb3dc5a88868f11a
User & Date: stephan 2020-12-26 15:40:13.135
Context
2020-12-26
16:20
Disabled position:sticky on the input area when in bottom-up chat mode pending resolution of a scrolling misbehaviour for messages with IMG tags. IMG.src is loaded async, so the scrolling is actually working but loading of the IMG.src is then pushing the message back down behind/under the input field. ... (check-in: 8e12b61b50 user: stephan tags: chat-mode-bottom-up)
15:40
Implement bottom-up and top-down chat layouts in chat-only mode and normal mode. There is a minor scroll-on-new-message quirk or two to resolve, but it otherwise seems to work. ... (check-in: dfc20f4297 user: stephan tags: chat-mode-bottom-up)
01:37
Settings menu now closes if a click or ESC happens outside of the menu. ... (check-in: 1f00036884 user: stephan tags: chat-mode-bottom-up)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
501
502
503
504
505
506
507
508


509
510
511
512
513
514
515
}

/*
** WEBPAGE: chat-download
**
** Download the CHAT.FILE attachment associated with a single chat
** entry.  The "name" query parameter begins with an integer that
** identifies the particular chat message.


*/
void chat_download_webpage(void){
  int msgid;
  Blob r;
  const char *zMime;
  login_check_credentials();
  if( !g.perm.Chat ){







|
>
>







501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
}

/*
** WEBPAGE: chat-download
**
** Download the CHAT.FILE attachment associated with a single chat
** entry.  The "name" query parameter begins with an integer that
** identifies the particular chat message. The integer may be followed
** by a / and a filename, which will indicate to the browser to use
** the indicated name when saving the file.
*/
void chat_download_webpage(void){
  int msgid;
  Blob r;
  const char *zMime;
  login_check_credentials();
  if( !g.perm.Chat ){
Changes to src/chat.js.
19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
        inputWrapper: E1("#chat-input-area"),
        messagesWrapper: E1('#chat-messages-wrapper'),
        inputForm: E1('#chat-form'),
        btnSubmit: E1('#chat-message-submit'),
        inputSingle: E1('#chat-input-single'),
        inputMulti: E1('#chat-input-multi'),
        inputCurrent: undefined/*one of inputSingle or inputMulti*/,
        inputFile: E1('#chat-input-file')

      },
      me: F.user.name,
      mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
      mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
      pageIsActive: 'visible'===document.visibilityState,
      changesSincePageHidden: 0,
      notificationBubbleColor: 'white',







|
>







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
        inputWrapper: E1("#chat-input-area"),
        messagesWrapper: E1('#chat-messages-wrapper'),
        inputForm: E1('#chat-form'),
        btnSubmit: E1('#chat-message-submit'),
        inputSingle: E1('#chat-input-single'),
        inputMulti: E1('#chat-input-multi'),
        inputCurrent: undefined/*one of inputSingle or inputMulti*/,
        inputFile: E1('#chat-input-file'),
        contentDiv: E1('div.content')
      },
      me: F.user.name,
      mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
      mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
      pageIsActive: 'visible'===document.visibilityState,
      changesSincePageHidden: 0,
      notificationBubbleColor: 'white',
62
63
64
65
66
67
68

69
70
71
72
73
74
75
          this.e.inputCurrent = this.e.inputMulti;
        }else{
          this.e.inputCurrent = this.e.inputSingle;
        }
        D.addClass(old, 'hidden');
        D.removeClass(this.e.inputCurrent, 'hidden');
        this.e.inputCurrent.value = old.value;

        return this;
      },
      /** Enables (if yes is truthy) or disables all elements in
       * this.disableDuringAjax. */
      enableAjaxComponents: function(yes){
        D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
        return this;







>







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
          this.e.inputCurrent = this.e.inputMulti;
        }else{
          this.e.inputCurrent = this.e.inputSingle;
        }
        D.addClass(old, 'hidden');
        D.removeClass(this.e.inputCurrent, 'hidden');
        this.e.inputCurrent.value = old.value;
        old.value = '';
        return this;
      },
      /** Enables (if yes is truthy) or disables all elements in
       * this.disableDuringAjax. */
      enableAjaxComponents: function(yes){
        D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
        return this;
113
114
115
116
117
118
119
120
121
122
123
124
125
126

127
128
129
130
131
132
133

134




135






136
137
138
139
140
141
142
      injectMessageElem: function f(e, atEnd){
        const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
        if(atEnd){
          mip.parentNode.insertBefore(e, mip);
        }else{
          if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
          else mip.parentNode.appendChild(e);
          if(this.isChatOnlyMode()){
            e.scrollIntoView();
          }else{
            //const rect = e.getBoundingClientRect();
            //const rect = this.e.inputWrapper.getBoundingClientRect();
            //window.scrollBy(0, -cs.height);
            //console.debug("rect =",rect);

            //window.scrollBy(0,rect.height);
            //window.scrollTo(0,rect.top);
            //e.querySelector('.message-widget-tab').scrollIntoView();
          }
        }
      },
      isChatOnlyMode: function(){

        return document.body.classList.contains('chat-only-mode');




      },






      chatOnlyMode: function f(yes){
        if(undefined === f.elemsToToggle){
          f.elemsToToggle = [];
          document.body.childNodes.forEach(function(e){
            if(!e.classList) return/*TEXT nodes and such*/;
            else if(!e.classList.contains('content')
                    && !e.classList.contains('fossil-PopupWidget')







|
|
<
<
|
|
<
>
|
|
|



<
>
|
>
>
>
>

>
>
>
>
>
>







115
116
117
118
119
120
121
122
123


124
125

126
127
128
129
130
131
132

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
      injectMessageElem: function f(e, atEnd){
        const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
        if(atEnd){
          mip.parentNode.insertBefore(e, mip);
        }else{
          if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
          else mip.parentNode.appendChild(e);
          if(this.isUiFlipped()){
            /* When UI is flipped, new messages start out under the


               text input area because of its position:sticky
               style. We have to scroll them up. When the page footer

               is not hidden but is not on-screen, this causes a
               slight amount of UI jarring as the footer is *also*
               scrolled into view (for whatever reason). */
            setTimeout(()=>e.scrollIntoView(), 0);
          }
        }
      },

      /** Returns true if chat-only mode is enabled. */
      isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
      /** Returns true if the UI seems to be in "bottom-up" mode. */
      isUiFlipped: function(){
        const style = window.getComputedStyle(this.e.contentDiv);
        return style.flexDirection.indexOf("-reverse")>0;
      },
      /**
         Enters (if passed a truthy value or no arguments) or leaves
         "chat-only" mode. That mode hides the page's header and
         footer, leaving only the chat application visible to the
         user.
      */
      chatOnlyMode: function f(yes){
        if(undefined === f.elemsToToggle){
          f.elemsToToggle = [];
          document.body.childNodes.forEach(function(e){
            if(!e.classList) return/*TEXT nodes and such*/;
            else if(!e.classList.contains('content')
                    && !e.classList.contains('fossil-PopupWidget')
151
152
153
154
155
156
157
158


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

175
176
177

178
179
180
181
182
183
184
185

186
187
188



189
190
191
192
193
194
195
          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');
          setTimeout(()=>document.body.scrollIntoView(
            /*moves to (0,0), whereas scrollTo(0,0) does not!*/


          ), 0);
        }
        const msg = document.querySelector('.message-widget');
        if(msg) msg.scrollIntoView();
        return this;
      },
      toggleChatOnlyMode: function(){
        return this.chatOnlyMode(!this.isChatOnlyMode());
      },
      settings:{
        get: (k,dflt)=>F.storage.get(k,dflt),
        getBool: (k,dflt)=>F.storage.getBool(k,dflt),
        set: (k,v)=>F.storage.set(k,v),
        defaults:{
          "images-inline": !!F.config.chat.imagesInline,
          "monospace-messages": false

        }
      }
    };

    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;

    if(true){







|
>
>



|











|
>



>
|
|
|




|
>
|


>
>
>







161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
          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');
          setTimeout(()=>document.body.scrollIntoView(
            /*moves to (0,0), whereas scrollTo(0,0) does not!
             setTimeout() is unfortunately necessary to get the scroll
             placement correct.*/
          ), 0);
        }
        const msg = document.querySelector('.message-widget');
        if(msg) setTimeout(()=>msg.scrollIntoView(),0);
        return this;
      },
      toggleChatOnlyMode: function(){
        return this.chatOnlyMode(!this.isChatOnlyMode());
      },
      settings:{
        get: (k,dflt)=>F.storage.get(k,dflt),
        getBool: (k,dflt)=>F.storage.getBool(k,dflt),
        set: (k,v)=>F.storage.set(k,v),
        defaults:{
          "images-inline": !!F.config.chat.imagesInline,
          "monospace-messages": false,
          "bottom-up": true
        }
      }
    };
    /* Install default settings... */
    Object.keys(cs.settings.defaults).forEach(function(k){
      const v = cs.settings.get(k,cs);
      if(cs===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), so we default to a
         layout based on the apparently "orientation" of the window:
         tall vs wide. Can be toggled via settings popup. */
      document.body.classList.add('my-messages-right');
    }
    if(cs.settings.getBool("bottom-up")){
      document.body.classList.add('chat-bottom-up');
    }
    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;

    if(true){
325
326
327
328
329
330
331
332


333

334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352

353
354



355
356
357
358
359
360
361
362
          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







|
>
>
|
>


















<
>


>
>
>
|







343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372

373
374
375
376
377
378
379
380
381
382
383
384
385
386
          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," #",m.msgid,' @ ',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);
          }

          ;
        }
        if(m.xmsg){
          if(m.fsize>0){
            /* We have file/image content, so need another element for
               the message text. */
            contentTarget = D.div();
            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
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
            btnDeleteGlobal.addEventListener('click', function(){
              self.hide();
              Chat.deleteMessage(eMsg);
            });
          }
        }/*refresh()*/
      });
      f.popup.installClickToHide();
      f.popup.hide = function(){
        delete this._eMsg;
        D.clearElement(this.e);
        return this.show(false);
      };
    }/*end static init*/
    const rect = ev.target.getBoundingClientRect();







|







574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
            btnDeleteGlobal.addEventListener('click', function(){
              self.hide();
              Chat.deleteMessage(eMsg);
            });
          }
        }/*refresh()*/
      });
      f.popup.installHideHandlers();
      f.popup.hide = function(){
        delete this._eMsg;
        D.clearElement(this.e);
        return this.show(false);
      };
    }/*end static init*/
    const rect = ev.target.getBoundingClientRect();
606
607
608
609
610
611
612








613
614
615
616
617
618
619
      }
    },{
      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);
        F.toast.message("Image mode set to "+(v ? "hyperlink" : "inline")+".");







>
>
>
>
>
>
>
>







630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
      }
    },{
      label: "Left-align my posts",
      boolValue: ()=>!document.body.classList.contains('my-messages-right'),
      callback: function f(){
        document.body.classList.toggle('my-messages-right');
      }
    },{
      label: "Bottom-up chat",
      boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
      callback: function(){
        document.body.classList.toggle('chat-bottom-up');
        Chat.settings.set('bottom-up',
                          document.body.classList.contains('chat-bottom-up'));
      }
    },{
      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);
        F.toast.message("Image mode set to "+(v ? "hyperlink" : "inline")+".");
640
641
642
643
644
645
646
647
648
649





650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
          D.append(line, check);
          check.addEventListener('click', callback);
        }
        D.append(settingsPopup.e, line);
        btn.addEventListener('click', callback);
      });
    };
    settingsPopup.installClickToHide()
    /** Reminder: that interferes with "?" embedded within the popup,
        so cannot be used together with those. */;





    D.attr(settingsButton, 'role', 'button');
    settingsButton.addEventListener('click',function(ev){
      //ev.preventDefault();
      if(settingsPopup.isShown()) settingsPopup.hide();
      else settingsPopup.show(settingsButton);
      /* Reminder: we cannot toggle the visibility from her
       */
    }, false);

    /* Find an ideal X/Y position for the popup, directly above the settings
       button, based on the size of the popup... */
    settingsPopup.show(document.body);
    popupSize = settingsPopup.e.getBoundingClientRect();
    settingsPopup.hide();
    settingsPopup.options.adjustX = function(x){
      const rect = settingsButton.getBoundingClientRect();
      return rect.right - popupSize.width;
    };
    settingsPopup.options.adjustY = function(y){
      const rect = settingsButton.getBoundingClientRect();
      if(Chat.isChatOnlyMode()){
        return rect.top - popupSize.height -2;
      }else{
        return rect.bottom + 2;
      }
    };
  })()/*#chat-settings-button setup*/;








|
|
|
>
>
>
>
>




















|







672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
          D.append(line, check);
          check.addEventListener('click', callback);
        }
        D.append(settingsPopup.e, line);
        btn.addEventListener('click', callback);
      });
    };
    settingsPopup.installHideHandlers(false, true, true)
    /** Reminder: click-to-hide interferes with "?" embedded within
        the popup, so cannot be used together with those. Enabling
        this means, however, that tapping the menu button to toggle
        the menu cannot work because tapping the menu button while the
        menu is opened will, because of the click-to-hide handler,
        hide the menu before the button gets an event saying to toggle
        it.*/;
    D.attr(settingsButton, 'role', 'button');
    settingsButton.addEventListener('click',function(ev){
      //ev.preventDefault();
      if(settingsPopup.isShown()) settingsPopup.hide();
      else settingsPopup.show(settingsButton);
      /* Reminder: we cannot toggle the visibility from her
       */
    }, false);

    /* Find an ideal X/Y position for the popup, directly above the settings
       button, based on the size of the popup... */
    settingsPopup.show(document.body);
    popupSize = settingsPopup.e.getBoundingClientRect();
    settingsPopup.hide();
    settingsPopup.options.adjustX = function(x){
      const rect = settingsButton.getBoundingClientRect();
      return rect.right - popupSize.width;
    };
    settingsPopup.options.adjustY = function(y){
      const rect = settingsButton.getBoundingClientRect();
      if(Chat.isUiFlipped()){
        return rect.top - popupSize.height -2;
      }else{
        return rect.bottom + 2;
      }
    };
  })()/*#chat-settings-button setup*/;

Changes to src/default.css.
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
1527
1528
1529
1530
1531
1532
1533

1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
  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 and timestamp (a LEGEND-like element) */
body.chat .message-widget .message-widget-tab {
  border-radius: 0.25em 0.25em 0 0;
  padding: 0 0.5em;
  margin: 0 0.25em 0em 0.15em;
  padding: 0 0.5em 0.15em 0.5em;
  cursor: pointer;
}

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

/* The popup element for displaying message timestamps
   and deletion controls. */
body.chat .chat-message-popup {
  font-family: monospace;
  font-size: 0.8em;
  text-align: left;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 0.25em;
  z-index: 200;
}

body.chat .chat-message-popup > span { white-space: nowrap; }
/* Container for the message deletion buttons. */
body.chat .chat-message-popup > .toolbar {
  padding: 0.2em;
  margin: 0;
  border: 2px inset rgba(0,0,0,0.3);
  border-radius: 0.25em;
  display: flex;
  flex-direction: row;
  justify-content: stretch;
  flex-wrap: wrap;
}
body.chat .chat-message-popup > .toolbar > button {
  flex: 1 1 auto;
}

/* The main widget for loading more/older chat messages. */
body.chat #load-msg-toolbar  {
  border-radius: 0.25em;
  padding: 0.1em 0.2em;
  margin-bottom: 1em;
}
/* Set when chat has loaded all of the available historical
   messages */
body.chat #load-msg-toolbar.all-done {
  opacity: 0.5;
}
body.chat #load-msg-toolbar > div {
  display: flex;
  flex-direction: row;
  justify-content: stretch;







<








<



<












>















<
|





|
|







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
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546

1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
  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 and timestamp (a LEGEND-like element) */
body.chat .message-widget .message-widget-tab {
  border-radius: 0.25em 0.25em 0 0;
  padding: 0 0.5em;
  margin: 0 0.25em 0em 0.15em;
  padding: 0 0.5em 0.15em 0.5em;
  cursor: pointer;
}

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

/* The popup element for displaying message timestamps
   and deletion controls. */
body.chat .chat-message-popup {
  font-family: monospace;
  font-size: 0.8em;
  text-align: left;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 0.25em;
  z-index: 200;
}
/* Full message timestamps. */
body.chat .chat-message-popup > span { white-space: nowrap; }
/* Container for the message deletion buttons. */
body.chat .chat-message-popup > .toolbar {
  padding: 0.2em;
  margin: 0;
  border: 2px inset rgba(0,0,0,0.3);
  border-radius: 0.25em;
  display: flex;
  flex-direction: row;
  justify-content: stretch;
  flex-wrap: wrap;
}
body.chat .chat-message-popup > .toolbar > button {
  flex: 1 1 auto;
}

/* The widget for loading more/older chat messages. */
body.chat #load-msg-toolbar  {
  border-radius: 0.25em;
  padding: 0.1em 0.2em;
  margin-bottom: 1em;
}
/* .all-done is set when chat has loaded all of the available
   historical messages */
body.chat #load-msg-toolbar.all-done {
  opacity: 0.5;
}
body.chat #load-msg-toolbar > div {
  display: flex;
  flex-direction: row;
  justify-content: stretch;
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
  border: 1px outset rgba(127,127,127,1);
}
body.fossil-dark-style .settings-icon {
  filter: invert(100%);
}
/* "Chat-only mode" hides the site header/footer, showing only
   the chat app. */
body.chat.chat-only-mode{
}
body.chat #chat-settings-button {
}
/** Popup widget for the /chat settings. */
body.chat .chat-settings-popup {
  font-size: 0.8em;
  text-align: left;
  display: flex;
  flex-direction: column;
  align-items: stretch;







|
<
|
<







1595
1596
1597
1598
1599
1600
1601
1602

1603

1604
1605
1606
1607
1608
1609
1610
  border: 1px outset rgba(127,127,127,1);
}
body.fossil-dark-style .settings-icon {
  filter: invert(100%);
}
/* "Chat-only mode" hides the site header/footer, showing only
   the chat app. */
body.chat.chat-only-mode{}

body.chat #chat-settings-button {}

/** Popup widget for the /chat settings. */
body.chat .chat-settings-popup {
  font-size: 0.8em;
  text-align: left;
  display: flex;
  flex-direction: column;
  align-items: stretch;
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680


1681
1682
1683
1684
1685
1686
1687
1688


1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703

1704
1705
1706
1707
1708
1709
1710
  cursor: inherit;
}
/** Container for the list of /chat messages. */
body.chat #chat-messages-wrapper {
  display: flex;
  flex-direction: column;
}
body.chat.chat-only-mode #chat-messages-wrapper {
  flex-direction: column-reverse;
  position: relative;
  top: 0;
  z-index: 99 /* so that it scrolls under input area. If it's
                 lower than div.content then mouse events to it
                 are blocked!*/;
}
body.chat.chat-only-mode > div.content {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
body.chat.chat-only-mode > div.content {
  flex-direction: column-reverse;
}
/* Wrapper for /chat user input controls */
body.chat #chat-input-area {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid black;
  padding: 0.5em 1em;
  margin-bottom: 0.5em;
  /*position: sticky; top: 0;*/
  /*position: -webkit-sticky*/ /* supposedly some versions of Safari */;
}
body.chat.chat-only-mode #chat-input-area {
  z-index: 100
    /* see notes in #chat-messages-wrapper. The various popups require a
       z-index higher than this one. */;


  border-bottom: none;
  border-top: 1px solid black;
  margin-bottom: 0;
  margin-top: 0.5em;
  position: sticky;
  position: -webkit-sticky/* supposedly some versions of Safari */;
  bottom: 0;
}


body.chat #chat-input-line {
  display: flex;
  flex-direction: row;
  margin-bottom: 0.25em;
  align-items: flex-start;
}
body.chat #chat-input-line > input[type=submit] {
  flex: 1 5 auto;
  max-width: 6em;
  margin: 0 1em;
}
body.chat #chat-input-line > input[type=text],
body.chat #chat-input-line > textarea {
  flex: 5 1 auto;
}

body.chat #chat-input-file-area  {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
}
body.chat #chat-input-file-area > .file-selection-wrapper {







|

<
<




|






|









|
<
<
<



>
>





<


>
>















>







1637
1638
1639
1640
1641
1642
1643
1644
1645


1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667



1668
1669
1670
1671
1672
1673
1674
1675
1676
1677

1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
  cursor: inherit;
}
/** Container for the list of /chat messages. */
body.chat #chat-messages-wrapper {
  display: flex;
  flex-direction: column;
}
body.chat.chat-bottom-up #chat-messages-wrapper {
  flex-direction: column-reverse;


  z-index: 99 /* so that it scrolls under input area. If it's
                 lower than div.content then mouse events to it
                 are blocked!*/;
}
body.chat > div.content {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
body.chat.chat-bottom-up > div.content {
  flex-direction: column-reverse;
}
/* Wrapper for /chat user input controls */
body.chat #chat-input-area {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid black;
  padding: 0.5em 1em;
  margin-bottom: 0.5em;
  position: sticky; top: 0;



  z-index: 100
    /* see notes in #chat-messages-wrapper. The various popups require a
       z-index higher than this one. */;
}
body.chat.chat-bottom-up #chat-input-area {
  border-bottom: none;
  border-top: 1px solid black;
  margin-bottom: 0;
  margin-top: 0.5em;
  position: sticky;

  bottom: 0;
}
/* Widget holding the chat message input field, send button, and
   settings button. */
body.chat #chat-input-line {
  display: flex;
  flex-direction: row;
  margin-bottom: 0.25em;
  align-items: flex-start;
}
body.chat #chat-input-line > input[type=submit] {
  flex: 1 5 auto;
  max-width: 6em;
  margin: 0 1em;
}
body.chat #chat-input-line > input[type=text],
body.chat #chat-input-line > textarea {
  flex: 5 1 auto;
}
/* Widget holding the file selection control and preview */
body.chat #chat-input-file-area  {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
}
body.chat #chat-input-file-area > .file-selection-wrapper {
1721
1722
1723
1724
1725
1726
1727

1728
1729
1730

1731
1732
1733
1734
1735
1736
1737
1738
  border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
  border-radius: 0.25em;
  padding: 0.25em;
}
body.chat #chat-input-file > input {
  flex: 1 0 auto;
}

body.chat #chat-input-file.dragover {
  border: 1px dashed green;
}

body.chat #chat-drop-details {
  flex: 0 1 auto;
  padding: 0.5em 1em;
  margin-left: 0.5em;
  white-space: pre;
  font-family: monospace;
  max-width: 50%;
}







>



>






<

1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732

1733
  border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
  border-radius: 0.25em;
  padding: 0.25em;
}
body.chat #chat-input-file > input {
  flex: 1 0 auto;
}
/* Indicator when a drag/drop is in progress */
body.chat #chat-input-file.dragover {
  border: 1px dashed green;
}
/* Widget holding the details of a selected/dropped file/image. */
body.chat #chat-drop-details {
  flex: 0 1 auto;
  padding: 0.5em 1em;
  margin-left: 0.5em;
  white-space: pre;
  font-family: monospace;

}
Changes to src/fossil.dom.js.
110
111
112
113
114
115
116



117

118
119
120
121
122
123
124
    const e = this.create('a');
    if(href) e.setAttribute('href',href);
    if(label) e.appendChild(dom.text(true===label ? href : label));
    return e;
  };
  dom.hr = dom.createElemFactory('hr');
  dom.br = dom.createElemFactory('br');



  dom.text = (t)=>document.createTextNode(t||'');

  dom.button = function(label){
    const b = this.create('button');
    if(label) b.appendChild(this.text(label));
    return b;
  };
  /**
     Returns a TEXTAREA element.







>
>
>
|
>







110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
    const e = this.create('a');
    if(href) e.setAttribute('href',href);
    if(label) e.appendChild(dom.text(true===label ? href : label));
    return e;
  };
  dom.hr = dom.createElemFactory('hr');
  dom.br = dom.createElemFactory('br');
  /** Returns a new TEXT node which contains the text of all of the
      arguments appended together. */
  dom.text = function(/*...*/){
    return document.createTextNode(argsToArray(arguments).join(''));
  };
  dom.button = function(label){
    const b = this.create('button');
    if(label) b.appendChild(this.text(label));
    return b;
  };
  /**
     Returns a TEXTAREA element.
Changes to src/fossil.popupwidget.js.
215
216
217
218
219
220
221









222
223



224
225

226
227
228
229

230
231
232
233
234
235
236
    hide: function(){return this.show(false)},

    /**
       A convenience method which adds click handlers to this popup's
       main element and document.body to hide (via hide()) the popup
       when either element is clicked or the ESC key is pressed. Only
       call this once per instance, if at all. Returns this;









    */
    installClickToHide: function f(){



      this.e.addEventListener('click', ()=>this.hide(), false);
      document.body.addEventListener('click', ()=>this.hide(), true);

      const self = this;
      document.body.addEventListener('keydown', function(ev){
        if(self.isShown() && 27===ev.which) self.hide();
      }, true);

      return this;
    }
  }/*F.PopupWidget.prototype*/;

  /**
     Internal impl for F.toast() and friends.








>
>
>
>
>
>
>
>
>

|
>
>
>
|
|
>
|
|
|
|
>







215
216
217
218
219
220
221
222
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
    hide: function(){return this.show(false)},

    /**
       A convenience method which adds click handlers to this popup's
       main element and document.body to hide (via hide()) the popup
       when either element is clicked or the ESC key is pressed. Only
       call this once per instance, if at all. Returns this;

       The first argument specifies whether a click handler on this
       object is installed. The second specifies whether a click
       outside of this object should close it. The third specifies
       whether an ESC handler is installed.

       Passing no arguments is equivalent to passing (true,true,true),
       and passing fewer arguments defaults the unpassed parameters to
       true.
    */
    installHideHandlers: function f(onClickSelf, onClickOther, onEsc){
      if(!arguments.length) onClick = onClickOther = onEsc = true;
      else if(2===arguments.length) onClickOther = onEsc = true;
      else if(1===arguments.length) onEsc = true;
      if(onClickSelf) this.e.addEventListener('click', ()=>this.hide(), false);
      if(onClickOther) document.body.addEventListener('click', ()=>this.hide(), true);
      if(onEsc){
        const self = this;
        document.body.addEventListener('keydown', function(ev){
          if(self.isShown() && 27===ev.which) self.hide();
        }, true);
      }
      return this;
    }
  }/*F.PopupWidget.prototype*/;

  /**
     Internal impl for F.toast() and friends.

340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
          if(!fch.popup){
            fch.popup = new F.PopupWidget({
              cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
              refresh: function(){
              }
            });
            fch.popup.e.style.maxWidth = '80%'/*of body*/;
            fch.popup.installClickToHide();
          }
          D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
          /* Shift the help around a bit to "better" fit the
             screen. However, fch.popup.e.getClientRects() is empty
             until the popup is shown, so we have to show it,
             calculate the resulting size, then move and/or resize it.








|







354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
          if(!fch.popup){
            fch.popup = new F.PopupWidget({
              cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
              refresh: function(){
              }
            });
            fch.popup.e.style.maxWidth = '80%'/*of body*/;
            fch.popup.installHideHandlers();
          }
          D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
          /* Shift the help around a bit to "better" fit the
             screen. However, fch.popup.e.getClientRects() is empty
             until the popup is shown, so we have to show it,
             calculate the resulting size, then move and/or resize it.