Fossil SCM

Proof of concept /chat "active user list" which keeps track only of users who have posted messages in the client's current list and allows filtering on those messages by tapping a user. Widget is hidden by default and can be toggled in the config area. There are still cases to figure out (e.g. new messages do not apply the current filter).

stephan 2021-09-23 09:41 trunk
Commit dafd5497118ed02401e3ce7d871f5fb5a976efb061e1c93a6844a2768aae9486
+1
--- src/chat.c
+++ src/chat.c
@@ -181,10 +181,11 @@
181181
@ <input type="file" name="file" id="chat-input-file">
182182
@ </div>
183183
@ <div id="chat-drop-details"></div>
184184
@ </div>
185185
@ </div>
186
+ @ <div id='chat-user-list' class='hidden'></div>
186187
@ <div id='chat-preview' class='hidden chat-view'>
187188
@ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
188189
@ <div id='chat-preview-content' class='message-widget-content'></div>
189190
@ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
190191
@ </div>
191192
--- src/chat.c
+++ src/chat.c
@@ -181,10 +181,11 @@
181 @ <input type="file" name="file" id="chat-input-file">
182 @ </div>
183 @ <div id="chat-drop-details"></div>
184 @ </div>
185 @ </div>
 
186 @ <div id='chat-preview' class='hidden chat-view'>
187 @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
188 @ <div id='chat-preview-content' class='message-widget-content'></div>
189 @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
190 @ </div>
191
--- src/chat.c
+++ src/chat.c
@@ -181,10 +181,11 @@
181 @ <input type="file" name="file" id="chat-input-file">
182 @ </div>
183 @ <div id="chat-drop-details"></div>
184 @ </div>
185 @ </div>
186 @ <div id='chat-user-list' class='hidden'></div>
187 @ <div id='chat-preview' class='hidden chat-view'>
188 @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
189 @ <div id='chat-preview-content' class='message-widget-content'></div>
190 @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
191 @ </div>
192
+107 -2
--- src/chat.js
+++ src/chat.js
@@ -116,11 +116,12 @@
116116
contentDiv: E1('div.content'),
117117
viewConfig: E1('#chat-config'),
118118
viewPreview: E1('#chat-preview'),
119119
previewContent: E1('#chat-preview-content'),
120120
btnPreview: E1('#chat-preview-button'),
121
- views: document.querySelectorAll('.chat-view')
121
+ views: document.querySelectorAll('.chat-view'),
122
+ activeUserList: D.append(E1('#chat-user-list'), "user list placeholder")
122123
},
123124
me: F.user.name,
124125
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
125126
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
126127
pageIsActive: 'visible'===document.visibilityState,
@@ -128,10 +129,17 @@
128129
notificationBubbleColor: 'white',
129130
totalMessageCount: 0, // total # of inbound messages
130131
//! Number of messages to load for the history buttons
131132
loadMessageCount: Math.abs(F.config.chat.initSize || 20),
132133
ajaxInflight: 0,
134
+ usersLastSeen:{
135
+ /* Map of user names to their most recent message time
136
+ (JS Date object). Only messages received by the chat client
137
+ are considered. */
138
+ /* Reminder: to convert a Julian time J to JS:
139
+ new Date((J - 2440587.5) * 86400000) */
140
+ },
133141
/** Gets (no args) or sets (1 arg) the current input text field value,
134142
taking into account single- vs multi-line input. The getter returns
135143
a string and the setter returns this object. */
136144
inputValue: function(){
137145
const e = this.inputElement();
@@ -367,11 +375,12 @@
367375
defaults:{
368376
"images-inline": !!F.config.chat.imagesInline,
369377
"edit-multiline": false,
370378
"monospace-messages": false,
371379
"chat-only-mode": false,
372
- "audible-alert": true
380
+ "audible-alert": true,
381
+ "active-user-list": false
373382
}
374383
},
375384
/** Plays a new-message notification sound IF the audible-alert
376385
setting is true, else this is a no-op. Returns this.
377386
*/
@@ -407,10 +416,42 @@
407416
setCurrentView: function(e){
408417
this.e.views.forEach(function(E){
409418
if(e!==E) D.addClass(E,'hidden');
410419
});
411420
return this.e.currentView = D.removeClass(e,'hidden');
421
+ },
422
+ /**
423
+ Updates the "active user list" view.
424
+ */
425
+ updateActiveUserList: function callee(){
426
+ if(!callee.sortUsersSeen){
427
+ /** Array.sort() callback. Expects an array of user names and
428
+ sorts them in last-received message order (newest first). */
429
+ const usersLastSeen = this.usersLastSeen;
430
+ callee.sortUsersSeen = function(l,r){
431
+ l = usersLastSeen[l];
432
+ r = usersLastSeen[r];
433
+ if(l && r) return r - l;
434
+ else if(l) return -1;
435
+ else if(r) return 1;
436
+ else return 0;
437
+ };
438
+ }
439
+ const self = this,
440
+ users = Object.keys(this.usersLastSeen).sort(callee.sortUsersSeen);
441
+ if(!users.length) return this;
442
+ const ael = this.e.activeUserList;
443
+ D.clearElement(ael);
444
+ users.forEach(function(u){
445
+ const uSpan = D.addClass(D.span(), 'chat-user');
446
+ const uDate = self.usersLastSeen[u];
447
+ D.append(uSpan, u);
448
+ if(uDate.$uColor){
449
+ uSpan.style.backgroundColor = uDate.$uColor;
450
+ }
451
+ D.append(ael, uSpan);
452
+ });
412453
}
413454
};
414455
F.fetch.beforesend = ()=>cs.ajaxStart();
415456
F.fetch.aftersend = ()=>cs.ajaxEnd();
416457
cs.e.inputCurrent = cs.e.inputSingle;
@@ -427,10 +468,13 @@
427468
tall vs wide. Can be toggled via settings popup. */
428469
document.body.classList.add('my-messages-right');
429470
}
430471
if(cs.settings.getBool('monospace-messages',false)){
431472
document.body.classList.add('monospace-messages');
473
+ }
474
+ if(cs.settings.getBool('active-user-list',false)){
475
+ cs.e.activeUserList.classList.remove('hidden');
432476
}
433477
cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
434478
cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
435479
cs.pageTitleOrig = cs.e.pageTitle.innerText;
436480
const qs = (e)=>document.querySelector(e);
@@ -624,10 +668,46 @@
624668
if(cs.pageIsActive){
625669
cs.e.pageTitle.innerText = cs.pageTitleOrig;
626670
}
627671
}, true);
628672
cs.setCurrentView(cs.e.viewMessages);
673
+
674
+ cs.e.activeUserList.addEventListener('click', function f(ev){
675
+ /* Filter messages on a user clicked in activeUserList */
676
+ ev.stopPropagation();
677
+ ev.preventDefault();
678
+ if(!ev.target.classList.contains('chat-user')) return false;
679
+ const eUser = ev.target;
680
+ const uname = eUser.innerText;
681
+ let eLast;
682
+ cs.setCurrentView(cs.e.viewMessages);
683
+ if(eUser.classList.contains('selected')){
684
+ eUser.classList.remove('selected');
685
+ cs.e.viewMessages.querySelectorAll(
686
+ '.message-widget.hidden'
687
+ ).forEach(function(e){
688
+ e.classList.remove('hidden');
689
+ eLast = e;
690
+ });
691
+ delete f.$eSelected;
692
+ }else{
693
+ if(f.$eSelected) f.$eSelected.classList.remove('selected');
694
+ f.$eSelected = eUser;
695
+ eUser.classList.add('selected');
696
+ cs.e.viewMessages.querySelectorAll(
697
+ '.message-widget'
698
+ ).forEach(function(e){
699
+ if(e.dataset.xfrom===uname){
700
+ e.classList.remove('hidden');
701
+ eLast = e;
702
+ }
703
+ else e.classList.add('hidden');
704
+ });
705
+ }
706
+ if(eLast) eLast.scrollIntoView(false);
707
+ return false;
708
+ }, false);
629709
return cs;
630710
})()/*Chat initialization*/;
631711
632712
/**
633713
Custom widget type for rendering messages (one message per
@@ -1060,10 +1140,25 @@
10601140
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
10611141
persistentSetting: 'edit-multiline',
10621142
callback: function(){
10631143
Chat.inputToggleSingleMulti();
10641144
}
1145
+ },{
1146
+ label: "Show recent user list",
1147
+ boolValue: ()=>!Chat.e.activeUserList.classList.contains('hidden'),
1148
+ persistentSetting: 'active-user-list',
1149
+ callback: function(){
1150
+ D.toggleClass(Chat.e.activeUserList,'hidden');
1151
+ if(Chat.e.activeUserList.classList.contains('hidden')){
1152
+ /* When hiding this element, undo all filtering */
1153
+ D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
1154
+ /*Ideally we'd scroll the final message into view
1155
+ now, but because viewMessages is currently hidden behind
1156
+ viewConfig, scrolling is a no-op. */
1157
+ Chat.scrollMessagesTo(1);
1158
+ }
1159
+ }
10651160
},{
10661161
label: "Monospace message font",
10671162
boolValue: ()=>document.body.classList.contains('monospace-messages'),
10681163
persistentSetting: 'monospace-messages',
10691164
callback: function(){
@@ -1228,10 +1323,18 @@
12281323
should only be true when loading older messages. */
12291324
f.processPost = function(m,atEnd){
12301325
++Chat.totalMessageCount;
12311326
if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
12321327
if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
1328
+ if(m.xfrom && m.mtime){
1329
+ const d = new Date(m.mtime);
1330
+ const uls = Chat.usersLastSeen[m.xfrom];
1331
+ if(!uls || uls<d){
1332
+ d.$uColor = m.uclr;
1333
+ Chat.usersLastSeen[m.xfrom] = d;
1334
+ }
1335
+ }
12331336
if( m.mdel ){
12341337
/* A record deletion notice. */
12351338
Chat.deleteMessageElem(m.mdel);
12361339
return;
12371340
}
@@ -1240,10 +1343,12 @@
12401343
}
12411344
const row = new Chat.MessageWidget(m);
12421345
Chat.injectMessageElem(row.e.body,atEnd);
12431346
if(m.isError){
12441347
Chat._gotServerError = m;
1348
+ }else{
1349
+ Chat.updateActiveUserList();
12451350
}
12461351
}/*processPost()*/;
12471352
}/*end static init*/
12481353
jx.msgs.forEach((m)=>f.processPost(m,atEnd));
12491354
if('visible'===document.visibilityState){
12501355
--- src/chat.js
+++ src/chat.js
@@ -116,11 +116,12 @@
116 contentDiv: E1('div.content'),
117 viewConfig: E1('#chat-config'),
118 viewPreview: E1('#chat-preview'),
119 previewContent: E1('#chat-preview-content'),
120 btnPreview: E1('#chat-preview-button'),
121 views: document.querySelectorAll('.chat-view')
 
122 },
123 me: F.user.name,
124 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
125 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
126 pageIsActive: 'visible'===document.visibilityState,
@@ -128,10 +129,17 @@
128 notificationBubbleColor: 'white',
129 totalMessageCount: 0, // total # of inbound messages
130 //! Number of messages to load for the history buttons
131 loadMessageCount: Math.abs(F.config.chat.initSize || 20),
132 ajaxInflight: 0,
 
 
 
 
 
 
 
