Fossil SCM

Added chat setting chat-inline-images: specifies whether /chat images default to display inline or as download links. Various code-adjacent tweaks.

stephan 2020-12-25 14:58 trunk
Commit 9d86a4af61b8cb57782ed47383934a1bbd0c8c27d04e53b38fcfa6d4e6d1054c
+13 -5
--- src/chat.c
+++ src/chat.c
@@ -67,11 +67,17 @@
6767
** number. So, for example, if you only want to keep chat messages for
6868
** 12 hours, set this value to 0.5.
6969
**
7070
** A value of 0.0 or less means that messages are retained forever.
7171
*/
72
-
72
+/*
73
+** SETTING: chat-inline-images boolean default=on
74
+**
75
+** Specifies whether posted images in /chat should default to being
76
+** displayed inline or as downloadable links. Each chat user can
77
+** change this value for their current chat session in the UI.
78
+*/
7379
/*
7480
** WEBPAGE: chat
7581
**
7682
** Start up a browser-based chat session.
7783
**
@@ -119,19 +125,21 @@
119125
/* New chat messages get inserted immediately after this element */
120126
@ <div id='chat-messages-wrapper'>
121127
@ <span id='message-inject-point'></span>
122128
@ </div>
123129
124
- builtin_fossil_js_bundle_or("popupwidget", NULL);
130
+ builtin_fossil_js_bundle_or("popupwidget", "storage", NULL);
125131
/* Always in-line the javascript for the chat page */
126132
@ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
127133
/* We need an onload handler to ensure that window.fossil is
128134
initialized before the chat init code runs. */
129135
@ window.addEventListener('load', function(){
130
- @ window.fossil.config.chatInitSize =\
131
- @ %d(db_get_int("chat-initial-history",50));
132
- @ window.fossil.config.pingTcp = %d(iPingTcp);
136
+ @ window.fossil.config.chat = {
137
+ @ pingTcp: %d(iPingTcp),
138
+ @ initSize: %d(db_get_int("chat-initial-history",50)),
139
+ @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
140
+ @ };
133141
cgi_append_content(builtin_text("chat.js"),-1);
134142
@ }, false);
135143
@ </script>
136144
137145
style_finish_page();
138146
--- src/chat.c
+++ src/chat.c
@@ -67,11 +67,17 @@
67 ** number. So, for example, if you only want to keep chat messages for
68 ** 12 hours, set this value to 0.5.
69 **
70 ** A value of 0.0 or less means that messages are retained forever.
71 */
72
 
 
 
 
 
 
73 /*
74 ** WEBPAGE: chat
75 **
76 ** Start up a browser-based chat session.
77 **
@@ -119,19 +125,21 @@
119 /* New chat messages get inserted immediately after this element */
120 @ <div id='chat-messages-wrapper'>
121 @ <span id='message-inject-point'></span>
122 @ </div>
123
124 builtin_fossil_js_bundle_or("popupwidget", NULL);
125 /* Always in-line the javascript for the chat page */
126 @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
127 /* We need an onload handler to ensure that window.fossil is
128 initialized before the chat init code runs. */
129 @ window.addEventListener('load', function(){
130 @ window.fossil.config.chatInitSize =\
131 @ %d(db_get_int("chat-initial-history",50));
132 @ window.fossil.config.pingTcp = %d(iPingTcp);
 
 
133 cgi_append_content(builtin_text("chat.js"),-1);
134 @ }, false);
135 @ </script>
136
137 style_finish_page();
138
--- src/chat.c
+++ src/chat.c
@@ -67,11 +67,17 @@
67 ** number. So, for example, if you only want to keep chat messages for
68 ** 12 hours, set this value to 0.5.
69 **
70 ** A value of 0.0 or less means that messages are retained forever.
71 */
72 /*
73 ** SETTING: chat-inline-images boolean default=on
74 **
75 ** Specifies whether posted images in /chat should default to being
76 ** displayed inline or as downloadable links. Each chat user can
77 ** change this value for their current chat session in the UI.
78 */
79 /*
80 ** WEBPAGE: chat
81 **
82 ** Start up a browser-based chat session.
83 **
@@ -119,19 +125,21 @@
125 /* New chat messages get inserted immediately after this element */
126 @ <div id='chat-messages-wrapper'>
127 @ <span id='message-inject-point'></span>
128 @ </div>
129
130 builtin_fossil_js_bundle_or("popupwidget", "storage", NULL);
131 /* Always in-line the javascript for the chat page */
132 @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
133 /* We need an onload handler to ensure that window.fossil is
134 initialized before the chat init code runs. */
135 @ window.addEventListener('load', function(){
136 @ window.fossil.config.chat = {
137 @ pingTcp: %d(iPingTcp),
138 @ initSize: %d(db_get_int("chat-initial-history",50)),
139 @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
140 @ };
141 cgi_append_content(builtin_text("chat.js"),-1);
142 @ }, false);
143 @ </script>
144
145 style_finish_page();
146
+63 -17
--- src/chat.js
+++ src/chat.js
@@ -18,18 +18,18 @@
1818
loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
1919
inputWrapper: E1("#chat-input-area"),
2020
messagesWrapper: E1('#chat-messages-wrapper')
2121
},
2222
me: F.user.name,
23
- mxMsg: F.config.chatInitSize ? -F.config.chatInitSize : -50,
23
+ mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
2424
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
2525
pageIsActive: 'visible'===document.visibilityState,
2626
changesSincePageHidden: 0,
2727
notificationBubbleColor: 'white',
2828
totalMessageCount: 0, // total # of inbound messages
2929
//! Number of messages to load for the history buttons
30
- loadMessageCount: Math.abs(F.config.chatInitSize || 20),
30
+ loadMessageCount: Math.abs(F.config.chat.initSize || 20),
3131
/* Alignment of 'my' messages: must be 'left' or 'right'. Note
3232
that 'right' is conventional for mobile chat apps but can be
3333
difficult to read in wide windows (desktop/tablet landscape
3434
mode). Can be toggled via settings popup. */
3535
msgMyAlign: (window.innerWidth<window.innerHeight) ? 'right' : 'left',
@@ -85,12 +85,24 @@
8585
mip.parentNode.insertBefore(e, mip.nextSibling);
8686
}else{
8787
mip.parentNode.appendChild(e);
8888
}
8989
}
90
+ },
91
+ settings:{
92
+ get: (k,dflt)=>F.storage.get(k,dflt),
93
+ getBool: (k,dflt)=>F.storage.getBool(k,dflt),
94
+ set: (k,v)=>F.storage.set(k,v),
95
+ defaults:{
96
+ "images-inline": !!F.config.chat.imagesInline
97
+ }
9098
}
9199
};
100
+ Object.keys(cs.settings.defaults).forEach(function f(k){
101
+ const v = cs.settings.get(k,f);
102
+ if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
103
+ });
92104
cs.pageTitleOrig = cs.e.pageTitle.innerText;
93105
const qs = (e)=>document.querySelector(e);
94106
const argsToArray = function(args){
95107
return Array.prototype.slice.call(args,0);
96108
};
@@ -368,22 +380,23 @@
368380
adjustY: function(y){
369381
const rect = settingsButton.getBoundingClientRect();
370382
return rect.top + rect.height + 2;
371383
}
372384
});
373
- settingsPopup.installClickToHide();
374
-
375385
/* Settings menu entries... */
376386
const settingsOps = [{
377
- label: "Toggle page body",
387
+ label: "Toggle chat-only mode",
388
+ tooltip: "Toggles the page's header and footer on and off.",
378389
callback: function f(){
379390
if(undefined === f.isHidden){
380391
f.isHidden = false;
381392
f.elemsToToggle = [];
382393
document.body.childNodes.forEach(function(e){
383394
if(!e.classList) return/*TEXT nodes and such*/;
384
- else if(!e.classList.contains('content')){
395
+ else if(!e.classList.contains('content')
396
+ && !e.classList.contains('fossil-PopupWidget')
397
+ /*kludge^^^ for settingsPopup click handling!*/){
385398
f.elemsToToggle.push(e);
386399
}
387400
});
388401
/* In order to make the input area opaque, such that the
389402
message list scrolls under it without being visible, we
@@ -399,23 +412,25 @@
399412
*/
400413
f.initialBg = Chat.e.messagesWrapper.style.backgroundColor;
401414
const cs = window.getComputedStyle(document.body);
402415
f.inheritedBg = cs.backgroundColor;
403416
}
404
- const cs = Chat.e.inputWrapper.style;
417
+ const iws = Chat.e.inputWrapper.style;
405418
if((f.isHidden = !f.isHidden)){
406419
D.addClass(f.elemsToToggle, 'hidden');
407420
D.addClass(document.body, 'chat-only-mode');
408
- cs.backgroundColor = f.inheritedBg;
421
+ iws.backgroundColor = f.inheritedBg;
409422
}else{
410423
D.removeClass(f.elemsToToggle, 'hidden');
411424
D.removeClass(document.body, 'chat-only-mode');
412
- cs.backgroundColor = f.initialBg;
425
+ iws.backgroundColor = f.initialBg;
413426
}
414427
}
415428
},{
416429
label: "Toggle left/right layout",
430
+ tooltip: "Toggles your own messages between the right (mobile-style) "+
431
+ "or left of the screen (more readable on large windows).",
417432
callback: function f(){
418433
if('right'===Chat.msgMyAlign) Chat.msgMyAlign = 'left';
419434
else Chat.msgMyAlign = 'right';
420435
const msgs = Chat.e.messagesWrapper.querySelectorAll('.message-row');
421436
msgs.forEach(function(row){
@@ -423,23 +438,49 @@
423438
row.querySelector('legend').setAttribute('align', Chat.msgMyAlign);
424439
if('right'===Chat.msgMyAlign) row.style.justifyContent = "flex-end";
425440
else row.style.justifyContent = "flex-start";
426441
});
427442
}
443
+ },{
444
+ label: "Toggle images inline",
445
+ persistent: true,
446
+ tooltip: "Toggles whether newly-arrived images appear "+
447
+ "inline or as download links.",
448
+ callback: function(){
449
+ const v = Chat.settings.getBool('images-inline',true);
450
+ Chat.settings.set('images-inline', !v);
451
+ F.toast.message("Image mode set to "+(v ? "hyperlink" : "inline")+".");
452
+ }
428453
}];
429454
430455
settingsOps.forEach(function(op){
431
- const btn = D.append(D.span(), op.label);
432
- D.append(settingsPopup.e, btn);
456
+ const line = D.addClass(D.span(), 'menu-entry');
457
+ const btn = D.append(D.addClass(D.span(), 'button'),
458
+ (op.persistent ? "[P] " : "")+op.label);
433459
op.callback.button = btn;
434460
if('function'===op.init) op.init();
461
+ if(op.tooltip){
462
+ const help = D.span();
463
+ D.append(line, help);
464
+ F.helpButtonlets.create(help, op.tooltip);
465
+ }
466
+ D.append(line, btn);
467
+ D.append(settingsPopup.e, line);
435468
btn.addEventListener('click', function(ev){
436469
settingsPopup.hide();
437470
op.callback.call(this,ev);
438471
});
439472
});
440
- settingsButton.addEventListener('click',()=>settingsPopup.show(settingsButton), false);
473
+ D.append(settingsPopup.e, D.append(D.span(),"[P] = locally-persistent setting"));
474
+ // settingsPopup.installClickToHide();// Don't do this for this popup!
475
+ settingsButton.addEventListener('click',function(ev){
476
+ //ev.preventDefault();
477
+ if(settingsPopup.isShown()) settingsPopup.hide();
478
+ else settingsPopup.show(settingsButton);
479
+ /* Reminder: we cannot toggle the visibility from her
480
+ */
481
+ }, false);
441482
442483
/* Find an ideal X position for the popup, directly under the settings
443484
button, based on the size of the popup... */
444485
settingsPopup.show(document.body);
445486
popupSize = settingsPopup.e.getBoundingClientRect();
@@ -501,19 +542,24 @@
501542
}
502543
let eContent = D.addClass(D.div(),'message-content','chat-message');
503544
eContent.style.backgroundColor = m.uclr;
504545
row.appendChild(eContent);
505546
if( m.fsize>0 ){
506
- if( m.fmime && m.fmime.startsWith("image/") ){
547
+ if( m.fmime
548
+ && m.fmime.startsWith("image/")
549
+ && Chat.settings.getBool('images-inline',true)
550
+ ){
507551
eContent.appendChild(D.img("chat-download/" + m.msgid));
508552
}else{
509
- eContent.appendChild(D.a(
553
+ const a = D.a(
510554
window.fossil.rootPath+
511555
'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
512556
// ^^^ add m.fname to URL to cause downloaded file to have that name.
513557
"(" + m.fname + " " + m.fsize + " bytes)"
514
- ));
558
+ )
559
+ D.attr(a,'target','_blank');
560
+ eContent.appendChild(a);
515561
}
516562
const br = D.br();
517563
br.style.clear = "both";
518564
eContent.appendChild(br);
519565
}
@@ -540,12 +586,12 @@
540586
}else{
541587
Chat.changesSincePageHidden += jx.msgs.length;
542588
Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
543589
Chat.pageTitleOrig;
544590
}
545
- if(jx.msgs.length && F.config.pingTcp){
546
- fetch("http:/"+"/localhost:"+window.fossil.config.pingTcp+"/chat-ping");
591
+ if(jx.msgs.length && F.config.chat.pingTcp){
592
+ fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
547593
}
548594
}/*newcontent()*/;
549595
550596
(function(){
551597
/** Add toolbar for loading older messages. We use a FIELDSET here
552598
--- src/chat.js
+++ src/chat.js
@@ -18,18 +18,18 @@
18 loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
19 inputWrapper: E1("#chat-input-area"),
20 messagesWrapper: E1('#chat-messages-wrapper')
21 },
22 me: F.user.name,
23 mxMsg: F.config.chatInitSize ? -F.config.chatInitSize : -50,
24 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
25 pageIsActive: 'visible'===document.visibilityState,
26 changesSincePageHidden: 0,
27 notificationBubbleColor: 'white',
28 totalMessageCount: 0, // total # of inbound messages
29 //! Number of messages to load for the history buttons
30 loadMessageCount: Math.abs(F.config.chatInitSize || 20),
31 /* Alignment of 'my' messages: must be 'left' or 'right'. Note
32 that 'right' is conventional for mobile chat apps but can be
33 difficult to read in wide windows (desktop/tablet landscape
34 mode). Can be toggled via settings popup. */
35 msgMyAlign: (window.innerWidth<window.innerHeight) ? 'right' : 'left',
@@ -85,12 +85,24 @@
85 mip.parentNode.insertBefore(e, mip.nextSibling);
86 }else{
87 mip.parentNode.appendChild(e);
88 }
89 }
 
 
 
 
 
 
 
 
90 }
91 };
 
 
 
 
92 cs.pageTitleOrig = cs.e.pageTitle.innerText;
93 const qs = (e)=>document.querySelector(e);
94 const argsToArray = function(args){
95 return Array.prototype.slice.call(args,0);
96 };
@@ -368,22 +380,23 @@
368 adjustY: function(y){
369 const rect = settingsButton.getBoundingClientRect();
370 return rect.top + rect.height + 2;
371 }
372 });
373 settingsPopup.installClickToHide();
374
375 /* Settings menu entries... */
376 const settingsOps = [{
377 label: "Toggle page body",
 
378 callback: function f(){
379 if(undefined === f.isHidden){
380 f.isHidden = false;
381 f.elemsToToggle = [];
382 document.body.childNodes.forEach(function(e){
383 if(!e.classList) return/*TEXT nodes and such*/;
384 else if(!e.classList.contains('content')){
 
 
385 f.elemsToToggle.push(e);
386 }
387 });
388 /* In order to make the input area opaque, such that the
389 message list scrolls under it without being visible, we
@@ -399,23 +412,25 @@
399 */
400 f.initialBg = Chat.e.messagesWrapper.style.backgroundColor;
401 const cs = window.getComputedStyle(document.body);
402 f.inheritedBg = cs.backgroundColor;
403 }
404 const cs = Chat.e.inputWrapper.style;
405 if((f.isHidden = !f.isHidden)){
406 D.addClass(f.elemsToToggle, 'hidden');
407 D.addClass(document.body, 'chat-only-mode');
408 cs.backgroundColor = f.inheritedBg;
409 }else{
410 D.removeClass(f.elemsToToggle, 'hidden');
411 D.removeClass(document.body, 'chat-only-mode');
412 cs.backgroundColor = f.initialBg;
413 }
414 }
415 },{
416 label: "Toggle left/right layout",
 
 
417 callback: function f(){
418 if('right'===Chat.msgMyAlign) Chat.msgMyAlign = 'left';
419 else Chat.msgMyAlign = 'right';
420 const msgs = Chat.e.messagesWrapper.querySelectorAll('.message-row');
421 msgs.forEach(function(row){
@@ -423,23 +438,49 @@
423 row.querySelector('legend').setAttribute('align', Chat.msgMyAlign);
424 if('right'===Chat.msgMyAlign) row.style.justifyContent = "flex-end";
425 else row.style.justifyContent = "flex-start";
426 });
427 }
 
 
 
 
 
 
 
 
 
 
428 }];
429
430 settingsOps.forEach(function(op){
431 const btn = D.append(D.span(), op.label);
432 D.append(settingsPopup.e, btn);
 
433 op.callback.button = btn;
434 if('function'===op.init) op.init();
 
 
 
 
 
 
 
435 btn.addEventListener('click', function(ev){
436 settingsPopup.hide();
437 op.callback.call(this,ev);
438 });
439 });
440 settingsButton.addEventListener('click',()=>settingsPopup.show(settingsButton), false);
 
 
 
 
 
 
 
 
441
442 /* Find an ideal X position for the popup, directly under the settings
443 button, based on the size of the popup... */
444 settingsPopup.show(document.body);
445 popupSize = settingsPopup.e.getBoundingClientRect();
@@ -501,19 +542,24 @@
501 }
502 let eContent = D.addClass(D.div(),'message-content','chat-message');
503 eContent.style.backgroundColor = m.uclr;
504 row.appendChild(eContent);
505 if( m.fsize>0 ){
506 if( m.fmime && m.fmime.startsWith("image/") ){
 
 
 
507 eContent.appendChild(D.img("chat-download/" + m.msgid));
508 }else{
509 eContent.appendChild(D.a(
510 window.fossil.rootPath+
511 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
512 // ^^^ add m.fname to URL to cause downloaded file to have that name.
513 "(" + m.fname + " " + m.fsize + " bytes)"
514 ));
 
 
515 }
516 const br = D.br();
517 br.style.clear = "both";
518 eContent.appendChild(br);
519 }
@@ -540,12 +586,12 @@
540 }else{
541 Chat.changesSincePageHidden += jx.msgs.length;
542 Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
543 Chat.pageTitleOrig;
544 }
545 if(jx.msgs.length && F.config.pingTcp){
546 fetch("http:/"+"/localhost:"+window.fossil.config.pingTcp+"/chat-ping");
547 }
548 }/*newcontent()*/;
549
550 (function(){
551 /** Add toolbar for loading older messages. We use a FIELDSET here
552
--- src/chat.js
+++ src/chat.js
@@ -18,18 +18,18 @@
18 loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
19 inputWrapper: E1("#chat-input-area"),
20 messagesWrapper: E1('#chat-messages-wrapper')
21 },
22 me: F.user.name,
23 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
24 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
25 pageIsActive: 'visible'===document.visibilityState,
26 changesSincePageHidden: 0,
27 notificationBubbleColor: 'white',
28 totalMessageCount: 0, // total # of inbound messages
29 //! Number of messages to load for the history buttons
30 loadMessageCount: Math.abs(F.config.chat.initSize || 20),
31 /* Alignment of 'my' messages: must be 'left' or 'right'. Note
32 that 'right' is conventional for mobile chat apps but can be
33 difficult to read in wide windows (desktop/tablet landscape
34 mode). Can be toggled via settings popup. */
35 msgMyAlign: (window.innerWidth<window.innerHeight) ? 'right' : 'left',
@@ -85,12 +85,24 @@
85 mip.parentNode.insertBefore(e, mip.nextSibling);
86 }else{
87 mip.parentNode.appendChild(e);
88 }
89 }
90 },
91 settings:{
92 get: (k,dflt)=>F.storage.get(k,dflt),
93 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
94 set: (k,v)=>F.storage.set(k,v),
95 defaults:{
96 "images-inline": !!F.config.chat.imagesInline
97 }
98 }
99 };
100 Object.keys(cs.settings.defaults).forEach(function f(k){
101 const v = cs.settings.get(k,f);
102 if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
103 });
104 cs.pageTitleOrig = cs.e.pageTitle.innerText;
105 const qs = (e)=>document.querySelector(e);
106 const argsToArray = function(args){
107 return Array.prototype.slice.call(args,0);
108 };
@@ -368,22 +380,23 @@
380 adjustY: function(y){
381 const rect = settingsButton.getBoundingClientRect();
382 return rect.top + rect.height + 2;
383 }
384 });
 
 
385 /* Settings menu entries... */
386 const settingsOps = [{
387 label: "Toggle chat-only mode",
388 tooltip: "Toggles the page's header and footer on and off.",
389 callback: function f(){
390 if(undefined === f.isHidden){
391 f.isHidden = false;
392 f.elemsToToggle = [];
393 document.body.childNodes.forEach(function(e){
394 if(!e.classList) return/*TEXT nodes and such*/;
395 else if(!e.classList.contains('content')
396 && !e.classList.contains('fossil-PopupWidget')
397 /*kludge^^^ for settingsPopup click handling!*/){
398 f.elemsToToggle.push(e);
399 }
400 });
401 /* In order to make the input area opaque, such that the
402 message list scrolls under it without being visible, we
@@ -399,23 +412,25 @@
412 */
413 f.initialBg = Chat.e.messagesWrapper.style.backgroundColor;
414 const cs = window.getComputedStyle(document.body);
415 f.inheritedBg = cs.backgroundColor;
416 }
417 const iws = Chat.e.inputWrapper.style;
418 if((f.isHidden = !f.isHidden)){
419 D.addClass(f.elemsToToggle, 'hidden');
420 D.addClass(document.body, 'chat-only-mode');
421 iws.backgroundColor = f.inheritedBg;
422 }else{
423 D.removeClass(f.elemsToToggle, 'hidden');
424 D.removeClass(document.body, 'chat-only-mode');
425 iws.backgroundColor = f.initialBg;
426 }
427 }
428 },{
429 label: "Toggle left/right layout",
430 tooltip: "Toggles your own messages between the right (mobile-style) "+
431 "or left of the screen (more readable on large windows).",
432 callback: function f(){
433 if('right'===Chat.msgMyAlign) Chat.msgMyAlign = 'left';
434 else Chat.msgMyAlign = 'right';
435 const msgs = Chat.e.messagesWrapper.querySelectorAll('.message-row');
436 msgs.forEach(function(row){
@@ -423,23 +438,49 @@
438 row.querySelector('legend').setAttribute('align', Chat.msgMyAlign);
439 if('right'===Chat.msgMyAlign) row.style.justifyContent = "flex-end";
440 else row.style.justifyContent = "flex-start";
441 });
442 }
443 },{
444 label: "Toggle images inline",
445 persistent: true,
446 tooltip: "Toggles whether newly-arrived images appear "+
447 "inline or as download links.",
448 callback: function(){
449 const v = Chat.settings.getBool('images-inline',true);
450 Chat.settings.set('images-inline', !v);
451 F.toast.message("Image mode set to "+(v ? "hyperlink" : "inline")+".");
452 }
453 }];
454
455 settingsOps.forEach(function(op){
456 const line = D.addClass(D.span(), 'menu-entry');
457 const btn = D.append(D.addClass(D.span(), 'button'),
458 (op.persistent ? "[P] " : "")+op.label);
459 op.callback.button = btn;
460 if('function'===op.init) op.init();
461 if(op.tooltip){
462 const help = D.span();
463 D.append(line, help);
464 F.helpButtonlets.create(help, op.tooltip);
465 }
466 D.append(line, btn);
467 D.append(settingsPopup.e, line);
468 btn.addEventListener('click', function(ev){
469 settingsPopup.hide();
470 op.callback.call(this,ev);
471 });
472 });
473 D.append(settingsPopup.e, D.append(D.span(),"[P] = locally-persistent setting"));
474 // settingsPopup.installClickToHide();// Don't do this for this popup!
475 settingsButton.addEventListener('click',function(ev){
476 //ev.preventDefault();
477 if(settingsPopup.isShown()) settingsPopup.hide();
478 else settingsPopup.show(settingsButton);
479 /* Reminder: we cannot toggle the visibility from her
480 */
481 }, false);
482
483 /* Find an ideal X position for the popup, directly under the settings
484 button, based on the size of the popup... */
485 settingsPopup.show(document.body);
486 popupSize = settingsPopup.e.getBoundingClientRect();
@@ -501,19 +542,24 @@
542 }
543 let eContent = D.addClass(D.div(),'message-content','chat-message');
544 eContent.style.backgroundColor = m.uclr;
545 row.appendChild(eContent);
546 if( m.fsize>0 ){
547 if( m.fmime
548 && m.fmime.startsWith("image/")
549 && Chat.settings.getBool('images-inline',true)
550 ){
551 eContent.appendChild(D.img("chat-download/" + m.msgid));
552 }else{
553 const a = D.a(
554 window.fossil.rootPath+
555 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
556 // ^^^ add m.fname to URL to cause downloaded file to have that name.
557 "(" + m.fname + " " + m.fsize + " bytes)"
558 )
559 D.attr(a,'target','_blank');
560 eContent.appendChild(a);
561 }
562 const br = D.br();
563 br.style.clear = "both";
564 eContent.appendChild(br);
565 }
@@ -540,12 +586,12 @@
586 }else{
587 Chat.changesSincePageHidden += jx.msgs.length;
588 Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
589 Chat.pageTitleOrig;
590 }
591 if(jx.msgs.length && F.config.chat.pingTcp){
592 fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
593 }
594 }/*newcontent()*/;
595
596 (function(){
597 /** Add toolbar for loading older messages. We use a FIELDSET here
598
+18 -3
--- src/default.css
+++ src/default.css
@@ -1261,11 +1261,16 @@
12611261
z-index: 19/*below default skin's hamburger popup*/;
12621262
box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
12631263
background-color: inherit;
12641264
color: inherit;
12651265
}
1266
-
1266
+.fossil-PopupWidget {
1267
+ /* This class is ALWAYS set on every fossil.PopupWidget instance, in
1268
+ addition to client/app-configured classes. It should not get any
1269
+ style - it is only used for DOM element selecting/filtering
1270
+ purposes. */
1271
+}
12671272
.fossil-toast-message {
12681273
/* "toast"-style popup message.
12691274
See fossil.popupwidget:toast() */
12701275
position: absolute;
12711276
display: block;
@@ -1577,16 +1582,26 @@
15771582
align-items: stretch;
15781583
padding: 0.25em;
15791584
z-index: 200;
15801585
}
15811586
body.chat .chat-settings-popup > span {
1582
- margin: 0.25em 0.2em;
1583
- padding: 0.5em;
1587
+ vertical-align: middle;
1588
+}
1589
+body.chat .chat-settings-popup > span.menu-entry{
15841590
white-space: nowrap;
15851591
cursor: pointer;
15861592
border: 1px outset;
15871593
border-radius: 0.25em;
1594
+ margin: 0.25em 0.2em;
1595
+ padding: 0.5em;
1596
+}
1597
+body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
1598
+ vertical-align: middle;
1599
+}
1600
+body.chat .chat-settings-popup > span.menu-entry > span.button {
1601
+ margin: 0.25em 0.2em;
1602
+ padding: 0.5em;
15881603
}
15891604
body.chat #chat-messages-wrapper {
15901605
display: flex;
15911606
flex-direction: column;
15921607
}
15931608
--- src/default.css
+++ src/default.css
@@ -1261,11 +1261,16 @@
1261 z-index: 19/*below default skin's hamburger popup*/;
1262 box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
1263 background-color: inherit;
1264 color: inherit;
1265 }
1266
 
 
 
 
 
