Fossil

Check-in [3c0c8954c7]
Login

Check-in [3c0c8954c7]

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

Overview
Comment:chat: send and poll can now report if the session is logged out, and client-side poll stops looping if that condition is detected. Both cases emit a message in the message area, from user 'fossil', with the CSS class 'error' and a link to the login page with a redirect back to the chat page.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 3c0c8954c73f7a523b9211d0882b000793eee4dec3d70ab5b605b0426c6ea4b6
User & Date: stephan 2020-12-29 04:18:45.942
Context
2020-12-29
16:49
Minor doc typo. ... (check-in: 47655d3996 user: stephan tags: trunk)
04:18
chat: send and poll can now report if the session is logged out, and client-side poll stops looping if that condition is detected. Both cases emit a message in the message area, from user 'fossil', with the CSS class 'error' and a link to the login page with a redirect back to the chat page. ... (check-in: 3c0c8954c7 user: stephan tags: trunk)
00:37
Replaced use of a deprecated DOM API. ... (check-in: b06442a621 user: stephan tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
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
             " AND msgid<%d", msgid);
       db_bind_double(&s, ":mxage", mxDays);
       db_step(&s);
       db_finalize(&s);
     }
   }
}



























/*
** WEBPAGE: chat-send
**
** This page receives (via XHR) a new chat-message and/or a new file
** to be entered into the chat history.






*/
void chat_send_webpage(void){
  int nByte;
  const char *zMsg;
  login_check_credentials();
  if( !g.perm.Chat ) return;



  chat_create_tables();
  nByte = atoi(PD("file:bytes",0));
  zMsg = PD("msg","");
  db_begin_write();
  chat_purge();
  if( nByte==0 ){
    if( zMsg[0] ){







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






>
>
>
>
>
>





|
>
>
>







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
             " AND msgid<%d", msgid);
       db_bind_double(&s, ":mxage", mxDays);
       db_step(&s);
       db_finalize(&s);
     }
   }
}

/*
** Sets the current CGI response type to application/json then emits a
** JSON-format error message object. If fAsMessageList is true then
** the object is output using the list format described for chat-post,
** else it is emitted as a single object in that same format.
*/
static void chat_emit_permissions_error(int fAsMessageList){
  char * zTime = cgi_iso8601_datestamp();
  cgi_set_content_type("application/json");
  if(fAsMessageList){
    CX("{\"msgs\":[{");
  }else{
    CX("{");
  }
  CX("\"isError\": true, \"xfrom\": \"fossil\",");
  CX("\"mtime\": %!j, \"lmtime\": %!j,", zTime, zTime);
  CX("\"xmsg\": \"Missing permissions or not logged in. "
     "Try <a href='%R/login?g=%R/chat'>logging in</a>.\"");
  if(fAsMessageList){
    CX("}]}");
  }else{
    CX("}");
  }
  fossil_free(zTime);
}

