Fossil SCM

Refactor and improve the input logic for /chat.

drh 2021-10-04 09:54 trunk merge
Commit 469f1b0dfc7e3ad82e955a954b2fa3690410d20afa4143b8e0b120dcb2d6f3ab
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -1155,12 +1155,12 @@
11551155
}
11561156
11571157
body.chat div.header, body.chat div.footer,
11581158
body.chat div.mainmenu, body.chat div.submenu,
11591159
body.chat div.content {
1160
- margin-left: auto;
1161
- margin-right: auto;
1160
+ margin-left: 0.5em;
1161
+ margin-right: 0.5em;
11621162
margin-top: auto/*eliminates unnecessary scrollbars*/;
11631163
}
11641164
body.chat.chat-only-mode div.content {
11651165
max-width: revert;
11661166
}
11671167
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -1155,12 +1155,12 @@
1155 }
1156
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
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -1155,12 +1155,12 @@
1155 }
1156
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: 0.5em;
1161 margin-right: 0.5em;
1162 margin-top: auto/*eliminates unnecessary scrollbars*/;
1163 }
1164 body.chat.chat-only-mode div.content {
1165 max-width: revert;
1166 }
1167
+25 -16
--- src/chat.c
+++ src/chat.c
@@ -142,15 +142,29 @@
142142
** send new chat message, delete older messages, or poll for changes.
143143
*/
144144
void chat_webpage(void){
145145
char *zAlert;
146146
char *zProjectName;
147
- const char * zInputPlaceholder1 = /* Placeholder for 1-line input */
148
- "Enter sends and Shift-Enter previews.";
149
- const char * zInputPlaceholder2 = /* Placeholder for textarea input*/
150
- "Ctrl-Enter sends and Shift-Enter previews.";
151147
char * zInputPlaceholder0; /* Common text input placeholder value */
148
+ const char *zPaperclip =
149
+ "<svg height=\"8.0\" width=\"16.0\"><path "
150
+ "stroke=\"rgb(100,100,100)\" "
151
+ "d=\"M 15.93452,3.2530441 "
152
+ "A 4.1499493,4.1265346 0 0 0 11.804809,6.5256284e-4 H 2.8582923 A "
153
+ "2.8239899,2.8080565 0 0 0 0.68965668,0.96142476 2.874599,2.8583801 "
154
+ "0 0 0 0.03119302,3.2388108 2.7632589,2.7476682 0 0 0 "
155
+ "0.81132923,4.7689293 3.168132,3.1502569 0 0 0 3.0300653,5.66565 l "
156
+ "7.7297897,-4e-7 a 1.6802234,1.6707433 0 0 0 0.0072,-3.3377933 H "
157
+ "5.6138192 v 1.0105899 l 5.1460358,-0.00712 a 0.66804062,0.66427143 "
158
+ "0 0 1 0,1.3237305 l -7.7226325,0.00712 A 2.0243655,2.0129437 0 0 1 "
159
+ "1.0332029,3.0964741 1.8522944,1.8418435 0 0 1 2.8511351,1.0041257 h "
160
+ "8.9465169 a 3.1478884,3.1301275 0 0 1 3.134859,2.4339559 3.0365483,"
161
+ "3.0194156 0 0 1 -0.629835,2.4908908 3.0365483,3.0194156 0 0 1 "
162
+ "-2.31178,1.0746415 l -7.5437026,-0.014233 -0.00716,1.0034736 "
163
+ "7.5365456,0.00715 a 4.048731,4.0258875 0 0 0 3.957938,-4.7469259 z\""
164
+ "/></svg>";
165
+
152166
login_check_credentials();
153167
if( !g.perm.Chat ){
154168
login_needed(g.anon.Chat);
155169
return;
156170
}
@@ -161,32 +175,27 @@
161175
mprintf("Type markdown-formatted message for %h.", zProjectName);
162176
style_set_current_feature("chat");
163177
style_header("Chat");
164178
@ <div id='chat-input-area'>
165179
@ <div id='chat-input-line' class='single-line'>
166
- @ <input type="text" name="msg" id="chat-input-single" \
167
- @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder1)" \
168
- @ autocomplete="off">
169
- @ <textarea rows="8" id="chat-input-multi" \
170
- @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder2)" \
171
- @ class="hidden"></textarea>
180
+ @ <div contenteditable id="chat-input-field" \
181
+ @ data-placeholder0="%h(zInputPlaceholder0)" \
182
+ @ data-placeholder="%h(zInputPlaceholder0)" \
183
+ @ class=""></div>
172184
@ <div id='chat-edit-buttons'>
173185
@ <button id="chat-preview-button" \
174186
@ title="Preview message (Shift-Enter)">&#128065;</button>
187
+ @ <button id="chat-message-attach" \
188
+ @ title="Attach file to message">%s(zPaperclip)</button>
175189
@ <button id="chat-settings-button" \
176190
@ title="Configure chat">&#9881;</button>
177191
@ <button id="chat-message-submit" \
178192
@ title="Send message (Ctrl-Enter)">&#128228;</button>
179193
@ </div>
180194
@ </div>
181195
@ <div id='chat-input-file-area'>
182
- @ <div class='file-selection-wrapper'>
183
- @ <div class='help-buttonlet'>
184
- @ Select a file to upload, drag/drop a file into this spot,
185
- @ or paste an image from the clipboard if supported by
186
- @ your environment.
187
- @ </div>
196
+ @ <div class='file-selection-wrapper hidden'>
188197
@ <input type="file" name="file" id="chat-input-file">
189198
@ </div>
190199
@ <div id="chat-drop-details"></div>
191200
@ </div>
192201
@ </div>
193202
--- src/chat.c
+++ src/chat.c
@@ -142,15 +142,29 @@
142 ** send new chat message, delete older messages, or poll for changes.
143 */
144 void chat_webpage(void){
145 char *zAlert;
146 char *zProjectName;
147 const char * zInputPlaceholder1 = /* Placeholder for 1-line input */
148 "Enter sends and Shift-Enter previews.";
149 const char * zInputPlaceholder2 = /* Placeholder for textarea input*/
150 "Ctrl-Enter sends and Shift-Enter previews.";
151 char * zInputPlaceholder0; /* Common text input placeholder value */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152 login_check_credentials();
153 if( !g.perm.Chat ){
154 login_needed(g.anon.Chat);
155 return;
156 }
@@ -161,32 +175,27 @@
161 mprintf("Type markdown-formatted message for %h.", zProjectName);
162 style_set_current_feature("chat");
163 style_header("Chat");
164 @ <div id='chat-input-area'>
165 @ <div id='chat-input-line' class='single-line'>
166 @ <input type="text" name="msg" id="chat-input-single" \
167 @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder1)" \
168 @ autocomplete="off">
169 @ <textarea rows="8" id="chat-input-multi" \
170 @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder2)" \
171 @ class="hidden"></textarea>
172 @ <div id='chat-edit-buttons'>
173 @ <button id="chat-preview-button" \
174 @ title="Preview message (Shift-Enter)">&#128065;</button>
 
 
175 @ <button id="chat-settings-button" \
176 @ title="Configure chat">&#9881;</button>
177 @ <button id="chat-message-submit" \
178 @ title="Send message (Ctrl-Enter)">&#128228;</button>
179 @ </div>
180 @ </div>
181 @ <div id='chat-input-file-area'>
182 @ <div class='file-selection-wrapper'>
183 @ <div class='help-buttonlet'>
184 @ Select a file to upload, drag/drop a file into this spot,
185 @ or paste an image from the clipboard if supported by
186 @ your environment.
187 @ </div>
188 @ <input type="file" name="file" id="chat-input-file">
189 @ </div>
190 @ <div id="chat-drop-details"></div>
191 @ </div>
192 @ </div>
193
--- src/chat.c
+++ src/chat.c
@@ -142,15 +142,29 @@
142 ** send new chat message, delete older messages, or poll for changes.
143 */
144 void chat_webpage(void){
145 char *zAlert;
146 char *zProjectName;
 
 
 
 
147 char * zInputPlaceholder0; /* Common text input placeholder value */
148 const char *zPaperclip =
149 "<svg height=\"8.0\" width=\"16.0\"><path "
150 "stroke=\"rgb(100,100,100)\" "
151 "d=\"M 15.93452,3.2530441 "
152 "A 4.1499493,4.1265346 0 0 0 11.804809,6.5256284e-4 H 2.8582923 A "
153 "2.8239899,2.8080565 0 0 0 0.68965668,0.96142476 2.874599,2.8583801 "
154 "0 0 0 0.03119302,3.2388108 2.7632589,2.7476682 0 0 0 "
155 "0.81132923,4.7689293 3.168132,3.1502569 0 0 0 3.0300653,5.66565 l "
156 "7.7297897,-4e-7 a 1.6802234,1.6707433 0 0 0 0.0072,-3.3377933 H "
157 "5.6138192 v 1.0105899 l 5.1460358,-0.00712 a 0.66804062,0.66427143 "
158 "0 0 1 0,1.3237305 l -7.7226325,0.00712 A 2.0243655,2.0129437 0 0 1 "
159 "1.0332029,3.0964741 1.8522944,1.8418435 0 0 1 2.8511351,1.0041257 h "
160 "8.9465169 a 3.1478884,3.1301275 0 0 1 3.134859,2.4339559 3.0365483,"
161 "3.0194156 0 0 1 -0.629835,2.4908908 3.0365483,3.0194156 0 0 1 "
162 "-2.31178,1.0746415 l -7.5437026,-0.014233 -0.00716,1.0034736 "
163 "7.5365456,0.00715 a 4.048731,4.0258875 0 0 0 3.957938,-4.7469259 z\""
164 "/></svg>";
165
166 login_check_credentials();
167 if( !g.perm.Chat ){
168 login_needed(g.anon.Chat);
169 return;
170 }
@@ -161,32 +175,27 @@
175 mprintf("Type markdown-formatted message for %h.", zProjectName);
176 style_set_current_feature("chat");
177 style_header("Chat");
178 @ <div id='chat-input-area'>
179 @ <div id='chat-input-line' class='single-line'>
180 @ <div contenteditable id="chat-input-field" \
181 @ data-placeholder0="%h(zInputPlaceholder0)" \
182 @ data-placeholder="%h(zInputPlaceholder0)" \
183 @ class=""></div>
 
 
184 @ <div id='chat-edit-buttons'>
185 @ <button id="chat-preview-button" \
186 @ title="Preview message (Shift-Enter)">&#128065;</button>
187 @ <button id="chat-message-attach" \
188 @ title="Attach file to message">%s(zPaperclip)</button>
189 @ <button id="chat-settings-button" \
190 @ title="Configure chat">&#9881;</button>
191 @ <button id="chat-message-submit" \
192 @ title="Send message (Ctrl-Enter)">&#128228;</button>
193 @ </div>
194 @ </div>
195 @ <div id='chat-input-file-area'>
196 @ <div class='file-selection-wrapper hidden'>
 
 
 
 
 
197 @ <input type="file" name="file" id="chat-input-file">
198 @ </div>
199 @ <div id="chat-drop-details"></div>
200 @ </div>
201 @ </div>
202
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -127,13 +127,12 @@
127127
inputWrapper: E1("#chat-input-area"),
128128
inputLine: E1('#chat-input-line'),
129129
fileSelectWrapper: E1('#chat-input-file-area'),
130130
viewMessages: E1('#chat-messages-wrapper'),
131131
btnSubmit: E1('#chat-message-submit'),
132
- inputSingle: E1('#chat-input-single'),
133
- inputMulti: E1('#chat-input-multi'),
134
- inputCurrent: undefined/*one of inputSingle or inputMulti*/,
132
+ btnAttach: E1('#chat-message-attach'),
133
+ inputField: E1('#chat-input-field'),
135134
inputFile: E1('#chat-input-file'),
136135
contentDiv: E1('div.content'),
137136
viewConfig: E1('#chat-config'),
138137
viewPreview: E1('#chat-preview'),
139138
previewContent: E1('#chat-preview-content'),
@@ -169,56 +168,23 @@
169168
taking into account single- vs multi-line input. The getter returns
170169
a string and the setter returns this object. */
171170
inputValue: function(){
172171
const e = this.inputElement();
173172
if(arguments.length){
174
- e.value = arguments[0];
173
+ e.innerText = arguments[0];
175174
return this;
176175
}
177
- return e.value;
176
+ return e.innerText;
178177
},
179178
/** Asks the current user input field to take focus. Returns this. */
180179
inputFocus: function(){
181180
this.inputElement().focus();
182181
return this;
183182
},
184183
/** Returns the current message input element. */
185184
inputElement: function(){
186
- return this.e.inputCurrent;
187
- },
188
- /** Toggles between single- and multi-line edit modes. Returns this. */
189
- inputToggleSingleMulti: function(){
190
- const old = this.e.inputCurrent;
191
- if(this.e.inputCurrent === this.e.inputSingle){
192
- this.e.inputCurrent = this.e.inputMulti;
193
- this.e.inputLine.classList.remove('single-line');
194
- }else{
195
- this.e.inputCurrent = this.e.inputSingle;
196
- this.e.inputLine.classList.add('single-line');
197
- }
198
- const m = this.e.viewMessages,
199
- sTop = m.scrollTop,
200
- mh1 = m.clientHeight;
201
- D.addClass(old, 'hidden');
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
- return this;
208
- },
209
- /**
210
- If passed true or no arguments, switches to multi-line mode
211
- if currently in single-line mode. If passed false, switches
212
- to single-line mode if currently in multi-line mode. Returns
213
- this.
214
- */
215
- inputMultilineMode: function(yes){
216
- if(!arguments.length) yes = true;
217
- if(yes && this.e.inputCurrent === this.e.inputMulti) return this;
218
- else if(!yes && this.e.inputCurrent === this.e.inputSingle) return this;
219
- else return this.inputToggleSingleMulti();
185
+ return this.e.inputField;
220186
},
221187
/** Enables (if yes is truthy) or disables all elements in
222188
* this.disableDuringAjax. */
223189
enableAjaxComponents: function(yes){
224190
D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -392,26 +358,62 @@
392358
return e ? overlapsElemView(e, this.e.viewMessages) : false;
393359
},
394360
settings:{
395361
get: (k,dflt)=>F.storage.get(k,dflt),
396362
getBool: (k,dflt)=>F.storage.getBool(k,dflt),
397
- set: (k,v)=>F.storage.set(k,v),
363
+ set: function(k,v){
364
+ F.storage.set(k,v);
365
+ F.page.dispatchEvent('chat-setting',{key: k, value: v});
366
+ },
398367
/* Toggles the boolean setting specified by k. Returns the
399368
new value.*/
400369
toggle: function(k){
401370
const v = this.getBool(k);
402371
this.set(k, !v);
403372
return !v;
404373
},
374
+ addListener: function(setting, f){
375
+ F.page.addEventListener('chat-setting', function(ev){
376
+ if(ev.detail.key===setting) f(ev.detail);
377
+ }, false);
378
+ },
379
+ /* Default values of settings. These are used for intializing
380
+ the setting event listeners and config view UI. */
405381
defaults:{
382
+ /* When on, inbound images are displayed inlined, else as a
383
+ link to download the image. */
406384
"images-inline": !!F.config.chat.imagesInline,
407
- "edit-multiline": false,
408
- "monospace-messages": false,
385
+ /* When on, ctrl-enter sends messages, else enter and
386
+ ctrl-enter both send them. */
387
+ "edit-ctrl-send": false,
388
+ /* When on, the edit field starts as a single line and
389
+ expands as the user types, and the relevant buttons are
390
+ laid out in a compact form. When off, the edit field and
391
+ buttons are larger. */
392
+ "edit-compact-mode": true,
393
+ /* When on, sets the font-family on messages and the edit
394
+ field to monospace. */
395
+ "monospace-messages": true,
396
+ /* When on, non-chat UI elements (page header/footer) are
397
+ hidden */
409398
"chat-only-mode": false,
399
+ /* When set to a URI, it is assumed to be an audio file,
400
+ which gets played when new messages arrive. When true,
401
+ the first entry in the audio file selection list will be
402
+ used. */
410403
"audible-alert": true,
404
+ /* When on, show the list of "active" users - those from
405
+ whom we have messages in the currently-loaded history
406
+ (noting that deletions are also messages). */
411407
"active-user-list": false,
412
- "active-user-list-timestamps": false
408
+ /* When on, the [active-user-list] setting includes the
409
+ timestamp of each user's most recent message. */
410
+ "active-user-list-timestamps": false,
411
+ /* When on, the [audible-alert] is played for one's own
412
+ messages, else it is only played for other users'
413
+ messages. */
414
+ "alert-own-messages": false
413415
}
414416
},
415417
/** Plays a new-message notification sound IF the audible-alert
416418
setting is true, else this is a no-op. Returns this.
417419
*/
@@ -420,11 +422,11 @@
420422
try{
421423
if(!f.audio) f.audio = new Audio(f.uri);
422424
f.audio.currentTime = 0;
423425
f.audio.play();
424426
}catch(e){
425
- console.error("Audio playblack failed.",e);
427
+ console.error("Audio playblack failed.", f.uri, e);
426428
}
427429
}
428430
return this;
429431
},
430432
/**
@@ -449,11 +451,13 @@
449451
return e;
450452
}
451453
this.e.views.forEach(function(E){
452454
if(e!==E) D.addClass(E,'hidden');
453455
});
454
- this.e.currentView = D.removeClass(e,'hidden');
456
+ this.e.currentView = e;
457
+ if(this.e.currentView.$beforeShow) this.e.currentView.$beforeShow();
458
+ D.removeClass(e,'hidden');
455459
this.animate(this.e.currentView, 'anim-fade-in-fast');
456460
return this.e.currentView;
457461
},
458462
/**
459463
Updates the "active user list" view if we are not currently
@@ -499,10 +503,35 @@
499503
Object.keys(this.usersLastSeen).sort(
500504
callee.sortUsersSeen
501505
).forEach(callee.addUserElem);
502506
return this;
503507
},
508
+ /** Show or hide the active user list. Returns this object. */
509
+ showActiveUserList: function(yes){
510
+ if(0===arguments.length) yes = true;
511
+ this.e.activeUserListWrapper.classList[
512
+ yes ? 'remove' : 'add'
513
+ ]('hidden');
514
+ D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
515
+ if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
516
+ /* When hiding this element, undo all filtering */
517
+ Chat.setUserFilter(false);
518
+ /*Ideally we'd scroll the final message into view
519
+ now, but because viewMessages is currently hidden behind
520
+ viewConfig, scrolling is a no-op. */
521
+ Chat.scrollMessagesTo(1);
522
+ }else{
523
+ Chat.updateActiveUserList();
524
+ Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
525
+ }
526
+ return this;
527
+ },
528
+ showActiveUserTimestamps: function(yes){
529
+ if(0===arguments.length) yes = true;
530
+ this.e.activeUserList.classList[yes ? 'add' : 'remove']('timestamps');
531
+ return this;
532
+ },
504533
/**
505534
Applies user name filter to all current messages, or clears
506535
the filter if uname is falsy.
507536
*/
508537
setUserFilter: function(uname){
@@ -542,38 +571,18 @@
542571
D.addClassBriefly(e, a, 0, cb);
543572
}
544573
return this;
545574
}
546575
};
576
+ if(!D.attr(cs.e.inputField,'contenteditable','plaintext-only').isContentEditable){
577
+ /* Only the Chrome family supports contenteditable=plaintext-only,
578
+ but Chrome is the only engine for which we need this flag: */
579
+ D.attr(cs.e.inputField,'contenteditable','true');
580
+ }
547581
cs.animate.$disabled = true;
548582
F.fetch.beforesend = ()=>cs.ajaxStart();
549583
F.fetch.aftersend = ()=>cs.ajaxEnd();
550
- cs.e.inputCurrent = cs.e.inputSingle;
551
- /* Install default settings... */
552
- Object.keys(cs.settings.defaults).forEach(function(k){
553
- const v = cs.settings.get(k,cs);
554
- if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
555
- });
556
- if(window.innerWidth<window.innerHeight){
557
- /* Alignment of 'my' messages: right alignment is conventional
558
- for mobile chat apps but can be difficult to read in wide
559
- windows (desktop/tablet landscape mode), so we default to a
560
- layout based on the apparent "orientation" of the window:
561
- tall vs wide. Can be toggled via settings popup. */
562
- document.body.classList.add('my-messages-right');
563
- }
564
- if(cs.settings.getBool('monospace-messages',false)){
565
- document.body.classList.add('monospace-messages');
566
- }
567
- if(cs.settings.getBool('active-user-list',false)){
568
- cs.e.activeUserListWrapper.classList.remove('hidden');
569
- }
570
- if(cs.settings.getBool('active-user-list-timestamps',false)){
571
- cs.e.activeUserList.classList.add('timestamps');
572
- }
573
- cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
574
- cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
575584
cs.pageTitleOrig = cs.e.pageTitle.innerText;
576585
const qs = (e)=>document.querySelector(e);
577586
const argsToArray = function(args){
578587
return Array.prototype.slice.call(args,0);
579588
};
@@ -1054,11 +1063,11 @@
10541063
}/*_handleLegendClicked()*/
10551064
};
10561065
return cf;
10571066
})()/*MessageWidget*/;
10581067
1059
- const BlobXferState = (function(){/*drag/drop bits...*/
1068
+ const BlobXferState = (function(){
10601069
/* State for paste and drag/drop */
10611070
const bxs = {
10621071
dropDetails: document.querySelector('#chat-drop-details'),
10631072
blob: undefined,
10641073
clear: function(){
@@ -1069,75 +1078,83 @@
10691078
};
10701079
/** Updates the paste/drop zone with details of the pasted/dropped
10711080
data. The argument must be a Blob or Blob-like object (File) or
10721081
it can be falsy to reset/clear that state.*/
10731082
const updateDropZoneContent = function(blob){
1083
+ //console.debug("updateDropZoneContent()",blob);
10741084
const dd = bxs.dropDetails;
10751085
bxs.blob = blob;
10761086
D.clearElement(dd);
10771087
if(!blob){
10781088
Chat.e.inputFile.value = '';
10791089
return;
10801090
}
1081
- D.append(dd, "Name: ", blob.name,
1091
+ D.append(dd, "Attached: ", blob.name,
10821092
D.br(), "Size: ",blob.size);
1083
- if(blob.type && blob.type.startsWith("image/")){
1093
+ const btn = D.button("Cancel");
1094
+ D.append(dd, D.br(), btn);
1095
+ btn.addEventListener('click', ()=>updateDropZoneContent(), false);
1096
+ if(blob.type && (blob.type.startsWith("image/") || blob.type==='BITMAP')){
10841097
const img = D.img();
10851098
D.append(dd, D.br(), img);
10861099
const reader = new FileReader();
10871100
reader.onload = (e)=>img.setAttribute('src', e.target.result);
10881101
reader.readAsDataURL(blob);
10891102
}
1090
- const btn = D.button("Cancel");
1091
- D.append(dd, D.br(), btn);
1092
- btn.addEventListener('click', ()=>updateDropZoneContent(), false);
10931103
};
10941104
Chat.e.inputFile.addEventListener('change', function(ev){
10951105
updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
10961106
});
10971107
/* Handle image paste from clipboard. TODO: figure out how we can
10981108
paste non-image binary data as if it had been selected via the
10991109
file selection element. */
1100
- document.addEventListener('paste', function(event){
1110
+ const pasteListener = function(event){
11011111
const items = event.clipboardData.items,
11021112
item = items[0];
1103
- if(!item || !item.type) return;
1104
- else if('file'===item.kind){
1113
+ //console.debug("paste event",event.target,item,event);
1114
+ //console.debug("paste event item",item);
1115
+ if(item && item.type && ('file'===item.kind || 'BITMAP'===item.type)){
11051116
updateDropZoneContent(false/*clear prev state*/);
11061117
updateDropZoneContent(item.getAsFile());
1107
- }
1108
- }, false);
1109
- /* Add help button for drag/drop/paste zone */
1110
- Chat.e.inputFile.parentNode.insertBefore(
1111
- F.helpButtonlets.create(
1112
- Chat.e.fileSelectWrapper.querySelector('.help-buttonlet')
1113
- ), Chat.e.inputFile
1114
- );
1115
- ////////////////////////////////////////////////////////////
1116
- // File drag/drop visual notification.
1117
- const dropHighlight = Chat.e.inputFile /* target zone */;
1118
- const dropEvents = {
1119
- drop: function(ev){
1120
- D.removeClass(dropHighlight, 'dragover');
1121
- },
1122
- dragenter: function(ev){
1118
+ event.stopPropagation();
1119
+ event.preventDefault(true);
1120
+ return false;
1121
+ }
1122
+ /* else continue propagating */
1123
+ };
1124
+ document.addEventListener('paste', pasteListener, true);
1125
+ if(0){
1126
+ const onPastePlainText = function(ev){
1127
+ var pastedText = undefined;
1128
+ if (window.clipboardData && window.clipboardData.getData) { // IE
1129
+ pastedText = window.clipboardData.getData('Text');
1130
+ }else if (ev.clipboardData && ev.clipboardData.getData) {
1131
+ pastedText = ev.clipboardData.getData('text/plain');
1132
+ }
1133
+ ev.target.textContent += pastedText;
11231134
ev.preventDefault();
1124
- ev.dataTransfer.dropEffect = "copy";
1125
- D.addClass(dropHighlight, 'dragover');
1126
- },
1127
- dragleave: function(ev){
1128
- D.removeClass(dropHighlight, 'dragover');
1129
- },
1130
- dragend: function(ev){
1131
- D.removeClass(dropHighlight, 'dragover');
1132
- }
1135
+ return false;
1136
+ };
1137
+ Chat.e.inputField.addEventListener('paste', onPastePlainText, false);
1138
+ }
1139
+ const noDragDropEvents = function(ev){
1140
+ /* contenteditable tries to do its own thing with dropped data,
1141
+ which is not compatible with how we use it, so... */
1142
+ ev.dataTransfer.effectAllowed = 'none';
1143
+ ev.dataTransfer.dropEffect = 'none';
1144
+ ev.preventDefault();
1145
+ ev.stopPropagation();
1146
+ return false;
11331147
};
1134
- Object.keys(dropEvents).forEach(
1135
- (k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true)
1148
+
1149
+ ['drop','dragenter','dragleave','dragend'].forEach(
1150
+ (k)=>{
1151
+ Chat.inputElement().addEventListener(k, noDragDropEvents, false);
1152
+ }
11361153
);
11371154
return bxs;
1138
- })()/*drag/drop*/;
1155
+ })()/*drag/drop/paste*/;
11391156
11401157
const tzOffsetToString = function(off){
11411158
const hours = Math.round(off/60), min = Math.round(off % 30);
11421159
return ''+(hours + (min ? '.5' : ''));
11431160
};
@@ -1155,21 +1172,30 @@
11551172
empty, this is a no-op.
11561173
*/
11571174
Chat.submitMessage = function f(){
11581175
if(!f.spaces){
11591176
f.spaces = /\s+$/;
1177
+ f.markdownContinuation = /\\\s+$/;
11601178
}
11611179
this.setCurrentView(this.e.viewMessages);
11621180
const fd = new FormData();
11631181
var msg = this.inputValue().trim();
11641182
if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
11651183
/* Cosmetic: trim whitespace from the ends of lines to try to
11661184
keep copy/paste from terminals, especially wide ones, from
1167
- forcing a horizontal scrollbar on all clients. */
1185
+ forcing a horizontal scrollbar on all clients. This breaks
1186
+ markdown's use of blackslash-space-space for paragraph
1187
+ continuation, but *not* doing this affects all clients every
1188
+ time someone pastes in console copy/paste from an affected
1189
+ platform. We seem to have narrowed to the console pasting
1190
+ problem to users of tmux. Most consoles don't behave
1191
+ that way. */
11681192
const xmsg = msg.split('\n');
11691193
xmsg.forEach(function(line,ndx){
1170
- xmsg[ndx] = line.trimRight();
1194
+ if(!f.markdownContinuation.test(line)){
1195
+ xmsg[ndx] = line.trimRight();
1196
+ }
11711197
});
11721198
msg = xmsg.join('\n');
11731199
}
11741200
if(msg) fd.set('msg',msg);
11751201
const file = BlobXferState.blob || this.e.inputFile.files[0];
@@ -1194,47 +1220,90 @@
11941220
});
11951221
BlobXferState.clear();
11961222
Chat.inputValue("").inputFocus();
11971223
};
11981224
1199
- const inputWidgetKeydown = function(ev){
1200
- if(13 === ev.keyCode){
1201
- if(ev.shiftKey){
1202
- ev.preventDefault();
1203
- ev.stopPropagation();
1204
- /* Shift-enter will run preview mode UNLESS preview mode is
1205
- active AND the input field is empty, in which case it will
1206
- switch back to message view. */
1207
- if(Chat.e.currentView===Chat.e.viewPreview
1208
- && !Chat.e.inputCurrent.value){
1209
- Chat.setCurrentView(Chat.e.viewMessages);
1210
- }else{
1211
- Chat.e.btnPreview.click();
1212
- }
1213
- return false;
1214
- }else if((Chat.e.inputSingle===ev.target)
1215
- || (ev.ctrlKey && Chat.e.inputMulti===ev.target)){
1216
- /* ^^^ note that it is intended that both ctrl-enter and enter
1217
- work for single-line input mode. */
1218
- ev.preventDefault();
1219
- ev.stopPropagation();
1220
- Chat.submitMessage();
1221
- return false;
1222
- }
1225
+ const inputWidgetKeydown = function f(ev){
1226
+ if(!f.$toggleCtrl){
1227
+ f.$toggleCtrl = function(currentMode){
1228
+ currentMode = !currentMode;
1229
+ Chat.settings.set('edit-ctrl-send', currentMode);
1230
+ };
1231
+ f.$toggleCompact = function(currentMode){
1232
+ currentMode = !currentMode;
1233
+ Chat.settings.set('edit-compact-mode', currentMode);
1234
+ };
1235
+ }
1236
+ if(13 !== ev.keyCode) return;
1237
+ const text = Chat.inputValue().trim();
1238
+ const ctrlMode = Chat.settings.getBool('edit-ctrl-send', false);
1239
+ //console.debug("Enter key event:", ctrlMode, ev.ctrlKey, ev.shiftKey, ev);
1240
+ if(ev.shiftKey){
1241
+ const compactMode = Chat.settings.getBool('edit-compact-mode', false);
1242
+ ev.preventDefault();
1243
+ ev.stopPropagation();
1244
+ /* Shift-enter will run preview mode UNLESS preview mode is
1245
+ active AND the input field is empty, in which case it will
1246
+ switch back to message view. */
1247
+ if(Chat.e.currentView===Chat.e.viewPreview && !text){
1248
+ Chat.setCurrentView(Chat.e.viewMessages);
1249
+ }else if(!text){
1250
+ f.$toggleCompact(compactMode);
1251
+ }else{
1252
+ Chat.e.btnPreview.click();
1253
+ }
1254
+ return false;
1255
+ }
1256
+ if(ev.ctrlKey && !text && !BlobXferState.blob){
1257
+ /* Ctrl-enter on empty input field(s) toggles Enter/Ctrl-enter mode */
1258
+ ev.preventDefault();
1259
+ ev.stopPropagation();
1260
+ f.$toggleCtrl(ctrlMode);
1261
+ return false;
1262
+ }
1263
+ if(!ctrlMode && ev.ctrlKey && text){
1264
+ //console.debug("!ctrlMode && ev.ctrlKey && text.");
1265
+ /* Ctrl-enter in Enter-sends mode SHOULD, with this logic add a
1266
+ newline, but that is not happening, for unknown reasons
1267
+ (possibly related to this element being a conteneditable DIV
1268
+ instead of a textarea). Forcibly appending a newline do the
1269
+ input area does not work, also for unknown reasons, and would
1270
+ only be suitable when we're at the end of the input.
1271
+
1272
+ Strangely, this approach DOES work for shift-enter, but we
1273
+ need shift-enter as a hotkey for preview mode.
1274
+ */
1275
+ //return;
1276
+ // return here "should" cause newline to be added, but that doesn't work
1277
+ }
1278
+ if((!ctrlMode && !ev.ctrlKey) || (ev.ctrlKey/* && ctrlMode*/)){
1279
+ /* Ship it! */
1280
+ ev.preventDefault();
1281
+ ev.stopPropagation();
1282
+ Chat.submitMessage();
1283
+ return false;
12231284
}
12241285
};
1225
- Chat.e.inputSingle
1226
- .addEventListener('keydown', inputWidgetKeydown, false);
1227
- Chat.e.inputMulti
1228
- .addEventListener('keydown', inputWidgetKeydown, false);
1286
+ Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false);
12291287
Chat.e.btnSubmit.addEventListener('click',(e)=>{
12301288
e.preventDefault();
12311289
Chat.submitMessage();
12321290
return false;
12331291
});
1292
+ Chat.e.btnAttach.addEventListener(
1293
+ 'click', ()=>Chat.e.inputFile.click(), false);
12341294
1235
- (function(){/*Set up #chat-settings-button */
1295
+ (function(){/*Set up #chat-settings-button and related bits */
1296
+ if(window.innerWidth<window.innerHeight){
1297
+ // Must be set up before config view is...
1298
+ /* Alignment of 'my' messages: right alignment is conventional
1299
+ for mobile chat apps but can be difficult to read in wide
1300
+ windows (desktop/tablet landscape mode), so we default to a
1301
+ layout based on the apparent "orientation" of the window:
1302
+ tall vs wide. Can be toggled via settings. */
1303
+ document.body.classList.add('my-messages-right');
1304
+ }
12361305
const settingsButton = document.querySelector('#chat-settings-button');
12371306
const optionsMenu = E1('#chat-config-options');
12381307
const cbToggle = function(ev){
12391308
ev.preventDefault();
12401309
ev.stopPropagation();
@@ -1248,27 +1317,12 @@
12481317
/** Internal acrobatics to allow certain settings toggles to access
12491318
related toggles. */
12501319
const namedOptions = {
12511320
activeUsers:{
12521321
label: "Show active users list",
1253
- boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1254
- persistentSetting: 'active-user-list',
1255
- callback: function(){
1256
- D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1257
- D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1258
- if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1259
- /* When hiding this element, undo all filtering */
1260
- Chat.setUserFilter(false);
1261
- /*Ideally we'd scroll the final message into view
1262
- now, but because viewMessages is currently hidden behind
1263
- viewConfig, scrolling is a no-op. */
1264
- Chat.scrollMessagesTo(1);
1265
- }else{
1266
- Chat.updateActiveUserList();
1267
- Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
1268
- }
1269
- }
1322
+ hint: "List users who have messages in the currently-loaded chat history.",
1323
+ boolValue: 'active-user-list'
12701324
}
12711325
};
12721326
if(1){
12731327
/* Per user request, toggle the list of users on and off if the
12741328
legend element is tapped. */
@@ -1280,65 +1334,77 @@
12801334
if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){
12811335
Chat.animate(optAu.theList,'anim-flip-v');
12821336
}
12831337
}, false);
12841338
}/*namedOptions.activeUsers additional setup*/
1285
- /* Settings menu entries... Remember that they will be rendered in
1286
- reverse order and the most frequently-needed ones "should"
1287
- (arguably) be closer to the start of this list so that they
1288
- will be rendered within easier reach of the settings button. */
1289
- const settingsOps = [{
1290
- label: "Multi-line input",
1291
- boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1292
- persistentSetting: 'edit-multiline',
1293
- callback: function(){
1294
- Chat.inputToggleSingleMulti();
1295
- }
1296
- },{
1297
- label: "Left-align my posts",
1298
- boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1299
- callback: function f(){
1300
- document.body.classList.toggle('my-messages-right');
1301
- }
1302
- },{
1303
- label: "Show images inline",
1304
- boolValue: ()=>Chat.settings.getBool('images-inline'),
1305
- callback: function(){
1306
- const v = Chat.settings.toggle('images-inline');
1307
- F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1308
- }
1309
- },{
1310
- label: "Timestamps in active users list",
1311
- boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1312
- persistentSetting: 'active-user-list-timestamps',
1313
- callback: function(){
1314
- D.toggleClass(Chat.e.activeUserList,'timestamps');
1315
- /* If the timestamp option is activated but
1316
- namedOptions.activeUsers is not currently checked then
1317
- toggle that option on as well. */
1318
- if(Chat.e.activeUserList.classList.contains('timestamps')
1319
- && !namedOptions.activeUsers.boolValue()){
1320
- namedOptions.activeUsers.checkbox.checked = true;
1321
- namedOptions.activeUsers.callback();
1322
- Chat.settings.set(namedOptions.activeUsers.persistentSetting, true);
1323
- }
1324
- }
1325
- },
1326
- namedOptions.activeUsers,{
1327
- label: "Monospace message font",
1328
- boolValue: ()=>document.body.classList.contains('monospace-messages'),
1329
- persistentSetting: 'monospace-messages',
1330
- callback: function(){
1331
- document.body.classList.toggle('monospace-messages');
1339
+ /* Settings menu entries... the most frequently-needed ones "should"
1340
+ (arguably) be closer to the start of this list. */
1341
+ /**
1342
+ Settings ops structure:
1343
+
1344
+ label: string for the UI
1345
+
1346
+ boolValue: string (name of Chat.settings setting) or a
1347
+ function which returns true or false.
1348
+
1349
+ select: SELECT element (instead of boolValue)
1350
+
1351
+ callback: optional handler to call after setting is modified.
1352
+
1353
+ If a setting has a boolValue set, that gets transformed into a
1354
+ checkbox which toggles the given persistent setting (if
1355
+ boolValue is a string) AND listens for changes to that setting
1356
+ fired via Chat.settings.set() so that the checkbox can stay in
1357
+ sync with external changes to that setting. Various Chat UI
1358
+ elements stay in sync with the config UI via those settings
1359
+ events.
1360
+ */
1361
+ const settingsOps = [{
1362
+ label: "Ctrl-enter to Send",
1363
+ hint: "When on, only Ctrl-Enter will send messages and Enter adds "+
1364
+ "blank lines. "+
1365
+ "When off, both Enter and Ctrl-Enter send. "+
1366
+ "When the input field has focus, is empty, and preview "+
1367
+ "mode is NOT active then Ctrl-Enter toggles this setting.",
1368
+ boolValue: 'edit-ctrl-send'
1369
+ },{
1370
+ label: "Compact mode",
1371
+ hint: "Toggle between a space-saving or more spacious writing area. "+
1372
+ "When the input field has focus, is empty, and preview mode "+
1373
+ "is NOT active then Shift-Enter toggles this setting.",
1374
+ boolValue: 'edit-compact-mode'
1375
+ },{
1376
+ label: "Left-align my posts",
1377
+ hint: "Default alignment of your own messages is selected "
1378
+ +"based window width/height relationship.",
1379
+ boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1380
+ callback: function f(){
1381
+ document.body.classList[
1382
+ this.checkbox.checked ? 'remove' : 'add'
1383
+ ]('my-messages-right');
1384
+ }
1385
+ },{
1386
+ label: "Monospace message font",
1387
+ hint: "Use monospace font for message text?",
1388
+ boolValue: 'monospace-messages',
1389
+ callback: function(setting){
1390
+ document.body.classList[
1391
+ setting.value ? 'add' : 'remove'
1392
+ ]('monospace-messages');
13321393
}
13331394
},{
13341395
label: "Chat-only mode",
1335
- boolValue: ()=>Chat.isChatOnlyMode(),
1336
- persistentSetting: 'chat-only-mode',
1337
- callback: function(){
1338
- Chat.toggleChatOnlyMode();
1339
- }
1396
+ hint: "Toggle the page between normal fossil view and chat-only view.",
1397
+ boolValue: 'chat-only-mode'
1398
+ },{
1399
+ label: "Show images inline",
1400
+ hint: "Whether to show images inline or as a hyperlink.",
1401
+ boolValue: 'images-inline'
1402
+ },namedOptions.activeUsers,{
1403
+ label: "Timestamps in active users list",
1404
+ hint: "Whether to show last-message timestamps.",
1405
+ boolValue: 'active-user-list-timestamps'
13401406
}];
13411407
13421408
/** Set up selection list of notification sounds. */
13431409
if(1){
13441410
const selectSound = D.select();
@@ -1348,90 +1414,168 @@
13481414
if(true===Chat.settings.getBool('audible-alert')){
13491415
/* This setting used to be a plain bool. If we encounter
13501416
such a setting, take the first sound in the list. */
13511417
selectSound.selectedIndex = firstSoundIndex;
13521418
}else{
1353
- selectSound.value = Chat.settings.get('audible-alert','');
1419
+ selectSound.value = Chat.settings.get('audible-alert','<none>');
13541420
if(selectSound.selectedIndex<0){
13551421
/* Missing file - removed after this setting was
13561422
applied. Fall back to the first sound in the list. */
13571423
selectSound.selectedIndex = firstSoundIndex;
13581424
}
13591425
}
13601426
Chat.setNewMessageSound(selectSound.value);
13611427
settingsOps.push({
1362
- label: "Audio alert",
1428
+ hint: "Audio alert. How to enable audio playback is browser-specific!",
13631429
select: selectSound,
13641430
callback: function(ev){
13651431
const v = ev.target.value;
13661432
Chat.setNewMessageSound(v);
13671433
F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
13681434
if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
13691435
}
13701436
});
13711437
}/*audio notification config*/
1438
+ settingsOps.push({
1439
+ label: "Play notification for your own messages.",
1440
+ hint: "When enabled, the audio notification will be played for all messages, "+
1441
+ "including your own. When disabled only messages from other users "+
1442
+ "will trigger a notification.",
1443
+ boolValue: 'alert-own-messages'
1444
+ });
13721445
/**
13731446
Build UI for config options...
13741447
*/
13751448
settingsOps.forEach(function f(op){
13761449
const line = D.addClass(D.div(), 'menu-entry');
1377
- const btn = D.append(
1450
+ const label = op.label ? D.append(
13781451
D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1379
- op.label);
1380
- const callback = function(ev){
1381
- op.callback(ev);
1382
- if(op.persistentSetting){
1383
- Chat.settings.set(op.persistentSetting, op.boolValue());
1384
- }
1385
- };
1452
+ op.label) : undefined;
1453
+ const labelWrapper = D.addClass(D.div(), 'label-wrapper');
1454
+ var hint;
1455
+ const col0 = D.span();
1456
+ if(op.hint){
1457
+ hint = D.append(D.addClass(D.span(),'hint'),op.hint);
1458
+ }
13861459
if(op.hasOwnProperty('select')){
1387
- D.append(line, btn, op.select);
1388
- op.select.addEventListener('change', callback, false);
1460
+ D.append(line, col0, labelWrapper);
1461
+ D.append(labelWrapper, op.select);
1462
+ if(hint) D.append(labelWrapper, hint);
1463
+ if(label) D.append(col0, label);
1464
+ if(op.callback){
1465
+ op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1466
+ }
13891467
}else if(op.hasOwnProperty('boolValue')){
13901468
if(undefined === f.$id) f.$id = 0;
13911469
++f.$id;
1470
+ if('string' ===typeof op.boolValue){
1471
+ const key = op.boolValue;
1472
+ op.boolValue = ()=>Chat.settings.getBool(key);
1473
+ op.persistentSetting = key;
1474
+ }
13921475
const check = op.checkbox
13931476
= D.attr(D.checkbox(1, op.boolValue()),
13941477
'aria-label', op.label);
13951478
const id = 'cfgopt'+f.$id;
1396
- if(op.boolValue()) check.checked = true;
1479
+ check.checked = op.boolValue();
1480
+ op.checkbox = check;
13971481
D.attr(check, 'id', id);
1398
- D.attr(btn, 'for', id);
1399
- D.append(line, check);
1400
- check.addEventListener('change', callback);
1401
- D.append(line, btn);
1482
+ D.append(line, col0, labelWrapper);
1483
+ D.append(col0, check);
1484
+ if(label){
1485
+ D.attr(label, 'for', id);
1486
+ D.append(labelWrapper, label);
1487
+ }
1488
+ if(hint) D.append(labelWrapper, hint);
14021489
}else{
14031490
line.addEventListener('click', callback);
1404
- D.append(line, btn);
1491
+ D.append(line, col0, labelWrapper);
1492
+ if(label) D.append(labelWrapper, label);
1493
+ if(hint) D.append(labelWrapper, hint);
14051494
}
14061495
D.append(optionsMenu, line);
1496
+ if(op.persistentSetting){
1497
+ Chat.settings.addListener(
1498
+ op.persistentSetting,
1499
+ function(setting){
1500
+ if(op.checkbox) op.checkbox.checked = !!setting.value;
1501
+ else if(op.select) op.select.value = setting.value;
1502
+ if(op.callback) op.callback(setting);
1503
+ }
1504
+ );
1505
+ if(op.checkbox){
1506
+ op.checkbox.addEventListener(
1507
+ 'change', function(){
1508
+ Chat.settings.set(op.persistentSetting, op.checkbox.checked)
1509
+ }, false);
1510
+ }
1511
+ }else if(op.callback && op.checkbox){
1512
+ op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false);
1513
+ }
14071514
});
1408
- if(0 && settingsOps.selectSound){
1409
- D.append(optionsMenu, settingsOps.selectSound);
1410
- }
1411
- //settingsButton.click()/*for for development*/;
14121515
})()/*#chat-settings-button setup*/;
14131516
1517
+ (function(){
1518
+ /* Install default settings... must come after
1519
+ chat-settings-button setup so that the listeners which that
1520
+ installs are notified via the properties getting initialized
1521
+ here. */
1522
+ Chat.settings.addListener('monospace-messages',function(s){
1523
+ document.body.classList[s.value ? 'add' : 'remove']('monospace-messages');
1524
+ })
1525
+ Chat.settings.addListener('active-user-list',function(s){
1526
+ Chat.showActiveUserList(s.value);
1527
+ });
1528
+ Chat.settings.addListener('active-user-list-timestamps',function(s){
1529
+ Chat.showActiveUserTimestamps(s.value);
1530
+ });
1531
+ Chat.settings.addListener('chat-only-mode',function(s){
1532
+ Chat.chatOnlyMode(s.value);
1533
+ });
1534
+ Chat.settings.addListener('edit-compact-mode',function(s){
1535
+ Chat.e.inputLine.classList[
1536
+ s.value ? 'add' : 'remove'
1537
+ ]('compact');
1538
+ });
1539
+ Chat.settings.addListener('edit-ctrl-send',function(s){
1540
+ const label = (s.value ? "Ctrl-" : "")+"Enter submits messages.";
1541
+ const eInput = Chat.inputElement();
1542
+ eInput.dataset.placeholder = eInput.dataset.placeholder0 + " " +label;
1543
+ Chat.e.btnSubmit.title = label;
1544
+ });
1545
+ const valueKludges = {
1546
+ /* Convert certain string-format values to other types... */
1547
+ "false": false,
1548
+ "true": true
1549
+ };
1550
+ Object.keys(Chat.settings.defaults).forEach(function(k){
1551
+ var v = Chat.settings.get(k,Chat);
1552
+ if(Chat===v) v = Chat.settings.defaults[k];
1553
+ if(valueKludges.hasOwnProperty(v)) v = valueKludges[v];
1554
+ Chat.settings.set(k,v)
1555
+ /* fires event listeners so that the Config area checkboxes
1556
+ get in sync */;
1557
+ });
1558
+ })();
1559
+
14141560
(function(){/*set up message preview*/
14151561
const btnPreview = Chat.e.btnPreview;
14161562
Chat.setPreviewText = function(t){
14171563
this.setCurrentView(this.e.viewPreview);
14181564
this.e.previewContent.innerHTML = t;
14191565
this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank);
1420
- this.e.inputCurrent.focus();
1566
+ this.inputFocus();
14211567
};
14221568
Chat.e.viewPreview.querySelector('#chat-preview-close').
14231569
addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
14241570
let previewPending = false;
1425
- const elemsToEnable = [
1426
- btnPreview, Chat.e.btnSubmit,
1427
- Chat.e.inputSingle, Chat.e.inputMulti];
1571
+ const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField];
14281572
const submit = function(ev){
14291573
ev.preventDefault();
14301574
ev.stopPropagation();
14311575
if(previewPending) return false;
1432
- const txt = Chat.e.inputCurrent.value;
1576
+ const txt = Chat.inputValue();
14331577
if(!txt){
14341578
Chat.setPreviewText('');
14351579
previewPending = false;
14361580
return false;
14371581
}
@@ -1490,11 +1634,13 @@
14901634
if( m.mdel ){
14911635
/* A record deletion notice. */
14921636
Chat.deleteMessageElem(m.mdel);
14931637
return;
14941638
}
1495
- if(!Chat._isBatchLoading /*&& Chat.me!==m.xfrom*/ && Chat.playNewMessageSound){
1639
+ if(!Chat._isBatchLoading
1640
+ && (Chat.me!==m.xfrom
1641
+ || Chat.settings.getBool('alert-own-messages'))){
14961642
Chat.playNewMessageSound();
14971643
}
14981644
const row = new Chat.MessageWidget(m);
14991645
Chat.injectMessageElem(row.e.body,atEnd);
15001646
if(m.isError){
15011647
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -127,13 +127,12 @@
127 inputWrapper: E1("#chat-input-area"),
128 inputLine: E1('#chat-input-line'),
129 fileSelectWrapper: E1('#chat-input-file-area'),
130 viewMessages: E1('#chat-messages-wrapper'),
131 btnSubmit: E1('#chat-message-submit'),
132 inputSingle: E1('#chat-input-single'),
133 inputMulti: E1('#chat-input-multi'),
134 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
135 inputFile: E1('#chat-input-file'),
136 contentDiv: E1('div.content'),
137 viewConfig: E1('#chat-config'),
138 viewPreview: E1('#chat-preview'),
139 previewContent: E1('#chat-preview-content'),
@@ -169,56 +168,23 @@
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();
173 if(arguments.length){
174 e.value = arguments[0];
175 return this;
176 }
177 return e.value;
178 },
179 /** Asks the current user input field to take focus. Returns this. */
180 inputFocus: function(){
181 this.inputElement().focus();
182 return this;
183 },
184 /** Returns the current message input element. */
185 inputElement: function(){
186 return this.e.inputCurrent;
187 },
188 /** Toggles between single- and multi-line edit modes. Returns this. */
189 inputToggleSingleMulti: function(){
190 const old = this.e.inputCurrent;
191 if(this.e.inputCurrent === this.e.inputSingle){
192 this.e.inputCurrent = this.e.inputMulti;
193 this.e.inputLine.classList.remove('single-line');
194 }else{
195 this.e.inputCurrent = this.e.inputSingle;
196 this.e.inputLine.classList.add('single-line');
197 }
198 const m = this.e.viewMessages,
199 sTop = m.scrollTop,
200 mh1 = m.clientHeight;
201 D.addClass(old, 'hidden');
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 return this;
208 },
209 /**
210 If passed true or no arguments, switches to multi-line mode
211 if currently in single-line mode. If passed false, switches
212 to single-line mode if currently in multi-line mode. Returns
213 this.
214 */
215 inputMultilineMode: function(yes){
216 if(!arguments.length) yes = true;
217 if(yes && this.e.inputCurrent === this.e.inputMulti) return this;
218 else if(!yes && this.e.inputCurrent === this.e.inputSingle) return this;
219 else return this.inputToggleSingleMulti();
220 },
221 /** Enables (if yes is truthy) or disables all elements in
222 * this.disableDuringAjax. */
223 enableAjaxComponents: function(yes){
224 D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -392,26 +358,62 @@
392 return e ? overlapsElemView(e, this.e.viewMessages) : false;
393 },
394 settings:{
395 get: (k,dflt)=>F.storage.get(k,dflt),
396 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
397 set: (k,v)=>F.storage.set(k,v),
 
 
 
398 /* Toggles the boolean setting specified by k. Returns the
399 new value.*/
400 toggle: function(k){
401 const v = this.getBool(k);
402 this.set(k, !v);
403 return !v;
404 },
 
 
 
 
 
 
 
405 defaults:{
 
 
406 "images-inline": !!F.config.chat.imagesInline,
407 "edit-multiline": false,
408 "monospace-messages": false,
 
 
 
 
 
 
 
 
 
 
 
409 "chat-only-mode": false,
 
 
 
 
410 "audible-alert": true,
 
 
 
411 "active-user-list": false,
412 "active-user-list-timestamps": false
 
 
 
 
 
 
413 }
414 },
415 /** Plays a new-message notification sound IF the audible-alert
416 setting is true, else this is a no-op. Returns this.
417 */
@@ -420,11 +422,11 @@
420 try{
421 if(!f.audio) f.audio = new Audio(f.uri);
422 f.audio.currentTime = 0;
423 f.audio.play();
424 }catch(e){
425 console.error("Audio playblack failed.",e);
426 }
427 }
428 return this;
429 },
430 /**
@@ -449,11 +451,13 @@
449 return e;
450 }
451 this.e.views.forEach(function(E){
452 if(e!==E) D.addClass(E,'hidden');
453 });
454 this.e.currentView = D.removeClass(e,'hidden');
 
 
455 this.animate(this.e.currentView, 'anim-fade-in-fast');
456 return this.e.currentView;
457 },
458 /**
459 Updates the "active user list" view if we are not currently
@@ -499,10 +503,35 @@
499 Object.keys(this.usersLastSeen).sort(
500 callee.sortUsersSeen
501 ).forEach(callee.addUserElem);
502 return this;
503 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504 /**
505 Applies user name filter to all current messages, or clears
506 the filter if uname is falsy.
507 */
508 setUserFilter: function(uname){
@@ -542,38 +571,18 @@
542 D.addClassBriefly(e, a, 0, cb);
543 }
544 return this;
545 }
546 };
 
 
 
 
 
547 cs.animate.$disabled = true;
548 F.fetch.beforesend = ()=>cs.ajaxStart();
549 F.fetch.aftersend = ()=>cs.ajaxEnd();
550 cs.e.inputCurrent = cs.e.inputSingle;
551 /* Install default settings... */
552 Object.keys(cs.settings.defaults).forEach(function(k){
553 const v = cs.settings.get(k,cs);
554 if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
555 });
556 if(window.innerWidth<window.innerHeight){
557 /* Alignment of 'my' messages: right alignment is conventional
558 for mobile chat apps but can be difficult to read in wide
559 windows (desktop/tablet landscape mode), so we default to a
560 layout based on the apparent "orientation" of the window:
561 tall vs wide. Can be toggled via settings popup. */
562 document.body.classList.add('my-messages-right');
563 }
564 if(cs.settings.getBool('monospace-messages',false)){
565 document.body.classList.add('monospace-messages');
566 }
567 if(cs.settings.getBool('active-user-list',false)){
568 cs.e.activeUserListWrapper.classList.remove('hidden');
569 }
570 if(cs.settings.getBool('active-user-list-timestamps',false)){
571 cs.e.activeUserList.classList.add('timestamps');
572 }
573 cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
574 cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
575 cs.pageTitleOrig = cs.e.pageTitle.innerText;
576 const qs = (e)=>document.querySelector(e);
577 const argsToArray = function(args){
578 return Array.prototype.slice.call(args,0);
579 };
@@ -1054,11 +1063,11 @@
1054 }/*_handleLegendClicked()*/
1055 };
1056 return cf;
1057 })()/*MessageWidget*/;
1058
1059 const BlobXferState = (function(){/*drag/drop bits...*/
1060 /* State for paste and drag/drop */
1061 const bxs = {
1062 dropDetails: document.querySelector('#chat-drop-details'),
1063 blob: undefined,
1064 clear: function(){
@@ -1069,75 +1078,83 @@
1069 };
1070 /** Updates the paste/drop zone with details of the pasted/dropped
1071 data. The argument must be a Blob or Blob-like object (File) or
1072 it can be falsy to reset/clear that state.*/
1073 const updateDropZoneContent = function(blob){
 
1074 const dd = bxs.dropDetails;
1075 bxs.blob = blob;
1076 D.clearElement(dd);
1077 if(!blob){
1078 Chat.e.inputFile.value = '';
1079 return;
1080 }
1081 D.append(dd, "Name: ", blob.name,
1082 D.br(), "Size: ",blob.size);
1083 if(blob.type && blob.type.startsWith("image/")){
 
 
 
1084 const img = D.img();
1085 D.append(dd, D.br(), img);
1086 const reader = new FileReader();
1087 reader.onload = (e)=>img.setAttribute('src', e.target.result);
1088 reader.readAsDataURL(blob);
1089 }
1090 const btn = D.button("Cancel");
1091 D.append(dd, D.br(), btn);
1092 btn.addEventListener('click', ()=>updateDropZoneContent(), false);
1093 };
1094 Chat.e.inputFile.addEventListener('change', function(ev){
1095 updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
1096 });
1097 /* Handle image paste from clipboard. TODO: figure out how we can
1098 paste non-image binary data as if it had been selected via the
1099 file selection element. */
1100 document.addEventListener('paste', function(event){
1101 const items = event.clipboardData.items,
1102 item = items[0];
1103 if(!item || !item.type) return;
1104 else if('file'===item.kind){
 
1105 updateDropZoneContent(false/*clear prev state*/);
1106 updateDropZoneContent(item.getAsFile());
1107 }
1108 }, false);
1109 /* Add help button for drag/drop/paste zone */
1110 Chat.e.inputFile.parentNode.insertBefore(
1111 F.helpButtonlets.create(
1112 Chat.e.fileSelectWrapper.querySelector('.help-buttonlet')
1113 ), Chat.e.inputFile
1114 );
1115 ////////////////////////////////////////////////////////////
1116 // File drag/drop visual notification.
1117 const dropHighlight = Chat.e.inputFile /* target zone */;
1118 const dropEvents = {
1119 drop: function(ev){
1120 D.removeClass(dropHighlight, 'dragover');
1121 },
1122 dragenter: function(ev){
1123 ev.preventDefault();
1124 ev.dataTransfer.dropEffect = "copy";
1125 D.addClass(dropHighlight, 'dragover');
1126 },
1127 dragleave: function(ev){
1128 D.removeClass(dropHighlight, 'dragover');
1129 },
1130 dragend: function(ev){
1131 D.removeClass(dropHighlight, 'dragover');
1132 }
 
 
 
1133 };
1134 Object.keys(dropEvents).forEach(
1135 (k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true)
 
 
 
1136 );
1137 return bxs;
1138 })()/*drag/drop*/;
1139
1140 const tzOffsetToString = function(off){
1141 const hours = Math.round(off/60), min = Math.round(off % 30);
1142 return ''+(hours + (min ? '.5' : ''));
1143 };
@@ -1155,21 +1172,30 @@
1155 empty, this is a no-op.
1156 */
1157 Chat.submitMessage = function f(){
1158 if(!f.spaces){
1159 f.spaces = /\s+$/;
 
1160 }
1161 this.setCurrentView(this.e.viewMessages);
1162 const fd = new FormData();
1163 var msg = this.inputValue().trim();
1164 if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
1165 /* Cosmetic: trim whitespace from the ends of lines to try to
1166 keep copy/paste from terminals, especially wide ones, from
1167 forcing a horizontal scrollbar on all clients. */
 
 
 
 
 
 
1168 const xmsg = msg.split('\n');
1169 xmsg.forEach(function(line,ndx){
1170 xmsg[ndx] = line.trimRight();
 
 
1171 });
1172 msg = xmsg.join('\n');
1173 }
1174 if(msg) fd.set('msg',msg);
1175 const file = BlobXferState.blob || this.e.inputFile.files[0];
@@ -1194,47 +1220,90 @@
1194 });
1195 BlobXferState.clear();
1196 Chat.inputValue("").inputFocus();
1197 };
1198
1199 const inputWidgetKeydown = function(ev){
1200 if(13 === ev.keyCode){
1201 if(ev.shiftKey){
1202 ev.preventDefault();
1203 ev.stopPropagation();
1204 /* Shift-enter will run preview mode UNLESS preview mode is
1205 active AND the input field is empty, in which case it will
1206 switch back to message view. */
1207 if(Chat.e.currentView===Chat.e.viewPreview
1208 && !Chat.e.inputCurrent.value){
1209 Chat.setCurrentView(Chat.e.viewMessages);
1210 }else{
1211 Chat.e.btnPreview.click();
1212 }
1213 return false;
1214 }else if((Chat.e.inputSingle===ev.target)
1215 || (ev.ctrlKey && Chat.e.inputMulti===ev.target)){
1216 /* ^^^ note that it is intended that both ctrl-enter and enter
1217 work for single-line input mode. */
1218 ev.preventDefault();
1219 ev.stopPropagation();
1220 Chat.submitMessage();
1221 return false;
1222 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1223 }
1224 };
1225 Chat.e.inputSingle
1226 .addEventListener('keydown', inputWidgetKeydown, false);
1227 Chat.e.inputMulti
1228 .addEventListener('keydown', inputWidgetKeydown, false);
1229 Chat.e.btnSubmit.addEventListener('click',(e)=>{
1230 e.preventDefault();
1231 Chat.submitMessage();
1232 return false;
1233 });
 
 
1234
1235 (function(){/*Set up #chat-settings-button */
 
 
 
 
 
 
 
 
 
1236 const settingsButton = document.querySelector('#chat-settings-button');
1237 const optionsMenu = E1('#chat-config-options');
1238 const cbToggle = function(ev){
1239 ev.preventDefault();
1240 ev.stopPropagation();
@@ -1248,27 +1317,12 @@
1248 /** Internal acrobatics to allow certain settings toggles to access
1249 related toggles. */
1250 const namedOptions = {
1251 activeUsers:{
1252 label: "Show active users list",
1253 boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1254 persistentSetting: 'active-user-list',
1255 callback: function(){
1256 D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1257 D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1258 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1259 /* When hiding this element, undo all filtering */
1260 Chat.setUserFilter(false);
1261 /*Ideally we'd scroll the final message into view
1262 now, but because viewMessages is currently hidden behind
1263 viewConfig, scrolling is a no-op. */
1264 Chat.scrollMessagesTo(1);
1265 }else{
1266 Chat.updateActiveUserList();
1267 Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
1268 }
1269 }
1270 }
1271 };
1272 if(1){
1273 /* Per user request, toggle the list of users on and off if the
1274 legend element is tapped. */
@@ -1280,65 +1334,77 @@
1280 if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){
1281 Chat.animate(optAu.theList,'anim-flip-v');
1282 }
1283 }, false);
1284 }/*namedOptions.activeUsers additional setup*/
1285 /* Settings menu entries... Remember that they will be rendered in
1286 reverse order and the most frequently-needed ones "should"
1287 (arguably) be closer to the start of this list so that they
1288 will be rendered within easier reach of the settings button. */
1289 const settingsOps = [{
1290 label: "Multi-line input",
1291 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1292 persistentSetting: 'edit-multiline',
1293 callback: function(){
1294 Chat.inputToggleSingleMulti();
1295 }
1296 },{
1297 label: "Left-align my posts",
1298 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1299 callback: function f(){
1300 document.body.classList.toggle('my-messages-right');
1301 }
1302 },{
1303 label: "Show images inline",
1304 boolValue: ()=>Chat.settings.getBool('images-inline'),
1305 callback: function(){
1306 const v = Chat.settings.toggle('images-inline');
1307 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1308 }
1309 },{
1310 label: "Timestamps in active users list",
1311 boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1312 persistentSetting: 'active-user-list-timestamps',
1313 callback: function(){
1314 D.toggleClass(Chat.e.activeUserList,'timestamps');
1315 /* If the timestamp option is activated but
1316 namedOptions.activeUsers is not currently checked then
1317 toggle that option on as well. */
1318 if(Chat.e.activeUserList.classList.contains('timestamps')
1319 && !namedOptions.activeUsers.boolValue()){
1320 namedOptions.activeUsers.checkbox.checked = true;
1321 namedOptions.activeUsers.callback();
1322 Chat.settings.set(namedOptions.activeUsers.persistentSetting, true);
1323 }
1324 }
1325 },
1326 namedOptions.activeUsers,{
1327 label: "Monospace message font",
1328 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1329 persistentSetting: 'monospace-messages',
1330 callback: function(){
1331 document.body.classList.toggle('monospace-messages');
 
 
 
 
 
 
 
1332 }
1333 },{
1334 label: "Chat-only mode",
1335 boolValue: ()=>Chat.isChatOnlyMode(),
1336 persistentSetting: 'chat-only-mode',
1337 callback: function(){
1338 Chat.toggleChatOnlyMode();
1339 }
 
 
 
 
 
1340 }];
1341
1342 /** Set up selection list of notification sounds. */
1343 if(1){
1344 const selectSound = D.select();
@@ -1348,90 +1414,168 @@
1348 if(true===Chat.settings.getBool('audible-alert')){
1349 /* This setting used to be a plain bool. If we encounter
1350 such a setting, take the first sound in the list. */
1351 selectSound.selectedIndex = firstSoundIndex;
1352 }else{
1353 selectSound.value = Chat.settings.get('audible-alert','');
1354 if(selectSound.selectedIndex<0){
1355 /* Missing file - removed after this setting was
1356 applied. Fall back to the first sound in the list. */
1357 selectSound.selectedIndex = firstSoundIndex;
1358 }
1359 }
1360 Chat.setNewMessageSound(selectSound.value);
1361 settingsOps.push({
1362 label: "Audio alert",
1363 select: selectSound,
1364 callback: function(ev){
1365 const v = ev.target.value;
1366 Chat.setNewMessageSound(v);
1367 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1368 if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1369 }
1370 });
1371 }/*audio notification config*/
 
 
 
 
 
 
 
