Fossil SCM

Improved the behavior in the face of multiple filters, applying only the most recent one. Added a button to clear filters which appears along the bottom of the message area if any filter is active.

stephan 2021-09-25 10:54 markdown-tagrefs
Commit 93bf25055ab7864649ace03d5062ef8b0d441b7db027c05df75215a91aa5c3c5
+1
--- src/chat.c
+++ src/chat.c
@@ -194,10 +194,11 @@
194194
@ </span>
195195
@ <span>Active users (sorted by last message time)</span>
196196
@ </div>
197197
@ <div id='chat-user-list'></div>
198198
@ </div>
199
+ @ <button id='chat-clear-filter' class='hidden'>Clear filter</button>
199200
@ <div id='chat-preview' class='hidden chat-view'>
200201
@ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
201202
@ <div id='chat-preview-content' class='message-widget-content'></div>
202203
@ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
203204
@ </div>
204205
--- src/chat.c
+++ src/chat.c
@@ -194,10 +194,11 @@
194 @ </span>
195 @ <span>Active users (sorted by last message time)</span>
196 @ </div>
197 @ <div id='chat-user-list'></div>
198 @ </div>
 
199 @ <div id='chat-preview' class='hidden chat-view'>
200 @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
201 @ <div id='chat-preview-content' class='message-widget-content'></div>
202 @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
203 @ </div>
204
--- src/chat.c
+++ src/chat.c
@@ -194,10 +194,11 @@
194 @ </span>
195 @ <span>Active users (sorted by last message time)</span>
196 @ </div>
197 @ <div id='chat-user-list'></div>
198 @ </div>
199 @ <button id='chat-clear-filter' class='hidden'>Clear filter</button>
200 @ <div id='chat-preview' class='hidden chat-view'>
201 @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
202 @ <div id='chat-preview-content' class='message-widget-content'></div>
203 @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
204 @ </div>
205
+157 -45
--- src/chat.js
+++ src/chat.js
@@ -138,11 +138,12 @@
138138
viewPreview: E1('#chat-preview'),
139139
previewContent: E1('#chat-preview-content'),
140140
btnPreview: E1('#chat-preview-button'),
141141
views: document.querySelectorAll('.chat-view'),
142142
activeUserListWrapper: E1('#chat-user-list-wrapper'),
143
- activeUserList: E1('#chat-user-list')
143
+ activeUserList: E1('#chat-user-list'),
144
+ btnClearFilter: E1('#chat-clear-filter')
144145
},
145146
me: F.user.name,
146147
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
147148
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
148149
pageIsActive: 'visible'===document.visibilityState,
@@ -157,15 +158,31 @@
157158
(JS Date object). Only messages received by the chat client
158159
are considered. */
159160
/* Reminder: to convert a Julian time J to JS:
160161
new Date((J - 2440587.5) * 86400000) */
161162
},
162
- filterState:{
163
- activeUser: undefined,
164
- match: function(uname){
165
- return this.activeUser===uname || !this.activeUser;
166
- }
163
+ filter: {
164
+ user:{
165
+ activeTag: undefined,
166
+ match: function(uname){
167
+ return !this.activeTag || this.activeTag===uname;
168
+ },
169
+ matchElem: function(e){
170
+ return !this.activeTag || this.activeTag===e.dataset.xfrom;
171
+ }
172
+ },
173
+ hashtag:{
174
+ activeTag: undefined,
175
+ match: function(tag){
176
+ return !this.activeTag || tag===this.activeTag;
177
+ },
178
+ matchElem: function(e){
179
+ return !this.activeTag
180
+ || !!e.querySelector('[data-hashtag='+this.activeTag+']');
181
+ }
182
+ },
183
+ current: undefined/*gets set to current active filter*/
167184
},
168185
/** Gets (no args) or sets (1 arg) the current input text field value,
169186
taking into account single- vs multi-line input. The getter returns
170187
a string and the setter returns this object. */
171188
inputValue: function(){
@@ -280,11 +297,12 @@
280297
the list. */
281298
injectMessageElem: function f(e, atEnd){
282299
const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
283300
holder = this.e.viewMessages,
284301
prevMessage = this.e.newestMessage;
285
- if(!this.filterState.match(e.dataset.xfrom)){
302
+ if(this.filter.current
303
+ && !this.filter.current.matchElem(e)){
286304
e.classList.add('hidden');
287305
}
288306
if(atEnd){
289307
const fe = mip.nextElementSibling;
290308
if(fe) mip.parentNode.insertBefore(e, fe);
@@ -478,11 +496,11 @@
478496
else return 0;
479497
};
480498
callee.addUserElem = function(u){
481499
const uSpan = D.addClass(D.span(), 'chat-user');
482500
const uDate = self.usersLastSeen[u];
483
- if(self.filterState.activeUser===u){
501
+ if(self.filter.user.activeTag===u){
484502
uSpan.classList.add('selected');
485503
}
486504
uSpan.dataset.uname = u;
487505
D.append(uSpan, u, "\n",
488506
D.append(
@@ -500,37 +518,111 @@
500518
Object.keys(this.usersLastSeen).sort(
501519
callee.sortUsersSeen
502520
).forEach(callee.addUserElem);
503521
return this;
504522
},
523
+ /**
524
+ For each Chat.MessageWidget element (X.message-widget) for
525
+ which predicate(elem) returns true, the 'hidden' class is
526
+ removed from that message. For all others, 'hidden' is
527
+ added. If predicate is falsy, 'hidden' is removed from all
528
+ elements. After filtering, it will try to scroll the last
529
+ not-filtered-out message into view, but exactly where it
530
+ scrolls into view (top, middle, button) is
531
+ unpredictable. Returns this object.
532
+
533
+ The argument may optionally be an object from this.filter,
534
+ in which case its matchElem() method becomes the predicate.
535
+
536
+ Note that this does not encapsulate certain filter-specific
537
+ logic which applies changes to elements other than the
538
+ main message list or this.e.btnClearFilter.
539
+ */
540
+ applyMessageFilter: function(predicate){
541
+ const self = this;
542
+ let eLast;
543
+ console.debug("applyMessageFilter(",predicate,")");
544
+ if(!predicate){
545
+ D.removeClass(this.e.viewMessages.querySelectorAll('.message-widget.hidden'),
546
+ 'hidden');
547
+ D.addClass(this.e.btnClearFilter, 'hidden');
548
+ }else if('function'!==typeof predicate
549
+ && predicate.matchElem){
550
+ /* assume Chat.filter object */
551
+ const p = predicate;
552
+ predicate = (e)=>p.matchElem(e);
553
+ }
554
+ if(predicate){
555
+ this.e.viewMessages.querySelectorAll('.message-widget').forEach(function(e){
556
+ if(predicate(e)){
557
+ e.classList.remove('hidden');
558
+ eLast = e;
559
+ }else{
560
+ e.classList.add('hidden');
561
+ }
562
+ });
563
+ D.removeClass(this.e.btnClearFilter, 'hidden');
564
+ }
565
+ if(eLast) eLast.scrollIntoView(false);
566
+ else this.scrollMessagesTo(1);
567
+ return this;
568
+ },
569
+ /**
570
+ Clears the current message filter, if any, and clears the
571
+ activeTag property of all members of this.filter. Returns
572
+ this object. This also unfortunately performs some
573
+ filter-type-specific logic which we have not yet managed to
574
+ encapsulate more cleanly.
575
+ */
576
+ clearFilters: function(){
577
+ if(!this.filter.current) return this;
578
+ this.filter.current = undefined;
579
+ this.applyMessageFilter(false);
580
+ const self = this;
581
+ Object.keys(this.filter).forEach(function(k){
582
+ const f = self.filter[k];
583
+ if(f) f.activeTag = undefined;
584
+ });
585
+ this.e.activeUserList.querySelectorAll('.chat-user').forEach(
586
+ /*Unfortante filter-specific logic*/
587
+ (e)=>e.classList.remove('selected')
588
+ );
589
+ return this;
590
+ },
505591
/**
506592
Applies user name filter to all current messages, or clears
507593
the filter if uname is falsy.
508594
*/
509595
setUserFilter: function(uname){
510
- this.filterState.activeUser = uname;
511
- const mw = this.e.viewMessages.querySelectorAll('.message-widget');
596
+ if(!uname || (this.filter.current
597
+ && this.filter.current!==this.filter.user)){
598
+ this.clearFilters();
599
+ }
600
+ this.filter.user.activeTag = uname;
601
+ if(uname) this.applyMessageFilter(this.filter.user);
602
+ this.filter.current = uname ? this.filter.user : undefined;
512603
const self = this;
513
- let eLast;
514
- if(!uname){
515
- D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'),
516
- 'hidden');
517
- }else{
518
- mw.forEach(function(w){
519
- if(self.filterState.match(w.dataset.xfrom)){
520
- w.classList.remove('hidden');
521
- eLast = w;
522
- }else{
523
- w.classList.add('hidden');
524
- }
525
- });
526
- }
527
- if(eLast) eLast.scrollIntoView(false);
528
- else this.scrollMessagesTo(1);
529
- cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
530
- e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected');
531
- });
604
+ this.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
605
+ e.classList[
606
+ self.filter.user.activeTag===e.dataset.uname
607
+ ? 'add' : 'remove'
608
+ ]('selected');
609
+ });
610
+ return this;
611
+ },
612
+ /**
613
+ Applies a hashtag filter to all current messages, or clears
614
+ the filter if tag is falsy.
615
+ */
616
+ setHashtagFilter: function(tag){
617
+ if(!tag || (this.filter.current
618
+ && this.filter.current!==this.filter.hashtag)){
619
+ this.clearFilters();
620
+ }
621
+ this.filter.hashtag.activeTag = tag;
622
+ if(tag) this.applyMessageFilter(this.filter.hashtag);
623
+ this.filter.current = tag ? this.filter.hashtag : undefined;
532624
return this;
533625
},
534626
535627
/**
536628
If animations are enabled, passes its arguments
@@ -782,21 +874,45 @@
782874
cs.setCurrentView(cs.e.viewMessages);
783875
if(eUser.classList.contains('selected')){
784876
/* If curently selected, toggle filter off */
785877
eUser.classList.remove('selected');
786878
cs.setUserFilter(false);
787
- delete f.$eSelected;
788879
}else{
789
- if(f.$eSelected) f.$eSelected.classList.remove('selected');
790
- f.$eSelected = eUser;
791880
eUser.classList.add('selected');
792881
cs.setUserFilter(uname);
793882
}
794883
return false;
795884
}, false);
885
+
886
+ cs.e.btnClearFilter.addEventListener('click',function(){
887
+ D.addClass(this,'hidden');
888
+ cs.clearFilters();
889
+ }, false);
796890
return cs;
797891
})()/*Chat initialization*/;
892
+
893
+ /** To be passed each MessageWidget's top-level DOM element
894
+ after initial processing of the message, to set up
895
+ hashtag references. */
896
+ const setupHashtags = function f(elem){
897
+ if(!f.$click){
898
+ f.$click = function(ev){
899
+ const tag = ev.target.dataset.hashtag;
900
+ if(tag){
901
+ console.debug("hashtag = ",tag);
902
+ Chat.setHashtagFilter(
903
+ tag===Chat.filter.hashtag.activeTag
904
+ ? false : tag
905
+ );
906
+ }
907
+ };
908
+ }
909
+ elem.querySelectorAll('[data-hashtag]').forEach(function(e){
910
+ e.dataset.hashtag = e.dataset.hashtag.toLowerCase();
911
+ e.addEventListener('click', f.$click, false);
912
+ })
913
+ };
798914
799915
/**
800916
Custom widget type for rendering messages (one message per
801917
instance). These are modelled after FIELDSET elements but we
802918
don't use FIELDSET because of cross-browser inconsistencies in
@@ -915,10 +1031,11 @@
9151031
// Used by Chat.reportErrorAsMessage()
9161032
D.append(contentTarget, m.xmsg);
9171033
}else{
9181034
contentTarget.innerHTML = m.xmsg;
9191035
contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
1036
+ setupHashtags(contentTarget);
9201037
if(F.pikchr){
9211038
F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
9221039
}
9231040
}
9241041
}
@@ -999,31 +1116,24 @@
9991116
y: 'a'
10001117
}), "User's Timeline"),
10011118
'target', '_blank'
10021119
);
10031120
D.append(toolbar2, timelineLink);
1004
- if(Chat.filterState.activeUser &&
1005
- Chat.filterState.match(eMsg.dataset.xfrom)){
1006
- /* Add a button to clear user filter and jump to
1121
+ if(Chat.filter.current){
1122
+ /* Add a button to clear filter and jump to
10071123
this message in its original context. */
10081124
D.append(
10091125
this.e,
10101126
D.append(
10111127
D.addClass(D.div(), 'toolbar'),
10121128
D.button(
10131129
"Message in context",
10141130
function(){
10151131
self.hide();
1016
- Chat.setUserFilter(false);
1132
+ Chat.clearFilters();
10171133
eMsg.scrollIntoView(false);
1018
- Chat.animate(
1019
- eMsg.firstElementChild, 'anim-flip-h'
1020
- //eMsg.firstElementChild, 'anim-flip-v'
1021
- //eMsg.childNodes, 'anim-rotate-360'
1022
- //eMsg.childNodes, 'anim-flip-v'
1023
- //eMsg, 'anim-flip-v'
1024
- );
1134
+ Chat.animate(eMsg.firstElementChild, 'anim-flip-h');
10251135
})
10261136
)
10271137
);
10281138
}/*jump-to button*/
10291139
}
@@ -1241,12 +1351,14 @@
12411351
persistentSetting: 'active-user-list',
12421352
callback: function(){
12431353
D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
12441354
D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
12451355
if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1246
- /* When hiding this element, undo all filtering */
1247
- Chat.setUserFilter(false);
1356
+ /* When hiding this element, undo user filtering */
1357
+ if(Chat.filter.current === Chat.filter.user){
1358
+ Chat.setUserFilter(false);
1359
+ }
12481360
/*Ideally we'd scroll the final message into view
12491361
now, but because viewMessages is currently hidden behind
12501362
viewConfig, scrolling is a no-op. */
12511363
Chat.scrollMessagesTo(1);
12521364
}else{
12531365
--- src/chat.js
+++ src/chat.js
@@ -138,11 +138,12 @@
138 viewPreview: E1('#chat-preview'),
139 previewContent: E1('#chat-preview-content'),
140 btnPreview: E1('#chat-preview-button'),
141 views: document.querySelectorAll('.chat-view'),
142 activeUserListWrapper: E1('#chat-user-list-wrapper'),
143 activeUserList: E1('#chat-user-list')
 
144 },
145 me: F.user.name,
146 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
147 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
148 pageIsActive: 'visible'===document.visibilityState,
@@ -157,15 +158,31 @@
157 (JS Date object). Only messages received by the chat client
158 are considered. */
159 /* Reminder: to convert a Julian time J to JS:
160 new Date((J - 2440587.5) * 86400000) */
161 },
162 filterState:{
163 activeUser: undefined,
164 match: function(uname){
165 return this.activeUser===uname || !this.activeUser;
166 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167 },
168 /** Gets (no args) or sets (1 arg) the current input text field value,
169 taking into account single- vs multi-line input. The getter returns
170 a string and the setter returns this object. */
171 inputValue: function(){
@@ -280,11 +297,12 @@
280 the list. */
281 injectMessageElem: function f(e, atEnd){
282 const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
283 holder = this.e.viewMessages,
284 prevMessage = this.e.newestMessage;
285 if(!this.filterState.match(e.dataset.xfrom)){
 
286 e.classList.add('hidden');
287 }
288 if(atEnd){
289 const fe = mip.nextElementSibling;
290 if(fe) mip.parentNode.insertBefore(e, fe);
@@ -478,11 +496,11 @@
478 else return 0;
479 };
480 callee.addUserElem = function(u){
481 const uSpan = D.addClass(D.span(), 'chat-user');
482 const uDate = self.usersLastSeen[u];
483 if(self.filterState.activeUser===u){
484 uSpan.classList.add('selected');
485 }
486 uSpan.dataset.uname = u;
487 D.append(uSpan, u, "\n",
488 D.append(
@@ -500,37 +518,111 @@
500 Object.keys(this.usersLastSeen).sort(
501 callee.sortUsersSeen
502 ).forEach(callee.addUserElem);
503 return this;
504 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505 /**
506 Applies user name filter to all current messages, or clears
507 the filter if uname is falsy.
508 */
509 setUserFilter: function(uname){
510 this.filterState.activeUser = uname;
511 const mw = this.e.viewMessages.querySelectorAll('.message-widget');
 
 
 
 
 
512 const self = this;
513 let eLast;
514 if(!uname){
515 D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'),
516 'hidden');
517 }else{
518 mw.forEach(function(w){
519 if(self.filterState.match(w.dataset.xfrom)){
520 w.classList.remove('hidden');
521 eLast = w;
522 }else{
523 w.classList.add('hidden');
524 }
525 });
526 }
527 if(eLast) eLast.scrollIntoView(false);
528 else this.scrollMessagesTo(1);
529 cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
530 e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected');
531 });
 
532 return this;
533 },
534
535 /**
536 If animations are enabled, passes its arguments
@@ -782,21 +874,45 @@
782 cs.setCurrentView(cs.e.viewMessages);
783 if(eUser.classList.contains('selected')){
784 /* If curently selected, toggle filter off */
785 eUser.classList.remove('selected');
786 cs.setUserFilter(false);
787 delete f.$eSelected;
788 }else{
789 if(f.$eSelected) f.$eSelected.classList.remove('selected');
790 f.$eSelected = eUser;
791 eUser.classList.add('selected');
792 cs.setUserFilter(uname);
793 }
794 return false;
795 }, false);
 
 
 
 
 
