Fossil

Changes On Branch chat-fetch-port
Login

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

Changes In Branch chat-fetch-port Excluding Merge-Ins

This is equivalent to a diff from e378f930 to 147ff84e

2021-03-25
00:17
Create new branch named "panic-reduction" ... (check-in: 1efa8d7f user: larrybr tags: panic-reduction)
2021-03-22
03:03
Ported /chat from windows.fetch() to fossil.fetch() for XHR calls, as discussed in forum post 04b37ca5a5. ... (check-in: e9ed3158 user: stephan tags: trunk)
2021-03-21
18:51
Cleaned up the is-first-call handling of the /chat message poller. ... (Closed-Leaf check-in: 147ff84e user: stephan tags: chat-fetch-port)
18:25
Ported /chat from window.fetch to fossil.fetch, as FF versions as recently as 2017 fail with window.fetch. Needs more testing before merge but seems to work. ... (check-in: c9736432 user: stephan tags: chat-fetch-port)
2021-03-19
16:01
Resolved display cookie diff setting inconsistency reported in forum post f7e18f946b. ... (check-in: e378f930 user: stephan tags: trunk)
03:10
Add compile-time option to enable TH1 memory leak tracking. Also, fix TH1 memory leak, improve a couple #ifdef's, and fix JSON assert in fossil_print_error() seen when an invalid repository is specified. ... (check-in: 999e33cc user: mistachkin tags: trunk)

Changes to src/chat.c.