1372 /**
1373 Build UI for config options...
1374 */
1375 settingsOps.forEach(function f(op){
1376 const line = D.addClass(D.div(), 'menu-entry');
1377 const btn = D.append(
1378 D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1379 op.label);
1380 const callback = function(ev){
1381 op.callback(ev);
1382 if(op.persistentSetting){
1383 Chat.settings.set(op.persistentSetting, op.boolValue());
1384 }
1385 };
1386 if(op.hasOwnProperty('select')){
1387 D.append(line, btn, op.select);
1388 op.select.addEventListener('change', callback, false);
 
 
 
 
 
1389 }else if(op.hasOwnProperty('boolValue')){
1390 if(undefined === f.$id) f.$id = 0;
1391 ++f.$id;
 
 
 
 
 
1392 const check = op.checkbox
1393 = D.attr(D.checkbox(1, op.boolValue()),
1394 'aria-label', op.label);
1395 const id = 'cfgopt'+f.$id;
1396 if(op.boolValue()) check.checked = true;
 
1397 D.attr(check, 'id', id);
1398 D.attr(btn, 'for', id);
1399 D.append(line, check);
1400 check.addEventListener('change', callback);
1401 D.append(line, btn);
 
 
 
1402 }else{
1403 line.addEventListener('click', callback);
1404 D.append(line, btn);
 
 
1405 }
1406 D.append(optionsMenu, line);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1407 });
1408 if(0 && settingsOps.selectSound){
1409 D.append(optionsMenu, settingsOps.selectSound);
1410 }
1411 //settingsButton.click()/*for for development*/;
1412 })()/*#chat-settings-button setup*/;
1413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1414 (function(){/*set up message preview*/
1415 const btnPreview = Chat.e.btnPreview;
1416 Chat.setPreviewText = function(t){
1417 this.setCurrentView(this.e.viewPreview);
1418 this.e.previewContent.innerHTML = t;
1419 this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank);
1420 this.e.inputCurrent.focus();
1421 };
1422 Chat.e.viewPreview.querySelector('#chat-preview-close').
1423 addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
1424 let previewPending = false;
1425 const elemsToEnable = [
1426 btnPreview, Chat.e.btnSubmit,
1427 Chat.e.inputSingle, Chat.e.inputMulti];
1428 const submit = function(ev){
1429 ev.preventDefault();
1430 ev.stopPropagation();
1431 if(previewPending) return false;
1432 const txt = Chat.e.inputCurrent.value;
1433 if(!txt){
1434 Chat.setPreviewText('');
1435 previewPending = false;
1436 return false;
1437 }
@@ -1490,11 +1634,13 @@
1490 if( m.mdel ){
1491 /* A record deletion notice. */
1492 Chat.deleteMessageElem(m.mdel);
1493 return;
1494 }
1495 if(!Chat._isBatchLoading /*&& Chat.me!==m.xfrom*/ && Chat.playNewMessageSound){
 
 
1496 Chat.playNewMessageSound();
1497 }
1498 const row = new Chat.MessageWidget(m);
1499 Chat.injectMessageElem(row.e.body,atEnd);
1500 if(m.isError){
1501
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -127,13 +127,12 @@
127 inputWrapper: E1("#chat-input-area"),
128 inputLine: E1('#chat-input-line'),
129 fileSelectWrapper: E1('#chat-input-file-area'),
130 viewMessages: E1('#chat-messages-wrapper'),
131 btnSubmit: E1('#chat-message-submit'),
132 btnAttach: E1('#chat-message-attach'),
133 inputField: E1('#chat-input-field'),
 
134 inputFile: E1('#chat-input-file'),
135 contentDiv: E1('div.content'),
136 viewConfig: E1('#chat-config'),
137 viewPreview: E1('#chat-preview'),
138 previewContent: E1('#chat-preview-content'),
@@ -169,56 +168,23 @@
168 taking into account single- vs multi-line input. The getter returns
169 a string and the setter returns this object. */
170 inputValue: function(){
171 const e = this.inputElement();
172 if(arguments.length){
173 e.innerText = arguments[0];
174 return this;
175 }
176 return e.innerText;
177 },
178 /** Asks the current user input field to take focus. Returns this. */
179 inputFocus: function(){
180 this.inputElement().focus();
181 return this;
182 },
183 /** Returns the current message input element. */
184 inputElement: function(){
185 return this.e.inputField;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186 },
187 /** Enables (if yes is truthy) or disables all elements in
188 * this.disableDuringAjax. */
189 enableAjaxComponents: function(yes){
190 D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -392,26 +358,62 @@
358 return e ? overlapsElemView(e, this.e.viewMessages) : false;
359 },
360 settings:{
361 get: (k,dflt)=>F.storage.get(k,dflt),
362 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
363 set: function(k,v){
364 F.storage.set(k,v);
365 F.page.dispatchEvent('chat-setting',{key: k, value: v});
366 },
367 /* Toggles the boolean setting specified by k. Returns the
368 new value.*/
369 toggle: function(k){
370 const v = this.getBool(k);
371 this.set(k, !v);
372 return !v;
373 },
374 addListener: function(setting, f){
375 F.page.addEventListener('chat-setting', function(ev){
376 if(ev.detail.key===setting) f(ev.detail);
377 }, false);
378 },
379 /* Default values of settings. These are used for intializing
380 the setting event listeners and config view UI. */
381 defaults:{
382 /* When on, inbound images are displayed inlined, else as a
383 link to download the image. */
384 "images-inline": !!F.config.chat.imagesInline,
385 /* When on, ctrl-enter sends messages, else enter and
386 ctrl-enter both send them. */
387 "edit-ctrl-send": false,
388 /* When on, the edit field starts as a single line and
389 expands as the user types, and the relevant buttons are
390 laid out in a compact form. When off, the edit field and
391 buttons are larger. */
392 "edit-compact-mode": true,
393 /* When on, sets the font-family on messages and the edit
394 field to monospace. */
395 "monospace-messages": true,
396 /* When on, non-chat UI elements (page header/footer) are
397 hidden */
398 "chat-only-mode": false,
399 /* When set to a URI, it is assumed to be an audio file,
400 which gets played when new messages arrive. When true,
401 the first entry in the audio file selection list will be
402 used. */
403 "audible-alert": true,
404 /* When on, show the list of "active" users - those from
405 whom we have messages in the currently-loaded history
406 (noting that deletions are also messages). */
407 "active-user-list": false,
408 /* When on, the [active-user-list] setting includes the
409 timestamp of each user's most recent message. */
410 "active-user-list-timestamps": false,
411 /* When on, the [audible-alert] is played for one's own
412 messages, else it is only played for other users'
413 messages. */
414 "alert-own-messages": false
415 }
416 },
417 /** Plays a new-message notification sound IF the audible-alert
418 setting is true, else this is a no-op. Returns this.
419 */
@@ -420,11 +422,11 @@
422 try{
423 if(!f.audio) f.audio = new Audio(f.uri);
424 f.audio.currentTime = 0;
425 f.audio.play();
426 }catch(e){
427 console.error("Audio playblack failed.", f.uri, e);
428 }
429 }
430 return this;
431 },
432 /**
@@ -449,11 +451,13 @@
451 return e;
452 }
453 this.e.views.forEach(function(E){
454 if(e!==E) D.addClass(E,'hidden');
455 });
456 this.e.currentView = e;
457 if(this.e.currentView.$beforeShow) this.e.currentView.$beforeShow();
458 D.removeClass(e,'hidden');
459 this.animate(this.e.currentView, 'anim-fade-in-fast');
460 return this.e.currentView;
461 },
462 /**
463 Updates the "active user list" view if we are not currently
@@ -499,10 +503,35 @@
503 Object.keys(this.usersLastSeen).sort(
504 callee.sortUsersSeen
505 ).forEach(callee.addUserElem);
506 return this;
507 },
508 /** Show or hide the active user list. Returns this object. */
509 showActiveUserList: function(yes){
510 if(0===arguments.length) yes = true;
511 this.e.activeUserListWrapper.classList[
512 yes ? 'remove' : 'add'
513 ]('hidden');
514 D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
515 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
516 /* When hiding this element, undo all filtering */
517 Chat.setUserFilter(false);
518 /*Ideally we'd scroll the final message into view
519 now, but because viewMessages is currently hidden behind
520 viewConfig, scrolling is a no-op. */
521 Chat.scrollMessagesTo(1);
522 }else{
523 Chat.updateActiveUserList();
524 Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
525 }
526 return this;
527 },
528 showActiveUserTimestamps: function(yes){
529 if(0===arguments.length) yes = true;
530 this.e.activeUserList.classList[yes ? 'add' : 'remove']('timestamps');
531 return this;
532 },
533 /**
534 Applies user name filter to all current messages, or clears
535 the filter if uname is falsy.
536 */
537 setUserFilter: function(uname){
@@ -542,38 +571,18 @@
571 D.addClassBriefly(e, a, 0, cb);
572 }
573 return this;
574 }
575 };
576 if(!D.attr(cs.e.inputField,'contenteditable','plaintext-only').isContentEditable){
577 /* Only the Chrome family supports contenteditable=plaintext-only,
578 but Chrome is the only engine for which we need this flag: */
579 D.attr(cs.e.inputField,'contenteditable','true');
580 }
581 cs.animate.$disabled = true;
582 F.fetch.beforesend = ()=>cs.ajaxStart();
583 F.fetch.aftersend = ()=>cs.ajaxEnd();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584 cs.pageTitleOrig = cs.e.pageTitle.innerText;
585 const qs = (e)=>document.querySelector(e);
586 const argsToArray = function(args){
587 return Array.prototype.slice.call(args,0);
588 };
@@ -1054,11 +1063,11 @@
1063 }/*_handleLegendClicked()*/
1064 };
1065 return cf;
1066 })()/*MessageWidget*/;
1067
1068 const BlobXferState = (function(){
1069 /* State for paste and drag/drop */
1070 const bxs = {
1071 dropDetails: document.querySelector('#chat-drop-details'),
1072 blob: undefined,
1073 clear: function(){
@@ -1069,75 +1078,83 @@
1078 };
1079 /** Updates the paste/drop zone with details of the pasted/dropped
1080 data. The argument must be a Blob or Blob-like object (File) or
1081 it can be falsy to reset/clear that state.*/
1082 const updateDropZoneContent = function(blob){
1083 //console.debug("updateDropZoneContent()",blob);
1084 const dd = bxs.dropDetails;
1085 bxs.blob = blob;
1086 D.clearElement(dd);
1087 if(!blob){
1088 Chat.e.inputFile.value = '';
1089 return;
1090 }
1091 D.append(dd, "Attached: ", blob.name,
1092 D.br(), "Size: ",blob.size);
1093 const btn = D.button("Cancel");
1094 D.append(dd, D.br(), btn);
1095 btn.addEventListener('click', ()=>updateDropZoneContent(), false);
1096 if(blob.type && (blob.type.startsWith("image/") || blob.type==='BITMAP')){
1097 const img = D.img();
1098 D.append(dd, D.br(), img);
1099 const reader = new FileReader();
1100 reader.onload = (e)=>img.setAttribute('src', e.target.result);
1101 reader.readAsDataURL(blob);
1102 }
 
 
 
1103 };
1104 Chat.e.inputFile.addEventListener('change', function(ev){
1105 updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
1106 });
1107 /* Handle image paste from clipboard. TODO: figure out how we can
1108 paste non-image binary data as if it had been selected via the
1109 file selection element. */
1110 const pasteListener = function(event){
1111 const items = event.clipboardData.items,
1112 item = items[0];
1113 //console.debug("paste event",event.target,item,event);
1114 //console.debug("paste event item",item);
1115 if(item && item.type && ('file'===item.kind || 'BITMAP'===item.type)){
1116 updateDropZoneContent(false/*clear prev state*/);
1117 updateDropZoneContent(item.getAsFile());
1118 event.stopPropagation();
1119 event.preventDefault(true);
1120 return false;
1121 }
1122 /* else continue propagating */
1123 };
1124 document.addEventListener('paste', pasteListener, true);
1125 if(0){
1126 const onPastePlainText = function(ev){
1127 var pastedText = undefined;
1128 if (window.clipboardData && window.clipboardData.getData) { // IE
1129 pastedText = window.clipboardData.getData('Text');
1130 }else if (ev.clipboardData && ev.clipboardData.getData) {
1131 pastedText = ev.clipboardData.getData('text/plain');
1132 }
1133 ev.target.textContent += pastedText;
1134 ev.preventDefault();
1135 return false;
1136 };
1137 Chat.e.inputField.addEventListener('paste', onPastePlainText, false);
1138 }
1139 const noDragDropEvents = function(ev){
1140 /* contenteditable tries to do its own thing with dropped data,
1141 which is not compatible with how we use it, so... */
1142 ev.dataTransfer.effectAllowed = 'none';
1143 ev.dataTransfer.dropEffect = 'none';
1144 ev.preventDefault();
1145 ev.stopPropagation();
1146 return false;
1147 };
1148
1149 ['drop','dragenter','dragleave','dragend'].forEach(
1150 (k)=>{
1151 Chat.inputElement().addEventListener(k, noDragDropEvents, false);
1152 }
1153 );
1154 return bxs;
1155 })()/*drag/drop/paste*/;
1156
1157 const tzOffsetToString = function(off){
1158 const hours = Math.round(off/60), min = Math.round(off % 30);
1159 return ''+(hours + (min ? '.5' : ''));
1160 };
@@ -1155,21 +1172,30 @@
1172 empty, this is a no-op.
1173 */
1174 Chat.submitMessage = function f(){
1175 if(!f.spaces){
1176 f.spaces = /\s+$/;
1177 f.markdownContinuation = /\\\s+$/;
1178 }
1179 this.setCurrentView(this.e.viewMessages);
1180 const fd = new FormData();
1181 var msg = this.inputValue().trim();
1182 if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
1183 /* Cosmetic: trim whitespace from the ends of lines to try to
1184 keep copy/paste from terminals, especially wide ones, from
1185 forcing a horizontal scrollbar on all clients. This breaks
1186 markdown's use of blackslash-space-space for paragraph
1187 continuation, but *not* doing this affects all clients every
1188 time someone pastes in console copy/paste from an affected
1189 platform. We seem to have narrowed to the console pasting
1190 problem to users of tmux. Most consoles don't behave
1191 that way. */
1192 const xmsg = msg.split('\n');
1193 xmsg.forEach(function(line,ndx){
1194 if(!f.markdownContinuation.test(line)){
1195 xmsg[ndx] = line.trimRight();
1196 }
1197 });
1198 msg = xmsg.join('\n');
1199 }
1200 if(msg) fd.set('msg',msg);
1201 const file = BlobXferState.blob || this.e.inputFile.files[0];
@@ -1194,47 +1220,90 @@
1220 });
1221 BlobXferState.clear();
1222 Chat.inputValue("").inputFocus();
1223 };
1224
1225 const inputWidgetKeydown = function f(ev){
1226 if(!f.$toggleCtrl){
1227 f.$toggleCtrl = function(currentMode){
1228 currentMode = !currentMode;
1229 Chat.settings.set('edit-ctrl-send', currentMode);
1230 };
1231 f.$toggleCompact = function(currentMode){
1232 currentMode = !currentMode;
1233 Chat.settings.set('edit-compact-mode', currentMode);
1234 };
1235 }
1236 if(13 !== ev.keyCode) return;
1237 const text = Chat.inputValue().trim();
1238 const ctrlMode = Chat.settings.getBool('edit-ctrl-send', false);
1239 //console.debug("Enter key event:", ctrlMode, ev.ctrlKey, ev.shiftKey, ev);
1240 if(ev.shiftKey){
1241 const compactMode = Chat.settings.getBool('edit-compact-mode', false);
1242 ev.preventDefault();
1243 ev.stopPropagation();
1244 /* Shift-enter will run preview mode UNLESS preview mode is
1245 active AND the input field is empty, in which case it will
1246 switch back to message view. */
1247 if(Chat.e.currentView===Chat.e.viewPreview && !text){
1248 Chat.setCurrentView(Chat.e.viewMessages);
1249 }else if(!text){
1250 f.$toggleCompact(compactMode);
1251 }else{
1252 Chat.e.btnPreview.click();
1253 }
1254 return false;
1255 }
1256 if(ev.ctrlKey && !text && !BlobXferState.blob){
1257 /* Ctrl-enter on empty input field(s) toggles Enter/Ctrl-enter mode */
1258 ev.preventDefault();
1259 ev.stopPropagation();
1260 f.$toggleCtrl(ctrlMode);
1261 return false;
1262 }
1263 if(!ctrlMode && ev.ctrlKey && text){
1264 //console.debug("!ctrlMode && ev.ctrlKey && text.");
1265 /* Ctrl-enter in Enter-sends mode SHOULD, with this logic add a
1266 newline, but that is not happening, for unknown reasons
1267 (possibly related to this element being a conteneditable DIV
1268 instead of a textarea). Forcibly appending a newline do the
1269 input area does not work, also for unknown reasons, and would
1270 only be suitable when we're at the end of the input.
1271
1272 Strangely, this approach DOES work for shift-enter, but we
1273 need shift-enter as a hotkey for preview mode.
1274 */
1275 //return;
1276 // return here "should" cause newline to be added, but that doesn't work
1277 }
1278 if((!ctrlMode && !ev.ctrlKey) || (ev.ctrlKey/* && ctrlMode*/)){
1279 /* Ship it! */
1280 ev.preventDefault();
1281 ev.stopPropagation();
1282 Chat.submitMessage();
1283 return false;
1284 }
1285 };
1286 Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false);
 
 
 
