Fossil SCM

Found what seems to be a more or less viable solution for the chat layout in which the input area is effectively sticky while not actually being so. New messages do not scroll to the start of the list except for when a user locally posts a message, but instead, if a new message arrives and is scrolled out of view, a toast is shown to gently alert the user that a new message has arrived.

stephan 2020-12-26 23:57 trunk
Commit 0a00a103125e1cd9e9cb7a4219601c76ad0dc3fbe1b31fb9e8090fa68621f821
2 files changed +21 -47 +13 -10
+21 -47
--- src/chat.js
+++ src/chat.js
@@ -6,10 +6,19 @@
66
const F = window.fossil, D = F.dom;
77
const E1 = function(selector){
88
const e = document.querySelector(selector);
99
if(!e) throw new Error("missing required DOM element: "+selector);
1010
return e;
11
+ };
12
+ const isInViewport = function(e) {
13
+ const rect = e.getBoundingClientRect();
14
+ return (
15
+ rect.top >= 0 &&
16
+ rect.left >= 0 &&
17
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
18
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
19
+ );
1120
};
1221
//document.body.classList.add('chat-only-mode');
1322
const Chat = (function(){
1423
const cs = {
1524
e:{/*map of certain DOM elements.*/
@@ -117,40 +126,19 @@
117126
const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
118127
if(atEnd){
119128
mip.parentNode.insertBefore(e, mip);
120129
}else{
121130
const self = this;
122
- if(false && this.isUiFlipped()){
123
- /* When UI is flipped, new messages start out under the
124
- text input area because of its position:sticky
125
- style. We have to scroll them up. When the page footer
126
- is not hidden but is not on-screen, this causes a
127
- slight amount of UI jarring as the footer is *also*
128
- scrolled into view (for whatever reason).
129
-
130
- The remaining problem here is messages with IMG tags.
131
- At this point in the process their IMG.src has not yet
132
- been loaded - that's async. We scroll the message into
133
- view, but then the downstream loading of IMG.src pushes
134
- the message content back down, sliding the message
135
- behind the input field. This can be verified by delaying the
136
- message scroll by a second or so to give the image time
137
- to load (from a local server instance).
138
- */
139
- D.addClass(self.e.inputWrapper,'unsticky');
140
- }
141131
if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
142132
else mip.parentNode.appendChild(e);
143
- if(false && this.isUiFlipped()){
144
- //e.scrollIntoView();
145
- setTimeout(function(){
146
- //self.e.inputWrapper.scrollIntoView();
147
- //self.e.fileSelectWrapper.scrollIntoView();
148
- //e.scrollIntoView();
149
- //D.removeClass(self.e.inputWrapper,'unsticky');
150
- self.e.inputWrapper.scrollIntoView();
151
- },0);
133
+ if(!atEnd && !this.isMassLoading
134
+ && e.dataset.xfrom!==Chat.me && !isInViewport(e)){
135
+ /* If a new non-history message arrives while the user is
136
+ scrolled elsewhere, do not scroll to the latest
137
+ message, but gently alert the user that a new message
138
+ has arrived. */
139
+ F.toast.message("New message has arrived.");
152140
}
153141
}
154142
},
155143
/** Returns true if chat-only mode is enabled. */
156144
isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
@@ -167,11 +155,11 @@
167155
*/
168156
chatOnlyMode: function f(yes){
169157
if(undefined === f.elemsToToggle){
170158
f.elemsToToggle = [];
171159
document.querySelectorAll(
172
- "body > div.header, body > div.footer"
160
+ "body > div.header, body > div.mainmenu, body > div.footer"
173161
).forEach((e)=>f.elemsToToggle.push(e));
174162
}
175163
if(!arguments.length) yes = true;
176164
if(yes === this.isChatOnlyMode()) return this;
177165
if(yes){
@@ -179,17 +167,10 @@
179167
D.addClass(document.body, 'chat-only-mode');
180168
document.body.scroll(0,document.body.height);
181169
}else{
182170
D.removeClass(f.elemsToToggle, 'hidden');
183171
D.removeClass(document.body, 'chat-only-mode');
184
- if(false){
185
- setTimeout(()=>document.body.scrollIntoView(
186
- /*moves to (0,0), whereas scrollTo(0,0) does not!
187
- setTimeout() is unfortunately necessary to get the scroll
188
- placement correct.*/
189
- ), 0);
190
- }
191172
}
192173
const msg = document.querySelector('.message-widget');
193174
if(msg) setTimeout(()=>msg.scrollIntoView(),0);
194175
return this;
195176
},
@@ -525,10 +506,12 @@
525506
body: fd
526507
});
527508
}
528509
BlobXferState.clear();
529510
Chat.inputValue("").inputFocus();
511
+ Chat.e.messagesWrapper.scrollTo(
512
+ 0,0/*scrolls to top or bottom, depending on flex direction!*/);
530513
};
531514
532515
Chat.e.inputSingle.addEventListener('keydown',function(ev){
533516
if(13===ev.keyCode/*ENTER*/){
534517
ev.preventDefault();
@@ -678,21 +661,10 @@
678661
boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
679662
callback: function(){
680663
document.body.classList.toggle('chat-bottom-up');
681664
Chat.settings.set('bottom-up',
682665
document.body.classList.contains('chat-bottom-up'));
683
- if(false){
684
- /* Reminder: in order to get a good scrolling effect when
685
- sticky mode is enabled for Chat.e.inputWrapper, BOTH of
686
- these scrollIntoView() calls are needed. */
687
- const e = document.querySelector(
688
- '.message-widget'/*this is always the most recent message,
689
- even if flexbox placed it at the end of
690
- the page!*/
691
- );
692
- if(e) e.scrollIntoView();
693
- }
694666
setTimeout(()=>Chat.e.inputWrapper.scrollIntoView(), 0);
695667
}
696668
},{
697669
label: "Images inline",
698670
boolValue: ()=>Chat.settings.getBool('images-inline'),
@@ -862,10 +834,11 @@
862834
})()/*end history loading widget setup*/;
863835
864836
async function poll(isFirstCall){
865837
if(poll.running) return;
866838
poll.running = true;
839
+ Chat.isMassLoading = isFirstCall;
867840
if(isFirstCall) Chat.ajaxStart();
868841
var p = fetch("chat-poll?name=" + Chat.mxMsg);
869842
p.then(x=>x.json())
870843
.then(y=>newcontent(y))
871844
.catch(e=>console.error(e))
@@ -872,10 +845,11 @@
872845
/* ^^^ we don't use Chat.reportError(e) here b/c the polling
873846
fails exepectedly when it times out, but is then immediately
874847
resumed, and reportError() produces a loud error message. */
875848
.finally(function(x){
876849
if(isFirstCall){
850
+ Chat.isMassLoading = false;
877851
Chat.ajaxEnd();
878852
Chat.e.inputWrapper.scrollIntoView();
879853
}
880854
poll.running=false;
881855
});
882856
--- src/chat.js
+++ src/chat.js
@@ -6,10 +6,19 @@
6 const F = window.fossil, D = F.dom;
7 const E1 = function(selector){
8 const e = document.querySelector(selector);
9 if(!e) throw new Error("missing required DOM element: "+selector);
10 return e;
 
 
 
 
 
 
 
 
 
11 };
12 //document.body.classList.add('chat-only-mode');
13 const Chat = (function(){
14 const cs = {
15 e:{/*map of certain DOM elements.*/
@@ -117,40 +126,19 @@
117 const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
118 if(atEnd){
119 mip.parentNode.insertBefore(e, mip);
120 }else{
121 const self = this;
122 if(false && this.isUiFlipped()){
123 /* When UI is flipped, new messages start out under the
124 text input area because of its position:sticky
125 style. We have to scroll them up. When the page footer
126 is not hidden but is not on-screen, this causes a
127 slight amount of UI jarring as the footer is *also*
128 scrolled into view (for whatever reason).
129
130 The remaining problem here is messages with IMG tags.
131 At this point in the process their IMG.src has not yet
132 been loaded - that's async. We scroll the message into
133 view, but then the downstream loading of IMG.src pushes
134 the message content back down, sliding the message
135 behind the input field. This can be verified by delaying the
136 message scroll by a second or so to give the image time
137 to load (from a local server instance).
138 */
139 D.addClass(self.e.inputWrapper,'unsticky');
140 }
141 if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
142 else mip.parentNode.appendChild(e);
143 if(false && this.isUiFlipped()){
144 //e.scrollIntoView();
145 setTimeout(function(){
146 //self.e.inputWrapper.scrollIntoView();
147 //self.e.fileSelectWrapper.scrollIntoView();
148 //e.scrollIntoView();
149 //D.removeClass(self.e.inputWrapper,'unsticky');
150 self.e.inputWrapper.scrollIntoView();
151 },0);
152 }
153 }
154 },
155 /** Returns true if chat-only mode is enabled. */
156 isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
@@ -167,11 +155,11 @@
167 */
168 chatOnlyMode: function f(yes){
169 if(undefined === f.elemsToToggle){
170 f.elemsToToggle = [];
171 document.querySelectorAll(
172 "body > div.header, body > div.footer"
173 ).forEach((e)=>f.elemsToToggle.push(e));
174 }
175 if(!arguments.length) yes = true;
176 if(yes === this.isChatOnlyMode()) return this;
177 if(yes){
@@ -179,17 +167,10 @@
179 D.addClass(document.body, 'chat-only-mode');
180 document.body.scroll(0,document.body.height);
181 }else{
182 D.removeClass(f.elemsToToggle, 'hidden');
183 D.removeClass(document.body, 'chat-only-mode');
184 if(false){
185 setTimeout(()=>document.body.scrollIntoView(
186 /*moves to (0,0), whereas scrollTo(0,0) does not!
187 setTimeout() is unfortunately necessary to get the scroll
188 placement correct.*/
189 ), 0);
190 }
191 }
192 const msg = document.querySelector('.message-widget');
193 if(msg) setTimeout(()=>msg.scrollIntoView(),0);
194 return this;
195 },
@@ -525,10 +506,12 @@
525 body: fd
526 });
527 }
528 BlobXferState.clear();
529 Chat.inputValue("").inputFocus();
 
 
530 };
531
532 Chat.e.inputSingle.addEventListener('keydown',function(ev){
533 if(13===ev.keyCode/*ENTER*/){
534 ev.preventDefault();
@@ -678,21 +661,10 @@
678 boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
679 callback: function(){
680 document.body.classList.toggle('chat-bottom-up');
681 Chat.settings.set('bottom-up',
682 document.body.classList.contains('chat-bottom-up'));
683 if(false){
684 /* Reminder: in order to get a good scrolling effect when
685 sticky mode is enabled for Chat.e.inputWrapper, BOTH of
686 these scrollIntoView() calls are needed. */
687 const e = document.querySelector(
688 '.message-widget'/*this is always the most recent message,
689 even if flexbox placed it at the end of
690 the page!*/
691 );
692 if(e) e.scrollIntoView();
693 }
694 setTimeout(()=>Chat.e.inputWrapper.scrollIntoView(), 0);
695 }
696 },{
697 label: "Images inline",
698 boolValue: ()=>Chat.settings.getBool('images-inline'),
@@ -862,10 +834,11 @@
862 })()/*end history loading widget setup*/;
863
864 async function poll(isFirstCall){
865 if(poll.running) return;
866 poll.running = true;
 
867 if(isFirstCall) Chat.ajaxStart();
868 var p = fetch("chat-poll?name=" + Chat.mxMsg);
869 p.then(x=>x.json())
870 .then(y=>newcontent(y))
871 .catch(e=>console.error(e))
@@ -872,10 +845,11 @@
872 /* ^^^ we don't use Chat.reportError(e) here b/c the polling
873 fails exepectedly when it times out, but is then immediately
874 resumed, and reportError() produces a loud error message. */
875 .finally(function(x){
876 if(isFirstCall){
 
877 Chat.ajaxEnd();
878 Chat.e.inputWrapper.scrollIntoView();
879 }
880 poll.running=false;
881 });
882
--- src/chat.js
+++ src/chat.js
@@ -6,10 +6,19 @@
6 const F = window.fossil, D = F.dom;
7 const E1 = function(selector){
8 const e = document.querySelector(selector);
9 if(!e) throw new Error("missing required DOM element: "+selector);
10 return e;
11 };
12 const isInViewport = function(e) {
13 const rect = e.getBoundingClientRect();
14 return (
15 rect.top >= 0 &&
16 rect.left >= 0 &&
17 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
18 rect.right <= (window.innerWidth || document.documentElement.clientWidth)
19 );
20 };
21 //document.body.classList.add('chat-only-mode');
22 const Chat = (function(){
23 const cs = {
24 e:{/*map of certain DOM elements.*/
@@ -117,40 +126,19 @@
126 const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
127 if(atEnd){
128 mip.parentNode.insertBefore(e, mip);
129 }else{
130 const self = this;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131 if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
132 else mip.parentNode.appendChild(e);
133 if(!atEnd && !this.isMassLoading
134 && e.dataset.xfrom!==Chat.me && !isInViewport(e)){
135 /* If a new non-history message arrives while the user is
136 scrolled elsewhere, do not scroll to the latest
137 message, but gently alert the user that a new message
138 has arrived. */
139 F.toast.message("New message has arrived.");
 
 
140 }
141 }
142 },
143 /** Returns true if chat-only mode is enabled. */
144 isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
@@ -167,11 +155,11 @@
155 */
156 chatOnlyMode: function f(yes){
157 if(undefined === f.elemsToToggle){
158 f.elemsToToggle = [];
159 document.querySelectorAll(
160 "body > div.header, body > div.mainmenu, body > div.footer"
161 ).forEach((e)=>f.elemsToToggle.push(e));
162 }
163 if(!arguments.length) yes = true;
164 if(yes === this.isChatOnlyMode()) return this;
165 if(yes){
@@ -179,17 +167,10 @@
167 D.addClass(document.body, 'chat-only-mode');
168 document.body.scroll(0,document.body.height);
169 }else{
170 D.removeClass(f.elemsToToggle, 'hidden');
171 D.removeClass(document.body, 'chat-only-mode');
 
 
 
 
 
 
 
172 }
173 const msg = document.querySelector('.message-widget');
174 if(msg) setTimeout(()=>msg.scrollIntoView(),0);
175 return this;
176 },
@@ -525,10 +506,12 @@
506 body: fd
507 });
508 }
509 BlobXferState.clear();
510 Chat.inputValue("").inputFocus();
511 Chat.e.messagesWrapper.scrollTo(
512 0,0/*scrolls to top or bottom, depending on flex direction!*/);
513 };
514
515 Chat.e.inputSingle.addEventListener('keydown',function(ev){
516 if(13===ev.keyCode/*ENTER*/){
517 ev.preventDefault();
@@ -678,21 +661,10 @@
661 boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
662 callback: function(){
663 document.body.classList.toggle('chat-bottom-up');
664 Chat.settings.set('bottom-up',
665 document.body.classList.contains('chat-bottom-up'));
 
 
 
 
 
 
 
 
 
 
 
666 setTimeout(()=>Chat.e.inputWrapper.scrollIntoView(), 0);
667 }
668 },{
669 label: "Images inline",
670 boolValue: ()=>Chat.settings.getBool('images-inline'),
@@ -862,10 +834,11 @@
834 })()/*end history loading widget setup*/;
835
836 async function poll(isFirstCall){
837 if(poll.running) return;
838 poll.running = true;
839 Chat.isMassLoading = isFirstCall;
840 if(isFirstCall) Chat.ajaxStart();
841 var p = fetch("chat-poll?name=" + Chat.mxMsg);
842 p.then(x=>x.json())
843 .then(y=>newcontent(y))
844 .catch(e=>console.error(e))
@@ -872,10 +845,11 @@
845 /* ^^^ we don't use Chat.reportError(e) here b/c the polling
846 fails exepectedly when it times out, but is then immediately
847 resumed, and reportError() produces a loud error message. */
848 .finally(function(x){
849 if(isFirstCall){
850 Chat.isMassLoading = false;
851 Chat.ajaxEnd();
852 Chat.e.inputWrapper.scrollIntoView();
853 }
854 poll.running=false;
855 });
856
+13 -10
--- src/default.css
+++ src/default.css
@@ -1638,52 +1638,55 @@
16381638
}
16391639
/** Container for the list of /chat messages. */
16401640
body.chat #chat-messages-wrapper {
16411641
display: flex;
16421642
flex-direction: column;
1643
+ overflow: auto;
1644
+ width: 100%;
1645
+ flex: 1 1 auto;
16431646
}
16441647
body.chat.chat-bottom-up #chat-messages-wrapper {
16451648
flex-direction: column-reverse;
16461649
z-index: 99 /* so that it scrolls under input area. If it's
16471650
lower than div.content then mouse events to it
16481651
are blocked!*/;
16491652
}
1653
+body.chat.chat-only-mode #chat-message-wrapper {
1654
+}
1655
+
16501656
body.chat div.content {
16511657
margin: 0;
16521658
padding: 0;
1659
+ position: relative;
16531660
display: flex;
16541661
flex-direction: column;
16551662
align-items: stretch;
1663
+ max-height: 85vh /* rough approximate */;
16561664
}
16571665
body.chat.chat-bottom-up div.content {
16581666
flex-direction: column-reverse;
16591667
}
1668
+body.chat.chat-only-mode div.content {
1669
+ max-height: 95vh/*larger than approx. this is too big for Firefox on Android*/;
1670
+}
16601671
/* Wrapper for /chat user input controls */
16611672
body.chat #chat-input-area {
16621673
display: flex;
16631674
flex-direction: column;
16641675
border-bottom: 1px solid black;
1665
- padding: 0.5em 1em;
1676
+ padding: 0.5em 1em 0 0.5em;
16661677
margin-bottom: 0.5em;
1667
- /*position: sticky; top: 0; disabled for the time being because of
1668
- scroll-related quirks which are still unresolved. */
16691678
z-index: 100
16701679
/* see notes in #chat-messages-wrapper. The various popups require a
16711680
z-index higher than this one. */;
1681
+ flex: 0 0 auto;
16721682
}
16731683
body.chat.chat-bottom-up #chat-input-area {
16741684
border-bottom: none;
16751685
border-top: 1px solid black;
16761686
margin-bottom: 0;
16771687
margin-top: 0.5em;
1678
- position: initial /*sticky currently disabled due to scrolling-related issues*/;
1679
- bottom: 0;
1680
-}
1681
-/* An internal hack to try to help resolve a message-scrolling quirk
1682
- when #chat-input-area is sticky on the bottom of the screen. */
1683
-body.chat.chat-bottom-up #chat-input-area.unsticky {
1684
- position: initial;
16851688
}
16861689
/* Widget holding the chat message input field, send button, and
16871690
settings button. */
16881691
body.chat #chat-input-line {
16891692
display: flex;
16901693
--- src/default.css
+++ src/default.css
@@ -1638,52 +1638,55 @@
1638 }
1639 /** Container for the list of /chat messages. */
1640 body.chat #chat-messages-wrapper {
1641 display: flex;
1642 flex-direction: column;
 
 
 
