Fossil

Check-in [5aac6ae058]
Login

Check-in [5aac6ae058]

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

Overview
Comment:When chat view is filtered on a single user, the per-message popup now offers the option to jump to that message in the larger unfiltered context. When toggling the active user timestamps on, also toggle the active user setting on if it's not already on.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | chat-user-filter
Files: files | file ages | folders
SHA3-256: 5aac6ae058f9d777664a83e26cbd77403ac43e9e99ea603976aa8f23e104e8fe
User & Date: stephan 2021-09-24 08:37:42.050
Context
2021-09-24
09:29
Changed the "message in context" animation to something more eye-catching and less stuttery. ... (check-in: fc27d6a333 user: stephan tags: chat-user-filter)
08:37
When chat view is filtered on a single user, the per-message popup now offers the option to jump to that message in the larger unfiltered context. When toggling the active user timestamps on, also toggle the active user setting on if it's not already on. ... (check-in: 5aac6ae058 user: stephan tags: chat-user-filter)
07:16
Added a description of the user activity list to www/chat.md. ... (check-in: d046ab687d user: stephan tags: chat-user-filter)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
  @       </div>
  @       <input type="file" name="file" id="chat-input-file">
  @     </div>
  @     <div id="chat-drop-details"></div>
  @   </div>
  @ </div>
  @ <div id='chat-user-list-wrapper' class='hidden'>
  @   <legend>Active Users (most recent first)</legend>
  @   <div id='chat-user-list'>
  @     <div class='help-buttonlet'>
  @      Users who have messages in the currently-loaded list.<br>
  @      Tap a user name to filter messages on that user and
  @      tap again to clear the filter.
  @     </div>
  @   </div>







|







180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
  @       </div>
  @       <input type="file" name="file" id="chat-input-file">
  @     </div>
  @     <div id="chat-drop-details"></div>
  @   </div>
  @ </div>
  @ <div id='chat-user-list-wrapper' class='hidden'>
  @   <legend>Active users (sorted by last message time)</legend>
  @   <div id='chat-user-list'>
  @     <div class='help-buttonlet'>
  @      Users who have messages in the currently-loaded list.<br>
  @      Tap a user name to filter messages on that user and
  @      tap again to clear the filter.
  @     </div>
  @   </div>
Changes to src/chat.js.
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
        this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
        /*if(eXFrom){
          eXFrom.addEventListener('click', ()=>this.e.tab.click(), 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 = {
            e: D.addClass(D.div(), 'chat-message-popup'),
            refresh:function(){
              const eMsg = this.$eMsg/*.message-widget element*/;
              if(!eMsg) return;
              D.clearElement(this.e);
              const d = new Date(eMsg.dataset.timestamp);







|


|







