Fossil SCM

/chat: fixed an error reporting bug which could cause server-triggered errors to not be displayed. When sending a message fails, the failed message is now presented as an error message, along with buttons to either retry or discard the message.

stephan 2021-10-12 20:28 trunk
Commit 9d693ef80a00290cf9a8daaf8a6efdfc8e51105f7b7927c4e9b23ead59985366
+1 -1
--- src/chat.c
+++ src/chat.c
@@ -358,11 +358,11 @@
358358
void chat_send_webpage(void){
359359
int nByte;
360360
const char *zMsg;
361361
const char *zUserName;
362362
login_check_credentials();
363
- if( !g.perm.Chat ) {
363
+ if( 0==g.perm.Chat ) {
364364
chat_emit_permissions_error(0);
365365
return;
366366
}
367367
chat_create_tables();
368368
zUserName = (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody";
369369
--- src/chat.c
+++ src/chat.c
@@ -358,11 +358,11 @@
358 void chat_send_webpage(void){
359 int nByte;
360 const char *zMsg;
361 const char *zUserName;
362 login_check_credentials();
363 if( !g.perm.Chat ) {
364 chat_emit_permissions_error(0);
365 return;
366 }
367 chat_create_tables();
368 zUserName = (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody";
369
--- src/chat.c
+++ src/chat.c
@@ -358,11 +358,11 @@
358 void chat_send_webpage(void){
359 int nByte;
360 const char *zMsg;
361 const char *zUserName;
362 login_check_credentials();
363 if( 0==g.perm.Chat ) {
364 chat_emit_permissions_error(0);
365 return;
366 }
367 chat_create_tables();
368 zUserName = (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody";
369
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -43,10 +43,11 @@
4343
const iso8601ish = function(d){
4444
return d.toISOString()
4545
.replace('T',' ').replace(/\.\d+/,'')
4646
.replace('Z', ' zulu');
4747
};
48
+ const pad2 = (x)=>('0'+x).substr(-2);
4849
/** Returns the local time string of Date object d, defaulting
4950
to the current time. */
5051
const localTimeString = function ff(d){
5152
d || (d = new Date());
5253
return [
@@ -620,18 +621,21 @@
620621
Reports an error in the form of a new message in the chat
621622
feed. All arguments are appended to the message's content area
622623
using fossil.dom.append(), so may be of any type supported by
623624
that function.
624625
*/
625
- cs.reportErrorAsMessage = function(/*msg args*/){
626
- const args = argsToArray(arguments);
626
+ cs.reportErrorAsMessage = function f(/*msg args*/){
627
+ if(undefined === f.$msgid) f.$msgid=0;
628
+ const args = argsToArray(arguments).map(function(v){
629
+ return (v instanceof Error) ? v.message : v;
630
+ });
627631
console.error("chat error:",args);
628632
const d = new Date().toISOString(),
629633
mw = new this.MessageWidget({
630634
isError: true,
631635
xfrom: null,
632
- msgid: -1,
636
+ msgid: "error-"+(++f.$msgid),
633637
mtime: d,
634638
lmtime: d,
635639
xmsg: args
636640
});
637641
this.injectMessageElem(mw.e.body);
@@ -833,10 +837,20 @@
833837
return false;
834838
}, false);
835839
return cs;
836840
})()/*Chat initialization*/;
837841
842
+
843
+ /** Returns the first .message-widget element in DOM element
844
+ e's lineage. */
845
+ const findMessageWidgetParent = function(e){
846
+ while( e && !e.classList.contains('message-widget')){
847
+ e = e.parentNode;
848
+ }
849
+ return e;
850
+ };
851
+
838852
/**
839853
Custom widget type for rendering messages (one message per
840854
instance). These are modelled after FIELDSET elements but we
841855
don't use FIELDSET because of cross-browser inconsistencies in
842856
features of the FIELDSET/LEGEND combination, e.g. inability to
@@ -859,11 +873,10 @@
859873
if(arguments.length){
860874
this.setMessage(arguments[0]);
861875
}
862876
};
863877
/* Left-zero-pad a number to at least 2 digits */
864
- const pad2 = (x)=>(''+x).length>1 ? x : '0'+x;
865878
const dowMap = {
866879
/* Map of Date.getDay() values to weekday names. */
867880
0: "Sunday", 1: "Monday", 2: "Tuesday",
868881
3: "Wednesday", 4: "Thursday", 5: "Friday",
869882
6: "Saturday"
@@ -877,10 +890,11 @@
877890
d.getHours(),":",
878891
(d.getMinutes()+100).toString().slice(1,3),
879892
' ', dowMap[d.getDay()]
880893
].join('');
881894
};
895
+
882896
cf.prototype = {
883897
scrollIntoView: function(){
884898
this.e.content.scrollIntoView();
885899
},
886900
setMessage: function(m){
@@ -911,11 +925,11 @@
911925
if(m.isError){
912926
D.addClass([contentTarget, this.e.tab], 'error');
913927
}
914928
D.append(
915929
this.e.tab,
916
- D.text('notification @ ',theTime(d))
930
+ D.append(D.code(), 'notification @ ',theTime(d))
917931
);
918932
}
919933
if( m.xfrom && m.fsize>0 ){
920934
if( m.fmime
921935
&& m.fmime.startsWith("image/")
@@ -948,11 +962,11 @@
948962
// hyperlinks, but otherwise it will be markup-free. See the
949963
// chat_format_to_html() routine in the server for details.
950964
//
951965
// Hence, even though innerHTML is normally frowned upon, it is
952966
// perfectly safe to use in this context.
953
- if(m.xmsg instanceof Array){
967
+ if(m.xmsg && 'string' !== typeof m.xmsg){
954968
// Used by Chat.reportErrorAsMessage()
955969
D.append(contentTarget, m.xmsg);
956970
}else{
957971
contentTarget.innerHTML = m.xmsg;
958972
contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
@@ -959,10 +973,12 @@
959973
if(F.pikchr){
960974
F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
961975
}
962976
}
963977
}
978
+ //console.debug("tab",this.e.tab);
979
+ //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild);
964980
this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
965981
/*if(eXFrom){
966982
eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
967983
}*/
968984
return this;
@@ -1099,14 +1115,11 @@
10991115
this.$eMsg = tgtMsg;
11001116
this.refresh();
11011117
}
11021118
}/*f.popup*/;
11031119
}/*end static init*/
1104
- let theMsg = ev.target;
1105
- while( theMsg && !theMsg.classList.contains('message-widget')){
1106
- theMsg = theMsg.parentNode;
1107
- }
1120
+ const theMsg = findMessageWidgetParent(ev.target);
11081121
if(theMsg) f.popup.show(theMsg);
11091122
}/*_handleLegendClicked()*/
11101123
};
11111124
return cf;
11121125
})()/*MessageWidget*/;
@@ -1123,11 +1136,11 @@
11231136
}
11241137
};
11251138
/** Updates the paste/drop zone with details of the pasted/dropped
11261139
data. The argument must be a Blob or Blob-like object (File) or
11271140
it can be falsy to reset/clear that state.*/
1128
- const updateDropZoneContent = function(blob){
1141
+ const updateDropZoneContent = bxs.updateDropZoneContent = function(blob){
11291142
//console.debug("updateDropZoneContent()",blob);
11301143
const dd = bxs.dropDetails;
11311144
bxs.blob = blob;
11321145
D.clearElement(dd);
11331146
if(!blob){
@@ -1206,17 +1219,50 @@
12061219
12071220
const tzOffsetToString = function(off){
12081221
const hours = Math.round(off/60), min = Math.round(off % 30);
12091222
return ''+(hours + (min ? '.5' : ''));
12101223
};
1211
- const pad2 = (x)=>('0'+x).substr(-2);
12121224
const localTime8601 = function(d){
12131225
return [
12141226
d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()),
12151227
'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds())
12161228
].join('');
12171229
};
1230
+
1231
+ /**
1232
+ Called by Chat.submitMessage() when message sending failed. Injects a fake message
1233
+ containing the content and attachment of the failed message and gives the user buttons
1234
+ to discard it or edit and retry.
1235
+ */
1236
+ const recoverFailedMessage = function(state){
1237
+ const w = D.addClass(D.div(), 'failed-message');
1238
+ D.append(w, D.append(
1239
+ D.span(),"This message was not successfully sent to the server:"
1240
+ ));
1241
+ if(state.msg){
1242
+ const ta = D.textarea();
1243
+ ta.value = state.msg;
1244
+ D.append(w,ta);
1245
+ }
1246
+ if(state.blob){
1247
+ D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed")));
1248
+ //console.debug("blob = ",state.blob);
1249
+ }
1250
+ const buttons = D.addClass(D.div(), 'buttons');
1251
+ D.append(w, buttons);
1252
+ D.append(buttons, D.button("Discard message?", function(){
1253
+ let theMsg = findMessageWidgetParent(w);
1254
+ if(theMsg) Chat.deleteMessageElem(theMsg);
1255
+ }));
1256
+ D.append(buttons, D.button("Edit message and try again?", function(){
1257
+ if(state.msg) Chat.inputValue(ta.value);
1258
+ if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
1259
+ let theMsg = findMessageWidgetParent(w);
1260
+ if(theMsg) Chat.deleteMessageElem(theMsg);
1261
+ }));
1262
+ Chat.reportErrorAsMessage(w);
1263
+ };
12181264
12191265
/**
12201266
Submits the contents of the message input field (if not empty)
12211267
and/or the file attachment field to the server. If both are
12221268
empty, this is a no-op.
@@ -1226,11 +1272,12 @@
12261272
f.spaces = /\s+$/;
12271273
f.markdownContinuation = /\\\s+$/;
12281274
}
12291275
this.setCurrentView(this.e.viewMessages);
12301276
const fd = new FormData();
1231
- var msg = this.inputValue().trim();
1277
+ const fallback = {msg: this.inputValue()};
1278
+ var msg = fallback.msg.trim();
12321279
if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
12331280
/* Cosmetic: trim whitespace from the ends of lines to try to
12341281
keep copy/paste from terminals, especially wide ones, from
12351282
forcing a horizontal scrollbar on all clients. This breaks
12361283
markdown's use of blackslash-space-space for paragraph
@@ -1249,25 +1296,29 @@
12491296
}
12501297
if(msg) fd.set('msg',msg);
12511298
const file = BlobXferState.blob || this.e.inputFile.files[0];
12521299
if(file) fd.set("file", file);
12531300
if( !msg && !file ) return;
1301
+ fallback.blob = file;
12541302
const self = this;
12551303
fd.set("lmtime", localTime8601(new Date()));
12561304
F.fetch("chat-send",{
12571305
payload: fd,
12581306
responseType: 'text',
1259
- onerror:(err)=>this.reportErrorAsMessage(err),
1307
+ onerror:function(err){
1308
+ self.reportErrorAsMessage(err);
1309
+ recoverFailedMessage(fallback);
1310
+ },
12601311
onload:function(txt){
12611312
if(!txt) return/*success response*/;
12621313
try{
12631314
const json = JSON.parse(txt);
12641315
self.newContent({msgs:[json]});
12651316
}catch(e){
12661317
self.reportError(e);
1267
- return;
12681318
}
1319
+ recoverFailedMessage(fallback);
12691320
}
12701321
});
12711322
BlobXferState.clear();
12721323
Chat.inputValue("").inputFocus();
12731324
};
12741325
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -43,10 +43,11 @@
43 const iso8601ish = function(d){
44 return d.toISOString()
45 .replace('T',' ').replace(/\.\d+/,'')
46 .replace('Z', ' zulu');
47 };
 