184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
  @ </div>
  @ </form>
  @ <div id='chat-messages-wrapper'>
  /* New chat messages get inserted immediately after this element */
  @ <span id='message-inject-point'></span>
  @ </div>

  builtin_fossil_js_bundle_or("popupwidget", "storage", NULL);
  /* Always in-line the javascript for the chat page */
  @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
  /* We need an onload handler to ensure that window.fossil is
     initialized before the chat init code runs. */
  @ window.addEventListener('load', function(){
  @ document.body.classList.add('chat')
  @ /*^^^for skins which add their own BODY tag */;







|







184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
  @ </div>
  @ </form>
  @ <div id='chat-messages-wrapper'>
  /* New chat messages get inserted immediately after this element */
  @ <span id='message-inject-point'></span>
  @ </div>

  builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", NULL);
  /* Always in-line the javascript for the chat page */
  @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
  /* We need an onload handler to ensure that window.fossil is
     initialized before the chat init code runs. */
  @ window.addEventListener('load', function(){
  @ document.body.classList.add('chat')
  @ /*^^^for skins which add their own BODY tag */;

Changes to src/chat.js.

395
396
397
398
399
400
401


402
403
404
405
406
407
408
      setNewMessageSound: function f(uri){
        delete this.playNewMessageSound.audio;
        this.playNewMessageSound.uri = uri;
        this.settings.set('audible-alert', !!uri);
        return this;
      }
    };


    cs.e.inputCurrent = cs.e.inputSingle;
    /* 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){







>
>







395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
      setNewMessageSound: function f(uri){
        delete this.playNewMessageSound.audio;
        this.playNewMessageSound.uri = uri;
        this.settings.set('audible-alert', !!uri);
        return this;
      }
    };
    F.fetch.beforesend = ()=>cs.ajaxStart();
    F.fetch.aftersend = ()=>cs.ajaxEnd();
    cs.e.inputCurrent = cs.e.inputSingle;
    /* 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){
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550

551
552
553
554
555
556
557
        id = e.dataset.msgid;
      }else{
        e = this.getMessageElemById(id);
      }
      if(!(e instanceof HTMLElement)) return;
      if(this.userMayDelete(e)){
        this.ajaxStart();
        fetch("chat-delete/" + id)
          .then(function(response){
            if(!response.ok) throw cs._newResponseError(response);
            return response;
          })
          .then(()=>this.deleteMessageElem(e))
          .catch(err=>this.reportErrorAsMessage(err))
          .finally(()=>this.ajaxEnd());

      }else{
        this.deleteMessageElem(id);
      }
    };
    document.addEventListener('visibilitychange', function(ev){
      cs.pageIsActive = ('visible' === document.visibilityState);
      if(cs.pageIsActive){







|
<
<
|
<
|
|
<
>







538
539
540
541
542
543
544
545


546

547
548

549
550
551
552
553
554
555
556
        id = e.dataset.msgid;
      }else{
        e = this.getMessageElemById(id);
      }
      if(!(e instanceof HTMLElement)) return;
      if(this.userMayDelete(e)){
        this.ajaxStart();
        F.fetch("chat-delete/" + id, {


          responseType: 'json',

          onload:(r)=>this.deleteMessageElem(r),
          onerror:(err)=>this.reportErrorAsMessage(err)

        });
      }else{
        this.deleteMessageElem(id);
      }
    };
    document.addEventListener('visibilitychange', function(ev){
      cs.pageIsActive = ('visible' === document.visibilityState);
      if(cs.pageIsActive){
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897

898
899
900
901
902
903
904
905
906
    const msg = this.inputValue().trim();
    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)=>{
      if(x.ok) return x.text();
      else throw Chat._newResponseError(x);
    }).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.reportErrorAsMessage(e));
    BlobXferState.clear();
    Chat.inputValue("").inputFocus();
  };

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







|
<
|
<
|
|
|








>
|
<







875
876
877
878
879
880
881
882

883

884
885
886
887
888
889
890
891
892
893
894
895
896

897
898
899
900
901
902
903
    const msg = this.inputValue().trim();
    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()));
    F.fetch("chat-send",{

      payload: fd,

      responseType: 'text',
      onerror:(err)=>this.reportErrorAsMessage(err),
      onload:function(txt){
        if(!txt) return/*success response*/;
        try{
          const json = JSON.parse(txt);
          self.newContent({msgs:[json]});
        }catch(e){
          self.reportError(e);
          return;
        }
      }
    });

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

  Chat.e.inputSingle.addEventListener('keydown',function(ev){
    if(13===ev.keyCode/*ENTER*/){
      ev.preventDefault();
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166

1167







1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180

1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203





1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217



















1218
1219
1220

1221

1222
1223
1224










1225
1226
1227
1228
1229
1230
1231
1232


1233
1234

1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248

1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
    const loadLegend = D.legend("Load...");
    const toolbar = Chat.e.loadOlderToolbar = D.attr(
      D.fieldset(loadLegend), "id", "load-msg-toolbar"
    );
    Chat.disableDuringAjax.push(toolbar);
    /* Loads the next n oldest messages, or all previous history if n is negative. */
    const loadOldMessages = function(n){
      Chat.ajaxStart();
      Chat.e.messagesWrapper.classList.add('loading');
      Chat._isBatchLoading = true;
      var gotMessages = false;
      const scrollHt = Chat.e.messagesWrapper.scrollHeight,
            scrollTop = Chat.e.messagesWrapper.scrollTop;
      fetch("chat-poll?before="+Chat.mnMsg+"&n="+n)

        .then(Chat._fetchJsonOrError)







        .then(function(x){
          gotMessages = x.msgs.length;
          newcontent(x,true);
        })
        .catch(e=>Chat.reportErrorAsMessage(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));
    btn = D.button("All previous messages");
    D.append(wrapper, btn);
    btn.addEventListener('click',()=>loadOldMessages(-1));
    D.append(Chat.e.messagesWrapper, toolbar);
    toolbar.disabled = true /*will be enabled when msg load finishes */;
  })()/*end history loading widget setup*/;




















  async function poll(isFirstCall){
    if(poll.running) return;
    poll.running = true;

    if(isFirstCall){

      Chat.ajaxStart();
      Chat.e.messagesWrapper.classList.add('loading');
    }










    Chat._isBatchLoading = isFirstCall;
    var p = fetch("chat-poll?name=" + Chat.mxMsg);
    p.then(Chat._fetchJsonOrError)
      .then(y=>newcontent(y))
      .catch(e=>console.error(e))
    /* ^^^ 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( window.fossil.config.chat.fromcli ){
    Chat.chatOnlyMode(true);
  }

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







<


<


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

<
<
<

<
<

|
<

>
|


|



















>
>
>
>
>
|













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



>
>
>
>
>
>
>
>
>
>
|
<
<
<
|
|
|
|
>
>
|
<
>
|
|
<
<
<
<
|
<
<
<
<
<
|
|
>

<
<
<
<



|


1150
1151
1152
1153
1154
1155
1156

1157
1158

1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173



1174


1175
1176

1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259



1260
1261
1262
1263
1264
1265
1266

1267
1268
1269




1270





1271
1272
1273
1274




1275
1276
1277
1278
1279
1280
    const loadLegend = D.legend("Load...");
    const toolbar = Chat.e.loadOlderToolbar = D.attr(
      D.fieldset(loadLegend), "id", "load-msg-toolbar"
    );
    Chat.disableDuringAjax.push(toolbar);
    /* Loads the next n oldest messages, or all previous history if n is negative. */
    const loadOldMessages = function(n){

      Chat.e.messagesWrapper.classList.add('loading');
      Chat._isBatchLoading = true;

      const scrollHt = Chat.e.messagesWrapper.scrollHeight,
            scrollTop = Chat.e.messagesWrapper.scrollTop;
      F.fetch("chat-poll",{
        urlParams:{
          before: Chat.mnMsg,
          n: n
        },
        responseType: 'json',
        onerror:function(err){
          Chat.reportErrorAsMessage(err);
          Chat._isBatchLoading = false;
        },
        onload:function(x){
          let gotMessages = x.msgs.length;
          newcontent(x,true);



          Chat._isBatchLoading = false;


          if(Chat._gotServerError){
            Chat._gotServerError = false;

            return;
          }
          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*/)
             || (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
            );
          }
        },
        aftersend:function(){
          Chat.e.messagesWrapper.classList.remove('loading');
          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));
    btn = D.button("All previous messages");
    D.append(wrapper, btn);
    btn.addEventListener('click',()=>loadOldMessages(-1));
    D.append(Chat.e.messagesWrapper, toolbar);
    toolbar.disabled = true /*will be enabled when msg load finishes */;
  })()/*end history loading widget setup*/;

  const afterFetch = function f(){
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.ajaxEnd();
      Chat.e.messagesWrapper.classList.remove('loading');
      setTimeout(function(){
        Chat.scrollMessagesTo(1);
      }, 250);
    }
    if(Chat._gotServerError && Chat.intervalTimer){
      clearInterval(Chat.intervalTimer);
      Chat.reportErrorAsMessage(
        "Shutting down chat poller due to server-side error. ",
        "Reload this page to reactivate it.");
      delete Chat.intervalTimer;
    }
    poll.running = false;
  };
  afterFetch.isFirstCall = true;
  const poll = async function f(){
    if(f.running) return;
    f.running = true;
    Chat._isBatchLoading = f.isFirstCall;
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.ajaxStart();
      Chat.e.messagesWrapper.classList.add('loading');
    }
    F.fetch("chat-poll",{
      timeout: 420 * 1000/*FIXME: get the value from the server*/,
      urlParams:{
        name: Chat.mxMsg
      },
      responseType: "json",
      // Disable the ajax start/end handling for this long-polling op:
      beforesend: function(){},
      aftersend: function(){},
      onerror:function(err){
        Chat._isBatchLoading = false;



        console.error(err);
        /* ^^^ we don't use Chat.reportError() here b/c the polling
           fails exepectedly when it times out, but is then immediately
           resumed, and reportError() produces a loud error message. */
        afterFetch();
      },
      onload:function(y){

        newcontent(y);
        Chat._isBatchLoading = false;
        afterFetch();




      }





    });
  };
  poll.isFirstCall = true;
  Chat._gotServerError = poll.running = false;




  if( window.fossil.config.chat.fromcli ){
    Chat.chatOnlyMode(true);
  }
  Chat.intervalTimer = setInterval(poll, 1000);
  F.page.chat = Chat/* enables testing the APIs via the dev tools */;
})();