899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
        this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
        /*if(eXFrom){
          eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
        }*/
        return this;
      },
      /* Event handler for clicking .message-user elements to show their
         timestamps and a set of actions. */
      _handleLegendClicked: function f(ev){
        if(!f.popup){
          /* "Popup" widget */
          f.popup = {
            e: D.addClass(D.div(), 'chat-message-popup'),
            refresh:function(){
              const eMsg = this.$eMsg/*.message-widget element*/;
              if(!eMsg) return;
              D.clearElement(this.e);
              const d = new Date(eMsg.dataset.timestamp);
971
972
973
974
975
976
977



















978

979
980
981
982
983
984
985
                  D.a(F.repoUrl('timeline',{
                    u: eMsg.dataset.xfrom,
                    y: 'a'
                  }), "User's Timeline"),
                  'target', '_blank'
                );
                D.append(toolbar2, timelineLink);



















              }

              const tab = eMsg.querySelector('.message-widget-tab');
              D.append(tab, this.e);
              D.removeClass(this.e, 'hidden');
            }/*refresh()*/,
            hide: function(){
              D.addClass(D.clearElement(this.e), 'hidden');
              delete this.$eMsg;







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

>







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
                  D.a(F.repoUrl('timeline',{
                    u: eMsg.dataset.xfrom,
                    y: 'a'
                  }), "User's Timeline"),
                  'target', '_blank'
                );
                D.append(toolbar2, timelineLink);
                if(Chat.filterState.activeUser &&
                   Chat.filterState.match(eMsg.dataset.xfrom)){
                  /* Add a button to jump to clear user filter
                     and jump to this message in context. */
                  D.append(
                    this.e,
                    D.append(
                      D.addClass(D.div(), 'toolbar'),
                      D.button(
                        "Message in context",
                        function(){
                          self.hide();
                          Chat.setUserFilter(false);
                          eMsg.scrollIntoView(false);
                          D.flashNTimes(eMsg, 3);
                        })
                    )
                  );
                }/*jump-to button*/
              }
 
              const tab = eMsg.querySelector('.message-widget-tab');
              D.append(tab, this.e);
              D.removeClass(this.e, 'hidden');
            }/*refresh()*/,
            hide: function(){
              D.addClass(D.clearElement(this.e), 'hidden');
              delete this.$eMsg;
1174
1175
1176
1177
1178
1179
1180























1181
1182
1183

1184
1185
1186
1187
1188
1189
1190
      ev.stopPropagation();
      Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig
                          ? Chat.e.viewMessages : Chat.e.viewConfig);
      return false;
    };
    D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
    Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);























    /* Settings menu entries... Remember that they will be rendered in reverse
       order and the most frequently-needed ones should be closer to the start
       of this list. */

    const settingsOps = [{
      label: "Multi-line input",
      boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
      persistentSetting: 'edit-multiline',
      callback: function(){
        Chat.inputToggleSingleMulti();
      }







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







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
      ev.stopPropagation();
      Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig
                          ? Chat.e.viewMessages : Chat.e.viewConfig);
      return false;
    };
    D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
    Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);

    /** Internal acrobatics to allow certain settings toggles to access
        related toggles. */
    const namedOptions = {
      activeUsers:{
        label: "Show active users list",
        boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
        persistentSetting: 'active-user-list',
        callback: function(){
          D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
          if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
            /* When hiding this element, undo all filtering */
            D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
            /*Ideally we'd scroll the final message into view
              now, but because viewMessages is currently hidden behind
              viewConfig, scrolling is a no-op. */
            Chat.scrollMessagesTo(1);
          }else{
            Chat.updateActiveUserList();
          }
        }
      }
    };
    /* Settings menu entries... Remember that they will be rendered in
       reverse order and the most frequently-needed ones "should"
       (arguably) be closer to the start of this list so that they
       will be rendered within easier reach of the settings button. */
    const settingsOps = [{
      label: "Multi-line input",
      boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
      persistentSetting: 'edit-multiline',
      callback: function(){
        Chat.inputToggleSingleMulti();
      }
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
      label: "Show images inline",
      boolValue: ()=>Chat.settings.getBool('images-inline'),
      callback: function(){
        const v = Chat.settings.toggle('images-inline');
        F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
      }
    },{
      label: "Show timestamps in recent activity list",
      boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
      persistentSetting: 'active-user-list-timestamps',
      callback: ()=>D.toggleClass(Chat.e.activeUserList,'timestamps')
    },{
      label: "Show recent activity list",
      boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
      persistentSetting: 'active-user-list',
      callback: function(){
        D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
        if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
          /* When hiding this element, undo all filtering */
          D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
          /*Ideally we'd scroll the final message into view
            now, but because viewMessages is currently hidden behind
            viewConfig, scrolling is a no-op. */
          Chat.scrollMessagesTo(1);
        }else{


          Chat.updateActiveUserList();
        }
      }
    },{

      label: "Monospace message font",
      boolValue: ()=>document.body.classList.contains('monospace-messages'),
      persistentSetting: 'monospace-messages',
      callback: function(){
        document.body.classList.toggle('monospace-messages');
      }
    },{







|


<
<
<
<
<

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


|
>







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
      label: "Show images inline",
      boolValue: ()=>Chat.settings.getBool('images-inline'),
      callback: function(){
        const v = Chat.settings.toggle('images-inline');
        F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
      }
    },{
      label: "Timestamps in active users list",
      boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
      persistentSetting: 'active-user-list-timestamps',





      callback: function(){
        D.toggleClass(Chat.e.activeUserList,'timestamps');
        /* If the timestamp option is activated but optActiveUsers is not



           currently checked then toggle that option on as well. */

        if(Chat.e.activeUserList.classList.contains('timestamps')

           && !namedOptions.activeUsers.boolValue()){
          namedOptions.activeUsers.checkbox.checked = true;
          namedOptions.activeUsers.callback();
        }
      }
    },
    namedOptions.activeUsers,{
      label: "Monospace message font",
      boolValue: ()=>document.body.classList.contains('monospace-messages'),
      persistentSetting: 'monospace-messages',
      callback: function(){
        document.body.classList.toggle('monospace-messages');
      }
    },{
1285
1286
1287
1288
1289
1290
1291

1292
1293
1294
1295
1296
1297
1298
1299
1300
      };
      if(op.hasOwnProperty('select')){
        D.append(line, btn, op.select);
        op.select.addEventListener('change', callback, false);
      }else if(op.hasOwnProperty('boolValue')){
        if(undefined === f.$id) f.$id = 0;
        ++f.$id;

        const check = D.attr(D.checkbox(1, op.boolValue()),
                             'aria-label', op.label);
        const id = 'cfgopt'+f.$id;
        if(op.boolValue()) check.checked = true;
        D.attr(check, 'id', id);
        D.attr(btn, 'for', id);
        D.append(line, check);
        check.addEventListener('change', callback);
        D.append(line, btn);







>
|
|







1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
      };
      if(op.hasOwnProperty('select')){
        D.append(line, btn, op.select);
        op.select.addEventListener('change', callback, false);
      }else if(op.hasOwnProperty('boolValue')){
        if(undefined === f.$id) f.$id = 0;
        ++f.$id;
        const check = op.checkbox
              = D.attr(D.checkbox(1, op.boolValue()),
                       'aria-label', op.label);
        const id = 'cfgopt'+f.$id;
        if(op.boolValue()) check.checked = true;
        D.attr(check, 'id', id);
        D.attr(btn, 'for', id);
        D.append(line, check);
        check.addEventListener('change', callback);
        D.append(line, btn);
Changes to src/fossil.dom.js.
117
118
119
120
121
122
123


124
125
126



127
128
129
130
131
132
133
  dom.hr = dom.createElemFactory('hr');
  dom.br = dom.createElemFactory('br');
  /** Returns a new TEXT node which contains the text of all of the
      arguments appended together. */
  dom.text = function(/*...*/){
    return document.createTextNode(argsToArray(arguments).join(''));
  };


  dom.button = function(label){
    const b = this.create('button');
    if(label) b.appendChild(this.text(label));



    return b;
  };
  /**
     Returns a TEXTAREA element.

     Usages:








>
>
|


>
>
>







117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
  dom.hr = dom.createElemFactory('hr');
  dom.br = dom.createElemFactory('br');
  /** Returns a new TEXT node which contains the text of all of the
      arguments appended together. */
  dom.text = function(/*...*/){
    return document.createTextNode(argsToArray(arguments).join(''));
  };
  /** Returns a new Button element with the given optional
      label and on-click event listener function. */
  dom.button = function(label,callback){
    const b = this.create('button');
    if(label) b.appendChild(this.text(label));
    if('function' === typeof callback){
      b.addEventListener('click', callback, false);
    }
    return b;
  };
  /**
     Returns a TEXTAREA element.

     Usages:

675
676
677
678
679
680
681




































682
683
684
685
686
687
688
  dom.flashOnce.defaultTimeMs = 400;
  /**
     A DOM event handler which simply passes event.target
     to dom.flashOnce().
  */
  dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)





































  /**
     Attempts to copy the given text to the system clipboard. Returns
     true if it succeeds, else false.
  */
  dom.copyTextToClipboard = function(text){
    if( window.clipboardData && window.clipboardData.setData ){
      window.clipboardData.setData('Text',text);







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







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
  dom.flashOnce.defaultTimeMs = 400;
  /**
     A DOM event handler which simply passes event.target
     to dom.flashOnce().
  */
  dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)

  /**
     This variant of flashOnce() flashes the element e n times
     for a duration of howLongMs milliseconds then calls the
     afterFlashCallback() callback. It may also be called with 2
     or 3 arguments, in which case:

     2 arguments: default flash time and no callback.

     3 arguments: 3rd may be a flash delay time or a callback
     function.

     Returns this object but the flashing is asynchronous.
  */
  dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
    const args = argsToArray(arguments);
    args.splice(1,1);
    if(arguments.length===3 && 'function'===typeof howLongMs){
      afterFlashCallback = howLongMs;
      howLongMs = args[1] = this.flashOnce.defaultTimeMs;
    }else if(arguments.length<3){
      args[1] = this.flashOnce.defaultTimeMs;
    }
    n = +n;
    const self = this;
    const cb = args[2] = function f(){
      if(--n){
        setTimeout(()=>self.flashOnce(e, howLongMs, f),
                   howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
      }else if(afterFlashCallback){
        afterFlashCallback();
      }
    };
    this.flashOnce.apply(this, args);
    return this;
  };
  
  /**
     Attempts to copy the given text to the system clipboard. Returns
     true if it succeeds, else false.
  */
  dom.copyTextToClipboard = function(text){
    if( window.clipboardData && window.clipboardData.setData ){
      window.clipboardData.setData('Text',text);