Fossil SCM

Added an optional widget to /chat which gives an overview of who is actively posting and enables filtering messages by users.

stephan 2021-09-24 17:18 trunk merge
Commit ce0d61bbae80a04d73df49695966304aa5a7cc82083d733c1764b37879f31d82
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -237,11 +237,11 @@
237237
div.footer div {
238238
background-color: #222;
239239
box-shadow: 3px 3px 1px #000;
240240
border-radius: 0 0 1rem 1rem;
241241
margin: 0 0 10px 0;
242
- padding: 0.5rem 0.75rem;
242
+ padding: 0.25rem 0.75rem;
243243
}
244244
245245
div.footer div.page-time {
246246
float: left;
247247
}
@@ -1157,14 +1157,13 @@
11571157
body.chat div.header, body.chat div.footer,
11581158
body.chat div.mainmenu, body.chat div.submenu,
11591159
body.chat div.content {
11601160
margin-left: auto;
11611161
margin-right: auto;
1162
+ margin-top: auto/*eliminates unnecessary scrollbars*/;
11621163
}
11631164
body.chat.chat-only-mode div.content {
11641165
max-width: revert;
11651166
}
1166
-
1167
-body.chat .message-widget .message-widget-tab {
1168
- /* Make /chat user names and timestamps more visible */
1169
- filter: saturate(6);
1167
+body.chat #chat-user-list .chat-user{
1168
+ color: white;
11701169
}
11711170
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -237,11 +237,11 @@
237 div.footer div {
238 background-color: #222;
239 box-shadow: 3px 3px 1px #000;
240 border-radius: 0 0 1rem 1rem;
241 margin: 0 0 10px 0;
242 padding: 0.5rem 0.75rem;
243 }
244
245 div.footer div.page-time {
246 float: left;
247 }
@@ -1157,14 +1157,13 @@
1157 body.chat div.header, body.chat div.footer,
1158 body.chat div.mainmenu, body.chat div.submenu,
1159 body.chat div.content {
1160 margin-left: auto;
1161 margin-right: auto;
 
1162 }
1163 body.chat.chat-only-mode div.content {
1164 max-width: revert;
1165 }
1166
1167 body.chat .message-widget .message-widget-tab {
1168 /* Make /chat user names and timestamps more visible */
1169 filter: saturate(6);
1170 }
1171
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -237,11 +237,11 @@
237 div.footer div {
238 background-color: #222;
239 box-shadow: 3px 3px 1px #000;
240 border-radius: 0 0 1rem 1rem;
241 margin: 0 0 10px 0;
242 padding: 0.25rem 0.75rem;
243 }
244
245 div.footer div.page-time {
246 float: left;
247 }
@@ -1157,14 +1157,13 @@
1157 body.chat div.header, body.chat div.footer,
1158 body.chat div.mainmenu, body.chat div.submenu,
1159 body.chat div.content {
1160 margin-left: auto;
1161 margin-right: auto;
1162 margin-top: auto/*eliminates unnecessary scrollbars*/;
1163 }
1164 body.chat.chat-only-mode div.content {
1165 max-width: revert;
1166 }
1167 body.chat #chat-user-list .chat-user{
1168 color: white;
 
 
1169 }
1170
+13
--- src/chat.c
+++ src/chat.c
@@ -180,10 +180,23 @@
180180
@ </div>
181181
@ <input type="file" name="file" id="chat-input-file">
182182
@ </div>
183183
@ <div id="chat-drop-details"></div>
184184
@ </div>
185
+ @ </div>
186
+ @ <div id='chat-user-list-wrapper' class='hidden'>
187
+ @ <div class='legend'>
188
+ @ <span class='help-buttonlet'>
189
+ @ Users who have messages in the currently-loaded list.<br><br>
190
+ @ <strong>Tap a user name</strong> to filter messages
191
+ @ on that user and tap again to clear the filter.<br><br>
192
+ @ <strong>Tap the title</strong> of this widget to toggle
193
+ @ the list on and off.
194
+ @ </span>
195
+ @ <span>Active users (sorted by last message time)</span>
196
+ @ </div>
197
+ @ <div id='chat-user-list'></div>
185198
@ </div>
186199
@ <div id='chat-preview' class='hidden chat-view'>
187200
@ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
188201
@ <div id='chat-preview-content' class='message-widget-content'></div>
189202
@ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
190203
--- src/chat.c
+++ src/chat.c
@@ -180,10 +180,23 @@
180 @ </div>
181 @ <input type="file" name="file" id="chat-input-file">
182 @ </div>
183 @ <div id="chat-drop-details"></div>
184 @ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
185 @ </div>
186 @ <div id='chat-preview' class='hidden chat-view'>
187 @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
188 @ <div id='chat-preview-content' class='message-widget-content'></div>
189 @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
190
--- src/chat.c
+++ src/chat.c
@@ -180,10 +180,23 @@
180 @ </div>
181 @ <input type="file" name="file" id="chat-input-file">
182 @ </div>
183 @ <div id="chat-drop-details"></div>
184 @ </div>
185 @ </div>
186 @ <div id='chat-user-list-wrapper' class='hidden'>
187 @ <div class='legend'>
188 @ <span class='help-buttonlet'>
189 @ Users who have messages in the currently-loaded list.<br><br>
190 @ <strong>Tap a user name</strong> to filter messages
191 @ on that user and tap again to clear the filter.<br><br>
192 @ <strong>Tap the title</strong> of this widget to toggle
193 @ the list on and off.
194 @ </span>
195 @ <span>Active users (sorted by last message time)</span>
196 @ </div>
197 @ <div id='chat-user-list'></div>
198 @ </div>
199 @ <div id='chat-preview' class='hidden chat-view'>
200 @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header>
201 @ <div id='chat-preview-content' class='message-widget-content'></div>
202 @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
203
+293 -37
--- src/chat.js
+++ src/chat.js
@@ -34,10 +34,30 @@
3434
else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true;
3535
return false;
3636
};
3737
3838
const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank');
39
+
40
+ /**
41
+ Returns an almost-ISO8601 form of Date object d.
42
+ */
43
+ const iso8601ish = function(d){
44
+ return d.toISOString()
45
+ .replace('T',' ').replace(/\.\d+/,'')
46
+ .replace('Z', ' zulu');
47
+ };
48
+ /** Returns the local time string of Date object d, defaulting
49
+ to the current time. */
50
+ const localTimeString = function ff(d){
51
+ d || (d = new Date());
52
+ return [
53
+ d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/),
54
+ '-',pad2(d.getDate()),
55
+ ' ',pad2(d.getHours()),':',pad2(d.getMinutes()),
56
+ ':',pad2(d.getSeconds())
57
+ ].join('');
58
+ };
3959
4060
(function(){
4161
let dbg = document.querySelector('#debugMsg');
4262
if(dbg){
4363
/* This can inadvertently influence our flexbox layouts, so move
@@ -116,11 +136,13 @@
116136
contentDiv: E1('div.content'),
117137
viewConfig: E1('#chat-config'),
118138
viewPreview: E1('#chat-preview'),
119139
previewContent: E1('#chat-preview-content'),
120140
btnPreview: E1('#chat-preview-button'),
121
- views: document.querySelectorAll('.chat-view')
141
+ views: document.querySelectorAll('.chat-view'),
142
+ activeUserListWrapper: E1('#chat-user-list-wrapper'),
143
+ activeUserList: E1('#chat-user-list')
122144
},
123145
me: F.user.name,
124146
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
125147
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
126148
pageIsActive: 'visible'===document.visibilityState,
@@ -128,10 +150,23 @@
128150
notificationBubbleColor: 'white',
129151
totalMessageCount: 0, // total # of inbound messages
130152
//! Number of messages to load for the history buttons
131153
loadMessageCount: Math.abs(F.config.chat.initSize || 20),
132154
ajaxInflight: 0,
155
+ usersLastSeen:{
156
+ /* Map of user names to their most recent message time
157
+ (JS Date object). Only messages received by the chat client
158
+ are considered. */
159
+ /* Reminder: to convert a Julian time J to JS:
160
+ new Date((J - 2440587.5) * 86400000) */
161
+ },
162
+ filterState:{
163
+ activeUser: undefined,
164
+ match: function(uname){
165
+ return this.activeUser===uname || !this.activeUser;
166
+ }
167
+ },
133168
/** Gets (no args) or sets (1 arg) the current input text field value,
134169
taking into account single- vs multi-line input. The getter returns
135170
a string and the setter returns this object. */
136171
inputValue: function(){
137172
const e = this.inputElement();
@@ -167,10 +202,11 @@
167202
D.removeClass(this.e.inputCurrent, 'hidden');
168203
const mh2 = m.clientHeight;
169204
m.scrollTo(0, sTop + (mh1-mh2));
170205
this.e.inputCurrent.value = old.value;
171206
old.value = '';
207
+ this.animate(this.e.inputCurrent, "anim-flip-v");
172208
return this;
173209
},
174210
/**
175211
If passed true or no arguments, switches to multi-line mode
176212
if currently in single-line mode. If passed false, switches
@@ -244,10 +280,13 @@
244280
the list. */
245281
injectMessageElem: function f(e, atEnd){
246282
const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
247283
holder = this.e.viewMessages,
248284
prevMessage = this.e.newestMessage;
285
+ if(!this.filterState.match(e.dataset.xfrom)){
286
+ e.classList.add('hidden');
287
+ }
249288
if(atEnd){
250289
const fe = mip.nextElementSibling;
251290
if(fe) mip.parentNode.insertBefore(e, fe);
252291
else D.append(mip.parentNode, e);
253292
}else{
@@ -367,11 +406,13 @@
367406
defaults:{
368407
"images-inline": !!F.config.chat.imagesInline,
369408
"edit-multiline": false,
370409
"monospace-messages": false,
371410
"chat-only-mode": false,
372
- "audible-alert": true
411
+ "audible-alert": true,
412
+ "active-user-list": false,
413
+ "active-user-list-timestamps": false
373414
}
374415
},
375416
/** Plays a new-message notification sound IF the audible-alert
376417
setting is true, else this is a no-op. Returns this.
377418
*/
@@ -403,16 +444,110 @@
403444
Expects e to be one of the elements in this.e.views.
404445
The 'hidden' class is removed from e and added to
405446
all other elements in that list. Returns e.
406447
*/
407448
setCurrentView: function(e){
449
+ if(e===this.e.currentView){
450
+ return e;
451
+ }
408452
this.e.views.forEach(function(E){
409453
if(e!==E) D.addClass(E,'hidden');
410454
});
411
- return this.e.currentView = D.removeClass(e,'hidden');
455
+ this.e.currentView = D.removeClass(e,'hidden');
456
+ this.animate(this.e.currentView, 'anim-fade-in-fast');
457
+ return this.e.currentView;
458
+ },
459
+ /**
460
+ Updates the "active user list" view if we are not currently
461
+ batch-loading messages and if the active user list UI element
462
+ is active.
463
+ */
464
+ updateActiveUserList: function callee(){
465
+ if(this._isBatchLoading
466
+ || this.e.activeUserListWrapper.classList.contains('hidden')){
467
+ return this;
468
+ }else if(!callee.sortUsersSeen){
469
+ /** Array.sort() callback. Expects an array of user names and
470
+ sorts them in last-received message order (newest first). */
471
+ const self = this;
472
+ callee.sortUsersSeen = function(l,r){
473
+ l = self.usersLastSeen[l];
474
+ r = self.usersLastSeen[r];
475
+ if(l && r) return r - l;
476
+ else if(l) return -1;
477
+ else if(r) return 1;
478
+ else return 0;
479
+ };
480
+ callee.addUserElem = function(u){
481
+ const uSpan = D.addClass(D.span(), 'chat-user');
482
+ const uDate = self.usersLastSeen[u];
483
+ if(self.filterState.activeUser===u){
484
+ uSpan.classList.add('selected');
485
+ }
486
+ uSpan.dataset.uname = u;
487
+ D.append(uSpan, u, "\n",
488
+ D.append(
489
+ D.addClass(D.span(),'timestamp'),
490
+ localTimeString(uDate)//.substr(5/*chop off year*/)
491
+ ));
492
+ if(uDate.$uColor){
493
+ uSpan.style.backgroundColor = uDate.$uColor;
494
+ }
495
+ D.append(self.e.activeUserList, uSpan);
496
+ };
497
+ }
498
+ //D.clearElement(this.e.activeUserList);
499
+ D.remove(this.e.activeUserList.querySelectorAll('.chat-user'));
500
+ Object.keys(this.usersLastSeen).sort(
501
+ callee.sortUsersSeen
502
+ ).forEach(callee.addUserElem);
503
+ return this;
504
+ },
505
+ /**
506
+ Applies user name filter to all current messages, or clears
507
+ the filter if uname is falsy.
508
+ */
509
+ setUserFilter: function(uname){
510
+ this.filterState.activeUser = uname;
511
+ const mw = this.e.viewMessages.querySelectorAll('.message-widget');
512
+ const self = this;
513
+ let eLast;
514
+ if(!uname){
515
+ D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'),
516
+ 'hidden');
517
+ }else{
518
+ mw.forEach(function(w){
519
+ if(self.filterState.match(w.dataset.xfrom)){
520
+ w.classList.remove('hidden');
521
+ eLast = w;
522
+ }else{
523
+ w.classList.add('hidden');
524
+ }
525
+ });
526
+ }
527
+ if(eLast) eLast.scrollIntoView(false);
528
+ else this.scrollMessagesTo(1);
529
+ cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
530
+ e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected');
531
+ });
532
+ return this;
533
+ },
534
+
535
+ /**
536
+ If animations are enabled, passes its arguments
537
+ to D.addClassBriefly(), else this is a no-op.
538
+ If cb is a function, it is called after the
539
+ CSS class is removed. Returns this object;
540
+ */
541
+ animate: function f(e,a,cb){
542
+ if(!f.$disabled){
543
+ D.addClassBriefly(e, a, 0, cb);
544
+ }
545
+ return this;
412546
}
413547
};
548
+ cs.animate.$disabled = true;
414549
F.fetch.beforesend = ()=>cs.ajaxStart();
415550
F.fetch.aftersend = ()=>cs.ajaxEnd();
416551
cs.e.inputCurrent = cs.e.inputSingle;
417552
/* Install default settings... */
418553
Object.keys(cs.settings.defaults).forEach(function(k){
@@ -427,10 +562,16 @@
427562
tall vs wide. Can be toggled via settings popup. */
428563
document.body.classList.add('my-messages-right');
429564
}
430565
if(cs.settings.getBool('monospace-messages',false)){
431566
document.body.classList.add('monospace-messages');
567
+ }
568
+ if(cs.settings.getBool('active-user-list',false)){
569
+ cs.e.activeUserListWrapper.classList.remove('hidden');
570
+ }
571
+ if(cs.settings.getBool('active-user-list-timestamps',false)){
572
+ cs.e.activeUserList.classList.add('timestamps');
432573
}
433574
cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
434575
cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
435576
cs.pageTitleOrig = cs.e.pageTitle.innerText;
436577
const qs = (e)=>document.querySelector(e);
@@ -624,10 +765,36 @@
624765
if(cs.pageIsActive){
625766
cs.e.pageTitle.innerText = cs.pageTitleOrig;
626767
}
627768
}, true);
628769
cs.setCurrentView(cs.e.viewMessages);
770
+
771
+ cs.e.activeUserList.addEventListener('click', function f(ev){
772
+ /* Filter messages on a user clicked in activeUserList */
773
+ ev.stopPropagation();
774
+ ev.preventDefault();
775
+ let eUser = ev.target;
776
+ while(eUser!==this && !eUser.classList.contains('chat-user')){
777
+ eUser = eUser.parentNode;
778
+ }
779
+ if(eUser==this || !eUser) return false;
780
+ const uname = eUser.dataset.uname;
781
+ let eLast;
782
+ cs.setCurrentView(cs.e.viewMessages);
783
+ if(eUser.classList.contains('selected')){
784
+ /* If curently selected, toggle filter off */
785
+ eUser.classList.remove('selected');
786
+ cs.setUserFilter(false);
787
+ delete f.$eSelected;
788
+ }else{
789
+ if(f.$eSelected) f.$eSelected.classList.remove('selected');
790
+ f.$eSelected = eUser;
791
+ eUser.classList.add('selected');
792
+ cs.setUserFilter(uname);
793
+ }
794
+ return false;
795
+ }, false);
629796
return cs;
630797
})()/*Chat initialization*/;
631798
632799
/**
633800
Custom widget type for rendering messages (one message per
@@ -671,21 +838,10 @@
671838
d.getHours(),":",
672839
(d.getMinutes()+100).toString().slice(1,3),
673840
' ', dowMap[d.getDay()]
674841
].join('');
675842
};
676
- /** Returns the local time string of Date object d, defaulting
677
- to the current time. */
678
- const localTimeString = function ff(d){
679
- d || (d = new Date());
680
- return [
681
- d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/),
682
- '-',pad2(d.getDate()),
683
- ' ',pad2(d.getHours()),':',pad2(d.getMinutes()),
684
- ':',pad2(d.getSeconds())
685
- ].join('');
686
- };
687843
cf.prototype = {
688844
scrollIntoView: function(){
689845
this.e.content.scrollIntoView();
690846
},
691847
setMessage: function(m){
@@ -771,14 +927,14 @@
771927
eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
772928
}*/
773929
return this;
774930
},
775931
/* Event handler for clicking .message-user elements to show their
776
- timestamps. */
932
+ timestamps and a set of actions. */
777933
_handleLegendClicked: function f(ev){
778934
if(!f.popup){
779
- /* Timestamp popup widget */
935
+ /* "Popup" widget */
780936
f.popup = {
781937
e: D.addClass(D.div(), 'chat-message-popup'),
782938
refresh:function(){
783939
const eMsg = this.$eMsg/*.message-widget element*/;
784940
if(!eMsg) return;
@@ -843,18 +999,45 @@
843999
y: 'a'
8441000
}), "User's Timeline"),
8451001
'target', '_blank'
8461002
);
8471003
D.append(toolbar2, timelineLink);
1004
+ if(Chat.filterState.activeUser &&
1005
+ Chat.filterState.match(eMsg.dataset.xfrom)){
1006
+ /* Add a button to clear user filter and jump to
1007
+ this message in its original context. */
1008
+ D.append(
1009
+ this.e,
1010
+ D.append(
1011
+ D.addClass(D.div(), 'toolbar'),
1012
+ D.button(
1013
+ "Message in context",
1014
+ function(){
1015
+ self.hide();
1016
+ Chat.setUserFilter(false);
1017
+ eMsg.scrollIntoView(false);
1018
+ Chat.animate(
1019
+ eMsg.firstElementChild, 'anim-flip-h'
1020
+ //eMsg.firstElementChild, 'anim-flip-v'
1021
+ //eMsg.childNodes, 'anim-rotate-360'
1022
+ //eMsg.childNodes, 'anim-flip-v'
1023
+ //eMsg, 'anim-flip-v'
1024
+ );
1025
+ })
1026
+ )
1027
+ );
1028
+ }/*jump-to button*/
8481029
}
8491030
const tab = eMsg.querySelector('.message-widget-tab');
8501031
D.append(tab, this.e);
8511032
D.removeClass(this.e, 'hidden');
1033
+ Chat.animate(this.e, 'anim-fade-in-fast');
8521034
}/*refresh()*/,
8531035
hide: function(){
854
- D.addClass(D.clearElement(this.e), 'hidden');
8551036
delete this.$eMsg;
1037
+ D.addClass(this.e, 'hidden');
1038
+ D.clearElement(this.e);
8561039
},
8571040
show: function(tgtMsg){
8581041
if(tgtMsg === this.$eMsg){
8591042
this.hide();
8601043
return;
@@ -1034,16 +1217,10 @@
10341217
e.preventDefault();
10351218
Chat.submitMessage();
10361219
return false;
10371220
});
10381221
1039
- /* Returns an almost-ISO8601 form of Date object d. */
1040
- const iso8601ish = function(d){
1041
- return d.toISOString()
1042
- .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu');
1043
- };
1044
-
10451222
(function(){/*Set up #chat-settings-button */
10461223
const settingsButton = document.querySelector('#chat-settings-button');
10471224
const optionsMenu = E1('#chat-config-options');
10481225
const cbToggle = function(ev){
10491226
ev.preventDefault();
@@ -1052,37 +1229,95 @@
10521229
? Chat.e.viewMessages : Chat.e.viewConfig);
10531230
return false;
10541231
};
10551232
D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
10561233
Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
1057
- /* Settings menu entries... */
1234
+
1235
+ /** Internal acrobatics to allow certain settings toggles to access
1236
+ related toggles. */
1237
+ const namedOptions = {
1238
+ activeUsers:{
1239
+ label: "Show active users list",
1240
+ boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1241
+ persistentSetting: 'active-user-list',
1242
+ callback: function(){
1243
+ D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1244
+ D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1245
+ if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1246
+ /* When hiding this element, undo all filtering */
1247
+ Chat.setUserFilter(false);
1248
+ /*Ideally we'd scroll the final message into view
1249
+ now, but because viewMessages is currently hidden behind
1250
+ viewConfig, scrolling is a no-op. */
1251
+ Chat.scrollMessagesTo(1);
1252
+ }else{
1253
+ Chat.updateActiveUserList();
1254
+ Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
1255
+ }
1256
+ }
1257
+ }
1258
+ };
1259
+ if(1){
1260
+ /* Per user request, toggle the list of users on and off if the
1261
+ legend element is tapped. */
1262
+ const optAu = namedOptions.activeUsers;
1263
+ optAu.theLegend = Chat.e.activeUserListWrapper.firstElementChild/*LEGEND*/;
1264
+ optAu.theList = optAu.theLegend.nextElementSibling/*user list container*/;
1265
+ optAu.theLegend.addEventListener('click',function(){
1266
+ D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed');
1267
+ if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){
1268
+ Chat.animate(optAu.theList,'anim-flip-v');
1269
+ }
1270
+ }, false);
1271
+ }/*namedOptions.activeUsers additional setup*/
1272
+ /* Settings menu entries... Remember that they will be rendered in
1273
+ reverse order and the most frequently-needed ones "should"
1274
+ (arguably) be closer to the start of this list so that they
1275
+ will be rendered within easier reach of the settings button. */
10581276
const settingsOps = [{
10591277
label: "Multi-line input",
10601278
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
10611279
persistentSetting: 'edit-multiline',
10621280
callback: function(){
10631281
Chat.inputToggleSingleMulti();
10641282
}
10651283
},{
1066
- label: "Monospace message font",
1067
- boolValue: ()=>document.body.classList.contains('monospace-messages'),
1068
- persistentSetting: 'monospace-messages',
1069
- callback: function(){
1070
- document.body.classList.toggle('monospace-messages');
1284
+ label: "Left-align my posts",
1285
+ boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1286
+ callback: function f(){
1287
+ document.body.classList.toggle('my-messages-right');
10711288
}
10721289
},{
1073
- label: "Images inline",
1290
+ label: "Show images inline",
10741291
boolValue: ()=>Chat.settings.getBool('images-inline'),
10751292
callback: function(){
10761293
const v = Chat.settings.toggle('images-inline');
10771294
F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
10781295
}
10791296
},{
1080
- label: "Left-align my posts",
1081
- boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1082
- callback: function f(){
1083
- document.body.classList.toggle('my-messages-right');
1297
+ label: "Timestamps in active users list",
1298
+ boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1299
+ persistentSetting: 'active-user-list-timestamps',
1300
+ callback: function(){
1301
+ D.toggleClass(Chat.e.activeUserList,'timestamps');
1302
+ /* If the timestamp option is activated but
1303
+ namedOptions.activeUsers is not currently checked then
1304
+ toggle that option on as well. */
1305
+ if(Chat.e.activeUserList.classList.contains('timestamps')
1306
+ && !namedOptions.activeUsers.boolValue()){
1307
+ namedOptions.activeUsers.checkbox.checked = true;
1308
+ namedOptions.activeUsers.callback();
1309
+ Chat.settings.set(namedOptions.activeUsers.persistentSetting, true);
1310
+ }
1311
+ }
1312
+ },
1313
+ namedOptions.activeUsers,{
1314
+ label: "Monospace message font",
1315
+ boolValue: ()=>document.body.classList.contains('monospace-messages'),
1316
+ persistentSetting: 'monospace-messages',
1317
+ callback: function(){
1318
+ document.body.classList.toggle('monospace-messages');
10841319
}
10851320
},{
10861321
label: "Chat-only mode",
10871322
boolValue: ()=>Chat.isChatOnlyMode(),
10881323
persistentSetting: 'chat-only-mode',
@@ -1139,12 +1374,13 @@
11391374
D.append(line, btn, op.select);
11401375
op.select.addEventListener('change', callback, false);
11411376
}else if(op.hasOwnProperty('boolValue')){
11421377
if(undefined === f.$id) f.$id = 0;
11431378
++f.$id;
1144
- const check = D.attr(D.checkbox(1, op.boolValue()),
1145
- 'aria-label', op.label);
1379
+ const check = op.checkbox
1380
+ = D.attr(D.checkbox(1, op.boolValue()),
1381
+ 'aria-label', op.label);
11461382
const id = 'cfgopt'+f.$id;
11471383
if(op.boolValue()) check.checked = true;
11481384
D.attr(check, 'id', id);
11491385
D.attr(btn, 'for', id);
11501386
D.append(line, check);
@@ -1228,10 +1464,18 @@
12281464
should only be true when loading older messages. */
12291465
f.processPost = function(m,atEnd){
12301466
++Chat.totalMessageCount;
12311467
if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
12321468
if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
1469
+ if(m.xfrom && m.mtime){
1470
+ const d = new Date(m.mtime);
1471
+ const uls = Chat.usersLastSeen[m.xfrom];
1472
+ if(!uls || uls<d){
1473
+ d.$uColor = m.uclr;
1474
+ Chat.usersLastSeen[m.xfrom] = d;
1475
+ }
1476
+ }
12331477
if( m.mdel ){
12341478
/* A record deletion notice. */
12351479
Chat.deleteMessageElem(m.mdel);
12361480
return;
12371481
}
@@ -1244,10 +1488,11 @@
12441488
Chat._gotServerError = m;
12451489
}
12461490
}/*processPost()*/;
12471491
}/*end static init*/
12481492
jx.msgs.forEach((m)=>f.processPost(m,atEnd));
1493
+ Chat.updateActiveUserList();
12491494
if('visible'===document.visibilityState){
12501495
if(Chat.changesSincePageHidden){
12511496
Chat.changesSincePageHidden = 0;
12521497
Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
12531498
}
@@ -1288,10 +1533,11 @@
12881533
},
12891534
onload:function(x){
12901535
let gotMessages = x.msgs.length;
12911536
newcontent(x,true);
12921537
Chat._isBatchLoading = false;
1538
+ Chat.updateActiveUserList();
12931539
if(Chat._gotServerError){
12941540
Chat._gotServerError = false;
12951541
return;
12961542
}
12971543
if(n<0/*we asked for all history*/
@@ -1381,11 +1627,14 @@
13811627
resumed, and reportError() produces a loud error message. */
13821628
afterFetch();
13831629
},
13841630
onload:function(y){
13851631
newcontent(y);
1386
- Chat._isBatchLoading = false;
1632
+ if(Chat._isBatchLoading){
1633
+ Chat._isBatchLoading = false;
1634
+ Chat.updateActiveUserList();
1635
+ }
13871636
afterFetch();
13881637
}
13891638
});
13901639
};
13911640
poll.isFirstCall = true;
@@ -1392,8 +1641,15 @@
13921641
Chat._gotServerError = poll.running = false;
13931642
if( window.fossil.config.chat.fromcli ){
13941643
Chat.chatOnlyMode(true);
13951644
}
13961645
Chat.intervalTimer = setInterval(poll, 1000);
1646
+ if(0){
1647
+ const flip = (ev)=>Chat.animate(ev.target,'anim-flip-h');
1648
+ document.querySelectorAll('#chat-edit-buttons button').forEach(function(e){
1649
+ e.addEventListener('click',flip, false);
1650
+ });
1651
+ }
13971652
setTimeout( ()=>Chat.inputFocus(), 0 );
1653
+ Chat.animate.$disabled = false;
13981654
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
13991655
})();
14001656
--- src/chat.js
+++ src/chat.js
@@ -34,10 +34,30 @@
34 else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true;
35 return false;
36 };
37
38 const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
40 (function(){
41 let dbg = document.querySelector('#debugMsg');
42 if(dbg){
43 /* This can inadvertently influence our flexbox layouts, so move
@@ -116,11 +136,13 @@
116 contentDiv: E1('div.content'),
117 viewConfig: E1('#chat-config'),
118 viewPreview: E1('#chat-preview'),
119 previewContent: E1('#chat-preview-content'),
120 btnPreview: E1('#chat-preview-button'),
121 views: document.querySelectorAll('.chat-view')
 
 
122 },
123 me: F.user.name,
124 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
125 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
126 pageIsActive: 'visible'===document.visibilityState,
@@ -128,10 +150,23 @@
128 notificationBubbleColor: 'white',
129 totalMessageCount: 0, // total # of inbound messages
130 //! Number of messages to load for the history buttons
131 loadMessageCount: Math.abs(F.config.chat.initSize || 20),
132 ajaxInflight: 0,
 
 
 
 
 
 
 
 
 
 
 
 
 
133 /** Gets (no args) or sets (1 arg) the current input text field value,
134 taking into account single- vs multi-line input. The getter returns
135 a string and the setter returns this object. */
136 inputValue: function(){
137 const e = this.inputElement();
@@ -167,10 +202,11 @@
167 D.removeClass(this.e.inputCurrent, 'hidden');
168 const mh2 = m.clientHeight;
169 m.scrollTo(0, sTop + (mh1-mh2));
170 this.e.inputCurrent.value = old.value;
171 old.value = '';
 
172 return this;
173 },
174 /**
175 If passed true or no arguments, switches to multi-line mode
176 if currently in single-line mode. If passed false, switches
@@ -244,10 +280,13 @@
244 the list. */
245 injectMessageElem: function f(e, atEnd){
246 const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
247 holder = this.e.viewMessages,
248 prevMessage = this.e.newestMessage;
 
 
 
249 if(atEnd){
250 const fe = mip.nextElementSibling;
251 if(fe) mip.parentNode.insertBefore(e, fe);
252 else D.append(mip.parentNode, e);
253 }else{
@@ -367,11 +406,13 @@
367 defaults:{
368 "images-inline": !!F.config.chat.imagesInline,
369 "edit-multiline": false,
370 "monospace-messages": false,
371 "chat-only-mode": false,
372 "audible-alert": true
 
 
373 }
374 },
375 /** Plays a new-message notification sound IF the audible-alert
376 setting is true, else this is a no-op. Returns this.
377 */
@@ -403,16 +444,110 @@
403 Expects e to be one of the elements in this.e.views.
404 The 'hidden' class is removed from e and added to
405 all other elements in that list. Returns e.
406 */
407 setCurrentView: function(e){
 
 
 
408 this.e.views.forEach(function(E){
409 if(e!==E) D.addClass(E,'hidden');
410 });
411 return this.e.currentView = D.removeClass(e,'hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412 }
413 };
 
414 F.fetch.beforesend = ()=>cs.ajaxStart();
415 F.fetch.aftersend = ()=>cs.ajaxEnd();
416 cs.e.inputCurrent = cs.e.inputSingle;
417 /* Install default settings... */
418 Object.keys(cs.settings.defaults).forEach(function(k){
@@ -427,10 +562,16 @@
427 tall vs wide. Can be toggled via settings popup. */
428 document.body.classList.add('my-messages-right');
429 }
430 if(cs.settings.getBool('monospace-messages',false)){
431 document.body.classList.add('monospace-messages');
 
 
 
 
 
 
432 }
433 cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
434 cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
435 cs.pageTitleOrig = cs.e.pageTitle.innerText;
436 const qs = (e)=>document.querySelector(e);
@@ -624,10 +765,36 @@
624 if(cs.pageIsActive){
625 cs.e.pageTitle.innerText = cs.pageTitleOrig;
626 }
627 }, true);
628 cs.setCurrentView(cs.e.viewMessages);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629 return cs;
630 })()/*Chat initialization*/;
631
632 /**
633 Custom widget type for rendering messages (one message per
@@ -671,21 +838,10 @@
671 d.getHours(),":",
672 (d.getMinutes()+100).toString().slice(1,3),
673 ' ', dowMap[d.getDay()]
674 ].join('');
675 };
676 /** Returns the local time string of Date object d, defaulting
677 to the current time. */
678 const localTimeString = function ff(d){
679 d || (d = new Date());
680 return [
681 d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/),
682 '-',pad2(d.getDate()),
683 ' ',pad2(d.getHours()),':',pad2(d.getMinutes()),
684 ':',pad2(d.getSeconds())
685 ].join('');
686 };
687 cf.prototype = {
688 scrollIntoView: function(){
689 this.e.content.scrollIntoView();
690 },
691 setMessage: function(m){
@@ -771,14 +927,14 @@
771 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
772 }*/
773 return this;
774 },
775 /* Event handler for clicking .message-user elements to show their
776 timestamps. */
777 _handleLegendClicked: function f(ev){
778 if(!f.popup){
779 /* Timestamp popup widget */
780 f.popup = {
781 e: D.addClass(D.div(), 'chat-message-popup'),
782 refresh:function(){
783 const eMsg = this.$eMsg/*.message-widget element*/;
784 if(!eMsg) return;
@@ -843,18 +999,45 @@
843 y: 'a'
844 }), "User's Timeline"),
845 'target', '_blank'
846 );
847 D.append(toolbar2, timelineLink);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848 }
849 const tab = eMsg.querySelector('.message-widget-tab');
850 D.append(tab, this.e);
851 D.removeClass(this.e, 'hidden');
 