Changes to src/fossil.fetch.js.

84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
   ignored (feature or bug?).

   - timeout: integer in milliseconds specifying the XHR timeout
   duration. Default = fossil.fetch.timeout.

   When an options object does not provide
   onload/onerror/beforesend/aftersend handlers of its own, this
   function falls to defaults which are member properties of this
   function with the same name, e.g. fossil.fetch.onload(). The
   default onload/onerror implementations route the data through the
   dev console and (for onerror()) through fossil.error(). The default
   beforesend/aftersend are no-ops. Individual pages may overwrite
   those members to provide default implementations suitable for the
   page's use, e.g. keeping track of how many in-flight ajax requests
   are pending.







|







84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
   ignored (feature or bug?).

   - timeout: integer in milliseconds specifying the XHR timeout
   duration. Default = fossil.fetch.timeout.

   When an options object does not provide
   onload/onerror/beforesend/aftersend handlers of its own, this
   function falls back to defaults which are member properties of this
   function with the same name, e.g. fossil.fetch.onload(). The
   default onload/onerror implementations route the data through the
   dev console and (for onerror()) through fossil.error(). The default
   beforesend/aftersend are no-ops. Individual pages may overwrite
   those members to provide default implementations suitable for the
   page's use, e.g. keeping track of how many in-flight ajax requests
   are pending.
164
165
166
167
168
169
170












171
172
173
174
175
176
177
  x.ontimeout = function(){
    try{opt.aftersend()}catch(e){/*ignore*/}
    opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired."));
  };
  x.onreadystatechange = function(){
    if(XMLHttpRequest.DONE !== x.readyState) return;
    try{opt.aftersend()}catch(e){/*ignore*/}












    if(200!==x.status){
      let err;
      try{
        const j = JSON.parse(x.response);
        if(j.error) err = new Error(j.error);
      }catch(ex){/*ignore*/}
      opt.onerror(err || new Error("HTTP response status "+x.status+"."));







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







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
  x.ontimeout = function(){
    try{opt.aftersend()}catch(e){/*ignore*/}
    opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired."));
  };
  x.onreadystatechange = function(){
    if(XMLHttpRequest.DONE !== x.readyState) return;
    try{opt.aftersend()}catch(e){/*ignore*/}
    if(false && 0===x.status){
      /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
         when the /chat page starts up, but not in Chrome nor in other
         apps. Insofar as has been determined, this happens before a
         request is actually sent and it appears to have no
         side-effects on the app other than to generate an error
         (i.e. no requests/responses are missing). This is a silly
         workaround which may or may not bite us later. If so, it can
         be removed at the cost of an unsightly console error message
         in FF. */
      return;
    }
    if(200!==x.status){
      let err;
      try{
        const j = JSON.parse(x.response);
        if(j.error) err = new Error(j.error);
      }catch(ex){/*ignore*/}
      opt.onerror(err || new Error("HTTP response status "+x.status+"."));