1287 Chat.e.btnSubmit.addEventListener('click',(e)=>{
1288 e.preventDefault();
1289 Chat.submitMessage();
1290 return false;
1291 });
1292 Chat.e.btnAttach.addEventListener(
1293 'click', ()=>Chat.e.inputFile.click(), false);
1294
1295 (function(){/*Set up #chat-settings-button and related bits */
1296 if(window.innerWidth<window.innerHeight){
1297 // Must be set up before config view is...
1298 /* Alignment of 'my' messages: right alignment is conventional
1299 for mobile chat apps but can be difficult to read in wide
1300 windows (desktop/tablet landscape mode), so we default to a
1301 layout based on the apparent "orientation" of the window:
1302 tall vs wide. Can be toggled via settings. */
1303 document.body.classList.add('my-messages-right');
1304 }
1305 const settingsButton = document.querySelector('#chat-settings-button');
1306 const optionsMenu = E1('#chat-config-options');
1307 const cbToggle = function(ev){
1308 ev.preventDefault();
1309 ev.stopPropagation();
@@ -1248,27 +1317,12 @@
1317 /** Internal acrobatics to allow certain settings toggles to access
1318 related toggles. */
1319 const namedOptions = {
1320 activeUsers:{
1321 label: "Show active users list",
1322 hint: "List users who have messages in the currently-loaded chat history.",
1323 boolValue: 'active-user-list'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1324 }
1325 };
1326 if(1){
1327 /* Per user request, toggle the list of users on and off if the
1328 legend element is tapped. */
@@ -1280,65 +1334,77 @@
1334 if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){
1335 Chat.animate(optAu.theList,'anim-flip-v');
1336 }
1337 }, false);
1338 }/*namedOptions.activeUsers additional setup*/
1339 /* Settings menu entries... the most frequently-needed ones "should"
1340 (arguably) be closer to the start of this list. */
1341 /**
1342 Settings ops structure:
1343
1344 label: string for the UI
1345
1346 boolValue: string (name of Chat.settings setting) or a
1347 function which returns true or false.
1348
1349 select: SELECT element (instead of boolValue)
1350
1351 callback: optional handler to call after setting is modified.
1352
1353 If a setting has a boolValue set, that gets transformed into a
1354 checkbox which toggles the given persistent setting (if
1355 boolValue is a string) AND listens for changes to that setting
1356 fired via Chat.settings.set() so that the checkbox can stay in
1357 sync with external changes to that setting. Various Chat UI
1358 elements stay in sync with the config UI via those settings
1359 events.
1360 */
1361 const settingsOps = [{
1362 label: "Ctrl-enter to Send",
1363 hint: "When on, only Ctrl-Enter will send messages and Enter adds "+
1364 "blank lines. "+
1365 "When off, both Enter and Ctrl-Enter send. "+
1366 "When the input field has focus, is empty, and preview "+
1367 "mode is NOT active then Ctrl-Enter toggles this setting.",
1368 boolValue: 'edit-ctrl-send'
1369 },{
1370 label: "Compact mode",
1371 hint: "Toggle between a space-saving or more spacious writing area. "+
1372 "When the input field has focus, is empty, and preview mode "+
1373 "is NOT active then Shift-Enter toggles this setting.",
1374 boolValue: 'edit-compact-mode'
1375 },{
1376 label: "Left-align my posts",
1377 hint: "Default alignment of your own messages is selected "
1378 +"based window width/height relationship.",
1379 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1380 callback: function f(){
1381 document.body.classList[
1382 this.checkbox.checked ? 'remove' : 'add'
1383 ]('my-messages-right');
1384 }
1385 },{
1386 label: "Monospace message font",
1387 hint: "Use monospace font for message text?",
1388 boolValue: 'monospace-messages',
1389 callback: function(setting){
1390 document.body.classList[
1391 setting.value ? 'add' : 'remove'
1392 ]('monospace-messages');
1393 }
1394 },{
1395 label: "Chat-only mode",
1396 hint: "Toggle the page between normal fossil view and chat-only view.",
1397 boolValue: 'chat-only-mode'
1398 },{
1399 label: "Show images inline",
1400 hint: "Whether to show images inline or as a hyperlink.",
1401 boolValue: 'images-inline'
1402 },namedOptions.activeUsers,{
1403 label: "Timestamps in active users list",
1404 hint: "Whether to show last-message timestamps.",
1405 boolValue: 'active-user-list-timestamps'
1406 }];
1407
1408 /** Set up selection list of notification sounds. */
1409 if(1){
1410 const selectSound = D.select();
@@ -1348,90 +1414,168 @@
1414 if(true===Chat.settings.getBool('audible-alert')){
1415 /* This setting used to be a plain bool. If we encounter
1416 such a setting, take the first sound in the list. */
1417 selectSound.selectedIndex = firstSoundIndex;
1418 }else{
1419 selectSound.value = Chat.settings.get('audible-alert','<none>');
1420 if(selectSound.selectedIndex<0){
1421 /* Missing file - removed after this setting was
1422 applied. Fall back to the first sound in the list. */
1423 selectSound.selectedIndex = firstSoundIndex;
1424 }
1425 }
1426 Chat.setNewMessageSound(selectSound.value);
1427 settingsOps.push({
1428 hint: "Audio alert. How to enable audio playback is browser-specific!",
1429 select: selectSound,
1430 callback: function(ev){
1431 const v = ev.target.value;
1432 Chat.setNewMessageSound(v);
1433 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1434 if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1435 }
1436 });
1437 }/*audio notification config*/
1438 settingsOps.push({
1439 label: "Play notification for your own messages.",
1440 hint: "When enabled, the audio notification will be played for all messages, "+
1441 "including your own. When disabled only messages from other users "+
1442 "will trigger a notification.",
1443 boolValue: 'alert-own-messages'
1444 });
1445 /**
1446 Build UI for config options...
1447 */
1448 settingsOps.forEach(function f(op){
1449 const line = D.addClass(D.div(), 'menu-entry');
1450 const label = op.label ? D.append(
1451 D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1452 op.label) : undefined;
1453 const labelWrapper = D.addClass(D.div(), 'label-wrapper');
1454 var hint;
1455 const col0 = D.span();
1456 if(op.hint){
1457 hint = D.append(D.addClass(D.span(),'hint'),op.hint);
1458 }
1459 if(op.hasOwnProperty('select')){
1460 D.append(line, col0, labelWrapper);
1461 D.append(labelWrapper, op.select);
1462 if(hint) D.append(labelWrapper, hint);
1463 if(label) D.append(col0, label);
1464 if(op.callback){
1465 op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1466 }
1467 }else if(op.hasOwnProperty('boolValue')){
1468 if(undefined === f.$id) f.$id = 0;
1469 ++f.$id;
1470 if('string' ===typeof op.boolValue){
1471 const key = op.boolValue;
1472 op.boolValue = ()=>Chat.settings.getBool(key);
1473 op.persistentSetting = key;
1474 }
1475 const check = op.checkbox
1476 = D.attr(D.checkbox(1, op.boolValue()),
1477 'aria-label', op.label);
1478 const id = 'cfgopt'+f.$id;
1479 check.checked = op.boolValue();
1480 op.checkbox = check;
1481 D.attr(check, 'id', id);
1482 D.append(line, col0, labelWrapper);
1483 D.append(col0, check);
1484 if(label){
1485 D.attr(label, 'for', id);
1486 D.append(labelWrapper, label);
1487 }
1488 if(hint) D.append(labelWrapper, hint);
1489 }else{
1490 line.addEventListener('click', callback);
1491 D.append(line, col0, labelWrapper);
1492 if(label) D.append(labelWrapper, label);
1493 if(hint) D.append(labelWrapper, hint);
1494 }
1495 D.append(optionsMenu, line);
1496 if(op.persistentSetting){
1497 Chat.settings.addListener(
1498 op.persistentSetting,
1499 function(setting){
1500 if(op.checkbox) op.checkbox.checked = !!setting.value;
1501 else if(op.select) op.select.value = setting.value;
1502 if(op.callback) op.callback(setting);
1503 }
1504 );
1505 if(op.checkbox){
1506 op.checkbox.addEventListener(
1507 'change', function(){
1508 Chat.settings.set(op.persistentSetting, op.checkbox.checked)
1509 }, false);
1510 }
1511 }else if(op.callback && op.checkbox){
1512 op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false);
1513 }
1514 });
 
 
 
 
1515 })()/*#chat-settings-button setup*/;
1516
1517 (function(){
1518 /* Install default settings... must come after
1519 chat-settings-button setup so that the listeners which that
1520 installs are notified via the properties getting initialized
1521 here. */
1522 Chat.settings.addListener('monospace-messages',function(s){
1523 document.body.classList[s.value ? 'add' : 'remove']('monospace-messages');
1524 })
1525 Chat.settings.addListener('active-user-list',function(s){
1526 Chat.showActiveUserList(s.value);
1527 });
1528 Chat.settings.addListener('active-user-list-timestamps',function(s){
1529 Chat.showActiveUserTimestamps(s.value);
1530 });
1531 Chat.settings.addListener('chat-only-mode',function(s){
1532 Chat.chatOnlyMode(s.value);
1533 });
1534 Chat.settings.addListener('edit-compact-mode',function(s){
1535 Chat.e.inputLine.classList[
1536 s.value ? 'add' : 'remove'
1537 ]('compact');
1538 });
1539 Chat.settings.addListener('edit-ctrl-send',function(s){
1540 const label = (s.value ? "Ctrl-" : "")+"Enter submits messages.";
1541 const eInput = Chat.inputElement();
1542 eInput.dataset.placeholder = eInput.dataset.placeholder0 + " " +label;
1543 Chat.e.btnSubmit.title = label;
1544 });
1545 const valueKludges = {
1546 /* Convert certain string-format values to other types... */
1547 "false": false,
1548 "true": true
1549 };
1550 Object.keys(Chat.settings.defaults).forEach(function(k){
1551 var v = Chat.settings.get(k,Chat);
1552 if(Chat===v) v = Chat.settings.defaults[k];
1553 if(valueKludges.hasOwnProperty(v)) v = valueKludges[v];
1554 Chat.settings.set(k,v)
1555 /* fires event listeners so that the Config area checkboxes
1556 get in sync */;
1557 });
1558 })();
1559
1560 (function(){/*set up message preview*/
1561 const btnPreview = Chat.e.btnPreview;
1562 Chat.setPreviewText = function(t){
1563 this.setCurrentView(this.e.viewPreview);
1564 this.e.previewContent.innerHTML = t;
1565 this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank);
1566 this.inputFocus();
1567 };
1568 Chat.e.viewPreview.querySelector('#chat-preview-close').
1569 addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
1570 let previewPending = false;
1571 const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField];
 
 
1572 const submit = function(ev){
1573 ev.preventDefault();
1574 ev.stopPropagation();
1575 if(previewPending) return false;
1576 const txt = Chat.inputValue();
1577 if(!txt){
1578 Chat.setPreviewText('');
1579 previewPending = false;
1580 return false;
1581 }
@@ -1490,11 +1634,13 @@
1634 if( m.mdel ){
1635 /* A record deletion notice. */
1636 Chat.deleteMessageElem(m.mdel);
1637 return;
1638 }
1639 if(!Chat._isBatchLoading
1640 && (Chat.me!==m.xfrom
1641 || Chat.settings.getBool('alert-own-messages'))){
1642 Chat.playNewMessageSound();
1643 }
1644 const row = new Chat.MessageWidget(m);
1645 Chat.injectMessageElem(row.e.body,atEnd);
1646 if(m.isError){
1647
+149 -37
--- src/style.chat.css
+++ src/style.chat.css
@@ -10,10 +10,14 @@
1010
display: flex;
1111
flex-direction: column;
1212
border: none;
1313
align-items: flex-start;
1414
}
15
+body.chat button,
16
+body.chat input[type=button] {
17
+ line-height: inherit/*undo skin-specific funkiness*/;
18
+}
1519
body.chat .message-widget:last-of-type {
1620
/* Latest message: reduce bottom gap */
1721
margin-bottom: 0.1em;
1822
}
1923
body.chat.my-messages-right .message-widget.mine {
@@ -36,12 +40,13 @@
3640
min-width: 9em /*avoid unsightly "underlap" with the neighboring
3741
.message-widget-tab element*/;
3842
white-space: normal;
3943
}
4044
body.chat.monospace-messages .message-widget-content,
41
-body.chat.monospace-messages textarea,
42
-body.chat.monospace-messages input[type=text]{
45
+/*body.chat.monospace-messages textarea,*/
46
+/*body.chat.monospace-messages input[type=text],*/
47
+body.chat.monospace-messages #chat-input-field{
4348
font-family: monospace;
4449
}
4550
body.chat .message-widget-content > * {
4651
margin: 0;
4752
padding: 0;
@@ -168,53 +173,146 @@
168173
body.chat #chat-input-area {
169174
display: flex;
170175
flex-direction: column;
171176
padding: 0;
172177
margin: 0;
173
- position: initial /*sticky currently disabled due to scrolling-related issues*/;
174
- /*bottom: 0;*/
178
+ flex: 0 1 auto;
175179
}
176180
body.chat:not(.chat-only-mode) #chat-input-area{
177181
/* Safari user reports that 2em is necessary to keep the file selection
178182
widget from overlapping the page footer, whereas a margin of 0 is fine
179183
for FF/Chrome (and 2em is a *huge* waste of space for those). */
180184
margin-bottom: 0;
181185
}
182
-
186
+#chat-input-field {
187
+ display: inline-block/*supposed workaround for Chrome weirdness*/;
188
+ padding: 0.2em;
189
+ flex: 10 1 auto;
190
+ background-color: rgba(156,156,156,0.3);
191
+ overflow: auto;
192
+ resize: vertical;
193
+}
194
+#chat-input-field:empty::before {
195
+ content: attr(data-placeholder);
196
+ opacity: 0.6;
197
+}
198
+#chat-input-field:not(:focus){
199
+ border-width: 1px;
200
+ border-style: solid;
201
+ border-radius: 0.25em;
202
+}
203
+#chat-input-field:focus{
204
+ /* This transparent border helps avoid the text shifting around
205
+ when the contenteditable attribute causes a border (which we
206
+ apparently cannot style) to be added. */
207
+ border-width: 1px;
208
+ border-style: solid;
209
+ border-color: transparent;
210
+ border-radius: 0.25em;
211
+}
183212
/* Widget holding the chat message input field, send button, and
184213
settings button. */
185214
body.chat #chat-input-line {
186215
display: flex;
187216
flex-direction: row;
188217
align-items: stretch;
218
+ flex-wrap: nowrap;
219
+}
220
+/*body.chat #chat-input-line:not(.compact) {
221
+ flex-wrap: nowrap;
222
+}*/
223
+body.chat #chat-input-line.compact {
224
+ /* "The problem" with wrapping, together with a contenteditable input
225
+ field, is that the latter grows as the user types, so causes
226
+ wrapping to happen while they type, then to unwrap as soon as the
227
+ input field is cleared (when the message is sent). When we stay
228
+ wrapped in compact mode, the wrapped buttons simply take up too
229
+ much space. */
230
+ /*flex-wrap: wrap;
231
+ justify-content: flex-end;*/
232
+ flex-direction: column;
233
+ /**
234
+ We "really do" need column orientation here because it's the
235
+ only way to eliminate the possibility that (A) the buttons
236
+ get truncated in very narrow windows and (B) that they keep
237
+ stable positions.
238
+ */
189239
}
190
-body.chat #chat-input-line.single-line {
191
- flex-wrap: wrap;
240
+body.chat #chat-input-line.compact #chat-input-field {
192241
}
242
+
193243
body.chat #chat-edit-buttons {
194
- flex: 1 1 auto;
244
+ flex: 0 1 auto;
195245
display: flex;
196246
flex-direction: column;
197
- justify-content: space-between;
247
+ align-items: center;
248
+ min-width: 4em;
249
+ min-height: 1.5em;
250
+ align-self: flex-end
251
+ /*keep buttons stable at bottom/right even when input field
252
+ resizes */;
198253
}
199
-body.chat #chat-input-line.single-line #chat-edit-buttons {
254
+body.chat #chat-input-line.compact #chat-edit-buttons {
200255
flex-direction: row;
256
+ flex: 1 1 auto;
257
+ align-self: stretch;
258
+ justify-content: flex-end;
259
+ /*flex-wrap: wrap;*/
260
+ /* Wrapping would be ideal except that the edit widget
261
+ grows in width as the user types, moving the buttons
262
+ around */
201263
}
202264
body.chat #chat-edit-buttons > * {
203
- flex: 1 1 auto;
204265
padding: initial/*some skins mess this up for buttons*/;
266
+ line-height:
267
+ 0.1 /* buggy glyph alignment workaround for FF78, possibly
268
+ others. Does not affect well-behaved browsers. */;
269
+ min-width: 4ex;
205270
}
206
-body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * {
207
- max-width: 4em;
208
- margin: 0.25em;
271
+body.chat #chat-input-line:not(.compact) #chat-edit-buttons > * {
272
+ max-width: 6ex;
273
+ min-width: 6ex;
274
+ min-height: 5ex;
275
+ max-height: 6ex;
276
+ margin: 0.125em;
209277
}
210
-body.chat #chat-input-line.single-line #chat-edit-buttons > * {
211
- margin: 0 0.25em;
278
+
279
+body.chat #chat-input-line:not(.compact) #chat-input-field {
280
+ /*border-left-style: double;
281
+ border-left-width: 3px;
282
+ border-right-style: double;
283
+ border-right-width: 3px;*/
284
+ min-height: 4rem;
285
+ /*max-height: 50rem;*/
286
+/*
287
+ Problems related to max-height:
288
+
289
+ - If we do NOT set a max-height then pasting/typing a large amount
290
+ of text can cause this element to grow without bounds, larger than
291
+ the window, and there's no way to navigate it sensibly. In this
292
+ case, manually resizing the element (desktop only - mobile doesn't
293
+ offer that) will force it to stay at the selected size even if more
294
+ content is added to it later.
295
+
296
+ - If we DO set a max-height then its growth is bounded but it also
297
+ cannot manually expanded by the user.
298
+
299
+ The lesser of the two evils seems to be to rely on the browser
300
+ feature that a manual resize of the element will pin its sits.
301
+*/
212302
}
213303
214
-body.chat #chat-input-line > button {
215
- max-width: 4em;
304
+body.chat #chat-input-line.compact #chat-edit-buttons > * {
305
+ margin: 2px 0.125em 0 0.125em;
306
+ min-width: 8ex;
307
+ max-width: unset;
308
+ min-height: 3ex;
309
+ max-height: 3ex;
310
+ /*flex: 1 1 auto;*/
311
+}
312
+body.chat #chat-input-line.compact #chat-edit-buttons #chat-message-submit {
313
+ min-width: 16ex;
216314
}
217315
body.chat #chat-input-line > #chat-settings-button{
218316
margin: 0 0 0 0.25em;
219317
max-width: 2em;
220318
}
@@ -222,32 +320,26 @@
222320
body.chat #chat-input-line > textarea {
223321
flex: 20 1 auto;
224322
max-width: revert;
225323
min-width: 20em;
226324
}
227
-body.chat #chat-input-line.single-line > input[type=text] {
325
+body.chat #chat-input-line.compact > input[type=text] {
228326
margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
229327
}
230328
/* Widget holding the file selection control and preview */
231329
body.chat #chat-input-file-area {
232330
display: flex;
233331
flex-direction: row;
234
- align-items: center;
235
- flex-wrap: wrap;
236
- margin: 0.25em 0 0 0 /* avoid nudging input area */;
332
+ margin: 0;
237333
}
238334
body.chat #chat-input-file-area > .file-selection-wrapper {
239335
align-self: flex-start;
240336
margin-right: 0.5em;
241337
flex: 0 1 auto;
242338
padding: 0.25em 0.5em;
243339
white-space: nowrap;
244340
}
245
-body.chat #chat-input-file-area .file-selection-wrapper > * {
246
- vertical-align: middle;
247
- margin: 0;
248
-}
249341
body.chat #chat-input-file {
250342
border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
251343
border-radius: 0.25em;
252344
padding: 0.25em;
253345
}
@@ -258,24 +350,25 @@
258350
body.chat #chat-input-file.dragover {
259351
border: 1px dashed green;
260352
}
261353
/* Widget holding the details of a selected/dropped file/image. */
262354
body.chat #chat-drop-details {
263
- flex: 0 1 auto;
264
- padding: 0.5em 1em;
265
- margin-left: 0.5em;
355
+ padding: 0 1em;
266356
white-space: pre;
267357
font-family: monospace;
358
+ margin: auto;
359
+ flex: 0;
268360
}
269361
270362
body.chat #chat-drop-details img {
271363
max-width: 45%;
272364
max-height: 45%;
273365
}
274366
body.chat .chat-view {
275367
flex: 20 1 auto
276
- /*ensure that these grow more than the non-.chat-view elements*/;
368
+ /*ensure that these grow more than the non-.chat-view elements.
369
+ Note that setting flex shrink to 0 breaks/disables scrolling!*/;
277370
margin-bottom: 0.2em;
278371
}
279372
body.chat #chat-config,
280373
body.chat #chat-preview {
281374
/* /chat configuration widget */
@@ -289,28 +382,41 @@
289382
}
290383
body.chat #chat-config #chat-config-options {
291384
/* /chat config options go here */
292385
flex: 1 1 auto;
293386
display: flex;
294
- flex-direction: column-reverse;
387
+ flex-direction: column;
295388
overflow: auto;
296389
}
297390
body.chat #chat-config #chat-config-options .menu-entry {
298391
display: flex;
299
- align-items: center;
392
+ align-items: baseline;
300393
flex-direction: row;
301394
flex-wrap: nowrap;
302395
padding: 1em;
303396
}
304
-body.chat #chat-config #chat-config-options .menu-entry > label {
397
+body.chat #chat-config #chat-config-options .menu-entry label[for] {
305398
cursor: pointer;
306399
}
307
-body.chat #chat-config #chat-config-options .menu-entry > input:first-child {
308
- margin-right: 1em;
400
+body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
401
+ min-width: 1.5rem;
402
+}
403
+body.chat #chat-config #chat-config-options .menu-entry span.hint {
404
+ /* Config menu hint text */
405
+ font-size: 80%;
406
+ white-space: pre-wrap;
407
+ display: inline-block;
408
+}
409
+body.chat #chat-config #chat-config-options .menu-entry:first-child {
410
+}
411
+body.chat #chat-config #chat-config-options .menu-entry div.label-wrapper {
412
+ display: flex;
413
+ flex-direction: column;
414
+ align-self: baseline;
415
+ margin-left: 1em;
309416
}
310
-body.chat #chat-config #chat-config-options .menu-entry > label:first-child {
311
- margin-right: 0.5em;
417
+body.chat #chat-config #chat-config-options .menu-entry select {
312418
}
313419
body.chat #chat-preview #chat-preview-content {
314420
overflow: auto;
315421
flex: 1 1 auto;
316422
padding: 0.5em;
@@ -394,10 +500,16 @@
394500
}
395501
body.chat #chat-user-list .chat-user.selected {
396502
font-weight: bold;
397503
text-decoration: underline;
398504
}
505
+
506
+body.chat.fossil-dark-style #chat-message-attach > svg {
507
+ /* The black paperclip is barely visible in dark-mode
508
+ skins when they have dark buttons */
509
+ filter: invert(0.5);
510
+}
399511
400512
body.chat .anim-rotate-360 {
401513
animation: rotate-360 750ms linear;
402514
}
403515
@keyframes rotate-360 {
404516
--- src/style.chat.css
+++ src/style.chat.css
@@ -10,10 +10,14 @@
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 {
@@ -36,12 +40,13 @@
36 min-width: 9em /*avoid unsightly "underlap" with the neighboring
37 .message-widget-tab element*/;
38 white-space: normal;
39 }
40 body.chat.monospace-messages .message-widget-content,
41 body.chat.monospace-messages textarea,
42 body.chat.monospace-messages input[type=text]{
 
43 font-family: monospace;
44 }
45 body.chat .message-widget-content > * {
46 margin: 0;
47 padding: 0;
@@ -168,53 +173,146 @@
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). */
180 margin-bottom: 0;
181 }
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183 /* Widget holding the chat message input field, send button, and
184 settings button. */
185 body.chat #chat-input-line {
186 display: flex;
187 flex-direction: row;
188 align-items: stretch;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189 }
190 body.chat #chat-input-line.single-line {
191 flex-wrap: wrap;
192 }
 
