Fossil SCM

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.

stephan 2021-09-24 08:37 chat-user-filter
Commit 5aac6ae058f9d777664a83e26cbd77403ac43e9e99ea603976aa8f23e104e8fe
3 files changed +1 -1 +62 -24 +42 -1
+1 -1
--- src/chat.c
+++ src/chat.c
@@ -182,11 +182,11 @@
182182
@ </div>
183183
@ <div id="chat-drop-details"></div>
184184
@ </div>
185185
@ </div>
186186
@ <div id='chat-user-list-wrapper' class='hidden'>
187
- @ <legend>Active Users (most recent first)</legend>
187
+ @ <legend>Active users (sorted by last message time)</legend>
188188
@ <div id='chat-user-list'>
189189
@ <div class='help-buttonlet'>
190190
@ Users who have messages in the currently-loaded list.<br>
191191
@ Tap a user name to filter messages on that user and
192192
@ tap again to clear the filter.
193193
--- src/chat.c
+++ src/chat.c
@@ -182,11 +182,11 @@
182 @ </div>
183 @ <div id="chat-drop-details"></div>
184 @ </div>
185 @ </div>
186 @ <div id='chat-user-list-wrapper' class='hidden'>
187 @ <legend>Active Users (most recent first)</legend>
188 @ <div id='chat-user-list'>
189 @ <div class='help-buttonlet'>
190 @ Users who have messages in the currently-loaded list.<br>
191 @ Tap a user name to filter messages on that user and
192 @ tap again to clear the filter.
193
--- src/chat.c
+++ src/chat.c
@@ -182,11 +182,11 @@
182 @ </div>
183 @ <div id="chat-drop-details"></div>
184 @ </div>
185 @ </div>
186 @ <div id='chat-user-list-wrapper' class='hidden'>
187 @ <legend>Active users (sorted by last message time)</legend>
188 @ <div id='chat-user-list'>
189 @ <div class='help-buttonlet'>
190 @ Users who have messages in the currently-loaded list.<br>
191 @ Tap a user name to filter messages on that user and
192 @ tap again to clear the filter.
193
+62 -24
--- src/chat.js
+++ src/chat.js
@@ -901,14 +901,14 @@
901901
eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
902902
}*/
903903
return this;
904904
},
905905
/* Event handler for clicking .message-user elements to show their
906
- timestamps. */
906
+ timestamps and a set of actions. */
907907
_handleLegendClicked: function f(ev){
908908
if(!f.popup){
909
- /* Timestamp popup widget */
909
+ /* "Popup" widget */
910910
f.popup = {
911911
e: D.addClass(D.div(), 'chat-message-popup'),
912912
refresh:function(){
913913
const eMsg = this.$eMsg/*.message-widget element*/;
914914
if(!eMsg) return;
@@ -973,11 +973,31 @@
973973
y: 'a'
974974
}), "User's Timeline"),
975975
'target', '_blank'
976976
);
977977
D.append(toolbar2, timelineLink);
978
+ if(Chat.filterState.activeUser &&
979
+ Chat.filterState.match(eMsg.dataset.xfrom)){
980
+ /* Add a button to jump to clear user filter
981
+ and jump to this message in context. */
982
+ D.append(
983
+ this.e,
984
+ D.append(
985
+ D.addClass(D.div(), 'toolbar'),
986
+ D.button(
987
+ "Message in context",
988
+ function(){
989
+ self.hide();
990
+ Chat.setUserFilter(false);
991
+ eMsg.scrollIntoView(false);
992
+ D.flashNTimes(eMsg, 3);
993
+ })
994
+ )
995
+ );
996
+ }/*jump-to button*/
978997
}
998
+
979999
const tab = eMsg.querySelector('.message-widget-tab');
9801000
D.append(tab, this.e);
9811001
D.removeClass(this.e, 'hidden');
9821002
}/*refresh()*/,
9831003
hide: function(){
@@ -1176,13 +1196,37 @@
11761196
? Chat.e.viewMessages : Chat.e.viewConfig);
11771197
return false;
11781198
};
11791199
D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
11801200
Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
1181
- /* Settings menu entries... Remember that they will be rendered in reverse
1182
- order and the most frequently-needed ones should be closer to the start
1183
- of this list. */
1201
+
1202
+ /** Internal acrobatics to allow certain settings toggles to access
1203
+ related toggles. */
1204
+ const namedOptions = {
1205
+ activeUsers:{
1206
+ label: "Show active users list",
1207
+ boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1208
+ persistentSetting: 'active-user-list',
1209
+ callback: function(){
1210
+ D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1211
+ if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1212
+ /* When hiding this element, undo all filtering */
1213
+ D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
1214
+ /*Ideally we'd scroll the final message into view
1215
+ now, but because viewMessages is currently hidden behind
1216
+ viewConfig, scrolling is a no-op. */
1217
+ Chat.scrollMessagesTo(1);
1218
+ }else{
1219
+ Chat.updateActiveUserList();
1220
+ }
1221
+ }
1222
+ }
1223
+ };
1224
+ /* Settings menu entries... Remember that they will be rendered in
1225
+ reverse order and the most frequently-needed ones "should"
1226
+ (arguably) be closer to the start of this list so that they
1227
+ will be rendered within easier reach of the settings button. */
11841228
const settingsOps = [{
11851229
label: "Multi-line input",
11861230
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
11871231
persistentSetting: 'edit-multiline',
11881232
callback: function(){
@@ -1200,32 +1244,25 @@
12001244
callback: function(){
12011245
const v = Chat.settings.toggle('images-inline');
12021246
F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
12031247
}
12041248
},{
1205
- label: "Show timestamps in recent activity list",
1249
+ label: "Timestamps in active users list",
12061250
boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
12071251
persistentSetting: 'active-user-list-timestamps',
1208
- callback: ()=>D.toggleClass(Chat.e.activeUserList,'timestamps')
1209
- },{
1210
- label: "Show recent activity list",
1211
- boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1212
- persistentSetting: 'active-user-list',
12131252
callback: function(){
1214
- D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1215
- if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1216
- /* When hiding this element, undo all filtering */
1217
- D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
1218
- /*Ideally we'd scroll the final message into view
1219
- now, but because viewMessages is currently hidden behind
1220
- viewConfig, scrolling is a no-op. */
1221
- Chat.scrollMessagesTo(1);
1222
- }else{
1223
- Chat.updateActiveUserList();
1253
+ D.toggleClass(Chat.e.activeUserList,'timestamps');
1254
+ /* If the timestamp option is activated but optActiveUsers is not
1255
+ currently checked then toggle that option on as well. */
1256
+ if(Chat.e.activeUserList.classList.contains('timestamps')
1257
+ && !namedOptions.activeUsers.boolValue()){
1258
+ namedOptions.activeUsers.checkbox.checked = true;
1259
+ namedOptions.activeUsers.callback();
12241260
}
12251261
}
1226
- },{
1262
+ },
1263
+ namedOptions.activeUsers,{
12271264
label: "Monospace message font",
12281265
boolValue: ()=>document.body.classList.contains('monospace-messages'),
12291266
persistentSetting: 'monospace-messages',
12301267
callback: function(){
12311268
document.body.classList.toggle('monospace-messages');
@@ -1287,12 +1324,13 @@
12871324
D.append(line, btn, op.select);
12881325
op.select.addEventListener('change', callback, false);
12891326
}else if(op.hasOwnProperty('boolValue')){
12901327
if(undefined === f.$id) f.$id = 0;
12911328
++f.$id;
1292
- const check = D.attr(D.checkbox(1, op.boolValue()),
1293
- 'aria-label', op.label);
1329
+ const check = op.checkbox
1330
+ = D.attr(D.checkbox(1, op.boolValue()),
1331
+ 'aria-label', op.label);
12941332
const id = 'cfgopt'+f.$id;
12951333
if(op.boolValue()) check.checked = true;
12961334
D.attr(check, 'id', id);
12971335
D.attr(btn, 'for', id);
12981336
D.append(line, check);
12991337
--- src/chat.js
+++ src/chat.js
@@ -901,14 +901,14 @@
901 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
902 }*/
903 return this;
904 },
905 /* Event handler for clicking .message-user elements to show their
906 timestamps. */
907 _handleLegendClicked: function f(ev){
908 if(!f.popup){
909 /* Timestamp popup widget */
910 f.popup = {
911 e: D.addClass(D.div(), 'chat-message-popup'),
912 refresh:function(){
913 const eMsg = this.$eMsg/*.message-widget element*/;
914 if(!eMsg) return;
@@ -973,11 +973,31 @@
973 y: 'a'
974 }), "User's Timeline"),
975 'target', '_blank'
976 );
977 D.append(toolbar2, timelineLink);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978 }
 
979 const tab = eMsg.querySelector('.message-widget-tab');
980 D.append(tab, this.e);
981 D.removeClass(this.e, 'hidden');
982 }/*refresh()*/,
983 hide: function(){
@@ -1176,13 +1196,37 @@
1176 ? Chat.e.viewMessages : Chat.e.viewConfig);
1177 return false;
1178 };
1179 D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
1180 Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
1181 /* Settings menu entries... Remember that they will be rendered in reverse
1182 order and the most frequently-needed ones should be closer to the start
1183 of this list. */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1184 const settingsOps = [{
1185 label: "Multi-line input",
1186 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1187 persistentSetting: 'edit-multiline',
1188 callback: function(){
@@ -1200,32 +1244,25 @@
1200 callback: function(){
1201 const v = Chat.settings.toggle('images-inline');
1202 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1203 }
1204 },{
1205 label: "Show timestamps in recent activity list",
1206 boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1207 persistentSetting: 'active-user-list-timestamps',
1208 callback: ()=>D.toggleClass(Chat.e.activeUserList,'timestamps')
1209 },{
1210 label: "Show recent activity list",
1211 boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1212 persistentSetting: 'active-user-list',
1213 callback: function(){
1214 D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1215 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1216 /* When hiding this element, undo all filtering */
1217 D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
1218 /*Ideally we'd scroll the final message into view
1219 now, but because viewMessages is currently hidden behind
1220 viewConfig, scrolling is a no-op. */
1221 Chat.scrollMessagesTo(1);
1222 }else{
1223 Chat.updateActiveUserList();
1224 }
1225 }
1226 },{
 
1227 label: "Monospace message font",
1228 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1229 persistentSetting: 'monospace-messages',
1230 callback: function(){
1231 document.body.classList.toggle('monospace-messages');
@@ -1287,12 +1324,13 @@
1287 D.append(line, btn, op.select);
1288 op.select.addEventListener('change', callback, false);
1289 }else if(op.hasOwnProperty('boolValue')){
1290 if(undefined === f.$id) f.$id = 0;
1291 ++f.$id;
1292 const check = D.attr(D.checkbox(1, op.boolValue()),
1293 'aria-label', op.label);
 
1294 const id = 'cfgopt'+f.$id;
1295 if(op.boolValue()) check.checked = true;
1296 D.attr(check, 'id', id);
1297 D.attr(btn, 'for', id);
1298 D.append(line, check);
1299
--- src/chat.js
+++ src/chat.js
@@ -901,14 +901,14 @@
901 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
902 }*/
903 return this;
904 },
905 /* Event handler for clicking .message-user elements to show their
906 timestamps and a set of actions. */
907 _handleLegendClicked: function f(ev){
908 if(!f.popup){
909 /* "Popup" widget */
910 f.popup = {
911 e: D.addClass(D.div(), 'chat-message-popup'),
912 refresh:function(){
913 const eMsg = this.$eMsg/*.message-widget element*/;
914 if(!eMsg) return;
@@ -973,11 +973,31 @@
973 y: 'a'
974 }), "User's Timeline"),
975 'target', '_blank'
976 );
977 D.append(toolbar2, timelineLink);
978 if(Chat.filterState.activeUser &&
979 Chat.filterState.match(eMsg.dataset.xfrom)){
980 /* Add a button to jump to clear user filter
981 and jump to this message in context. */
982 D.append(
983 this.e,
984 D.append(
985 D.addClass(D.div(), 'toolbar'),
986 D.button(
987 "Message in context",
988 function(){
989 self.hide();
990 Chat.setUserFilter(false);
991 eMsg.scrollIntoView(false);
992 D.flashNTimes(eMsg, 3);
993 })
994 )
995 );
996 }/*jump-to button*/
997 }
998
999 const tab = eMsg.querySelector('.message-widget-tab');
1000 D.append(tab, this.e);
1001 D.removeClass(this.e, 'hidden');
1002 }/*refresh()*/,
1003 hide: function(){
@@ -1176,13 +1196,37 @@
1196 ? Chat.e.viewMessages : Chat.e.viewConfig);
1197 return false;
1198 };
1199 D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
1200 Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
1201
1202 /** Internal acrobatics to allow certain settings toggles to access
1203 related toggles. */
1204 const namedOptions = {
1205 activeUsers:{
1206 label: "Show active users list",
1207 boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1208 persistentSetting: 'active-user-list',
1209 callback: function(){
1210 D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1211 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1212 /* When hiding this element, undo all filtering */
1213 D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
1214 /*Ideally we'd scroll the final message into view
1215 now, but because viewMessages is currently hidden behind
1216 viewConfig, scrolling is a no-op. */
1217 Chat.scrollMessagesTo(1);
1218 }else{
1219 Chat.updateActiveUserList();
1220 }
1221 }
1222 }
1223 };
1224 /* Settings menu entries... Remember that they will be rendered in
1225 reverse order and the most frequently-needed ones "should"
1226 (arguably) be closer to the start of this list so that they
1227 will be rendered within easier reach of the settings button. */
1228 const settingsOps = [{
1229 label: "Multi-line input",
1230 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1231 persistentSetting: 'edit-multiline',
1232 callback: function(){
@@ -1200,32 +1244,25 @@
1244 callback: function(){
1245 const v = Chat.settings.toggle('images-inline');
1246 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1247 }
1248 },{
1249 label: "Timestamps in active users list",
1250 boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1251 persistentSetting: 'active-user-list-timestamps',
 
 
 
 
 
1252 callback: function(){
1253 D.toggleClass(Chat.e.activeUserList,'timestamps');
1254 /* If the timestamp option is activated but optActiveUsers is not
1255 currently checked then toggle that option on as well. */
1256 if(Chat.e.activeUserList.classList.contains('timestamps')
1257 && !namedOptions.activeUsers.boolValue()){
1258 namedOptions.activeUsers.checkbox.checked = true;
1259 namedOptions.activeUsers.callback();
 
 
 
1260 }
1261 }
1262 },
1263 namedOptions.activeUsers,{
1264 label: "Monospace message font",
1265 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1266 persistentSetting: 'monospace-messages',
1267 callback: function(){
1268 document.body.classList.toggle('monospace-messages');
@@ -1287,12 +1324,13 @@
1324 D.append(line, btn, op.select);
1325 op.select.addEventListener('change', callback, false);
1326 }else if(op.hasOwnProperty('boolValue')){
1327 if(undefined === f.$id) f.$id = 0;
1328 ++f.$id;
1329 const check = op.checkbox
1330 = D.attr(D.checkbox(1, op.boolValue()),
1331 'aria-label', op.label);
1332 const id = 'cfgopt'+f.$id;
1333 if(op.boolValue()) check.checked = true;
1334 D.attr(check, 'id', id);
1335 D.attr(btn, 'for', id);
1336 D.append(line, check);
1337
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -119,13 +119,18 @@
119119
/** Returns a new TEXT node which contains the text of all of the
120120
arguments appended together. */
121121
dom.text = function(/*...*/){
122122
return document.createTextNode(argsToArray(arguments).join(''));
123123
};
124
- dom.button = function(label){
124
+ /** Returns a new Button element with the given optional
125
+ label and on-click event listener function. */
126
+ dom.button = function(label,callback){
125127
const b = this.create('button');
126128
if(label) b.appendChild(this.text(label));
129
+ if('function' === typeof callback){
130
+ b.addEventListener('click', callback, false);
131
+ }
127132
return b;
128133
};
129134
/**
130135
Returns a TEXTAREA element.
131136
@@ -677,10 +682,46 @@
677682
A DOM event handler which simply passes event.target
678683
to dom.flashOnce().
679684
*/
680685
dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
681686
687
+ /**
688
+ This variant of flashOnce() flashes the element e n times
689
+ for a duration of howLongMs milliseconds then calls the
690
+ afterFlashCallback() callback. It may also be called with 2
691
+ or 3 arguments, in which case:
692
+
693
+ 2 arguments: default flash time and no callback.
694
+
695
+ 3 arguments: 3rd may be a flash delay time or a callback
696
+ function.
697
+
698
+ Returns this object but the flashing is asynchronous.
699
+ */
700
+ dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
701
+ const args = argsToArray(arguments);
702
+ args.splice(1,1);
703
+ if(arguments.length===3 && 'function'===typeof howLongMs){
704
+ afterFlashCallback = howLongMs;
705
+ howLongMs = args[1] = this.flashOnce.defaultTimeMs;
706
+ }else if(arguments.length<3){
707
+ args[1] = this.flashOnce.defaultTimeMs;
708
+ }
709
+ n = +n;
710
+ const self = this;
711
+ const cb = args[2] = function f(){
712
+ if(--n){
713
+ setTimeout(()=>self.flashOnce(e, howLongMs, f),
714
+ howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
715
+ }else if(afterFlashCallback){
716
+ afterFlashCallback();
717
+ }
718
+ };
719
+ this.flashOnce.apply(this, args);
720
+ return this;
721
+ };
722
+
682723
/**
683724
Attempts to copy the given text to the system clipboard. Returns
684725
true if it succeeds, else false.
685726
*/
686727
dom.copyTextToClipboard = function(text){
687728
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -119,13 +119,18 @@
119 /** Returns a new TEXT node which contains the text of all of the
120 arguments appended together. */
121 dom.text = function(/*...*/){
122 return document.createTextNode(argsToArray(arguments).join(''));
123 };
124 dom.button = function(label){
 
 
125 const b = this.create('button');
126 if(label) b.appendChild(this.text(label));
 
 
 
127 return b;
128 };
129 /**
130 Returns a TEXTAREA element.
131
@@ -677,10 +682,46 @@
677 A DOM event handler which simply passes event.target
678 to dom.flashOnce().
679 */
680 dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682 /**
683 Attempts to copy the given text to the system clipboard. Returns
684 true if it succeeds, else false.
685 */
686 dom.copyTextToClipboard = function(text){
687
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -119,13 +119,18 @@
119 /** Returns a new TEXT node which contains the text of all of the
120 arguments appended together. */
121 dom.text = function(/*...*/){
122 return document.createTextNode(argsToArray(arguments).join(''));
123 };
124 /** Returns a new Button element with the given optional
125 label and on-click event listener function. */
126 dom.button = function(label,callback){
127 const b = this.create('button');
128 if(label) b.appendChild(this.text(label));
129 if('function' === typeof callback){
130 b.addEventListener('click', callback, false);
131 }
132 return b;
133 };
134 /**
135 Returns a TEXTAREA element.
136
@@ -677,10 +682,46 @@
682 A DOM event handler which simply passes event.target
683 to dom.flashOnce().
684 */
685 dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
686
687 /**
688 This variant of flashOnce() flashes the element e n times
689 for a duration of howLongMs milliseconds then calls the
690 afterFlashCallback() callback. It may also be called with 2
691 or 3 arguments, in which case:
692
693 2 arguments: default flash time and no callback.
694
695 3 arguments: 3rd may be a flash delay time or a callback
696 function.
697
698 Returns this object but the flashing is asynchronous.
699 */
700 dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
701 const args = argsToArray(arguments);
702 args.splice(1,1);
703 if(arguments.length===3 && 'function'===typeof howLongMs){
704 afterFlashCallback = howLongMs;
705 howLongMs = args[1] = this.flashOnce.defaultTimeMs;
706 }else if(arguments.length<3){
707 args[1] = this.flashOnce.defaultTimeMs;
708 }
709 n = +n;
710 const self = this;
711 const cb = args[2] = function f(){
712 if(--n){
713 setTimeout(()=>self.flashOnce(e, howLongMs, f),
714 howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
715 }else if(afterFlashCallback){
716 afterFlashCallback();
717 }
718 };
719 this.flashOnce.apply(this, args);
720 return this;
721 };
722
723 /**
724 Attempts to copy the given text to the system clipboard. Returns
725 true if it succeeds, else false.
726 */
727 dom.copyTextToClipboard = function(text){
728

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button