| | @@ -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 |
| | @@ -116,11 +136,13 @@ |
| 116 | 136 | contentDiv: E1('div.content'), |
| 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 | | - views: document.querySelectorAll('.chat-view') |
| 141 | + views: document.querySelectorAll('.chat-view'), |
| 142 | + activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 143 | + activeUserList: E1('#chat-user-list') |
| 122 | 144 | }, |
| 123 | 145 | me: F.user.name, |
| 124 | 146 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 125 | 147 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 126 | 148 | pageIsActive: 'visible'===document.visibilityState, |
| | @@ -128,10 +150,23 @@ |
| 128 | 150 | notificationBubbleColor: 'white', |
| 129 | 151 | totalMessageCount: 0, // total # of inbound messages |
| 130 | 152 | //! Number of messages to load for the history buttons |
| 131 | 153 | loadMessageCount: Math.abs(F.config.chat.initSize || 20), |
| 132 | 154 | ajaxInflight: 0, |
| 155 | + usersLastSeen:{ |
| 156 | + /* Map of user names to their most recent message time |
| 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 | + }, |
| 133 | 168 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 134 | 169 | taking into account single- vs multi-line input. The getter returns |
| 135 | 170 | a string and the setter returns this object. */ |
| 136 | 171 | inputValue: function(){ |
| 137 | 172 | const e = this.inputElement(); |
| | @@ -167,10 +202,11 @@ |
| 167 | 202 | D.removeClass(this.e.inputCurrent, 'hidden'); |
| 168 | 203 | const mh2 = m.clientHeight; |
| 169 | 204 | m.scrollTo(0, sTop + (mh1-mh2)); |
| 170 | 205 | this.e.inputCurrent.value = old.value; |
| 171 | 206 | old.value = ''; |
| 207 | + this.animate(this.e.inputCurrent, "anim-flip-v"); |
| 172 | 208 | return this; |
| 173 | 209 | }, |
| 174 | 210 | /** |
| 175 | 211 | If passed true or no arguments, switches to multi-line mode |
| 176 | 212 | if currently in single-line mode. If passed false, switches |
| | @@ -244,10 +280,13 @@ |
| 244 | 280 | the list. */ |
| 245 | 281 | injectMessageElem: function f(e, atEnd){ |
| 246 | 282 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 247 | 283 | holder = this.e.viewMessages, |
| 248 | 284 | prevMessage = this.e.newestMessage; |
| 285 | + if(!this.filterState.match(e.dataset.xfrom)){ |
| 286 | + e.classList.add('hidden'); |
| 287 | + } |
| 249 | 288 | if(atEnd){ |
| 250 | 289 | const fe = mip.nextElementSibling; |
| 251 | 290 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 252 | 291 | else D.append(mip.parentNode, e); |
| 253 | 292 | }else{ |
| | @@ -367,11 +406,13 @@ |
| 367 | 406 | defaults:{ |
| 368 | 407 | "images-inline": !!F.config.chat.imagesInline, |
| 369 | 408 | "edit-multiline": false, |
| 370 | 409 | "monospace-messages": false, |
| 371 | 410 | "chat-only-mode": false, |
| 372 | | - "audible-alert": true |
| 411 | + "audible-alert": true, |
| 412 | + "active-user-list": false, |
| 413 | + "active-user-list-timestamps": false |
| 373 | 414 | } |
| 374 | 415 | }, |
| 375 | 416 | /** Plays a new-message notification sound IF the audible-alert |
| 376 | 417 | setting is true, else this is a no-op. Returns this. |
| 377 | 418 | */ |
| | @@ -403,16 +444,110 @@ |
| 403 | 444 | Expects e to be one of the elements in this.e.views. |
| 404 | 445 | The 'hidden' class is removed from e and added to |
| 405 | 446 | all other elements in that list. Returns e. |
| 406 | 447 | */ |
| 407 | 448 | setCurrentView: function(e){ |
| 449 | + if(e===this.e.currentView){ |
| 450 | + return e; |
| 451 | + } |
| 408 | 452 | this.e.views.forEach(function(E){ |
| 409 | 453 | if(e!==E) D.addClass(E,'hidden'); |
| 410 | 454 | }); |
| 411 | | - return this.e.currentView = D.removeClass(e,'hidden'); |
| 455 | + this.e.currentView = D.removeClass(e,'hidden'); |
| 456 | + this.animate(this.e.currentView, 'anim-fade-in-fast'); |
| 457 | + return this.e.currentView; |
| 458 | + }, |
| 459 | + /** |
| 460 | + Updates the "active user list" view if we are not currently |
| 461 | + batch-loading messages and if the active user list UI element |
| 462 | + is active. |
| 463 | + */ |
| 464 | + updateActiveUserList: function callee(){ |
| 465 | + if(this._isBatchLoading |
| 466 | + || this.e.activeUserListWrapper.classList.contains('hidden')){ |
| 467 | + return this; |
| 468 | + }else if(!callee.sortUsersSeen){ |
| 469 | + /** Array.sort() callback. Expects an array of user names and |
| 470 | + sorts them in last-received message order (newest first). */ |
| 471 | + const self = this; |
| 472 | + callee.sortUsersSeen = function(l,r){ |
| 473 | + l = self.usersLastSeen[l]; |
| 474 | + r = self.usersLastSeen[r]; |
| 475 | + if(l && r) return r - l; |
| 476 | + else if(l) return -1; |
| 477 | + else if(r) return 1; |
| 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( |
| 489 | + D.addClass(D.span(),'timestamp'), |
| 490 | + localTimeString(uDate)//.substr(5/*chop off year*/) |
| 491 | + )); |
| 492 | + if(uDate.$uColor){ |
| 493 | + uSpan.style.backgroundColor = uDate.$uColor; |
| 494 | + } |
| 495 | + D.append(self.e.activeUserList, uSpan); |
| 496 | + }; |
| 497 | + } |
| 498 | + //D.clearElement(this.e.activeUserList); |
| 499 | + D.remove(this.e.activeUserList.querySelectorAll('.chat-user')); |
| 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 |
| 537 | + to D.addClassBriefly(), else this is a no-op. |
| 538 | + If cb is a function, it is called after the |
| 539 | + CSS class is removed. Returns this object; |
| 540 | + */ |
| 541 | + animate: function f(e,a,cb){ |
| 542 | + if(!f.$disabled){ |
| 543 | + D.addClassBriefly(e, a, 0, cb); |
| 544 | + } |
| 545 | + return this; |
| 412 | 546 | } |
| 413 | 547 | }; |
| 548 | + cs.animate.$disabled = true; |
| 414 | 549 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 415 | 550 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 416 | 551 | cs.e.inputCurrent = cs.e.inputSingle; |
| 417 | 552 | /* Install default settings... */ |
| 418 | 553 | Object.keys(cs.settings.defaults).forEach(function(k){ |
| | @@ -427,10 +562,16 @@ |
| 427 | 562 | tall vs wide. Can be toggled via settings popup. */ |
| 428 | 563 | document.body.classList.add('my-messages-right'); |
| 429 | 564 | } |
| 430 | 565 | if(cs.settings.getBool('monospace-messages',false)){ |
| 431 | 566 | document.body.classList.add('monospace-messages'); |
| 567 | + } |
| 568 | + if(cs.settings.getBool('active-user-list',false)){ |
| 569 | + cs.e.activeUserListWrapper.classList.remove('hidden'); |
| 570 | + } |
| 571 | + if(cs.settings.getBool('active-user-list-timestamps',false)){ |
| 572 | + cs.e.activeUserList.classList.add('timestamps'); |
| 432 | 573 | } |
| 433 | 574 | cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false)); |
| 434 | 575 | cs.chatOnlyMode(cs.settings.getBool('chat-only-mode')); |
| 435 | 576 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 436 | 577 | const qs = (e)=>document.querySelector(e); |
| | @@ -624,10 +765,36 @@ |
| 624 | 765 | if(cs.pageIsActive){ |
| 625 | 766 | cs.e.pageTitle.innerText = cs.pageTitleOrig; |
| 626 | 767 | } |
| 627 | 768 | }, true); |
| 628 | 769 | cs.setCurrentView(cs.e.viewMessages); |
| 770 | + |
| 771 | + cs.e.activeUserList.addEventListener('click', function f(ev){ |
| 772 | + /* Filter messages on a user clicked in activeUserList */ |
| 773 | + ev.stopPropagation(); |
| 774 | + ev.preventDefault(); |
| 775 | + let eUser = ev.target; |
| 776 | + while(eUser!==this && !eUser.classList.contains('chat-user')){ |
| 777 | + eUser = eUser.parentNode; |
| 778 | + } |
| 779 | + if(eUser==this || !eUser) return false; |
| 780 | + const uname = eUser.dataset.uname; |
| 781 | + let eLast; |
| 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); |
| 629 | 796 | return cs; |
| 630 | 797 | })()/*Chat initialization*/; |
| 631 | 798 | |
| 632 | 799 | /** |
| 633 | 800 | Custom widget type for rendering messages (one message per |
| | @@ -671,21 +838,10 @@ |
| 671 | 838 | d.getHours(),":", |
| 672 | 839 | (d.getMinutes()+100).toString().slice(1,3), |
| 673 | 840 | ' ', dowMap[d.getDay()] |
| 674 | 841 | ].join(''); |
| 675 | 842 | }; |
| 676 | | - /** Returns the local time string of Date object d, defaulting |
| 677 | | - to the current time. */ |
| 678 | | - const localTimeString = function ff(d){ |
| 679 | | - d || (d = new Date()); |
| 680 | | - return [ |
| 681 | | - d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), |
| 682 | | - '-',pad2(d.getDate()), |
| 683 | | - ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), |
| 684 | | - ':',pad2(d.getSeconds()) |
| 685 | | - ].join(''); |
| 686 | | - }; |
| 687 | 843 | cf.prototype = { |
| 688 | 844 | scrollIntoView: function(){ |
| 689 | 845 | this.e.content.scrollIntoView(); |
| 690 | 846 | }, |
| 691 | 847 | setMessage: function(m){ |
| | @@ -771,14 +927,14 @@ |
| 771 | 927 | eXFrom.addEventListener('click', ()=>this.e.tab.click(), false); |
| 772 | 928 | }*/ |
| 773 | 929 | return this; |
| 774 | 930 | }, |
| 775 | 931 | /* Event handler for clicking .message-user elements to show their |
| 776 | | - timestamps. */ |
| 932 | + timestamps and a set of actions. */ |
| 777 | 933 | _handleLegendClicked: function f(ev){ |
| 778 | 934 | if(!f.popup){ |
| 779 | | - /* Timestamp popup widget */ |
| 935 | + /* "Popup" widget */ |
| 780 | 936 | f.popup = { |
| 781 | 937 | e: D.addClass(D.div(), 'chat-message-popup'), |
| 782 | 938 | refresh:function(){ |
| 783 | 939 | const eMsg = this.$eMsg/*.message-widget element*/; |
| 784 | 940 | if(!eMsg) return; |
| | @@ -843,18 +999,45 @@ |
| 843 | 999 | y: 'a' |
| 844 | 1000 | }), "User's Timeline"), |
| 845 | 1001 | 'target', '_blank' |
| 846 | 1002 | ); |
| 847 | 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*/ |
| 848 | 1029 | } |
| 849 | 1030 | const tab = eMsg.querySelector('.message-widget-tab'); |
| 850 | 1031 | D.append(tab, this.e); |
| 851 | 1032 | D.removeClass(this.e, 'hidden'); |
| 1033 | + Chat.animate(this.e, 'anim-fade-in-fast'); |
| 852 | 1034 | }/*refresh()*/, |
| 853 | 1035 | hide: function(){ |
| 854 | | - D.addClass(D.clearElement(this.e), 'hidden'); |
| 855 | 1036 | delete this.$eMsg; |
| 1037 | + D.addClass(this.e, 'hidden'); |
| 1038 | + D.clearElement(this.e); |
| 856 | 1039 | }, |
| 857 | 1040 | show: function(tgtMsg){ |
| 858 | 1041 | if(tgtMsg === this.$eMsg){ |
| 859 | 1042 | this.hide(); |
| 860 | 1043 | return; |
| | @@ -1034,16 +1217,10 @@ |
| 1034 | 1217 | e.preventDefault(); |
| 1035 | 1218 | Chat.submitMessage(); |
| 1036 | 1219 | return false; |
| 1037 | 1220 | }); |
| 1038 | 1221 | |
| 1039 | | - /* Returns an almost-ISO8601 form of Date object d. */ |
| 1040 | | - const iso8601ish = function(d){ |
| 1041 | | - return d.toISOString() |
| 1042 | | - .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu'); |
| 1043 | | - }; |
| 1044 | | - |
| 1045 | 1222 | (function(){/*Set up #chat-settings-button */ |
| 1046 | 1223 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1047 | 1224 | const optionsMenu = E1('#chat-config-options'); |
| 1048 | 1225 | const cbToggle = function(ev){ |
| 1049 | 1226 | ev.preventDefault(); |
| | @@ -1052,37 +1229,95 @@ |
| 1052 | 1229 | ? Chat.e.viewMessages : Chat.e.viewConfig); |
| 1053 | 1230 | return false; |
| 1054 | 1231 | }; |
| 1055 | 1232 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 1056 | 1233 | Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false); |
| 1057 | | - /* Settings menu entries... */ |
| 1234 | + |
| 1235 | + /** Internal acrobatics to allow certain settings toggles to access |
| 1236 | + related toggles. */ |
| 1237 | + const namedOptions = { |
| 1238 | + activeUsers:{ |
| 1239 | + label: "Show active users list", |
| 1240 | + boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'), |
| 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 | + Chat.updateActiveUserList(); |
| 1254 | + Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v'); |
| 1255 | + } |
| 1256 | + } |
| 1257 | + } |
| 1258 | + }; |
| 1259 | + if(1){ |
| 1260 | + /* Per user request, toggle the list of users on and off if the |
| 1261 | + legend element is tapped. */ |
| 1262 | + const optAu = namedOptions.activeUsers; |
| 1263 | + optAu.theLegend = Chat.e.activeUserListWrapper.firstElementChild/*LEGEND*/; |
| 1264 | + optAu.theList = optAu.theLegend.nextElementSibling/*user list container*/; |
| 1265 | + optAu.theLegend.addEventListener('click',function(){ |
| 1266 | + D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed'); |
| 1267 | + if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){ |
| 1268 | + Chat.animate(optAu.theList,'anim-flip-v'); |
| 1269 | + } |
| 1270 | + }, false); |
| 1271 | + }/*namedOptions.activeUsers additional setup*/ |
| 1272 | + /* Settings menu entries... Remember that they will be rendered in |
| 1273 | + reverse order and the most frequently-needed ones "should" |
| 1274 | + (arguably) be closer to the start of this list so that they |
| 1275 | + will be rendered within easier reach of the settings button. */ |
| 1058 | 1276 | const settingsOps = [{ |
| 1059 | 1277 | label: "Multi-line input", |
| 1060 | 1278 | boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1061 | 1279 | persistentSetting: 'edit-multiline', |
| 1062 | 1280 | callback: function(){ |
| 1063 | 1281 | Chat.inputToggleSingleMulti(); |
| 1064 | 1282 | } |
| 1065 | 1283 | },{ |
| 1066 | | - label: "Monospace message font", |
| 1067 | | - boolValue: ()=>document.body.classList.contains('monospace-messages'), |
| 1068 | | - persistentSetting: 'monospace-messages', |
| 1069 | | - callback: function(){ |
| 1070 | | - document.body.classList.toggle('monospace-messages'); |
| 1284 | + label: "Left-align my posts", |
| 1285 | + boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1286 | + callback: function f(){ |
| 1287 | + document.body.classList.toggle('my-messages-right'); |
| 1071 | 1288 | } |
| 1072 | 1289 | },{ |
| 1073 | | - label: "Images inline", |
| 1290 | + label: "Show images inline", |
| 1074 | 1291 | boolValue: ()=>Chat.settings.getBool('images-inline'), |
| 1075 | 1292 | callback: function(){ |
| 1076 | 1293 | const v = Chat.settings.toggle('images-inline'); |
| 1077 | 1294 | F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+"."); |
| 1078 | 1295 | } |
| 1079 | 1296 | },{ |
| 1080 | | - label: "Left-align my posts", |
| 1081 | | - boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1082 | | - callback: function f(){ |
| 1083 | | - document.body.classList.toggle('my-messages-right'); |
| 1297 | + label: "Timestamps in active users list", |
| 1298 | + boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'), |
| 1299 | + persistentSetting: 'active-user-list-timestamps', |
| 1300 | + callback: function(){ |
| 1301 | + D.toggleClass(Chat.e.activeUserList,'timestamps'); |
| 1302 | + /* If the timestamp option is activated but |
| 1303 | + namedOptions.activeUsers is not currently checked then |
| 1304 | + toggle that option on as well. */ |
| 1305 | + if(Chat.e.activeUserList.classList.contains('timestamps') |
| 1306 | + && !namedOptions.activeUsers.boolValue()){ |
| 1307 | + namedOptions.activeUsers.checkbox.checked = true; |
| 1308 | + namedOptions.activeUsers.callback(); |
| 1309 | + Chat.settings.set(namedOptions.activeUsers.persistentSetting, true); |
| 1310 | + } |
| 1311 | + } |
| 1312 | + }, |
| 1313 | + namedOptions.activeUsers,{ |
| 1314 | + label: "Monospace message font", |
| 1315 | + boolValue: ()=>document.body.classList.contains('monospace-messages'), |
| 1316 | + persistentSetting: 'monospace-messages', |
| 1317 | + callback: function(){ |
| 1318 | + document.body.classList.toggle('monospace-messages'); |
| 1084 | 1319 | } |
| 1085 | 1320 | },{ |
| 1086 | 1321 | label: "Chat-only mode", |
| 1087 | 1322 | boolValue: ()=>Chat.isChatOnlyMode(), |
| 1088 | 1323 | persistentSetting: 'chat-only-mode', |
| | @@ -1139,12 +1374,13 @@ |
| 1139 | 1374 | D.append(line, btn, op.select); |
| 1140 | 1375 | op.select.addEventListener('change', callback, false); |
| 1141 | 1376 | }else if(op.hasOwnProperty('boolValue')){ |
| 1142 | 1377 | if(undefined === f.$id) f.$id = 0; |
| 1143 | 1378 | ++f.$id; |
| 1144 | | - const check = D.attr(D.checkbox(1, op.boolValue()), |
| 1145 | | - 'aria-label', op.label); |
| 1379 | + const check = op.checkbox |
| 1380 | + = D.attr(D.checkbox(1, op.boolValue()), |
| 1381 | + 'aria-label', op.label); |
| 1146 | 1382 | const id = 'cfgopt'+f.$id; |
| 1147 | 1383 | if(op.boolValue()) check.checked = true; |
| 1148 | 1384 | D.attr(check, 'id', id); |
| 1149 | 1385 | D.attr(btn, 'for', id); |
| 1150 | 1386 | D.append(line, check); |
| | @@ -1228,10 +1464,18 @@ |
| 1228 | 1464 | should only be true when loading older messages. */ |
| 1229 | 1465 | f.processPost = function(m,atEnd){ |
| 1230 | 1466 | ++Chat.totalMessageCount; |
| 1231 | 1467 | if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid; |
| 1232 | 1468 | if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid; |
| 1469 | + if(m.xfrom && m.mtime){ |
| 1470 | + const d = new Date(m.mtime); |
| 1471 | + const uls = Chat.usersLastSeen[m.xfrom]; |
| 1472 | + if(!uls || uls<d){ |
| 1473 | + d.$uColor = m.uclr; |
| 1474 | + Chat.usersLastSeen[m.xfrom] = d; |
| 1475 | + } |
| 1476 | + } |
| 1233 | 1477 | if( m.mdel ){ |
| 1234 | 1478 | /* A record deletion notice. */ |
| 1235 | 1479 | Chat.deleteMessageElem(m.mdel); |
| 1236 | 1480 | return; |
| 1237 | 1481 | } |
| | @@ -1244,10 +1488,11 @@ |
| 1244 | 1488 | Chat._gotServerError = m; |
| 1245 | 1489 | } |
| 1246 | 1490 | }/*processPost()*/; |
| 1247 | 1491 | }/*end static init*/ |
| 1248 | 1492 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 1493 | + Chat.updateActiveUserList(); |
| 1249 | 1494 | if('visible'===document.visibilityState){ |
| 1250 | 1495 | if(Chat.changesSincePageHidden){ |
| 1251 | 1496 | Chat.changesSincePageHidden = 0; |
| 1252 | 1497 | Chat.e.pageTitle.innerText = Chat.pageTitleOrig; |
| 1253 | 1498 | } |
| | @@ -1288,10 +1533,11 @@ |
| 1288 | 1533 | }, |
| 1289 | 1534 | onload:function(x){ |
| 1290 | 1535 | let gotMessages = x.msgs.length; |
| 1291 | 1536 | newcontent(x,true); |
| 1292 | 1537 | Chat._isBatchLoading = false; |
| 1538 | + Chat.updateActiveUserList(); |
| 1293 | 1539 | if(Chat._gotServerError){ |
| 1294 | 1540 | Chat._gotServerError = false; |
| 1295 | 1541 | return; |
| 1296 | 1542 | } |
| 1297 | 1543 | if(n<0/*we asked for all history*/ |
| | @@ -1381,11 +1627,14 @@ |
| 1381 | 1627 | resumed, and reportError() produces a loud error message. */ |
| 1382 | 1628 | afterFetch(); |
| 1383 | 1629 | }, |
| 1384 | 1630 | onload:function(y){ |
| 1385 | 1631 | newcontent(y); |
| 1386 | | - Chat._isBatchLoading = false; |
| 1632 | + if(Chat._isBatchLoading){ |
| 1633 | + Chat._isBatchLoading = false; |
| 1634 | + Chat.updateActiveUserList(); |
| 1635 | + } |
| 1387 | 1636 | afterFetch(); |
| 1388 | 1637 | } |
| 1389 | 1638 | }); |
| 1390 | 1639 | }; |
| 1391 | 1640 | poll.isFirstCall = true; |
| | @@ -1392,8 +1641,15 @@ |
| 1392 | 1641 | Chat._gotServerError = poll.running = false; |
| 1393 | 1642 | if( window.fossil.config.chat.fromcli ){ |
| 1394 | 1643 | Chat.chatOnlyMode(true); |
| 1395 | 1644 | } |
| 1396 | 1645 | Chat.intervalTimer = setInterval(poll, 1000); |
| 1646 | + if(0){ |
| 1647 | + const flip = (ev)=>Chat.animate(ev.target,'anim-flip-h'); |
| 1648 | + document.querySelectorAll('#chat-edit-buttons button').forEach(function(e){ |
| 1649 | + e.addEventListener('click',flip, false); |
| 1650 | + }); |
| 1651 | + } |
| 1397 | 1652 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 1653 | + Chat.animate.$disabled = false; |
| 1398 | 1654 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 1399 | 1655 | })(); |
| 1400 | 1656 | |