Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | UI refinement of the chat user activity list. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | chat-user-filter |
| Files: | files | file ages | folders |
| SHA3-256: |
7aea432a4786882ab3016807fb2c78b6 |
| User & Date: | stephan 2021-09-23 11:44:11.747 |
Context
|
2021-09-23
| ||
| 12:00 | Added toggle for the recent activity timestamps. ... (check-in: 9938acb049 user: stephan tags: chat-user-filter) | |
| 11:44 | UI refinement of the chat user activity list. ... (check-in: 7aea432a47 user: stephan tags: chat-user-filter) | |
| 09:41 | 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). ... (check-in: dafd549711 user: stephan tags: chat-user-filter) | |
Changes
Changes to src/chat.c.
| ︙ | ︙ | |||
179 180 181 182 183 184 185 | @ your environment. @ </div> @ <input type="file" name="file" id="chat-input-file"> @ </div> @ <div id="chat-drop-details"></div> @ </div> @ </div> | > > | > | 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | @ your environment. @ </div> @ <input type="file" name="file" id="chat-input-file"> @ </div> @ <div id="chat-drop-details"></div> @ </div> @ </div> @ <fieldset id='chat-user-list-wrapper' class='hidden'> @ <legend>Recently Active</legend> @ <div id='chat-user-list'>(user list goes here)</div> @ </fieldset> @ <div id='chat-preview' class='hidden chat-view'> @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> @ <div id='chat-preview-content' class='message-widget-content'></div> @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div> @ </div> @ <div id='chat-config' class='hidden chat-view'> @ <div id='chat-config-options'></div> |
| ︙ | ︙ |
Changes to src/chat.js.
| ︙ | ︙ | |||
32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
r2 = v.getBoundingClientRect();
if(r1.top<=r2.bottom && r1.top>=r2.top) return true;
else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true;
return false;
};
const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank');
(function(){
let dbg = document.querySelector('#debugMsg');
if(dbg){
/* This can inadvertently influence our flexbox layouts, so move
it out of the way. */
D.append(document.body,dbg);
| > > > > > > > > > > > > > > > > > > > > | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
r2 = v.getBoundingClientRect();
if(r1.top<=r2.bottom && r1.top>=r2.top) return true;
else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true;
return false;
};
const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank');
/**
Returns an almost-ISO8601 form of Date object d.
*/
const iso8601ish = function(d){
return d.toISOString()
.replace('T',' ').replace(/\.\d+/,'')
.replace('Z', ' zulu');
};
/** Returns the local time string of Date object d, defaulting
to the current time. */
const localTimeString = function ff(d){
d || (d = new Date());
return [
d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/),
'-',pad2(d.getDate()),
' ',pad2(d.getHours()),':',pad2(d.getMinutes()),
':',pad2(d.getSeconds())
].join('');
};
(function(){
let dbg = document.querySelector('#debugMsg');
if(dbg){
/* This can inadvertently influence our flexbox layouts, so move
it out of the way. */
D.append(document.body,dbg);
|
| ︙ | ︙ | |||
115 116 117 118 119 120 121 |
inputFile: E1('#chat-input-file'),
contentDiv: E1('div.content'),
viewConfig: E1('#chat-config'),
viewPreview: E1('#chat-preview'),
previewContent: E1('#chat-preview-content'),
btnPreview: E1('#chat-preview-button'),
views: document.querySelectorAll('.chat-view'),
| > | > > > > > > | 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
inputFile: E1('#chat-input-file'),
contentDiv: E1('div.content'),
viewConfig: E1('#chat-config'),
viewPreview: E1('#chat-preview'),
previewContent: E1('#chat-preview-content'),
btnPreview: E1('#chat-preview-button'),
views: document.querySelectorAll('.chat-view'),
activeUserListWrapper: E1('#chat-user-list-wrapper'),
activeUserList: E1('#chat-user-list')
},
me: F.user.name,
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
pageIsActive: 'visible'===document.visibilityState,
changesSincePageHidden: 0,
notificationBubbleColor: 'white',
totalMessageCount: 0, // total # of inbound messages
//! Number of messages to load for the history buttons
loadMessageCount: Math.abs(F.config.chat.initSize || 20),
ajaxInflight: 0,
usersLastSeen:{
/* Map of user names to their most recent message time
(JS Date object). Only messages received by the chat client
are considered. */
/* Reminder: to convert a Julian time J to JS:
new Date((J - 2440587.5) * 86400000) */
},
filterState:{
activeUser: undefined,
match: function(uname){
return this.activeUser===uname || !this.activeUser;
}
},
/** Gets (no args) or sets (1 arg) the current input text field value,
taking into account single- vs multi-line input. The getter returns
a string and the setter returns this object. */
inputValue: function(){
const e = this.inputElement();
if(arguments.length){
|
| ︙ | ︙ | |||
250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
/* Injects DOM element e as a new row in the chat, at the oldest
end of the list if atEnd is truthy, else at the newest end of
the list. */
injectMessageElem: function f(e, atEnd){
const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
holder = this.e.viewMessages,
prevMessage = this.e.newestMessage;
if(atEnd){
const fe = mip.nextElementSibling;
if(fe) mip.parentNode.insertBefore(e, fe);
else D.append(mip.parentNode, e);
}else{
D.append(holder,e);
this.e.newestMessage = e;
| > > > | 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
/* Injects DOM element e as a new row in the chat, at the oldest
end of the list if atEnd is truthy, else at the newest end of
the list. */
injectMessageElem: function f(e, atEnd){
const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
holder = this.e.viewMessages,
prevMessage = this.e.newestMessage;
if(!this.filterState.match(e.dataset.xfrom)){
e.classList.add('hidden');
}
if(atEnd){
const fe = mip.nextElementSibling;
if(fe) mip.parentNode.insertBefore(e, fe);
else D.append(mip.parentNode, e);
}else{
D.append(holder,e);
this.e.newestMessage = e;
|
| ︙ | ︙ | |||
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 |
Updates the "active user list" view.
*/
updateActiveUserList: function callee(){
if(!callee.sortUsersSeen){
/** Array.sort() callback. Expects an array of user names and
sorts them in last-received message order (newest first). */
const usersLastSeen = this.usersLastSeen;
callee.sortUsersSeen = function(l,r){
l = usersLastSeen[l];
r = usersLastSeen[r];
if(l && r) return r - l;
else if(l) return -1;
else if(r) return 1;
else return 0;
};
| > < < < < < < | | | > > > > | > > > > | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 |
Updates the "active user list" view.
*/
updateActiveUserList: function callee(){
if(!callee.sortUsersSeen){
/** Array.sort() callback. Expects an array of user names and
sorts them in last-received message order (newest first). */
const usersLastSeen = this.usersLastSeen;
const self = this;
callee.sortUsersSeen = function(l,r){
l = usersLastSeen[l];
r = usersLastSeen[r];
if(l && r) return r - l;
else if(l) return -1;
else if(r) return 1;
else return 0;
};
callee.addUserElem = function(u){
const uSpan = D.addClass(D.span(), 'chat-user');
const uDate = self.usersLastSeen[u];
if(self.filterState.activeUser===u){
uSpan.classList.add('selected');
}
uSpan.dataset.uname = u;
D.append(uSpan, u, "\n",
D.append(
D.addClass(D.span(),'timestamp'),
localTimeString(uDate)//.substr(5/*chop off year*/)
));
if(uDate.$uColor){
uSpan.style.backgroundColor = uDate.$uColor;
}
D.append(self.e.activeUserList, uSpan);
};
}
D.clearElement(this.e.activeUserList);
Object.keys(this.usersLastSeen).sort(
callee.sortUsersSeen
).forEach(callee.addUserElem);
return this;
},
/**
Applies user name filter to all current messages, or clears
the filter if uname is falsy.
*/
setUserFilter: function(uname){
this.filterState.activeUser = uname;
const mw = this.e.viewMessages.querySelectorAll('.message-widget');
const self = this;
let eLast;
mw.forEach(function(w){
if(self.filterState.match(w.dataset.xfrom)){
w.classList.remove('hidden');
eLast = w;
}else{
w.classList.add('hidden');
}
});
if(eLast) eLast.scrollIntoView(false);
cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected');
});
return this;
}
};
F.fetch.beforesend = ()=>cs.ajaxStart();
F.fetch.aftersend = ()=>cs.ajaxEnd();
cs.e.inputCurrent = cs.e.inputSingle;
/* Install default settings... */
Object.keys(cs.settings.defaults).forEach(function(k){
|
| ︙ | ︙ | |||
468 469 470 471 472 473 474 |
tall vs wide. Can be toggled via settings popup. */
document.body.classList.add('my-messages-right');
}
if(cs.settings.getBool('monospace-messages',false)){
document.body.classList.add('monospace-messages');
}
if(cs.settings.getBool('active-user-list',false)){
| | | 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 |
tall vs wide. Can be toggled via settings popup. */
document.body.classList.add('my-messages-right');
}
if(cs.settings.getBool('monospace-messages',false)){
document.body.classList.add('monospace-messages');
}
if(cs.settings.getBool('active-user-list',false)){
cs.e.activeUserListWrapper.classList.remove('hidden');
}
cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
cs.pageTitleOrig = cs.e.pageTitle.innerText;
const qs = (e)=>document.querySelector(e);
const argsToArray = function(args){
return Array.prototype.slice.call(args,0);
|
| ︙ | ︙ | |||
671 672 673 674 675 676 677 |
}, true);
cs.setCurrentView(cs.e.viewMessages);
cs.e.activeUserList.addEventListener('click', function f(ev){
/* Filter messages on a user clicked in activeUserList */
ev.stopPropagation();
ev.preventDefault();
| > | | > > | > < < < < < | < < < | < < | < < < < | 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 |
}, true);
cs.setCurrentView(cs.e.viewMessages);
cs.e.activeUserList.addEventListener('click', function f(ev){
/* Filter messages on a user clicked in activeUserList */
ev.stopPropagation();
ev.preventDefault();
let eUser = ev.target;
while(eUser!==this && !eUser.classList.contains('chat-user')){
eUser = eUser.parentNode;
}
if(eUser==this || !eUser) return false;
const uname = eUser.dataset.uname;
let eLast;
cs.setCurrentView(cs.e.viewMessages);
if(eUser.classList.contains('selected')){
/* If curently selected, toggle filter off */
eUser.classList.remove('selected');
cs.setUserFilter(false);
delete f.$eSelected;
}else{
if(f.$eSelected) f.$eSelected.classList.remove('selected');
f.$eSelected = eUser;
eUser.classList.add('selected');
cs.setUserFilter(uname);
}
return false;
}, false);
return cs;
})()/*Chat initialization*/;
/**
Custom widget type for rendering messages (one message per
|
| ︙ | ︙ | |||
749 750 751 752 753 754 755 |
//d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/),
//'-',pad2(d.getDate()), ' ',
d.getHours(),":",
(d.getMinutes()+100).toString().slice(1,3),
' ', dowMap[d.getDay()]
].join('');
};
| < < < < < < < < < < < | 801 802 803 804 805 806 807 808 809 810 811 812 813 814 |
//d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/),
//'-',pad2(d.getDate()), ' ',
d.getHours(),":",
(d.getMinutes()+100).toString().slice(1,3),
' ', dowMap[d.getDay()]
].join('');
};
cf.prototype = {
scrollIntoView: function(){
this.e.content.scrollIntoView();
},
setMessage: function(m){
const ds = this.e.body.dataset;
ds.timestamp = m.mtime;
|
| ︙ | ︙ | |||
1112 1113 1114 1115 1116 1117 1118 |
}, false);
Chat.e.btnSubmit.addEventListener('click',(e)=>{
e.preventDefault();
Chat.submitMessage();
return false;
});
| < < < < < < | 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 |
}, false);
Chat.e.btnSubmit.addEventListener('click',(e)=>{
e.preventDefault();
Chat.submitMessage();
return false;
});
(function(){/*Set up #chat-settings-button */
const settingsButton = document.querySelector('#chat-settings-button');
const optionsMenu = E1('#chat-config-options');
const cbToggle = function(ev){
ev.preventDefault();
ev.stopPropagation();
Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig
|
| ︙ | ︙ | |||
1140 1141 1142 1143 1144 1145 1146 |
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
persistentSetting: 'edit-multiline',
callback: function(){
Chat.inputToggleSingleMulti();
}
},{
label: "Show recent user list",
| | | | | 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 |
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
persistentSetting: 'edit-multiline',
callback: function(){
Chat.inputToggleSingleMulti();
}
},{
label: "Show recent user 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);
}
|
| ︙ | ︙ |
Changes to src/style.chat.css.
| ︙ | ︙ | |||
198 199 200 201 202 203 204 |
align-items: stretch;
}
/* Wrapper for /chat user input controls */
body.chat #chat-input-area {
display: flex;
flex-direction: column;
padding: 0.5em 0 0 0;
| < < | | 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
align-items: stretch;
}
/* Wrapper for /chat user input controls */
body.chat #chat-input-area {
display: flex;
flex-direction: column;
padding: 0.5em 0 0 0;
margin: 0.5em 0 0 0;
position: initial /*sticky currently disabled due to scrolling-related issues*/;
/*bottom: 0;*/
}
body.chat:not(.chat-only-mode) #chat-input-area{
/* Safari user reports that 2em is necessary to keep the file selection
widget from overlapping the page footer, whereas a margin of 0 is fine
for FF/Chrome (and 2em is a *huge* waste of space for those). */
margin-bottom: 0;
}
|
| ︙ | ︙ | |||
356 357 358 359 360 361 362 |
body.chat #chat-config > button,
body.chat #chat-preview #chat-preview-buttons > button {
padding: 0.5em;
flex: 0 1 auto;
margin: 0.25em 0;
}
| | < < | > > > > < < < < | | > > > > > > | 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 |
body.chat #chat-config > button,
body.chat #chat-preview #chat-preview-buttons > button {
padding: 0.5em;
flex: 0 1 auto;
margin: 0.25em 0;
}
body.chat #chat-user-list-wrapper {
border-radius: 0.5em;
margin: 0.5em 0 0 0;
font-size: 85%;
}
body.chat #chat-user-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
body.chat #chat-user-list::before {
/*content: "Recently active: ";*/
}
body.chat #chat-user-list .chat-user {
margin: 0.2em;
padding: 0.1em 0.5em 0.2em 0.5em;
border-radius: 0.5em;
cursor: pointer;
text-align: center;
white-space: pre;
}
body.chat #chat-user-list .chat-user > .timestamp {
font-size: 80%;
font-family: monospace;
}
body.chat #chat-user-list .chat-user.selected {
font-weight: bold;
text-decoration: underline;
}
|