193 body.chat #chat-edit-buttons {
194 flex: 1 1 auto;
195 display: flex;
196 flex-direction: column;
197 justify-content: space-between;
 
 
 
 
 
198 }
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 }
210 body.chat #chat-input-line.single-line #chat-edit-buttons > * {
211 margin: 0 0.25em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212 }
213
214 body.chat #chat-input-line > button {
215 max-width: 4em;
 
 
 
 
 
 
 
 
216 }
217 body.chat #chat-input-line > #chat-settings-button{
218 margin: 0 0 0 0.25em;
219 max-width: 2em;
220 }
@@ -222,32 +320,26 @@
222 body.chat #chat-input-line > textarea {
223 flex: 20 1 auto;
224 max-width: revert;
225 min-width: 20em;
226 }
227 body.chat #chat-input-line.single-line > input[type=text] {
228 margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
229 }
230 /* Widget holding the file selection control and preview */
231 body.chat #chat-input-file-area {
232 display: flex;
233 flex-direction: row;
234 align-items: center;
235 flex-wrap: wrap;
236 margin: 0.25em 0 0 0 /* avoid nudging input area */;
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;
248 }
249 body.chat #chat-input-file {
250 border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
251 border-radius: 0.25em;
252 padding: 0.25em;
253 }
@@ -258,24 +350,25 @@
258 body.chat #chat-input-file.dragover {
259 border: 1px dashed green;
260 }
261 /* Widget holding the details of a selected/dropped file/image. */
262 body.chat #chat-drop-details {
263 flex: 0 1 auto;
264 padding: 0.5em 1em;
265 margin-left: 0.5em;
266 white-space: pre;
267 font-family: monospace;
 
 
268 }
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 */
@@ -289,28 +382,41 @@
289 }
290 body.chat #chat-config #chat-config-options {
291 /* /chat config options go here */
292 flex: 1 1 auto;
293 display: flex;
294 flex-direction: column-reverse;
295 overflow: auto;
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 {
311 margin-right: 0.5em;
312 }
313 body.chat #chat-preview #chat-preview-content {
314 overflow: auto;
315 flex: 1 1 auto;
316 padding: 0.5em;
@@ -394,10 +500,16 @@
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
--- src/style.chat.css
+++ src/style.chat.css
@@ -10,10 +10,14 @@
10 display: flex;
11 flex-direction: column;
12 border: none;
13 align-items: flex-start;
14 }
15 body.chat button,
16 body.chat input[type=button] {
17 line-height: inherit/*undo skin-specific funkiness*/;
18 }
19 body.chat .message-widget:last-of-type {
20 /* Latest message: reduce bottom gap */
21 margin-bottom: 0.1em;
22 }
23 body.chat.my-messages-right .message-widget.mine {
@@ -36,12 +40,13 @@
40 min-width: 9em /*avoid unsightly "underlap" with the neighboring
41 .message-widget-tab element*/;
42 white-space: normal;
43 }
44 body.chat.monospace-messages .message-widget-content,
45 /*body.chat.monospace-messages textarea,*/
46 /*body.chat.monospace-messages input[type=text],*/
47 body.chat.monospace-messages #chat-input-field{
48 font-family: monospace;
49 }
50 body.chat .message-widget-content > * {
51 margin: 0;
52 padding: 0;
@@ -168,53 +173,146 @@
173 body.chat #chat-input-area {
174 display: flex;
175 flex-direction: column;
176 padding: 0;
177 margin: 0;
178 flex: 0 1 auto;
 
179 }
180 body.chat:not(.chat-only-mode) #chat-input-area{
181 /* Safari user reports that 2em is necessary to keep the file selection
182 widget from overlapping the page footer, whereas a margin of 0 is fine
183 for FF/Chrome (and 2em is a *huge* waste of space for those). */
184 margin-bottom: 0;
185 }
186 #chat-input-field {
187 display: inline-block/*supposed workaround for Chrome weirdness*/;
188 padding: 0.2em;
189 flex: 10 1 auto;
190 background-color: rgba(156,156,156,0.3);
191 overflow: auto;
192 resize: vertical;
193 }
194 #chat-input-field:empty::before {
195 content: attr(data-placeholder);
196 opacity: 0.6;
197 }
198 #chat-input-field:not(:focus){
199 border-width: 1px;
200 border-style: solid;
201 border-radius: 0.25em;
202 }
203 #chat-input-field:focus{
204 /* This transparent border helps avoid the text shifting around
205 when the contenteditable attribute causes a border (which we
206 apparently cannot style) to be added. */
207 border-width: 1px;
208 border-style: solid;
209 border-color: transparent;
210 border-radius: 0.25em;
211 }
212 /* Widget holding the chat message input field, send button, and
213 settings button. */
214 body.chat #chat-input-line {
215 display: flex;
216 flex-direction: row;
217 align-items: stretch;
218 flex-wrap: nowrap;
219 }
220 /*body.chat #chat-input-line:not(.compact) {
221 flex-wrap: nowrap;
222 }*/
223 body.chat #chat-input-line.compact {
224 /* "The problem" with wrapping, together with a contenteditable input
225 field, is that the latter grows as the user types, so causes
226 wrapping to happen while they type, then to unwrap as soon as the
227 input field is cleared (when the message is sent). When we stay
228 wrapped in compact mode, the wrapped buttons simply take up too
229 much space. */
230 /*flex-wrap: wrap;
231 justify-content: flex-end;*/
232 flex-direction: column;
233 /**
234 We "really do" need column orientation here because it's the
235 only way to eliminate the possibility that (A) the buttons
236 get truncated in very narrow windows and (B) that they keep
237 stable positions.
238 */
239 }
240 body.chat #chat-input-line.compact #chat-input-field {
 
