Diff
Not logged in

Differences From Artifact [69a71e600f]:

To Artifact [32db0175d8]:


153
154
155
156
157
158
159
160

161
162
163
164
165
166
167
153
154
155
156
157
158
159

160
161
162
163
164
165
166
167







-
+







        }
        const m = this.e.messagesWrapper,
              sTop = m.scrollTop,
              mh1 = m.clientHeight;
        D.addClass(old, 'hidden');
        D.removeClass(this.e.inputCurrent, 'hidden');
        const mh2 = m.clientHeight;
        m.scrollTo( 0, sTop + (mh1-mh2));
        m.scrollTo(0, sTop + (mh1-mh2));
        this.e.inputCurrent.value = old.value;
        old.value = '';
        return this;
      },
      /**
         If passed true or no arguments, switches to multi-line mode
         if currently in single-line mode. If passed false, switches
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
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







-
+









-
+

-
+







          const fe = mip.nextElementSibling;
          if(fe) mip.parentNode.insertBefore(e, fe);
          else D.append(mip.parentNode, e);
        }else{
          D.append(holder,e);
          this.e.newestMessage = e;
        }
        if(!atEnd && !this.isBatchLoading
        if(!atEnd && !this._isBatchLoading
           && e.dataset.xfrom!==this.me
           && (prevMessage
               ? !this.messageIsInView(prevMessage)
               : false)){
          /* If a new non-history message arrives while the user is
             scrolled elsewhere, do not scroll to the latest
             message, but gently alert the user that a new message
             has arrived. */
          F.toast.message("New message has arrived.");
        }else if(!this.isBatchLoading && e.dataset.xfrom===Chat.me){
        }else if(!this._isBatchLoading && e.dataset.xfrom===Chat.me){
          this.scheduleScrollOfMsg(e);
        }else if(!this.isBatchLoading){
        }else if(!this._isBatchLoading){
          /* When a message from someone else arrives, we have to
             figure out whether or not to scroll it into view. Ideally
             we'd just stuff it in the UI and let the flexbox layout
             DTRT, but Safari has expressed, in no uncertain terms,
             some disappointment with that approach, so we'll
             heuristicize it: if the previous last message is in view,
             assume the user is at or near the input element and
382
383
384
385
386
387
388




389
390
391
392
393






















394
395
396
397
398
399
400
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426







+
+
+
+





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







    cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
    cs.pageTitleOrig = cs.e.pageTitle.innerText;

    const qs = (e)=>document.querySelector(e);
    const argsToArray = function(args){
      return Array.prototype.slice.call(args,0);
    };
    /**
       Reports an error via console.error() and as a toast message.
       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);
    };
    /**
       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(/*msg args*/){
      const args = argsToArray(arguments);
      console.error("chat error:",args);
      const d = new Date().toISOString(),
            msg = {
              isError: true,
              xfrom: "chat.js",
              msgid: -1,
              mtime: d,
              lmtime: d,
              xmsg: args
            }, mw = new this.MessageWidget();
      mw.setMessage(msg);
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();
    };

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

    /** Finds the last .message-widget element and returns it or
        the undefined value if none are found. */
433
434
435
436
437
438
439

440
441


442
443
















444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463

464
465

466
467
468
469
470
471
472
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
506
507
508

509
510
511
512
513
514
515
516







+
-
-
+
+


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




















+

