Fossil SCM
Simplified and consolidated how /chat internally manages its 3 separate main views, with an eye towards making it easy to add additional views. No user-visible changes.
Commit
593d3a3a1e5455a40663ce45dc50915d6bcd64f206ffa6f803bc2553628c3913
Parent
15d58775a75f094…
2 files changed
+3
-3
+40
-46
+3
-3
| --- src/chat.c | ||
| +++ src/chat.c | ||
| @@ -181,21 +181,21 @@ | ||
| 181 | 181 | @ <input type="file" name="file" id="chat-input-file"> |
| 182 | 182 | @ </div> |
| 183 | 183 | @ <div id="chat-drop-details"></div> |
| 184 | 184 | @ </div> |
| 185 | 185 | @ </div> |
| 186 | - @ <div id='chat-preview' class='hidden'> | |
| 186 | + @ <div id='chat-preview' class='hidden chat-view'> | |
| 187 | 187 | @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> |
| 188 | 188 | @ <div id='chat-preview-content' class='message-widget-content'></div> |
| 189 | 189 | @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div> |
| 190 | 190 | @ </div> |
| 191 | - @ <div id='chat-config' class='hidden'> | |
| 191 | + @ <div id='chat-config' class='hidden chat-view'> | |
| 192 | 192 | @ <div id='chat-config-options'></div> |
| 193 | 193 | /* ^^^populated client-side */ |
| 194 | 194 | @ <button>Close Settings</button> |
| 195 | 195 | @ </div> |
| 196 | - @ <div id='chat-messages-wrapper'> | |
| 196 | + @ <div id='chat-messages-wrapper' class='chat-view'> | |
| 197 | 197 | /* New chat messages get inserted immediately after this element */ |
| 198 | 198 | @ <span id='message-inject-point'></span> |
| 199 | 199 | @ </div> |
| 200 | 200 | fossil_free(zProjectName); |
| 201 | 201 | builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", |
| 202 | 202 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -181,21 +181,21 @@ | |
| 181 | @ <input type="file" name="file" id="chat-input-file"> |
| 182 | @ </div> |
| 183 | @ <div id="chat-drop-details"></div> |
| 184 | @ </div> |
| 185 | @ </div> |
| 186 | @ <div id='chat-preview' class='hidden'> |
| 187 | @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> |
| 188 | @ <div id='chat-preview-content' class='message-widget-content'></div> |
| 189 | @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div> |
| 190 | @ </div> |
| 191 | @ <div id='chat-config' class='hidden'> |
| 192 | @ <div id='chat-config-options'></div> |
| 193 | /* ^^^populated client-side */ |
| 194 | @ <button>Close Settings</button> |
| 195 | @ </div> |
| 196 | @ <div id='chat-messages-wrapper'> |
| 197 | /* New chat messages get inserted immediately after this element */ |
| 198 | @ <span id='message-inject-point'></span> |
| 199 | @ </div> |
| 200 | fossil_free(zProjectName); |
| 201 | builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", |
| 202 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -181,21 +181,21 @@ | |
| 181 | @ <input type="file" name="file" id="chat-input-file"> |
| 182 | @ </div> |
| 183 | @ <div id="chat-drop-details"></div> |
| 184 | @ </div> |
| 185 | @ </div> |
| 186 | @ <div id='chat-preview' class='hidden chat-view'> |
| 187 | @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> |
| 188 | @ <div id='chat-preview-content' class='message-widget-content'></div> |
| 189 | @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div> |
| 190 | @ </div> |
| 191 | @ <div id='chat-config' class='hidden chat-view'> |
| 192 | @ <div id='chat-config-options'></div> |
| 193 | /* ^^^populated client-side */ |
| 194 | @ <button>Close Settings</button> |
| 195 | @ </div> |
| 196 | @ <div id='chat-messages-wrapper' class='chat-view'> |
| 197 | /* New chat messages get inserted immediately after this element */ |
| 198 | @ <span id='message-inject-point'></span> |
| 199 | @ </div> |
| 200 | fossil_free(zProjectName); |
| 201 | builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", |
| 202 |
+40
-46
| --- src/chat.js | ||
| +++ src/chat.js | ||
| @@ -105,21 +105,22 @@ | ||
| 105 | 105 | pageTitle: E1('head title'), |
| 106 | 106 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 107 | 107 | inputWrapper: E1("#chat-input-area"), |
| 108 | 108 | inputLine: E1('#chat-input-line'), |
| 109 | 109 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 110 | - messagesWrapper: E1('#chat-messages-wrapper'), | |
| 110 | + viewMessages: E1('#chat-messages-wrapper'), | |
| 111 | 111 | btnSubmit: E1('#chat-message-submit'), |
| 112 | 112 | inputSingle: E1('#chat-input-single'), |
| 113 | 113 | inputMulti: E1('#chat-input-multi'), |
| 114 | 114 | inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 115 | 115 | inputFile: E1('#chat-input-file'), |
| 116 | 116 | contentDiv: E1('div.content'), |
| 117 | - configArea: E1('#chat-config'), | |
| 118 | - previewArea: E1('#chat-preview'), | |
| 117 | + viewConfig: E1('#chat-config'), | |
| 118 | + viewPreview: E1('#chat-preview'), | |
| 119 | 119 | previewContent: E1('#chat-preview-content'), |
| 120 | - btnPreview: E1('#chat-preview-button') | |
| 120 | + btnPreview: E1('#chat-preview-button'), | |
| 121 | + views: document.querySelectorAll('.chat-view') | |
| 121 | 122 | }, |
| 122 | 123 | me: F.user.name, |
| 123 | 124 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 124 | 125 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 125 | 126 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -157,11 +158,11 @@ | ||
| 157 | 158 | this.e.inputLine.classList.remove('single-line'); |
| 158 | 159 | }else{ |
| 159 | 160 | this.e.inputCurrent = this.e.inputSingle; |
| 160 | 161 | this.e.inputLine.classList.add('single-line'); |
| 161 | 162 | } |
| 162 | - const m = this.e.messagesWrapper, | |
| 163 | + const m = this.e.viewMessages, | |
| 163 | 164 | sTop = m.scrollTop, |
| 164 | 165 | mh1 = m.clientHeight; |
| 165 | 166 | D.addClass(old, 'hidden'); |
| 166 | 167 | D.removeClass(this.e.inputCurrent, 'hidden'); |
| 167 | 168 | const mh2 = m.clientHeight; |
| @@ -241,11 +242,11 @@ | ||
| 241 | 242 | /* Injects DOM element e as a new row in the chat, at the oldest |
| 242 | 243 | end of the list if atEnd is truthy, else at the newest end of |
| 243 | 244 | the list. */ |
| 244 | 245 | injectMessageElem: function f(e, atEnd){ |
| 245 | 246 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 246 | - holder = this.e.messagesWrapper, | |
| 247 | + holder = this.e.viewMessages, | |
| 247 | 248 | prevMessage = this.e.newestMessage; |
| 248 | 249 | if(atEnd){ |
| 249 | 250 | const fe = mip.nextElementSibling; |
| 250 | 251 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 251 | 252 | else D.append(mip.parentNode, e); |
| @@ -337,22 +338,22 @@ | ||
| 337 | 338 | <0 = top of the message list, >0 = bottom of the message list, |
| 338 | 339 | 0 == the newest message (normally the same position as >1). |
| 339 | 340 | */ |
| 340 | 341 | scrollMessagesTo: function(where){ |
| 341 | 342 | if(where<0){ |
| 342 | - Chat.e.messagesWrapper.scrollTop = 0; | |
| 343 | + Chat.e.viewMessages.scrollTop = 0; | |
| 343 | 344 | }else if(where>0){ |
| 344 | - Chat.e.messagesWrapper.scrollTop = Chat.e.messagesWrapper.scrollHeight; | |
| 345 | + Chat.e.viewMessages.scrollTop = Chat.e.viewMessages.scrollHeight; | |
| 345 | 346 | }else if(Chat.e.newestMessage){ |
| 346 | 347 | Chat.e.newestMessage.scrollIntoView(false); |
| 347 | 348 | } |
| 348 | 349 | }, |
| 349 | 350 | toggleChatOnlyMode: function(){ |
| 350 | 351 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 351 | 352 | }, |
| 352 | 353 | messageIsInView: function(e){ |
| 353 | - return e ? overlapsElemView(e, this.e.messagesWrapper) : false; | |
| 354 | + return e ? overlapsElemView(e, this.e.viewMessages) : false; | |
| 354 | 355 | }, |
| 355 | 356 | settings:{ |
| 356 | 357 | get: (k,dflt)=>F.storage.get(k,dflt), |
| 357 | 358 | getBool: (k,dflt)=>F.storage.getBool(k,dflt), |
| 358 | 359 | set: (k,v)=>F.storage.set(k,v), |
| @@ -395,10 +396,21 @@ | ||
| 395 | 396 | setNewMessageSound: function f(uri){ |
| 396 | 397 | delete this.playNewMessageSound.audio; |
| 397 | 398 | this.playNewMessageSound.uri = uri; |
| 398 | 399 | this.settings.set('audible-alert', uri); |
| 399 | 400 | return this; |
| 401 | + }, | |
| 402 | + /** | |
| 403 | + Expects e to be one of the elements in this.e.views. | |
| 404 | + The 'hidden' class is removed from e and added to | |
| 405 | + all other elements in that list. Returns e. | |
| 406 | + */ | |
| 407 | + setCurrentView: function(e){ | |
| 408 | + this.e.views.forEach(function(E){ | |
| 409 | + if(e!==E) D.addClass(E,'hidden'); | |
| 410 | + }); | |
| 411 | + return this.e.currentView = D.removeClass(e,'hidden'); | |
| 400 | 412 | } |
| 401 | 413 | }; |
| 402 | 414 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 403 | 415 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 404 | 416 | cs.e.inputCurrent = cs.e.inputSingle; |
| @@ -611,10 +623,11 @@ | ||
| 611 | 623 | cs.pageIsActive = ('visible' === document.visibilityState); |
| 612 | 624 | if(cs.pageIsActive){ |
| 613 | 625 | cs.e.pageTitle.innerText = cs.pageTitleOrig; |
| 614 | 626 | } |
| 615 | 627 | }, true); |
| 628 | + cs.setCurrentView(cs.e.viewMessages); | |
| 616 | 629 | return cs; |
| 617 | 630 | })()/*Chat initialization*/; |
| 618 | 631 | |
| 619 | 632 | /** |
| 620 | 633 | Custom widget type for rendering messages (one message per |
| @@ -961,11 +974,11 @@ | ||
| 961 | 974 | */ |
| 962 | 975 | Chat.submitMessage = function f(){ |
| 963 | 976 | if(!f.spaces){ |
| 964 | 977 | f.spaces = /\s+$/; |
| 965 | 978 | } |
| 966 | - this.revealPreview(false); | |
| 979 | + this.setCurrentView(this.e.viewMessages); | |
| 967 | 980 | const fd = new FormData(); |
| 968 | 981 | var msg = this.inputValue().trim(); |
| 969 | 982 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 970 | 983 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 971 | 984 | keep copy/paste from terminals, especially wide ones, from |
| @@ -1033,21 +1046,16 @@ | ||
| 1033 | 1046 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1034 | 1047 | const optionsMenu = E1('#chat-config-options'); |
| 1035 | 1048 | const cbToggle = function(ev){ |
| 1036 | 1049 | ev.preventDefault(); |
| 1037 | 1050 | ev.stopPropagation(); |
| 1038 | - if(Chat.e.configArea.classList.contains('hidden')){ | |
| 1039 | - D.removeClass(Chat.e.configArea, 'hidden'); | |
| 1040 | - D.addClass([Chat.e.messagesWrapper, Chat.e.previewArea], 'hidden'); | |
| 1041 | - }else{ | |
| 1042 | - D.addClass(Chat.e.configArea, 'hidden'); | |
| 1043 | - D.removeClass(Chat.e.messagesWrapper, 'hidden'); | |
| 1044 | - } | |
| 1051 | + Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig | |
| 1052 | + ? Chat.e.viewMessages : Chat.e.viewConfig); | |
| 1045 | 1053 | return false; |
| 1046 | 1054 | }; |
| 1047 | 1055 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 1048 | - Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false); | |
| 1056 | + Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false); | |
| 1049 | 1057 | /* Settings menu entries... */ |
| 1050 | 1058 | const settingsOps = [{ |
| 1051 | 1059 | label: "Multi-line input", |
| 1052 | 1060 | boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1053 | 1061 | persistentSetting: 'edit-multiline', |
| @@ -1155,31 +1163,17 @@ | ||
| 1155 | 1163 | })()/*#chat-settings-button setup*/; |
| 1156 | 1164 | |
| 1157 | 1165 | (function(){/*set up message preview*/ |
| 1158 | 1166 | const btnPreview = Chat.e.btnPreview; |
| 1159 | 1167 | Chat.setPreviewText = function(t){ |
| 1160 | - this.revealPreview(true).e.previewContent.innerHTML = t; | |
| 1161 | - this.e.previewArea.querySelectorAll('a').forEach(addAnchorTargetBlank); | |
| 1168 | + this.setCurrentView(this.e.viewPreview); | |
| 1169 | + this.e.previewContent.innerHTML = t; | |
| 1170 | + this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); | |
| 1162 | 1171 | this.e.inputCurrent.focus(); |
| 1163 | 1172 | }; |
| 1164 | - /** | |
| 1165 | - Reveals preview area if showIt is true, else hides it. | |
| 1166 | - This also shows/hides other elements, "as appropriate." | |
| 1167 | - */ | |
| 1168 | - Chat.revealPreview = function(showIt){ | |
| 1169 | - if(showIt){ | |
| 1170 | - D.removeClass(Chat.e.previewArea, 'hidden'); | |
| 1171 | - D.addClass([Chat.e.messagesWrapper, Chat.e.configArea], | |
| 1172 | - 'hidden'); | |
| 1173 | - }else{ | |
| 1174 | - D.addClass([Chat.e.configArea, Chat.e.previewArea], 'hidden'); | |
| 1175 | - D.removeClass(Chat.e.messagesWrapper, 'hidden'); | |
| 1176 | - } | |
| 1177 | - return this; | |
| 1178 | - }; | |
| 1179 | - Chat.e.previewArea.querySelector('#chat-preview-close'). | |
| 1180 | - addEventListener('click', ()=>Chat.revealPreview(false), false); | |
| 1173 | + Chat.e.viewPreview.querySelector('#chat-preview-close'). | |
| 1174 | + addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); | |
| 1181 | 1175 | let previewPending = false; |
| 1182 | 1176 | const elemsToEnable = [ |
| 1183 | 1177 | btnPreview, Chat.e.btnSubmit, |
| 1184 | 1178 | Chat.e.inputSingle, Chat.e.inputMulti]; |
| 1185 | 1179 | Chat.disableDuringAjax.push(btnPreview); |
| @@ -1277,14 +1271,14 @@ | ||
| 1277 | 1271 | D.fieldset(loadLegend), "id", "load-msg-toolbar" |
| 1278 | 1272 | ); |
| 1279 | 1273 | Chat.disableDuringAjax.push(toolbar); |
| 1280 | 1274 | /* Loads the next n oldest messages, or all previous history if n is negative. */ |
| 1281 | 1275 | const loadOldMessages = function(n){ |
| 1282 | - Chat.e.messagesWrapper.classList.add('loading'); | |
| 1276 | + Chat.e.viewMessages.classList.add('loading'); | |
| 1283 | 1277 | Chat._isBatchLoading = true; |
| 1284 | - const scrollHt = Chat.e.messagesWrapper.scrollHeight, | |
| 1285 | - scrollTop = Chat.e.messagesWrapper.scrollTop; | |
| 1278 | + const scrollHt = Chat.e.viewMessages.scrollHeight, | |
| 1279 | + scrollTop = Chat.e.viewMessages.scrollTop; | |
| 1286 | 1280 | F.fetch("chat-poll",{ |
| 1287 | 1281 | urlParams:{ |
| 1288 | 1282 | before: Chat.mnMsg, |
| 1289 | 1283 | n: n |
| 1290 | 1284 | }, |
| @@ -1318,17 +1312,17 @@ | ||
| 1318 | 1312 | } |
| 1319 | 1313 | if(gotMessages > 0){ |
| 1320 | 1314 | F.toast.message("Loaded "+gotMessages+" older messages."); |
| 1321 | 1315 | /* Return scroll position to where it was when the history load |
| 1322 | 1316 | was requested, per user request */ |
| 1323 | - Chat.e.messagesWrapper.scrollTo( | |
| 1324 | - 0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop | |
| 1317 | + Chat.e.viewMessages.scrollTo( | |
| 1318 | + 0, Chat.e.viewMessages.scrollHeight - scrollHt + scrollTop | |
| 1325 | 1319 | ); |
| 1326 | 1320 | } |
| 1327 | 1321 | }, |
| 1328 | 1322 | aftersend:function(){ |
| 1329 | - Chat.e.messagesWrapper.classList.remove('loading'); | |
| 1323 | + Chat.e.viewMessages.classList.remove('loading'); | |
| 1330 | 1324 | Chat.ajaxEnd(); |
| 1331 | 1325 | } |
| 1332 | 1326 | }); |
| 1333 | 1327 | }; |
| 1334 | 1328 | const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */; |
| @@ -1337,19 +1331,19 @@ | ||
| 1337 | 1331 | D.append(wrapper, btn); |
| 1338 | 1332 | btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount)); |
| 1339 | 1333 | btn = D.button("All previous messages"); |
| 1340 | 1334 | D.append(wrapper, btn); |
| 1341 | 1335 | btn.addEventListener('click',()=>loadOldMessages(-1)); |
| 1342 | - D.append(Chat.e.messagesWrapper, toolbar); | |
| 1336 | + D.append(Chat.e.viewMessages, toolbar); | |
| 1343 | 1337 | toolbar.disabled = true /*will be enabled when msg load finishes */; |
| 1344 | 1338 | })()/*end history loading widget setup*/; |
| 1345 | 1339 | |
| 1346 | 1340 | const afterFetch = function f(){ |
| 1347 | 1341 | if(true===f.isFirstCall){ |
| 1348 | 1342 | f.isFirstCall = false; |
| 1349 | 1343 | Chat.ajaxEnd(); |
| 1350 | - Chat.e.messagesWrapper.classList.remove('loading'); | |
| 1344 | + Chat.e.viewMessages.classList.remove('loading'); | |
| 1351 | 1345 | setTimeout(function(){ |
| 1352 | 1346 | Chat.scrollMessagesTo(1); |
| 1353 | 1347 | }, 250); |
| 1354 | 1348 | } |
| 1355 | 1349 | if(Chat._gotServerError && Chat.intervalTimer){ |
| @@ -1367,11 +1361,11 @@ | ||
| 1367 | 1361 | f.running = true; |
| 1368 | 1362 | Chat._isBatchLoading = f.isFirstCall; |
| 1369 | 1363 | if(true===f.isFirstCall){ |
| 1370 | 1364 | f.isFirstCall = false; |
| 1371 | 1365 | Chat.ajaxStart(); |
| 1372 | - Chat.e.messagesWrapper.classList.add('loading'); | |
| 1366 | + Chat.e.viewMessages.classList.add('loading'); | |
| 1373 | 1367 | } |
| 1374 | 1368 | F.fetch("chat-poll",{ |
| 1375 | 1369 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 1376 | 1370 | urlParams:{ |
| 1377 | 1371 | name: Chat.mxMsg |
| 1378 | 1372 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -105,21 +105,22 @@ | |
| 105 | pageTitle: E1('head title'), |
| 106 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 107 | inputWrapper: E1("#chat-input-area"), |
| 108 | inputLine: E1('#chat-input-line'), |
| 109 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 110 | messagesWrapper: E1('#chat-messages-wrapper'), |
| 111 | btnSubmit: E1('#chat-message-submit'), |
| 112 | inputSingle: E1('#chat-input-single'), |
| 113 | inputMulti: E1('#chat-input-multi'), |
| 114 | inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 115 | inputFile: E1('#chat-input-file'), |
| 116 | contentDiv: E1('div.content'), |
| 117 | configArea: E1('#chat-config'), |
| 118 | previewArea: E1('#chat-preview'), |
| 119 | previewContent: E1('#chat-preview-content'), |
| 120 | btnPreview: E1('#chat-preview-button') |
| 121 | }, |
| 122 | me: F.user.name, |
| 123 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 124 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 125 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -157,11 +158,11 @@ | |
| 157 | this.e.inputLine.classList.remove('single-line'); |
| 158 | }else{ |
| 159 | this.e.inputCurrent = this.e.inputSingle; |
| 160 | this.e.inputLine.classList.add('single-line'); |
| 161 | } |
| 162 | const m = this.e.messagesWrapper, |
| 163 | sTop = m.scrollTop, |
| 164 | mh1 = m.clientHeight; |
| 165 | D.addClass(old, 'hidden'); |
| 166 | D.removeClass(this.e.inputCurrent, 'hidden'); |
| 167 | const mh2 = m.clientHeight; |
| @@ -241,11 +242,11 @@ | |
| 241 | /* Injects DOM element e as a new row in the chat, at the oldest |
| 242 | end of the list if atEnd is truthy, else at the newest end of |
| 243 | the list. */ |
| 244 | injectMessageElem: function f(e, atEnd){ |
| 245 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 246 | holder = this.e.messagesWrapper, |
| 247 | prevMessage = this.e.newestMessage; |
| 248 | if(atEnd){ |
| 249 | const fe = mip.nextElementSibling; |
| 250 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 251 | else D.append(mip.parentNode, e); |
| @@ -337,22 +338,22 @@ | |
| 337 | <0 = top of the message list, >0 = bottom of the message list, |
| 338 | 0 == the newest message (normally the same position as >1). |
| 339 | */ |
| 340 | scrollMessagesTo: function(where){ |
| 341 | if(where<0){ |
| 342 | Chat.e.messagesWrapper.scrollTop = 0; |
| 343 | }else if(where>0){ |
| 344 | Chat.e.messagesWrapper.scrollTop = Chat.e.messagesWrapper.scrollHeight; |
| 345 | }else if(Chat.e.newestMessage){ |
| 346 | Chat.e.newestMessage.scrollIntoView(false); |
| 347 | } |
| 348 | }, |
| 349 | toggleChatOnlyMode: function(){ |
| 350 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 351 | }, |
| 352 | messageIsInView: function(e){ |
| 353 | return e ? overlapsElemView(e, this.e.messagesWrapper) : false; |
| 354 | }, |
| 355 | settings:{ |
| 356 | get: (k,dflt)=>F.storage.get(k,dflt), |
| 357 | getBool: (k,dflt)=>F.storage.getBool(k,dflt), |
| 358 | set: (k,v)=>F.storage.set(k,v), |
| @@ -395,10 +396,21 @@ | |
| 395 | setNewMessageSound: function f(uri){ |
| 396 | delete this.playNewMessageSound.audio; |
| 397 | this.playNewMessageSound.uri = uri; |
| 398 | this.settings.set('audible-alert', uri); |
| 399 | return this; |
| 400 | } |
| 401 | }; |
| 402 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 403 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 404 | cs.e.inputCurrent = cs.e.inputSingle; |
| @@ -611,10 +623,11 @@ | |
| 611 | cs.pageIsActive = ('visible' === document.visibilityState); |
| 612 | if(cs.pageIsActive){ |
| 613 | cs.e.pageTitle.innerText = cs.pageTitleOrig; |
| 614 | } |
| 615 | }, true); |
| 616 | return cs; |
| 617 | })()/*Chat initialization*/; |
| 618 | |
| 619 | /** |
| 620 | Custom widget type for rendering messages (one message per |
| @@ -961,11 +974,11 @@ | |
| 961 | */ |
| 962 | Chat.submitMessage = function f(){ |
| 963 | if(!f.spaces){ |
| 964 | f.spaces = /\s+$/; |
| 965 | } |
| 966 | this.revealPreview(false); |
| 967 | const fd = new FormData(); |
| 968 | var msg = this.inputValue().trim(); |
| 969 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 970 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 971 | keep copy/paste from terminals, especially wide ones, from |
| @@ -1033,21 +1046,16 @@ | |
| 1033 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1034 | const optionsMenu = E1('#chat-config-options'); |
| 1035 | const cbToggle = function(ev){ |
| 1036 | ev.preventDefault(); |
| 1037 | ev.stopPropagation(); |
| 1038 | if(Chat.e.configArea.classList.contains('hidden')){ |
| 1039 | D.removeClass(Chat.e.configArea, 'hidden'); |
| 1040 | D.addClass([Chat.e.messagesWrapper, Chat.e.previewArea], 'hidden'); |
| 1041 | }else{ |
| 1042 | D.addClass(Chat.e.configArea, 'hidden'); |
| 1043 | D.removeClass(Chat.e.messagesWrapper, 'hidden'); |
| 1044 | } |
| 1045 | return false; |
| 1046 | }; |
| 1047 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 1048 | Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false); |
| 1049 | /* Settings menu entries... */ |
| 1050 | const settingsOps = [{ |
| 1051 | label: "Multi-line input", |
| 1052 | boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1053 | persistentSetting: 'edit-multiline', |
| @@ -1155,31 +1163,17 @@ | |
| 1155 | })()/*#chat-settings-button setup*/; |
| 1156 | |
| 1157 | (function(){/*set up message preview*/ |
| 1158 | const btnPreview = Chat.e.btnPreview; |
| 1159 | Chat.setPreviewText = function(t){ |
| 1160 | this.revealPreview(true).e.previewContent.innerHTML = t; |
| 1161 | this.e.previewArea.querySelectorAll('a').forEach(addAnchorTargetBlank); |
| 1162 | this.e.inputCurrent.focus(); |
| 1163 | }; |
| 1164 | /** |
| 1165 | Reveals preview area if showIt is true, else hides it. |
| 1166 | This also shows/hides other elements, "as appropriate." |
| 1167 | */ |
| 1168 | Chat.revealPreview = function(showIt){ |
| 1169 | if(showIt){ |
| 1170 | D.removeClass(Chat.e.previewArea, 'hidden'); |
| 1171 | D.addClass([Chat.e.messagesWrapper, Chat.e.configArea], |
| 1172 | 'hidden'); |
| 1173 | }else{ |
| 1174 | D.addClass([Chat.e.configArea, Chat.e.previewArea], 'hidden'); |
| 1175 | D.removeClass(Chat.e.messagesWrapper, 'hidden'); |
| 1176 | } |
| 1177 | return this; |
| 1178 | }; |
| 1179 | Chat.e.previewArea.querySelector('#chat-preview-close'). |
| 1180 | addEventListener('click', ()=>Chat.revealPreview(false), false); |
| 1181 | let previewPending = false; |
| 1182 | const elemsToEnable = [ |
| 1183 | btnPreview, Chat.e.btnSubmit, |
| 1184 | Chat.e.inputSingle, Chat.e.inputMulti]; |
| 1185 | Chat.disableDuringAjax.push(btnPreview); |
| @@ -1277,14 +1271,14 @@ | |
| 1277 | D.fieldset(loadLegend), "id", "load-msg-toolbar" |
| 1278 | ); |
| 1279 | Chat.disableDuringAjax.push(toolbar); |
| 1280 | /* Loads the next n oldest messages, or all previous history if n is negative. */ |
| 1281 | const loadOldMessages = function(n){ |
| 1282 | Chat.e.messagesWrapper.classList.add('loading'); |
| 1283 | Chat._isBatchLoading = true; |
| 1284 | const scrollHt = Chat.e.messagesWrapper.scrollHeight, |
| 1285 | scrollTop = Chat.e.messagesWrapper.scrollTop; |
| 1286 | F.fetch("chat-poll",{ |
| 1287 | urlParams:{ |
| 1288 | before: Chat.mnMsg, |
| 1289 | n: n |
| 1290 | }, |
| @@ -1318,17 +1312,17 @@ | |
| 1318 | } |
| 1319 | if(gotMessages > 0){ |
| 1320 | F.toast.message("Loaded "+gotMessages+" older messages."); |
| 1321 | /* Return scroll position to where it was when the history load |
| 1322 | was requested, per user request */ |
| 1323 | Chat.e.messagesWrapper.scrollTo( |
| 1324 | 0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop |
| 1325 | ); |
| 1326 | } |
| 1327 | }, |
| 1328 | aftersend:function(){ |
| 1329 | Chat.e.messagesWrapper.classList.remove('loading'); |
| 1330 | Chat.ajaxEnd(); |
| 1331 | } |
| 1332 | }); |
| 1333 | }; |
| 1334 | const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */; |
| @@ -1337,19 +1331,19 @@ | |
| 1337 | D.append(wrapper, btn); |
| 1338 | btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount)); |
| 1339 | btn = D.button("All previous messages"); |
| 1340 | D.append(wrapper, btn); |
| 1341 | btn.addEventListener('click',()=>loadOldMessages(-1)); |
| 1342 | D.append(Chat.e.messagesWrapper, toolbar); |
| 1343 | toolbar.disabled = true /*will be enabled when msg load finishes */; |
| 1344 | })()/*end history loading widget setup*/; |
| 1345 | |
| 1346 | const afterFetch = function f(){ |
| 1347 | if(true===f.isFirstCall){ |
| 1348 | f.isFirstCall = false; |
| 1349 | Chat.ajaxEnd(); |
| 1350 | Chat.e.messagesWrapper.classList.remove('loading'); |
| 1351 | setTimeout(function(){ |
| 1352 | Chat.scrollMessagesTo(1); |
| 1353 | }, 250); |
| 1354 | } |
| 1355 | if(Chat._gotServerError && Chat.intervalTimer){ |
| @@ -1367,11 +1361,11 @@ | |
| 1367 | f.running = true; |
| 1368 | Chat._isBatchLoading = f.isFirstCall; |
| 1369 | if(true===f.isFirstCall){ |
| 1370 | f.isFirstCall = false; |
| 1371 | Chat.ajaxStart(); |
| 1372 | Chat.e.messagesWrapper.classList.add('loading'); |
| 1373 | } |
| 1374 | F.fetch("chat-poll",{ |
| 1375 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 1376 | urlParams:{ |
| 1377 | name: Chat.mxMsg |
| 1378 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -105,21 +105,22 @@ | |
| 105 | pageTitle: E1('head title'), |
| 106 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 107 | inputWrapper: E1("#chat-input-area"), |
| 108 | inputLine: E1('#chat-input-line'), |
| 109 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 110 | viewMessages: E1('#chat-messages-wrapper'), |
| 111 | btnSubmit: E1('#chat-message-submit'), |
| 112 | inputSingle: E1('#chat-input-single'), |
| 113 | inputMulti: E1('#chat-input-multi'), |
| 114 | inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 115 | inputFile: E1('#chat-input-file'), |
| 116 | contentDiv: E1('div.content'), |
| 117 | viewConfig: E1('#chat-config'), |
| 118 | viewPreview: E1('#chat-preview'), |
| 119 | previewContent: E1('#chat-preview-content'), |
| 120 | btnPreview: E1('#chat-preview-button'), |
| 121 | views: document.querySelectorAll('.chat-view') |
| 122 | }, |
| 123 | me: F.user.name, |
| 124 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 125 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 126 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -157,11 +158,11 @@ | |
| 158 | this.e.inputLine.classList.remove('single-line'); |
| 159 | }else{ |
| 160 | this.e.inputCurrent = this.e.inputSingle; |
| 161 | this.e.inputLine.classList.add('single-line'); |
| 162 | } |
| 163 | const m = this.e.viewMessages, |
| 164 | sTop = m.scrollTop, |
| 165 | mh1 = m.clientHeight; |
| 166 | D.addClass(old, 'hidden'); |
| 167 | D.removeClass(this.e.inputCurrent, 'hidden'); |
| 168 | const mh2 = m.clientHeight; |
| @@ -241,11 +242,11 @@ | |
| 242 | /* Injects DOM element e as a new row in the chat, at the oldest |
| 243 | end of the list if atEnd is truthy, else at the newest end of |
| 244 | the list. */ |
| 245 | injectMessageElem: function f(e, atEnd){ |
| 246 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 247 | holder = this.e.viewMessages, |
| 248 | prevMessage = this.e.newestMessage; |
| 249 | if(atEnd){ |
| 250 | const fe = mip.nextElementSibling; |
| 251 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 252 | else D.append(mip.parentNode, e); |
| @@ -337,22 +338,22 @@ | |
| 338 | <0 = top of the message list, >0 = bottom of the message list, |
| 339 | 0 == the newest message (normally the same position as >1). |
| 340 | */ |
| 341 | scrollMessagesTo: function(where){ |
| 342 | if(where<0){ |
| 343 | Chat.e.viewMessages.scrollTop = 0; |
| 344 | }else if(where>0){ |
| 345 | Chat.e.viewMessages.scrollTop = Chat.e.viewMessages.scrollHeight; |
| 346 | }else if(Chat.e.newestMessage){ |
| 347 | Chat.e.newestMessage.scrollIntoView(false); |
| 348 | } |
| 349 | }, |
| 350 | toggleChatOnlyMode: function(){ |
| 351 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 352 | }, |
| 353 | messageIsInView: function(e){ |
| 354 | return e ? overlapsElemView(e, this.e.viewMessages) : false; |
| 355 | }, |
| 356 | settings:{ |
| 357 | get: (k,dflt)=>F.storage.get(k,dflt), |
| 358 | getBool: (k,dflt)=>F.storage.getBool(k,dflt), |
| 359 | set: (k,v)=>F.storage.set(k,v), |
| @@ -395,10 +396,21 @@ | |
| 396 | setNewMessageSound: function f(uri){ |
| 397 | delete this.playNewMessageSound.audio; |
| 398 | this.playNewMessageSound.uri = uri; |
| 399 | this.settings.set('audible-alert', uri); |
| 400 | return this; |
| 401 | }, |
| 402 | /** |
| 403 | Expects e to be one of the elements in this.e.views. |
| 404 | The 'hidden' class is removed from e and added to |
| 405 | all other elements in that list. Returns e. |
| 406 | */ |
| 407 | setCurrentView: function(e){ |
| 408 | this.e.views.forEach(function(E){ |
| 409 | if(e!==E) D.addClass(E,'hidden'); |
| 410 | }); |
| 411 | return this.e.currentView = D.removeClass(e,'hidden'); |
| 412 | } |
| 413 | }; |
| 414 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 415 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 416 | cs.e.inputCurrent = cs.e.inputSingle; |
| @@ -611,10 +623,11 @@ | |
| 623 | cs.pageIsActive = ('visible' === document.visibilityState); |
| 624 | if(cs.pageIsActive){ |
| 625 | cs.e.pageTitle.innerText = cs.pageTitleOrig; |
| 626 | } |
| 627 | }, true); |
| 628 | cs.setCurrentView(cs.e.viewMessages); |
| 629 | return cs; |
| 630 | })()/*Chat initialization*/; |
| 631 | |
| 632 | /** |
| 633 | Custom widget type for rendering messages (one message per |
| @@ -961,11 +974,11 @@ | |
| 974 | */ |
| 975 | Chat.submitMessage = function f(){ |
| 976 | if(!f.spaces){ |
| 977 | f.spaces = /\s+$/; |
| 978 | } |
| 979 | this.setCurrentView(this.e.viewMessages); |
| 980 | const fd = new FormData(); |
| 981 | var msg = this.inputValue().trim(); |
| 982 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 983 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 984 | keep copy/paste from terminals, especially wide ones, from |
| @@ -1033,21 +1046,16 @@ | |
| 1046 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1047 | const optionsMenu = E1('#chat-config-options'); |
| 1048 | const cbToggle = function(ev){ |
| 1049 | ev.preventDefault(); |
| 1050 | ev.stopPropagation(); |
| 1051 | Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig |
| 1052 | ? Chat.e.viewMessages : Chat.e.viewConfig); |
| 1053 | return false; |
| 1054 | }; |
| 1055 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 1056 | Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false); |
| 1057 | /* Settings menu entries... */ |
| 1058 | const settingsOps = [{ |
| 1059 | label: "Multi-line input", |
| 1060 | boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1061 | persistentSetting: 'edit-multiline', |
| @@ -1155,31 +1163,17 @@ | |
| 1163 | })()/*#chat-settings-button setup*/; |
| 1164 | |
| 1165 | (function(){/*set up message preview*/ |
| 1166 | const btnPreview = Chat.e.btnPreview; |
| 1167 | Chat.setPreviewText = function(t){ |
| 1168 | this.setCurrentView(this.e.viewPreview); |
| 1169 | this.e.previewContent.innerHTML = t; |
| 1170 | this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); |
| 1171 | this.e.inputCurrent.focus(); |
| 1172 | }; |
| 1173 | Chat.e.viewPreview.querySelector('#chat-preview-close'). |
| 1174 | addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); |
| 1175 | let previewPending = false; |
| 1176 | const elemsToEnable = [ |
| 1177 | btnPreview, Chat.e.btnSubmit, |
| 1178 | Chat.e.inputSingle, Chat.e.inputMulti]; |
| 1179 | Chat.disableDuringAjax.push(btnPreview); |
| @@ -1277,14 +1271,14 @@ | |
| 1271 | D.fieldset(loadLegend), "id", "load-msg-toolbar" |
| 1272 | ); |
| 1273 | Chat.disableDuringAjax.push(toolbar); |
| 1274 | /* Loads the next n oldest messages, or all previous history if n is negative. */ |
| 1275 | const loadOldMessages = function(n){ |
| 1276 | Chat.e.viewMessages.classList.add('loading'); |
| 1277 | Chat._isBatchLoading = true; |
| 1278 | const scrollHt = Chat.e.viewMessages.scrollHeight, |
| 1279 | scrollTop = Chat.e.viewMessages.scrollTop; |
| 1280 | F.fetch("chat-poll",{ |
| 1281 | urlParams:{ |
| 1282 | before: Chat.mnMsg, |
| 1283 | n: n |
| 1284 | }, |
| @@ -1318,17 +1312,17 @@ | |
| 1312 | } |
| 1313 | if(gotMessages > 0){ |
| 1314 | F.toast.message("Loaded "+gotMessages+" older messages."); |
| 1315 | /* Return scroll position to where it was when the history load |
| 1316 | was requested, per user request */ |
| 1317 | Chat.e.viewMessages.scrollTo( |
| 1318 | 0, Chat.e.viewMessages.scrollHeight - scrollHt + scrollTop |
| 1319 | ); |
| 1320 | } |
| 1321 | }, |
| 1322 | aftersend:function(){ |
| 1323 | Chat.e.viewMessages.classList.remove('loading'); |
| 1324 | Chat.ajaxEnd(); |
| 1325 | } |
| 1326 | }); |
| 1327 | }; |
| 1328 | const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */; |
| @@ -1337,19 +1331,19 @@ | |
| 1331 | D.append(wrapper, btn); |
| 1332 | btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount)); |
| 1333 | btn = D.button("All previous messages"); |
| 1334 | D.append(wrapper, btn); |
| 1335 | btn.addEventListener('click',()=>loadOldMessages(-1)); |
| 1336 | D.append(Chat.e.viewMessages, toolbar); |
| 1337 | toolbar.disabled = true /*will be enabled when msg load finishes */; |
| 1338 | })()/*end history loading widget setup*/; |
| 1339 | |
| 1340 | const afterFetch = function f(){ |
| 1341 | if(true===f.isFirstCall){ |
| 1342 | f.isFirstCall = false; |
| 1343 | Chat.ajaxEnd(); |
| 1344 | Chat.e.viewMessages.classList.remove('loading'); |
| 1345 | setTimeout(function(){ |
| 1346 | Chat.scrollMessagesTo(1); |
| 1347 | }, 250); |
| 1348 | } |
| 1349 | if(Chat._gotServerError && Chat.intervalTimer){ |
| @@ -1367,11 +1361,11 @@ | |
| 1361 | f.running = true; |
| 1362 | Chat._isBatchLoading = f.isFirstCall; |
| 1363 | if(true===f.isFirstCall){ |
| 1364 | f.isFirstCall = false; |
| 1365 | Chat.ajaxStart(); |
| 1366 | Chat.e.viewMessages.classList.add('loading'); |
| 1367 | } |
| 1368 | F.fetch("chat-poll",{ |
| 1369 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 1370 | urlParams:{ |
| 1371 | name: Chat.mxMsg |
| 1372 |