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.

stephan 2025-04-11 15:30 trunk
Commit e3eb83997b9dfbabac425310184596efe11b78fc01d949e7840ff52d2bf190ba
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -1,6 +1,6 @@
1
-/**
1
+-/**
22
This file contains the client-side implementation of fossil's /chat
33
application.
44
*/
55
window.fossil.onPageLoad(function(){
66
const F = window.fossil, D = F.dom;
@@ -129,20 +129,21 @@
129129
return resized;
130130
})();
131131
fossil.FRK = ForceResizeKludge/*for debugging*/;
132132
const Chat = ForceResizeKludge.chat = (function(){
133133
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. */,
137138
playedBeep: false /* used for the beep-once setting */,
138139
e:{/*map of certain DOM elements.*/
139140
messageInjectPoint: E1('#message-inject-point'),
140141
pageTitle: E1('head title'),
141142
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'),
144145
fileSelectWrapper: E1('#chat-input-file-area'),
145146
viewMessages: E1('#chat-messages-wrapper'),
146147
btnSubmit: E1('#chat-button-submit'),
147148
btnAttach: E1('#chat-button-attach'),
148149
inputX: E1('#chat-input-field-x'),
@@ -157,11 +158,12 @@
157158
searchContent: E1('#chat-search-content'),
158159
btnPreview: E1('#chat-button-preview'),
159160
views: document.querySelectorAll('.chat-view'),
160161
activeUserListWrapper: E1('#chat-user-list-wrapper'),
161162
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 */
163165
},
164166
me: F.user.name,
165167
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
166168
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
167169
pageIsActive: 'visible'===document.visibilityState,
@@ -201,10 +203,15 @@
201203
currentDelay: 1000 /* current polling interval */,
202204
maxDelay: 60000 * 5 /* max interval when backing off for
203205
connection errors */,
204206
minDelay: 5000 /* minimum delay time */,
205207
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. */,
206213
randomInterval: function(factor){
207214
return Math.floor(Math.random() * factor);
208215
},
209216
incrDelay: function(){
210217
if( this.maxDelay > this.currentDelay ){
@@ -214,12 +221,12 @@
214221
this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
215222
}
216223
}
217224
return this.currentDelay;
218225
},
219
- resetDelay: function(){
220
- return this.currentDelay = this.$initialDelay;
226
+ resetDelay: function(ms){
227
+ return this.currentDelay = ms || this.$initialDelay;
221228
},
222229
isDelayed: function(){
223230
return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
224231
}
225232
},
@@ -655,11 +662,11 @@
655662
if(!f.$disabled){
656663
D.addClassBriefly(e, a, 0, cb);
657664
}
658665
return this;
659666
}
660
- };
667
+ }/*Chat object*/;
661668
cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
662669
cs.e.inputFields.$currentIndex = 0;
663670
cs.e.inputFields.forEach(function(e,ndx){
664671
if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
665672
else D.addClass(e,'hidden');
@@ -669,10 +676,11 @@
669676
}else{
670677
/* Only the Chrome family supports contenteditable=plaintext-only */
671678
cs.$browserHasPlaintextOnly = false;
672679
D.attr(cs.e.inputX,'contenteditable','true');
673680
}
681
+ cs.e.pollErrorMarker = cs.e.viewMessages;
674682
cs.animate.$disabled = true;
675683
F.fetch.beforesend = ()=>cs.ajaxStart();
676684
F.fetch.aftersend = ()=>cs.ajaxEnd();
677685
cs.pageTitleOrig = cs.e.pageTitle.innerText;
678686
const qs = (e)=>document.querySelector(e);
@@ -1727,45 +1735,56 @@
17271735
};
17281736
17291737
/* Assume the connection has been established, reset the
17301738
Chat.timer.tidReconnect, and (if showMsg and
17311739
!!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. */
17331742
const reportConnectionReestablished = function(dbgContext, showMsg = true){
17341743
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;
17371752
}
17381753
if( Chat.timer.tidReconnect ){
17391754
clearTimeout(Chat.timer.tidReconnect);
17401755
Chat.timer.tidReconnect = 0;
17411756
}
1742
- Chat.timer.resetDelay();
17431757
if( Chat.e.eMsgPollError ) {
17441758
const oldErrMsg = Chat.e.eMsgPollError;
17451759
Chat.e.eMsgPollError = undefined;
17461760
if( showMsg ){
1761
+ if(Chat.beVerbose){
1762
+ console.log("Poller Connection restored.");
1763
+ }
17471764
const m = Chat.reportReconnection("Poller connection restored.");
17481765
if( oldErrMsg ){
17491766
D.remove(oldErrMsg.e?.body.querySelector('button.retry-now'));
17501767
}
17511768
m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
17521769
D.addClass(m.e.body,'poller-connection');
17531770
}
17541771
}
1755
- setTimeout( Chat.poll, 0 );
1772
+ setTimeout( Chat.poll, Chat.timer.resetDelay() );
17561773
};
17571774
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. */
17641781
const clearPollErrOnWait = function(){
1782
+ //console.warn('clearPollErrOnWait outer', Chat.timer.tidReconnect, Chat.timer.currentDelay);
17651783
if( !Chat.timer.tidReconnect && Chat.timer.isDelayed() ){
17661784
Chat.timer.tidReconnect = setTimeout(()=>{
1785
+ //console.warn('clearPollErrOnWait inner');
17671786
Chat.timer.tidReconnect = 0;
17681787
if( poll.running ){
17691788
/* This chat-poll F.fetch() is still underway, so let's
17701789
assume the connection is back up until/unless it times
17711790
out or breaks again. */
@@ -2275,11 +2294,11 @@
22752294
Chat.e.inputFields.$currentIndex = a[2];
22762295
Chat.inputValue(v);
22772296
D.removeClass(a[0], 'hidden');
22782297
D.addClass(a[1], 'hidden');
22792298
}
2280
- Chat.e.inputElementWrapper.classList[
2299
+ Chat.e.inputLineWrapper.classList[
22812300
s.value ? 'add' : 'remove'
22822301
]('compact');
22832302
Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
22842303
});
22852304
Chat.settings.addListener('edit-ctrl-send',function(s){
@@ -2630,40 +2649,47 @@
26302649
}else{
26312650
/* Delay a while before trying again, noting that other Chat
26322651
APIs may try and succeed at connections before this timer
26332652
resolves, in which case they'll clear this timeout and the
26342653
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);
26652691
}
26662692
}
26672693
};
26682694
afterPollFetch.isFirstCall = true;
26692695
@@ -2752,11 +2778,11 @@
27522778
Chat.updateActiveUserList();
27532779
}
27542780
afterPollFetch();
27552781
}
27562782
});
2757
- };
2783
+ }/*poll()*/;
27582784
poll.isFirstCall = true;
27592785
Chat._gotServerError = poll.running = false;
27602786
if( window.fossil.config.chat.fromcli ){
27612787
Chat.chatOnlyMode(true);
27622788
}
27632789
--- 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
--- src/style.chat.css
+++ src/style.chat.css
@@ -213,10 +213,14 @@
213213
}
214214
body.chat #chat-messages-wrapper.loading > * {
215215
/* An attempt at reducing flicker when loading lots of messages. */
216216
visibility: hidden;
217217
}
218
+body.chat #chat-messages-wrapper.connection-error {
219
+ border-bottom: thin dotted red;
220
+}
221
+
218222
body.chat div.content {
219223
margin: 0;
220224
padding: 0;
221225
display: flex;
222226
flex-direction: column-reverse;
@@ -241,20 +245,21 @@
241245
/* Safari user reports that 2em is necessary to keep the file selection
242246
widget from overlapping the page footer, whereas a margin of 0 is fine
243247
for FF/Chrome (and 2em is a *huge* waste of space for those). */
244248
margin-bottom: 0;
245249
}
246
-.chat-input-field {
250
+
251
+body.chat .chat-input-field {
247252
flex: 10 1 auto;
248253
margin: 0;
249254
}
250
-#chat-input-field-x,
251
-#chat-input-field-multi {
255
+body.chat #chat-input-field-x,
256
+body.chat #chat-input-field-multi {
252257
overflow: auto;
253258
resize: vertical;
254259
}
255
-#chat-input-field-x {
260
+body.chat #chat-input-field-x {
256261
display: inline-block/*supposed workaround for Chrome weirdness*/;
257262
padding: 0.2em;
258263
background-color: rgba(156,156,156,0.3);
259264
white-space: pre-wrap;
260265
/* ^^^ Firefox, when pasting plain text into a contenteditable field,
@@ -261,20 +266,20 @@
261266
loses all newlines unless we explicitly set this. Chrome does not. */
262267
cursor: text;
263268
/* ^^^ In some browsers the cursor may not change for a contenteditable
264269
element until it has focus, causing potential confusion. */
265270
}
266
-#chat-input-field-x:empty::before {
271
+body.chat #chat-input-field-x:empty::before {
267272
content: attr(data-placeholder);
268273
opacity: 0.6;
269274
}
270
-.chat-input-field:not(:focus){
275
+body.chat .chat-input-field:not(:focus){
271276
border-width: 1px;
272277
border-style: solid;
273278
border-radius: 0.25em;
274279
}
275
-.chat-input-field:focus{
280
+body.chat .chat-input-field:focus{
276281
/* This transparent border helps avoid the text shifting around
277282
when the contenteditable attribute causes a border (which we
278283
apparently cannot style) to be added. */
279284
border-width: 1px;
280285
border-style: solid;
281286
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button