241 }
242
243 body.chat #chat-edit-buttons {
244 flex: 0 1 auto;
245 display: flex;
246 flex-direction: column;
247 align-items: center;
248 min-width: 4em;
249 min-height: 1.5em;
250 align-self: flex-end
251 /*keep buttons stable at bottom/right even when input field
252 resizes */;
253 }
254 body.chat #chat-input-line.compact #chat-edit-buttons {
255 flex-direction: row;
256 flex: 1 1 auto;
257 align-self: stretch;
258 justify-content: flex-end;
259 /*flex-wrap: wrap;*/
260 /* Wrapping would be ideal except that the edit widget
261 grows in width as the user types, moving the buttons
262 around */
263 }
264 body.chat #chat-edit-buttons > * {
 
265 padding: initial/*some skins mess this up for buttons*/;
266 line-height:
267 0.1 /* buggy glyph alignment workaround for FF78, possibly
268 others. Does not affect well-behaved browsers. */;
269 min-width: 4ex;
270 }
271 body.chat #chat-input-line:not(.compact) #chat-edit-buttons > * {
272 max-width: 6ex;
273 min-width: 6ex;
274 min-height: 5ex;
275 max-height: 6ex;
276 margin: 0.125em;
277 }
278
279 body.chat #chat-input-line:not(.compact) #chat-input-field {
280 /*border-left-style: double;
281 border-left-width: 3px;
282 border-right-style: double;
283 border-right-width: 3px;*/
284 min-height: 4rem;
285 /*max-height: 50rem;*/
286 /*
287 Problems related to max-height:
288
289 - If we do NOT set a max-height then pasting/typing a large amount
290 of text can cause this element to grow without bounds, larger than
291 the window, and there's no way to navigate it sensibly. In this
292 case, manually resizing the element (desktop only - mobile doesn't
293 offer that) will force it to stay at the selected size even if more
294 content is added to it later.
295
296 - If we DO set a max-height then its growth is bounded but it also
297 cannot manually expanded by the user.
298
299 The lesser of the two evils seems to be to rely on the browser
300 feature that a manual resize of the element will pin its sits.
301 */
302 }
303
304 body.chat #chat-input-line.compact #chat-edit-buttons > * {
305 margin: 2px 0.125em 0 0.125em;
306 min-width: 8ex;
307 max-width: unset;
308 min-height: 3ex;
309 max-height: 3ex;
310 /*flex: 1 1 auto;*/
311 }
312 body.chat #chat-input-line.compact #chat-edit-buttons #chat-message-submit {
313 min-width: 16ex;
314 }
315 body.chat #chat-input-line > #chat-settings-button{
316 margin: 0 0 0 0.25em;
317 max-width: 2em;
318 }
@@ -222,32 +320,26 @@
320 body.chat #chat-input-line > textarea {
321 flex: 20 1 auto;
322 max-width: revert;
323 min-width: 20em;
324 }
325 body.chat #chat-input-line.compact > input[type=text] {
326 margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
327 }
328 /* Widget holding the file selection control and preview */
329 body.chat #chat-input-file-area {
330 display: flex;
331 flex-direction: row;
332 margin: 0;
 
 
333 }
334 body.chat #chat-input-file-area > .file-selection-wrapper {
335 align-self: flex-start;
336 margin-right: 0.5em;
337 flex: 0 1 auto;
338 padding: 0.25em 0.5em;
339 white-space: nowrap;
340 }
 
 
 
 
341 body.chat #chat-input-file {
342 border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
343 border-radius: 0.25em;
344 padding: 0.25em;
345 }
@@ -258,24 +350,25 @@
350 body.chat #chat-input-file.dragover {
351 border: 1px dashed green;
352 }
353 /* Widget holding the details of a selected/dropped file/image. */
354 body.chat #chat-drop-details {
355 padding: 0 1em;
 
 
356 white-space: pre;
357 font-family: monospace;
358 margin: auto;
359 flex: 0;
360 }
361
362 body.chat #chat-drop-details img {
363 max-width: 45%;
364 max-height: 45%;
365 }
366 body.chat .chat-view {
367 flex: 20 1 auto
368 /*ensure that these grow more than the non-.chat-view elements.
369 Note that setting flex shrink to 0 breaks/disables scrolling!*/;
370 margin-bottom: 0.2em;
371 }
372 body.chat #chat-config,
373 body.chat #chat-preview {
374 /* /chat configuration widget */
@@ -289,28 +382,41 @@
382 }
383 body.chat #chat-config #chat-config-options {
384 /* /chat config options go here */
385 flex: 1 1 auto;
386 display: flex;
387 flex-direction: column;
388 overflow: auto;
389 }
390 body.chat #chat-config #chat-config-options .menu-entry {
391 display: flex;
392 align-items: baseline;
393 flex-direction: row;
394 flex-wrap: nowrap;
395 padding: 1em;
396 }
397 body.chat #chat-config #chat-config-options .menu-entry label[for] {
398 cursor: pointer;
399 }
400 body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
401 min-width: 1.5rem;
402 }
403 body.chat #chat-config #chat-config-options .menu-entry span.hint {
404 /* Config menu hint text */
405 font-size: 80%;
406 white-space: pre-wrap;
407 display: inline-block;
408 }
409 body.chat #chat-config #chat-config-options .menu-entry:first-child {
410 }
411 body.chat #chat-config #chat-config-options .menu-entry div.label-wrapper {
412 display: flex;
413 flex-direction: column;
414 align-self: baseline;
415 margin-left: 1em;
416 }
417 body.chat #chat-config #chat-config-options .menu-entry select {
 