48 /** Returns the local time string of Date object d, defaulting
49 to the current time. */
50 const localTimeString = function ff(d){
51 d || (d = new Date());
52 return [
@@ -620,18 +621,21 @@
620 Reports an error in the form of a new message in the chat
621 feed. All arguments are appended to the message's content area
622 using fossil.dom.append(), so may be of any type supported by
623 that function.
624 */
625 cs.reportErrorAsMessage = function(/*msg args*/){
626 const args = argsToArray(arguments);
 
 
 
627 console.error("chat error:",args);
628 const d = new Date().toISOString(),
629 mw = new this.MessageWidget({
630 isError: true,
631 xfrom: null,
632 msgid: -1,
633 mtime: d,
634 lmtime: d,
635 xmsg: args
636 });
637 this.injectMessageElem(mw.e.body);
@@ -833,10 +837,20 @@
833 return false;
834 }, false);
835 return cs;
836 })()/*Chat initialization*/;
837
 
 
 
 
 
 
 
 
 
 
838 /**
839 Custom widget type for rendering messages (one message per
840 instance). These are modelled after FIELDSET elements but we
841 don't use FIELDSET because of cross-browser inconsistencies in
842 features of the FIELDSET/LEGEND combination, e.g. inability to
@@ -859,11 +873,10 @@
859 if(arguments.length){
860 this.setMessage(arguments[0]);
861 }
862 };
863 /* Left-zero-pad a number to at least 2 digits */
864 const pad2 = (x)=>(''+x).length>1 ? x : '0'+x;
865 const dowMap = {
866 /* Map of Date.getDay() values to weekday names. */
867 0: "Sunday", 1: "Monday", 2: "Tuesday",
868 3: "Wednesday", 4: "Thursday", 5: "Friday",
869 6: "Saturday"
@@ -877,10 +890,11 @@
877 d.getHours(),":",
878 (d.getMinutes()+100).toString().slice(1,3),
879 ' ', dowMap[d.getDay()]
880 ].join('');
881 };
 