133 /** Gets (no args) or sets (1 arg) the current input text field value,
134 taking into account single- vs multi-line input. The getter returns
135 a string and the setter returns this object. */
136 inputValue: function(){
137 const e = this.inputElement();
@@ -367,11 +375,12 @@
367 defaults:{
368 "images-inline": !!F.config.chat.imagesInline,
369 "edit-multiline": false,
370 "monospace-messages": false,
371 "chat-only-mode": false,
372 "audible-alert": true
 
373 }
374 },
375 /** Plays a new-message notification sound IF the audible-alert
376 setting is true, else this is a no-op. Returns this.
377 */
@@ -407,10 +416,42 @@
407 setCurrentView: function(e){
408 this.e.views.forEach(function(E){
409 if(e!==E) D.addClass(E,'hidden');
410 });
411 return this.e.currentView = D.removeClass(e,'hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412 }
413 };
414 F.fetch.beforesend = ()=>cs.ajaxStart();
415 F.fetch.aftersend = ()=>cs.ajaxEnd();
416 cs.e.inputCurrent = cs.e.inputSingle;
@@ -427,10 +468,13 @@
427 tall vs wide. Can be toggled via settings popup. */
428 document.body.classList.add('my-messages-right');
429 }
430 if(cs.settings.getBool('monospace-messages',false)){
431 document.body.classList.add('monospace-messages');
 
 
 
432 }
433 cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
434 cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
435 cs.pageTitleOrig = cs.e.pageTitle.innerText;
436 const qs = (e)=>document.querySelector(e);
@@ -624,10 +668,46 @@
624 if(cs.pageIsActive){
625 cs.e.pageTitle.innerText = cs.pageTitleOrig;
626 }
627 }, true);
628 cs.setCurrentView(cs.e.viewMessages);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629 return cs;
630 })()/*Chat initialization*/;
631
632 /**
633 Custom widget type for rendering messages (one message per
@@ -1060,10 +1140,25 @@
1060 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1061 persistentSetting: 'edit-multiline',
1062 callback: function(){
1063 Chat.inputToggleSingleMulti();
1064 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065 },{
1066 label: "Monospace message font",
1067 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1068 persistentSetting: 'monospace-messages',
1069 callback: function(){
@@ -1228,10 +1323,18 @@
1228 should only be true when loading older messages. */
1229 f.processPost = function(m,atEnd){
1230 ++Chat.totalMessageCount;
1231 if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
1232 if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
 
 
 
 
 
 
 
 
1233 if( m.mdel ){
1234 /* A record deletion notice. */
1235 Chat.deleteMessageElem(m.mdel);
1236 return;
1237 }
@@ -1240,10 +1343,12 @@
1240 }
1241 const row = new Chat.MessageWidget(m);
1242 Chat.injectMessageElem(row.e.body,atEnd);
1243 if(m.isError){
1244 Chat._gotServerError = m;
 
 
1245 }
1246 }/*processPost()*/;
1247 }/*end static init*/
1248 jx.msgs.forEach((m)=>f.processPost(m,atEnd));
1249 if('visible'===document.visibilityState){
1250
--- src/chat.js
+++ src/chat.js
@@ -116,11 +116,12 @@
116 contentDiv: E1('div.content'),
117 viewConfig: E1('#chat-config'),
118 viewPreview: E1('#chat-preview'),
119 previewContent: E1('#chat-preview-content'),
120 btnPreview: E1('#chat-preview-button'),
121 views: document.querySelectorAll('.chat-view'),
122 activeUserList: D.append(E1('#chat-user-list'), "user list placeholder")
123 },
124 me: F.user.name,
125 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
126 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
127 pageIsActive: 'visible'===document.visibilityState,
@@ -128,10 +129,17 @@
129 notificationBubbleColor: 'white',
130 totalMessageCount: 0, // total # of inbound messages
131 //! Number of messages to load for the history buttons
132 loadMessageCount: Math.abs(F.config.chat.initSize || 20),
133 ajaxInflight: 0,
134 usersLastSeen:{
135 /* Map of user names to their most recent message time
136 (JS Date object). Only messages received by the chat client
137 are considered. */
138 /* Reminder: to convert a Julian time J to JS:
139 new Date((J - 2440587.5) * 86400000) */
140 },
141 /** Gets (no args) or sets (1 arg) the current input text field value,
142 taking into account single- vs multi-line input. The getter returns
143 a string and the setter returns this object. */
144 inputValue: function(){
145 const e = this.inputElement();
@@ -367,11 +375,12 @@
375 defaults:{
376 "images-inline": !!F.config.chat.imagesInline,
377 "edit-multiline": false,
378 "monospace-messages": false,
379 "chat-only-mode": false,
380 "audible-alert": true,
381 "active-user-list": false
382 }
383 },
384 /** Plays a new-message notification sound IF the audible-alert
385 setting is true, else this is a no-op. Returns this.
386 */
@@ -407,10 +416,42 @@
416 setCurrentView: function(e){
417 this.e.views.forEach(function(E){
418 if(e!==E) D.addClass(E,'hidden');
419 });
420 return this.e.currentView = D.removeClass(e,'hidden');
421 },
422 /**
423 Updates the "active user list" view.
424 */
425 updateActiveUserList: function callee(){
426 if(!callee.sortUsersSeen){
427 /** Array.sort() callback. Expects an array of user names and
428 sorts them in last-received message order (newest first). */
429 const usersLastSeen = this.usersLastSeen;
430 callee.sortUsersSeen = function(l,r){
431 l = usersLastSeen[l];
432 r = usersLastSeen[r];
433 if(l && r) return r - l;
434 else if(l) return -1;
435 else if(r) return 1;
436 else return 0;
437 };
438 }
439 const self = this,
440 users = Object.keys(this.usersLastSeen).sort(callee.sortUsersSeen);
441 if(!users.length) return this;
442 const ael = this.e.activeUserList;
443 D.clearElement(ael);
444 users.forEach(function(u){
445 const uSpan = D.addClass(D.span(), 'chat-user');
446 const uDate = self.usersLastSeen[u];
447 D.append(uSpan, u);
448 if(uDate.$uColor){
449 uSpan.style.backgroundColor = uDate.$uColor;
450 }
451 D.append(ael, uSpan);
452 });
453 }
454 };
455 F.fetch.beforesend = ()=>cs.ajaxStart();
456 F.fetch.aftersend = ()=>cs.ajaxEnd();
457 cs.e.inputCurrent = cs.e.inputSingle;
@@ -427,10 +468,13 @@
468 tall vs wide. Can be toggled via settings popup. */
469 document.body.classList.add('my-messages-right');
470 }
471 if(cs.settings.getBool('monospace-messages',false)){
472 document.body.classList.add('monospace-messages');
473 }
474 if(cs.settings.getBool('active-user-list',false)){
475 cs.e.activeUserList.classList.remove('hidden');
476 }
477 cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
478 cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
479 cs.pageTitleOrig = cs.e.pageTitle.innerText;
480 const qs = (e)=>document.querySelector(e);
@@ -624,10 +668,46 @@
668 if(cs.pageIsActive){
669 cs.e.pageTitle.innerText = cs.pageTitleOrig;
670 }
671 }, true);
672 cs.setCurrentView(cs.e.viewMessages);
673
674 cs.e.activeUserList.addEventListener('click', function f(ev){
675 /* Filter messages on a user clicked in activeUserList */
676 ev.stopPropagation();
677 ev.preventDefault();
678 if(!ev.target.classList.contains('chat-user')) return false;
679 const eUser = ev.target;
680 const uname = eUser.innerText;
681 let eLast;
682 cs.setCurrentView(cs.e.viewMessages);
683 if(eUser.classList.contains('selected')){
684 eUser.classList.remove('selected');
685 cs.e.viewMessages.querySelectorAll(
686 '.message-widget.hidden'
687 ).forEach(function(e){
688 e.classList.remove('hidden');
689 eLast = e;
690 });
691 delete f.$eSelected;
692 }else{
693 if(f.$eSelected) f.$eSelected.classList.remove('selected');
694 f.$eSelected = eUser;
695 eUser.classList.add('selected');
696 cs.e.viewMessages.querySelectorAll(
697 '.message-widget'
698 ).forEach(function(e){
699 if(e.dataset.xfrom===uname){
700 e.classList.remove('hidden');
701 eLast = e;
702 }
703 else e.classList.add('hidden');
704 });
705 }
706 if(eLast) eLast.scrollIntoView(false);
707 return false;
708 }, false);
709 return cs;
710 })()/*Chat initialization*/;
711
712 /**
713 Custom widget type for rendering messages (one message per
@@ -1060,10 +1140,25 @@
1140 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1141 persistentSetting: 'edit-multiline',
1142 callback: function(){
1143 Chat.inputToggleSingleMulti();
1144 }
1145 },{
1146 label: "Show recent user list",
1147 boolValue: ()=>!Chat.e.activeUserList.classList.contains('hidden'),
1148 persistentSetting: 'active-user-list',
1149 callback: function(){
1150 D.toggleClass(Chat.e.activeUserList,'hidden');
1151 if(Chat.e.activeUserList.classList.contains('hidden')){
1152 /* When hiding this element, undo all filtering */
1153 D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
1154 /*Ideally we'd scroll the final message into view
1155 now, but because viewMessages is currently hidden behind
1156 viewConfig, scrolling is a no-op. */
1157 Chat.scrollMessagesTo(1);
1158 }
1159 }
1160 },{
1161 label: "Monospace message font",
1162 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1163 persistentSetting: 'monospace-messages',
1164 callback: function(){
@@ -1228,10 +1323,18 @@
1323 should only be true when loading older messages. */
1324 f.processPost = function(m,atEnd){
1325 ++Chat.totalMessageCount;
1326 if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
1327 if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
1328 if(m.xfrom && m.mtime){
1329 const d = new Date(m.mtime);
1330 const uls = Chat.usersLastSeen[m.xfrom];
1331 if(!uls || uls<d){
1332 d.$uColor = m.uclr;
1333 Chat.usersLastSeen[m.xfrom] = d;
1334 }
1335 }
1336 if( m.mdel ){
1337 /* A record deletion notice. */
1338 Chat.deleteMessageElem(m.mdel);
1339 return;
1340 }
@@ -1240,10 +1343,12 @@
1343 }
1344 const row = new Chat.MessageWidget(m);
1345 Chat.injectMessageElem(row.e.body,atEnd);
1346 if(m.isError){
1347 Chat._gotServerError = m;
1348 }else{
1349 Chat.updateActiveUserList();
1350 }
1351 }/*processPost()*/;
1352 }/*end static init*/
1353 jx.msgs.forEach((m)=>f.processPost(m,atEnd));
1354 if('visible'===document.visibilityState){
1355
--- src/style.chat.css
+++ src/style.chat.css
@@ -357,5 +357,32 @@
357357
body.chat #chat-preview #chat-preview-buttons > button {
358358
padding: 0.5em;
359359
flex: 0 1 auto;
360360
margin: 0.25em 0;
361361
}
362
+
363
+body.chat #chat-user-list {
364
+ border: 1px inset;
365
+ padding: 0.1em 0.2em;
366
+ border-radius: 0.25em;
367
+ display: flex;
368
+ flex-direction: row;
369
+ flex-wrap: wrap;
370
+ align-items: center;
371
+ font-size: 85%;
372
+ margin: 0.5em 0 0 0;
373
+ border-radius: 0.5em;
374
+ padding: 0.5em;
375
+}
376
+body.chat #chat-user-list::before {
377
+ content: "Most recently active:";
378
+}
379
+body.chat #chat-user-list .chat-user {
380
+ margin: 0.2em;
381
+ padding: 0.1em 0.5em;
382
+ border-radius: 0.5em;
383
+ cursor: pointer;
384
+}
385
+body.chat #chat-user-list .chat-user.selected {
386
+ font-weight: bold;
387
+ text-decoration: underline;
388
+}
362389
--- src/style.chat.css
+++ src/style.chat.css
@@ -357,5 +357,32 @@
357 body.chat #chat-preview #chat-preview-buttons > button {
358 padding: 0.5em;
359 flex: 0 1 auto;
360 margin: 0.25em 0;
361 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
--- src/style.chat.css
+++ src/style.chat.css
@@ -357,5 +357,32 @@
357 body.chat #chat-preview #chat-preview-buttons > button {
358 padding: 0.5em;
359 flex: 0 1 auto;
360 margin: 0.25em 0;
361 }
362
363 body.chat #chat-user-list {
364 border: 1px inset;
365 padding: 0.1em 0.2em;
366 border-radius: 0.25em;
367 display: flex;
368 flex-direction: row;
369 flex-wrap: wrap;
370 align-items: center;
371 font-size: 85%;
372 margin: 0.5em 0 0 0;
373 border-radius: 0.5em;
374 padding: 0.5em;
375 }
376 body.chat #chat-user-list::before {
377 content: "Most recently active:";
378 }
379 body.chat #chat-user-list .chat-user {
380 margin: 0.2em;
381 padding: 0.1em 0.5em;
382 border-radius: 0.5em;
383 cursor: pointer;
384 }
385 body.chat #chat-user-list .chat-user.selected {
386 font-weight: bold;
387 text-decoration: underline;
388 }
389

Keyboard Shortcuts

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