Diff
Not logged in

Differences From Artifact [79429e7e10]:

To Artifact [fda30be86e]:


178
179
180
181
182
183
184



































185
186
187
188
189
190
191
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
214
215
216
217
218
219
220
221
222
223
224
225
226







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







      },
      filterState:{
        activeUser: undefined,
        match: function(uname){
          return this.activeUser===uname || !this.activeUser;
        }
      },
      /**
         The timer object is used to control connection throttling
         when connection errors arrise. It starts off with a polling
         delay of $initialDelay ms. If there's a connection error,
         that gets bumped by some value for each subsequent error, up
         to some max value.

         The timeing of resetting the delay when service returns is,
         because of the long-poll connection and our lack of low-level
         insight into the connection at this level, a bit wonky.
      */
      timer:{
        id: undefined,
        $initialDelay: 1000 /* initial polling interval (ms) */,
        currentDelay: 1000 /* current polling interval */,
        maxDelay: 60000 /* max interval when backing off for
                              connection errors */,
        minDelay: 7000 /* minimum delay time */,
        idReconnect: undefined /*timer id for reconnection determination*/,
        incrDelay: function(){
          if( this.maxDelay > this.currentDelay ){
            this.currentDelay =
              (this.currentDelay < this.minDelay)
              ? this.minDelay
              : (this.currentDelay * 3);
          }
          return this.currentDelay;
        },
        resetDelay: function(){
          return this.currentDelay = this.$initialDelay;
        },
        isDelayed: function(){
          return this.currentDelay > this.$initialDelay;
        }
      },
      /**
         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 trim()'d string and the setter returns this
         object. As a special case, if arguments[0] is a boolean
         value, it behaves like a getter and, if arguments[0]===true
         it clears the input field before returning.
604
605
606
607
608
609
610
611

612
613
614
615
616
617
618
639
640
641
642
643
644
645

646
647
648
649
650
651
652
653







-
+







        return this;
      },

      /**
         If animations are enabled, passes its arguments
         to D.addClassBriefly(), else this is a no-op.
         If cb is a function, it is called after the
         CSS class is removed. Returns this object; 
         CSS class is removed. Returns this object;
      */
      animate: function f(e,a,cb){
        if(!f.$disabled){
          D.addClassBriefly(e, a, 0, cb);
        }
        return this;
      }
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
678
679
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
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740







+
+







-



+
-
+
+



-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+






+







       Accepts any argument types valid for fossil.toast.error().
    */
    cs.reportError = function(/*msg args*/){
      const args = argsToArray(arguments);
      console.error("chat error:",args);
      F.toast.error.apply(F.toast, args);
    };

    let InternalMsgId = 0;
    /**
       Reports an error in the form of a new message in the chat
       feed. All arguments are appended to the message's content area
       using fossil.dom.append(), so may be of any type supported by
       that function.
    */
    cs.reportErrorAsMessage = function f(/*msg args*/){
      if(undefined === f.$msgid) f.$msgid=0;
      const args = argsToArray(arguments).map(function(v){
        return (v instanceof Error) ? v.message : v;
      });
      if(Chat.verboseErrors){
      console.error("chat error:",args);
        console.error("chat error:",args);
      }
      const d = new Date().toISOString(),
            mw = new this.MessageWidget({
              isError: true,
              xfrom: null,
              msgid: "error-"+(++f.$msgid),
              xfrom: undefined,
              msgid: "error-"+(++InternalMsgId),
              mtime: d,
              lmtime: d,
              xmsg: args
            });
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();
      return mw;
    };

    /**
       For use by the connection poller to send a "connection
       restored" message.
    */
    cs.reportReconnection = function f(/*msg args*/){
      const args = argsToArray(arguments).map(function(v){
        return (v instanceof Error) ? v.message : v;
      });
      const d = new Date().toISOString(),
            mw = new this.MessageWidget({
              isError: false,
              xfrom: undefined,
              msgid: "reconnect-"+(++InternalMsgId),
              mtime: d,
              lmtime: d,
              xmsg: args
            });
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();
      return mw;
    };

    cs.getMessageElemById = function(id){
      return qs('[data-msgid="'+id+'"]');
    };

    /** Finds the last .message-widget element and returns it or
1622
1623
1624
1625
1626
1627
1628



































1629
1630
1631
1632
1633
1634
1635
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
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







      if(state.msg) Chat.inputValue(state.msg);
      if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
      const theMsg = findMessageWidgetParent(w);
      if(theMsg) Chat.deleteMessageElem(theMsg);
    }));
    Chat.reportErrorAsMessage(w);
  };

  /* Assume the connection has been established, reset
     the Chat.timer.idReconnect, and alert the user
     that the outage appears to be over. */
  const reportConnectionReestablished = function(){
    if( Chat.timer.idReconnect ){
      clearTimeout(Chat.timer.idReconnect);
      Chat.timer.idReconnect = 0;
    }
    if( Chat.timer.isDelayed() ){
      Chat.timer.resetDelay();
      Chat.reportReconnection(
        "Connection restored after outage."
      );
      setTimeout( Chat.poll, 0 );
    }
  };

  /* If we're currently in delayed-retry mode, try to reset the delay
     if we're waiting for a while for the connection to complete,
     as that's an indication (not a guaranty) that we're connected
     and long-polling. */
  const setupConnectionReestablished = function(){
    if( !Chat.timer.idReconnect && Chat.timer.isDelayed() ){
      Chat.timer.idReconnect = setTimeout(()=>{
        Chat.timer.idReconnect = 0;
        if( poll.running ){
          reportConnectionReestablished();
        }
      }, Chat.timer.$initialDelay * 5 );
      Chat.e.viewMessages.querySelectorAll(
        '.message-widget.error-connection'
      ).forEach(e=>D.remove(e));
    }
  };

  /**
     Submits the contents of the message input field (if not empty)
     and/or the file attachment field to the server. If both are
     empty, this is a no-op.

     If the current view is the history search, this instead sends the
1679
1680
1681
1682
1683
1684
1685




1686
1687
1688
1689
1690

1691
1692
1693
1694
1695
1696
1697
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798







+
+
+
+





+







    if( !msg && !file ) return;
    fallback.blob = file;
    const self = this;
    fd.set("lmtime", localTime8601(new Date()));
    F.fetch("chat-send",{
      payload: fd,
      responseType: 'text',
      beforesend: function(){
        Chat.ajaxStart();
        setupConnectionReestablished();
      },
      onerror:function(err){
        self.reportErrorAsMessage(err);
        recoverFailedMessage(fallback);
      },
      onload:function(txt){
        reportConnectionReestablished();
        if(!txt) return/*success response*/;
        try{
          const json = JSON.parse(txt);
          self.newContent({msgs:[json]});
        }catch(e){
          self.reportError(e);
        }