852 }/*refresh()*/,
853 hide: function(){
854 D.addClass(D.clearElement(this.e), 'hidden');
855 delete this.$eMsg;
 
 
856 },
857 show: function(tgtMsg){
858 if(tgtMsg === this.$eMsg){
859 this.hide();
860 return;
@@ -1034,16 +1217,10 @@
1034 e.preventDefault();
1035 Chat.submitMessage();
1036 return false;
1037 });
1038
1039 /* Returns an almost-ISO8601 form of Date object d. */
1040 const iso8601ish = function(d){
1041 return d.toISOString()
1042 .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu');
1043 };
1044
1045 (function(){/*Set up #chat-settings-button */
1046 const settingsButton = document.querySelector('#chat-settings-button');
1047 const optionsMenu = E1('#chat-config-options');
1048 const cbToggle = function(ev){
1049 ev.preventDefault();
@@ -1052,37 +1229,95 @@
1052 ? Chat.e.viewMessages : Chat.e.viewConfig);
1053 return false;
1054 };
1055 D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
1056 Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
1057 /* Settings menu entries... */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1058 const settingsOps = [{
1059 label: "Multi-line input",
1060 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1061 persistentSetting: 'edit-multiline',
1062 callback: function(){
1063 Chat.inputToggleSingleMulti();
1064 }
1065 },{
1066 label: "Monospace message font",
1067 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1068 persistentSetting: 'monospace-messages',
1069 callback: function(){
1070 document.body.classList.toggle('monospace-messages');
1071 }
1072 },{
1073 label: "Images inline",
1074 boolValue: ()=>Chat.settings.getBool('images-inline'),
1075 callback: function(){
1076 const v = Chat.settings.toggle('images-inline');
1077 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1078 }
1079 },{
1080 label: "Left-align my posts",
1081 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1082 callback: function f(){
1083 document.body.classList.toggle('my-messages-right');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1084 }
1085 },{
1086 label: "Chat-only mode",
1087 boolValue: ()=>Chat.isChatOnlyMode(),
1088 persistentSetting: 'chat-only-mode',
@@ -1139,12 +1374,13 @@
1139 D.append(line, btn, op.select);
1140 op.select.addEventListener('change', callback, false);
1141 }else if(op.hasOwnProperty('boolValue')){
1142 if(undefined === f.$id) f.$id = 0;
1143 ++f.$id;
1144 const check = D.attr(D.checkbox(1, op.boolValue()),
1145 'aria-label', op.label);
 
1146 const id = 'cfgopt'+f.$id;
1147 if(op.boolValue()) check.checked = true;
1148 D.attr(check, 'id', id);
1149 D.attr(btn, 'for', id);
1150 D.append(line, check);
@@ -1228,10 +1464,18 @@
1228 should only be true when loading older messages. */
1229 f.processPost = function(m,atEnd){
1230 ++Chat.totalMessageCount;
1231 if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
1232 if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
 
 
 
 
 
 
 
 
1233 if( m.mdel ){
1234 /* A record deletion notice. */
1235 Chat.deleteMessageElem(m.mdel);
1236 return;
1237 }
@@ -1244,10 +1488,11 @@
1244 Chat._gotServerError = m;
1245 }
1246 }/*processPost()*/;
1247 }/*end static init*/
1248 jx.msgs.forEach((m)=>f.processPost(m,atEnd));
 
1249 if('visible'===document.visibilityState){
1250 if(Chat.changesSincePageHidden){
1251 Chat.changesSincePageHidden = 0;
1252 Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
1253 }
@@ -1288,10 +1533,11 @@
1288 },
1289 onload:function(x){
1290 let gotMessages = x.msgs.length;
1291 newcontent(x,true);
1292 Chat._isBatchLoading = false;
 
1293 if(Chat._gotServerError){
1294 Chat._gotServerError = false;
1295 return;
1296 }
1297 if(n<0/*we asked for all history*/
@@ -1381,11 +1627,14 @@
1381 resumed, and reportError() produces a loud error message. */
1382 afterFetch();
1383 },
1384 onload:function(y){
1385 newcontent(y);
1386 Chat._isBatchLoading = false;
 
 
 
1387 afterFetch();
1388 }
1389 });
1390 };
1391 poll.isFirstCall = true;
@@ -1392,8 +1641,15 @@
1392 Chat._gotServerError = poll.running = false;
1393 if( window.fossil.config.chat.fromcli ){
1394 Chat.chatOnlyMode(true);
1395 }
1396 Chat.intervalTimer = setInterval(poll, 1000);
 
 
 
 
 
 
1397 setTimeout( ()=>Chat.inputFocus(), 0 );
 
