Fossil SCM
Teach /chat to not be so verbose about connection errors. The first 3 will be subtly signaled via a tiny red line between the input field and message list, which will go away once the poller connection is re-established. After that, it will resort to the more verbose notifications.
Commit
e3eb83997b9dfbabac425310184596efe11b78fc01d949e7840ff52d2bf190ba
Parent
7ce8400d02223ba…
2 files changed
+79
-53
+12
-7
+79
-53
| --- src/fossil.page.chat.js | ||
| +++ src/fossil.page.chat.js | ||
| @@ -1,6 +1,6 @@ | ||
| 1 | -/** | |
| 1 | +-/** | |
| 2 | 2 | This file contains the client-side implementation of fossil's /chat |
| 3 | 3 | application. |
| 4 | 4 | */ |
| 5 | 5 | window.fossil.onPageLoad(function(){ |
| 6 | 6 | const F = window.fossil, D = F.dom; |
| @@ -129,20 +129,21 @@ | ||
| 129 | 129 | return resized; |
| 130 | 130 | })(); |
| 131 | 131 | fossil.FRK = ForceResizeKludge/*for debugging*/; |
| 132 | 132 | const Chat = ForceResizeKludge.chat = (function(){ |
| 133 | 133 | const cs = { // the "Chat" object (result of this function) |
| 134 | - beVerbose: false /* if true then certain, mostly extraneous, | |
| 135 | - error messages and log messages may be sent | |
| 136 | - to the console. */, | |
| 134 | + beVerbose: false | |
| 135 | + //!!window.location.hostname.match("localhost") | |
| 136 | + /* if true then certain, mostly extraneous, error messages and | |
| 137 | + log messages may be sent to the console. */, | |
| 137 | 138 | playedBeep: false /* used for the beep-once setting */, |
| 138 | 139 | e:{/*map of certain DOM elements.*/ |
| 139 | 140 | messageInjectPoint: E1('#message-inject-point'), |
| 140 | 141 | pageTitle: E1('head title'), |
| 141 | 142 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 142 | - inputWrapper: E1("#chat-input-area"), | |
| 143 | - inputElementWrapper: E1('#chat-input-line-wrapper'), | |
| 143 | + inputArea: E1("#chat-input-area"), | |
| 144 | + inputLineWrapper: E1('#chat-input-line-wrapper'), | |
| 144 | 145 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 145 | 146 | viewMessages: E1('#chat-messages-wrapper'), |
| 146 | 147 | btnSubmit: E1('#chat-button-submit'), |
| 147 | 148 | btnAttach: E1('#chat-button-attach'), |
| 148 | 149 | inputX: E1('#chat-input-field-x'), |
| @@ -157,11 +158,12 @@ | ||
| 157 | 158 | searchContent: E1('#chat-search-content'), |
| 158 | 159 | btnPreview: E1('#chat-button-preview'), |
| 159 | 160 | views: document.querySelectorAll('.chat-view'), |
| 160 | 161 | activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 161 | 162 | activeUserList: E1('#chat-user-list'), |
| 162 | - eMsgPollError: undefined /* current connection error MessageMidget */ | |
| 163 | + eMsgPollError: undefined /* current connection error MessageMidget */, | |
| 164 | + pollErrorMarker: undefined /* element to toggle 'connection-error' CSS class on */ | |
| 163 | 165 | }, |
| 164 | 166 | me: F.user.name, |
| 165 | 167 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 166 | 168 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 167 | 169 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -201,10 +203,15 @@ | ||
| 201 | 203 | currentDelay: 1000 /* current polling interval */, |
| 202 | 204 | maxDelay: 60000 * 5 /* max interval when backing off for |
| 203 | 205 | connection errors */, |
| 204 | 206 | minDelay: 5000 /* minimum delay time */, |
| 205 | 207 | tidReconnect: undefined /*timer id for reconnection determination*/, |
| 208 | + errCount: 0 /* Current poller connection error count */, | |
| 209 | + minErrForNotify: 4 /* Don't warn for connection errors until this | |
| 210 | + many have occurred */, | |
| 211 | + skipErrDelay: 3500 /* time to wait/retry for the first minErrForNotify'th | |
| 212 | + connection errors. */, | |
| 206 | 213 | randomInterval: function(factor){ |
| 207 | 214 | return Math.floor(Math.random() * factor); |
| 208 | 215 | }, |
| 209 | 216 | incrDelay: function(){ |
| 210 | 217 | if( this.maxDelay > this.currentDelay ){ |
| @@ -214,12 +221,12 @@ | ||
| 214 | 221 | this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay); |
| 215 | 222 | } |
| 216 | 223 | } |
| 217 | 224 | return this.currentDelay; |
| 218 | 225 | }, |
| 219 | - resetDelay: function(){ | |
| 220 | - return this.currentDelay = this.$initialDelay; | |
| 226 | + resetDelay: function(ms){ | |
| 227 | + return this.currentDelay = ms || this.$initialDelay; | |
| 221 | 228 | }, |
| 222 | 229 | isDelayed: function(){ |
| 223 | 230 | return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; |
| 224 | 231 | } |
| 225 | 232 | }, |
| @@ -655,11 +662,11 @@ | ||
| 655 | 662 | if(!f.$disabled){ |
| 656 | 663 | D.addClassBriefly(e, a, 0, cb); |
| 657 | 664 | } |
| 658 | 665 | return this; |
| 659 | 666 | } |
| 660 | - }; | |
| 667 | + }/*Chat object*/; | |
| 661 | 668 | cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; |
| 662 | 669 | cs.e.inputFields.$currentIndex = 0; |
| 663 | 670 | cs.e.inputFields.forEach(function(e,ndx){ |
| 664 | 671 | if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); |
| 665 | 672 | else D.addClass(e,'hidden'); |
| @@ -669,10 +676,11 @@ | ||
| 669 | 676 | }else{ |
| 670 | 677 | /* Only the Chrome family supports contenteditable=plaintext-only */ |
| 671 | 678 | cs.$browserHasPlaintextOnly = false; |
| 672 | 679 | D.attr(cs.e.inputX,'contenteditable','true'); |
| 673 | 680 | } |
| 681 | + cs.e.pollErrorMarker = cs.e.viewMessages; | |
| 674 | 682 | cs.animate.$disabled = true; |
| 675 | 683 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 676 | 684 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 677 | 685 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 678 | 686 | const qs = (e)=>document.querySelector(e); |
| @@ -1727,45 +1735,56 @@ | ||
| 1727 | 1735 | }; |
| 1728 | 1736 | |
| 1729 | 1737 | /* Assume the connection has been established, reset the |
| 1730 | 1738 | Chat.timer.tidReconnect, and (if showMsg and |
| 1731 | 1739 | !!Chat.e.eMsgPollError) alert the user that the outage appears to |
| 1732 | - be over. */ | |
| 1740 | + be over. Then schedule Chat.poll() to run in the very near | |
| 1741 | + future. */ | |
| 1733 | 1742 | const reportConnectionReestablished = function(dbgContext, showMsg = true){ |
| 1734 | 1743 | if(Chat.beVerbose){ |
| 1735 | - console.warn("reportConnectionReestablished()", | |
| 1736 | - dbgContext, showMsg, Chat.timer.tidReconnect, Chat.e.eMsgPollError); | |
| 1744 | + console.warn('reportConnectionReestablished', dbgContext, | |
| 1745 | + 'Chat.e.pollErrorMarker =',Chat.e.pollErrorMarker, | |
| 1746 | + 'Chat.timer.tidReconnect =',Chat.timer.tidReconnect, | |
| 1747 | + 'Chat.timer =',Chat.timer); | |
| 1748 | + } | |
| 1749 | + if( Chat.timer.errCount ){ | |
| 1750 | + D.removeClass(Chat.e.pollErrorMarker, 'connection-error'); | |
| 1751 | + Chat.timer.errCount = 0; | |
| 1737 | 1752 | } |
| 1738 | 1753 | if( Chat.timer.tidReconnect ){ |
| 1739 | 1754 | clearTimeout(Chat.timer.tidReconnect); |
| 1740 | 1755 | Chat.timer.tidReconnect = 0; |
| 1741 | 1756 | } |
| 1742 | - Chat.timer.resetDelay(); | |
| 1743 | 1757 | if( Chat.e.eMsgPollError ) { |
| 1744 | 1758 | const oldErrMsg = Chat.e.eMsgPollError; |
| 1745 | 1759 | Chat.e.eMsgPollError = undefined; |
| 1746 | 1760 | if( showMsg ){ |
| 1761 | + if(Chat.beVerbose){ | |
| 1762 | + console.log("Poller Connection restored."); | |
| 1763 | + } | |
| 1747 | 1764 | const m = Chat.reportReconnection("Poller connection restored."); |
| 1748 | 1765 | if( oldErrMsg ){ |
| 1749 | 1766 | D.remove(oldErrMsg.e?.body.querySelector('button.retry-now')); |
| 1750 | 1767 | } |
| 1751 | 1768 | m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid; |
| 1752 | 1769 | D.addClass(m.e.body,'poller-connection'); |
| 1753 | 1770 | } |
| 1754 | 1771 | } |
| 1755 | - setTimeout( Chat.poll, 0 ); | |
| 1772 | + setTimeout( Chat.poll, Chat.timer.resetDelay() ); | |
| 1756 | 1773 | }; |
| 1757 | 1774 | |
| 1758 | - /* To be called from F.fetch('chat-poll') beforesend() handlers. If we're | |
| 1759 | - currently in delayed-retry mode and a connection is started, try | |
| 1760 | - to reset the delay after N time waiting on that connection. The | |
| 1761 | - fact that the connection is waiting to respond, rather than | |
| 1762 | - outright failing, is a good hint that the outage is over and we | |
| 1763 | - can reset the back-off timer. */ | |
| 1775 | + /* To be called from F.fetch('chat-poll') beforesend() handler. If | |
| 1776 | + we're currently in delayed-retry mode and a connection is | |
| 1777 | + started, try to reset the delay after N time waiting on that | |
| 1778 | + connection. The fact that the connection is waiting to respond, | |
| 1779 | + rather than outright failing, is a good hint that the outage is | |
| 1780 | + over and we can reset the back-off timer. */ | |
| 1764 | 1781 | const clearPollErrOnWait = function(){ |
| 1782 | + //console.warn('clearPollErrOnWait outer', Chat.timer.tidReconnect, Chat.timer.currentDelay); | |
| 1765 | 1783 | if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){ |
| 1766 | 1784 | Chat.timer.tidReconnect = setTimeout(()=>{ |
| 1785 | + //console.warn('clearPollErrOnWait inner'); | |
| 1767 | 1786 | Chat.timer.tidReconnect = 0; |
| 1768 | 1787 | if( poll.running ){ |
| 1769 | 1788 | /* This chat-poll F.fetch() is still underway, so let's |
| 1770 | 1789 | assume the connection is back up until/unless it times |
| 1771 | 1790 | out or breaks again. */ |
| @@ -2275,11 +2294,11 @@ | ||
| 2275 | 2294 | Chat.e.inputFields.$currentIndex = a[2]; |
| 2276 | 2295 | Chat.inputValue(v); |
| 2277 | 2296 | D.removeClass(a[0], 'hidden'); |
| 2278 | 2297 | D.addClass(a[1], 'hidden'); |
| 2279 | 2298 | } |
| 2280 | - Chat.e.inputElementWrapper.classList[ | |
| 2299 | + Chat.e.inputLineWrapper.classList[ | |
| 2281 | 2300 | s.value ? 'add' : 'remove' |
| 2282 | 2301 | ]('compact'); |
| 2283 | 2302 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 2284 | 2303 | }); |
| 2285 | 2304 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| @@ -2630,40 +2649,47 @@ | ||
| 2630 | 2649 | }else{ |
| 2631 | 2650 | /* Delay a while before trying again, noting that other Chat |
| 2632 | 2651 | APIs may try and succeed at connections before this timer |
| 2633 | 2652 | resolves, in which case they'll clear this timeout and the |
| 2634 | 2653 | UI message about the outage. */ |
| 2635 | - const delay = Chat.timer.incrDelay(); | |
| 2636 | - //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError); | |
| 2637 | - const msg = "Poller connection error. Retrying in "+delay+ " ms."; | |
| 2638 | - /* Replace the current/newest connection error widget. We could also | |
| 2639 | - just update its body with the new message, but then its timestamp | |
| 2640 | - never updates. OTOH, if we replace the message, we lose the | |
| 2641 | - start time of the outage in the log. It seems more useful to | |
| 2642 | - update the timestamp so that it doesn't look like it's hung. */ | |
| 2643 | - if( Chat.e.eMsgPollError ){ | |
| 2644 | - Chat.deleteMessageElem(Chat.e.eMsgPollError, false); | |
| 2645 | - } | |
| 2646 | - const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); | |
| 2647 | - D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); | |
| 2648 | - /* Add a "retry now" button */ | |
| 2649 | - const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); | |
| 2650 | - D.append(Chat.e.eMsgPollError.e.content, " ", btnDel); | |
| 2651 | - btnDel.addEventListener('click', function(){ | |
| 2652 | - D.remove(btnDel); | |
| 2653 | - Chat.timer.currentDelay = | |
| 2654 | - Chat.timer.resetDelay() + 1 /*workaround for showing the "connection restored" message*/; | |
| 2655 | - if( Chat.timer.tidPoller ){ | |
| 2656 | - clearTimeout(Chat.timer.tidPoller); | |
| 2657 | - Chat.timer.tidPoller = 0; | |
| 2658 | - } | |
| 2659 | - poll(); | |
| 2660 | - }); | |
| 2661 | - //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction | |
| 2662 | - Chat.timer.tidPoller = setTimeout(()=>{ | |
| 2663 | - poll(); | |
| 2664 | - }, delay); | |
| 2654 | + let delay; | |
| 2655 | + D.addClass(Chat.e.pollErrorMarker, 'connection-error'); | |
| 2656 | + if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){ | |
| 2657 | + if(Chat.beVerbose){ | |
| 2658 | + console.warn("Ignoring polling error #", Chat.timer.errCount); | |
| 2659 | + } | |
| 2660 | + delay = Chat.timer.resetDelay(Chat.timer.skipErrDelay); | |
| 2661 | + } else { | |
| 2662 | + delay = Chat.timer.incrDelay(); | |
| 2663 | + //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError); | |
| 2664 | + const msg = "Poller connection error. Retrying in "+delay+ " ms."; | |
| 2665 | + /* Replace the current/newest connection error widget. We could also | |
| 2666 | + just update its body with the new message, but then its timestamp | |
| 2667 | + never updates. OTOH, if we replace the message, we lose the | |
| 2668 | + start time of the outage in the log. It seems more useful to | |
| 2669 | + update the timestamp so that it doesn't look like it's hung. */ | |
| 2670 | + if( Chat.e.eMsgPollError ){ | |
| 2671 | + Chat.deleteMessageElem(Chat.e.eMsgPollError, false); | |
| 2672 | + } | |
| 2673 | + const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); | |
| 2674 | + D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); | |
| 2675 | + /* Add a "retry now" button */ | |
| 2676 | + const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); | |
| 2677 | + D.append(Chat.e.eMsgPollError.e.content, " ", btnDel); | |
| 2678 | + btnDel.addEventListener('click', function(){ | |
| 2679 | + D.remove(btnDel); | |
| 2680 | + Chat.timer.currentDelay = | |
| 2681 | + Chat.timer.resetDelay() + 1 /*workaround for showing the "connection restored" message*/; | |
| 2682 | + if( Chat.timer.tidPoller ){ | |
| 2683 | + clearTimeout(Chat.timer.tidPoller); | |
| 2684 | + Chat.timer.tidPoller = 0; | |
| 2685 | + } | |
| 2686 | + poll(); | |
| 2687 | + }); | |
| 2688 | + //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction | |
| 2689 | + } | |
| 2690 | + Chat.timer.tidPoller = setTimeout(Chat.poll, delay); | |
| 2665 | 2691 | } |
| 2666 | 2692 | } |
| 2667 | 2693 | }; |
| 2668 | 2694 | afterPollFetch.isFirstCall = true; |
| 2669 | 2695 | |
| @@ -2752,11 +2778,11 @@ | ||
| 2752 | 2778 | Chat.updateActiveUserList(); |
| 2753 | 2779 | } |
| 2754 | 2780 | afterPollFetch(); |
| 2755 | 2781 | } |
| 2756 | 2782 | }); |
| 2757 | - }; | |
| 2783 | + }/*poll()*/; | |
| 2758 | 2784 | poll.isFirstCall = true; |
| 2759 | 2785 | Chat._gotServerError = poll.running = false; |
| 2760 | 2786 | if( window.fossil.config.chat.fromcli ){ |
| 2761 | 2787 | Chat.chatOnlyMode(true); |
| 2762 | 2788 | } |
| 2763 | 2789 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -1,6 +1,6 @@ | |
| 1 | /** |
| 2 | This file contains the client-side implementation of fossil's /chat |
| 3 | application. |
| 4 | */ |
| 5 | window.fossil.onPageLoad(function(){ |
| 6 | const F = window.fossil, D = F.dom; |
| @@ -129,20 +129,21 @@ | |
| 129 | return resized; |
| 130 | })(); |
| 131 | fossil.FRK = ForceResizeKludge/*for debugging*/; |
| 132 | const Chat = ForceResizeKludge.chat = (function(){ |
| 133 | const cs = { // the "Chat" object (result of this function) |
| 134 | beVerbose: false /* if true then certain, mostly extraneous, |
| 135 | error messages and log messages may be sent |
| 136 | to the console. */, |
| 137 | playedBeep: false /* used for the beep-once setting */, |
| 138 | e:{/*map of certain DOM elements.*/ |
| 139 | messageInjectPoint: E1('#message-inject-point'), |
| 140 | pageTitle: E1('head title'), |
| 141 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 142 | inputWrapper: E1("#chat-input-area"), |
| 143 | inputElementWrapper: E1('#chat-input-line-wrapper'), |
| 144 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 145 | viewMessages: E1('#chat-messages-wrapper'), |
| 146 | btnSubmit: E1('#chat-button-submit'), |
| 147 | btnAttach: E1('#chat-button-attach'), |
| 148 | inputX: E1('#chat-input-field-x'), |
| @@ -157,11 +158,12 @@ | |
| 157 | searchContent: E1('#chat-search-content'), |
| 158 | btnPreview: E1('#chat-button-preview'), |
| 159 | views: document.querySelectorAll('.chat-view'), |
| 160 | activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 161 | activeUserList: E1('#chat-user-list'), |
| 162 | eMsgPollError: undefined /* current connection error MessageMidget */ |
| 163 | }, |
| 164 | me: F.user.name, |
| 165 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 166 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 167 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -201,10 +203,15 @@ | |
| 201 | currentDelay: 1000 /* current polling interval */, |
| 202 | maxDelay: 60000 * 5 /* max interval when backing off for |
| 203 | connection errors */, |
| 204 | minDelay: 5000 /* minimum delay time */, |
| 205 | tidReconnect: undefined /*timer id for reconnection determination*/, |
| 206 | randomInterval: function(factor){ |
| 207 | return Math.floor(Math.random() * factor); |
| 208 | }, |
| 209 | incrDelay: function(){ |
| 210 | if( this.maxDelay > this.currentDelay ){ |
| @@ -214,12 +221,12 @@ | |
| 214 | this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay); |
| 215 | } |
| 216 | } |
| 217 | return this.currentDelay; |
| 218 | }, |
| 219 | resetDelay: function(){ |
| 220 | return this.currentDelay = this.$initialDelay; |
| 221 | }, |
| 222 | isDelayed: function(){ |
| 223 | return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; |
| 224 | } |
| 225 | }, |
| @@ -655,11 +662,11 @@ | |
| 655 | if(!f.$disabled){ |
| 656 | D.addClassBriefly(e, a, 0, cb); |
| 657 | } |
| 658 | return this; |
| 659 | } |
| 660 | }; |
| 661 | cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; |
| 662 | cs.e.inputFields.$currentIndex = 0; |
| 663 | cs.e.inputFields.forEach(function(e,ndx){ |
| 664 | if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); |
| 665 | else D.addClass(e,'hidden'); |
| @@ -669,10 +676,11 @@ | |
| 669 | }else{ |
| 670 | /* Only the Chrome family supports contenteditable=plaintext-only */ |
| 671 | cs.$browserHasPlaintextOnly = false; |
| 672 | D.attr(cs.e.inputX,'contenteditable','true'); |
| 673 | } |
| 674 | cs.animate.$disabled = true; |
| 675 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 676 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 677 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 678 | const qs = (e)=>document.querySelector(e); |
| @@ -1727,45 +1735,56 @@ | |
| 1727 | }; |
| 1728 | |
| 1729 | /* Assume the connection has been established, reset the |
| 1730 | Chat.timer.tidReconnect, and (if showMsg and |
| 1731 | !!Chat.e.eMsgPollError) alert the user that the outage appears to |
| 1732 | be over. */ |
| 1733 | const reportConnectionReestablished = function(dbgContext, showMsg = true){ |
| 1734 | if(Chat.beVerbose){ |
| 1735 | console.warn("reportConnectionReestablished()", |
| 1736 | dbgContext, showMsg, Chat.timer.tidReconnect, Chat.e.eMsgPollError); |
| 1737 | } |
| 1738 | if( Chat.timer.tidReconnect ){ |
| 1739 | clearTimeout(Chat.timer.tidReconnect); |
| 1740 | Chat.timer.tidReconnect = 0; |
| 1741 | } |
| 1742 | Chat.timer.resetDelay(); |
| 1743 | if( Chat.e.eMsgPollError ) { |
| 1744 | const oldErrMsg = Chat.e.eMsgPollError; |
| 1745 | Chat.e.eMsgPollError = undefined; |
| 1746 | if( showMsg ){ |
| 1747 | const m = Chat.reportReconnection("Poller connection restored."); |
| 1748 | if( oldErrMsg ){ |
| 1749 | D.remove(oldErrMsg.e?.body.querySelector('button.retry-now')); |
| 1750 | } |
| 1751 | m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid; |
| 1752 | D.addClass(m.e.body,'poller-connection'); |
| 1753 | } |
| 1754 | } |
| 1755 | setTimeout( Chat.poll, 0 ); |
| 1756 | }; |
| 1757 | |
| 1758 | /* To be called from F.fetch('chat-poll') beforesend() handlers. If we're |
| 1759 | currently in delayed-retry mode and a connection is started, try |
| 1760 | to reset the delay after N time waiting on that connection. The |
| 1761 | fact that the connection is waiting to respond, rather than |
| 1762 | outright failing, is a good hint that the outage is over and we |
| 1763 | can reset the back-off timer. */ |
| 1764 | const clearPollErrOnWait = function(){ |
| 1765 | if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){ |
| 1766 | Chat.timer.tidReconnect = setTimeout(()=>{ |
| 1767 | Chat.timer.tidReconnect = 0; |
| 1768 | if( poll.running ){ |
| 1769 | /* This chat-poll F.fetch() is still underway, so let's |
| 1770 | assume the connection is back up until/unless it times |
| 1771 | out or breaks again. */ |
| @@ -2275,11 +2294,11 @@ | |
| 2275 | Chat.e.inputFields.$currentIndex = a[2]; |
| 2276 | Chat.inputValue(v); |
| 2277 | D.removeClass(a[0], 'hidden'); |
| 2278 | D.addClass(a[1], 'hidden'); |
| 2279 | } |
| 2280 | Chat.e.inputElementWrapper.classList[ |
| 2281 | s.value ? 'add' : 'remove' |
| 2282 | ]('compact'); |
| 2283 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 2284 | }); |
| 2285 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| @@ -2630,40 +2649,47 @@ | |
| 2630 | }else{ |
| 2631 | /* Delay a while before trying again, noting that other Chat |
| 2632 | APIs may try and succeed at connections before this timer |
| 2633 | resolves, in which case they'll clear this timeout and the |
| 2634 | UI message about the outage. */ |
| 2635 | const delay = Chat.timer.incrDelay(); |
| 2636 | //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError); |
| 2637 | const msg = "Poller connection error. Retrying in "+delay+ " ms."; |
| 2638 | /* Replace the current/newest connection error widget. We could also |
| 2639 | just update its body with the new message, but then its timestamp |
| 2640 | never updates. OTOH, if we replace the message, we lose the |
| 2641 | start time of the outage in the log. It seems more useful to |
| 2642 | update the timestamp so that it doesn't look like it's hung. */ |
| 2643 | if( Chat.e.eMsgPollError ){ |
| 2644 | Chat.deleteMessageElem(Chat.e.eMsgPollError, false); |
| 2645 | } |
| 2646 | const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); |
| 2647 | D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); |
| 2648 | /* Add a "retry now" button */ |
| 2649 | const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); |
| 2650 | D.append(Chat.e.eMsgPollError.e.content, " ", btnDel); |
| 2651 | btnDel.addEventListener('click', function(){ |
| 2652 | D.remove(btnDel); |
| 2653 | Chat.timer.currentDelay = |
| 2654 | Chat.timer.resetDelay() + 1 /*workaround for showing the "connection restored" message*/; |
| 2655 | if( Chat.timer.tidPoller ){ |
| 2656 | clearTimeout(Chat.timer.tidPoller); |
| 2657 | Chat.timer.tidPoller = 0; |
| 2658 | } |
| 2659 | poll(); |
| 2660 | }); |
| 2661 | //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction |
| 2662 | Chat.timer.tidPoller = setTimeout(()=>{ |
| 2663 | poll(); |
| 2664 | }, delay); |
| 2665 | } |
| 2666 | } |
| 2667 | }; |
| 2668 | afterPollFetch.isFirstCall = true; |
| 2669 | |
| @@ -2752,11 +2778,11 @@ | |
| 2752 | Chat.updateActiveUserList(); |
| 2753 | } |
| 2754 | afterPollFetch(); |
| 2755 | } |
| 2756 | }); |
| 2757 | }; |
| 2758 | poll.isFirstCall = true; |
| 2759 | Chat._gotServerError = poll.running = false; |
| 2760 | if( window.fossil.config.chat.fromcli ){ |
| 2761 | Chat.chatOnlyMode(true); |
| 2762 | } |
| 2763 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -1,6 +1,6 @@ | |
| 1 | -/** |
| 2 | This file contains the client-side implementation of fossil's /chat |
| 3 | application. |
| 4 | */ |
| 5 | window.fossil.onPageLoad(function(){ |
| 6 | const F = window.fossil, D = F.dom; |
| @@ -129,20 +129,21 @@ | |
| 129 | return resized; |
| 130 | })(); |
| 131 | fossil.FRK = ForceResizeKludge/*for debugging*/; |
| 132 | const Chat = ForceResizeKludge.chat = (function(){ |
| 133 | const cs = { // the "Chat" object (result of this function) |
| 134 | beVerbose: false |
| 135 | //!!window.location.hostname.match("localhost") |
| 136 | /* if true then certain, mostly extraneous, error messages and |
| 137 | log messages may be sent to the console. */, |
| 138 | playedBeep: false /* used for the beep-once setting */, |
| 139 | e:{/*map of certain DOM elements.*/ |
| 140 | messageInjectPoint: E1('#message-inject-point'), |
| 141 | pageTitle: E1('head title'), |
| 142 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 143 | inputArea: E1("#chat-input-area"), |
| 144 | inputLineWrapper: E1('#chat-input-line-wrapper'), |
| 145 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 146 | viewMessages: E1('#chat-messages-wrapper'), |
| 147 | btnSubmit: E1('#chat-button-submit'), |
| 148 | btnAttach: E1('#chat-button-attach'), |
| 149 | inputX: E1('#chat-input-field-x'), |
| @@ -157,11 +158,12 @@ | |
| 158 | searchContent: E1('#chat-search-content'), |
| 159 | btnPreview: E1('#chat-button-preview'), |
| 160 | views: document.querySelectorAll('.chat-view'), |
| 161 | activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 162 | activeUserList: E1('#chat-user-list'), |
| 163 | eMsgPollError: undefined /* current connection error MessageMidget */, |
| 164 | pollErrorMarker: undefined /* element to toggle 'connection-error' CSS class on */ |
| 165 | }, |
| 166 | me: F.user.name, |
| 167 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 168 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 169 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -201,10 +203,15 @@ | |
| 203 | currentDelay: 1000 /* current polling interval */, |
| 204 | maxDelay: 60000 * 5 /* max interval when backing off for |
| 205 | connection errors */, |
| 206 | minDelay: 5000 /* minimum delay time */, |
| 207 | tidReconnect: undefined /*timer id for reconnection determination*/, |
| 208 | errCount: 0 /* Current poller connection error count */, |
| 209 | minErrForNotify: 4 /* Don't warn for connection errors until this |
| 210 | many have occurred */, |
| 211 | skipErrDelay: 3500 /* time to wait/retry for the first minErrForNotify'th |
| 212 | connection errors. */, |
| 213 | randomInterval: function(factor){ |
| 214 | return Math.floor(Math.random() * factor); |
| 215 | }, |
| 216 | incrDelay: function(){ |
| 217 | if( this.maxDelay > this.currentDelay ){ |
| @@ -214,12 +221,12 @@ | |
| 221 | this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay); |
| 222 | } |
| 223 | } |
| 224 | return this.currentDelay; |
| 225 | }, |
| 226 | resetDelay: function(ms){ |
| 227 | return this.currentDelay = ms || this.$initialDelay; |
| 228 | }, |
| 229 | isDelayed: function(){ |
| 230 | return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; |
| 231 | } |
| 232 | }, |
| @@ -655,11 +662,11 @@ | |
| 662 | if(!f.$disabled){ |
| 663 | D.addClassBriefly(e, a, 0, cb); |
| 664 | } |
| 665 | return this; |
| 666 | } |
| 667 | }/*Chat object*/; |
| 668 | cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; |
| 669 | cs.e.inputFields.$currentIndex = 0; |
| 670 | cs.e.inputFields.forEach(function(e,ndx){ |
| 671 | if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); |
| 672 | else D.addClass(e,'hidden'); |
| @@ -669,10 +676,11 @@ | |
| 676 | }else{ |
| 677 | /* Only the Chrome family supports contenteditable=plaintext-only */ |
| 678 | cs.$browserHasPlaintextOnly = false; |
| 679 | D.attr(cs.e.inputX,'contenteditable','true'); |
| 680 | } |
| 681 | cs.e.pollErrorMarker = cs.e.viewMessages; |
| 682 | cs.animate.$disabled = true; |
| 683 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 684 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 685 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 686 | const qs = (e)=>document.querySelector(e); |
| @@ -1727,45 +1735,56 @@ | |
| 1735 | }; |
| 1736 | |
| 1737 | /* Assume the connection has been established, reset the |
| 1738 | Chat.timer.tidReconnect, and (if showMsg and |
| 1739 | !!Chat.e.eMsgPollError) alert the user that the outage appears to |
| 1740 | be over. Then schedule Chat.poll() to run in the very near |
| 1741 | future. */ |
| 1742 | const reportConnectionReestablished = function(dbgContext, showMsg = true){ |
| 1743 | if(Chat.beVerbose){ |
| 1744 | console.warn('reportConnectionReestablished', dbgContext, |
| 1745 | 'Chat.e.pollErrorMarker =',Chat.e.pollErrorMarker, |
| 1746 | 'Chat.timer.tidReconnect =',Chat.timer.tidReconnect, |
| 1747 | 'Chat.timer =',Chat.timer); |
| 1748 | } |
| 1749 | if( Chat.timer.errCount ){ |
| 1750 | D.removeClass(Chat.e.pollErrorMarker, 'connection-error'); |
| 1751 | Chat.timer.errCount = 0; |
| 1752 | } |
| 1753 | if( Chat.timer.tidReconnect ){ |
| 1754 | clearTimeout(Chat.timer.tidReconnect); |
| 1755 | Chat.timer.tidReconnect = 0; |
| 1756 | } |
| 1757 | if( Chat.e.eMsgPollError ) { |
| 1758 | const oldErrMsg = Chat.e.eMsgPollError; |
| 1759 | Chat.e.eMsgPollError = undefined; |
| 1760 | if( showMsg ){ |
| 1761 | if(Chat.beVerbose){ |
| 1762 | console.log("Poller Connection restored."); |
| 1763 | } |
| 1764 | const m = Chat.reportReconnection("Poller connection restored."); |
| 1765 | if( oldErrMsg ){ |
| 1766 | D.remove(oldErrMsg.e?.body.querySelector('button.retry-now')); |
| 1767 | } |
| 1768 | m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid; |
| 1769 | D.addClass(m.e.body,'poller-connection'); |
| 1770 | } |
| 1771 | } |
| 1772 | setTimeout( Chat.poll, Chat.timer.resetDelay() ); |
| 1773 | }; |
| 1774 | |
| 1775 | /* To be called from F.fetch('chat-poll') beforesend() handler. If |
| 1776 | we're currently in delayed-retry mode and a connection is |
| 1777 | started, try to reset the delay after N time waiting on that |
| 1778 | connection. The fact that the connection is waiting to respond, |
| 1779 | rather than outright failing, is a good hint that the outage is |
| 1780 | over and we can reset the back-off timer. */ |
| 1781 | const clearPollErrOnWait = function(){ |
| 1782 | //console.warn('clearPollErrOnWait outer', Chat.timer.tidReconnect, Chat.timer.currentDelay); |
| 1783 | if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){ |
| 1784 | Chat.timer.tidReconnect = setTimeout(()=>{ |
| 1785 | //console.warn('clearPollErrOnWait inner'); |
| 1786 | Chat.timer.tidReconnect = 0; |
| 1787 | if( poll.running ){ |
| 1788 | /* This chat-poll F.fetch() is still underway, so let's |
| 1789 | assume the connection is back up until/unless it times |
| 1790 | out or breaks again. */ |
| @@ -2275,11 +2294,11 @@ | |
| 2294 | Chat.e.inputFields.$currentIndex = a[2]; |
| 2295 | Chat.inputValue(v); |
| 2296 | D.removeClass(a[0], 'hidden'); |
| 2297 | D.addClass(a[1], 'hidden'); |
| 2298 | } |
| 2299 | Chat.e.inputLineWrapper.classList[ |
| 2300 | s.value ? 'add' : 'remove' |
| 2301 | ]('compact'); |
| 2302 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 2303 | }); |
| 2304 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| @@ -2630,40 +2649,47 @@ | |
| 2649 | }else{ |
| 2650 | /* Delay a while before trying again, noting that other Chat |
| 2651 | APIs may try and succeed at connections before this timer |
| 2652 | resolves, in which case they'll clear this timeout and the |
| 2653 | UI message about the outage. */ |
| 2654 | let delay; |
| 2655 | D.addClass(Chat.e.pollErrorMarker, 'connection-error'); |
| 2656 | if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){ |
| 2657 | if(Chat.beVerbose){ |
| 2658 | console.warn("Ignoring polling error #", Chat.timer.errCount); |
| 2659 | } |
| 2660 | delay = Chat.timer.resetDelay(Chat.timer.skipErrDelay); |
| 2661 | } else { |
| 2662 | delay = Chat.timer.incrDelay(); |
| 2663 | //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError); |
| 2664 | const msg = "Poller connection error. Retrying in "+delay+ " ms."; |
| 2665 | /* Replace the current/newest connection error widget. We could also |
| 2666 | just update its body with the new message, but then its timestamp |
| 2667 | never updates. OTOH, if we replace the message, we lose the |
| 2668 | start time of the outage in the log. It seems more useful to |
| 2669 | update the timestamp so that it doesn't look like it's hung. */ |
| 2670 | if( Chat.e.eMsgPollError ){ |
| 2671 | Chat.deleteMessageElem(Chat.e.eMsgPollError, false); |
| 2672 | } |
| 2673 | const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); |
| 2674 | D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); |
| 2675 | /* Add a "retry now" button */ |
| 2676 | const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); |
| 2677 | D.append(Chat.e.eMsgPollError.e.content, " ", btnDel); |
| 2678 | btnDel.addEventListener('click', function(){ |
| 2679 | D.remove(btnDel); |
| 2680 | Chat.timer.currentDelay = |
| 2681 | Chat.timer.resetDelay() + 1 /*workaround for showing the "connection restored" message*/; |
| 2682 | if( Chat.timer.tidPoller ){ |
| 2683 | clearTimeout(Chat.timer.tidPoller); |
| 2684 | Chat.timer.tidPoller = 0; |
| 2685 | } |
| 2686 | poll(); |
| 2687 | }); |
| 2688 | //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction |
| 2689 | } |
| 2690 | Chat.timer.tidPoller = setTimeout(Chat.poll, delay); |
| 2691 | } |
| 2692 | } |
| 2693 | }; |
| 2694 | afterPollFetch.isFirstCall = true; |
| 2695 | |
| @@ -2752,11 +2778,11 @@ | |
| 2778 | Chat.updateActiveUserList(); |
| 2779 | } |
| 2780 | afterPollFetch(); |
| 2781 | } |
| 2782 | }); |
| 2783 | }/*poll()*/; |
| 2784 | poll.isFirstCall = true; |
| 2785 | Chat._gotServerError = poll.running = false; |
| 2786 | if( window.fossil.config.chat.fromcli ){ |
| 2787 | Chat.chatOnlyMode(true); |
| 2788 | } |
| 2789 |
+12
-7
| --- src/style.chat.css | ||
| +++ src/style.chat.css | ||
| @@ -213,10 +213,14 @@ | ||
| 213 | 213 | } |
| 214 | 214 | body.chat #chat-messages-wrapper.loading > * { |
| 215 | 215 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 216 | 216 | visibility: hidden; |
| 217 | 217 | } |
| 218 | +body.chat #chat-messages-wrapper.connection-error { | |
| 219 | + border-bottom: thin dotted red; | |
| 220 | +} | |
| 221 | + | |
| 218 | 222 | body.chat div.content { |
| 219 | 223 | margin: 0; |
| 220 | 224 | padding: 0; |
| 221 | 225 | display: flex; |
| 222 | 226 | flex-direction: column-reverse; |
| @@ -241,20 +245,21 @@ | ||
| 241 | 245 | /* Safari user reports that 2em is necessary to keep the file selection |
| 242 | 246 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 243 | 247 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 244 | 248 | margin-bottom: 0; |
| 245 | 249 | } |
| 246 | -.chat-input-field { | |
| 250 | + | |
| 251 | +body.chat .chat-input-field { | |
| 247 | 252 | flex: 10 1 auto; |
| 248 | 253 | margin: 0; |
| 249 | 254 | } |
| 250 | -#chat-input-field-x, | |
| 251 | -#chat-input-field-multi { | |
| 255 | +body.chat #chat-input-field-x, | |
| 256 | +body.chat #chat-input-field-multi { | |
| 252 | 257 | overflow: auto; |
| 253 | 258 | resize: vertical; |
| 254 | 259 | } |
| 255 | -#chat-input-field-x { | |
| 260 | +body.chat #chat-input-field-x { | |
| 256 | 261 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 257 | 262 | padding: 0.2em; |
| 258 | 263 | background-color: rgba(156,156,156,0.3); |
| 259 | 264 | white-space: pre-wrap; |
| 260 | 265 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| @@ -261,20 +266,20 @@ | ||
| 261 | 266 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 262 | 267 | cursor: text; |
| 263 | 268 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| 264 | 269 | element until it has focus, causing potential confusion. */ |
| 265 | 270 | } |
| 266 | -#chat-input-field-x:empty::before { | |
| 271 | +body.chat #chat-input-field-x:empty::before { | |
| 267 | 272 | content: attr(data-placeholder); |
| 268 | 273 | opacity: 0.6; |
| 269 | 274 | } |
| 270 | -.chat-input-field:not(:focus){ | |
| 275 | +body.chat .chat-input-field:not(:focus){ | |
| 271 | 276 | border-width: 1px; |
| 272 | 277 | border-style: solid; |
| 273 | 278 | border-radius: 0.25em; |
| 274 | 279 | } |
| 275 | -.chat-input-field:focus{ | |
| 280 | +body.chat .chat-input-field:focus{ | |
| 276 | 281 | /* This transparent border helps avoid the text shifting around |
| 277 | 282 | when the contenteditable attribute causes a border (which we |
| 278 | 283 | apparently cannot style) to be added. */ |
| 279 | 284 | border-width: 1px; |
| 280 | 285 | border-style: solid; |
| 281 | 286 |
| --- src/style.chat.css | |
| +++ src/style.chat.css | |
| @@ -213,10 +213,14 @@ | |
| 213 | } |
| 214 | body.chat #chat-messages-wrapper.loading > * { |
| 215 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 216 | visibility: hidden; |
| 217 | } |
| 218 | body.chat div.content { |
| 219 | margin: 0; |
| 220 | padding: 0; |
| 221 | display: flex; |
| 222 | flex-direction: column-reverse; |
| @@ -241,20 +245,21 @@ | |
| 241 | /* Safari user reports that 2em is necessary to keep the file selection |
| 242 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 243 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 244 | margin-bottom: 0; |
| 245 | } |
| 246 | .chat-input-field { |
| 247 | flex: 10 1 auto; |
| 248 | margin: 0; |
| 249 | } |
| 250 | #chat-input-field-x, |
| 251 | #chat-input-field-multi { |
| 252 | overflow: auto; |
| 253 | resize: vertical; |
| 254 | } |
| 255 | #chat-input-field-x { |
| 256 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 257 | padding: 0.2em; |
| 258 | background-color: rgba(156,156,156,0.3); |
| 259 | white-space: pre-wrap; |
| 260 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| @@ -261,20 +266,20 @@ | |
| 261 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 262 | cursor: text; |
| 263 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| 264 | element until it has focus, causing potential confusion. */ |
| 265 | } |
| 266 | #chat-input-field-x:empty::before { |
| 267 | content: attr(data-placeholder); |
| 268 | opacity: 0.6; |
| 269 | } |
| 270 | .chat-input-field:not(:focus){ |
| 271 | border-width: 1px; |
| 272 | border-style: solid; |
| 273 | border-radius: 0.25em; |
| 274 | } |
| 275 | .chat-input-field:focus{ |
| 276 | /* This transparent border helps avoid the text shifting around |
| 277 | when the contenteditable attribute causes a border (which we |
| 278 | apparently cannot style) to be added. */ |
| 279 | border-width: 1px; |
| 280 | border-style: solid; |
| 281 |
| --- src/style.chat.css | |
| +++ src/style.chat.css | |
| @@ -213,10 +213,14 @@ | |
| 213 | } |
| 214 | body.chat #chat-messages-wrapper.loading > * { |
| 215 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 216 | visibility: hidden; |
| 217 | } |
| 218 | body.chat #chat-messages-wrapper.connection-error { |
| 219 | border-bottom: thin dotted red; |
| 220 | } |
| 221 | |
| 222 | body.chat div.content { |
| 223 | margin: 0; |
| 224 | padding: 0; |
| 225 | display: flex; |
| 226 | flex-direction: column-reverse; |
| @@ -241,20 +245,21 @@ | |
| 245 | /* Safari user reports that 2em is necessary to keep the file selection |
| 246 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 247 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 248 | margin-bottom: 0; |
| 249 | } |
| 250 | |
| 251 | body.chat .chat-input-field { |
| 252 | flex: 10 1 auto; |
| 253 | margin: 0; |
| 254 | } |
| 255 | body.chat #chat-input-field-x, |
| 256 | body.chat #chat-input-field-multi { |
| 257 | overflow: auto; |
| 258 | resize: vertical; |
| 259 | } |
| 260 | body.chat #chat-input-field-x { |
| 261 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 262 | padding: 0.2em; |
| 263 | background-color: rgba(156,156,156,0.3); |
| 264 | white-space: pre-wrap; |
| 265 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| @@ -261,20 +266,20 @@ | |
| 266 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 267 | cursor: text; |
| 268 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| 269 | element until it has focus, causing potential confusion. */ |
| 270 | } |
| 271 | body.chat #chat-input-field-x:empty::before { |
| 272 | content: attr(data-placeholder); |
| 273 | opacity: 0.6; |
| 274 | } |
| 275 | body.chat .chat-input-field:not(:focus){ |
| 276 | border-width: 1px; |
| 277 | border-style: solid; |
| 278 | border-radius: 0.25em; |
| 279 | } |
| 280 | body.chat .chat-input-field:focus{ |
| 281 | /* This transparent border helps avoid the text shifting around |
| 282 | when the contenteditable attribute causes a border (which we |
| 283 | apparently cannot style) to be added. */ |
| 284 | border-width: 1px; |
| 285 | border-style: solid; |
| 286 |