1267 .fossil-toast-message {
1268 /* "toast"-style popup message.
1269 See fossil.popupwidget:toast() */
1270 position: absolute;
1271 display: block;
@@ -1577,16 +1582,26 @@
1577 align-items: stretch;
1578 padding: 0.25em;
1579 z-index: 200;
1580 }
1581 body.chat .chat-settings-popup > span {
1582 margin: 0.25em 0.2em;
1583 padding: 0.5em;
 
1584 white-space: nowrap;
1585 cursor: pointer;
1586 border: 1px outset;
1587 border-radius: 0.25em;
 
 
 
 
 
 
 
 
 
1588 }
1589 body.chat #chat-messages-wrapper {
1590 display: flex;
1591 flex-direction: column;
1592 }
1593
--- src/default.css
+++ src/default.css
@@ -1261,11 +1261,16 @@
1261 z-index: 19/*below default skin's hamburger popup*/;
1262 box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
1263 background-color: inherit;
1264 color: inherit;
1265 }
1266 .fossil-PopupWidget {
1267 /* This class is ALWAYS set on every fossil.PopupWidget instance, in
1268 addition to client/app-configured classes. It should not get any
1269 style - it is only used for DOM element selecting/filtering
1270 purposes. */
1271 }
1272 .fossil-toast-message {
1273 /* "toast"-style popup message.
1274 See fossil.popupwidget:toast() */
1275 position: absolute;
1276 display: block;
@@ -1577,16 +1582,26 @@
1582 align-items: stretch;
1583 padding: 0.25em;
1584 z-index: 200;
1585 }
1586 body.chat .chat-settings-popup > span {
1587 vertical-align: middle;
1588 }
1589 body.chat .chat-settings-popup > span.menu-entry{
1590 white-space: nowrap;
1591 cursor: pointer;
1592 border: 1px outset;
1593 border-radius: 0.25em;
1594 margin: 0.25em 0.2em;
1595 padding: 0.5em;
1596 }
1597 body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
1598 vertical-align: middle;
1599 }
1600 body.chat .chat-settings-popup > span.menu-entry > span.button {
1601 margin: 0.25em 0.2em;
1602 padding: 0.5em;
1603 }
1604 body.chat #chat-messages-wrapper {
1605 display: flex;
1606 flex-direction: column;
1607 }
1608
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -49,11 +49,14 @@
4949
All callback options are called with the PopupWidget object as
5050
their "this".
5151
5252
5353
.cssClass: optional CSS class, or list of classes, to apply to
54
- the new element.
54
+ the new element. In addition to any supplied here (or inherited
55
+ from the default), the class "fossil-PopupWidget" is always set
56
+ in order to allow certain app-internal CSS to account for popup
57
+ windows in special cases.
5558
5659
.style: optional object of properties to copy directly into
5760
the element's style object.
5861
5962
The options passed to this constructor get normalized into a
@@ -84,11 +87,12 @@
8487
8588
*/
8689
F.PopupWidget = function f(opt){
8790
opt = F.mergeLastWins(f.defaultOptions,opt);
8891
this.options = opt;
89
- const e = this.e = D.addClass(D.div(), opt.cssClass);
92
+ const e = this.e = D.addClass(D.div(), opt.cssClass,
93
+ "fossil-PopupWidget");
9094
this.show(false);
9195
if(opt.style){
9296
let k;
9397
for(k in opt.style){
9498
if(opt.style.hasOwnProperty(k)) e.style[k] = opt.style[k];
@@ -330,10 +334,11 @@
330334
331335
*/
332336
setup: function f(){
333337
if(!f.hasOwnProperty('clickHandler')){
334338
f.clickHandler = function fch(ev){
339
+ ev.preventDefault();
335340
if(!fch.popup){
336341
fch.popup = new F.PopupWidget({
337342
cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
338343
refresh: function(){
339344
}
@@ -340,22 +345,39 @@
340345
});
341346
fch.popup.e.style.maxWidth = '80%'/*of body*/;
342347
fch.popup.installClickToHide();
343348
}
344349
D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
345
- var popupRect = ev.target.getClientRects()[0];
346
- var x = popupRect.left, y = popupRect.top;
347
- if(x<0) x = 0;
348
- if(y<0) y = 0;
349350
/* Shift the help around a bit to "better" fit the
350351
screen. However, fch.popup.e.getClientRects() is empty
351352
until the popup is shown, so we have to show it,
352353
calculate the resulting size, then move and/or resize it.
353354
354355
This algorithm/these heuristics can certainly be improved
355356
upon.
356357
*/
358
+ var popupRect, rectElem = ev.target;
359
+ while(rectElem){
360
+ popupRect = rectElem.getClientRects()[0]/*undefined if off-screen!*/;
361
+ if(popupRect) break;
362
+ rectElem = rectElem.parentNode;
363
+ }
364
+ if(!popupRect) popupRect = {x:0, y:0, left:0, right:0};
365
+ var x = popupRect.left, y = popupRect.top;
366
+ if(x<0) x = 0;
367
+ if(y<0) y = 0;
368
+ if(rectElem){
369
+ /* Try to ensure that the popup's z-level is higher than this element's */
370
+ const rz = window.getComputedStyle(rectElem).zIndex;
371
+ var myZ;
372
+ if(rz && !isNaN(+rz)){
373
+ myZ = +rz + 1;
374
+ }else{
375
+ myZ = 10000/*guess!*/;
376
+ }
377
+ fch.popup.e.style.zIndex = myZ;
378
+ }
357379
fch.popup.show(x, y);
358380
x = popupRect.left, y = popupRect.top;
359381
popupRect = fch.popup.e.getBoundingClientRect();
360382
const rectBody = document.body.getClientRects()[0];
361383
if(popupRect.right > rectBody.right){
362384
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -49,11 +49,14 @@
49 All callback options are called with the PopupWidget object as
50 their "this".
51
52
53 .cssClass: optional CSS class, or list of classes, to apply to
54 the new element.
 
 
 
55
56 .style: optional object of properties to copy directly into
57 the element's style object.
58
59 The options passed to this constructor get normalized into a
@@ -84,11 +87,12 @@
84
85 */
86 F.PopupWidget = function f(opt){
87 opt = F.mergeLastWins(f.defaultOptions,opt);
88 this.options = opt;
89 const e = this.e = D.addClass(D.div(), opt.cssClass);
 
90 this.show(false);
91 if(opt.style){
92 let k;
93 for(k in opt.style){
94 if(opt.style.hasOwnProperty(k)) e.style[k] = opt.style[k];
@@ -330,10 +334,11 @@
330
331 */
332 setup: function f(){
333 if(!f.hasOwnProperty('clickHandler')){
334 f.clickHandler = function fch(ev){
 
335 if(!fch.popup){
336 fch.popup = new F.PopupWidget({
337 cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
338 refresh: function(){
339 }
@@ -340,22 +345,39 @@
340 });
341 fch.popup.e.style.maxWidth = '80%'/*of body*/;
342 fch.popup.installClickToHide();
343 }
344 D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
345 var popupRect = ev.target.getClientRects()[0];
346 var x = popupRect.left, y = popupRect.top;
347 if(x<0) x = 0;
348 if(y<0) y = 0;
349 /* Shift the help around a bit to "better" fit the
350 screen. However, fch.popup.e.getClientRects() is empty
351 until the popup is shown, so we have to show it,
352 calculate the resulting size, then move and/or resize it.
353
354 This algorithm/these heuristics can certainly be improved
355 upon.
356 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357 fch.popup.show(x, y);
358 x = popupRect.left, y = popupRect.top;
359 popupRect = fch.popup.e.getBoundingClientRect();
360 const rectBody = document.body.getClientRects()[0];
361 if(popupRect.right > rectBody.right){
362
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -49,11 +49,14 @@
49 All callback options are called with the PopupWidget object as
50 their "this".
51
52
53 .cssClass: optional CSS class, or list of classes, to apply to
54 the new element. In addition to any supplied here (or inherited
55 from the default), the class "fossil-PopupWidget" is always set
56 in order to allow certain app-internal CSS to account for popup
57 windows in special cases.
58
59 .style: optional object of properties to copy directly into
60 the element's style object.
61
62 The options passed to this constructor get normalized into a
@@ -84,11 +87,12 @@
87
88 */
89 F.PopupWidget = function f(opt){
90 opt = F.mergeLastWins(f.defaultOptions,opt);
91 this.options = opt;
92 const e = this.e = D.addClass(D.div(), opt.cssClass,
93 "fossil-PopupWidget");
94 this.show(false);
95 if(opt.style){
96 let k;
97 for(k in opt.style){
98 if(opt.style.hasOwnProperty(k)) e.style[k] = opt.style[k];
@@ -330,10 +334,11 @@
334
335 */
336 setup: function f(){
337 if(!f.hasOwnProperty('clickHandler')){
338 f.clickHandler = function fch(ev){
339 ev.preventDefault();
340 if(!fch.popup){
341 fch.popup = new F.PopupWidget({
342 cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
343 refresh: function(){
344 }
@@ -340,22 +345,39 @@
345 });
346 fch.popup.e.style.maxWidth = '80%'/*of body*/;
347 fch.popup.installClickToHide();
348 }
349 D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
 
 
 
 
350 /* Shift the help around a bit to "better" fit the
351 screen. However, fch.popup.e.getClientRects() is empty
352 until the popup is shown, so we have to show it,
353 calculate the resulting size, then move and/or resize it.
354
355 This algorithm/these heuristics can certainly be improved
356 upon.
357 */
358 var popupRect, rectElem = ev.target;
359 while(rectElem){
360 popupRect = rectElem.getClientRects()[0]/*undefined if off-screen!*/;
361 if(popupRect) break;
362 rectElem = rectElem.parentNode;
363 }
364 if(!popupRect) popupRect = {x:0, y:0, left:0, right:0};
365 var x = popupRect.left, y = popupRect.top;
366 if(x<0) x = 0;
367 if(y<0) y = 0;
368 if(rectElem){
369 /* Try to ensure that the popup's z-level is higher than this element's */
370 const rz = window.getComputedStyle(rectElem).zIndex;
371 var myZ;
372 if(rz && !isNaN(+rz)){
373 myZ = +rz + 1;
374 }else{
375 myZ = 10000/*guess!*/;
376 }
377 fch.popup.e.style.zIndex = myZ;
378 }
379 fch.popup.show(x, y);
380 x = popupRect.left, y = popupRect.top;
381 popupRect = fch.popup.e.getBoundingClientRect();
382 const rectBody = document.body.getClientRects()[0];
383 if(popupRect.right > rectBody.right){
384
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -65,16 +65,16 @@
6565
6666
Sidebar: it might seem odd to provide a key prefix and stick all
6767
properties in the topmost level of the storage object. We do that
6868
because adding a layer of object to sandbox each repo would mean
6969
(de)serializing that whole tree on every storage property change
70
- (and we update storage often during editing
71
- sessions). e.g. instead of storageObject.projectName.foo we have
70
+ (and we update storage often during editing sessions).
71
+ e.g. instead of storageObject.projectName.foo we have
7272
storageObject[storageKeyPrefix+'foo']. That's soley for
7373
efficiency's sake (in terms of battery life and
74
- environment-internal storage-level effort). Even so, it might
75
- (or might not) be useful to do that someday.
74
+ environment-internal storage-level effort). Even so, it might (or
75
+ might not) be useful to do that someday.
7676
*/
7777
const storageKeyPrefix = (
7878
$storageHolder===$storage/*localStorage or sessionStorage*/
7979
? (
8080
F.config.projectCode || F.config.projectName
@@ -101,10 +101,17 @@
101101
/** Returns the value for the given storage key, or
102102
dflt if the key is not found in the storage. */
103103
get: (k,dflt)=>$storageHolder.hasOwnProperty(
104104
storageKeyPrefix+k
105105
) ? $storage.getItem(storageKeyPrefix+k) : dflt,
106
+ /** Returns true if the given key has a value of "true". If the
107
+ key is not found, it returns true if the boolean value of dflt
108
+ is "true". (Remember that JS persistent storage values are all
109
+ strings.) */
110
+ getBool: function(k,dflt){
111
+ return 'true'===this.get(k,''+(!!dflt));
112
+ },
106113
/** Returns the JSON.parse()'d value of the given
107114
storage key's value, or dflt is the key is not
108115
found or JSON.parse() fails. */
109116
getJSON: function f(k,dflt){
110117
try {
111118
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -65,16 +65,16 @@
65
66 Sidebar: it might seem odd to provide a key prefix and stick all
67 properties in the topmost level of the storage object. We do that
68 because adding a layer of object to sandbox each repo would mean
69 (de)serializing that whole tree on every storage property change
70 (and we update storage often during editing
71 sessions). e.g. instead of storageObject.projectName.foo we have
72 storageObject[storageKeyPrefix+'foo']. That's soley for
73 efficiency's sake (in terms of battery life and
74 environment-internal storage-level effort). Even so, it might
75 (or might not) be useful to do that someday.
76 */
77 const storageKeyPrefix = (
78 $storageHolder===$storage/*localStorage or sessionStorage*/
79 ? (
80 F.config.projectCode || F.config.projectName
@@ -101,10 +101,17 @@
101 /** Returns the value for the given storage key, or
102 dflt if the key is not found in the storage. */
103 get: (k,dflt)=>$storageHolder.hasOwnProperty(
104 storageKeyPrefix+k
105 ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
 
 
 
 
 
 
 
106 /** Returns the JSON.parse()'d value of the given
107 storage key's value, or dflt is the key is not
108 found or JSON.parse() fails. */
109 getJSON: function f(k,dflt){
110 try {
111
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -65,16 +65,16 @@
65
66 Sidebar: it might seem odd to provide a key prefix and stick all
67 properties in the topmost level of the storage object. We do that
68 because adding a layer of object to sandbox each repo would mean
69 (de)serializing that whole tree on every storage property change
70 (and we update storage often during editing sessions).
71 e.g. instead of storageObject.projectName.foo we have
72 storageObject[storageKeyPrefix+'foo']. That's soley for
73 efficiency's sake (in terms of battery life and
74 environment-internal storage-level effort). Even so, it might (or
75 might not) be useful to do that someday.
76 */
77 const storageKeyPrefix = (
78 $storageHolder===$storage/*localStorage or sessionStorage*/
79 ? (
80 F.config.projectCode || F.config.projectName
@@ -101,10 +101,17 @@
101 /** Returns the value for the given storage key, or
102 dflt if the key is not found in the storage. */
103 get: (k,dflt)=>$storageHolder.hasOwnProperty(
104 storageKeyPrefix+k
105 ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
106 /** Returns true if the given key has a value of "true". If the
107 key is not found, it returns true if the boolean value of dflt
108 is "true". (Remember that JS persistent storage values are all
109 strings.) */
110 getBool: function(k,dflt){
111 return 'true'===this.get(k,''+(!!dflt));
112 },
113 /** Returns the JSON.parse()'d value of the given
114 storage key's value, or dflt is the key is not
115 found or JSON.parse() fails. */
116 getJSON: function f(k,dflt){
117 try {
118

Keyboard Shortcuts

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