Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Added chat setting chat-inline-images: specifies whether /chat images default to display inline or as download links. Various code-adjacent tweaks. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
9d86a4af61b8cb57782ed47383934a1b |
| User & Date: | stephan 2020-12-25 14:58:22.951 |
Context
|
2020-12-25
| ||
| 15:27 | Chat settings menu tweaks based on chat session feedback. ... (check-in: 9e797bf9bf user: stephan tags: trunk) | |
| 14:58 | Added chat setting chat-inline-images: specifies whether /chat images default to display inline or as download links. Various code-adjacent tweaks. ... (check-in: 9d86a4af61 user: stephan tags: trunk) | |
| 14:36 | In the default skin, add a Chat menu item for wide screens if Chat is enabled for the user. ... (check-in: 8049da83c4 user: drh tags: trunk) | |
Changes
Changes to src/chat.c.
| ︙ | ︙ | |||
65 66 67 68 69 70 71 | ** The /chat subsystem will try to discard messages that are older then ** chat-keep-days. The value of chat-keep-days can be a floating point ** number. So, for example, if you only want to keep chat messages for ** 12 hours, set this value to 0.5. ** ** A value of 0.0 or less means that messages are retained forever. */ | | > > > > > > | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | ** The /chat subsystem will try to discard messages that are older then ** chat-keep-days. The value of chat-keep-days can be a floating point ** number. So, for example, if you only want to keep chat messages for ** 12 hours, set this value to 0.5. ** ** A value of 0.0 or less means that messages are retained forever. */ /* ** SETTING: chat-inline-images boolean default=on ** ** Specifies whether posted images in /chat should default to being ** displayed inline or as downloadable links. Each chat user can ** change this value for their current chat session in the UI. */ /* ** WEBPAGE: chat ** ** Start up a browser-based chat session. ** ** This is the main page that humans use to access the chatroom. Simply ** point a web-browser at /chat and the screen fills with the latest |
| ︙ | ︙ | |||
117 118 119 120 121 122 123 | @ </div> /* New chat messages get inserted immediately after this element */ @ <div id='chat-messages-wrapper'> @ <span id='message-inject-point'></span> @ </div> | | | > | < > > | 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 |
@ </div>
/* New chat messages get inserted immediately after this element */
@ <div id='chat-messages-wrapper'>
@ <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
initialized before the chat init code runs. */
@ window.addEventListener('load', function(){
@ window.fossil.config.chat = {
@ pingTcp: %d(iPingTcp),
@ initSize: %d(db_get_int("chat-initial-history",50)),
@ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
@ };
cgi_append_content(builtin_text("chat.js"),-1);
@ }, false);
@ </script>
style_finish_page();
}
|
| ︙ | ︙ |
Changes to src/chat.js.
| ︙ | ︙ | |||
16 17 18 19 20 21 22 |
messageInjectPoint: E1('#message-inject-point'),
pageTitle: E1('head title'),
loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
inputWrapper: E1("#chat-input-area"),
messagesWrapper: E1('#chat-messages-wrapper')
},
me: F.user.name,
| | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
messageInjectPoint: E1('#message-inject-point'),
pageTitle: E1('head title'),
loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
inputWrapper: E1("#chat-input-area"),
messagesWrapper: E1('#chat-messages-wrapper')
},
me: F.user.name,
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
pageIsActive: 'visible'===document.visibilityState,
changesSincePageHidden: 0,
notificationBubbleColor: 'white',
totalMessageCount: 0, // total # of inbound messages
//! Number of messages to load for the history buttons
loadMessageCount: Math.abs(F.config.chat.initSize || 20),
/* Alignment of 'my' messages: must be 'left' or 'right'. Note
that 'right' is conventional for mobile chat apps but can be
difficult to read in wide windows (desktop/tablet landscape
mode). Can be toggled via settings popup. */
msgMyAlign: (window.innerWidth<window.innerHeight) ? 'right' : 'left',
ajaxInflight: 0,
/** Enables (if yes is truthy) or disables all elements in
|
| ︙ | ︙ | |||
83 84 85 86 87 88 89 |
}else{
if(mip.nextSibling){
mip.parentNode.insertBefore(e, mip.nextSibling);
}else{
mip.parentNode.appendChild(e);
}
}
| > > > > > > > | > > > > > | 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
}else{
if(mip.nextSibling){
mip.parentNode.insertBefore(e, mip.nextSibling);
}else{
mip.parentNode.appendChild(e);
}
}
},
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
}
}
};
Object.keys(cs.settings.defaults).forEach(function f(k){
const v = cs.settings.get(k,f);
if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
});
cs.pageTitleOrig = cs.e.pageTitle.innerText;
const qs = (e)=>document.querySelector(e);
const argsToArray = function(args){
return Array.prototype.slice.call(args,0);
};
cs.reportError = function(/*msg args*/){
const args = argsToArray(arguments);
|
| ︙ | ︙ | |||
366 367 368 369 370 371 372 |
const settingsPopup = new F.PopupWidget({
cssClass: ['fossil-tooltip', 'chat-settings-popup'],
adjustY: function(y){
const rect = settingsButton.getBoundingClientRect();
return rect.top + rect.height + 2;
}
});
| < < | > | > > | | | > > > > > > > > > > > > > | < > > > > > > > > > > | > > > > > > | 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
const settingsPopup = new F.PopupWidget({
cssClass: ['fossil-tooltip', 'chat-settings-popup'],
adjustY: function(y){
const rect = settingsButton.getBoundingClientRect();
return rect.top + rect.height + 2;
}
});
/* Settings menu entries... */
const settingsOps = [{
label: "Toggle chat-only mode",
tooltip: "Toggles the page's header and footer on and off.",
callback: function f(){
if(undefined === f.isHidden){
f.isHidden = false;
f.elemsToToggle = [];
document.body.childNodes.forEach(function(e){
if(!e.classList) return/*TEXT nodes and such*/;
else if(!e.classList.contains('content')
&& !e.classList.contains('fossil-PopupWidget')
/*kludge^^^ for settingsPopup click handling!*/){
f.elemsToToggle.push(e);
}
});
/* In order to make the input area opaque, such that the
message list scrolls under it without being visible, we
have to ensure that the input area has a non-inherited
background color. Ideally we'd select the color of
div.content, but that is not necessarily set, so we fall
back to using the body's background color. If we rely on
the input area having its own color specified in CSS then
all skins would have to define an appropriate color.
Thus our selection of the body color, while slightly unfortunate,
is in the interest of keeping skins from being forced to
define an opaque bg color.
*/
f.initialBg = Chat.e.messagesWrapper.style.backgroundColor;
const cs = window.getComputedStyle(document.body);
f.inheritedBg = cs.backgroundColor;
}
const iws = Chat.e.inputWrapper.style;
if((f.isHidden = !f.isHidden)){
D.addClass(f.elemsToToggle, 'hidden');
D.addClass(document.body, 'chat-only-mode');
iws.backgroundColor = f.inheritedBg;
}else{
D.removeClass(f.elemsToToggle, 'hidden');
D.removeClass(document.body, 'chat-only-mode');
iws.backgroundColor = f.initialBg;
}
}
},{
label: "Toggle left/right layout",
tooltip: "Toggles your own messages between the right (mobile-style) "+
"or left of the screen (more readable on large windows).",
callback: function f(){
if('right'===Chat.msgMyAlign) Chat.msgMyAlign = 'left';
else Chat.msgMyAlign = 'right';
const msgs = Chat.e.messagesWrapper.querySelectorAll('.message-row');
msgs.forEach(function(row){
if(row.dataset.xfrom!==Chat.me) return;
row.querySelector('legend').setAttribute('align', Chat.msgMyAlign);
if('right'===Chat.msgMyAlign) row.style.justifyContent = "flex-end";
else row.style.justifyContent = "flex-start";
});
}
},{
label: "Toggle images inline",
persistent: true,
tooltip: "Toggles whether newly-arrived images appear "+
"inline or as download links.",
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")+".");
}
}];
settingsOps.forEach(function(op){
const line = D.addClass(D.span(), 'menu-entry');
const btn = D.append(D.addClass(D.span(), 'button'),
(op.persistent ? "[P] " : "")+op.label);
op.callback.button = btn;
if('function'===op.init) op.init();
if(op.tooltip){
const help = D.span();
D.append(line, help);
F.helpButtonlets.create(help, op.tooltip);
}
D.append(line, btn);
D.append(settingsPopup.e, line);
btn.addEventListener('click', function(ev){
settingsPopup.hide();
op.callback.call(this,ev);
});
});
D.append(settingsPopup.e, D.append(D.span(),"[P] = locally-persistent setting"));
// settingsPopup.installClickToHide();// Don't do this for this popup!
settingsButton.addEventListener('click',function(ev){
//ev.preventDefault();
if(settingsPopup.isShown()) settingsPopup.hide();
else settingsPopup.show(settingsButton);
/* Reminder: we cannot toggle the visibility from her
*/
}, false);
/* Find an ideal X position for the popup, directly under the settings
button, based on the size of the popup... */
settingsPopup.show(document.body);
popupSize = settingsPopup.e.getBoundingClientRect();
settingsPopup.hide();
settingsPopup.options.adjustX = function(x){
|
| ︙ | ︙ | |||
499 500 501 502 503 504 505 |
/* Show UTC on systems where Date() does not work */
eWho.append(D.text(whoName+' @ '+m.mtime.slice(11,16)))
}
let eContent = D.addClass(D.div(),'message-content','chat-message');
eContent.style.backgroundColor = m.uclr;
row.appendChild(eContent);
if( m.fsize>0 ){
| > | > > | < > > > | 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
/* Show UTC on systems where Date() does not work */
eWho.append(D.text(whoName+' @ '+m.mtime.slice(11,16)))
}
let eContent = D.addClass(D.div(),'message-content','chat-message');
eContent.style.backgroundColor = m.uclr;
row.appendChild(eContent);
if( m.fsize>0 ){
if( m.fmime
&& m.fmime.startsWith("image/")
&& Chat.settings.getBool('images-inline',true)
){
eContent.appendChild(D.img("chat-download/" + m.msgid));
}else{
const a = D.a(
window.fossil.rootPath+
'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
// ^^^ add m.fname to URL to cause downloaded file to have that name.
"(" + m.fname + " " + m.fsize + " bytes)"
)
D.attr(a,'target','_blank');
eContent.appendChild(a);
}
const br = D.br();
br.style.clear = "both";
eContent.appendChild(br);
}
if(m.xmsg){
// The m.xmsg text comes from the same server as this script and
|
| ︙ | ︙ | |||
538 539 540 541 542 543 544 |
Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
}
}else{
Chat.changesSincePageHidden += jx.msgs.length;
Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
Chat.pageTitleOrig;
}
| | | | 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 |
Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
}
}else{
Chat.changesSincePageHidden += jx.msgs.length;
Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
Chat.pageTitleOrig;
}
if(jx.msgs.length && F.config.chat.pingTcp){
fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
}
}/*newcontent()*/;
(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
|
| ︙ | ︙ |
Changes to src/default.css.
| ︙ | ︙ | |||
1259 1260 1261 1262 1263 1264 1265 | position: absolute; display: inline-block; z-index: 19/*below default skin's hamburger popup*/; box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75); background-color: inherit; color: inherit; } | > > > > > | | 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 |
position: absolute;
display: inline-block;
z-index: 19/*below default skin's hamburger popup*/;
box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
background-color: inherit;
color: inherit;
}
.fossil-PopupWidget {
/* This class is ALWAYS set on every fossil.PopupWidget instance, in
addition to client/app-configured classes. It should not get any
style - it is only used for DOM element selecting/filtering
purposes. */
}
.fossil-toast-message {
/* "toast"-style popup message.
See fossil.popupwidget:toast() */
position: absolute;
display: block;
z-index: 1001;
text-align: left;
|
| ︙ | ︙ | |||
1575 1576 1577 1578 1579 1580 1581 |
display: flex;
flex-direction: column;
align-items: stretch;
padding: 0.25em;
z-index: 200;
}
body.chat .chat-settings-popup > span {
| | < > > > > > > > > > > > | 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 |
display: flex;
flex-direction: column;
align-items: stretch;
padding: 0.25em;
z-index: 200;
}
body.chat .chat-settings-popup > span {
vertical-align: middle;
}
body.chat .chat-settings-popup > span.menu-entry{
white-space: nowrap;
cursor: pointer;
border: 1px outset;
border-radius: 0.25em;
margin: 0.25em 0.2em;
padding: 0.5em;
}
body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
vertical-align: middle;
}
body.chat .chat-settings-popup > span.menu-entry > span.button {
margin: 0.25em 0.2em;
padding: 0.5em;
}
body.chat #chat-messages-wrapper {
display: flex;
flex-direction: column;
}
body.chat.chat-only-mode{
}
|
| ︙ | ︙ |
Changes to src/fossil.popupwidget.js.
| ︙ | ︙ | |||
47 48 49 50 51 52 53 |
removed from the object immediately after it is called.
All callback options are called with the PopupWidget object as
their "this".
.cssClass: optional CSS class, or list of classes, to apply to
| | > > > | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
removed from the object immediately after it is called.
All callback options are called with the PopupWidget object as
their "this".
.cssClass: optional CSS class, or list of classes, to apply to
the new element. In addition to any supplied here (or inherited
from the default), the class "fossil-PopupWidget" is always set
in order to allow certain app-internal CSS to account for popup
windows in special cases.
.style: optional object of properties to copy directly into
the element's style object.
The options passed to this constructor get normalized into a
separate object which includes any default values for options not
provided by the caller. That object is available this the
|
| ︙ | ︙ | |||
82 83 84 85 86 87 88 |
tip.show(50, 100);
// ^^^ viewport-relative coordinates. See show() for other options.
*/
F.PopupWidget = function f(opt){
opt = F.mergeLastWins(f.defaultOptions,opt);
this.options = opt;
| | > | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
tip.show(50, 100);
// ^^^ viewport-relative coordinates. See show() for other options.
*/
F.PopupWidget = function f(opt){
opt = F.mergeLastWins(f.defaultOptions,opt);
this.options = opt;
const e = this.e = D.addClass(D.div(), opt.cssClass,
"fossil-PopupWidget");
this.show(false);
if(opt.style){
let k;
for(k in opt.style){
if(opt.style.hasOwnProperty(k)) e.style[k] = opt.style[k];
}
}
|
| ︙ | ︙ | |||
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
during initialization and stashed away for use in a PopupWidget
when the botton is clicked.
*/
setup: function f(){
if(!f.hasOwnProperty('clickHandler')){
f.clickHandler = function fch(ev){
if(!fch.popup){
fch.popup = new F.PopupWidget({
cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
refresh: function(){
}
});
fch.popup.e.style.maxWidth = '80%'/*of body*/;
fch.popup.installClickToHide();
}
D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
| > < < < < > > > > > > > > > > > > > > > > > > > > > | 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
during initialization and stashed away for use in a PopupWidget
when the botton is clicked.
*/
setup: function f(){
if(!f.hasOwnProperty('clickHandler')){
f.clickHandler = function fch(ev){
ev.preventDefault();
if(!fch.popup){
fch.popup = new F.PopupWidget({
cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
refresh: function(){
}
});
fch.popup.e.style.maxWidth = '80%'/*of body*/;
fch.popup.installClickToHide();
}
D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
/* Shift the help around a bit to "better" fit the
screen. However, fch.popup.e.getClientRects() is empty
until the popup is shown, so we have to show it,
calculate the resulting size, then move and/or resize it.
This algorithm/these heuristics can certainly be improved
upon.
*/
var popupRect, rectElem = ev.target;
while(rectElem){
popupRect = rectElem.getClientRects()[0]/*undefined if off-screen!*/;
if(popupRect) break;
rectElem = rectElem.parentNode;
}
if(!popupRect) popupRect = {x:0, y:0, left:0, right:0};
var x = popupRect.left, y = popupRect.top;
if(x<0) x = 0;
if(y<0) y = 0;
if(rectElem){
/* Try to ensure that the popup's z-level is higher than this element's */
const rz = window.getComputedStyle(rectElem).zIndex;
var myZ;
if(rz && !isNaN(+rz)){
myZ = +rz + 1;
}else{
myZ = 10000/*guess!*/;
}
fch.popup.e.style.zIndex = myZ;
}
fch.popup.show(x, y);
x = popupRect.left, y = popupRect.top;
popupRect = fch.popup.e.getBoundingClientRect();
const rectBody = document.body.getClientRects()[0];
if(popupRect.right > rectBody.right){
x -= (popupRect.right - rectBody.right);
}
|
| ︙ | ︙ |
Changes to src/fossil.storage.js.
| ︙ | ︙ | |||
63 64 65 66 67 68 69 |
See: https://fossil-scm.org/forum/forumpost/4afc4d34de
Sidebar: it might seem odd to provide a key prefix and stick all
properties in the topmost level of the storage object. We do that
because adding a layer of object to sandbox each repo would mean
(de)serializing that whole tree on every storage property change
| | | | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
See: https://fossil-scm.org/forum/forumpost/4afc4d34de
Sidebar: it might seem odd to provide a key prefix and stick all
properties in the topmost level of the storage object. We do that
because adding a layer of object to sandbox each repo would mean
(de)serializing that whole tree on every storage property change
(and we update storage often during editing sessions).
e.g. instead of storageObject.projectName.foo we have
storageObject[storageKeyPrefix+'foo']. That's soley for
efficiency's sake (in terms of battery life and
environment-internal storage-level effort). Even so, it might (or
might not) be useful to do that someday.
*/
const storageKeyPrefix = (
$storageHolder===$storage/*localStorage or sessionStorage*/
? (
F.config.projectCode || F.config.projectName
|| F.config.shortProjectName || window.location.pathname
)+'::' : (
|
| ︙ | ︙ | |||
99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
/** Sets storage key k to JSON.stringify(v). */
setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
/** Returns the value for the given storage key, or
dflt if the key is not found in the storage. */
get: (k,dflt)=>$storageHolder.hasOwnProperty(
storageKeyPrefix+k
) ? $storage.getItem(storageKeyPrefix+k) : dflt,
/** Returns the JSON.parse()'d value of the given
storage key's value, or dflt is the key is not
found or JSON.parse() fails. */
getJSON: function f(k,dflt){
try {
const x = this.get(k,f);
return x===f ? dflt : JSON.parse(x);
| > > > > > > > | 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
/** Sets storage key k to JSON.stringify(v). */
setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
/** Returns the value for the given storage key, or
dflt if the key is not found in the storage. */
get: (k,dflt)=>$storageHolder.hasOwnProperty(
storageKeyPrefix+k
) ? $storage.getItem(storageKeyPrefix+k) : dflt,
/** Returns true if the given key has a value of "true". If the
key is not found, it returns true if the boolean value of dflt
is "true". (Remember that JS persistent storage values are all
strings.) */
getBool: function(k,dflt){
return 'true'===this.get(k,''+(!!dflt));
},
/** Returns the JSON.parse()'d value of the given
storage key's value, or dflt is the key is not
found or JSON.parse() fails. */
getJSON: function f(k,dflt){
try {
const x = this.get(k,f);
return x===f ? dflt : JSON.parse(x);
|
| ︙ | ︙ |