Fossil SCM
chat: auto-scrolling of other peoples' posts into view works based on a heuristic of whether the *previous* post is in view or not (else we assume the user is back in the history), with the notable caveat that posts with inlined images play havok with this, in part because loading of images is async and we race against it. Moved the #debugMsg element out of div.content to keep it from unduly influencing our layout.
Commit
6c28d7d6cb593463c2203a16c028a64f304cd8bb23337d79a21d18901073e23c
Parent
b09f7e990d5cc59…
1 file changed
+78
-17
+78
-17
| --- src/chat.js | ||
| +++ src/chat.js | ||
| @@ -17,10 +17,18 @@ | ||
| 17 | 17 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && |
| 18 | 18 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) |
| 19 | 19 | ); |
| 20 | 20 | }; |
| 21 | 21 | |
| 22 | + (function(){ | |
| 23 | + let dbg = document.querySelector('#debugMsg'); | |
| 24 | + if(dbg){ | |
| 25 | + /* This can inadvertently influence our flexbox layouts, so move | |
| 26 | + it out of the way. */ | |
| 27 | + D.append(document.body,dbg); | |
| 28 | + } | |
| 29 | + })(); | |
| 22 | 30 | const ForceResizeKludge = 0 ? function(){} : (function(){ |
| 23 | 31 | /* Workaround for Safari mayhem regarding use of vh CSS units.... |
| 24 | 32 | We tried to use vh units to set the content area size for the |
| 25 | 33 | chat layout, but Safari chokes on that, so we calculate that |
| 26 | 34 | height here: 85% when in "normal" mode and 95% in chat-only |
| @@ -141,33 +149,82 @@ | ||
| 141 | 149 | /* List of DOM elements disable while ajax traffic is in |
| 142 | 150 | transit. Must be populated before ajax starts. We do this |
| 143 | 151 | to avoid various race conditions in the UI and long-running |
| 144 | 152 | network requests. */ |
| 145 | 153 | ], |
| 154 | + /** Either scrolls .message-widget element eMsg into view | |
| 155 | + immediately or, if it represents an inlined image, delays | |
| 156 | + the scroll until the image is loaded, at which point it will | |
| 157 | + scroll to either the newest message, if one is set or to | |
| 158 | + eMsg (the liklihood is good, at least on initial page load, | |
| 159 | + that the the image won't be loaded until other messages have | |
| 160 | + been injected). */ | |
| 161 | + scheduleScrollOfMsg: function(eMsg){ | |
| 162 | + if(1===+eMsg.dataset.hasImage){ | |
| 163 | + console.debug("Delaying scroll for IMG."); | |
| 164 | + eMsg.querySelector('img').addEventListener( | |
| 165 | + 'load', ()=>(this.e.newestMessage || eMsg).scrollIntoView() | |
| 166 | + ); | |
| 167 | + }else{ | |
| 168 | + eMsg.scrollIntoView(); | |
| 169 | + } | |
| 170 | + return this; | |
| 171 | + }, | |
| 146 | 172 | /* Injects element e as a new row in the chat, at the top of the |
| 147 | 173 | list if atEnd is falsy, else at the end of the list, before |
| 148 | 174 | the load-history widget. */ |
| 149 | 175 | injectMessageElem: function f(e, atEnd){ |
| 150 | 176 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 151 | - holder = this.e.messagesWrapper; | |
| 177 | + holder = this.e.messagesWrapper, | |
| 178 | + prevMessage = this.e.newestMessage; | |
| 152 | 179 | if(atEnd){ |
| 153 | 180 | const fe = mip.nextElementSibling; |
| 154 | 181 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 155 | 182 | else D.append(mip.parentNode, e); |
| 156 | 183 | }else{ |
| 157 | 184 | D.append(holder,e); |
| 158 | - Chat.newestMessageElem = e; | |
| 185 | + this.e.newestMessage = e; | |
| 159 | 186 | } |
| 160 | 187 | if(!atEnd && !this.isMassLoading |
| 161 | - && e.dataset.xfrom!==Chat.me && !isInViewport(e)){ | |
| 188 | + && e.dataset.xfrom!==this.me && !isInViewport(e)){ | |
| 162 | 189 | /* If a new non-history message arrives while the user is |
| 163 | 190 | scrolled elsewhere, do not scroll to the latest |
| 164 | 191 | message, but gently alert the user that a new message |
| 165 | 192 | has arrived. */ |
| 166 | 193 | F.toast.message("New message has arrived."); |
| 167 | - }else if(e.dataset.xfrom===Chat.me){ | |
| 168 | - e.scrollIntoView(); | |
| 194 | + }else if(!this.isMassLoading && e.dataset.xfrom===Chat.me){ | |
| 195 | + this.scheduleScrollOfMsg(e); | |
| 196 | + }else if(!this.isMassLoading){ | |
| 197 | + /* When a message from someone else arrives, we have to | |
| 198 | + figure out whether or not to scroll it into view. Ideally | |
| 199 | + we'd just stuff it in the UI and let the flexbox layout | |
| 200 | + DTRT, but Safari has expressed, in no uncertain terms, | |
| 201 | + some disappointment with that approach, so we'll | |
| 202 | + heuristicize it: if the previous last message is in view, | |
| 203 | + assume the user is at or near the input element and | |
| 204 | + scroll this one into view. If that message is NOT in | |
| 205 | + view, assume the user is up reading history somewhere and | |
| 206 | + do NOT scroll because doing so would interrupt | |
| 207 | + them. There are middle grounds here where the user will | |
| 208 | + experience a slight UI jolt, but this heuristic mostly | |
| 209 | + seems to work out okay. If there was no previous message, | |
| 210 | + assume we don't have any messages yet and go ahead and | |
| 211 | + scroll this message into view (noting that that scrolling | |
| 212 | + is hypothetically a no-op in such cases). | |
| 213 | + | |
| 214 | + The wrench in these works are posts with IMG tags, as | |
| 215 | + those images are loaded async and the element does not | |
| 216 | + yet have enough information to know how far to scroll! | |
| 217 | + For such cases we have to delay the scroll until the | |
| 218 | + image loads (and we hope it does so before another | |
| 219 | + message arrives). | |
| 220 | + */ | |
| 221 | + if(1===+e.dataset.hasImage){ | |
| 222 | + e.querySelector('img').addEventListener('load',()=>e.scrollIntoView()); | |
| 223 | + }else if(!prevMessage || (prevMessage && isInViewport(prevMessage))){ | |
| 224 | + e.scrollIntoView(); | |
| 225 | + } | |
| 169 | 226 | } |
| 170 | 227 | }, |
| 171 | 228 | /** Returns true if chat-only mode is enabled. */ |
| 172 | 229 | isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'), |
| 173 | 230 | /** |
| @@ -178,11 +235,15 @@ | ||
| 178 | 235 | */ |
| 179 | 236 | chatOnlyMode: function f(yes){ |
| 180 | 237 | if(undefined === f.elemsToToggle){ |
| 181 | 238 | f.elemsToToggle = []; |
| 182 | 239 | document.querySelectorAll( |
| 183 | - "body > div.header, body > div.mainmenu, body > div.footer" | |
| 240 | + ["body > div.header", | |
| 241 | + "body > div.mainmenu", | |
| 242 | + "body > div.footer", | |
| 243 | + "#debugMsg" | |
| 244 | + ].join(',') | |
| 184 | 245 | ).forEach((e)=>f.elemsToToggle.push(e)); |
| 185 | 246 | } |
| 186 | 247 | if(!arguments.length) yes = true; |
| 187 | 248 | if(yes === this.isChatOnlyMode()) return this; |
| 188 | 249 | if(yes){ |
| @@ -261,11 +322,15 @@ | ||
| 261 | 322 | |
| 262 | 323 | /** Finds the last .message-widget element and returns it or |
| 263 | 324 | the undefined value if none are found. */ |
| 264 | 325 | cs.fetchLastMessageElem = function(){ |
| 265 | 326 | const msgs = document.querySelectorAll('.message-widget'); |
| 266 | - return msgs.length ? msgs[msgs.length-1] : undefined; | |
| 327 | + var rc; | |
| 328 | + if(msgs.length){ | |
| 329 | + rc = this.e.newestMessage = msgs[msgs.length-1]; | |
| 330 | + } | |
| 331 | + return rc; | |
| 267 | 332 | }; |
| 268 | 333 | |
| 269 | 334 | /** |
| 270 | 335 | LOCALLY deletes a message element by the message ID or passing |
| 271 | 336 | the .message-row element. Returns true if it removes an element, |
| @@ -279,12 +344,12 @@ | ||
| 279 | 344 | }else{ |
| 280 | 345 | e = this.getMessageElemById(id); |
| 281 | 346 | } |
| 282 | 347 | if(e && id){ |
| 283 | 348 | D.remove(e); |
| 284 | - if(e===this.newestMessageElem){ | |
| 285 | - Chat.newestMessageElem = Chat.fetchLastMessageElem(); | |
| 349 | + if(e===this.e.newestMessage){ | |
| 350 | + this.fetchLastMessageElem(); | |
| 286 | 351 | } |
| 287 | 352 | F.toast.message("Deleted message "+id+"."); |
| 288 | 353 | } |
| 289 | 354 | return !!e; |
| 290 | 355 | }; |
| @@ -388,10 +453,11 @@ | ||
| 388 | 453 | if( m.fmime |
| 389 | 454 | && m.fmime.startsWith("image/") |
| 390 | 455 | && Chat.settings.getBool('images-inline',true) |
| 391 | 456 | ){ |
| 392 | 457 | contentTarget.appendChild(D.img("chat-download/" + m.msgid)); |
| 458 | + ds.hasImage = 1; | |
| 393 | 459 | }else{ |
| 394 | 460 | const a = D.a( |
| 395 | 461 | window.fossil.rootPath+ |
| 396 | 462 | 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname), |
| 397 | 463 | // ^^^ add m.fname to URL to cause downloaded file to have that name. |
| @@ -861,22 +927,17 @@ | ||
| 861 | 927 | resumed, and reportError() produces a loud error message. */ |
| 862 | 928 | .finally(function(){ |
| 863 | 929 | if(isFirstCall){ |
| 864 | 930 | Chat.isMassLoading = false; |
| 865 | 931 | Chat.ajaxEnd(); |
| 866 | - setTimeout(function(){ | |
| 867 | - const m = Chat.newestMessageElem; | |
| 868 | - if(m){ | |
| 869 | - m.scrollIntoView(); | |
| 870 | - //console.debug("Scrolling into view...",msgs[msgs.length-1]); | |
| 871 | - } | |
| 872 | - Chat.e.inputWrapper.scrollIntoView() | |
| 873 | - }, 0); | |
| 932 | + const m = Chat.e.newestMessage; | |
| 933 | + if(m) Chat.scheduleScrollOfMsg(m); | |
| 934 | + setTimeout(()=>Chat.e.inputWrapper.scrollIntoView(), 0); | |
| 874 | 935 | } |
| 875 | 936 | poll.running=false; |
| 876 | 937 | }); |
| 877 | 938 | } |
| 878 | 939 | poll.running = false; |
| 879 | 940 | poll(true); |
| 880 | 941 | setInterval(poll, 1000); |
| 881 | 942 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 882 | 943 | })(); |
| 883 | 944 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -17,10 +17,18 @@ | |
| 17 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && |
| 18 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) |
| 19 | ); |
| 20 | }; |
| 21 | |
| 22 | const ForceResizeKludge = 0 ? function(){} : (function(){ |
| 23 | /* Workaround for Safari mayhem regarding use of vh CSS units.... |
| 24 | We tried to use vh units to set the content area size for the |
| 25 | chat layout, but Safari chokes on that, so we calculate that |
| 26 | height here: 85% when in "normal" mode and 95% in chat-only |
| @@ -141,33 +149,82 @@ | |
| 141 | /* List of DOM elements disable while ajax traffic is in |
| 142 | transit. Must be populated before ajax starts. We do this |
| 143 | to avoid various race conditions in the UI and long-running |
| 144 | network requests. */ |
| 145 | ], |
| 146 | /* Injects element e as a new row in the chat, at the top of the |
| 147 | list if atEnd is falsy, else at the end of the list, before |
| 148 | the load-history widget. */ |
| 149 | injectMessageElem: function f(e, atEnd){ |
| 150 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 151 | holder = this.e.messagesWrapper; |
| 152 | if(atEnd){ |
| 153 | const fe = mip.nextElementSibling; |
| 154 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 155 | else D.append(mip.parentNode, e); |
| 156 | }else{ |
| 157 | D.append(holder,e); |
| 158 | Chat.newestMessageElem = e; |
| 159 | } |
| 160 | if(!atEnd && !this.isMassLoading |
| 161 | && e.dataset.xfrom!==Chat.me && !isInViewport(e)){ |
| 162 | /* If a new non-history message arrives while the user is |
| 163 | scrolled elsewhere, do not scroll to the latest |
| 164 | message, but gently alert the user that a new message |
| 165 | has arrived. */ |
| 166 | F.toast.message("New message has arrived."); |
| 167 | }else if(e.dataset.xfrom===Chat.me){ |
| 168 | e.scrollIntoView(); |
| 169 | } |
| 170 | }, |
| 171 | /** Returns true if chat-only mode is enabled. */ |
| 172 | isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'), |
| 173 | /** |
| @@ -178,11 +235,15 @@ | |
| 178 | */ |
| 179 | chatOnlyMode: function f(yes){ |
| 180 | if(undefined === f.elemsToToggle){ |
| 181 | f.elemsToToggle = []; |
| 182 | document.querySelectorAll( |
| 183 | "body > div.header, body > div.mainmenu, body > div.footer" |
| 184 | ).forEach((e)=>f.elemsToToggle.push(e)); |
| 185 | } |
| 186 | if(!arguments.length) yes = true; |
| 187 | if(yes === this.isChatOnlyMode()) return this; |
| 188 | if(yes){ |
| @@ -261,11 +322,15 @@ | |
| 261 | |
| 262 | /** Finds the last .message-widget element and returns it or |
| 263 | the undefined value if none are found. */ |
| 264 | cs.fetchLastMessageElem = function(){ |
| 265 | const msgs = document.querySelectorAll('.message-widget'); |
| 266 | return msgs.length ? msgs[msgs.length-1] : undefined; |
| 267 | }; |
| 268 | |
| 269 | /** |
| 270 | LOCALLY deletes a message element by the message ID or passing |
| 271 | the .message-row element. Returns true if it removes an element, |
| @@ -279,12 +344,12 @@ | |
| 279 | }else{ |
| 280 | e = this.getMessageElemById(id); |
| 281 | } |
| 282 | if(e && id){ |
| 283 | D.remove(e); |
| 284 | if(e===this.newestMessageElem){ |
| 285 | Chat.newestMessageElem = Chat.fetchLastMessageElem(); |
| 286 | } |
| 287 | F.toast.message("Deleted message "+id+"."); |
| 288 | } |
| 289 | return !!e; |
| 290 | }; |
| @@ -388,10 +453,11 @@ | |
| 388 | if( m.fmime |
| 389 | && m.fmime.startsWith("image/") |
| 390 | && Chat.settings.getBool('images-inline',true) |
| 391 | ){ |
| 392 | contentTarget.appendChild(D.img("chat-download/" + m.msgid)); |
| 393 | }else{ |
| 394 | const a = D.a( |
| 395 | window.fossil.rootPath+ |
| 396 | 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname), |
| 397 | // ^^^ add m.fname to URL to cause downloaded file to have that name. |
| @@ -861,22 +927,17 @@ | |
| 861 | resumed, and reportError() produces a loud error message. */ |
| 862 | .finally(function(){ |
| 863 | if(isFirstCall){ |
| 864 | Chat.isMassLoading = false; |
| 865 | Chat.ajaxEnd(); |
| 866 | setTimeout(function(){ |
| 867 | const m = Chat.newestMessageElem; |
| 868 | if(m){ |
| 869 | m.scrollIntoView(); |
| 870 | //console.debug("Scrolling into view...",msgs[msgs.length-1]); |
| 871 | } |
| 872 | Chat.e.inputWrapper.scrollIntoView() |
| 873 | }, 0); |
| 874 | } |
| 875 | poll.running=false; |
| 876 | }); |
| 877 | } |
| 878 | poll.running = false; |
| 879 | poll(true); |
| 880 | setInterval(poll, 1000); |
| 881 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 882 | })(); |
| 883 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -17,10 +17,18 @@ | |
| 17 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && |
| 18 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) |
| 19 | ); |
| 20 | }; |
| 21 | |
| 22 | (function(){ |
| 23 | let dbg = document.querySelector('#debugMsg'); |
| 24 | if(dbg){ |
| 25 | /* This can inadvertently influence our flexbox layouts, so move |
| 26 | it out of the way. */ |
| 27 | D.append(document.body,dbg); |
| 28 | } |
| 29 | })(); |
| 30 | const ForceResizeKludge = 0 ? function(){} : (function(){ |
| 31 | /* Workaround for Safari mayhem regarding use of vh CSS units.... |
| 32 | We tried to use vh units to set the content area size for the |
| 33 | chat layout, but Safari chokes on that, so we calculate that |
| 34 | height here: 85% when in "normal" mode and 95% in chat-only |
| @@ -141,33 +149,82 @@ | |
| 149 | /* List of DOM elements disable while ajax traffic is in |
| 150 | transit. Must be populated before ajax starts. We do this |
| 151 | to avoid various race conditions in the UI and long-running |
| 152 | network requests. */ |
| 153 | ], |
| 154 | /** Either scrolls .message-widget element eMsg into view |
| 155 | immediately or, if it represents an inlined image, delays |
| 156 | the scroll until the image is loaded, at which point it will |
| 157 | scroll to either the newest message, if one is set or to |
| 158 | eMsg (the liklihood is good, at least on initial page load, |
| 159 | that the the image won't be loaded until other messages have |
| 160 | been injected). */ |
| 161 | scheduleScrollOfMsg: function(eMsg){ |
| 162 | if(1===+eMsg.dataset.hasImage){ |
| 163 | console.debug("Delaying scroll for IMG."); |
| 164 | eMsg.querySelector('img').addEventListener( |
| 165 | 'load', ()=>(this.e.newestMessage || eMsg).scrollIntoView() |
| 166 | ); |
| 167 | }else{ |
| 168 | eMsg.scrollIntoView(); |
| 169 | } |
| 170 | return this; |
| 171 | }, |
| 172 | /* Injects element e as a new row in the chat, at the top of the |
| 173 | list if atEnd is falsy, else at the end of the list, before |
| 174 | the load-history widget. */ |
| 175 | injectMessageElem: function f(e, atEnd){ |
| 176 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 177 | holder = this.e.messagesWrapper, |
| 178 | prevMessage = this.e.newestMessage; |
| 179 | if(atEnd){ |
| 180 | const fe = mip.nextElementSibling; |
| 181 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 182 | else D.append(mip.parentNode, e); |
| 183 | }else{ |
| 184 | D.append(holder,e); |
| 185 | this.e.newestMessage = e; |
| 186 | } |
| 187 | if(!atEnd && !this.isMassLoading |
| 188 | && e.dataset.xfrom!==this.me && !isInViewport(e)){ |
| 189 | /* If a new non-history message arrives while the user is |
| 190 | scrolled elsewhere, do not scroll to the latest |
| 191 | message, but gently alert the user that a new message |
| 192 | has arrived. */ |
| 193 | F.toast.message("New message has arrived."); |
| 194 | }else if(!this.isMassLoading && e.dataset.xfrom===Chat.me){ |
| 195 | this.scheduleScrollOfMsg(e); |
| 196 | }else if(!this.isMassLoading){ |
| 197 | /* When a message from someone else arrives, we have to |
| 198 | figure out whether or not to scroll it into view. Ideally |
| 199 | we'd just stuff it in the UI and let the flexbox layout |
| 200 | DTRT, but Safari has expressed, in no uncertain terms, |
| 201 | some disappointment with that approach, so we'll |
| 202 | heuristicize it: if the previous last message is in view, |
| 203 | assume the user is at or near the input element and |
| 204 | scroll this one into view. If that message is NOT in |
| 205 | view, assume the user is up reading history somewhere and |
| 206 | do NOT scroll because doing so would interrupt |
| 207 | them. There are middle grounds here where the user will |
| 208 | experience a slight UI jolt, but this heuristic mostly |
| 209 | seems to work out okay. If there was no previous message, |
| 210 | assume we don't have any messages yet and go ahead and |
| 211 | scroll this message into view (noting that that scrolling |
| 212 | is hypothetically a no-op in such cases). |
| 213 | |
| 214 | The wrench in these works are posts with IMG tags, as |
| 215 | those images are loaded async and the element does not |
| 216 | yet have enough information to know how far to scroll! |
| 217 | For such cases we have to delay the scroll until the |
| 218 | image loads (and we hope it does so before another |
| 219 | message arrives). |
| 220 | */ |
| 221 | if(1===+e.dataset.hasImage){ |
| 222 | e.querySelector('img').addEventListener('load',()=>e.scrollIntoView()); |
| 223 | }else if(!prevMessage || (prevMessage && isInViewport(prevMessage))){ |
| 224 | e.scrollIntoView(); |
| 225 | } |
| 226 | } |
| 227 | }, |
| 228 | /** Returns true if chat-only mode is enabled. */ |
| 229 | isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'), |
| 230 | /** |
| @@ -178,11 +235,15 @@ | |
| 235 | */ |
| 236 | chatOnlyMode: function f(yes){ |
| 237 | if(undefined === f.elemsToToggle){ |
| 238 | f.elemsToToggle = []; |
| 239 | document.querySelectorAll( |
| 240 | ["body > div.header", |
| 241 | "body > div.mainmenu", |
| 242 | "body > div.footer", |
| 243 | "#debugMsg" |
| 244 | ].join(',') |
| 245 | ).forEach((e)=>f.elemsToToggle.push(e)); |
| 246 | } |
| 247 | if(!arguments.length) yes = true; |
| 248 | if(yes === this.isChatOnlyMode()) return this; |
| 249 | if(yes){ |
| @@ -261,11 +322,15 @@ | |
| 322 | |
| 323 | /** Finds the last .message-widget element and returns it or |
| 324 | the undefined value if none are found. */ |
| 325 | cs.fetchLastMessageElem = function(){ |
| 326 | const msgs = document.querySelectorAll('.message-widget'); |
| 327 | var rc; |
| 328 | if(msgs.length){ |
| 329 | rc = this.e.newestMessage = msgs[msgs.length-1]; |
| 330 | } |
| 331 | return rc; |
| 332 | }; |
| 333 | |
| 334 | /** |
| 335 | LOCALLY deletes a message element by the message ID or passing |
| 336 | the .message-row element. Returns true if it removes an element, |
| @@ -279,12 +344,12 @@ | |
| 344 | }else{ |
| 345 | e = this.getMessageElemById(id); |
| 346 | } |
| 347 | if(e && id){ |
| 348 | D.remove(e); |
| 349 | if(e===this.e.newestMessage){ |
| 350 | this.fetchLastMessageElem(); |
| 351 | } |
| 352 | F.toast.message("Deleted message "+id+"."); |
| 353 | } |
| 354 | return !!e; |
| 355 | }; |
| @@ -388,10 +453,11 @@ | |
| 453 | if( m.fmime |
| 454 | && m.fmime.startsWith("image/") |
| 455 | && Chat.settings.getBool('images-inline',true) |
| 456 | ){ |
| 457 | contentTarget.appendChild(D.img("chat-download/" + m.msgid)); |
| 458 | ds.hasImage = 1; |
| 459 | }else{ |
| 460 | const a = D.a( |
| 461 | window.fossil.rootPath+ |
| 462 | 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname), |
| 463 | // ^^^ add m.fname to URL to cause downloaded file to have that name. |
| @@ -861,22 +927,17 @@ | |
| 927 | resumed, and reportError() produces a loud error message. */ |
| 928 | .finally(function(){ |
| 929 | if(isFirstCall){ |
| 930 | Chat.isMassLoading = false; |
| 931 | Chat.ajaxEnd(); |
| 932 | const m = Chat.e.newestMessage; |
| 933 | if(m) Chat.scheduleScrollOfMsg(m); |
| 934 | setTimeout(()=>Chat.e.inputWrapper.scrollIntoView(), 0); |
| 935 | } |
| 936 | poll.running=false; |
| 937 | }); |
| 938 | } |
| 939 | poll.running = false; |
| 940 | poll(true); |
| 941 | setInterval(poll, 1000); |
| 942 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 943 | })(); |
| 944 |