-
+







    /** Given a .message-row element, this function returns whethe the
        current user may, at least hypothetically, delete the message
        globally.  A user may always delete a local copy of a
        post. The server may trump this, e.g. if the login has been
        cancelled after this page was loaded.
    */
    cs.userMayDelete = function(eMsg){
      return eMsg.msgid>0
      return this.me === eMsg.dataset.xfrom
        || F.user.isAdmin/*will be confirmed server-side*/;
        && (this.me === eMsg.dataset.xfrom
            || F.user.isAdmin/*will be confirmed server-side*/);
    };

    /** Returns a new Error() object encapsulating state from the given
        fetch() response promise. */
    cs._newResponseError = function(response){
      return new Error([
        "HTTP status ", response.status,": ",response.url,": ",
        response.statusText].join(''));
    };
    
    /** Helper for reporting HTTP-level response errors via fetch().
        If response.ok then response.json() is returned, else an Error
        is thrown. */
    cs._fetchJsonOrError = function(response){
      if(response.ok) return response.json();
      else throw cs._newResponseError(response);
    };
    
    /**
       Removes the given message ID from the local chat record and, if
       the message was posted by this user OR this user in an
       admin/setup, also submits it for removal on the remote.

       id may optionally be a DOM element, in which case it must be a
       .message-row element.
    */
    cs.deleteMessage = function(id){
      var e;
      if(id instanceof HTMLElement){
        e = id;
        id = e.dataset.msgid;
      }else{
        e = this.getMessageElemById(id);
      }
      if(!(e instanceof HTMLElement)) return;
      if(this.userMayDelete(e)){
        this.ajaxStart();
        fetch("chat-delete?name=" + id)
          .then(this._fetchJsonOrError)
          .then(()=>this.deleteMessageElem(e))
          .catch(err=>this.reportError(err))
          .catch(err=>this.reportErrorAsMessage(err))
          .finally(()=>this.ajaxEnd());
      }else{
        this.deleteMessageElem(id);
      }
    };
    document.addEventListener('visibilitychange', function(ev){
      cs.pageIsActive = ('visible' === document.visibilityState);
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

506
507
508
509
510
511
512
525
526
527
528
529
530
531

532
533
534
535
536
537
538

539

540
541
542
543
544
545

546


547
548
549
550
551
552
553
554







-
+






-
+
-






-
+
-
-
+







     Custom widget type for rendering messages (one message per
     instance). These are modelled after FIELDSET elements but we
     don't use FIELDSET because of cross-browser inconsistencies in
     features of the FIELDSET/LEGEND combination, e.g. inability to
     align legends via CSS in Firefox and clicking-related
     deficiencies in Safari.
  */
  const MessageWidget = (function(){
  Chat.MessageWidget = (function(){
    const cf = function(){
      this.e = {
        body: D.addClass(D.div(), 'message-widget'),
        tab: D.addClass(D.span(), 'message-widget-tab'),
        content: D.addClass(D.div(), 'message-widget-content')
      };
      D.append(this.e.body, this.e.tab);
      D.append(this.e.body, this.e.tab, this.e.content);
      D.append(this.e.body, this.e.content);
      this.e.tab.setAttribute('role', 'button');
    };
    cf.prototype = {
      setLabel: function(label){
        return this;
      },
      setPopupCallback: function(callback){
      scrollIntoView: function(){
        this.e.tab.addEventListener('click', callback, false);
        return this;
        this.e.content.scrollIntoView();
      },
      setMessage: function(m){
        const ds = this.e.body.dataset;
        ds.timestamp = m.mtime;
        ds.lmtime = m.lmtime;
        ds.msgid = m.msgid;
        ds.xfrom = m.xfrom;
556
557
558
559
560
561
562




563
564




565
























566













































567
568
569
570
571
572
573
598
599
600
601
602
603
604
605
606
607
608


609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637

638
639
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
678
679
680
681
682
683
684
685
686
687
688
689







+
+
+
+
-
-
+
+
+
+

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







          // 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
          // chat_format_to_html() routine in the server for details.
          //
          // Hence, even though innerHTML is normally frowned upon, it is
          // perfectly safe to use in this context.
          if(m.xmsg instanceof Array){
            // Used by Chat.reportErrorAsMessage()
            D.append(contentTarget, m.xmsg);
          }else{
          contentTarget.innerHTML = m.xmsg;
        }
            contentTarget.innerHTML = m.xmsg;
          }
        }
        this.e.tab.addEventListener('click', this._handleLegendClicked, false);
        return this;
      },
      /* Event handler for clicking .message-user elements to show their
         timestamps. */
      _handleLegendClicked: function f(ev){
        if(!f.popup){
          /* Timestamp popup widget */
          f.popup = new F.PopupWidget({
            cssClass: ['fossil-tooltip', 'chat-message-popup'],
            refresh:function(){
              const eMsg = this._eMsg;
              if(!eMsg) return;
              D.clearElement(this.e);
              const d = new Date(eMsg.dataset.timestamp);
              if(d.getMinutes().toString()!=="NaN"){
                // Date works, render informative timestamps
                const xfrom = eMsg.dataset.xfrom;
                D.append(this.e,
                         D.append(D.span(), localTimeString(d)," ",Chat.me," time"),
                         D.append(D.span(), iso8601ish(d)));
                if(eMsg.dataset.lmtime && xfrom!==Chat.me){
                  D.append(this.e,
                           D.append(D.span(), localTime8601(
                             new Date(eMsg.dataset.lmtime)
                           ).replace('T',' ')," ",xfrom," time"));
      }
                }
              }else{
                // Date doesn't work, so dumb it down...
                D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," zulu"));
              }
              const toolbar = D.addClass(D.div(), 'toolbar');
              D.append(this.e, toolbar);
              const btnDeleteLocal = D.button("Delete locally");
              D.append(toolbar, btnDeleteLocal);
              const self = this;
              btnDeleteLocal.addEventListener('click', function(){
                self.hide();
                Chat.deleteMessageElem(eMsg);
              });
              if(Chat.userMayDelete(eMsg)){
                const btnDeleteGlobal = D.button("Delete globally");
                D.append(toolbar, btnDeleteGlobal);
                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();
        const eMsg = ev.target.parentNode/*the owning .message-widget element*/;
        f.popup._eMsg = eMsg;
        let x = rect.left, y = rect.topm;
        f.popup.show(ev.target)/*so we can get its computed size*/;
        if(eMsg.dataset.xfrom===Chat.me
           && document.body.classList.contains('my-messages-right')){
          // Shift popup to the left for right-aligned messages to avoid
          // truncation off the right edge of the page.
          const pRect = f.popup.e.getBoundingClientRect();
          x = rect.right - pRect.width;
        }
        f.popup.show(x, y);
      }/*_handleLegendClicked()*/
    };
    return cf;
  })()/*MessageWidget*/;

  const BlobXferState = (function(){/*drag/drop bits...*/
    /* State for paste and drag/drop */
    const bxs = {
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
788
789
790
791
792
793
794


795
796
797
798
799
800
801
802
803
804
805
806
807

808
809
810
811
812
813
814
815







-
-
+
+
+
+









-
+







    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){
    }).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.reportError(e));
      .catch((e)=>this.reportErrorAsMessage(e));
    BlobXferState.clear();
    Chat.inputValue("").inputFocus();
  };

  Chat.e.inputSingle.addEventListener('keydown',function(ev){
    if(13===ev.keyCode/*ENTER*/){
      ev.preventDefault();
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
848
849
850
851
852
853
854




































































855
856
857
858
859
860
861







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







    ].join('');
  };
  /* Returns an almost-ISO8601 form of Date object d. */
  const iso8601ish = function(d){
    return d.toISOString()
      .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu');
  };
  /* Event handler for clicking .message-user elements to show their
     timestamps. */
  const handleLegendClicked = function f(ev){
    if(!f.popup){
      /* Timestamp popup widget */
      f.popup = new F.PopupWidget({
        cssClass: ['fossil-tooltip', 'chat-message-popup'],
        refresh:function(){
          const eMsg = this._eMsg;
          if(!eMsg) return;
          D.clearElement(this.e);
          const d = new Date(eMsg.dataset.timestamp);
          if(d.getMinutes().toString()!=="NaN"){
            // Date works, render informative timestamps
            const xfrom = eMsg.dataset.xfrom;
            D.append(this.e,
                     D.append(D.span(), localTimeString(d)," ",Chat.me," time"),
                     D.append(D.span(), iso8601ish(d)));
            if(eMsg.dataset.lmtime && xfrom!==Chat.me){
              D.append(this.e,
                       D.append(D.span(), localTime8601(
                         new Date(eMsg.dataset.lmtime)
                       ).replace('T',' ')," ",xfrom," time"));
            }
           }else{
            // Date doesn't work, so dumb it down...
            D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," zulu"));
          }
          const toolbar = D.addClass(D.div(), 'toolbar');
          D.append(this.e, toolbar);
          const btnDeleteLocal = D.button("Delete locally");
          D.append(toolbar, btnDeleteLocal);
          const self = this;
          btnDeleteLocal.addEventListener('click', function(){
            self.hide();
            Chat.deleteMessageElem(eMsg);
          });
          if(Chat.userMayDelete(eMsg)){
            const btnDeleteGlobal = D.button("Delete globally");
            D.append(toolbar, btnDeleteGlobal);
            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();
    const eMsg = ev.target.parentNode/*the owning .message-widget element*/;
    f.popup._eMsg = eMsg;
    let x = rect.left, y = rect.topm;
    f.popup.show(ev.target)/*so we can get its computed size*/;
    if(eMsg.dataset.xfrom===Chat.me
       && document.body.classList.contains('my-messages-right')){
      // Shift popup to the left for right-aligned messages to avoid
      // truncation off the right edge of the page.
      const pRect = f.popup.e.getBoundingClientRect();
      x = rect.right - pRect.width;
    }
    f.popup.show(x, y);
  }/*handleLegendClicked()*/;

  (function(){/*Set up #chat-settings-button */
    const settingsButton = document.querySelector('#chat-settings-button');
    var popupSize = undefined/*placement workaround*/;
    const settingsPopup = new F.PopupWidget({
      cssClass: ['fossil-tooltip', 'chat-settings-popup']
    });
937
938
939
940
941
942
943
944

945
946
947
948
949

950
951
952
953
954
955
956
987
988
989
990
991
992
993

994
995

996
997

998
999
1000
1001
1002
1003
1004
1005







-
+

-


-
+







        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()
        const row = new Chat.MessageWidget()
        row.setMessage(m);
        row.setPopupCallback(handleLegendClicked);
        Chat.injectMessageElem(row.e.body,atEnd);
        if(m.isError){
          Chat.gotServerError = m;
          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;
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
1027
1028
1029
1030
1031
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







-
+




-
+




-
+

-
+


-
+

-
+







      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;
      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(x=>x.json())
        .then(Chat._fetchJsonOrError)
        .then(function(x){
          gotMessages = x.msgs.length;
          newcontent(x,true);
        })
        .catch(e=>Chat.reportError(e))
        .catch(e=>Chat.reportErrorAsMessage(e))
        .finally(function(){
          Chat.isBatchLoading = false;
          Chat._isBatchLoading = false;
          Chat.e.messagesWrapper.classList.remove('loading');
          Chat.ajaxEnd();
          if(Chat.gotServerError){
          if(Chat._gotServerError){
            F.toast.error("Got an error response from the server. ",
                          "See message for details");
                          "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
1041
1042
1043
1044
1045
1046
1047
1048

1049
1050

1051
1052
1053
1054
1055
1056
1057
1058

1059
1060
1061
1062
1063
1064
1065

1066
1067
1068
1069
1070
1071
1072

1073
1074

1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1090
1091
1092
1093
1094
1095
1096

1097
1098

1099
1100
1101
1102
1103
1104
1105
1106

1107
1108
1109
1110
1111
1112
1113

1114
1115
1116
1117
1118
1119
1120

1121
1122

1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134







-
+

-
+







-
+






-
+






-
+

-
+











  async function poll(isFirstCall){
    if(poll.running) return;
    poll.running = true;
    if(isFirstCall){
      Chat.ajaxStart();
      Chat.e.messagesWrapper.classList.add('loading');
    }
    Chat.isBatchLoading = isFirstCall;
    Chat._isBatchLoading = isFirstCall;
    var p = fetch("chat-poll?name=" + Chat.mxMsg);
    p.then(x=>x.json())
    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._isBatchLoading = false;
          Chat.ajaxEnd();
          Chat.e.messagesWrapper.classList.remove('loading');
          setTimeout(function(){
            Chat.scrollMessagesTo(1);
          }, 250);
        }
        if(Chat.gotServerError && Chat.intervalTimer){
        if(Chat._gotServerError && Chat.intervalTimer){
          clearInterval(Chat.intervalTimer);
          delete Chat.intervalTimer;
        }
        poll.running=false;
      });
  }
  Chat.gotServerError = poll.running = false;
  Chat._gotServerError = poll.running = false;
  poll(true);
  if(!Chat.gotServerError){
  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 */;
})();