/*
** WEBPAGE: chat-send
**
** This page receives (via XHR) a new chat-message and/or a new file
** to be entered into the chat history.
**
** On success it responds with an empty response: the new message
** should be fetched via /chat-poll. On error, e.g. login expiry,
** it emits a JSON response in the same form as described for
** /chat-poll errors, but as a standalone object instead of a
** list of objects.
*/
void chat_send_webpage(void){
  int nByte;
  const char *zMsg;
  login_check_credentials();
  if( !g.perm.Chat ) {
    chat_emit_permissions_error(0);
    return;
  }
  chat_create_tables();
  nByte = atoi(PD("file:bytes",0));
  zMsg = PD("msg","");
  db_begin_write();
  chat_purge();
  if( nByte==0 ){
    if( zMsg[0] ){
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
**
** If "before" is provided, "name" is ignored.
**
** The reply from this webpage is JSON that describes the new content.
** Format of the json:
**
** |    {
** |      "msg":[
** |        {
** |           "msgid": integer // message id
** |           "mtime": text    // When sent:  YYYY-MM-DD HH:MM:SS UTC
** |           "lmtime: text    // Localtime where the message was sent from
** |           "xfrom": text    // Login name of sender
** |           "uclr":  text    // Color string associated with the user
** |           "xmsg":  text    // HTML text of the message







|







427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
**
** If "before" is provided, "name" is ignored.
**
** The reply from this webpage is JSON that describes the new content.
** Format of the json:
**
** |    {
** |      "msgs":[
** |        {
** |           "msgid": integer // message id
** |           "mtime": text    // When sent:  YYYY-MM-DD HH:MM:SS UTC
** |           "lmtime: text    // Localtime where the message was sent from
** |           "xfrom": text    // Login name of sender
** |           "uclr":  text    // Color string associated with the user
** |           "xmsg":  text    // HTML text of the message
419
420
421
422
423
424
425



















426
427
428
429
430
431
432
433
434
435
436
437
438
439



440
441
442
443
444
445
446
447
448
**
** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero.
**
** The "lmtime" value might be known, in which case it is omitted.
**
** The messages are ordered oldest first unless "before" is provided, in which
** case they are sorted newest first (to facilitate the client-side UI update).



















*/
void chat_poll_webpage(void){
  Blob json;                  /* The json to be constructed and returned */
  sqlite3_int64 dataVersion;  /* Data version.  Used for polling. */
  const int iDelay = 1000;    /* Delay until next poll (milliseconds) */
  int nDelay;                 /* Maximum delay.*/
  int msgid = atoi(PD("name","0"));
  const int msgBefore = atoi(PD("before","0"));
  int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
  Blob sql = empty_blob;
  Stmt q1;
  nDelay = db_get_int("chat-poll-timeout",420);  /* Default about 7 minutes */
  login_check_credentials();
  if( !g.perm.Chat ) return;



  chat_create_tables();
  cgi_set_content_type("text/json");
  dataVersion = db_int64(0, "PRAGMA data_version");
  blob_append_sql(&sql,
    "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
    "       fname, fmime, %s, lmtime"
    "  FROM chat ",
    msgBefore>0 ? "0 as mdel" : "mdel");
  if( msgid<=0 || msgBefore>0 ){







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













|
>
>
>

|







454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
**
** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero.
**
** The "lmtime" value might be known, in which case it is omitted.
**
** The messages are ordered oldest first unless "before" is provided, in which
** case they are sorted newest first (to facilitate the client-side UI update).
**
** As a special case, if this routine encounters an error, e.g. the user's
** permissions cannot be verified because their login cookie expired, the
** request returns a slightly modified structure:
**
** |    {
** |      "msgs":[
** |        {
** |          "isError": true,
** |          "xfrom": "fossil",
** |          "xmsg": "error details"
** |          "mtime": as above,
** |          "ltime": same as mtime
** |        }
** |      ]
** |    }
**
** If the client gets such a response, it should display the message
** in a prominent manner and then stop polling for new messages.
*/
void chat_poll_webpage(void){
  Blob json;                  /* The json to be constructed and returned */
  sqlite3_int64 dataVersion;  /* Data version.  Used for polling. */
  const int iDelay = 1000;    /* Delay until next poll (milliseconds) */
  int nDelay;                 /* Maximum delay.*/
  int msgid = atoi(PD("name","0"));
  const int msgBefore = atoi(PD("before","0"));
  int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
  Blob sql = empty_blob;
  Stmt q1;
  nDelay = db_get_int("chat-poll-timeout",420);  /* Default about 7 minutes */
  login_check_credentials();
  if( !g.perm.Chat ) {
    chat_emit_permissions_error(1);
    return;
  }
  chat_create_tables();
  cgi_set_content_type("application/json");
  dataVersion = db_int64(0, "PRAGMA data_version");
  blob_append_sql(&sql,
    "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
    "       fname, fmime, %s, lmtime"
    "  FROM chat ",
    msgBefore>0 ? "0 as mdel" : "mdel");
  if( msgid<=0 || msgBefore>0 ){
Changes to src/chat.js.
495
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
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
        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," #",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));
            ds.hasImage = 1;
          }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);







<




|




>
>
|
















<







495
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
521
522
523
524
525
526
527
528
529

530
531
532
533
534
535
536
        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," #",(m.msgid||'???'),' @ ',d.getHours(),":",
            (d.getMinutes()+100).toString().slice(1,3)            
          )
        );
        var contentTarget = this.e.content;
        if(m.isError){
          D.addClass([contentTarget, this.e.tab], 'error');
        }else 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));
            ds.hasImage = 1;
          }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);
650
651
652
653
654
655
656
657

658
659
660
661







662

663


664
665
666
667
668
669
670
    /* ^^^^ we don't really want/need the FORM element, but when
       FormData() is default-constructed here then the server
       segfaults, and i have no clue why! */;
    const msg = this.inputValue();
    if(msg) fd.set('msg',msg);
    const file = BlobXferState.blob || this.e.inputFile.files[0];
    if(file) fd.set("file", file);
    if( msg || file ){

      fd.set("lmtime", localTime8601(new Date()));
      fetch("chat-send",{
        method: 'POST',
        body: fd







      });

    }


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

  Chat.e.inputSingle.addEventListener('keydown',function(ev){
    if(13===ev.keyCode/*ENTER*/){
      ev.preventDefault();







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







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
678
679
680
681
    /* ^^^^ we don't really want/need the FORM element, but when
       FormData() is default-constructed here then the server
       segfaults, and i have no clue why! */;
    const msg = this.inputValue();
    if(msg) fd.set('msg',msg);
    const file = BlobXferState.blob || this.e.inputFile.files[0];
    if(file) fd.set("file", file);
    if( !msg && !file ) return;
    const self = this;
    fd.set("lmtime", localTime8601(new Date()));
    fetch("chat-send",{
      method: 'POST',
      body: fd
    }).then((x)=>x.text())
      .then(function(txt){
        if(!txt) return/*success response*/;
        try{
          const json = JSON.parse(txt);
          self.newContent({msgs:[json]});
        }catch(e){
          self.reportError(e);
          return;
        }
      })
      .catch((e)=>this.reportError(e));
    BlobXferState.clear();
    Chat.inputValue("").inputFocus();
  };

  Chat.e.inputSingle.addEventListener('keydown',function(ev){
    if(13===ev.keyCode/*ENTER*/){
      ev.preventDefault();
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915



916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933

934
935
936
937
938
939
940
      Chat.scrollMessagesTo(-1);
      return false;
    });
  })();
  
  /** Callback for poll() to inject new content into the page.  jx ==
      the response from /chat-poll. If atEnd is true, the message is
      appended to the end of the chat list, else the beginning (the
      default). */
  const newcontent = function f(jx,atEnd){
    if(!f.processPost){
      /** Processes chat message m, placing it either the start (if atEnd
          is falsy) or end (if atEnd is truthy) of the chat history. atEnd
          should only be true when loading older messages. */
      f.processPost = function(m,atEnd){
        ++Chat.totalMessageCount;
        if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
        if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
        if( m.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;
      }
    }else{
      Chat.changesSincePageHidden += jx.msgs.length;
      if(jx.msgs.length){
        Chat.e.pageTitle.innerText = '[*] '+Chat.pageTitleOrig;
      }
    }
    if(jx.msgs.length && F.config.chat.pingTcp){
      fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
    }
  }/*newcontent()*/;


  (function(){
    /** Add toolbar for loading older messages. We use a FIELDSET here
        because a fieldset is the only parent element type which can
        automatically enable/disable its children by
        enabling/disabling the parent element. */
    const loadLegend = D.legend("Load...");







|
|


















>
>
>


















>







900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
      Chat.scrollMessagesTo(-1);
      return false;
    });
  })();
  
  /** Callback for poll() to inject new content into the page.  jx ==
      the response from /chat-poll. If atEnd is true, the message is
      appended to the end of the chat list (for loading older
      messages), else the beginning (the default). */
  const newcontent = function f(jx,atEnd){
    if(!f.processPost){
      /** Processes chat message m, placing it either the start (if atEnd
          is falsy) or end (if atEnd is truthy) of the chat history. atEnd
          should only be true when loading older messages. */
      f.processPost = function(m,atEnd){
        ++Chat.totalMessageCount;
        if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
        if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
        if( m.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);
        if(m.isError){
          Chat.gotServerError = m;
        }
      }/*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;
      }
    }else{
      Chat.changesSincePageHidden += jx.msgs.length;
      if(jx.msgs.length){
        Chat.e.pageTitle.innerText = '[*] '+Chat.pageTitleOrig;
      }
    }
    if(jx.msgs.length && F.config.chat.pingTcp){
      fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
    }
  }/*newcontent()*/;
  Chat.newContent = newcontent;

  (function(){
    /** Add toolbar for loading older messages. We use a FIELDSET here
        because a fieldset is the only parent element type which can
        automatically enable/disable its children by
        enabling/disabling the parent element. */
    const loadLegend = D.legend("Load...");
956
957
958
959
960
961
962





963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979


980
981
982
983
984
985
986
987
988
989
990
991
          gotMessages = x.msgs.length;
          newcontent(x,true);
        })
        .catch(e=>Chat.reportError(e))
        .finally(function(){
          Chat.isBatchLoading = false;
          Chat.e.messagesWrapper.classList.remove('loading');





          if(n<0/*we asked for all history*/
             || 0===gotMessages/*we found no history*/
             || (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
             || (false!==gotMessages && n===0 && gotMessages<Chat.loadMessageCount
                 /*we asked for default amount and got fewer than that.*/)){
            /* We've loaded all history. Permanently disable the
               history-load toolbar and keep it from being re-enabled
               via the ajaxStart()/ajaxEnd() mechanism... */
            const div = Chat.e.loadOlderToolbar.querySelector('div');
            D.append(D.clearElement(div), "All history has been loaded.");
            D.addClass(Chat.e.loadOlderToolbar, 'all-done');
            const ndx = Chat.disableDuringAjax.indexOf(Chat.e.loadOlderToolbar);
            if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1);
            Chat.e.loadOlderToolbar.disabled = true;
          }
          if(gotMessages > 0){
            F.toast.message("Loaded "+gotMessages+" older messages.");


            Chat.e.messagesWrapper.scrollTo(
              0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop
            );
          }
          Chat.ajaxEnd();
        });
    };
    const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */;
    D.append(toolbar, wrapper);
    var btn = D.button("Previous "+Chat.loadMessageCount+" messages");
    D.append(wrapper, btn);
    btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount));







>
>
>
>
>
|
















>
>




<







971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005

1006
1007
1008
1009
1010
1011
1012
          gotMessages = x.msgs.length;
          newcontent(x,true);
        })
        .catch(e=>Chat.reportError(e))
        .finally(function(){
          Chat.isBatchLoading = false;
          Chat.e.messagesWrapper.classList.remove('loading');
          Chat.ajaxEnd();
          if(Chat.gotServerError){
            F.toast.error("Got an error response from the server. ",
                          "See message for details");
            return;
          }else if(n<0/*we asked for all history*/
             || 0===gotMessages/*we found no history*/
             || (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
             || (false!==gotMessages && n===0 && gotMessages<Chat.loadMessageCount
                 /*we asked for default amount and got fewer than that.*/)){
            /* We've loaded all history. Permanently disable the
               history-load toolbar and keep it from being re-enabled
               via the ajaxStart()/ajaxEnd() mechanism... */
            const div = Chat.e.loadOlderToolbar.querySelector('div');
            D.append(D.clearElement(div), "All history has been loaded.");
            D.addClass(Chat.e.loadOlderToolbar, 'all-done');
            const ndx = Chat.disableDuringAjax.indexOf(Chat.e.loadOlderToolbar);
            if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1);
            Chat.e.loadOlderToolbar.disabled = true;
          }
          if(gotMessages > 0){
            F.toast.message("Loaded "+gotMessages+" older messages.");
            /* Return scroll position to where it was when the history load
               was requested, per user request */
            Chat.e.messagesWrapper.scrollTo(
              0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop
            );
          }

        });
    };
    const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */;
    D.append(toolbar, wrapper);
    var btn = D.button("Previous "+Chat.loadMessageCount+" messages");
    D.append(wrapper, btn);
    btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount));