1643 }
1644 body.chat.chat-bottom-up #chat-messages-wrapper {
1645 flex-direction: column-reverse;
1646 z-index: 99 /* so that it scrolls under input area. If it's
1647 lower than div.content then mouse events to it
1648 are blocked!*/;
1649 }
 
 
 
1650 body.chat div.content {
1651 margin: 0;
1652 padding: 0;
 
1653 display: flex;
1654 flex-direction: column;
1655 align-items: stretch;
 
1656 }
1657 body.chat.chat-bottom-up div.content {
1658 flex-direction: column-reverse;
1659 }
 
 
 
1660 /* Wrapper for /chat user input controls */
1661 body.chat #chat-input-area {
1662 display: flex;
1663 flex-direction: column;
1664 border-bottom: 1px solid black;
1665 padding: 0.5em 1em;
1666 margin-bottom: 0.5em;
1667 /*position: sticky; top: 0; disabled for the time being because of
1668 scroll-related quirks which are still unresolved. */
1669 z-index: 100
1670 /* see notes in #chat-messages-wrapper. The various popups require a
1671 z-index higher than this one. */;
 
1672 }
1673 body.chat.chat-bottom-up #chat-input-area {
1674 border-bottom: none;
1675 border-top: 1px solid black;
1676 margin-bottom: 0;
1677 margin-top: 0.5em;
1678 position: initial /*sticky currently disabled due to scrolling-related issues*/;
1679 bottom: 0;
1680 }
1681 /* An internal hack to try to help resolve a message-scrolling quirk
1682 when #chat-input-area is sticky on the bottom of the screen. */
1683 body.chat.chat-bottom-up #chat-input-area.unsticky {
1684 position: initial;
1685 }
1686 /* Widget holding the chat message input field, send button, and
1687 settings button. */
1688 body.chat #chat-input-line {
1689 display: flex;
1690
--- src/default.css
+++ src/default.css
@@ -1638,52 +1638,55 @@
1638 }
1639 /** Container for the list of /chat messages. */
1640 body.chat #chat-messages-wrapper {
1641 display: flex;
1642 flex-direction: column;
1643 overflow: auto;
1644 width: 100%;
1645 flex: 1 1 auto;
1646 }
1647 body.chat.chat-bottom-up #chat-messages-wrapper {
1648 flex-direction: column-reverse;
1649 z-index: 99 /* so that it scrolls under input area. If it's
1650 lower than div.content then mouse events to it
1651 are blocked!*/;
1652 }
1653 body.chat.chat-only-mode #chat-message-wrapper {
1654 }
1655
1656 body.chat div.content {
1657 margin: 0;
1658 padding: 0;
1659 position: relative;
1660 display: flex;
1661 flex-direction: column;
1662 align-items: stretch;
1663 max-height: 85vh /* rough approximate */;
1664 }
1665 body.chat.chat-bottom-up div.content {
1666 flex-direction: column-reverse;
1667 }
1668 body.chat.chat-only-mode div.content {
1669 max-height: 95vh/*larger than approx. this is too big for Firefox on Android*/;
1670 }
1671 /* Wrapper for /chat user input controls */
1672 body.chat #chat-input-area {
1673 display: flex;
1674 flex-direction: column;
1675 border-bottom: 1px solid black;
1676 padding: 0.5em 1em 0 0.5em;
1677 margin-bottom: 0.5em;
 
 
1678 z-index: 100
1679 /* see notes in #chat-messages-wrapper. The various popups require a
1680 z-index higher than this one. */;
1681 flex: 0 0 auto;
1682 }
1683 body.chat.chat-bottom-up #chat-input-area {
1684 border-bottom: none;
1685 border-top: 1px solid black;
1686 margin-bottom: 0;
1687 margin-top: 0.5em;
 
 
 
 
 
 
 
1688 }
1689 /* Widget holding the chat message input field, send button, and
1690 settings button. */
1691 body.chat #chat-input-line {
1692 display: flex;
1693

Keyboard Shortcuts

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