882 cf.prototype = {
883 scrollIntoView: function(){
884 this.e.content.scrollIntoView();
885 },
886 setMessage: function(m){
@@ -911,11 +925,11 @@
911 if(m.isError){
912 D.addClass([contentTarget, this.e.tab], 'error');
913 }
914 D.append(
915 this.e.tab,
916 D.text('notification @ ',theTime(d))
917 );
918 }
919 if( m.xfrom && m.fsize>0 ){
920 if( m.fmime
921 && m.fmime.startsWith("image/")
@@ -948,11 +962,11 @@
948 // hyperlinks, but otherwise it will be markup-free. See the
949 // chat_format_to_html() routine in the server for details.
950 //
951 // Hence, even though innerHTML is normally frowned upon, it is
952 // perfectly safe to use in this context.
953 if(m.xmsg instanceof Array){
954 // Used by Chat.reportErrorAsMessage()
955 D.append(contentTarget, m.xmsg);
956 }else{
957 contentTarget.innerHTML = m.xmsg;
958 contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
@@ -959,10 +973,12 @@
959 if(F.pikchr){
960 F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
961 }
962 }
963 }
 
 
964 this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
965 /*if(eXFrom){
966 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
967 }*/
968 return this;
@@ -1099,14 +1115,11 @@
1099 this.$eMsg = tgtMsg;
1100 this.refresh();
1101 }
1102 }/*f.popup*/;
1103 }/*end static init*/
1104 let theMsg = ev.target;
1105 while( theMsg && !theMsg.classList.contains('message-widget')){
1106 theMsg = theMsg.parentNode;
1107 }
1108 if(theMsg) f.popup.show(theMsg);
1109 }/*_handleLegendClicked()*/
1110 };
1111 return cf;
1112 })()/*MessageWidget*/;
@@ -1123,11 +1136,11 @@
1123 }
1124 };
1125 /** Updates the paste/drop zone with details of the pasted/dropped
1126 data. The argument must be a Blob or Blob-like object (File) or
1127 it can be falsy to reset/clear that state.*/
1128 const updateDropZoneContent = function(blob){
1129 //console.debug("updateDropZoneContent()",blob);
1130 const dd = bxs.dropDetails;
1131 bxs.blob = blob;
1132 D.clearElement(dd);
1133 if(!blob){
@@ -1206,17 +1219,50 @@
1206
1207 const tzOffsetToString = function(off){
1208 const hours = Math.round(off/60), min = Math.round(off % 30);
1209 return ''+(hours + (min ? '.5' : ''));
1210 };
1211 const pad2 = (x)=>('0'+x).substr(-2);
1212 const localTime8601 = function(d){
1213 return [
1214 d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()),
1215 'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds())
1216 ].join('');
1217 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1218
1219 /**
1220 Submits the contents of the message input field (if not empty)
1221 and/or the file attachment field to the server. If both are
1222 empty, this is a no-op.
@@ -1226,11 +1272,12 @@
1226 f.spaces = /\s+$/;
1227 f.markdownContinuation = /\\\s+$/;
1228 }
1229 this.setCurrentView(this.e.viewMessages);
1230 const fd = new FormData();
1231 var msg = this.inputValue().trim();
 
1232 if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
1233 /* Cosmetic: trim whitespace from the ends of lines to try to
1234 keep copy/paste from terminals, especially wide ones, from
1235 forcing a horizontal scrollbar on all clients. This breaks
1236 markdown's use of blackslash-space-space for paragraph
@@ -1249,25 +1296,29 @@
1249 }
1250 if(msg) fd.set('msg',msg);
1251 const file = BlobXferState.blob || this.e.inputFile.files[0];
1252 if(file) fd.set("file", file);
1253 if( !msg && !file ) return;
 
1254 const self = this;
1255 fd.set("lmtime", localTime8601(new Date()));
1256 F.fetch("chat-send",{
1257 payload: fd,
1258 responseType: 'text',
1259 onerror:(err)=>this.reportErrorAsMessage(err),
 
 
 
1260 onload:function(txt){
1261 if(!txt) return/*success response*/;
1262 try{
1263 const json = JSON.parse(txt);
1264 self.newContent({msgs:[json]});
1265 }catch(e){
1266 self.reportError(e);
1267 return;
1268 }
 
1269 }
1270 });
1271 BlobXferState.clear();
1272 Chat.inputValue("").inputFocus();
1273 };
1274
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -43,10 +43,11 @@
43 const iso8601ish = function(d){
44 return d.toISOString()
45 .replace('T',' ').replace(/\.\d+/,'')
46 .replace('Z', ' zulu');
47 };
48 const pad2 = (x)=>('0'+x).substr(-2);
49 /** Returns the local time string of Date object d, defaulting
50 to the current time. */
51 const localTimeString = function ff(d){
52 d || (d = new Date());
53 return [
@@ -620,18 +621,21 @@
621 Reports an error in the form of a new message in the chat
622 feed. All arguments are appended to the message's content area
623 using fossil.dom.append(), so may be of any type supported by
624 that function.
625 */
626 cs.reportErrorAsMessage = function f(/*msg args*/){
627 if(undefined === f.$msgid) f.$msgid=0;
628 const args = argsToArray(arguments).map(function(v){
629 return (v instanceof Error) ? v.message : v;
630 });
631 console.error("chat error:",args);
632 const d = new Date().toISOString(),
633 mw = new this.MessageWidget({
634 isError: true,
635 xfrom: null,
636 msgid: "error-"+(++f.$msgid),
637 mtime: d,
638 lmtime: d,
639 xmsg: args
640 });
641 this.injectMessageElem(mw.e.body);
@@ -833,10 +837,20 @@
837 return false;
838 }, false);
839 return cs;
840 })()/*Chat initialization*/;
841
842
843 /** Returns the first .message-widget element in DOM element
844 e's lineage. */
845 const findMessageWidgetParent = function(e){
846 while( e && !e.classList.contains('message-widget')){
847 e = e.parentNode;
848 }
849 return e;
850 };
851
852 /**
853 Custom widget type for rendering messages (one message per
854 instance). These are modelled after FIELDSET elements but we
855 don't use FIELDSET because of cross-browser inconsistencies in
856 features of the FIELDSET/LEGEND combination, e.g. inability to
@@ -859,11 +873,10 @@
873 if(arguments.length){
874 this.setMessage(arguments[0]);
875 }
876 };
877 /* Left-zero-pad a number to at least 2 digits */
 