1398 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
1399 })();
1400
--- src/chat.js
+++ src/chat.js
@@ -34,10 +34,30 @@
34 else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true;
35 return false;
36 };
37
38 const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank');
39
40 /**
41 Returns an almost-ISO8601 form of Date object d.
42 */
43 const iso8601ish = function(d){
44 return d.toISOString()
45 .replace('T',' ').replace(/\.\d+/,'')
46 .replace('Z', ' zulu');
47 };
48 /** Returns the local time string of Date object d, defaulting
49 to the current time. */
50 const localTimeString = function ff(d){
51 d || (d = new Date());
52 return [
53 d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/),
54 '-',pad2(d.getDate()),
55 ' ',pad2(d.getHours()),':',pad2(d.getMinutes()),
56 ':',pad2(d.getSeconds())
57 ].join('');
58 };
59
60 (function(){
61 let dbg = document.querySelector('#debugMsg');
62 if(dbg){
63 /* This can inadvertently influence our flexbox layouts, so move
@@ -116,11 +136,13 @@
136 contentDiv: E1('div.content'),
137 viewConfig: E1('#chat-config'),
138 viewPreview: E1('#chat-preview'),
139 previewContent: E1('#chat-preview-content'),
140 btnPreview: E1('#chat-preview-button'),
141 views: document.querySelectorAll('.chat-view'),
142 activeUserListWrapper: E1('#chat-user-list-wrapper'),
143 activeUserList: E1('#chat-user-list')
144 },
145 me: F.user.name,
146 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
147 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
148 pageIsActive: 'visible'===document.visibilityState,
@@ -128,10 +150,23 @@
150 notificationBubbleColor: 'white',
151 totalMessageCount: 0, // total # of inbound messages
152 //! Number of messages to load for the history buttons
153 loadMessageCount: Math.abs(F.config.chat.initSize || 20),
154 ajaxInflight: 0,
155 usersLastSeen:{
156 /* Map of user names to their most recent message time
157 (JS Date object). Only messages received by the chat client
158 are considered. */
159 /* Reminder: to convert a Julian time J to JS:
160 new Date((J - 2440587.5) * 86400000) */
161 },
162 filterState:{
163 activeUser: undefined,
164 match: function(uname){
165 return this.activeUser===uname || !this.activeUser;
166 }
167 },
168 /** Gets (no args) or sets (1 arg) the current input text field value,
169 taking into account single- vs multi-line input. The getter returns
170 a string and the setter returns this object. */
171 inputValue: function(){
172 const e = this.inputElement();
@@ -167,10 +202,11 @@
202 D.removeClass(this.e.inputCurrent, 'hidden');
203 const mh2 = m.clientHeight;
204 m.scrollTo(0, sTop + (mh1-mh2));
205 this.e.inputCurrent.value = old.value;
206 old.value = '';
207 this.animate(this.e.inputCurrent, "anim-flip-v");
208 return this;
209 },
210 /**
211 If passed true or no arguments, switches to multi-line mode
212 if currently in single-line mode. If passed false, switches
@@ -244,10 +280,13 @@
280 the list. */
281 injectMessageElem: function f(e, atEnd){
282 const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint,
283 holder = this.e.viewMessages,
284 prevMessage = this.e.newestMessage;
285 if(!this.filterState.match(e.dataset.xfrom)){
286 e.classList.add('hidden');
287 }
288 if(atEnd){
289 const fe = mip.nextElementSibling;
290 if(fe) mip.parentNode.insertBefore(e, fe);
291 else D.append(mip.parentNode, e);
292 }else{
@@ -367,11 +406,13 @@
406 defaults:{
407 "images-inline": !!F.config.chat.imagesInline,
408 "edit-multiline": false,
409 "monospace-messages": false,
410 "chat-only-mode": false,
411 "audible-alert": true,
412 "active-user-list": false,
413 "active-user-list-timestamps": false
414 }
415 },
416 /** Plays a new-message notification sound IF the audible-alert
417 setting is true, else this is a no-op. Returns this.
418 */
@@ -403,16 +444,110 @@
444 Expects e to be one of the elements in this.e.views.
445 The 'hidden' class is removed from e and added to
446 all other elements in that list. Returns e.
447 */
448 setCurrentView: function(e){
449 if(e===this.e.currentView){
450 return e;
451 }
452 this.e.views.forEach(function(E){
453 if(e!==E) D.addClass(E,'hidden');
454 });
455 this.e.currentView = D.removeClass(e,'hidden');
456 this.animate(this.e.currentView, 'anim-fade-in-fast');
457 return this.e.currentView;
458 },
459 /**
460 Updates the "active user list" view if we are not currently
461 batch-loading messages and if the active user list UI element
462 is active.
463 */
464 updateActiveUserList: function callee(){
465 if(this._isBatchLoading
466 || this.e.activeUserListWrapper.classList.contains('hidden')){
467 return this;
468 }else if(!callee.sortUsersSeen){
469 /** Array.sort() callback. Expects an array of user names and
470 sorts them in last-received message order (newest first). */
471 const self = this;
472 callee.sortUsersSeen = function(l,r){
473 l = self.usersLastSeen[l];
474 r = self.usersLastSeen[r];
475 if(l && r) return r - l;
476 else if(l) return -1;
477 else if(r) return 1;
478 else return 0;
479 };
480 callee.addUserElem = function(u){
481 const uSpan = D.addClass(D.span(), 'chat-user');
482 const uDate = self.usersLastSeen[u];
483 if(self.filterState.activeUser===u){
484 uSpan.classList.add('selected');
485 }
486 uSpan.dataset.uname = u;
487 D.append(uSpan, u, "\n",
488 D.append(
489 D.addClass(D.span(),'timestamp'),
490 localTimeString(uDate)//.substr(5/*chop off year*/)
491 ));
492 if(uDate.$uColor){
493 uSpan.style.backgroundColor = uDate.$uColor;
494 }
495 D.append(self.e.activeUserList, uSpan);
496 };
497 }
498 //D.clearElement(this.e.activeUserList);
499 D.remove(this.e.activeUserList.querySelectorAll('.chat-user'));
500 Object.keys(this.usersLastSeen).sort(
501 callee.sortUsersSeen
502 ).forEach(callee.addUserElem);
503 return this;
504 },
505 /**
506 Applies user name filter to all current messages, or clears
507 the filter if uname is falsy.
508 */
509 setUserFilter: function(uname){
510 this.filterState.activeUser = uname;
511 const mw = this.e.viewMessages.querySelectorAll('.message-widget');
512 const self = this;
513 let eLast;
514 if(!uname){
515 D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'),
516 'hidden');
517 }else{
518 mw.forEach(function(w){
519 if(self.filterState.match(w.dataset.xfrom)){
520 w.classList.remove('hidden');
521 eLast = w;
522 }else{
523 w.classList.add('hidden');
524 }
525 });
526 }
527 if(eLast) eLast.scrollIntoView(false);
528 else this.scrollMessagesTo(1);
529 cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){
530 e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected');
531 });
532 return this;
533 },
534
535 /**
536 If animations are enabled, passes its arguments
537 to D.addClassBriefly(), else this is a no-op.
538 If cb is a function, it is called after the
539 CSS class is removed. Returns this object;
540 */
541 animate: function f(e,a,cb){
542 if(!f.$disabled){
543 D.addClassBriefly(e, a, 0, cb);
544 }
545 return this;
546 }
547 };
548 cs.animate.$disabled = true;
549 F.fetch.beforesend = ()=>cs.ajaxStart();
550 F.fetch.aftersend = ()=>cs.ajaxEnd();
551 cs.e.inputCurrent = cs.e.inputSingle;
552 /* Install default settings... */
553 Object.keys(cs.settings.defaults).forEach(function(k){
@@ -427,10 +562,16 @@
562 tall vs wide. Can be toggled via settings popup. */
563 document.body.classList.add('my-messages-right');
564 }
565 if(cs.settings.getBool('monospace-messages',false)){
566 document.body.classList.add('monospace-messages');
567 }
568 if(cs.settings.getBool('active-user-list',false)){
569 cs.e.activeUserListWrapper.classList.remove('hidden');
570 }
571 if(cs.settings.getBool('active-user-list-timestamps',false)){
572 cs.e.activeUserList.classList.add('timestamps');
573 }
574 cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
575 cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
576 cs.pageTitleOrig = cs.e.pageTitle.innerText;
577 const qs = (e)=>document.querySelector(e);
@@ -624,10 +765,36 @@
765 if(cs.pageIsActive){
766 cs.e.pageTitle.innerText = cs.pageTitleOrig;
767 }
768 }, true);
769 cs.setCurrentView(cs.e.viewMessages);
770
771 cs.e.activeUserList.addEventListener('click', function f(ev){
772 /* Filter messages on a user clicked in activeUserList */
773 ev.stopPropagation();
774 ev.preventDefault();
775 let eUser = ev.target;
776 while(eUser!==this && !eUser.classList.contains('chat-user')){
777 eUser = eUser.parentNode;
778 }
779 if(eUser==this || !eUser) return false;
780 const uname = eUser.dataset.uname;
781 let eLast;
782 cs.setCurrentView(cs.e.viewMessages);
783 if(eUser.classList.contains('selected')){
784 /* If curently selected, toggle filter off */
785 eUser.classList.remove('selected');
786 cs.setUserFilter(false);
787 delete f.$eSelected;
788 }else{
789 if(f.$eSelected) f.$eSelected.classList.remove('selected');
790 f.$eSelected = eUser;
791 eUser.classList.add('selected');
792 cs.setUserFilter(uname);
793 }
794 return false;
795 }, false);
796 return cs;
797 })()/*Chat initialization*/;
798
799 /**
800 Custom widget type for rendering messages (one message per
@@ -671,21 +838,10 @@
838 d.getHours(),":",
839 (d.getMinutes()+100).toString().slice(1,3),
840 ' ', dowMap[d.getDay()]
841 ].join('');
842 };
 
 
 
 
 
 
 
 
 
 
 
843 cf.prototype = {
844 scrollIntoView: function(){
845 this.e.content.scrollIntoView();
846 },
847 setMessage: function(m){
@@ -771,14 +927,14 @@
927 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
928 }*/
929 return this;
930 },
931 /* Event handler for clicking .message-user elements to show their
932 timestamps and a set of actions. */
933 _handleLegendClicked: function f(ev){
934 if(!f.popup){
935 /* "Popup" widget */
936 f.popup = {
937 e: D.addClass(D.div(), 'chat-message-popup'),
938 refresh:function(){
939 const eMsg = this.$eMsg/*.message-widget element*/;
940 if(!eMsg) return;
@@ -843,18 +999,45 @@
999 y: 'a'
1000 }), "User's Timeline"),
1001 'target', '_blank'
1002 );
1003 D.append(toolbar2, timelineLink);
1004 if(Chat.filterState.activeUser &&
1005 Chat.filterState.match(eMsg.dataset.xfrom)){
1006 /* Add a button to clear user filter and jump to
1007 this message in its original context. */
1008 D.append(
1009 this.e,
1010 D.append(
1011 D.addClass(D.div(), 'toolbar'),
1012 D.button(
1013 "Message in context",
1014 function(){
1015 self.hide();
1016 Chat.setUserFilter(false);
1017 eMsg.scrollIntoView(false);
1018 Chat.animate(
1019 eMsg.firstElementChild, 'anim-flip-h'
1020 //eMsg.firstElementChild, 'anim-flip-v'
1021 //eMsg.childNodes, 'anim-rotate-360'
1022 //eMsg.childNodes, 'anim-flip-v'
1023 //eMsg, 'anim-flip-v'
1024 );
1025 })
1026 )
1027 );
1028 }/*jump-to button*/
1029 }
1030 const tab = eMsg.querySelector('.message-widget-tab');
1031 D.append(tab, this.e);
1032 D.removeClass(this.e, 'hidden');
1033 Chat.animate(this.e, 'anim-fade-in-fast');
1034 }/*refresh()*/,
1035 hide: function(){
 
1036 delete this.$eMsg;
1037 D.addClass(this.e, 'hidden');
1038 D.clearElement(this.e);
1039 },
1040 show: function(tgtMsg){
1041 if(tgtMsg === this.$eMsg){
1042 this.hide();
1043 return;
@@ -1034,16 +1217,10 @@
1217 e.preventDefault();
1218 Chat.submitMessage();
1219 return false;
1220 });
1221
 
 
 
 
 
 
1222 (function(){/*Set up #chat-settings-button */
1223 const settingsButton = document.querySelector('#chat-settings-button');
1224 const optionsMenu = E1('#chat-config-options');
1225 const cbToggle = function(ev){
1226 ev.preventDefault();
@@ -1052,37 +1229,95 @@
1229 ? Chat.e.viewMessages : Chat.e.viewConfig);
1230 return false;
1231 };
1232 D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
1233 Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
1234
1235 /** Internal acrobatics to allow certain settings toggles to access
1236 related toggles. */
1237 const namedOptions = {
1238 activeUsers:{
1239 label: "Show active users list",
1240 boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1241 persistentSetting: 'active-user-list',
1242 callback: function(){
1243 D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1244 D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1245 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1246 /* When hiding this element, undo all filtering */
1247 Chat.setUserFilter(false);
1248 /*Ideally we'd scroll the final message into view
1249 now, but because viewMessages is currently hidden behind
1250 viewConfig, scrolling is a no-op. */
1251 Chat.scrollMessagesTo(1);
1252 }else{
1253 Chat.updateActiveUserList();
1254 Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
1255 }
1256 }
1257 }
1258 };
1259 if(1){
1260 /* Per user request, toggle the list of users on and off if the
1261 legend element is tapped. */
1262 const optAu = namedOptions.activeUsers;
1263 optAu.theLegend = Chat.e.activeUserListWrapper.firstElementChild/*LEGEND*/;
1264 optAu.theList = optAu.theLegend.nextElementSibling/*user list container*/;
1265 optAu.theLegend.addEventListener('click',function(){
1266 D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed');
1267 if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){
1268 Chat.animate(optAu.theList,'anim-flip-v');
1269 }
1270 }, false);
1271 }/*namedOptions.activeUsers additional setup*/
1272 /* Settings menu entries... Remember that they will be rendered in
1273 reverse order and the most frequently-needed ones "should"
1274 (arguably) be closer to the start of this list so that they
1275 will be rendered within easier reach of the settings button. */
1276 const settingsOps = [{
1277 label: "Multi-line input",
1278 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1279 persistentSetting: 'edit-multiline',
1280 callback: function(){
1281 Chat.inputToggleSingleMulti();
1282 }
1283 },{
1284 label: "Left-align my posts",
1285 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1286 callback: function f(){
1287 document.body.classList.toggle('my-messages-right');
 
1288 }
1289 },{
1290 label: "Show images inline",
1291 boolValue: ()=>Chat.settings.getBool('images-inline'),
1292 callback: function(){
1293 const v = Chat.settings.toggle('images-inline');
1294 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1295 }
1296 },{
1297 label: "Timestamps in active users list",
1298 boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1299 persistentSetting: 'active-user-list-timestamps',
1300 callback: function(){
1301 D.toggleClass(Chat.e.activeUserList,'timestamps');
1302 /* If the timestamp option is activated but
1303 namedOptions.activeUsers is not currently checked then
1304 toggle that option on as well. */
1305 if(Chat.e.activeUserList.classList.contains('timestamps')
1306 && !namedOptions.activeUsers.boolValue()){
1307 namedOptions.activeUsers.checkbox.checked = true;
1308 namedOptions.activeUsers.callback();
1309 Chat.settings.set(namedOptions.activeUsers.persistentSetting, true);
1310 }
1311 }
1312 },
1313 namedOptions.activeUsers,{
1314 label: "Monospace message font",
1315 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1316 persistentSetting: 'monospace-messages',
1317 callback: function(){
1318 document.body.classList.toggle('monospace-messages');
1319 }
1320 },{
1321 label: "Chat-only mode",
1322 boolValue: ()=>Chat.isChatOnlyMode(),
1323 persistentSetting: 'chat-only-mode',
@@ -1139,12 +1374,13 @@
1374 D.append(line, btn, op.select);
1375 op.select.addEventListener('change', callback, false);
1376 }else if(op.hasOwnProperty('boolValue')){
1377 if(undefined === f.$id) f.$id = 0;
1378 ++f.$id;
1379 const check = op.checkbox
1380 = D.attr(D.checkbox(1, op.boolValue()),
1381 'aria-label', op.label);
1382 const id = 'cfgopt'+f.$id;
1383 if(op.boolValue()) check.checked = true;
1384 D.attr(check, 'id', id);
1385 D.attr(btn, 'for', id);
1386 D.append(line, check);
@@ -1228,10 +1464,18 @@
1464 should only be true when loading older messages. */
1465 f.processPost = function(m,atEnd){
1466 ++Chat.totalMessageCount;
1467 if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
1468 if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
1469 if(m.xfrom && m.mtime){
1470 const d = new Date(m.mtime);
1471 const uls = Chat.usersLastSeen[m.xfrom];
1472 if(!uls || uls<d){
1473 d.$uColor = m.uclr;
1474 Chat.usersLastSeen[m.xfrom] = d;
1475 }
1476 }
1477 if( m.mdel ){
1478 /* A record deletion notice. */
1479 Chat.deleteMessageElem(m.mdel);
1480 return;
1481 }
@@ -1244,10 +1488,11 @@
1488 Chat._gotServerError = m;
1489 }
1490 }/*processPost()*/;
1491 }/*end static init*/
1492 jx.msgs.forEach((m)=>f.processPost(m,atEnd));
1493 Chat.updateActiveUserList();
1494 if('visible'===document.visibilityState){
1495 if(Chat.changesSincePageHidden){
1496 Chat.changesSincePageHidden = 0;
1497 Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
1498 }
@@ -1288,10 +1533,11 @@
1533 },
1534 onload:function(x){
1535 let gotMessages = x.msgs.length;
1536 newcontent(x,true);
1537 Chat._isBatchLoading = false;
1538 Chat.updateActiveUserList();
1539 if(Chat._gotServerError){
1540 Chat._gotServerError = false;
1541 return;
1542 }
1543 if(n<0/*we asked for all history*/
@@ -1381,11 +1627,14 @@
1627 resumed, and reportError() produces a loud error message. */
1628 afterFetch();
1629 },
1630 onload:function(y){
1631 newcontent(y);
1632 if(Chat._isBatchLoading){
1633 Chat._isBatchLoading = false;
1634 Chat.updateActiveUserList();
1635 }
1636 afterFetch();
1637 }
1638 });
1639 };
1640 poll.isFirstCall = true;
@@ -1392,8 +1641,15 @@
1641 Chat._gotServerError = poll.running = false;
1642 if( window.fossil.config.chat.fromcli ){
1643 Chat.chatOnlyMode(true);
1644 }
1645 Chat.intervalTimer = setInterval(poll, 1000);
1646 if(0){
1647 const flip = (ev)=>Chat.animate(ev.target,'anim-flip-h');
1648 document.querySelectorAll('#chat-edit-buttons button').forEach(function(e){
1649 e.addEventListener('click',flip, false);
1650 });
1651 }
1652 setTimeout( ()=>Chat.inputFocus(), 0 );
1653 Chat.animate.$disabled = false;
1654 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
1655 })();
1656
+1 -1
--- src/default.css
+++ src/default.css
@@ -1387,11 +1387,11 @@
13871387
13881388
.fossil-tooltip {
13891389
text-align: center;
13901390
padding: 0.2em 1em;
13911391
border: 1px solid black;
1392
- border-radius: 0.25em;
1392
+ border-radius: 0.5em;
13931393
position: absolute;
13941394
display: inline-block;
13951395
z-index: 19/*below default skin's hamburger popup*/;
13961396
box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
13971397
background-color: inherit;
13981398
--- src/default.css
+++ src/default.css
@@ -1387,11 +1387,11 @@
1387
1388 .fossil-tooltip {
1389 text-align: center;
1390 padding: 0.2em 1em;
1391 border: 1px solid black;
1392 border-radius: 0.25em;
1393 position: absolute;
1394 display: inline-block;
1395 z-index: 19/*below default skin's hamburger popup*/;
1396 box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
1397 background-color: inherit;
1398
--- src/default.css
+++ src/default.css
@@ -1387,11 +1387,11 @@
1387
1388 .fossil-tooltip {
1389 text-align: center;
1390 padding: 0.2em 1em;
1391 border: 1px solid black;
1392 border-radius: 0.5em;
1393 position: absolute;
1394 display: inline-block;
1395 z-index: 19/*below default skin's hamburger popup*/;
1396 box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
1397 background-color: inherit;
1398
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -119,13 +119,18 @@
119119
/** Returns a new TEXT node which contains the text of all of the
120120
arguments appended together. */
121121
dom.text = function(/*...*/){
122122
return document.createTextNode(argsToArray(arguments).join(''));
123123
};
124
- dom.button = function(label){
124
+ /** Returns a new Button element with the given optional
125
+ label and on-click event listener function. */
126
+ dom.button = function(label,callback){
125127
const b = this.create('button');
126128
if(label) b.appendChild(this.text(label));
129
+ if('function' === typeof callback){
130
+ b.addEventListener('click', callback, false);
131
+ }
127132
return b;
128133
};
129134
/**
130135
Returns a TEXTAREA element.
131136
@@ -677,10 +682,80 @@
677682
A DOM event handler which simply passes event.target
678683
to dom.flashOnce().
679684
*/
680685
dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
681686
687
+ /**
688
+ This variant of flashOnce() flashes the element e n times
689
+ for a duration of howLongMs milliseconds then calls the
690
+ afterFlashCallback() callback. It may also be called with 2
691
+ or 3 arguments, in which case:
692
+
693
+ 2 arguments: default flash time and no callback.
694
+
695
+ 3 arguments: 3rd may be a flash delay time or a callback
696
+ function.
697
+
698
+ Returns this object but the flashing is asynchronous.
699
+
700
+ Depending on system load and related factors, a multi-flash
701
+ animation might stutter and look suboptimal.
702
+ */
703
+ dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
704
+ const args = argsToArray(arguments);
705
+ args.splice(1,1);
706
+ if(arguments.length===3 && 'function'===typeof howLongMs){
707
+ afterFlashCallback = howLongMs;
708
+ howLongMs = args[1] = this.flashOnce.defaultTimeMs;
709
+ }else if(arguments.length<3){
710
+ args[1] = this.flashOnce.defaultTimeMs;
711
+ }
712
+ n = +n;
713
+ const self = this;
714
+ const cb = args[2] = function f(){
715
+ if(--n){
716
+ setTimeout(()=>self.flashOnce(e, howLongMs, f),
717
+ howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
718
+ }else if(afterFlashCallback){
719
+ afterFlashCallback();
720
+ }
721
+ };
722
+ this.flashOnce.apply(this, args);
723
+ return this;
724
+ };
725
+
726
+ /**
727
+ Adds the given CSS class or array of CSS classes to the given
728
+ element or forEach-capable list of elements for howLongMs, then
729
+ removes it. If afterCallack is a function, it is called after the
730
+ CSS class is removed from all elements. If called with 3
731
+ arguments and the 3rd is a function, the 3rd is treated as a
732
+ callback and the default time (addClassBriefly.defaultTimeMs) is
733
+ used. If called with only 2 arguments, a time of
734
+ addClassBriefly.defaultTimeMs is used.
735
+
736
+ Passing a value of 0 for howLongMs causes the default value
737
+ to be applied.
738
+
739
+ Returns this object but the CSS removal is asynchronous.
740
+ */
741
+ dom.addClassBriefly = function f(e, className, howLongMs, afterCallback){
742
+ if(arguments.length<4 && 'function'===typeof howLongMs){
743
+ afterCallback = howLongMs;
744
+ howLongMs = f.defaultTimeMs;
745
+ }else if(arguments.length<3 || !+howLongMs){
746
+ howLongMs = f.defaultTimeMs;
747
+ }
748
+ this.addClass(e, className);
749
+ setTimeout(function(){
750
+ dom.removeClass(e, className);
751
+ if(afterCallback) afterCallback();
752
+ }, howLongMs);
753
+ return this;
754
+ };
755
+ dom.addClassBriefly.defaultTimeMs = 1000;
756
+
682757
/**
683758
Attempts to copy the given text to the system clipboard. Returns
684759
true if it succeeds, else false.
685760
*/
686761
dom.copyTextToClipboard = function(text){
687762
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -119,13 +119,18 @@
119 /** Returns a new TEXT node which contains the text of all of the
120 arguments appended together. */
121 dom.text = function(/*...*/){
122 return document.createTextNode(argsToArray(arguments).join(''));
123 };
124 dom.button = function(label){
 
 
125 const b = this.create('button');
126 if(label) b.appendChild(this.text(label));
 
 
 
127 return b;
128 };
129 /**
130 Returns a TEXTAREA element.
131
@@ -677,10 +682,80 @@
677 A DOM event handler which simply passes event.target
678 to dom.flashOnce().
679 */
680 dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682 /**
683 Attempts to copy the given text to the system clipboard. Returns
684 true if it succeeds, else false.
685 */
686 dom.copyTextToClipboard = function(text){
687
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -119,13 +119,18 @@
119 /** Returns a new TEXT node which contains the text of all of the
120 arguments appended together. */
121 dom.text = function(/*...*/){
122 return document.createTextNode(argsToArray(arguments).join(''));
123 };
124 /** Returns a new Button element with the given optional
125 label and on-click event listener function. */
126 dom.button = function(label,callback){
127 const b = this.create('button');
128 if(label) b.appendChild(this.text(label));
129 if('function' === typeof callback){
130 b.addEventListener('click', callback, false);
131 }
132 return b;
133 };
134 /**
135 Returns a TEXTAREA element.
136
@@ -677,10 +682,80 @@
682 A DOM event handler which simply passes event.target
683 to dom.flashOnce().
684 */
685 dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
686
687 /**
688 This variant of flashOnce() flashes the element e n times
689 for a duration of howLongMs milliseconds then calls the
690 afterFlashCallback() callback. It may also be called with 2
691 or 3 arguments, in which case:
692
693 2 arguments: default flash time and no callback.
694
695 3 arguments: 3rd may be a flash delay time or a callback
696 function.
697
698 Returns this object but the flashing is asynchronous.
699
700 Depending on system load and related factors, a multi-flash
701 animation might stutter and look suboptimal.
702 */
703 dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
704 const args = argsToArray(arguments);
705 args.splice(1,1);
706 if(arguments.length===3 && 'function'===typeof howLongMs){
707 afterFlashCallback = howLongMs;
708 howLongMs = args[1] = this.flashOnce.defaultTimeMs;
709 }else if(arguments.length<3){
710 args[1] = this.flashOnce.defaultTimeMs;
711 }
712 n = +n;
713 const self = this;
714 const cb = args[2] = function f(){
715 if(--n){
716 setTimeout(()=>self.flashOnce(e, howLongMs, f),
717 howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
718 }else if(afterFlashCallback){
719 afterFlashCallback();
720 }
721 };
722 this.flashOnce.apply(this, args);
723 return this;
724 };
725
726 /**
727 Adds the given CSS class or array of CSS classes to the given
728 element or forEach-capable list of elements for howLongMs, then
729 removes it. If afterCallack is a function, it is called after the
730 CSS class is removed from all elements. If called with 3
731 arguments and the 3rd is a function, the 3rd is treated as a
732 callback and the default time (addClassBriefly.defaultTimeMs) is
733 used. If called with only 2 arguments, a time of
734 addClassBriefly.defaultTimeMs is used.
735
736 Passing a value of 0 for howLongMs causes the default value
737 to be applied.
738
739 Returns this object but the CSS removal is asynchronous.
740 */
741 dom.addClassBriefly = function f(e, className, howLongMs, afterCallback){
742 if(arguments.length<4 && 'function'===typeof howLongMs){
743 afterCallback = howLongMs;
744 howLongMs = f.defaultTimeMs;
745 }else if(arguments.length<3 || !+howLongMs){
746 howLongMs = f.defaultTimeMs;
747 }
748 this.addClass(e, className);
749 setTimeout(function(){
750 dom.removeClass(e, className);
751 if(afterCallback) afterCallback();
752 }, howLongMs);
753 return this;
754 };
755 dom.addClassBriefly.defaultTimeMs = 1000;
756
757 /**
758 Attempts to copy the given text to the system clipboard. Returns
759 true if it succeeds, else false.
760 */
761 dom.copyTextToClipboard = function(text){
762
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -355,10 +355,11 @@
355355
*/
356356
setup: function f(){
357357
if(!f.hasOwnProperty('clickHandler')){
358358
f.clickHandler = function fch(ev){
359359
ev.preventDefault();
360
+ ev.stopPropagation();
360361
if(!fch.popup){
361362
fch.popup = new F.PopupWidget({
362363
cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
363364
refresh: function(){
364365
}
@@ -411,10 +412,11 @@
411412
x -= popupRect.width/2;
412413
}
413414
if(x<0) x = 0;
414415
//console.debug("dimensions",x,y, popupRect, rectBody);
415416
fch.popup.show(x, y);
417
+ return false;
416418
};
417419
f.foreachElement = function(e){
418420
if(e.classList.contains('processed')) return;
419421
e.classList.add('processed');
420422
e.$helpContent = [];
421423
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -355,10 +355,11 @@
355 */
356 setup: function f(){
357 if(!f.hasOwnProperty('clickHandler')){
358 f.clickHandler = function fch(ev){
359 ev.preventDefault();
 
360 if(!fch.popup){
361 fch.popup = new F.PopupWidget({
362 cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
363 refresh: function(){
364 }
@@ -411,10 +412,11 @@
411 x -= popupRect.width/2;
412 }
413 if(x<0) x = 0;
414 //console.debug("dimensions",x,y, popupRect, rectBody);
415 fch.popup.show(x, y);
 
416 };
417 f.foreachElement = function(e){
418 if(e.classList.contains('processed')) return;
419 e.classList.add('processed');
420 e.$helpContent = [];
421
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -355,10 +355,11 @@
355 */
356 setup: function f(){
357 if(!f.hasOwnProperty('clickHandler')){
358 f.clickHandler = function fch(ev){
359 ev.preventDefault();
360 ev.stopPropagation();
361 if(!fch.popup){
362 fch.popup = new F.PopupWidget({
363 cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
364 refresh: function(){
365 }
@@ -411,10 +412,11 @@
412 x -= popupRect.width/2;
413 }
414 if(x<0) x = 0;
415 //console.debug("dimensions",x,y, popupRect, rectBody);
416 fch.popup.show(x, y);
417 return false;
418 };
419 f.foreachElement = function(e){
420 if(e.classList.contains('processed')) return;
421 e.classList.add('processed');
422 e.$helpContent = [];
423
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -1,8 +1,8 @@
11
(function(F){
22
/**
3
- fossil.store is a basic wrapper around localStorage
3
+ fossil.storage is a basic wrapper around localStorage
44
or sessionStorage or a dummy proxy object if neither
55
of those are available.
66
*/
77
const tryStorage = function f(obj){
88
if(!f.key) f.key = 'fossil.access.check';
99
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -1,8 +1,8 @@
1 (function(F){
2 /**
3 fossil.store is a basic wrapper around localStorage
4 or sessionStorage or a dummy proxy object if neither
5 of those are available.
6 */
7 const tryStorage = function f(obj){
8 if(!f.key) f.key = 'fossil.access.check';
9
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -1,8 +1,8 @@
1 (function(F){
2 /**
3 fossil.storage is a basic wrapper around localStorage
4 or sessionStorage or a dummy proxy object if neither
5 of those are available.
6 */
7 const tryStorage = function f(obj){
8 if(!f.key) f.key = 'fossil.access.check';
9
+125 -49
--- src/style.chat.css
+++ src/style.chat.css
@@ -9,14 +9,18 @@
99
border: none;
1010
display: flex;
1111
flex-direction: column;
1212
border: none;
1313
align-items: flex-start;
14
+}
15
+body.chat .message-widget:last-of-type {
16
+ /* Latest message: reduce bottom gap */
17
+ margin-bottom: 0.1em;
1418
}
1519
body.chat.my-messages-right .message-widget.mine {
1620
/* Right-aligns a user's own chat messages, similar to how
17
- most mobile messaging apps do it. */
21
+ most/some mobile messaging apps do it. */
1822
align-items: flex-end;
1923
}
2024
body.chat.my-messages-right .message-widget.notification {
2125
/* Center-aligns a system-level notification message. */
2226
align-items: center;
@@ -77,10 +81,12 @@
7781
display: flex;
7882
flex-direction: column;
7983
align-items: stretch;
8084
padding: 0.25em;
8185
margin-top: 0.25em;
86
+ border: 1px outset;
87
+ border-radius: 0.5em;
8288
}
8389
/* Full message timestamps. */
8490
body.chat .chat-message-popup > span { white-space: nowrap; }
8591
/* Container for the message deletion buttons. */
8692
body.chat .chat-message-popup > .toolbar {
@@ -132,53 +138,14 @@
132138
flex-direction: column;
133139
align-items: stretch;
134140
padding: 0.25em;
135141
z-index: 200;
136142
}
137
-body.chat .chat-settings-popup > span {
138
- vertical-align: middle;
139
-}
140
-body.chat .chat-settings-popup > span.menu-entry{
141
- white-space: nowrap;
142
- cursor: pointer;
143
- border: 1px solid;
144
- border-radius: 0.25em;
145
- padding: 0.25em 0.5em;
146
- display: flex;
147
- flex-direction: row;
148
- align-items: center;
149
- justify-content: space-between;
150
-}
151
-body.chat .chat-settings-popup > span.menu-entry:hover {
152
-}
153
-body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
154
- vertical-align: middle;
155
-}
156
-body.chat .chat-settings-popup > span.menu-entry > span.button {
157
- margin: 0.25em 0.2em;
158
- padding: 0.25em;
159
- flex: 1 1 auto/*eliminates dead no-click zones on the right*/;
160
-}
161
-body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] {
162
- cursor: inherit;
163
-}
164
-body.chat .chat-settings-popup > select.menu-entry {
165
- flex: 1 1 auto;
166
- padding: 0;
167
- cursor: pointer;
168
- min-height: 2.5em;
169
- border-radius: 0.25em;
170
-}
171
-body.chat .chat-settings-popup > select.menu-entry > option {
172
- /*Recall that many browsers don't allow styling of OPTION
173
- elements, or allow only very limited styling.*/
174
-}
175143
176144
/** Container for the list of /chat messages. */
177145
body.chat #chat-messages-wrapper {
178146
overflow: auto;
179
- flex: 2 1 auto;
180147
padding: 0 0.25em;
181148
}
182149
body.chat #chat-messages-wrapper.loading > * {
183150
/* An attempt at reducing flicker when loading lots of messages. */
184151
visibility: hidden;
@@ -199,16 +166,14 @@
199166
}
200167
/* Wrapper for /chat user input controls */
201168
body.chat #chat-input-area {
202169
display: flex;
203170
flex-direction: column;
204
- padding: 0.5em 0 0 0;
205
- border-bottom: none;
206
- border-top: 1px solid black;
207
- margin: 0.5em 0 0 0;
171
+ padding: 0;
172
+ margin: 0;
208173
position: initial /*sticky currently disabled due to scrolling-related issues*/;
209
- bottom: 0;
174
+ /*bottom: 0;*/
210175
}
211176
body.chat:not(.chat-only-mode) #chat-input-area{
212177
/* Safari user reports that 2em is necessary to keep the file selection
213178
widget from overlapping the page footer, whereas a margin of 0 is fine
214179
for FF/Chrome (and 2em is a *huge* waste of space for those). */
@@ -234,10 +199,11 @@
234199
body.chat #chat-input-line.single-line #chat-edit-buttons {
235200
flex-direction: row;
236201
}
237202
body.chat #chat-edit-buttons > * {
238203
flex: 1 1 auto;
204
+ padding: initial/*some skins mess this up for buttons*/;
239205
}
240206
body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * {
241207
max-width: 4em;
242208
margin: 0.25em;
243209
}
@@ -271,11 +237,11 @@
271237
}
272238
body.chat #chat-input-file-area > .file-selection-wrapper {
273239
align-self: flex-start;
274240
margin-right: 0.5em;
275241
flex: 0 1 auto;
276
- padding: 0.25em 0.25em 0.25em 0;
242
+ padding: 0.25em 0.5em;
277243
white-space: nowrap;
278244
}
279245
body.chat #chat-input-file-area .file-selection-wrapper > * {
280246
vertical-align: middle;
281247
margin: 0;
@@ -303,18 +269,21 @@
303269
304270
body.chat #chat-drop-details img {
305271
max-width: 45%;
306272
max-height: 45%;
307273
}
308
-
274
+body.chat .chat-view {
275
+ flex: 20 1 auto
276
+ /*ensure that these grow more than the non-.chat-view elements*/;
277
+ margin-bottom: 0.2em;
278
+}
309279
body.chat #chat-config,
310280
body.chat #chat-preview {
311281
/* /chat configuration widget */
312282
display: flex;
313283
flex-direction: column;
314284
overflow: auto;
315
- flex: 2 1 auto;
316285
padding: 0;
317286
margin: 0;
318287
align-items: stretch;
319288
min-height: 6em;
320289
}
@@ -327,12 +296,15 @@
327296
}
328297
body.chat #chat-config #chat-config-options .menu-entry {
329298
display: flex;
330299
align-items: center;
331300
flex-direction: row;
332
- flex-wrap: wrap;
301
+ flex-wrap: nowrap;
333302
padding: 1em;
303
+}
304
+body.chat #chat-config #chat-config-options .menu-entry > label {
305
+ cursor: pointer;
334306
}
335307
body.chat #chat-config #chat-config-options .menu-entry > input:first-child {
336308
margin-right: 1em;
337309
}
338310
body.chat #chat-config #chat-config-options .menu-entry > label:first-child {
@@ -357,5 +329,109 @@
357329
body.chat #chat-preview #chat-preview-buttons > button {
358330
padding: 0.5em;
359331
flex: 0 1 auto;
360332
margin: 0.25em 0;
361333
}
334
+
335
+body.chat #chat-user-list-wrapper {
336
+ /* Safari can't do fieldsets right, so we emulate one. */
337
+ border-radius: 0.5em;
338
+ margin: 1em 0 0.2em 0;
339
+ padding: 0 0.5em;
340
+ border-style: inset;
341
+ border-width: 0 1px 1px 1px/*else collides with the LEGEND*/;
342
+}
343
+body.chat #chat-user-list-wrapper.collapsed {
344
+ padding: 0;
345
+}
346
+body.chat #chat-user-list-wrapper > .legend {
347
+ font-weight: initial;
348
+ padding: 0 0.5em 0 0.5em;
349
+ position: relative;
350
+ top: -1.75ex/* place it like a fieldset legend */;
351
+ cursor: pointer;
352
+}
353
+body.chat #chat-user-list-wrapper > .legend > * {
354
+ vertical-align: middle;
355
+}
356
+body.chat #chat-user-list-wrapper > .legend > *:nth-child(2){
357
+ /* Title label */
358
+ opacity: 0.6;
359
+ font-size: 0.8em;
360
+}
361
+body.chat #chat-user-list-wrapper.collapsed > .legend > *:nth-child(2)::after {
362
+ content: " (tap to toggle)";
363
+}
364
+body.chat #chat-user-list-wrapper .help-buttonlet {
365
+ margin: 0;
366
+}
367
+body.chat #chat-user-list-wrapper.collapsed #chat-user-list {
368
+ position: absolute !important;
369
+ opacity: 0 !important;
370
+ pointer-events: none !important;
371
+ display: none !important;
372
+}
373
+body.chat #chat-user-list {
374
+ margin-top: -1.25ex;
375
+ display: flex;
376
+ flex-direction: row;
377
+ flex-wrap: wrap;
378
+ align-items: center;
379
+}
380
+body.chat #chat-user-list .chat-user {
381
+ margin: 0.2em;
382
+ padding: 0.1em 0.5em 0.2em 0.5em;
383
+ border-radius: 0.5em;
384
+ cursor: pointer;
385
+ text-align: center;
386
+ white-space: pre;
387
+}
388
+body.chat #chat-user-list .timestamp {
389
+ font-size: 85%;
390
+ font-family: monospace;
391
+}
392
+body.chat #chat-user-list:not(.timestamps) .timestamp {
393
+ display: none;
394
+}
395
+body.chat #chat-user-list .chat-user.selected {
396
+ font-weight: bold;
397
+ text-decoration: underline;
398
+}
399
+
400
+body.chat .anim-rotate-360 {
401
+ animation: rotate-360 750ms linear;
402
+}
403
+@keyframes rotate-360 {
404
+ from { transform: rotate(0deg); }
405
+ to { transform: rotate(360deg); }
406
+}
407
+body.chat .anim-flip-h {
408
+ animation: flip-h 750ms linear;
409
+}
410
+@keyframes flip-h{
411
+ from { transform: rotateY(0deg); }
412
+ to { transform: rotateY(360deg); }
413
+}
414
+body.chat .anim-flip-v {
415
+ animation: flip-v 750ms linear;
416
+}
417
+@keyframes flip-v{
418
+ from { transform: rotateX(0deg); }
419
+ to { transform: rotateX(360deg); }
420
+}
421
+body.chat .anim-fade-in {
422
+ animation: fade-in 750ms linear;
423
+}
424
+body.chat .anim-fade-in-fast {
425
+ animation: fade-in 350ms linear;
426
+}
427
+@keyframes fade-in {
428
+ from { opacity: 0; }
429
+ to { opacity: 1; }
430
+}
431
+body.chat .anim-fade-out-fast {
432
+ animation: fade-out 250ms linear;
433
+}
434
+@keyframes fade-out {
435
+ from { opacity: 1; }
436
+ to { opacity: 0; }
437
+}
362438
--- src/style.chat.css
+++ src/style.chat.css
@@ -9,14 +9,18 @@
9 border: none;
10 display: flex;
11 flex-direction: column;
12 border: none;
13 align-items: flex-start;
 
 
 
 
14 }
15 body.chat.my-messages-right .message-widget.mine {
16 /* Right-aligns a user's own chat messages, similar to how
17 most mobile messaging apps do it. */
18 align-items: flex-end;
19 }
20 body.chat.my-messages-right .message-widget.notification {
21 /* Center-aligns a system-level notification message. */
22 align-items: center;
@@ -77,10 +81,12 @@
77 display: flex;
78 flex-direction: column;
79 align-items: stretch;
80 padding: 0.25em;
81 margin-top: 0.25em;
 
 
82 }
83 /* Full message timestamps. */
84 body.chat .chat-message-popup > span { white-space: nowrap; }
85 /* Container for the message deletion buttons. */
86 body.chat .chat-message-popup > .toolbar {
@@ -132,53 +138,14 @@
132 flex-direction: column;
133 align-items: stretch;
134 padding: 0.25em;
135 z-index: 200;
136 }
137 body.chat .chat-settings-popup > span {
138 vertical-align: middle;
139 }
140 body.chat .chat-settings-popup > span.menu-entry{
141 white-space: nowrap;
142 cursor: pointer;
143 border: 1px solid;
144 border-radius: 0.25em;
145 padding: 0.25em 0.5em;
146 display: flex;
147 flex-direction: row;
148 align-items: center;
149 justify-content: space-between;
150 }
151 body.chat .chat-settings-popup > span.menu-entry:hover {
152 }
153 body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
154 vertical-align: middle;
155 }
156 body.chat .chat-settings-popup > span.menu-entry > span.button {
157 margin: 0.25em 0.2em;
158 padding: 0.25em;
159 flex: 1 1 auto/*eliminates dead no-click zones on the right*/;
160 }
161 body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] {
162 cursor: inherit;
163 }
164 body.chat .chat-settings-popup > select.menu-entry {
165 flex: 1 1 auto;
166 padding: 0;
167 cursor: pointer;
168 min-height: 2.5em;
169 border-radius: 0.25em;
170 }
171 body.chat .chat-settings-popup > select.menu-entry > option {
172 /*Recall that many browsers don't allow styling of OPTION
173 elements, or allow only very limited styling.*/
174 }
175
176 /** Container for the list of /chat messages. */
177 body.chat #chat-messages-wrapper {
178 overflow: auto;
179 flex: 2 1 auto;
180 padding: 0 0.25em;
181 }
182 body.chat #chat-messages-wrapper.loading > * {
183 /* An attempt at reducing flicker when loading lots of messages. */
184 visibility: hidden;
@@ -199,16 +166,14 @@
199 }
200 /* Wrapper for /chat user input controls */
201 body.chat #chat-input-area {
202 display: flex;
203 flex-direction: column;
204 padding: 0.5em 0 0 0;
205 border-bottom: none;
206 border-top: 1px solid black;
207 margin: 0.5em 0 0 0;
208 position: initial /*sticky currently disabled due to scrolling-related issues*/;
209 bottom: 0;
210 }
211 body.chat:not(.chat-only-mode) #chat-input-area{
212 /* Safari user reports that 2em is necessary to keep the file selection
213 widget from overlapping the page footer, whereas a margin of 0 is fine
214 for FF/Chrome (and 2em is a *huge* waste of space for those). */
@@ -234,10 +199,11 @@
234 body.chat #chat-input-line.single-line #chat-edit-buttons {
235 flex-direction: row;
236 }
237 body.chat #chat-edit-buttons > * {
238 flex: 1 1 auto;
 
