| | @@ -127,13 +127,12 @@ |
| 127 | 127 | inputWrapper: E1("#chat-input-area"), |
| 128 | 128 | inputLine: E1('#chat-input-line'), |
| 129 | 129 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 130 | 130 | viewMessages: E1('#chat-messages-wrapper'), |
| 131 | 131 | btnSubmit: E1('#chat-message-submit'), |
| 132 | | - inputSingle: E1('#chat-input-single'), |
| 133 | | - inputMulti: E1('#chat-input-multi'), |
| 134 | | - inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 132 | + btnAttach: E1('#chat-message-attach'), |
| 133 | + inputField: E1('#chat-input-field'), |
| 135 | 134 | inputFile: E1('#chat-input-file'), |
| 136 | 135 | contentDiv: E1('div.content'), |
| 137 | 136 | viewConfig: E1('#chat-config'), |
| 138 | 137 | viewPreview: E1('#chat-preview'), |
| 139 | 138 | previewContent: E1('#chat-preview-content'), |
| | @@ -169,56 +168,23 @@ |
| 169 | 168 | taking into account single- vs multi-line input. The getter returns |
| 170 | 169 | a string and the setter returns this object. */ |
| 171 | 170 | inputValue: function(){ |
| 172 | 171 | const e = this.inputElement(); |
| 173 | 172 | if(arguments.length){ |
| 174 | | - e.value = arguments[0]; |
| 173 | + e.innerText = arguments[0]; |
| 175 | 174 | return this; |
| 176 | 175 | } |
| 177 | | - return e.value; |
| 176 | + return e.innerText; |
| 178 | 177 | }, |
| 179 | 178 | /** Asks the current user input field to take focus. Returns this. */ |
| 180 | 179 | inputFocus: function(){ |
| 181 | 180 | this.inputElement().focus(); |
| 182 | 181 | return this; |
| 183 | 182 | }, |
| 184 | 183 | /** Returns the current message input element. */ |
| 185 | 184 | inputElement: function(){ |
| 186 | | - return this.e.inputCurrent; |
| 187 | | - }, |
| 188 | | - /** Toggles between single- and multi-line edit modes. Returns this. */ |
| 189 | | - inputToggleSingleMulti: function(){ |
| 190 | | - const old = this.e.inputCurrent; |
| 191 | | - if(this.e.inputCurrent === this.e.inputSingle){ |
| 192 | | - this.e.inputCurrent = this.e.inputMulti; |
| 193 | | - this.e.inputLine.classList.remove('single-line'); |
| 194 | | - }else{ |
| 195 | | - this.e.inputCurrent = this.e.inputSingle; |
| 196 | | - this.e.inputLine.classList.add('single-line'); |
| 197 | | - } |
| 198 | | - const m = this.e.viewMessages, |
| 199 | | - sTop = m.scrollTop, |
| 200 | | - mh1 = m.clientHeight; |
| 201 | | - D.addClass(old, 'hidden'); |
| 202 | | - D.removeClass(this.e.inputCurrent, 'hidden'); |
| 203 | | - const mh2 = m.clientHeight; |
| 204 | | - m.scrollTo(0, sTop + (mh1-mh2)); |
| 205 | | - this.e.inputCurrent.value = old.value; |
| 206 | | - old.value = ''; |
| 207 | | - return this; |
| 208 | | - }, |
| 209 | | - /** |
| 210 | | - If passed true or no arguments, switches to multi-line mode |
| 211 | | - if currently in single-line mode. If passed false, switches |
| 212 | | - to single-line mode if currently in multi-line mode. Returns |
| 213 | | - this. |
| 214 | | - */ |
| 215 | | - inputMultilineMode: function(yes){ |
| 216 | | - if(!arguments.length) yes = true; |
| 217 | | - if(yes && this.e.inputCurrent === this.e.inputMulti) return this; |
| 218 | | - else if(!yes && this.e.inputCurrent === this.e.inputSingle) return this; |
| 219 | | - else return this.inputToggleSingleMulti(); |
| 185 | + return this.e.inputField; |
| 220 | 186 | }, |
| 221 | 187 | /** Enables (if yes is truthy) or disables all elements in |
| 222 | 188 | * this.disableDuringAjax. */ |
| 223 | 189 | enableAjaxComponents: function(yes){ |
| 224 | 190 | D[yes ? 'enable' : 'disable'](this.disableDuringAjax); |
| | @@ -392,26 +358,62 @@ |
| 392 | 358 | return e ? overlapsElemView(e, this.e.viewMessages) : false; |
| 393 | 359 | }, |
| 394 | 360 | settings:{ |
| 395 | 361 | get: (k,dflt)=>F.storage.get(k,dflt), |
| 396 | 362 | getBool: (k,dflt)=>F.storage.getBool(k,dflt), |
| 397 | | - set: (k,v)=>F.storage.set(k,v), |
| 363 | + set: function(k,v){ |
| 364 | + F.storage.set(k,v); |
| 365 | + F.page.dispatchEvent('chat-setting',{key: k, value: v}); |
| 366 | + }, |
| 398 | 367 | /* Toggles the boolean setting specified by k. Returns the |
| 399 | 368 | new value.*/ |
| 400 | 369 | toggle: function(k){ |
| 401 | 370 | const v = this.getBool(k); |
| 402 | 371 | this.set(k, !v); |
| 403 | 372 | return !v; |
| 404 | 373 | }, |
| 374 | + addListener: function(setting, f){ |
| 375 | + F.page.addEventListener('chat-setting', function(ev){ |
| 376 | + if(ev.detail.key===setting) f(ev.detail); |
| 377 | + }, false); |
| 378 | + }, |
| 379 | + /* Default values of settings. These are used for intializing |
| 380 | + the setting event listeners and config view UI. */ |
| 405 | 381 | defaults:{ |
| 382 | + /* When on, inbound images are displayed inlined, else as a |
| 383 | + link to download the image. */ |
| 406 | 384 | "images-inline": !!F.config.chat.imagesInline, |
| 407 | | - "edit-multiline": false, |
| 408 | | - "monospace-messages": false, |
| 385 | + /* When on, ctrl-enter sends messages, else enter and |
| 386 | + ctrl-enter both send them. */ |
| 387 | + "edit-ctrl-send": false, |
| 388 | + /* When on, the edit field starts as a single line and |
| 389 | + expands as the user types, and the relevant buttons are |
| 390 | + laid out in a compact form. When off, the edit field and |
| 391 | + buttons are larger. */ |
| 392 | + "edit-compact-mode": true, |
| 393 | + /* When on, sets the font-family on messages and the edit |
| 394 | + field to monospace. */ |
| 395 | + "monospace-messages": true, |
| 396 | + /* When on, non-chat UI elements (page header/footer) are |
| 397 | + hidden */ |
| 409 | 398 | "chat-only-mode": false, |
| 399 | + /* When set to a URI, it is assumed to be an audio file, |
| 400 | + which gets played when new messages arrive. When true, |
| 401 | + the first entry in the audio file selection list will be |
| 402 | + used. */ |
| 410 | 403 | "audible-alert": true, |
| 404 | + /* When on, show the list of "active" users - those from |
| 405 | + whom we have messages in the currently-loaded history |
| 406 | + (noting that deletions are also messages). */ |
| 411 | 407 | "active-user-list": false, |
| 412 | | - "active-user-list-timestamps": false |
| 408 | + /* When on, the [active-user-list] setting includes the |
| 409 | + timestamp of each user's most recent message. */ |
| 410 | + "active-user-list-timestamps": false, |
| 411 | + /* When on, the [audible-alert] is played for one's own |
| 412 | + messages, else it is only played for other users' |
| 413 | + messages. */ |
| 414 | + "alert-own-messages": false |
| 413 | 415 | } |
| 414 | 416 | }, |
| 415 | 417 | /** Plays a new-message notification sound IF the audible-alert |
| 416 | 418 | setting is true, else this is a no-op. Returns this. |
| 417 | 419 | */ |
| | @@ -420,11 +422,11 @@ |
| 420 | 422 | try{ |
| 421 | 423 | if(!f.audio) f.audio = new Audio(f.uri); |
| 422 | 424 | f.audio.currentTime = 0; |
| 423 | 425 | f.audio.play(); |
| 424 | 426 | }catch(e){ |
| 425 | | - console.error("Audio playblack failed.",e); |
| 427 | + console.error("Audio playblack failed.", f.uri, e); |
| 426 | 428 | } |
| 427 | 429 | } |
| 428 | 430 | return this; |
| 429 | 431 | }, |
| 430 | 432 | /** |
| | @@ -449,11 +451,13 @@ |
| 449 | 451 | return e; |
| 450 | 452 | } |
| 451 | 453 | this.e.views.forEach(function(E){ |
| 452 | 454 | if(e!==E) D.addClass(E,'hidden'); |
| 453 | 455 | }); |
| 454 | | - this.e.currentView = D.removeClass(e,'hidden'); |
| 456 | + this.e.currentView = e; |
| 457 | + if(this.e.currentView.$beforeShow) this.e.currentView.$beforeShow(); |
| 458 | + D.removeClass(e,'hidden'); |
| 455 | 459 | this.animate(this.e.currentView, 'anim-fade-in-fast'); |
| 456 | 460 | return this.e.currentView; |
| 457 | 461 | }, |
| 458 | 462 | /** |
| 459 | 463 | Updates the "active user list" view if we are not currently |
| | @@ -499,10 +503,35 @@ |
| 499 | 503 | Object.keys(this.usersLastSeen).sort( |
| 500 | 504 | callee.sortUsersSeen |
| 501 | 505 | ).forEach(callee.addUserElem); |
| 502 | 506 | return this; |
| 503 | 507 | }, |
| 508 | + /** Show or hide the active user list. Returns this object. */ |
| 509 | + showActiveUserList: function(yes){ |
| 510 | + if(0===arguments.length) yes = true; |
| 511 | + this.e.activeUserListWrapper.classList[ |
| 512 | + yes ? 'remove' : 'add' |
| 513 | + ]('hidden'); |
| 514 | + D.removeClass(Chat.e.activeUserListWrapper, 'collapsed'); |
| 515 | + if(Chat.e.activeUserListWrapper.classList.contains('hidden')){ |
| 516 | + /* When hiding this element, undo all filtering */ |
| 517 | + Chat.setUserFilter(false); |
| 518 | + /*Ideally we'd scroll the final message into view |
| 519 | + now, but because viewMessages is currently hidden behind |
| 520 | + viewConfig, scrolling is a no-op. */ |
| 521 | + Chat.scrollMessagesTo(1); |
| 522 | + }else{ |
| 523 | + Chat.updateActiveUserList(); |
| 524 | + Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v'); |
| 525 | + } |
| 526 | + return this; |
| 527 | + }, |
| 528 | + showActiveUserTimestamps: function(yes){ |
| 529 | + if(0===arguments.length) yes = true; |
| 530 | + this.e.activeUserList.classList[yes ? 'add' : 'remove']('timestamps'); |
| 531 | + return this; |
| 532 | + }, |
| 504 | 533 | /** |
| 505 | 534 | Applies user name filter to all current messages, or clears |
| 506 | 535 | the filter if uname is falsy. |
| 507 | 536 | */ |
| 508 | 537 | setUserFilter: function(uname){ |
| | @@ -542,38 +571,18 @@ |
| 542 | 571 | D.addClassBriefly(e, a, 0, cb); |
| 543 | 572 | } |
| 544 | 573 | return this; |
| 545 | 574 | } |
| 546 | 575 | }; |
| 576 | + if(!D.attr(cs.e.inputField,'contenteditable','plaintext-only').isContentEditable){ |
| 577 | + /* Only the Chrome family supports contenteditable=plaintext-only, |
| 578 | + but Chrome is the only engine for which we need this flag: */ |
| 579 | + D.attr(cs.e.inputField,'contenteditable','true'); |
| 580 | + } |
| 547 | 581 | cs.animate.$disabled = true; |
| 548 | 582 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 549 | 583 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 550 | | - cs.e.inputCurrent = cs.e.inputSingle; |
| 551 | | - /* Install default settings... */ |
| 552 | | - Object.keys(cs.settings.defaults).forEach(function(k){ |
| 553 | | - const v = cs.settings.get(k,cs); |
| 554 | | - if(cs===v) cs.settings.set(k,cs.settings.defaults[k]); |
| 555 | | - }); |
| 556 | | - if(window.innerWidth<window.innerHeight){ |
| 557 | | - /* Alignment of 'my' messages: right alignment is conventional |
| 558 | | - for mobile chat apps but can be difficult to read in wide |
| 559 | | - windows (desktop/tablet landscape mode), so we default to a |
| 560 | | - layout based on the apparent "orientation" of the window: |
| 561 | | - tall vs wide. Can be toggled via settings popup. */ |
| 562 | | - document.body.classList.add('my-messages-right'); |
| 563 | | - } |
| 564 | | - if(cs.settings.getBool('monospace-messages',false)){ |
| 565 | | - document.body.classList.add('monospace-messages'); |
| 566 | | - } |
| 567 | | - if(cs.settings.getBool('active-user-list',false)){ |
| 568 | | - cs.e.activeUserListWrapper.classList.remove('hidden'); |
| 569 | | - } |
| 570 | | - if(cs.settings.getBool('active-user-list-timestamps',false)){ |
| 571 | | - cs.e.activeUserList.classList.add('timestamps'); |
| 572 | | - } |
| 573 | | - cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false)); |
| 574 | | - cs.chatOnlyMode(cs.settings.getBool('chat-only-mode')); |
| 575 | 584 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 576 | 585 | const qs = (e)=>document.querySelector(e); |
| 577 | 586 | const argsToArray = function(args){ |
| 578 | 587 | return Array.prototype.slice.call(args,0); |
| 579 | 588 | }; |
| | @@ -1054,11 +1063,11 @@ |
| 1054 | 1063 | }/*_handleLegendClicked()*/ |
| 1055 | 1064 | }; |
| 1056 | 1065 | return cf; |
| 1057 | 1066 | })()/*MessageWidget*/; |
| 1058 | 1067 | |
| 1059 | | - const BlobXferState = (function(){/*drag/drop bits...*/ |
| 1068 | + const BlobXferState = (function(){ |
| 1060 | 1069 | /* State for paste and drag/drop */ |
| 1061 | 1070 | const bxs = { |
| 1062 | 1071 | dropDetails: document.querySelector('#chat-drop-details'), |
| 1063 | 1072 | blob: undefined, |
| 1064 | 1073 | clear: function(){ |
| | @@ -1069,75 +1078,83 @@ |
| 1069 | 1078 | }; |
| 1070 | 1079 | /** Updates the paste/drop zone with details of the pasted/dropped |
| 1071 | 1080 | data. The argument must be a Blob or Blob-like object (File) or |
| 1072 | 1081 | it can be falsy to reset/clear that state.*/ |
| 1073 | 1082 | const updateDropZoneContent = function(blob){ |
| 1083 | + //console.debug("updateDropZoneContent()",blob); |
| 1074 | 1084 | const dd = bxs.dropDetails; |
| 1075 | 1085 | bxs.blob = blob; |
| 1076 | 1086 | D.clearElement(dd); |
| 1077 | 1087 | if(!blob){ |
| 1078 | 1088 | Chat.e.inputFile.value = ''; |
| 1079 | 1089 | return; |
| 1080 | 1090 | } |
| 1081 | | - D.append(dd, "Name: ", blob.name, |
| 1091 | + D.append(dd, "Attached: ", blob.name, |
| 1082 | 1092 | D.br(), "Size: ",blob.size); |
| 1083 | | - if(blob.type && blob.type.startsWith("image/")){ |
| 1093 | + const btn = D.button("Cancel"); |
| 1094 | + D.append(dd, D.br(), btn); |
| 1095 | + btn.addEventListener('click', ()=>updateDropZoneContent(), false); |
| 1096 | + if(blob.type && (blob.type.startsWith("image/") || blob.type==='BITMAP')){ |
| 1084 | 1097 | const img = D.img(); |
| 1085 | 1098 | D.append(dd, D.br(), img); |
| 1086 | 1099 | const reader = new FileReader(); |
| 1087 | 1100 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| 1088 | 1101 | reader.readAsDataURL(blob); |
| 1089 | 1102 | } |
| 1090 | | - const btn = D.button("Cancel"); |
| 1091 | | - D.append(dd, D.br(), btn); |
| 1092 | | - btn.addEventListener('click', ()=>updateDropZoneContent(), false); |
| 1093 | 1103 | }; |
| 1094 | 1104 | Chat.e.inputFile.addEventListener('change', function(ev){ |
| 1095 | 1105 | updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined) |
| 1096 | 1106 | }); |
| 1097 | 1107 | /* Handle image paste from clipboard. TODO: figure out how we can |
| 1098 | 1108 | paste non-image binary data as if it had been selected via the |
| 1099 | 1109 | file selection element. */ |
| 1100 | | - document.addEventListener('paste', function(event){ |
| 1110 | + const pasteListener = function(event){ |
| 1101 | 1111 | const items = event.clipboardData.items, |
| 1102 | 1112 | item = items[0]; |
| 1103 | | - if(!item || !item.type) return; |
| 1104 | | - else if('file'===item.kind){ |
| 1113 | + //console.debug("paste event",event.target,item,event); |
| 1114 | + //console.debug("paste event item",item); |
| 1115 | + if(item && item.type && ('file'===item.kind || 'BITMAP'===item.type)){ |
| 1105 | 1116 | updateDropZoneContent(false/*clear prev state*/); |
| 1106 | 1117 | updateDropZoneContent(item.getAsFile()); |
| 1107 | | - } |
| 1108 | | - }, false); |
| 1109 | | - /* Add help button for drag/drop/paste zone */ |
| 1110 | | - Chat.e.inputFile.parentNode.insertBefore( |
| 1111 | | - F.helpButtonlets.create( |
| 1112 | | - Chat.e.fileSelectWrapper.querySelector('.help-buttonlet') |
| 1113 | | - ), Chat.e.inputFile |
| 1114 | | - ); |
| 1115 | | - //////////////////////////////////////////////////////////// |
| 1116 | | - // File drag/drop visual notification. |
| 1117 | | - const dropHighlight = Chat.e.inputFile /* target zone */; |
| 1118 | | - const dropEvents = { |
| 1119 | | - drop: function(ev){ |
| 1120 | | - D.removeClass(dropHighlight, 'dragover'); |
| 1121 | | - }, |
| 1122 | | - dragenter: function(ev){ |
| 1118 | + event.stopPropagation(); |
| 1119 | + event.preventDefault(true); |
| 1120 | + return false; |
| 1121 | + } |
| 1122 | + /* else continue propagating */ |
| 1123 | + }; |
| 1124 | + document.addEventListener('paste', pasteListener, true); |
| 1125 | + if(0){ |
| 1126 | + const onPastePlainText = function(ev){ |
| 1127 | + var pastedText = undefined; |
| 1128 | + if (window.clipboardData && window.clipboardData.getData) { // IE |
| 1129 | + pastedText = window.clipboardData.getData('Text'); |
| 1130 | + }else if (ev.clipboardData && ev.clipboardData.getData) { |
| 1131 | + pastedText = ev.clipboardData.getData('text/plain'); |
| 1132 | + } |
| 1133 | + ev.target.textContent += pastedText; |
| 1123 | 1134 | ev.preventDefault(); |
| 1124 | | - ev.dataTransfer.dropEffect = "copy"; |
| 1125 | | - D.addClass(dropHighlight, 'dragover'); |
| 1126 | | - }, |
| 1127 | | - dragleave: function(ev){ |
| 1128 | | - D.removeClass(dropHighlight, 'dragover'); |
| 1129 | | - }, |
| 1130 | | - dragend: function(ev){ |
| 1131 | | - D.removeClass(dropHighlight, 'dragover'); |
| 1132 | | - } |
| 1135 | + return false; |
| 1136 | + }; |
| 1137 | + Chat.e.inputField.addEventListener('paste', onPastePlainText, false); |
| 1138 | + } |
| 1139 | + const noDragDropEvents = function(ev){ |
| 1140 | + /* contenteditable tries to do its own thing with dropped data, |
| 1141 | + which is not compatible with how we use it, so... */ |
| 1142 | + ev.dataTransfer.effectAllowed = 'none'; |
| 1143 | + ev.dataTransfer.dropEffect = 'none'; |
| 1144 | + ev.preventDefault(); |
| 1145 | + ev.stopPropagation(); |
| 1146 | + return false; |
| 1133 | 1147 | }; |
| 1134 | | - Object.keys(dropEvents).forEach( |
| 1135 | | - (k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true) |
| 1148 | + |
| 1149 | + ['drop','dragenter','dragleave','dragend'].forEach( |
| 1150 | + (k)=>{ |
| 1151 | + Chat.inputElement().addEventListener(k, noDragDropEvents, false); |
| 1152 | + } |
| 1136 | 1153 | ); |
| 1137 | 1154 | return bxs; |
| 1138 | | - })()/*drag/drop*/; |
| 1155 | + })()/*drag/drop/paste*/; |
| 1139 | 1156 | |
| 1140 | 1157 | const tzOffsetToString = function(off){ |
| 1141 | 1158 | const hours = Math.round(off/60), min = Math.round(off % 30); |
| 1142 | 1159 | return ''+(hours + (min ? '.5' : '')); |
| 1143 | 1160 | }; |
| | @@ -1155,21 +1172,30 @@ |
| 1155 | 1172 | empty, this is a no-op. |
| 1156 | 1173 | */ |
| 1157 | 1174 | Chat.submitMessage = function f(){ |
| 1158 | 1175 | if(!f.spaces){ |
| 1159 | 1176 | f.spaces = /\s+$/; |
| 1177 | + f.markdownContinuation = /\\\s+$/; |
| 1160 | 1178 | } |
| 1161 | 1179 | this.setCurrentView(this.e.viewMessages); |
| 1162 | 1180 | const fd = new FormData(); |
| 1163 | 1181 | var msg = this.inputValue().trim(); |
| 1164 | 1182 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 1165 | 1183 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 1166 | 1184 | keep copy/paste from terminals, especially wide ones, from |
| 1167 | | - forcing a horizontal scrollbar on all clients. */ |
| 1185 | + forcing a horizontal scrollbar on all clients. This breaks |
| 1186 | + markdown's use of blackslash-space-space for paragraph |
| 1187 | + continuation, but *not* doing this affects all clients every |
| 1188 | + time someone pastes in console copy/paste from an affected |
| 1189 | + platform. We seem to have narrowed to the console pasting |
| 1190 | + problem to users of tmux. Most consoles don't behave |
| 1191 | + that way. */ |
| 1168 | 1192 | const xmsg = msg.split('\n'); |
| 1169 | 1193 | xmsg.forEach(function(line,ndx){ |
| 1170 | | - xmsg[ndx] = line.trimRight(); |
| 1194 | + if(!f.markdownContinuation.test(line)){ |
| 1195 | + xmsg[ndx] = line.trimRight(); |
| 1196 | + } |
| 1171 | 1197 | }); |
| 1172 | 1198 | msg = xmsg.join('\n'); |
| 1173 | 1199 | } |
| 1174 | 1200 | if(msg) fd.set('msg',msg); |
| 1175 | 1201 | const file = BlobXferState.blob || this.e.inputFile.files[0]; |
| | @@ -1194,47 +1220,90 @@ |
| 1194 | 1220 | }); |
| 1195 | 1221 | BlobXferState.clear(); |
| 1196 | 1222 | Chat.inputValue("").inputFocus(); |
| 1197 | 1223 | }; |
| 1198 | 1224 | |
| 1199 | | - const inputWidgetKeydown = function(ev){ |
| 1200 | | - if(13 === ev.keyCode){ |
| 1201 | | - if(ev.shiftKey){ |
| 1202 | | - ev.preventDefault(); |
| 1203 | | - ev.stopPropagation(); |
| 1204 | | - /* Shift-enter will run preview mode UNLESS preview mode is |
| 1205 | | - active AND the input field is empty, in which case it will |
| 1206 | | - switch back to message view. */ |
| 1207 | | - if(Chat.e.currentView===Chat.e.viewPreview |
| 1208 | | - && !Chat.e.inputCurrent.value){ |
| 1209 | | - Chat.setCurrentView(Chat.e.viewMessages); |
| 1210 | | - }else{ |
| 1211 | | - Chat.e.btnPreview.click(); |
| 1212 | | - } |
| 1213 | | - return false; |
| 1214 | | - }else if((Chat.e.inputSingle===ev.target) |
| 1215 | | - || (ev.ctrlKey && Chat.e.inputMulti===ev.target)){ |
| 1216 | | - /* ^^^ note that it is intended that both ctrl-enter and enter |
| 1217 | | - work for single-line input mode. */ |
| 1218 | | - ev.preventDefault(); |
| 1219 | | - ev.stopPropagation(); |
| 1220 | | - Chat.submitMessage(); |
| 1221 | | - return false; |
| 1222 | | - } |
| 1225 | + const inputWidgetKeydown = function f(ev){ |
| 1226 | + if(!f.$toggleCtrl){ |
| 1227 | + f.$toggleCtrl = function(currentMode){ |
| 1228 | + currentMode = !currentMode; |
| 1229 | + Chat.settings.set('edit-ctrl-send', currentMode); |
| 1230 | + }; |
| 1231 | + f.$toggleCompact = function(currentMode){ |
| 1232 | + currentMode = !currentMode; |
| 1233 | + Chat.settings.set('edit-compact-mode', currentMode); |
| 1234 | + }; |
| 1235 | + } |
| 1236 | + if(13 !== ev.keyCode) return; |
| 1237 | + const text = Chat.inputValue().trim(); |
| 1238 | + const ctrlMode = Chat.settings.getBool('edit-ctrl-send', false); |
| 1239 | + //console.debug("Enter key event:", ctrlMode, ev.ctrlKey, ev.shiftKey, ev); |
| 1240 | + if(ev.shiftKey){ |
| 1241 | + const compactMode = Chat.settings.getBool('edit-compact-mode', false); |
| 1242 | + ev.preventDefault(); |
| 1243 | + ev.stopPropagation(); |
| 1244 | + /* Shift-enter will run preview mode UNLESS preview mode is |
| 1245 | + active AND the input field is empty, in which case it will |
| 1246 | + switch back to message view. */ |
| 1247 | + if(Chat.e.currentView===Chat.e.viewPreview && !text){ |
| 1248 | + Chat.setCurrentView(Chat.e.viewMessages); |
| 1249 | + }else if(!text){ |
| 1250 | + f.$toggleCompact(compactMode); |
| 1251 | + }else{ |
| 1252 | + Chat.e.btnPreview.click(); |
| 1253 | + } |
| 1254 | + return false; |
| 1255 | + } |
| 1256 | + if(ev.ctrlKey && !text && !BlobXferState.blob){ |
| 1257 | + /* Ctrl-enter on empty input field(s) toggles Enter/Ctrl-enter mode */ |
| 1258 | + ev.preventDefault(); |
| 1259 | + ev.stopPropagation(); |
| 1260 | + f.$toggleCtrl(ctrlMode); |
| 1261 | + return false; |
| 1262 | + } |
| 1263 | + if(!ctrlMode && ev.ctrlKey && text){ |
| 1264 | + //console.debug("!ctrlMode && ev.ctrlKey && text."); |
| 1265 | + /* Ctrl-enter in Enter-sends mode SHOULD, with this logic add a |
| 1266 | + newline, but that is not happening, for unknown reasons |
| 1267 | + (possibly related to this element being a conteneditable DIV |
| 1268 | + instead of a textarea). Forcibly appending a newline do the |
| 1269 | + input area does not work, also for unknown reasons, and would |
| 1270 | + only be suitable when we're at the end of the input. |
| 1271 | + |
| 1272 | + Strangely, this approach DOES work for shift-enter, but we |
| 1273 | + need shift-enter as a hotkey for preview mode. |
| 1274 | + */ |
| 1275 | + //return; |
| 1276 | + // return here "should" cause newline to be added, but that doesn't work |
| 1277 | + } |
| 1278 | + if((!ctrlMode && !ev.ctrlKey) || (ev.ctrlKey/* && ctrlMode*/)){ |
| 1279 | + /* Ship it! */ |
| 1280 | + ev.preventDefault(); |
| 1281 | + ev.stopPropagation(); |
| 1282 | + Chat.submitMessage(); |
| 1283 | + return false; |
| 1223 | 1284 | } |
| 1224 | 1285 | }; |
| 1225 | | - Chat.e.inputSingle |
| 1226 | | - .addEventListener('keydown', inputWidgetKeydown, false); |
| 1227 | | - Chat.e.inputMulti |
| 1228 | | - .addEventListener('keydown', inputWidgetKeydown, false); |
| 1286 | + Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false); |
| 1229 | 1287 | Chat.e.btnSubmit.addEventListener('click',(e)=>{ |
| 1230 | 1288 | e.preventDefault(); |
| 1231 | 1289 | Chat.submitMessage(); |
| 1232 | 1290 | return false; |
| 1233 | 1291 | }); |
| 1292 | + Chat.e.btnAttach.addEventListener( |
| 1293 | + 'click', ()=>Chat.e.inputFile.click(), false); |
| 1234 | 1294 | |
| 1235 | | - (function(){/*Set up #chat-settings-button */ |
| 1295 | + (function(){/*Set up #chat-settings-button and related bits */ |
| 1296 | + if(window.innerWidth<window.innerHeight){ |
| 1297 | + // Must be set up before config view is... |
| 1298 | + /* Alignment of 'my' messages: right alignment is conventional |
| 1299 | + for mobile chat apps but can be difficult to read in wide |
| 1300 | + windows (desktop/tablet landscape mode), so we default to a |
| 1301 | + layout based on the apparent "orientation" of the window: |
| 1302 | + tall vs wide. Can be toggled via settings. */ |
| 1303 | + document.body.classList.add('my-messages-right'); |
| 1304 | + } |
| 1236 | 1305 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1237 | 1306 | const optionsMenu = E1('#chat-config-options'); |
| 1238 | 1307 | const cbToggle = function(ev){ |
| 1239 | 1308 | ev.preventDefault(); |
| 1240 | 1309 | ev.stopPropagation(); |
| | @@ -1248,27 +1317,12 @@ |
| 1248 | 1317 | /** Internal acrobatics to allow certain settings toggles to access |
| 1249 | 1318 | related toggles. */ |
| 1250 | 1319 | const namedOptions = { |
| 1251 | 1320 | activeUsers:{ |
| 1252 | 1321 | label: "Show active users list", |
| 1253 | | - boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'), |
| 1254 | | - persistentSetting: 'active-user-list', |
| 1255 | | - callback: function(){ |
| 1256 | | - D.toggleClass(Chat.e.activeUserListWrapper,'hidden'); |
| 1257 | | - D.removeClass(Chat.e.activeUserListWrapper, 'collapsed'); |
| 1258 | | - if(Chat.e.activeUserListWrapper.classList.contains('hidden')){ |
| 1259 | | - /* When hiding this element, undo all filtering */ |
| 1260 | | - Chat.setUserFilter(false); |
| 1261 | | - /*Ideally we'd scroll the final message into view |
| 1262 | | - now, but because viewMessages is currently hidden behind |
| 1263 | | - viewConfig, scrolling is a no-op. */ |
| 1264 | | - Chat.scrollMessagesTo(1); |
| 1265 | | - }else{ |
| 1266 | | - Chat.updateActiveUserList(); |
| 1267 | | - Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v'); |
| 1268 | | - } |
| 1269 | | - } |
| 1322 | + hint: "List users who have messages in the currently-loaded chat history.", |
| 1323 | + boolValue: 'active-user-list' |
| 1270 | 1324 | } |
| 1271 | 1325 | }; |
| 1272 | 1326 | if(1){ |
| 1273 | 1327 | /* Per user request, toggle the list of users on and off if the |
| 1274 | 1328 | legend element is tapped. */ |
| | @@ -1280,65 +1334,77 @@ |
| 1280 | 1334 | if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){ |
| 1281 | 1335 | Chat.animate(optAu.theList,'anim-flip-v'); |
| 1282 | 1336 | } |
| 1283 | 1337 | }, false); |
| 1284 | 1338 | }/*namedOptions.activeUsers additional setup*/ |
| 1285 | | - /* Settings menu entries... Remember that they will be rendered in |
| 1286 | | - reverse order and the most frequently-needed ones "should" |
| 1287 | | - (arguably) be closer to the start of this list so that they |
| 1288 | | - will be rendered within easier reach of the settings button. */ |
| 1289 | | - const settingsOps = [{ |
| 1290 | | - label: "Multi-line input", |
| 1291 | | - boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1292 | | - persistentSetting: 'edit-multiline', |
| 1293 | | - callback: function(){ |
| 1294 | | - Chat.inputToggleSingleMulti(); |
| 1295 | | - } |
| 1296 | | - },{ |
| 1297 | | - label: "Left-align my posts", |
| 1298 | | - boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1299 | | - callback: function f(){ |
| 1300 | | - document.body.classList.toggle('my-messages-right'); |
| 1301 | | - } |
| 1302 | | - },{ |
| 1303 | | - label: "Show images inline", |
| 1304 | | - boolValue: ()=>Chat.settings.getBool('images-inline'), |
| 1305 | | - callback: function(){ |
| 1306 | | - const v = Chat.settings.toggle('images-inline'); |
| 1307 | | - F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+"."); |
| 1308 | | - } |
| 1309 | | - },{ |
| 1310 | | - label: "Timestamps in active users list", |
| 1311 | | - boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'), |
| 1312 | | - persistentSetting: 'active-user-list-timestamps', |
| 1313 | | - callback: function(){ |
| 1314 | | - D.toggleClass(Chat.e.activeUserList,'timestamps'); |
| 1315 | | - /* If the timestamp option is activated but |
| 1316 | | - namedOptions.activeUsers is not currently checked then |
| 1317 | | - toggle that option on as well. */ |
| 1318 | | - if(Chat.e.activeUserList.classList.contains('timestamps') |
| 1319 | | - && !namedOptions.activeUsers.boolValue()){ |
| 1320 | | - namedOptions.activeUsers.checkbox.checked = true; |
| 1321 | | - namedOptions.activeUsers.callback(); |
| 1322 | | - Chat.settings.set(namedOptions.activeUsers.persistentSetting, true); |
| 1323 | | - } |
| 1324 | | - } |
| 1325 | | - }, |
| 1326 | | - namedOptions.activeUsers,{ |
| 1327 | | - label: "Monospace message font", |
| 1328 | | - boolValue: ()=>document.body.classList.contains('monospace-messages'), |
| 1329 | | - persistentSetting: 'monospace-messages', |
| 1330 | | - callback: function(){ |
| 1331 | | - document.body.classList.toggle('monospace-messages'); |
| 1339 | + /* Settings menu entries... the most frequently-needed ones "should" |
| 1340 | + (arguably) be closer to the start of this list. */ |
| 1341 | + /** |
| 1342 | + Settings ops structure: |
| 1343 | + |
| 1344 | + label: string for the UI |
| 1345 | + |
| 1346 | + boolValue: string (name of Chat.settings setting) or a |
| 1347 | + function which returns true or false. |
| 1348 | + |
| 1349 | + select: SELECT element (instead of boolValue) |
| 1350 | + |
| 1351 | + callback: optional handler to call after setting is modified. |
| 1352 | + |
| 1353 | + If a setting has a boolValue set, that gets transformed into a |
| 1354 | + checkbox which toggles the given persistent setting (if |
| 1355 | + boolValue is a string) AND listens for changes to that setting |
| 1356 | + fired via Chat.settings.set() so that the checkbox can stay in |
| 1357 | + sync with external changes to that setting. Various Chat UI |
| 1358 | + elements stay in sync with the config UI via those settings |
| 1359 | + events. |
| 1360 | + */ |
| 1361 | + const settingsOps = [{ |
| 1362 | + label: "Ctrl-enter to Send", |
| 1363 | + hint: "When on, only Ctrl-Enter will send messages and Enter adds "+ |
| 1364 | + "blank lines. "+ |
| 1365 | + "When off, both Enter and Ctrl-Enter send. "+ |
| 1366 | + "When the input field has focus, is empty, and preview "+ |
| 1367 | + "mode is NOT active then Ctrl-Enter toggles this setting.", |
| 1368 | + boolValue: 'edit-ctrl-send' |
| 1369 | + },{ |
| 1370 | + label: "Compact mode", |
| 1371 | + hint: "Toggle between a space-saving or more spacious writing area. "+ |
| 1372 | + "When the input field has focus, is empty, and preview mode "+ |
| 1373 | + "is NOT active then Shift-Enter toggles this setting.", |
| 1374 | + boolValue: 'edit-compact-mode' |
| 1375 | + },{ |
| 1376 | + label: "Left-align my posts", |
| 1377 | + hint: "Default alignment of your own messages is selected " |
| 1378 | + +"based window width/height relationship.", |
| 1379 | + boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1380 | + callback: function f(){ |
| 1381 | + document.body.classList[ |
| 1382 | + this.checkbox.checked ? 'remove' : 'add' |
| 1383 | + ]('my-messages-right'); |
| 1384 | + } |
| 1385 | + },{ |
| 1386 | + label: "Monospace message font", |
| 1387 | + hint: "Use monospace font for message text?", |
| 1388 | + boolValue: 'monospace-messages', |
| 1389 | + callback: function(setting){ |
| 1390 | + document.body.classList[ |
| 1391 | + setting.value ? 'add' : 'remove' |
| 1392 | + ]('monospace-messages'); |
| 1332 | 1393 | } |
| 1333 | 1394 | },{ |
| 1334 | 1395 | label: "Chat-only mode", |
| 1335 | | - boolValue: ()=>Chat.isChatOnlyMode(), |
| 1336 | | - persistentSetting: 'chat-only-mode', |
| 1337 | | - callback: function(){ |
| 1338 | | - Chat.toggleChatOnlyMode(); |
| 1339 | | - } |
| 1396 | + hint: "Toggle the page between normal fossil view and chat-only view.", |
| 1397 | + boolValue: 'chat-only-mode' |
| 1398 | + },{ |
| 1399 | + label: "Show images inline", |
| 1400 | + hint: "Whether to show images inline or as a hyperlink.", |
| 1401 | + boolValue: 'images-inline' |
| 1402 | + },namedOptions.activeUsers,{ |
| 1403 | + label: "Timestamps in active users list", |
| 1404 | + hint: "Whether to show last-message timestamps.", |
| 1405 | + boolValue: 'active-user-list-timestamps' |
| 1340 | 1406 | }]; |
| 1341 | 1407 | |
| 1342 | 1408 | /** Set up selection list of notification sounds. */ |
| 1343 | 1409 | if(1){ |
| 1344 | 1410 | const selectSound = D.select(); |
| | @@ -1348,90 +1414,168 @@ |
| 1348 | 1414 | if(true===Chat.settings.getBool('audible-alert')){ |
| 1349 | 1415 | /* This setting used to be a plain bool. If we encounter |
| 1350 | 1416 | such a setting, take the first sound in the list. */ |
| 1351 | 1417 | selectSound.selectedIndex = firstSoundIndex; |
| 1352 | 1418 | }else{ |
| 1353 | | - selectSound.value = Chat.settings.get('audible-alert',''); |
| 1419 | + selectSound.value = Chat.settings.get('audible-alert','<none>'); |
| 1354 | 1420 | if(selectSound.selectedIndex<0){ |
| 1355 | 1421 | /* Missing file - removed after this setting was |
| 1356 | 1422 | applied. Fall back to the first sound in the list. */ |
| 1357 | 1423 | selectSound.selectedIndex = firstSoundIndex; |
| 1358 | 1424 | } |
| 1359 | 1425 | } |
| 1360 | 1426 | Chat.setNewMessageSound(selectSound.value); |
| 1361 | 1427 | settingsOps.push({ |
| 1362 | | - label: "Audio alert", |
| 1428 | + hint: "Audio alert. How to enable audio playback is browser-specific!", |
| 1363 | 1429 | select: selectSound, |
| 1364 | 1430 | callback: function(ev){ |
| 1365 | 1431 | const v = ev.target.value; |
| 1366 | 1432 | Chat.setNewMessageSound(v); |
| 1367 | 1433 | F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); |
| 1368 | 1434 | if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); |
| 1369 | 1435 | } |
| 1370 | 1436 | }); |
| 1371 | 1437 | }/*audio notification config*/ |
| 1438 | + settingsOps.push({ |
| 1439 | + label: "Play notification for your own messages.", |
| 1440 | + hint: "When enabled, the audio notification will be played for all messages, "+ |
| 1441 | + "including your own. When disabled only messages from other users "+ |
| 1442 | + "will trigger a notification.", |
| 1443 | + boolValue: 'alert-own-messages' |
| 1444 | + }); |
| 1372 | 1445 | /** |
| 1373 | 1446 | Build UI for config options... |
| 1374 | 1447 | */ |
| 1375 | 1448 | settingsOps.forEach(function f(op){ |
| 1376 | 1449 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1377 | | - const btn = D.append( |
| 1450 | + const label = op.label ? D.append( |
| 1378 | 1451 | D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/), |
| 1379 | | - op.label); |
| 1380 | | - const callback = function(ev){ |
| 1381 | | - op.callback(ev); |
| 1382 | | - if(op.persistentSetting){ |
| 1383 | | - Chat.settings.set(op.persistentSetting, op.boolValue()); |
| 1384 | | - } |
| 1385 | | - }; |
| 1452 | + op.label) : undefined; |
| 1453 | + const labelWrapper = D.addClass(D.div(), 'label-wrapper'); |
| 1454 | + var hint; |
| 1455 | + const col0 = D.span(); |
| 1456 | + if(op.hint){ |
| 1457 | + hint = D.append(D.addClass(D.span(),'hint'),op.hint); |
| 1458 | + } |
| 1386 | 1459 | if(op.hasOwnProperty('select')){ |
| 1387 | | - D.append(line, btn, op.select); |
| 1388 | | - op.select.addEventListener('change', callback, false); |
| 1460 | + D.append(line, col0, labelWrapper); |
| 1461 | + D.append(labelWrapper, op.select); |
| 1462 | + if(hint) D.append(labelWrapper, hint); |
| 1463 | + if(label) D.append(col0, label); |
| 1464 | + if(op.callback){ |
| 1465 | + op.select.addEventListener('change', (ev)=>op.callback(ev), false); |
| 1466 | + } |
| 1389 | 1467 | }else if(op.hasOwnProperty('boolValue')){ |
| 1390 | 1468 | if(undefined === f.$id) f.$id = 0; |
| 1391 | 1469 | ++f.$id; |
| 1470 | + if('string' ===typeof op.boolValue){ |
| 1471 | + const key = op.boolValue; |
| 1472 | + op.boolValue = ()=>Chat.settings.getBool(key); |
| 1473 | + op.persistentSetting = key; |
| 1474 | + } |
| 1392 | 1475 | const check = op.checkbox |
| 1393 | 1476 | = D.attr(D.checkbox(1, op.boolValue()), |
| 1394 | 1477 | 'aria-label', op.label); |
| 1395 | 1478 | const id = 'cfgopt'+f.$id; |
| 1396 | | - if(op.boolValue()) check.checked = true; |
| 1479 | + check.checked = op.boolValue(); |
| 1480 | + op.checkbox = check; |
| 1397 | 1481 | D.attr(check, 'id', id); |
| 1398 | | - D.attr(btn, 'for', id); |
| 1399 | | - D.append(line, check); |
| 1400 | | - check.addEventListener('change', callback); |
| 1401 | | - D.append(line, btn); |
| 1482 | + D.append(line, col0, labelWrapper); |
| 1483 | + D.append(col0, check); |
| 1484 | + if(label){ |
| 1485 | + D.attr(label, 'for', id); |
| 1486 | + D.append(labelWrapper, label); |
| 1487 | + } |
| 1488 | + if(hint) D.append(labelWrapper, hint); |
| 1402 | 1489 | }else{ |
| 1403 | 1490 | line.addEventListener('click', callback); |
| 1404 | | - D.append(line, btn); |
| 1491 | + D.append(line, col0, labelWrapper); |
| 1492 | + if(label) D.append(labelWrapper, label); |
| 1493 | + if(hint) D.append(labelWrapper, hint); |
| 1405 | 1494 | } |
| 1406 | 1495 | D.append(optionsMenu, line); |
| 1496 | + if(op.persistentSetting){ |
| 1497 | + Chat.settings.addListener( |
| 1498 | + op.persistentSetting, |
| 1499 | + function(setting){ |
| 1500 | + if(op.checkbox) op.checkbox.checked = !!setting.value; |
| 1501 | + else if(op.select) op.select.value = setting.value; |
| 1502 | + if(op.callback) op.callback(setting); |
| 1503 | + } |
| 1504 | + ); |
| 1505 | + if(op.checkbox){ |
| 1506 | + op.checkbox.addEventListener( |
| 1507 | + 'change', function(){ |
| 1508 | + Chat.settings.set(op.persistentSetting, op.checkbox.checked) |
| 1509 | + }, false); |
| 1510 | + } |
| 1511 | + }else if(op.callback && op.checkbox){ |
| 1512 | + op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false); |
| 1513 | + } |
| 1407 | 1514 | }); |
| 1408 | | - if(0 && settingsOps.selectSound){ |
| 1409 | | - D.append(optionsMenu, settingsOps.selectSound); |
| 1410 | | - } |
| 1411 | | - //settingsButton.click()/*for for development*/; |
| 1412 | 1515 | })()/*#chat-settings-button setup*/; |
| 1413 | 1516 | |
| 1517 | + (function(){ |
| 1518 | + /* Install default settings... must come after |
| 1519 | + chat-settings-button setup so that the listeners which that |
| 1520 | + installs are notified via the properties getting initialized |
| 1521 | + here. */ |
| 1522 | + Chat.settings.addListener('monospace-messages',function(s){ |
| 1523 | + document.body.classList[s.value ? 'add' : 'remove']('monospace-messages'); |
| 1524 | + }) |
| 1525 | + Chat.settings.addListener('active-user-list',function(s){ |
| 1526 | + Chat.showActiveUserList(s.value); |
| 1527 | + }); |
| 1528 | + Chat.settings.addListener('active-user-list-timestamps',function(s){ |
| 1529 | + Chat.showActiveUserTimestamps(s.value); |
| 1530 | + }); |
| 1531 | + Chat.settings.addListener('chat-only-mode',function(s){ |
| 1532 | + Chat.chatOnlyMode(s.value); |
| 1533 | + }); |
| 1534 | + Chat.settings.addListener('edit-compact-mode',function(s){ |
| 1535 | + Chat.e.inputLine.classList[ |
| 1536 | + s.value ? 'add' : 'remove' |
| 1537 | + ]('compact'); |
| 1538 | + }); |
| 1539 | + Chat.settings.addListener('edit-ctrl-send',function(s){ |
| 1540 | + const label = (s.value ? "Ctrl-" : "")+"Enter submits messages."; |
| 1541 | + const eInput = Chat.inputElement(); |
| 1542 | + eInput.dataset.placeholder = eInput.dataset.placeholder0 + " " +label; |
| 1543 | + Chat.e.btnSubmit.title = label; |
| 1544 | + }); |
| 1545 | + const valueKludges = { |
| 1546 | + /* Convert certain string-format values to other types... */ |
| 1547 | + "false": false, |
| 1548 | + "true": true |
| 1549 | + }; |
| 1550 | + Object.keys(Chat.settings.defaults).forEach(function(k){ |
| 1551 | + var v = Chat.settings.get(k,Chat); |
| 1552 | + if(Chat===v) v = Chat.settings.defaults[k]; |
| 1553 | + if(valueKludges.hasOwnProperty(v)) v = valueKludges[v]; |
| 1554 | + Chat.settings.set(k,v) |
| 1555 | + /* fires event listeners so that the Config area checkboxes |
| 1556 | + get in sync */; |
| 1557 | + }); |
| 1558 | + })(); |
| 1559 | + |
| 1414 | 1560 | (function(){/*set up message preview*/ |
| 1415 | 1561 | const btnPreview = Chat.e.btnPreview; |
| 1416 | 1562 | Chat.setPreviewText = function(t){ |
| 1417 | 1563 | this.setCurrentView(this.e.viewPreview); |
| 1418 | 1564 | this.e.previewContent.innerHTML = t; |
| 1419 | 1565 | this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); |
| 1420 | | - this.e.inputCurrent.focus(); |
| 1566 | + this.inputFocus(); |
| 1421 | 1567 | }; |
| 1422 | 1568 | Chat.e.viewPreview.querySelector('#chat-preview-close'). |
| 1423 | 1569 | addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); |
| 1424 | 1570 | let previewPending = false; |
| 1425 | | - const elemsToEnable = [ |
| 1426 | | - btnPreview, Chat.e.btnSubmit, |
| 1427 | | - Chat.e.inputSingle, Chat.e.inputMulti]; |
| 1571 | + const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField]; |
| 1428 | 1572 | const submit = function(ev){ |
| 1429 | 1573 | ev.preventDefault(); |
| 1430 | 1574 | ev.stopPropagation(); |
| 1431 | 1575 | if(previewPending) return false; |
| 1432 | | - const txt = Chat.e.inputCurrent.value; |
| 1576 | + const txt = Chat.inputValue(); |
| 1433 | 1577 | if(!txt){ |
| 1434 | 1578 | Chat.setPreviewText(''); |
| 1435 | 1579 | previewPending = false; |
| 1436 | 1580 | return false; |
| 1437 | 1581 | } |
| | @@ -1490,11 +1634,13 @@ |
| 1490 | 1634 | if( m.mdel ){ |
| 1491 | 1635 | /* A record deletion notice. */ |
| 1492 | 1636 | Chat.deleteMessageElem(m.mdel); |
| 1493 | 1637 | return; |
| 1494 | 1638 | } |
| 1495 | | - if(!Chat._isBatchLoading /*&& Chat.me!==m.xfrom*/ && Chat.playNewMessageSound){ |
| 1639 | + if(!Chat._isBatchLoading |
| 1640 | + && (Chat.me!==m.xfrom |
| 1641 | + || Chat.settings.getBool('alert-own-messages'))){ |
| 1496 | 1642 | Chat.playNewMessageSound(); |
| 1497 | 1643 | } |
| 1498 | 1644 | const row = new Chat.MessageWidget(m); |
| 1499 | 1645 | Chat.injectMessageElem(row.e.body,atEnd); |
| 1500 | 1646 | if(m.isError){ |
| 1501 | 1647 | |