2442
2443
2444
2445
2446
2447
2448



2449

2450
2451
2452
2453
2454
2455
2456
2457




2458

2459
2460
2461
2462
2463
2464
2465























2466
2467


2468
2469
2470
2471
2472
2473
2474
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552

2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565

2566

2567
2568
2569



2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593

2594
2595
2596
2597
2598
2599
2600
2601
2602







+
+
+
-
+








+
+
+
+
-
+
-



-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+
+







                      term );
          }
        }
      }
    );
  }/*Chat.submitSearch()*/;

  /**
     Deal with the last poll() response and maybe re-start poll().
  */
  const afterFetch = function f(){
  const afterPollFetch = function f(isOkay = true){
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.ajaxEnd();
      Chat.e.viewMessages.classList.remove('loading');
      setTimeout(function(){
        Chat.scrollMessagesTo(1);
      }, 250);
    }
    if(Chat.timer.id) {
      clearTimeout(Chat.timer.id);
      Chat.timer.id = 0;
    }
    if(Chat._gotServerError && Chat.intervalTimer){
    if(Chat._gotServerError){
      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;
      Chat.timer.id = undefined;
      poll.running = false;
    } else {
      poll.running = false;
      if( isOkay ){
        Chat.timer.id = setTimeout(
          poll, Chat.timer.resetDelay()
        );
      }else{
        const delay = Chat.timer.incrDelay();
        const msg = D.addClass(
          Chat.reportErrorAsMessage(
            "Connection error. Retrying in ",
            delay, " ms.").e.body,
          'error-connection'
        );
        Chat.timer.id = setTimeout(()=>{
          D.remove(msg);
          poll();
        }, delay);
      }
      //console.log("isOkay =",isOkay,"currentDelay =",Chat.timer.currentDelay);
    }
  };
  afterFetch.isFirstCall = true;
  afterPollFetch.isFirstCall = true;

  /**
     FIXME: when polling fails because the remote server is
     reachable but it's not accepting HTTP requests, we should back
     off on polling for a while. e.g. if the remote web server process
     is killed, the poll fails quickly and immediately retries,
     hammering the remote server until the httpd is back up. That
     happens often during development of this application.
2487
2488
2489
2490
2491
2492
2493

2494
2495
2496
2497
2498
2499
2500





2501
2502
2503

2504
2505
2506
2507

2508
2509

2510
2511
2512
2513
2514
2515

2516
2517
2518
2519

2520
2521
2522
2523
2524

2525
2526
2527
2528
2529
2530
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627


2628
2629
2630
2631
2632
2633
2634

2635
2636
2637
2638

2639
2640
2641
2642
2643
2644
2645
2646
2647

2648
2649
2650
2651
2652
2653
2654
2655
2656
2657

2658
2659
2660
2661
2662
2663
2664







+





-
-
+
+
+
+
+


-
+



-
+


+





-
+




+




-
+






    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.ajaxStart();
      Chat.e.viewMessages.classList.add('loading');
    }
    F.fetch("chat-poll",{
      timeout: 420 * 1000/*FIXME: get the value from the server*/,
      //timeout: 8000,
      urlParams:{
        name: Chat.mxMsg
      },
      responseType: "json",
      // Disable the ajax start/end handling for this long-polling op:
      beforesend: function(){},
      aftersend: function(){},
      beforesend: function(){
        setupConnectionReestablished();
      },
      aftersend: function(){
      },
      onerror:function(err){
        Chat._isBatchLoading = false;
        if(Chat.verboseErrors) console.error(err);
        if(Chat.verboseErrors) console.error("poll onerror:",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();
        afterPollFetch(false);
      },
      onload:function(y){
        reportConnectionReestablished();
        newcontent(y);
        if(Chat._isBatchLoading){
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
        }
        afterFetch();
        afterPollFetch(true);
      }
    });
  };
  poll.isFirstCall = true;
  Chat.poll = poll;
  Chat._gotServerError = poll.running = false;
  if( window.fossil.config.chat.fromcli ){
    Chat.chatOnlyMode(true);
  }
  Chat.intervalTimer = setInterval(poll, 1000);
  Chat.timer.id = setTimeout(poll, Chat.timer.resetDelay());
  delete ForceResizeKludge.$disabled;
  ForceResizeKludge();
  Chat.animate.$disabled = false;
  setTimeout( ()=>Chat.inputFocus(), 0 );
  F.page.chat = Chat/* enables testing the APIs via the dev tools */;
});