239 }
240 body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * {
241 max-width: 4em;
242 margin: 0.25em;
243 }
@@ -271,11 +237,11 @@
271 }
272 body.chat #chat-input-file-area > .file-selection-wrapper {
273 align-self: flex-start;
274 margin-right: 0.5em;
275 flex: 0 1 auto;
276 padding: 0.25em 0.25em 0.25em 0;
277 white-space: nowrap;
278 }
279 body.chat #chat-input-file-area .file-selection-wrapper > * {
280 vertical-align: middle;
281 margin: 0;
@@ -303,18 +269,21 @@
303
304 body.chat #chat-drop-details img {
305 max-width: 45%;
306 max-height: 45%;
307 }
308
 
 
 
 
309 body.chat #chat-config,
310 body.chat #chat-preview {
311 /* /chat configuration widget */
312 display: flex;
313 flex-direction: column;
314 overflow: auto;
315 flex: 2 1 auto;
316 padding: 0;
317 margin: 0;
318 align-items: stretch;
319 min-height: 6em;
320 }
@@ -327,12 +296,15 @@
327 }
328 body.chat #chat-config #chat-config-options .menu-entry {
329 display: flex;
330 align-items: center;
331 flex-direction: row;
332 flex-wrap: wrap;
333 padding: 1em;
 
 
 