878 const dowMap = {
879 /* Map of Date.getDay() values to weekday names. */
880 0: "Sunday", 1: "Monday", 2: "Tuesday",
881 3: "Wednesday", 4: "Thursday", 5: "Friday",
882 6: "Saturday"
@@ -877,10 +890,11 @@
890 d.getHours(),":",
891 (d.getMinutes()+100).toString().slice(1,3),
892 ' ', dowMap[d.getDay()]
893 ].join('');
894 };
895
896 cf.prototype = {
897 scrollIntoView: function(){
898 this.e.content.scrollIntoView();
899 },
900 setMessage: function(m){
@@ -911,11 +925,11 @@
925 if(m.isError){
926 D.addClass([contentTarget, this.e.tab], 'error');
927 }
928 D.append(
929 this.e.tab,
930 D.append(D.code(), 'notification @ ',theTime(d))
931 );
932 }
933 if( m.xfrom && m.fsize>0 ){
934 if( m.fmime
935 && m.fmime.startsWith("image/")
@@ -948,11 +962,11 @@
962 // hyperlinks, but otherwise it will be markup-free. See the
963 // chat_format_to_html() routine in the server for details.
964 //
965 // Hence, even though innerHTML is normally frowned upon, it is
966 // perfectly safe to use in this context.
967 if(m.xmsg && 'string' !== typeof m.xmsg){
968 // Used by Chat.reportErrorAsMessage()
969 D.append(contentTarget, m.xmsg);
970 }else{
971 contentTarget.innerHTML = m.xmsg;
972 contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
@@ -959,10 +973,12 @@
973 if(F.pikchr){
974 F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
975 }
976 }
977 }
978 //console.debug("tab",this.e.tab);
979 //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild);
980 this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
981 /*if(eXFrom){
982 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
983 }*/
984 return this;
@@ -1099,14 +1115,11 @@
1115 this.$eMsg = tgtMsg;
1116 this.refresh();
1117 }
1118 }/*f.popup*/;
1119 }/*end static init*/
1120 const theMsg = findMessageWidgetParent(ev.target);
 
 
 