1011
1012
1013
1014
1015
1016
1017

1018
1019
1020
1021




1022
1023
1024
1025
1026
1027

1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
    /* ^^^ we don't use Chat.reportError(e) here b/c the polling
       fails exepectedly when it times out, but is then immediately
       resumed, and reportError() produces a loud error message. */
      .finally(function(){
        if(isFirstCall){
          Chat.isBatchLoading = false;
          Chat.ajaxEnd();

          setTimeout(function(){
            Chat.scrollMessagesTo(1);
            Chat.e.messagesWrapper.classList.remove('loading');
          }, 250);




        }
        poll.running=false;
      });
  }
  poll.running = false;
  poll(true);

  setInterval(poll, 1000);

  if(/\bping=\d+/.test(window.location.search)){
    /* If we see the 'ping' parameter we're certain this was run via
       the 'fossil chat' CLI command, in which case we start up in
       chat-only mode. */
    Chat.chatOnlyMode(true);
  }

  F.page.chat = Chat/* enables testing the APIs via the dev tools */;
})();







>


<

>
>
>
>




|

>
|
|









1032
1033
1034
1035
1036
1037
1038
1039
1040
1041

1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
    /* ^^^ we don't use Chat.reportError(e) here b/c the polling
       fails exepectedly when it times out, but is then immediately
       resumed, and reportError() produces a loud error message. */
      .finally(function(){
        if(isFirstCall){
          Chat.isBatchLoading = false;
          Chat.ajaxEnd();
          Chat.e.messagesWrapper.classList.remove('loading');
          setTimeout(function(){
            Chat.scrollMessagesTo(1);

          }, 250);
        }
        if(Chat.gotServerError && Chat.intervalTimer){
          clearInterval(Chat.intervalTimer);
          delete Chat.intervalTimer;
        }
        poll.running=false;
      });
  }
  Chat.gotServerError = poll.running = false;
  poll(true);
  if(!Chat.gotServerError){
    Chat.intervalTimer = setInterval(poll, 1000);
  }
  if(/\bping=\d+/.test(window.location.search)){
    /* If we see the 'ping' parameter we're certain this was run via
       the 'fossil chat' CLI command, in which case we start up in
       chat-only mode. */
    Chat.chatOnlyMode(true);
  }

  F.page.chat = Chat/* enables testing the APIs via the dev tools */;
})();