334 }
335 body.chat #chat-config #chat-config-options .menu-entry > input:first-child {
336 margin-right: 1em;
337 }
338 body.chat #chat-config #chat-config-options .menu-entry > label:first-child {
@@ -357,5 +329,109 @@
357 body.chat #chat-preview #chat-preview-buttons > button {
358 padding: 0.5em;
359 flex: 0 1 auto;
360 margin: 0.25em 0;
361 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
--- src/style.chat.css
+++ src/style.chat.css
@@ -9,14 +9,18 @@
9 border: none;
10 display: flex;
11 flex-direction: column;
12 border: none;
13 align-items: flex-start;
14 }
15 body.chat .message-widget:last-of-type {
16 /* Latest message: reduce bottom gap */
17 margin-bottom: 0.1em;
18 }
19 body.chat.my-messages-right .message-widget.mine {
20 /* Right-aligns a user's own chat messages, similar to how
21 most/some mobile messaging apps do it. */
22 align-items: flex-end;
23 }
24 body.chat.my-messages-right .message-widget.notification {
25 /* Center-aligns a system-level notification message. */
26 align-items: center;
@@ -77,10 +81,12 @@
81 display: flex;
82 flex-direction: column;
83 align-items: stretch;
84 padding: 0.25em;
85 margin-top: 0.25em;
86 border: 1px outset;
87 border-radius: 0.5em;
88 }
89 /* Full message timestamps. */
90 body.chat .chat-message-popup > span { white-space: nowrap; }
91 /* Container for the message deletion buttons. */
92 body.chat .chat-message-popup > .toolbar {
@@ -132,53 +138,14 @@
138 flex-direction: column;
139 align-items: stretch;
140 padding: 0.25em;
141 z-index: 200;
142 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
144 /** Container for the list of /chat messages. */
145 body.chat #chat-messages-wrapper {
146 overflow: auto;
 
147 padding: 0 0.25em;
148 }
149 body.chat #chat-messages-wrapper.loading > * {
150 /* An attempt at reducing flicker when loading lots of messages. */
151 visibility: hidden;
@@ -199,16 +166,14 @@
166 }
167 /* Wrapper for /chat user input controls */
168 body.chat #chat-input-area {
169 display: flex;
170 flex-direction: column;
171 padding: 0;
172 margin: 0;
 
 
173 position: initial /*sticky currently disabled due to scrolling-related issues*/;
174 /*bottom: 0;*/
175 }
176 body.chat:not(.chat-only-mode) #chat-input-area{
177 /* Safari user reports that 2em is necessary to keep the file selection
178 widget from overlapping the page footer, whereas a margin of 0 is fine
179 for FF/Chrome (and 2em is a *huge* waste of space for those). */
@@ -234,10 +199,11 @@
199 body.chat #chat-input-line.single-line #chat-edit-buttons {
200 flex-direction: row;
201 }
202 body.chat #chat-edit-buttons > * {
203 flex: 1 1 auto;
204 padding: initial/*some skins mess this up for buttons*/;
205 }
206 body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * {
207 max-width: 4em;
208 margin: 0.25em;
209 }
@@ -271,11 +237,11 @@
237 }
238 body.chat #chat-input-file-area > .file-selection-wrapper {
239 align-self: flex-start;
240 margin-right: 0.5em;
241 flex: 0 1 auto;
242 padding: 0.25em 0.5em;
243 white-space: nowrap;
244 }
245 body.chat #chat-input-file-area .file-selection-wrapper > * {
246 vertical-align: middle;
247 margin: 0;
@@ -303,18 +269,21 @@
269
270 body.chat #chat-drop-details img {
271 max-width: 45%;
272 max-height: 45%;
273 }
274 body.chat .chat-view {
275 flex: 20 1 auto
276 /*ensure that these grow more than the non-.chat-view elements*/;
277 margin-bottom: 0.2em;
278 }
279 body.chat #chat-config,
280 body.chat #chat-preview {
281 /* /chat configuration widget */
282 display: flex;
283 flex-direction: column;
284 overflow: auto;
 