418 }
419 body.chat #chat-preview #chat-preview-content {
420 overflow: auto;
421 flex: 1 1 auto;
422 padding: 0.5em;
@@ -394,10 +500,16 @@
500 }
501 body.chat #chat-user-list .chat-user.selected {
502 font-weight: bold;
503 text-decoration: underline;
504 }
505
506 body.chat.fossil-dark-style #chat-message-attach > svg {
507 /* The black paperclip is barely visible in dark-mode
508 skins when they have dark buttons */
509 filter: invert(0.5);
510 }
511
512 body.chat .anim-rotate-360 {
513 animation: rotate-360 750ms linear;
514 }
515 @keyframes rotate-360 {
516
+13 -9
--- www/changes.wiki
+++ www/changes.wiki
@@ -36,19 +36,23 @@
3636
* The [/help?cmd=wiki|wiki list command] no longer lists "deleted"
3737
pages by default. Use the new <tt>--all</tt> option to include deleted
3838
pages in the output.
3939
* The [/help?cmd=all|fossil all git status] command only shows reports for
4040
the subset of repositories that have a configured Git export.
41
- * Enhanced the [/help?cmd=/chat|/chat page] configuration and added the ability
42
- for a repository administrator to [./chat.md#notifications|extend the
43
- selection of notification sounds via unversioned files].
44
- * The [/help?cmd=/chat|/chat] messages now use fossil's full complement of
45
- markdown features, instead of the prior small subset of markup it
46
- previously supported. This retroactively applies to all chat messages,
47
- as they are markdown-processed when they are sent instead of when they
48
- are saved. Added a preview mode so messages can be previewed before
49
- being sent. See [./chat.md#usage|the chat docs] for more details.
41
+ * The [/help?cmd=/chat|/chat] configuration was reimplemented and
42
+ provides new options, including the ability for a repository
43
+ administrator to
44
+ [./chat.md#notifications|extend the selection of notification sounds]
45
+ using unversioned files.
46
+ * Chat now uses fossil's full complement of markdown features,
47
+ instead of the prior small subset of markup it previously supported.
48
+ This retroactively applies to all chat messages, as they are
49
+ markdown-processed when they are sent instead of when they
50
+ are saved.
51
+ * Added a chat message preview mode so messages can be previewed
52
+ before being sent. Similarly, added a per-message ability to view
53
+ the raw un-parsed message text.
5054
* The hotkey to activate preview mode in [/help?cmd=/wikiedit|/wikiedit],
5155
[/help?cmd=/fileedit|/fileedit], and [/help?cmd=/pikchrshow|/pikchrshow]
5256
was changed from ctrl-enter to shift-enter in order to align with
5357
[/help?cmd=/chat|/chat]'s new preview feature and related future
5458
changes.
5559
--- www/changes.wiki
+++ www/changes.wiki
@@ -36,19 +36,23 @@
36 * The [/help?cmd=wiki|wiki list command] no longer lists "deleted"
37 pages by default. Use the new <tt>--all</tt> option to include deleted
38 pages in the output.
39 * The [/help?cmd=all|fossil all git status] command only shows reports for
40 the subset of repositories that have a configured Git export.
41 * Enhanced the [/help?cmd=/chat|/chat page] configuration and added the ability
42 for a repository administrator to [./chat.md#notifications|extend the
43 selection of notification sounds via unversioned files].
44 * The [/help?cmd=/chat|/chat] messages now use fossil's full complement of
45 markdown features, instead of the prior small subset of markup it
46 previously supported. This retroactively applies to all chat messages,
47 as they are markdown-processed when they are sent instead of when they
48 are saved. Added a preview mode so messages can be previewed before
49 being sent. See [./chat.md#usage|the chat docs] for more details.
 
 
 
 
50 * The hotkey to activate preview mode in [/help?cmd=/wikiedit|/wikiedit],
51 [/help?cmd=/fileedit|/fileedit], and [/help?cmd=/pikchrshow|/pikchrshow]
52 was changed from ctrl-enter to shift-enter in order to align with
53 [/help?cmd=/chat|/chat]'s new preview feature and related future
54 changes.
55
--- www/changes.wiki
+++ www/changes.wiki
@@ -36,19 +36,23 @@
36 * The [/help?cmd=wiki|wiki list command] no longer lists "deleted"
37 pages by default. Use the new <tt>--all</tt> option to include deleted
38 pages in the output.
39 * The [/help?cmd=all|fossil all git status] command only shows reports for
40 the subset of repositories that have a configured Git export.
41 * The [/help?cmd=/chat|/chat] configuration was reimplemented and
42 provides new options, including the ability for a repository
43 administrator to
44 [./chat.md#notifications|extend the selection of notification sounds]
45 using unversioned files.
46 * Chat now uses fossil's full complement of markdown features,
47 instead of the prior small subset of markup it previously supported.
48 This retroactively applies to all chat messages, as they are
49 markdown-processed when they are sent instead of when they
50 are saved.
51 * Added a chat message preview mode so messages can be previewed
52 before being sent. Similarly, added a per-message ability to view
53 the raw un-parsed message text.
54 * The hotkey to activate preview mode in [/help?cmd=/wikiedit|/wikiedit],
55 [/help?cmd=/fileedit|/fileedit], and [/help?cmd=/pikchrshow|/pikchrshow]
56 was changed from ctrl-enter to shift-enter in order to align with
57 [/help?cmd=/chat|/chat]'s new preview feature and related future
58 changes.
59

Keyboard Shortcuts

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