796 return cs;
797 })()/*Chat initialization*/;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
799 /**
800 Custom widget type for rendering messages (one message per
801 instance). These are modelled after FIELDSET elements but we
802 don't use FIELDSET because of cross-browser inconsistencies in
@@ -915,10 +1031,11 @@
915 // Used by Chat.reportErrorAsMessage()
916 D.append(contentTarget, m.xmsg);
917 }else{
918 contentTarget.innerHTML = m.xmsg;
919 contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
 
920 if(F.pikchr){
921 F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
922 }
923 }
924 }
@@ -999,31 +1116,24 @@
999 y: 'a'
1000 }), "User's Timeline"),
1001 'target', '_blank'
1002 );
1003 D.append(toolbar2, timelineLink);
1004 if(Chat.filterState.activeUser &&
1005 Chat.filterState.match(eMsg.dataset.xfrom)){
1006 /* Add a button to clear user filter and jump to
1007 this message in its original context. */
1008 D.append(
1009 this.e,
1010 D.append(
1011 D.addClass(D.div(), 'toolbar'),
1012 D.button(
1013 "Message in context",
1014 function(){
1015 self.hide();
1016 Chat.setUserFilter(false);
1017 eMsg.scrollIntoView(false);
1018 Chat.animate(
1019 eMsg.firstElementChild, 'anim-flip-h'
1020 //eMsg.firstElementChild, 'anim-flip-v'
1021 //eMsg.childNodes, 'anim-rotate-360'
1022 //eMsg.childNodes, 'anim-flip-v'
1023 //eMsg, 'anim-flip-v'
1024 );
1025 })
1026 )
1027 );
1028 }/*jump-to button*/
1029 }
@@ -1241,12 +1351,14 @@
1241 persistentSetting: 'active-user-list',
1242 callback: function(){
1243 D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1244 D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1245 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1246 /* When hiding this element, undo all filtering */
1247 Chat.setUserFilter(false);
 
 
1248 /*Ideally we'd scroll the final message into view
1249 now, but because viewMessages is currently hidden behind
1250 viewConfig, scrolling is a no-op. */
1251 Chat.scrollMessagesTo(1);
1252 }else{
1253
--- src/chat.js
+++ src/chat.js
@@ -138,11 +138,12 @@
138 viewPreview: E1('#chat-preview'),
139 previewContent: E1('#chat-preview-content'),
140 btnPreview: E1('#chat-preview-button'),
141 views: document.querySelectorAll('.chat-view'),
142 activeUserListWrapper: E1('#chat-user-list-wrapper'),
143 activeUserList: E1('#chat-user-list'),
144 btnClearFilter: E1('#chat-clear-filter')
145 },
146 me: F.user.name,
147 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
148 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
149 pageIsActive: 'visible'===document.visibilityState,
@@ -157,15 +158,31 @@
158 (JS Date object). Only messages received by the chat client
159 are considered. */
160 /* Reminder: to convert a Julian time J to JS:
161 new Date((J - 2440587.5) * 86400000) */
162 },
163 filter: {
164 user:{
165 activeTag: undefined,
166 match: function(uname){
167 return !this.activeTag || this.activeTag===uname;
168 },
169 matchElem: function(e){
170 return !this.activeTag || this.activeTag===e.dataset.xfrom;
171 }
172 },
173 hashtag:{
174 activeTag: undefined,
175 match: function(tag){
176 return !this.activeTag || tag===this.activeTag;
177 },
178 matchElem: function(e){
179 return !this.activeTag
180 || !!e.querySelector('[data-hashtag='+this.activeTag+']');
181 }
182 },
183 current: undefined/*gets set to current active filter*/
184 },
185 /** Gets (no args) or sets (1 arg) the current input text field value,
186 taking into account single- vs multi-line input. The getter returns
187 a string and the setter returns this object. */
188 inputValue: function(){
@@ -280,11 +297,12 @@
297 the list. */
298 injectMessageElem: function f(e, atEnd){
299 const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
300 holder = this.e.viewMessages,
301 prevMessage = this.e.newestMessage;
302 if(this.filter.current
303 && !this.filter.current.matchElem(e)){
304 e.classList.add('hidden');
305 }
306 if(atEnd){
307 const fe = mip.nextElementSibling;
308 if(fe) mip.parentNode.insertBefore(e, fe);
@@ -478,11 +496,11 @@
496 else return 0;
497 };
498 callee.addUserElem = function(u){
499 const uSpan = D.addClass(D.span(), 'chat-user');
500 const uDate = self.usersLastSeen[u];
501 if(self.filter.user.activeTag===u){
502 uSpan.classList.add('selected');
503 }
504 uSpan.dataset.uname = u;
505 D.append(uSpan, u, "\n",
506 D.append(
@@ -500,37 +518,111 @@
518 Object.keys(this.usersLastSeen).sort(
519 callee.sortUsersSeen
520 ).forEach(callee.addUserElem);
521 return this;
522 },
523 /**
524 For each Chat.MessageWidget element (X.message-widget) for
525 which predicate(elem) returns true, the 'hidden' class is
526 removed from that message. For all others, 'hidden' is
527 added. If predicate is falsy, 'hidden' is removed from all
528 elements. After filtering, it will try to scroll the last
529 not-filtered-out message into view, but exactly where it
530 scrolls into view (top, middle, button) is
531 unpredictable. Returns this object.
532
533 The argument may optionally be an object from this.filter,
534 in which case its matchElem() method becomes the predicate.
535
536 Note that this does not encapsulate certain filter-specific
537 logic which applies changes to elements other than the
538 main message list or this.e.btnClearFilter.
539 */
540 applyMessageFilter: function(predicate){
541 const self = this;
542 let eLast;
543 console.debug("applyMessageFilter(",predicate,")");
544 if(!predicate){
545 D.removeClass(this.e.viewMessages.querySelectorAll('.message-widget.hidden'),
546 'hidden');
547 D.addClass(this.e.btnClearFilter, 'hidden');
548 }else if('function'!==typeof predicate
549 && predicate.matchElem){
550 /* assume Chat.filter object */
551 const p = predicate;
552 predicate = (e)=>p.matchElem(e);
553 }
554 if(predicate){
555 this.e.viewMessages.querySelectorAll('.message-widget').forEach(function(e){
556 if(predicate(e)){
557 e.classList.remove('hidden');
558 eLast = e;
559 }else{
560 e.classList.add('hidden');
561 }
562 });
563 D.removeClass(this.e.btnClearFilter, 'hidden');
564 }
565 if(eLast) eLast.scrollIntoView(false);
566 else this.scrollMessagesTo(1);
567 return this;
568 },
569 /**
570 Clears the current message filter, if any, and clears the
571 activeTag property of all members of this.filter. Returns
572 this object. This also unfortunately performs some
573 filter-type-specific logic which we have not yet managed to
574 encapsulate more cleanly.
575 */
576 clearFilters: function(){
577 if(!this.filter.current) return this;
578 this.filter.current = undefined;
579 this.applyMessageFilter(false);
580 const self = this;
581 Object.keys(this.filter).forEach(function(k){
582 const f = self.filter[k];
583 if(f) f.activeTag = undefined;
584 });
585 this.e.activeUserList.querySelectorAll('.chat-user').forEach(
586 /*Unfortante filter-specific logic*/
587 (e)=>e.classList.remove('selected')
588 );
589 return this;
590 },
591 /**
592 Applies user name filter to all current messages, or clears
593 the filter if uname is falsy.
594 */
595 setUserFilter: function(uname){
596 if(!uname || (this.filter.current
597 && this.filter.current!==this.filter.user)){
598 this.clearFilters();
599 }
600 this.filter.user.activeTag = uname;
601 if(uname) this.applyMessageFilter(this.filter.user);
602 this.filter.current = uname ? this.filter.user : undefined;
603 const self = this;
604 this.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
605 e.classList[
606 self.filter.user.activeTag===e.dataset.uname
607 ? 'add' : 'remove'
608 ]('selected');
609 });
610 return this;
611 },
612 /**
613 Applies a hashtag filter to all current messages, or clears
614 the filter if tag is falsy.
615 */
616 setHashtagFilter: function(tag){
617 if(!tag || (this.filter.current
618 && this.filter.current!==this.filter.hashtag)){
619 this.clearFilters();
620 }
621 this.filter.hashtag.activeTag = tag;
622 if(tag) this.applyMessageFilter(this.filter.hashtag);
623 this.filter.current = tag ? this.filter.hashtag : undefined;
624 return this;
625 },
626
627 /**
628 If animations are enabled, passes its arguments
@@ -782,21 +874,45 @@
874 cs.setCurrentView(cs.e.viewMessages);
875 if(eUser.classList.contains('selected')){
876 /* If curently selected, toggle filter off */
877 eUser.classList.remove('selected');
878 cs.setUserFilter(false);
 
879 }else{
 
 
880 eUser.classList.add('selected');
881 cs.setUserFilter(uname);
882 }
883 return false;
884 }, false);
885
886 cs.e.btnClearFilter.addEventListener('click',function(){
887 D.addClass(this,'hidden');
888 cs.clearFilters();
889 }, false);
890 return cs;
891 })()/*Chat initialization*/;
892
893 /** To be passed each MessageWidget's top-level DOM element
894 after initial processing of the message, to set up
895 hashtag references. */
896 const setupHashtags = function f(elem){
897 if(!f.$click){
898 f.$click = function(ev){
899 const tag = ev.target.dataset.hashtag;
900 if(tag){
901 console.debug("hashtag = ",tag);
902 Chat.setHashtagFilter(
903 tag===Chat.filter.hashtag.activeTag
904 ? false : tag
905 );
906 }
907 };
908 }
909 elem.querySelectorAll('[data-hashtag]').forEach(function(e){
910 e.dataset.hashtag = e.dataset.hashtag.toLowerCase();
911 e.addEventListener('click', f.$click, false);
912 })
913 };
914
915 /**
916 Custom widget type for rendering messages (one message per
917 instance). These are modelled after FIELDSET elements but we
918 don't use FIELDSET because of cross-browser inconsistencies in
@@ -915,10 +1031,11 @@
1031 // Used by Chat.reportErrorAsMessage()
1032 D.append(contentTarget, m.xmsg);
1033 }else{
1034 contentTarget.innerHTML = m.xmsg;
1035 contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
1036 setupHashtags(contentTarget);
1037 if(F.pikchr){
1038 F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
1039 }
1040 }
1041 }
@@ -999,31 +1116,24 @@
1116 y: 'a'
1117 }), "User's Timeline"),
1118 'target', '_blank'
1119 );
1120 D.append(toolbar2, timelineLink);
1121 if(Chat.filter.current){
1122 /* Add a button to clear filter and jump to
 
1123 this message in its original context. */
1124 D.append(
1125 this.e,
1126 D.append(
1127 D.addClass(D.div(), 'toolbar'),
1128 D.button(
1129 "Message in context",
1130 function(){
1131 self.hide();
1132 Chat.clearFilters();
1133 eMsg.scrollIntoView(false);
1134 Chat.animate(eMsg.firstElementChild, 'anim-flip-h');
 
 
 
 
 
 
1135 })
1136 )
1137 );
1138 }/*jump-to button*/
1139 }
@@ -1241,12 +1351,14 @@
1351 persistentSetting: 'active-user-list',
1352 callback: function(){
1353 D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1354 D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1355 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1356 /* When hiding this element, undo user filtering */
1357 if(Chat.filter.current === Chat.filter.user){
1358 Chat.setUserFilter(false);
1359 }
1360 /*Ideally we'd scroll the final message into view
1361 now, but because viewMessages is currently hidden behind
1362 viewConfig, scrolling is a no-op. */
1363 Chat.scrollMessagesTo(1);
1364 }else{
1365
--- src/style.chat.css
+++ src/style.chat.css
@@ -394,10 +394,20 @@
394394
}
395395
body.chat #chat-user-list .chat-user.selected {
396396
font-weight: bold;
397397
text-decoration: underline;
398398
}
399
+
400
+body.chat span[data-hashtag] {
401
+ font-family: monospace;
402
+ text-decoration: underline;
403
+ cursor: pointer;
404
+}
405
+
406
+body.chat #chat-clear-filter {
407
+ margin: 0.25em 0.5em;
408
+}
399409
400410
body.chat .anim-rotate-360 {
401411
animation: rotate-360 750ms linear;
402412
}
403413
@keyframes rotate-360 {
@@ -420,18 +430,18 @@
420430
}
421431
body.chat .anim-fade-in {
422432
animation: fade-in 750ms linear;
423433
}
424434
body.chat .anim-fade-in-fast {
425
- animation: fade-in 350ms linear;
435
+ animation: fade-in 300ms linear;
426436
}
427437
@keyframes fade-in {
428438
from { opacity: 0; }
429439
to { opacity: 1; }
430440
}
431441
body.chat .anim-fade-out-fast {
432
- animation: fade-out 250ms linear;
442
+ animation: fade-out 300ms linear;
433443
}
434444
@keyframes fade-out {
435445
from { opacity: 1; }
436446
to { opacity: 0; }
437447
}
438448
--- src/style.chat.css
+++ src/style.chat.css
@@ -394,10 +394,20 @@
394 }
395 body.chat #chat-user-list .chat-user.selected {
396 font-weight: bold;
397 text-decoration: underline;
398 }
 
 
 
 
 
 
 
 
 
 
399
400 body.chat .anim-rotate-360 {
401 animation: rotate-360 750ms linear;
402 }
403 @keyframes rotate-360 {
@@ -420,18 +430,18 @@
420 }
421 body.chat .anim-fade-in {
422 animation: fade-in 750ms linear;
423 }
424 body.chat .anim-fade-in-fast {
425 animation: fade-in 350ms linear;
426 }
427 @keyframes fade-in {
428 from { opacity: 0; }
429 to { opacity: 1; }
430 }
431 body.chat .anim-fade-out-fast {
432 animation: fade-out 250ms linear;
433 }
434 @keyframes fade-out {
435 from { opacity: 1; }
436 to { opacity: 0; }
437 }
438
--- src/style.chat.css
+++ src/style.chat.css
@@ -394,10 +394,20 @@
394 }
395 body.chat #chat-user-list .chat-user.selected {
396 font-weight: bold;
397 text-decoration: underline;
398 }
399
400 body.chat span[data-hashtag] {
401 font-family: monospace;
402 text-decoration: underline;
403 cursor: pointer;
404 }
405
406 body.chat #chat-clear-filter {
407 margin: 0.25em 0.5em;
408 }
409
410 body.chat .anim-rotate-360 {
411 animation: rotate-360 750ms linear;
412 }
413 @keyframes rotate-360 {
@@ -420,18 +430,18 @@
430 }
431 body.chat .anim-fade-in {
432 animation: fade-in 750ms linear;
433 }
434 body.chat .anim-fade-in-fast {
435 animation: fade-in 300ms linear;
436 }
437 @keyframes fade-in {
438 from { opacity: 0; }
439 to { opacity: 1; }
440 }
441 body.chat .anim-fade-out-fast {
442 animation: fade-out 300ms linear;
443 }
444 @keyframes fade-out {
445 from { opacity: 1; }
446 to { opacity: 0; }
447 }
448

Keyboard Shortcuts

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