Fossil

Check-in [e52d0fd5ce]
Login

Check-in [e52d0fd5ce]

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

Overview
Comment:chat internal cleanups in prep for upcoming changes. Possibly fixed the cosmetic bug where the titlebar says '(0) ...' after receiving an empty list of messages in response to an auto-reconnect after a timeout.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: e52d0fd5ce1cd608600fadecaaf5fa2bac02593b6c2f2dbec35b16c1c2d06ef8
User & Date: stephan 2020-12-25 16:08:00.085
Context
2020-12-25
16:09
First attempt at documentation for Fossil chat. ... (check-in: bcfdc1a106 user: drh tags: trunk)
16:08
chat internal cleanups in prep for upcoming changes. Possibly fixed the cosmetic bug where the titlebar says '(0) ...' after receiving an empty list of messages in response to an auto-reconnect after a timeout. ... (check-in: e52d0fd5ce user: stephan tags: trunk)
15:27
Chat settings menu tweaks based on chat session feedback. ... (check-in: 9e797bf9bf user: stephan tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
  if( iPingTcp<1000 || iPingTcp>65535 ) iPingTcp = 0;
  if( iPingTcp ) style_disable_csp();
  style_set_current_feature("chat");
  style_header("Chat");
  @ <div id='chat-input-area'>
  @ <form accept-encoding="utf-8" id="chat-form" autocomplete="off">
  @   <div id='chat-input-line'>
  @     <input type="text" name="msg" id="sbox" \
  @      placeholder="Type message here.">
  @     <input type="submit" value="Send">
  @     <span id="chat-settings-button" class="settings-icon"></span>
  @   </div>
  @   <div id='chat-input-file-area'>
  @     <div class='file-selection-wrapper'>
  @       <div class='help-buttonlet'>







|







99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
  if( iPingTcp<1000 || iPingTcp>65535 ) iPingTcp = 0;
  if( iPingTcp ) style_disable_csp();
  style_set_current_feature("chat");
  style_header("Chat");
  @ <div id='chat-input-area'>
  @ <form accept-encoding="utf-8" id="chat-form" autocomplete="off">
  @   <div id='chat-input-line'>
  @     <input type="text" name="msg" id="chat-input-single" \
  @      placeholder="Type message here.">
  @     <input type="submit" value="Send">
  @     <span id="chat-settings-button" class="settings-icon"></span>
  @   </div>
  @   <div id='chat-input-file-area'>
  @     <div class='file-selection-wrapper'>
  @       <div class='help-buttonlet'>
Changes to src/chat.js.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

















37
38
39
40
41
42
43
/**
   This file contains the client-side implementation of fossil's /chat
   application. 
*/
(function(){
  const form = document.querySelector('#chat-form');
  const F = window.fossil, D = F.dom;
  const E1 = function(selector){
    const e = document.querySelector(selector);
    if(!e) throw new Error("missing required DOM element: "+selector);
    return e;
  };
  const Chat = (function(){
    const cs = {
      e:{/*map of certain DOM elements.*/
        messageInjectPoint: E1('#message-inject-point'),
        pageTitle: E1('head title'),
        loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
        inputWrapper: E1("#chat-input-area"),
        messagesWrapper: E1('#chat-messages-wrapper')



      },
      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',
      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,

















      /** Enables (if yes is truthy) or disables all elements in
       * this.disableDuringAjax. */
      enableAjaxComponents: function(yes){
        D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
        return this;
      },
      /* Must be called before any API is used which starts ajax traffic.





<













|
>
>
>
















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







1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
   This file contains the client-side implementation of fossil's /chat
   application. 
*/
(function(){

  const F = window.fossil, D = F.dom;
  const E1 = function(selector){
    const e = document.querySelector(selector);
    if(!e) throw new Error("missing required DOM element: "+selector);
    return e;
  };
  const Chat = (function(){
    const cs = {
      e:{/*map of certain DOM elements.*/
        messageInjectPoint: E1('#message-inject-point'),
        pageTitle: E1('head title'),
        loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
        inputWrapper: E1("#chat-input-area"),
        messagesWrapper: E1('#chat-messages-wrapper'),
        inputForm: E1('#chat-form'),
        inputSingle: E1('#chat-input-single'),
        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',
      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.e.inputSingle;
        if(arguments.length){
          e.value = arguments[0];
          return this;
        }else {
          return e.value;
        }
      },
      /** Asks the current user input field to take focus. Returns this. */
      inputFocus: function(){
        this.e.inputSingle.focus();
        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;
      },
      /* Must be called before any API is used which starts ajax traffic.
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
214
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
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
    /* State for paste and drag/drop */
    const bxs = {
      dropDetails: document.querySelector('#chat-drop-details'),
      blob: undefined,
      clear: function(){
        this.blob = undefined;
        D.clearElement(this.dropDetails);
        form.file.value = "";
      }
    };
    /** Updates the paste/drop zone with details of the pasted/dropped
        data. The argument must be a Blob or Blob-like object (File) or
        it can be falsy to reset/clear that state.*/
    const updateDropZoneContent = function(blob){
      const dd = bxs.dropDetails;
      bxs.blob = blob;
      D.clearElement(dd);
      if(!blob){
        form.file.value = '';
        return;
      }
      D.append(dd, "Name: ", blob.name,
               D.br(), "Size: ",blob.size);
      if(blob.type && blob.type.startsWith("image/")){
        const img = D.img();
        D.append(dd, D.br(), img);
        const reader = new FileReader();
        reader.onload = (e)=>img.setAttribute('src', e.target.result);
        reader.readAsDataURL(blob);
      }
      const btn = D.button("Cancel");
      D.append(dd, D.br(), btn);
      btn.addEventListener('click', ()=>updateDropZoneContent(), false);
    };
    form.file.addEventListener('change', function(ev){
      updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
    });
    /* Handle image paste from clipboard. TODO: figure out how we can
       paste non-image binary data as if it had been selected via the
       file selection element. */
    document.addEventListener('paste', function(event){
      const items = event.clipboardData.items,
            item = items[0];
      if(!item || !item.type) return;
      else if('file'===item.kind){
        updateDropZoneContent(false/*clear prev state*/);
        updateDropZoneContent(items[0].getAsFile());
      }else if(false && 'string'===item.kind){
        /* ----^^^^^ disabled for now: the intent here is that if
           form.msg is not active, populate it with this text, but
           whether populating it from ctrl-v when it does not have focus
           is a feature or a bug is debatable.  It seems useful but may
           violate the Principle of Least Surprise. */
        if(document.activeElement !== form.msg){
          /* Overwrite input field if it DOES NOT have focus,
             otherwise let it do its own paste handling. */
          item.getAsString((v)=>form.msg.value = v);
        }
      }
    }, false);
    /* Add help button for drag/drop/paste zone */
    form.file.parentNode.insertBefore(
      F.helpButtonlets.create(
        document.querySelector('#chat-input-file-area .help-buttonlet')
      ), form.file
    );
    ////////////////////////////////////////////////////////////
    // File drag/drop visual notification.
    const dropHighlight = form.file /* target zone */;
    const dropEvents = {
      drop: function(ev){
        D.removeClass(dropHighlight, 'dragover');
      },
      dragenter: function(ev){
        ev.preventDefault();
        ev.dataTransfer.dropEffect = "copy";
        D.addClass(dropHighlight, 'dragover');
      },
      dragleave: function(ev){
        D.removeClass(dropHighlight, 'dragover');
      },
      dragend: function(ev){
        D.removeClass(dropHighlight, 'dragover');
      }
    };
    Object.keys(dropEvents).forEach(
      (k)=>form.file.addEventListener(k, dropEvents[k], true)
    );
    return bxs;
  })()/*drag/drop*/;

  form.addEventListener('submit',(e)=>{
    e.preventDefault();
    const fd = new FormData(form);
    if(BlobXferState.blob/*replace file content with this*/){
      fd.set("file", BlobXferState.blob);
    }


    if( form.msg.value.length>0 || form.file.value.length>0 || BlobXferState.blob ){
      fetch("chat-send",{
        method: 'POST',
        body: fd
      });
    }
    BlobXferState.clear();
    form.msg.value = "";
    form.msg.focus();
  });

  /* Returns a new TEXT node with the given text content. */
  /** Returns the local time string of Date object d, defaulting
      to the current time. */
  const localTimeString = function ff(d){
    if(!ff.pad){







|










|















|












<
<
<
<
<
<
<
<
<
<
<



|


|



|

















|




|

|



>
>
|






<
|







205
206
207
208
209
210
211
212
213
214
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
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
    /* State for paste and drag/drop */
    const bxs = {
      dropDetails: document.querySelector('#chat-drop-details'),
      blob: undefined,
      clear: function(){
        this.blob = undefined;
        D.clearElement(this.dropDetails);
        Chat.e.inputFile.value = "";
      }
    };
    /** Updates the paste/drop zone with details of the pasted/dropped
        data. The argument must be a Blob or Blob-like object (File) or
        it can be falsy to reset/clear that state.*/
    const updateDropZoneContent = function(blob){
      const dd = bxs.dropDetails;
      bxs.blob = blob;
      D.clearElement(dd);
      if(!blob){
        Chat.e.inputFile.value = '';
        return;
      }
      D.append(dd, "Name: ", blob.name,
               D.br(), "Size: ",blob.size);
      if(blob.type && blob.type.startsWith("image/")){
        const img = D.img();
        D.append(dd, D.br(), img);
        const reader = new FileReader();
        reader.onload = (e)=>img.setAttribute('src', e.target.result);
        reader.readAsDataURL(blob);
      }
      const btn = D.button("Cancel");
      D.append(dd, D.br(), btn);
      btn.addEventListener('click', ()=>updateDropZoneContent(), false);
    };
    Chat.e.inputFile.addEventListener('change', function(ev){
      updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
    });
    /* Handle image paste from clipboard. TODO: figure out how we can
       paste non-image binary data as if it had been selected via the
       file selection element. */
    document.addEventListener('paste', function(event){
      const items = event.clipboardData.items,
            item = items[0];
      if(!item || !item.type) return;
      else if('file'===item.kind){
        updateDropZoneContent(false/*clear prev state*/);
        updateDropZoneContent(items[0].getAsFile());











      }
    }, false);
    /* Add help button for drag/drop/paste zone */
    Chat.e.inputFile.parentNode.insertBefore(
      F.helpButtonlets.create(
        document.querySelector('#chat-input-file-area .help-buttonlet')
      ), Chat.e.inputFile
    );
    ////////////////////////////////////////////////////////////
    // File drag/drop visual notification.
    const dropHighlight = Chat.e.inputFile /* target zone */;
    const dropEvents = {
      drop: function(ev){
        D.removeClass(dropHighlight, 'dragover');
      },
      dragenter: function(ev){
        ev.preventDefault();
        ev.dataTransfer.dropEffect = "copy";
        D.addClass(dropHighlight, 'dragover');
      },
      dragleave: function(ev){
        D.removeClass(dropHighlight, 'dragover');
      },
      dragend: function(ev){
        D.removeClass(dropHighlight, 'dragover');
      }
    };
    Object.keys(dropEvents).forEach(
      (k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true)
    );
    return bxs;
  })()/*drag/drop*/;

  Chat.e.inputForm.addEventListener('submit',(e)=>{
    e.preventDefault();
    const fd = new FormData(Chat.e.inputForm);
    if(BlobXferState.blob/*replace file content with this*/){
      fd.set("file", BlobXferState.blob);
    }
    if( !!Chat.inputValue()
        || Chat.e.inputFile.value.length>0
        || BlobXferState.blob ){
      fetch("chat-send",{
        method: 'POST',
        body: fd
      });
    }
    BlobXferState.clear();

    Chat.inputValue("").inputFocus();
  });

  /* Returns a new TEXT node with the given text content. */
  /** Returns the local time string of Date object d, defaulting
      to the current time. */
  const localTimeString = function ff(d){
    if(!ff.pad){
587
588
589
590
591
592
593

594
595

596
597
598
599
600
601
602
    if('visible'===document.visibilityState){
      if(Chat.changesSincePageHidden){
        Chat.changesSincePageHidden = 0;
        Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
      }
    }else{
      Chat.changesSincePageHidden += jx.msgs.length;

      Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
        Chat.pageTitleOrig;

    }
    if(jx.msgs.length && F.config.chat.pingTcp){
      fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
    }
  }/*newcontent()*/;

  (function(){







>
|
|
>







596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
    if('visible'===document.visibilityState){
      if(Chat.changesSincePageHidden){
        Chat.changesSincePageHidden = 0;
        Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
      }
    }else{
      Chat.changesSincePageHidden += jx.msgs.length;
      if(jx.msgs.length){
        Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
          Chat.pageTitleOrig;
      }
    }
    if(jx.msgs.length && F.config.chat.pingTcp){
      fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
    }
  }/*newcontent()*/;

  (function(){
Changes to src/default.css.
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
  font-size: 80%;
}

body.chat .chat-message-popup {
  font-family: monospace;
  font-size: 0.8em;
  text-align: left;
  opacity: 0.8;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 0.25em;
  z-index: 200;
}
body.chat .chat-message-popup > span { white-space: nowrap; }







<







1511
1512
1513
1514
1515
1516
1517

1518
1519
1520
1521
1522
1523
1524
  font-size: 80%;
}

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; }
1564
1565
1566
1567
1568
1569
1570






1571
1572
1573
1574
1575
1576
1577
  display: inline-block;
  min-height: 1em;
  max-height: 1em;
  min-width: 1em;
  max-width: 1em;
  margin: 0;
  padding: 0.2em/*needed to avoid image truncation*/;






}
body.fossil-dark-style .settings-icon {
  filter: invert(100%);
}
body.chat #chat-settings-button {
}
body.chat .chat-settings-popup {







>
>
>
>
>
>







1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
  display: inline-block;
  min-height: 1em;
  max-height: 1em;
  min-width: 1em;
  max-width: 1em;
  margin: 0;
  padding: 0.2em/*needed to avoid image truncation*/;
  border: 1px solid rgba(0,0,0,0.0)/*avoid resize when hover style kicks in*/;
  cursor: pointer;
  border-radius: 0.25em;
}
.settings-icon:hover {
  border: 1px outset rgba(127,127,127,1);
}
body.fossil-dark-style .settings-icon {
  filter: invert(100%);
}
body.chat #chat-settings-button {
}
body.chat .chat-settings-popup {
1599
1600
1601
1602
1603
1604
1605




1606
1607
1608
1609
1610
1611
1612
}
body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
  vertical-align: middle;
}
body.chat .chat-settings-popup > span.menu-entry > span.button {
  margin: 0.25em 0.2em;
  padding: 0.5em;




}
body.chat #chat-messages-wrapper {
  display: flex;
  flex-direction: column;
}
body.chat.chat-only-mode{
}







>
>
>
>







1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
}
body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
  vertical-align: middle;
}
body.chat .chat-settings-popup > span.menu-entry > span.button {
  margin: 0.25em 0.2em;
  padding: 0.5em;
  flex: 1 1 auto/*eliminates dead no-click zones on the right*/;
}
body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] {
  cursor: inherit;
}
body.chat #chat-messages-wrapper {
  display: flex;
  flex-direction: column;
}
body.chat.chat-only-mode{
}
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
  /* would like to pin this to the top so that it stays in place when
 scrolling, but doing so causes #chat-messages-wrapper to scroll
 behind it visibly, which is really ugly. Only current workaround is
 to force an opaque background color on this element, but that's not
 skin-friendly. */
  position: sticky;
  top: 0;
  left: 0;
  padding: 0.5em 1em;
  z-index: 100
    /* see notes in #chat-messages-wrapper. The various popups require a
       z-index higher than this one. */
}
body.chat.chat-only-mode #chat-messages-wrapper {
  position: relative;







<







1630
1631
1632
1633
1634
1635
1636

1637
1638
1639
1640
1641
1642
1643
  /* would like to pin this to the top so that it stays in place when
 scrolling, but doing so causes #chat-messages-wrapper to scroll
 behind it visibly, which is really ugly. Only current workaround is
 to force an opaque background color on this element, but that's not
 skin-friendly. */
  position: sticky;
  top: 0;

  padding: 0.5em 1em;
  z-index: 100
    /* see notes in #chat-messages-wrapper. The various popups require a
       z-index higher than this one. */
}
body.chat.chat-only-mode #chat-messages-wrapper {
  position: relative;