Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Eliminated top-down chat mode altogether in an attempt to eliminate some complexity and cruft. Re-added the toast-on-new-invisible-message from [0a00a103]. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
421d6570785de527afc88ea7a164303f |
| User & Date: | stephan 2020-12-27 03:39:40.011 |
Context
|
2020-12-27
| ||
| 06:48 | chat: next round of Safari-friendly baby steps, developed in conjunction with Safari user mgagnon via chat session. ... (check-in: a1161fa9bd user: stephan tags: trunk) | |
| 04:30 | chat: re-integrated JS-based div.content resizer to do approximately what the preferred 'vh' CSS units would, but upon which Safari apparently chokes. Message area now gets a scrollbar. This works reasonably well on FF/Chrome on both Linux and Android. The jury is still out on Safari. Edit: Martin G. confirmed this one also suffers from the "collapsing messages" problem on Safari. ... (check-in: d488f5c66c user: stephan tags: no-joy) | |
| 03:39 | Eliminated top-down chat mode altogether in an attempt to eliminate some complexity and cruft. Re-added the toast-on-new-invisible-message from [0a00a103]. ... (check-in: 421d657078 user: stephan tags: trunk) | |
|
2020-12-26
| ||
| 22:09 | Disabled automatic scrolling when a new chat message arrives, as it is unnecessary when the user input fields are not sticky. To revisit later with sticky input fields. ... (check-in: b75ce86581 user: stephan tags: trunk) | |
Changes
Changes to src/chat.c.
| ︙ | ︙ | |||
133 134 135 136 137 138 139 | @ </div> @ <input type="file" name="file" id="chat-input-file"> @ </div> @ <div id="chat-drop-details"></div> @ </div> @ </form> @ </div> | | < | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
@ </div>
@ <input type="file" name="file" id="chat-input-file">
@ </div>
@ <div id="chat-drop-details"></div>
@ </div>
@ </form>
@ </div>
@ <div id='chat-messages-wrapper'>
/* New chat messages get inserted immediately after this element */
@ <span id='message-inject-point'></span>
@ </div>
builtin_fossil_js_bundle_or("popupwidget", "storage", NULL);
/* Always in-line the javascript for the chat page */
@ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
/* We need an onload handler to ensure that window.fossil is
|
| ︙ | ︙ |
Changes to src/chat.js.
1 2 3 4 5 6 7 8 9 10 11 |
/**
This file contains the client-side implementation of fossil's /chat
application.
*/
(function(){
const F = window.fossil, D = F.dom;
const E1 = function(selector){
const e = document.querySelector(selector);
if(!e) throw new Error("missing required DOM element: "+selector);
return e;
};
| > > > > > | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/**
This file contains the client-side implementation of fossil's /chat
application.
*/
(function(){
const F = window.fossil, D = F.dom;
const E1 = function(selector){
const e = document.querySelector(selector);
if(!e) throw new Error("missing required DOM element: "+selector);
return e;
};
const isInViewport = function(e) {
const rect = e.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
const Chat = (function(){
const cs = {
e:{/*map of certain DOM elements.*/
messageInjectPoint: E1('#message-inject-point'),
pageTitle: E1('head title'),
loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
inputWrapper: E1("#chat-input-area"),
fileSelectWrapper: E1('#chat-input-file-area'),
messagesWrapper: E1('#chat-messages-wrapper'),
inputForm: E1('#chat-form'),
btnSubmit: E1('#chat-message-submit'),
inputSingle: E1('#chat-input-single'),
inputMulti: E1('#chat-input-multi'),
|
| ︙ | ︙ | |||
110 111 112 113 114 115 116 |
to avoid various race conditions in the UI and long-running
network requests. */
],
/* Injects element e as a new row in the chat, at the top of the
list if atEnd is falsy, else at the end of the list, before
the load-history widget. */
injectMessageElem: function f(e, atEnd){
| | > > | > < | < < < < < < | < < < | > | < > | < | < > | < < < < < < < < < < < < < < < < < < | < < < < < < < | < < < < | 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
to avoid various race conditions in the UI and long-running
network requests. */
],
/* Injects element e as a new row in the chat, at the top of the
list if atEnd is falsy, else at the end of the list, before
the load-history widget. */
injectMessageElem: function f(e, atEnd){
const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
holder = this.e.messagesWrapper;
if(atEnd){
const fe = mip.nextElementSibling;
if(fe) mip.parentNode.insertBefore(e, fe);
else D.append(mip.parentNode, e);
}else{
D.append(holder,e);
}
if(!atEnd && !this.isMassLoading
&& e.dataset.xfrom!==Chat.me && !isInViewport(e)){
/* If a new non-history message arrives while the user is
scrolled elsewhere, do not scroll to the latest
message, but gently alert the user that a new message
has arrived. */
F.toast.message("New message has arrived.");
}
},
/** Returns true if chat-only mode is enabled. */
isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
/**
Enters (if passed a truthy value or no arguments) or leaves
"chat-only" mode. That mode hides the page's header and
footer, leaving only the chat application visible to the
user.
*/
chatOnlyMode: function f(yes){
if(undefined === f.elemsToToggle){
f.elemsToToggle = [];
document.querySelectorAll(
"body > div.header, body > div.mainmenu, body > div.footer"
).forEach((e)=>f.elemsToToggle.push(e));
}
if(!arguments.length) yes = true;
if(yes === this.isChatOnlyMode()) return this;
if(yes){
D.addClass(f.elemsToToggle, 'hidden');
D.addClass(document.body, 'chat-only-mode');
document.body.scroll(0,document.body.height);
}else{
D.removeClass(f.elemsToToggle, 'hidden');
D.removeClass(document.body, 'chat-only-mode');
}
const msg = document.querySelector('.message-widget');
if(msg) setTimeout(()=>msg.scrollIntoView(),0);
return this;
},
toggleChatOnlyMode: function(){
return this.chatOnlyMode(!this.isChatOnlyMode());
},
settings:{
get: (k,dflt)=>F.storage.get(k,dflt),
getBool: (k,dflt)=>F.storage.getBool(k,dflt),
set: (k,v)=>F.storage.set(k,v),
defaults:{
"images-inline": !!F.config.chat.imagesInline,
"monospace-messages": false
}
}
};
/* Install default settings... */
Object.keys(cs.settings.defaults).forEach(function(k){
const v = cs.settings.get(k,cs);
if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
});
if(window.innerWidth<window.innerHeight){
/* Alignment of 'my' messages: right alignment is conventional
for mobile chat apps but can be difficult to read in wide
windows (desktop/tablet landscape mode), so we default to a
layout based on the apparently "orientation" of the window:
tall vs wide. Can be toggled via settings popup. */
document.body.classList.add('my-messages-right');
}
if(cs.settings.getBool('monospace-messages',false)){
document.body.classList.add('monospace-messages');
}
cs.e.inputCurrent = cs.e.inputSingle;
cs.pageTitleOrig = cs.e.pageTitle.innerText;
if(true){
|
| ︙ | ︙ | |||
669 670 671 672 673 674 675 |
}
},{
label: "Left-align my posts",
boolValue: ()=>!document.body.classList.contains('my-messages-right'),
callback: function f(){
document.body.classList.toggle('my-messages-right');
}
| < < < < < < < < < < < < < < < < < < < < | 641 642 643 644 645 646 647 648 649 650 651 652 653 654 |
}
},{
label: "Left-align my posts",
boolValue: ()=>!document.body.classList.contains('my-messages-right'),
callback: function f(){
document.body.classList.toggle('my-messages-right');
}
},{
label: "Images inline",
boolValue: ()=>Chat.settings.getBool('images-inline'),
callback: function(){
const v = Chat.settings.getBool('images-inline',true);
Chat.settings.set('images-inline', !v);
F.toast.message("Image mode set to "+(v ? "hyperlink" : "inline")+".");
|
| ︙ | ︙ | |||
751 752 753 754 755 756 757 |
settingsPopup.hide();
settingsPopup.options.adjustX = function(x){
const rect = settingsButton.getBoundingClientRect();
return rect.right - popupSize.width;
};
settingsPopup.options.adjustY = function(y){
const rect = settingsButton.getBoundingClientRect();
| < | < < < | 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 |
settingsPopup.hide();
settingsPopup.options.adjustX = function(x){
const rect = settingsButton.getBoundingClientRect();
return rect.right - popupSize.width;
};
settingsPopup.options.adjustY = function(y){
const rect = settingsButton.getBoundingClientRect();
return rect.top - popupSize.height -2;
};
})()/*#chat-settings-button setup*/;
/** Callback for poll() to inject new content into the page. jx ==
the response from /chat-poll. If atEnd is true, the message is
appended to the end of the chat list, else the beginning (the
|
| ︙ | ︙ | |||
808 809 810 811 812 813 814 |
(function(){
/** Add toolbar for loading older messages. We use a FIELDSET here
because a fieldset is the only parent element type which can
automatically enable/disable its children by
enabling/disabling the parent element. */
const loadLegend = D.legend("Load...");
| | | 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 |
(function(){
/** Add toolbar for loading older messages. We use a FIELDSET here
because a fieldset is the only parent element type which can
automatically enable/disable its children by
enabling/disabling the parent element. */
const loadLegend = D.legend("Load...");
const toolbar = Chat.e.loadOlderToolbar = D.attr(
D.fieldset(loadLegend), "id", "load-msg-toolbar"
);
Chat.disableDuringAjax.push(toolbar);
/* Loads the next n oldest messages, or all previous history if n is negative. */
const loadOldMessages = function(n){
Chat.ajaxStart();
var gotMessages = false;
|
| ︙ | ︙ | |||
832 833 834 835 836 837 838 |
|| 0===gotMessages/*we found no history*/
|| (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
|| (false!==gotMessages && n<0 && gotMessages<Chat.loadMessageCount
/*we asked for default amount and got fewer than that.*/)){
/* We've loaded all history. Permanently disable the
history-load toolbar and keep it from being re-enabled
via the ajaxStart()/ajaxEnd() mechanism... */
| | | | | | 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 |
|| 0===gotMessages/*we found no history*/
|| (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
|| (false!==gotMessages && n<0 && gotMessages<Chat.loadMessageCount
/*we asked for default amount and got fewer than that.*/)){
/* We've loaded all history. Permanently disable the
history-load toolbar and keep it from being re-enabled
via the ajaxStart()/ajaxEnd() mechanism... */
const div = Chat.e.loadOlderToolbar.querySelector('div');
D.append(D.clearElement(div), "All history has been loaded.");
D.addClass(Chat.e.loadOlderToolbar, 'all-done');
const ndx = Chat.disableDuringAjax.indexOf(Chat.e.loadOlderToolbar);
if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1);
Chat.e.loadOlderToolbar.disabled = true;
}
if(gotMessages > 0){
F.toast.message("Loaded "+gotMessages+" older messages.");
}
Chat.ajaxEnd();
});
};
|
| ︙ | ︙ | |||
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 |
toolbar.disabled = true /*will be enabled when msg load finishes */;
})()/*end history loading widget setup*/;
async function poll(isFirstCall){
if(poll.running) return;
poll.running = true;
if(isFirstCall) Chat.ajaxStart();
var p = fetch("chat-poll?name=" + Chat.mxMsg);
p.then(x=>x.json())
.then(y=>newcontent(y))
.catch(e=>console.error(e))
/* ^^^ we don't use Chat.reportError(e) here b/c the polling
fails exepectedly when it times out, but is then immediately
resumed, and reportError() produces a loud error message. */
.finally(function(x){
if(isFirstCall){
Chat.ajaxEnd();
Chat.e.inputWrapper.scrollIntoView();
}
poll.running=false;
});
}
poll.running = false;
poll(true);
setInterval(poll, 1000);
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
})();
| > > | 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 |
toolbar.disabled = true /*will be enabled when msg load finishes */;
})()/*end history loading widget setup*/;
async function poll(isFirstCall){
if(poll.running) return;
poll.running = true;
if(isFirstCall) Chat.ajaxStart();
Chat.isMassLoading = isFirstCall;
var p = fetch("chat-poll?name=" + Chat.mxMsg);
p.then(x=>x.json())
.then(y=>newcontent(y))
.catch(e=>console.error(e))
/* ^^^ we don't use Chat.reportError(e) here b/c the polling
fails exepectedly when it times out, but is then immediately
resumed, and reportError() produces a loud error message. */
.finally(function(x){
if(isFirstCall){
Chat.isMassLoading = false;
Chat.ajaxEnd();
Chat.e.inputWrapper.scrollIntoView();
}
poll.running=false;
});
}
poll.running = false;
poll(true);
setInterval(poll, 1000);
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
})();
|
Changes to src/default.css.
| ︙ | ︙ | |||
1634 1635 1636 1637 1638 1639 1640 |
flex: 1 1 auto/*eliminates dead no-click zones on the right*/;
}
body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] {
cursor: inherit;
}
/** Container for the list of /chat messages. */
body.chat #chat-messages-wrapper {
| < < < < < < < < | > > > > > > > < < < < < < < < < < < < < < < < < | 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 |
flex: 1 1 auto/*eliminates dead no-click zones on the right*/;
}
body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] {
cursor: inherit;
}
/** Container for the list of /chat messages. */
body.chat #chat-messages-wrapper {
}
body.chat div.content {
margin: 0;
padding: 0;
display: flex;
flex-direction: column-reverse;
/* ^^^^ In order to get good automatic scrolling of new messages on
the BOTTOM in bottom-up chat mode, such that they scroll up
instead of down, we have to use column-reverse layout, which
changes #chat-messages-wrapper's "gravity" for purposes of
scrolling! If we instead use flex-direction:column then each new
message pushes #chat-input-area down further off the screen!
*/
align-items: stretch;
}
/* Wrapper for /chat user input controls */
body.chat #chat-input-area {
display: flex;
flex-direction: column;
padding: 0.5em 1em;
border-bottom: none;
border-top: 1px solid black;
margin-bottom: 0;
margin-top: 0.5em;
position: initial /*sticky currently disabled due to scrolling-related issues*/;
bottom: 0;
}
/* Widget holding the chat message input field, send button, and
settings button. */
body.chat #chat-input-line {
display: flex;
flex-direction: row;
margin-bottom: 0.25em;
align-items: flex-start;
|
| ︙ | ︙ |