| | @@ -34,10 +34,30 @@ |
| 34 | 34 | else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true; |
| 35 | 35 | return false; |
| 36 | 36 | }; |
| 37 | 37 | |
| 38 | 38 | const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank'); |
| 39 | + |
| 40 | + /** |
| 41 | + Returns an almost-ISO8601 form of Date object d. |
| 42 | + */ |
| 43 | + const iso8601ish = function(d){ |
| 44 | + return d.toISOString() |
| 45 | + .replace('T',' ').replace(/\.\d+/,'') |
| 46 | + .replace('Z', ' zulu'); |
| 47 | + }; |
| 48 | + /** Returns the local time string of Date object d, defaulting |
| 49 | + to the current time. */ |
| 50 | + const localTimeString = function ff(d){ |
| 51 | + d || (d = new Date()); |
| 52 | + return [ |
| 53 | + d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), |
| 54 | + '-',pad2(d.getDate()), |
| 55 | + ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), |
| 56 | + ':',pad2(d.getSeconds()) |
| 57 | + ].join(''); |
| 58 | + }; |
| 39 | 59 | |
| 40 | 60 | (function(){ |
| 41 | 61 | let dbg = document.querySelector('#debugMsg'); |
| 42 | 62 | if(dbg){ |
| 43 | 63 | /* This can inadvertently influence our flexbox layouts, so move |
| | @@ -117,11 +137,12 @@ |
| 117 | 137 | viewConfig: E1('#chat-config'), |
| 118 | 138 | viewPreview: E1('#chat-preview'), |
| 119 | 139 | previewContent: E1('#chat-preview-content'), |
| 120 | 140 | btnPreview: E1('#chat-preview-button'), |
| 121 | 141 | views: document.querySelectorAll('.chat-view'), |
| 122 | | - activeUserList: D.append(E1('#chat-user-list'), "user list placeholder") |
| 142 | + activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 143 | + activeUserList: E1('#chat-user-list') |
| 123 | 144 | }, |
| 124 | 145 | me: F.user.name, |
| 125 | 146 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 126 | 147 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 127 | 148 | pageIsActive: 'visible'===document.visibilityState, |
| | @@ -135,10 +156,16 @@ |
| 135 | 156 | /* Map of user names to their most recent message time |
| 136 | 157 | (JS Date object). Only messages received by the chat client |
| 137 | 158 | are considered. */ |
| 138 | 159 | /* Reminder: to convert a Julian time J to JS: |
| 139 | 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 | + } |
| 140 | 167 | }, |
| 141 | 168 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 142 | 169 | taking into account single- vs multi-line input. The getter returns |
| 143 | 170 | a string and the setter returns this object. */ |
| 144 | 171 | inputValue: function(){ |
| | @@ -252,10 +279,13 @@ |
| 252 | 279 | the list. */ |
| 253 | 280 | injectMessageElem: function f(e, atEnd){ |
| 254 | 281 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 255 | 282 | holder = this.e.viewMessages, |
| 256 | 283 | prevMessage = this.e.newestMessage; |
| 284 | + if(!this.filterState.match(e.dataset.xfrom)){ |
| 285 | + e.classList.add('hidden'); |
| 286 | + } |
| 257 | 287 | if(atEnd){ |
| 258 | 288 | const fe = mip.nextElementSibling; |
| 259 | 289 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 260 | 290 | else D.append(mip.parentNode, e); |
| 261 | 291 | }else{ |
| | @@ -425,33 +455,65 @@ |
| 425 | 455 | updateActiveUserList: function callee(){ |
| 426 | 456 | if(!callee.sortUsersSeen){ |
| 427 | 457 | /** Array.sort() callback. Expects an array of user names and |
| 428 | 458 | sorts them in last-received message order (newest first). */ |
| 429 | 459 | const usersLastSeen = this.usersLastSeen; |
| 460 | + const self = this; |
| 430 | 461 | callee.sortUsersSeen = function(l,r){ |
| 431 | 462 | l = usersLastSeen[l]; |
| 432 | 463 | r = usersLastSeen[r]; |
| 433 | 464 | if(l && r) return r - l; |
| 434 | 465 | else if(l) return -1; |
| 435 | 466 | else if(r) return 1; |
| 436 | 467 | else return 0; |
| 437 | 468 | }; |
| 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); |
| 469 | + callee.addUserElem = function(u){ |
| 470 | + const uSpan = D.addClass(D.span(), 'chat-user'); |
| 471 | + const uDate = self.usersLastSeen[u]; |
| 472 | + if(self.filterState.activeUser===u){ |
| 473 | + uSpan.classList.add('selected'); |
| 474 | + } |
| 475 | + uSpan.dataset.uname = u; |
| 476 | + D.append(uSpan, u, "\n", |
| 477 | + D.append( |
| 478 | + D.addClass(D.span(),'timestamp'), |
| 479 | + localTimeString(uDate)//.substr(5/*chop off year*/) |
| 480 | + )); |
| 481 | + if(uDate.$uColor){ |
| 482 | + uSpan.style.backgroundColor = uDate.$uColor; |
| 483 | + } |
| 484 | + D.append(self.e.activeUserList, uSpan); |
| 485 | + }; |
| 486 | + } |
| 487 | + D.clearElement(this.e.activeUserList); |
| 488 | + Object.keys(this.usersLastSeen).sort( |
| 489 | + callee.sortUsersSeen |
| 490 | + ).forEach(callee.addUserElem); |
| 491 | + return this; |
| 492 | + }, |
| 493 | + /** |
| 494 | + Applies user name filter to all current messages, or clears |
| 495 | + the filter if uname is falsy. |
| 496 | + */ |
| 497 | + setUserFilter: function(uname){ |
| 498 | + this.filterState.activeUser = uname; |
| 499 | + const mw = this.e.viewMessages.querySelectorAll('.message-widget'); |
| 500 | + const self = this; |
| 501 | + let eLast; |
| 502 | + mw.forEach(function(w){ |
| 503 | + if(self.filterState.match(w.dataset.xfrom)){ |
| 504 | + w.classList.remove('hidden'); |
| 505 | + eLast = w; |
| 506 | + }else{ |
| 507 | + w.classList.add('hidden'); |
| 508 | + } |
| 509 | + }); |
| 510 | + if(eLast) eLast.scrollIntoView(false); |
| 511 | + cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){ |
| 512 | + e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected'); |
| 452 | 513 | }); |
| 514 | + return this; |
| 453 | 515 | } |
| 454 | 516 | }; |
| 455 | 517 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 456 | 518 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 457 | 519 | cs.e.inputCurrent = cs.e.inputSingle; |
| | @@ -470,11 +532,11 @@ |
| 470 | 532 | } |
| 471 | 533 | if(cs.settings.getBool('monospace-messages',false)){ |
| 472 | 534 | document.body.classList.add('monospace-messages'); |
| 473 | 535 | } |
| 474 | 536 | if(cs.settings.getBool('active-user-list',false)){ |
| 475 | | - cs.e.activeUserList.classList.remove('hidden'); |
| 537 | + cs.e.activeUserListWrapper.classList.remove('hidden'); |
| 476 | 538 | } |
| 477 | 539 | cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false)); |
| 478 | 540 | cs.chatOnlyMode(cs.settings.getBool('chat-only-mode')); |
| 479 | 541 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 480 | 542 | const qs = (e)=>document.querySelector(e); |
| | @@ -673,39 +735,29 @@ |
| 673 | 735 | |
| 674 | 736 | cs.e.activeUserList.addEventListener('click', function f(ev){ |
| 675 | 737 | /* Filter messages on a user clicked in activeUserList */ |
| 676 | 738 | ev.stopPropagation(); |
| 677 | 739 | ev.preventDefault(); |
| 678 | | - if(!ev.target.classList.contains('chat-user')) return false; |
| 679 | | - const eUser = ev.target; |
| 680 | | - const uname = eUser.innerText; |
| 740 | + let eUser = ev.target; |
| 741 | + while(eUser!==this && !eUser.classList.contains('chat-user')){ |
| 742 | + eUser = eUser.parentNode; |
| 743 | + } |
| 744 | + if(eUser==this || !eUser) return false; |
| 745 | + const uname = eUser.dataset.uname; |
| 681 | 746 | let eLast; |
| 682 | 747 | cs.setCurrentView(cs.e.viewMessages); |
| 683 | 748 | if(eUser.classList.contains('selected')){ |
| 749 | + /* If curently selected, toggle filter off */ |
| 684 | 750 | 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 | | - }); |
| 751 | + cs.setUserFilter(false); |
| 691 | 752 | delete f.$eSelected; |
| 692 | 753 | }else{ |
| 693 | 754 | if(f.$eSelected) f.$eSelected.classList.remove('selected'); |
| 694 | 755 | f.$eSelected = eUser; |
| 695 | 756 | 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); |
| 757 | + cs.setUserFilter(uname); |
| 758 | + } |
| 707 | 759 | return false; |
| 708 | 760 | }, false); |
| 709 | 761 | return cs; |
| 710 | 762 | })()/*Chat initialization*/; |
| 711 | 763 | |
| | @@ -751,21 +803,10 @@ |
| 751 | 803 | d.getHours(),":", |
| 752 | 804 | (d.getMinutes()+100).toString().slice(1,3), |
| 753 | 805 | ' ', dowMap[d.getDay()] |
| 754 | 806 | ].join(''); |
| 755 | 807 | }; |
| 756 | | - /** Returns the local time string of Date object d, defaulting |
| 757 | | - to the current time. */ |
| 758 | | - const localTimeString = function ff(d){ |
| 759 | | - d || (d = new Date()); |
| 760 | | - return [ |
| 761 | | - d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), |
| 762 | | - '-',pad2(d.getDate()), |
| 763 | | - ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), |
| 764 | | - ':',pad2(d.getSeconds()) |
| 765 | | - ].join(''); |
| 766 | | - }; |
| 767 | 808 | cf.prototype = { |
| 768 | 809 | scrollIntoView: function(){ |
| 769 | 810 | this.e.content.scrollIntoView(); |
| 770 | 811 | }, |
| 771 | 812 | setMessage: function(m){ |
| | @@ -1114,16 +1155,10 @@ |
| 1114 | 1155 | e.preventDefault(); |
| 1115 | 1156 | Chat.submitMessage(); |
| 1116 | 1157 | return false; |
| 1117 | 1158 | }); |
| 1118 | 1159 | |
| 1119 | | - /* Returns an almost-ISO8601 form of Date object d. */ |
| 1120 | | - const iso8601ish = function(d){ |
| 1121 | | - return d.toISOString() |
| 1122 | | - .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu'); |
| 1123 | | - }; |
| 1124 | | - |
| 1125 | 1160 | (function(){/*Set up #chat-settings-button */ |
| 1126 | 1161 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1127 | 1162 | const optionsMenu = E1('#chat-config-options'); |
| 1128 | 1163 | const cbToggle = function(ev){ |
| 1129 | 1164 | ev.preventDefault(); |
| | @@ -1142,15 +1177,15 @@ |
| 1142 | 1177 | callback: function(){ |
| 1143 | 1178 | Chat.inputToggleSingleMulti(); |
| 1144 | 1179 | } |
| 1145 | 1180 | },{ |
| 1146 | 1181 | label: "Show recent user list", |
| 1147 | | - boolValue: ()=>!Chat.e.activeUserList.classList.contains('hidden'), |
| 1182 | + boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'), |
| 1148 | 1183 | persistentSetting: 'active-user-list', |
| 1149 | 1184 | callback: function(){ |
| 1150 | | - D.toggleClass(Chat.e.activeUserList,'hidden'); |
| 1151 | | - if(Chat.e.activeUserList.classList.contains('hidden')){ |
| 1185 | + D.toggleClass(Chat.e.activeUserListWrapper,'hidden'); |
| 1186 | + if(Chat.e.activeUserListWrapper.classList.contains('hidden')){ |
| 1152 | 1187 | /* When hiding this element, undo all filtering */ |
| 1153 | 1188 | D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden'); |
| 1154 | 1189 | /*Ideally we'd scroll the final message into view |
| 1155 | 1190 | now, but because viewMessages is currently hidden behind |
| 1156 | 1191 | viewConfig, scrolling is a no-op. */ |
| 1157 | 1192 | |