1121 if(theMsg) f.popup.show(theMsg);
1122 }/*_handleLegendClicked()*/
1123 };
1124 return cf;
1125 })()/*MessageWidget*/;
@@ -1123,11 +1136,11 @@
1136 }
1137 };
1138 /** Updates the paste/drop zone with details of the pasted/dropped
1139 data. The argument must be a Blob or Blob-like object (File) or
1140 it can be falsy to reset/clear that state.*/
1141 const updateDropZoneContent = bxs.updateDropZoneContent = function(blob){
1142 //console.debug("updateDropZoneContent()",blob);
1143 const dd = bxs.dropDetails;
1144 bxs.blob = blob;
1145 D.clearElement(dd);
1146 if(!blob){
@@ -1206,17 +1219,50 @@
1219
1220 const tzOffsetToString = function(off){
1221 const hours = Math.round(off/60), min = Math.round(off % 30);
1222 return ''+(hours + (min ? '.5' : ''));
1223 };
 
1224 const localTime8601 = function(d){
1225 return [
1226 d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()),
1227 'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds())
1228 ].join('');
1229 };
1230
1231 /**
1232 Called by Chat.submitMessage() when message sending failed. Injects a fake message
1233 containing the content and attachment of the failed message and gives the user buttons
1234 to discard it or edit and retry.
1235 */
1236 const recoverFailedMessage = function(state){
1237 const w = D.addClass(D.div(), 'failed-message');
1238 D.append(w, D.append(
1239 D.span(),"This message was not successfully sent to the server:"
1240 ));
1241 if(state.msg){
1242 const ta = D.textarea();
1243 ta.value = state.msg;
1244 D.append(w,ta);
1245 }
1246 if(state.blob){
1247 D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed")));
1248 //console.debug("blob = ",state.blob);
1249 }
1250 const buttons = D.addClass(D.div(), 'buttons');
1251 D.append(w, buttons);
1252 D.append(buttons, D.button("Discard message?", function(){
1253 let theMsg = findMessageWidgetParent(w);
1254 if(theMsg) Chat.deleteMessageElem(theMsg);
1255 }));
1256 D.append(buttons, D.button("Edit message and try again?", function(){
1257 if(state.msg) Chat.inputValue(ta.value);
1258 if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
1259 let theMsg = findMessageWidgetParent(w);
1260 if(theMsg) Chat.deleteMessageElem(theMsg);
1261 }));
1262 Chat.reportErrorAsMessage(w);
1263 };
1264
1265 /**
1266 Submits the contents of the message input field (if not empty)
1267 and/or the file attachment field to the server. If both are
1268 empty, this is a no-op.
@@ -1226,11 +1272,12 @@
1272 f.spaces = /\s+$/;
1273 f.markdownContinuation = /\\\s+$/;
1274 }
1275 this.setCurrentView(this.e.viewMessages);
1276 const fd = new FormData();
1277 const fallback = {msg: this.inputValue()};
1278 var msg = fallback.msg.trim();
1279 if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
1280 /* Cosmetic: trim whitespace from the ends of lines to try to
1281 keep copy/paste from terminals, especially wide ones, from
1282 forcing a horizontal scrollbar on all clients. This breaks
1283 markdown's use of blackslash-space-space for paragraph
@@ -1249,25 +1296,29 @@
1296 }
1297 if(msg) fd.set('msg',msg);
1298 const file = BlobXferState.blob || this.e.inputFile.files[0];
1299 if(file) fd.set("file", file);
1300 if( !msg && !file ) return;
1301 fallback.blob = file;
1302 const self = this;
1303 fd.set("lmtime", localTime8601(new Date()));
1304 F.fetch("chat-send",{
1305 payload: fd,
1306 responseType: 'text',
1307 onerror:function(err){
1308 self.reportErrorAsMessage(err);
1309 recoverFailedMessage(fallback);
1310 },
1311 onload:function(txt){
1312 if(!txt) return/*success response*/;
1313 try{
1314 const json = JSON.parse(txt);
1315 self.newContent({msgs:[json]});
1316 }catch(e){
1317 self.reportError(e);
 
1318 }
1319 recoverFailedMessage(fallback);
1320 }
1321 });
1322 BlobXferState.clear();
1323 Chat.inputValue("").inputFocus();
1324 };
1325
--- src/style.chat.css
+++ src/style.chat.css
@@ -56,10 +56,31 @@
5656
margin-top: 0;
5757
}
5858
body.chat .message-widget-content > .markdown > *:last-child {
5959
margin-bottom: 0;
6060
}
61
+body.chat .message-widget-content.error .buttons {
62
+ display: flex;
63
+ flex-direction: row;
64
+ justify-content: space-around;
65
+ flex-wrap: wrap;
66
+}
67
+body.chat .message-widget-content.error .buttons > button {
68
+ margin: 0.25em;
69
+}
70
+
71
+body.chat .message-widget-content.error a {
72
+ color: inherit;
73
+}
74
+body.chat .message-widget-content.error .failed-message {
75
+ display: flex;
76
+ flex-direction: column;
77
+}
78
+body.chat .message-widget-content.error .failed-message textarea {
79
+ min-height: 5rem;
80
+}
81
+
6182
/* User name and timestamp (a LEGEND-like element) */
6283
body.chat .message-widget .message-widget-tab {
6384
border-radius: 0.25em 0.25em 0 0;
6485
margin: 0 0.25em 0em 0.15em;
6586
padding: 0 0.5em 0.15em 0.5em;
6687
--- src/style.chat.css
+++ src/style.chat.css
@@ -56,10 +56,31 @@
56 margin-top: 0;
57 }
58 body.chat .message-widget-content > .markdown > *:last-child {
59 margin-bottom: 0;
60 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61 /* User name and timestamp (a LEGEND-like element) */
62 body.chat .message-widget .message-widget-tab {
63 border-radius: 0.25em 0.25em 0 0;
64 margin: 0 0.25em 0em 0.15em;
65 padding: 0 0.5em 0.15em 0.5em;
66
--- src/style.chat.css
+++ src/style.chat.css
@@ -56,10 +56,31 @@
56 margin-top: 0;
57 }
58 body.chat .message-widget-content > .markdown > *:last-child {
59 margin-bottom: 0;
60 }
61 body.chat .message-widget-content.error .buttons {
62 display: flex;
63 flex-direction: row;
64 justify-content: space-around;
65 flex-wrap: wrap;
66 }
67 body.chat .message-widget-content.error .buttons > button {
68 margin: 0.25em;
69 }
70
71 body.chat .message-widget-content.error a {
72 color: inherit;
73 }
74 body.chat .message-widget-content.error .failed-message {
75 display: flex;
76 flex-direction: column;
77 }
78 body.chat .message-widget-content.error .failed-message textarea {
79 min-height: 5rem;
80 }
81
82 /* User name and timestamp (a LEGEND-like element) */
83 body.chat .message-widget .message-widget-tab {
84 border-radius: 0.25em 0.25em 0 0;
85 margin: 0 0.25em 0em 0.15em;
86 padding: 0 0.5em 0.15em 0.5em;
87
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,10 +1,12 @@
11
<title>Change Log</title>
22
33
<h2 id='v2_18'>Changes for version 2.18 (pending)</h2>
44
* [/help?cmd=/chat|The /chat page] input options have been reworked
55
again for better cross-browser portability.
6
+ * When sending a [/help?cmd=/chat|/chat] message fails, it is no longer
7
+ immediately lost and sending may optionally be retried.
68
79
<h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
810
911
* Major improvements to the "diff" subsystem, including: <ul>
1012
<li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
1113
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,10 +1,12 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_18'>Changes for version 2.18 (pending)</h2>
4 * [/help?cmd=/chat|The /chat page] input options have been reworked
5 again for better cross-browser portability.
 
 
6
7 <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
8
9 * Major improvements to the "diff" subsystem, including: <ul>
10 <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
11
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,10 +1,12 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_18'>Changes for version 2.18 (pending)</h2>
4 * [/help?cmd=/chat|The /chat page] input options have been reworked
5 again for better cross-browser portability.
6 * When sending a [/help?cmd=/chat|/chat] message fails, it is no longer
7 immediately lost and sending may optionally be retried.
8
9 <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
10
11 * Major improvements to the "diff" subsystem, including: <ul>
12 <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
13

Keyboard Shortcuts

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