Fossil SCM
chat: next round of Safari-friendly baby steps, developed in conjunction with Safari user mgagnon via chat session.
Commit
a1161fa9bd6587f81e30dd4f0b0170eb83396f8519550e5a2367230b4b62efa3
Parent
421d6570785de52…
2 files changed
+49
-2
+3
+49
-2
| --- src/chat.js | ||
| +++ src/chat.js | ||
| @@ -16,10 +16,35 @@ | ||
| 16 | 16 | rect.left >= 0 && |
| 17 | 17 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && |
| 18 | 18 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) |
| 19 | 19 | ); |
| 20 | 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 | |
| 27 | + mode. Larger than ~95% is too big for Firefox on Android, | |
| 28 | + causing the input area to move off-screen. */ | |
| 29 | + const contentArea = E1('div.content'), | |
| 30 | + bcl = document.body.classList; | |
| 31 | + const resized = function(){ | |
| 32 | + const wh = window.innerHeight, | |
| 33 | + mult = bcl.contains('chat-only-mode') ? 0.95 : 0.85; | |
| 34 | + contentArea.style.height = contentArea.style.maxHeight = (wh * mult)+"px"; | |
| 35 | + //console.debug("resized.",wh, mult, window.getComputedStyle(contentArea).maxHeight); | |
| 36 | + }; | |
| 37 | + var doit; | |
| 38 | + window.addEventListener('resize',function(ev){ | |
| 39 | + clearTimeout(doit); | |
| 40 | + doit = setTimeout(resized, 100); | |
| 41 | + }, false); | |
| 42 | + resized(); | |
| 43 | + return resized; | |
| 44 | + })(); | |
| 45 | + | |
| 21 | 46 | const Chat = (function(){ |
| 22 | 47 | const cs = { |
| 23 | 48 | e:{/*map of certain DOM elements.*/ |
| 24 | 49 | messageInjectPoint: E1('#message-inject-point'), |
| 25 | 50 | pageTitle: E1('head title'), |
| @@ -128,18 +153,21 @@ | ||
| 128 | 153 | const fe = mip.nextElementSibling; |
| 129 | 154 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 130 | 155 | else D.append(mip.parentNode, e); |
| 131 | 156 | }else{ |
| 132 | 157 | D.append(holder,e); |
| 158 | + Chat.newestMessageElem = e; | |
| 133 | 159 | } |
| 134 | 160 | if(!atEnd && !this.isMassLoading |
| 135 | 161 | && e.dataset.xfrom!==Chat.me && !isInViewport(e)){ |
| 136 | 162 | /* If a new non-history message arrives while the user is |
| 137 | 163 | scrolled elsewhere, do not scroll to the latest |
| 138 | 164 | message, but gently alert the user that a new message |
| 139 | 165 | has arrived. */ |
| 140 | 166 | F.toast.message("New message has arrived."); |
| 167 | + }else if(e.dataset.xfrom===Chat.me){ | |
| 168 | + e.scrollIntoView(); | |
| 141 | 169 | } |
| 142 | 170 | }, |
| 143 | 171 | /** Returns true if chat-only mode is enabled. */ |
| 144 | 172 | isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'), |
| 145 | 173 | /** |
| @@ -165,10 +193,11 @@ | ||
| 165 | 193 | D.removeClass(f.elemsToToggle, 'hidden'); |
| 166 | 194 | D.removeClass(document.body, 'chat-only-mode'); |
| 167 | 195 | } |
| 168 | 196 | const msg = document.querySelector('.message-widget'); |
| 169 | 197 | if(msg) setTimeout(()=>msg.scrollIntoView(),0); |
| 198 | + ForceResizeKludge(); | |
| 170 | 199 | return this; |
| 171 | 200 | }, |
| 172 | 201 | toggleChatOnlyMode: function(){ |
| 173 | 202 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 174 | 203 | }, |
| @@ -229,10 +258,18 @@ | ||
| 229 | 258 | }; |
| 230 | 259 | |
| 231 | 260 | cs.getMessageElemById = function(id){ |
| 232 | 261 | return qs('[data-msgid="'+id+'"]'); |
| 233 | 262 | }; |
| 263 | + | |
| 264 | + /** Finds the last .message-widget element and returns it or | |
| 265 | + the undefined value if none are found. */ | |
| 266 | + cs.fetchLastMessageElem = function(){ | |
| 267 | + const msgs = document.querySelectorAll('.message-widget'); | |
| 268 | + return msgs.length ? msgs[msgs.length-1] : undefined; | |
| 269 | + }; | |
| 270 | + | |
| 234 | 271 | /** |
| 235 | 272 | LOCALLY deletes a message element by the message ID or passing |
| 236 | 273 | the .message-row element. Returns true if it removes an element, |
| 237 | 274 | else false. |
| 238 | 275 | */ |
| @@ -244,10 +281,13 @@ | ||
| 244 | 281 | }else{ |
| 245 | 282 | e = this.getMessageElemById(id); |
| 246 | 283 | } |
| 247 | 284 | if(e && id){ |
| 248 | 285 | D.remove(e); |
| 286 | + if(e===this.newestMessageElem){ | |
| 287 | + Chat.newestMessageElem = Chat.fetchLastMessageElem(); | |
| 288 | + } | |
| 249 | 289 | F.toast.message("Deleted message "+id+"."); |
| 250 | 290 | } |
| 251 | 291 | return !!e; |
| 252 | 292 | }; |
| 253 | 293 | |
| @@ -819,19 +859,26 @@ | ||
| 819 | 859 | .then(y=>newcontent(y)) |
| 820 | 860 | .catch(e=>console.error(e)) |
| 821 | 861 | /* ^^^ we don't use Chat.reportError(e) here b/c the polling |
| 822 | 862 | fails exepectedly when it times out, but is then immediately |
| 823 | 863 | resumed, and reportError() produces a loud error message. */ |
| 824 | - .finally(function(x){ | |
| 864 | + .finally(function(){ | |
| 825 | 865 | if(isFirstCall){ |
| 826 | 866 | Chat.isMassLoading = false; |
| 827 | 867 | Chat.ajaxEnd(); |
| 828 | - Chat.e.inputWrapper.scrollIntoView(); | |
| 868 | + setTimeout(function(){ | |
| 869 | + const m = Chat.newestMessageElem; | |
| 870 | + if(m){ | |
| 871 | + m.scrollIntoView(); | |
| 872 | + //console.debug("Scrolling into view...",msgs[msgs.length-1]); | |
| 873 | + } | |
| 874 | + Chat.e.inputWrapper.scrollIntoView() | |
| 875 | + }, 0); | |
| 829 | 876 | } |
| 830 | 877 | poll.running=false; |
| 831 | 878 | }); |
| 832 | 879 | } |
| 833 | 880 | poll.running = false; |
| 834 | 881 | poll(true); |
| 835 | 882 | setInterval(poll, 1000); |
| 836 | 883 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 837 | 884 | })(); |
| 838 | 885 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -16,10 +16,35 @@ | |
| 16 | rect.left >= 0 && |
| 17 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && |
| 18 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) |
| 19 | ); |
| 20 | }; |
| 21 | const Chat = (function(){ |
| 22 | const cs = { |
| 23 | e:{/*map of certain DOM elements.*/ |
| 24 | messageInjectPoint: E1('#message-inject-point'), |
| 25 | pageTitle: E1('head title'), |
| @@ -128,18 +153,21 @@ | |
| 128 | const fe = mip.nextElementSibling; |
| 129 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 130 | else D.append(mip.parentNode, e); |
| 131 | }else{ |
| 132 | D.append(holder,e); |
| 133 | } |
| 134 | if(!atEnd && !this.isMassLoading |
| 135 | && e.dataset.xfrom!==Chat.me && !isInViewport(e)){ |
| 136 | /* If a new non-history message arrives while the user is |
| 137 | scrolled elsewhere, do not scroll to the latest |
| 138 | message, but gently alert the user that a new message |
| 139 | has arrived. */ |
| 140 | F.toast.message("New message has arrived."); |
| 141 | } |
| 142 | }, |
| 143 | /** Returns true if chat-only mode is enabled. */ |
| 144 | isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'), |
| 145 | /** |
| @@ -165,10 +193,11 @@ | |
| 165 | D.removeClass(f.elemsToToggle, 'hidden'); |
| 166 | D.removeClass(document.body, 'chat-only-mode'); |
| 167 | } |
| 168 | const msg = document.querySelector('.message-widget'); |
| 169 | if(msg) setTimeout(()=>msg.scrollIntoView(),0); |
| 170 | return this; |
| 171 | }, |
| 172 | toggleChatOnlyMode: function(){ |
| 173 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 174 | }, |
| @@ -229,10 +258,18 @@ | |
| 229 | }; |
| 230 | |
| 231 | cs.getMessageElemById = function(id){ |
| 232 | return qs('[data-msgid="'+id+'"]'); |
| 233 | }; |
| 234 | /** |
| 235 | LOCALLY deletes a message element by the message ID or passing |
| 236 | the .message-row element. Returns true if it removes an element, |
| 237 | else false. |
| 238 | */ |
| @@ -244,10 +281,13 @@ | |
| 244 | }else{ |
| 245 | e = this.getMessageElemById(id); |
| 246 | } |
| 247 | if(e && id){ |
| 248 | D.remove(e); |
| 249 | F.toast.message("Deleted message "+id+"."); |
| 250 | } |
| 251 | return !!e; |
| 252 | }; |
| 253 | |
| @@ -819,19 +859,26 @@ | |
| 819 | .then(y=>newcontent(y)) |
| 820 | .catch(e=>console.error(e)) |
| 821 | /* ^^^ we don't use Chat.reportError(e) here b/c the polling |
| 822 | fails exepectedly when it times out, but is then immediately |
| 823 | resumed, and reportError() produces a loud error message. */ |
| 824 | .finally(function(x){ |
| 825 | if(isFirstCall){ |
| 826 | Chat.isMassLoading = false; |
| 827 | Chat.ajaxEnd(); |
| 828 | Chat.e.inputWrapper.scrollIntoView(); |
| 829 | } |
| 830 | poll.running=false; |
| 831 | }); |
| 832 | } |
| 833 | poll.running = false; |
| 834 | poll(true); |
| 835 | setInterval(poll, 1000); |
| 836 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 837 | })(); |
| 838 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -16,10 +16,35 @@ | |
| 16 | rect.left >= 0 && |
| 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 |
| 27 | mode. Larger than ~95% is too big for Firefox on Android, |
| 28 | causing the input area to move off-screen. */ |
| 29 | const contentArea = E1('div.content'), |
| 30 | bcl = document.body.classList; |
| 31 | const resized = function(){ |
| 32 | const wh = window.innerHeight, |
| 33 | mult = bcl.contains('chat-only-mode') ? 0.95 : 0.85; |
| 34 | contentArea.style.height = contentArea.style.maxHeight = (wh * mult)+"px"; |
| 35 | //console.debug("resized.",wh, mult, window.getComputedStyle(contentArea).maxHeight); |
| 36 | }; |
| 37 | var doit; |
| 38 | window.addEventListener('resize',function(ev){ |
| 39 | clearTimeout(doit); |
| 40 | doit = setTimeout(resized, 100); |
| 41 | }, false); |
| 42 | resized(); |
| 43 | return resized; |
| 44 | })(); |
| 45 | |
| 46 | const Chat = (function(){ |
| 47 | const cs = { |
| 48 | e:{/*map of certain DOM elements.*/ |
| 49 | messageInjectPoint: E1('#message-inject-point'), |
| 50 | pageTitle: E1('head title'), |
| @@ -128,18 +153,21 @@ | |
| 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 | /** |
| @@ -165,10 +193,11 @@ | |
| 193 | D.removeClass(f.elemsToToggle, 'hidden'); |
| 194 | D.removeClass(document.body, 'chat-only-mode'); |
| 195 | } |
| 196 | const msg = document.querySelector('.message-widget'); |
| 197 | if(msg) setTimeout(()=>msg.scrollIntoView(),0); |
| 198 | ForceResizeKludge(); |
| 199 | return this; |
| 200 | }, |
| 201 | toggleChatOnlyMode: function(){ |
| 202 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 203 | }, |
| @@ -229,10 +258,18 @@ | |
| 258 | }; |
| 259 | |
| 260 | cs.getMessageElemById = function(id){ |
| 261 | return qs('[data-msgid="'+id+'"]'); |
| 262 | }; |
| 263 | |
| 264 | /** Finds the last .message-widget element and returns it or |
| 265 | the undefined value if none are found. */ |
| 266 | cs.fetchLastMessageElem = function(){ |
| 267 | const msgs = document.querySelectorAll('.message-widget'); |
| 268 | return msgs.length ? msgs[msgs.length-1] : undefined; |
| 269 | }; |
| 270 | |
| 271 | /** |
| 272 | LOCALLY deletes a message element by the message ID or passing |
| 273 | the .message-row element. Returns true if it removes an element, |
| 274 | else false. |
| 275 | */ |
| @@ -244,10 +281,13 @@ | |
| 281 | }else{ |
| 282 | e = this.getMessageElemById(id); |
| 283 | } |
| 284 | if(e && id){ |
| 285 | D.remove(e); |
| 286 | if(e===this.newestMessageElem){ |
| 287 | Chat.newestMessageElem = Chat.fetchLastMessageElem(); |
| 288 | } |
| 289 | F.toast.message("Deleted message "+id+"."); |
| 290 | } |
| 291 | return !!e; |
| 292 | }; |
| 293 | |
| @@ -819,19 +859,26 @@ | |
| 859 | .then(y=>newcontent(y)) |
| 860 | .catch(e=>console.error(e)) |
| 861 | /* ^^^ we don't use Chat.reportError(e) here b/c the polling |
| 862 | fails exepectedly when it times out, but is then immediately |
| 863 | resumed, and reportError() produces a loud error message. */ |
| 864 | .finally(function(){ |
| 865 | if(isFirstCall){ |
| 866 | Chat.isMassLoading = false; |
| 867 | Chat.ajaxEnd(); |
| 868 | setTimeout(function(){ |
| 869 | const m = Chat.newestMessageElem; |
| 870 | if(m){ |
| 871 | m.scrollIntoView(); |
| 872 | //console.debug("Scrolling into view...",msgs[msgs.length-1]); |
| 873 | } |
| 874 | Chat.e.inputWrapper.scrollIntoView() |
| 875 | }, 0); |
| 876 | } |
| 877 | poll.running=false; |
| 878 | }); |
| 879 | } |
| 880 | poll.running = false; |
| 881 | poll(true); |
| 882 | setInterval(poll, 1000); |
| 883 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 884 | })(); |
| 885 |
+3
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -1636,10 +1636,13 @@ | ||
| 1636 | 1636 | body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] { |
| 1637 | 1637 | cursor: inherit; |
| 1638 | 1638 | } |
| 1639 | 1639 | /** Container for the list of /chat messages. */ |
| 1640 | 1640 | body.chat #chat-messages-wrapper { |
| 1641 | + overflow: auto; | |
| 1642 | + /*max-height: 800em*//*will be re-calc'd in JS*/; | |
| 1643 | + flex: 2 1 auto; | |
| 1641 | 1644 | } |
| 1642 | 1645 | body.chat div.content { |
| 1643 | 1646 | margin: 0; |
| 1644 | 1647 | padding: 0; |
| 1645 | 1648 | display: flex; |
| 1646 | 1649 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1636,10 +1636,13 @@ | |
| 1636 | body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] { |
| 1637 | cursor: inherit; |
| 1638 | } |
| 1639 | /** Container for the list of /chat messages. */ |
| 1640 | body.chat #chat-messages-wrapper { |
| 1641 | } |
| 1642 | body.chat div.content { |
| 1643 | margin: 0; |
| 1644 | padding: 0; |
| 1645 | display: flex; |
| 1646 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1636,10 +1636,13 @@ | |
| 1636 | body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] { |
| 1637 | cursor: inherit; |
| 1638 | } |
| 1639 | /** Container for the list of /chat messages. */ |
| 1640 | body.chat #chat-messages-wrapper { |
| 1641 | overflow: auto; |
| 1642 | /*max-height: 800em*//*will be re-calc'd in JS*/; |
| 1643 | flex: 2 1 auto; |
| 1644 | } |
| 1645 | body.chat div.content { |
| 1646 | margin: 0; |
| 1647 | padding: 0; |
| 1648 | display: flex; |
| 1649 |