285 padding: 0;
286 margin: 0;
287 align-items: stretch;
288 min-height: 6em;
289 }
@@ -327,12 +296,15 @@
296 }
297 body.chat #chat-config #chat-config-options .menu-entry {
298 display: flex;
299 align-items: center;
300 flex-direction: row;
301 flex-wrap: nowrap;
302 padding: 1em;
303 }
304 body.chat #chat-config #chat-config-options .menu-entry > label {
305 cursor: pointer;
306 }
307 body.chat #chat-config #chat-config-options .menu-entry > input:first-child {
308 margin-right: 1em;
309 }
310 body.chat #chat-config #chat-config-options .menu-entry > label:first-child {
@@ -357,5 +329,109 @@
329 body.chat #chat-preview #chat-preview-buttons > button {
330 padding: 0.5em;
331 flex: 0 1 auto;
332 margin: 0.25em 0;
333 }
334
335 body.chat #chat-user-list-wrapper {
336 /* Safari can't do fieldsets right, so we emulate one. */
337 border-radius: 0.5em;
338 margin: 1em 0 0.2em 0;
339 padding: 0 0.5em;
340 border-style: inset;
341 border-width: 0 1px 1px 1px/*else collides with the LEGEND*/;
342 }
343 body.chat #chat-user-list-wrapper.collapsed {
344 padding: 0;
345 }
346 body.chat #chat-user-list-wrapper > .legend {
347 font-weight: initial;
348 padding: 0 0.5em 0 0.5em;
349 position: relative;
350 top: -1.75ex/* place it like a fieldset legend */;
351 cursor: pointer;
352 }
353 body.chat #chat-user-list-wrapper > .legend > * {
354 vertical-align: middle;
355 }
356 body.chat #chat-user-list-wrapper > .legend > *:nth-child(2){
357 /* Title label */
358 opacity: 0.6;
359 font-size: 0.8em;
360 }
361 body.chat #chat-user-list-wrapper.collapsed > .legend > *:nth-child(2)::after {
362 content: " (tap to toggle)";
363 }
364 body.chat #chat-user-list-wrapper .help-buttonlet {
365 margin: 0;
366 }
367 body.chat #chat-user-list-wrapper.collapsed #chat-user-list {
368 position: absolute !important;
369 opacity: 0 !important;
370 pointer-events: none !important;
371 display: none !important;
372 }
373 body.chat #chat-user-list {
374 margin-top: -1.25ex;
375 display: flex;
376 flex-direction: row;
377 flex-wrap: wrap;
378 align-items: center;
379 }
380 body.chat #chat-user-list .chat-user {
381 margin: 0.2em;
382 padding: 0.1em 0.5em 0.2em 0.5em;
383 border-radius: 0.5em;
384 cursor: pointer;
385 text-align: center;
386 white-space: pre;
387 }
388 body.chat #chat-user-list .timestamp {
389 font-size: 85%;
390 font-family: monospace;
391 }
392 body.chat #chat-user-list:not(.timestamps) .timestamp {
393 display: none;
394 }
395 body.chat #chat-user-list .chat-user.selected {
396 font-weight: bold;
397 text-decoration: underline;
398 }
399
400 body.chat .anim-rotate-360 {
401 animation: rotate-360 750ms linear;
402 }
403 @keyframes rotate-360 {
404 from { transform: rotate(0deg); }
405 to { transform: rotate(360deg); }
406 }
407 body.chat .anim-flip-h {
408 animation: flip-h 750ms linear;
409 }
410 @keyframes flip-h{
411 from { transform: rotateY(0deg); }
412 to { transform: rotateY(360deg); }
413 }
414 body.chat .anim-flip-v {
415 animation: flip-v 750ms linear;
416 }
417 @keyframes flip-v{
418 from { transform: rotateX(0deg); }
419 to { transform: rotateX(360deg); }
420 }
421 body.chat .anim-fade-in {
422 animation: fade-in 750ms linear;
423 }
424 body.chat .anim-fade-in-fast {
425 animation: fade-in 350ms linear;
426 }
427 @keyframes fade-in {
428 from { opacity: 0; }
429 to { opacity: 1; }
430 }
431 body.chat .anim-fade-out-fast {
432 animation: fade-out 250ms linear;
433 }
434 @keyframes fade-out {
435 from { opacity: 1; }
436 to { opacity: 0; }
437 }
438
+25
--- www/chat.md
+++ www/chat.md
@@ -96,10 +96,35 @@
9696
By default, the list of new-message notification sounds is limited to
9797
a few built in to the fossil binary. In addition, any
9898
[unversioned files](./unvers.wiki) named `alert-sounds/*.{mp3,wav,ogg}`
9999
will be included in that list. To switch sounds, tap the "settings"
100100
button.
101
+
102
+### <a id='connection'></a> Who's Online?
103
+
104
+Because the chat app has to be able to work over transient CGI-based
105
+connections, as opposed to a stable socket connection to the server,
106
+real-time tracking of "who's online" is not feasible. As of version
107
+2.17, chat offers an optional feature, toggleable in the settings,
108
+which can list users who have posted messages in the client's current
109
+list of loaded messages. This is not the same thing as tracking who's
110
+online, but it gives an overview of which users have been active most
111
+recently, noting that "lurkers" (people who post no messages) will not
112
+show up in that list, nor does the chat infrastructure have a way to
113
+track and present those. That list can be used to filter messages on a
114
+specific user by tapping on that user's name, tapping a second time to
115
+remove the filter.
116
+
117
+Sidebar: message deletion is a type of message and deletions count
118
+towards updates in the recent activity list (counted for the person
119
+who performed the deletion, not the author of the deleted
120
+comment). That can potentially lead to odd corner cases where a user
121
+shows up in the list but has no messages which are currently visible
122
+because they were deleted, or an admin user who has not posted
123
+anything but deleted a message. That is a known minor cosmetic-only
124
+bug with a resolution of "will not fix."
125
+
101126
102127
## Implementation Details
103128
104129
*You do not need to understand how Fossil chat works in order to use it.
105130
But many developers prefer to know how their tools work.
106131
--- www/chat.md
+++ www/chat.md
@@ -96,10 +96,35 @@
96 By default, the list of new-message notification sounds is limited to
97 a few built in to the fossil binary. In addition, any
98 [unversioned files](./unvers.wiki) named `alert-sounds/*.{mp3,wav,ogg}`
99 will be included in that list. To switch sounds, tap the "settings"
100 button.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
102 ## Implementation Details
103
104 *You do not need to understand how Fossil chat works in order to use it.
105 But many developers prefer to know how their tools work.
106
--- www/chat.md
+++ www/chat.md
@@ -96,10 +96,35 @@
96 By default, the list of new-message notification sounds is limited to
97 a few built in to the fossil binary. In addition, any
98 [unversioned files](./unvers.wiki) named `alert-sounds/*.{mp3,wav,ogg}`
99 will be included in that list. To switch sounds, tap the "settings"
100 button.
101
102 ### <a id='connection'></a> Who's Online?
103
104 Because the chat app has to be able to work over transient CGI-based
105 connections, as opposed to a stable socket connection to the server,
106 real-time tracking of "who's online" is not feasible. As of version
107 2.17, chat offers an optional feature, toggleable in the settings,
108 which can list users who have posted messages in the client's current
109 list of loaded messages. This is not the same thing as tracking who's
110 online, but it gives an overview of which users have been active most
111 recently, noting that "lurkers" (people who post no messages) will not
112 show up in that list, nor does the chat infrastructure have a way to
113 track and present those. That list can be used to filter messages on a
114 specific user by tapping on that user's name, tapping a second time to
115 remove the filter.
116
117 Sidebar: message deletion is a type of message and deletions count
118 towards updates in the recent activity list (counted for the person
119 who performed the deletion, not the author of the deleted
120 comment). That can potentially lead to odd corner cases where a user
121 shows up in the list but has no messages which are currently visible
122 because they were deleted, or an admin user who has not posted
123 anything but deleted a message. That is a known minor cosmetic-only
124 bug with a resolution of "will not fix."
125
126
127 ## Implementation Details
128
129 *You do not need to understand how Fossil chat works in order to use it.
130 But many developers prefer to know how their tools work.
131

Keyboard Shortcuts

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