Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | /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. |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
9d693ef80a00290cf9a8daaf8a6efdfc |
User & Date: | stephan 2021-10-12 20:28:20 |
Context
2021-10-13
| ||
10:01 | Applied SSL fingerprint comparison patch from forum post c1e3c18afb. Incremented version to 2.18. ... (check-in: 48a860f6 user: stephan tags: trunk) | |
2021-10-12
| ||
20:28 | /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. ... (check-in: 9d693ef8 user: stephan tags: trunk) | |
17:11 | Corrected misuse of g.argv in /ci and /ci_tags pages, per forum post 74ec2261df. ... (check-in: ba3323da user: stephan tags: trunk) | |
Changes
Changes to src/chat.c.
︙ | ︙ | |||
356 357 358 359 360 361 362 | ** list of objects. */ void chat_send_webpage(void){ int nByte; const char *zMsg; const char *zUserName; login_check_credentials(); | | | 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | ** list of objects. */ void chat_send_webpage(void){ int nByte; const char *zMsg; const char *zUserName; login_check_credentials(); if( 0==g.perm.Chat ) { chat_emit_permissions_error(0); return; } chat_create_tables(); zUserName = (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody"; nByte = atoi(PD("file:bytes","0")); zMsg = PD("msg",""); |
︙ | ︙ |
Changes to src/fossil.page.chat.js.
︙ | ︙ | |||
41 42 43 44 45 46 47 48 49 50 51 52 53 54 | Returns an almost-ISO8601 form of Date object d. */ const iso8601ish = function(d){ return d.toISOString() .replace('T',' ').replace(/\.\d+/,'') .replace('Z', ' zulu'); }; /** Returns the local time string of Date object d, defaulting to the current time. */ const localTimeString = function ff(d){ d || (d = new Date()); return [ d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), '-',pad2(d.getDate()), | > | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | Returns an almost-ISO8601 form of Date object d. */ const iso8601ish = function(d){ return d.toISOString() .replace('T',' ').replace(/\.\d+/,'') .replace('Z', ' zulu'); }; const pad2 = (x)=>('0'+x).substr(-2); /** Returns the local time string of Date object d, defaulting to the current time. */ const localTimeString = function ff(d){ d || (d = new Date()); return [ d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), '-',pad2(d.getDate()), |
︙ | ︙ | |||
618 619 620 621 622 623 624 | }; /** Reports an error in the form of a new message in the chat feed. All arguments are appended to the message's content area using fossil.dom.append(), so may be of any type supported by that function. */ | | > | > > | | 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 | }; /** Reports an error in the form of a new message in the chat feed. All arguments are appended to the message's content area using fossil.dom.append(), so may be of any type supported by that function. */ cs.reportErrorAsMessage = function f(/*msg args*/){ if(undefined === f.$msgid) f.$msgid=0; const args = argsToArray(arguments).map(function(v){ return (v instanceof Error) ? v.message : v; }); console.error("chat error:",args); const d = new Date().toISOString(), mw = new this.MessageWidget({ isError: true, xfrom: null, msgid: "error-"+(++f.$msgid), mtime: d, lmtime: d, xmsg: args }); this.injectMessageElem(mw.e.body); mw.scrollIntoView(); }; |
︙ | ︙ | |||
831 832 833 834 835 836 837 838 839 840 841 842 843 844 | cs.setUserFilter(uname); } return false; }, false); return cs; })()/*Chat initialization*/; /** Custom widget type for rendering messages (one message per instance). These are modelled after FIELDSET elements but we don't use FIELDSET because of cross-browser inconsistencies in features of the FIELDSET/LEGEND combination, e.g. inability to align legends via CSS in Firefox and clicking-related deficiencies in Safari. | > > > > > > > > > > | 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 | cs.setUserFilter(uname); } return false; }, false); return cs; })()/*Chat initialization*/; /** Returns the first .message-widget element in DOM element e's lineage. */ const findMessageWidgetParent = function(e){ while( e && !e.classList.contains('message-widget')){ e = e.parentNode; } return e; }; /** Custom widget type for rendering messages (one message per instance). These are modelled after FIELDSET elements but we don't use FIELDSET because of cross-browser inconsistencies in features of the FIELDSET/LEGEND combination, e.g. inability to align legends via CSS in Firefox and clicking-related deficiencies in Safari. |
︙ | ︙ | |||
857 858 859 860 861 862 863 | D.append(this.e.body, this.e.tab, this.e.content); this.e.tab.setAttribute('role', 'button'); if(arguments.length){ this.setMessage(arguments[0]); } }; /* Left-zero-pad a number to at least 2 digits */ | < > | 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | D.append(this.e.body, this.e.tab, this.e.content); this.e.tab.setAttribute('role', 'button'); if(arguments.length){ this.setMessage(arguments[0]); } }; /* Left-zero-pad a number to at least 2 digits */ const dowMap = { /* Map of Date.getDay() values to weekday names. */ 0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday" }; /* Given a Date, returns the timestamp string used in the "tab" part of message widgets. */ const theTime = function(d){ return [ //d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), //'-',pad2(d.getDate()), ' ', d.getHours(),":", (d.getMinutes()+100).toString().slice(1,3), ' ', dowMap[d.getDay()] ].join(''); }; cf.prototype = { scrollIntoView: function(){ this.e.content.scrollIntoView(); }, setMessage: function(m){ const ds = this.e.body.dataset; ds.timestamp = m.mtime; |
︙ | ︙ | |||
909 910 911 912 913 914 915 | }else{/*notification*/ D.addClass(this.e.body, 'notification'); if(m.isError){ D.addClass([contentTarget, this.e.tab], 'error'); } D.append( this.e.tab, | | | 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 | }else{/*notification*/ D.addClass(this.e.body, 'notification'); if(m.isError){ D.addClass([contentTarget, this.e.tab], 'error'); } D.append( this.e.tab, D.append(D.code(), 'notification @ ',theTime(d)) ); } if( m.xfrom && m.fsize>0 ){ if( m.fmime && m.fmime.startsWith("image/") && Chat.settings.getBool('images-inline',true) ){ |
︙ | ︙ | |||
946 947 948 949 950 951 952 | // sense that it is not possible for a malefactor to inject HTML // or javascript or CSS. The m.xmsg content might contain // hyperlinks, but otherwise it will be markup-free. See the // chat_format_to_html() routine in the server for details. // // Hence, even though innerHTML is normally frowned upon, it is // perfectly safe to use in this context. | | > > | 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 | // sense that it is not possible for a malefactor to inject HTML // or javascript or CSS. The m.xmsg content might contain // hyperlinks, but otherwise it will be markup-free. See the // chat_format_to_html() routine in the server for details. // // Hence, even though innerHTML is normally frowned upon, it is // perfectly safe to use in this context. if(m.xmsg && 'string' !== typeof m.xmsg){ // Used by Chat.reportErrorAsMessage() D.append(contentTarget, m.xmsg); }else{ contentTarget.innerHTML = m.xmsg; contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank); if(F.pikchr){ F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr')); } } } //console.debug("tab",this.e.tab); //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild); this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false); /*if(eXFrom){ eXFrom.addEventListener('click', ()=>this.e.tab.click(), false); }*/ return this; }, /* Event handler for clicking .message-user elements to show their |
︙ | ︙ | |||
1097 1098 1099 1100 1101 1102 1103 | return; } this.$eMsg = tgtMsg; this.refresh(); } }/*f.popup*/; }/*end static init*/ | | < < < | | 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 | return; } this.$eMsg = tgtMsg; this.refresh(); } }/*f.popup*/; }/*end static init*/ const theMsg = findMessageWidgetParent(ev.target); if(theMsg) f.popup.show(theMsg); }/*_handleLegendClicked()*/ }; return cf; })()/*MessageWidget*/; const BlobXferState = (function(){ /* State for paste and drag/drop */ const bxs = { dropDetails: document.querySelector('#chat-drop-details'), blob: undefined, clear: function(){ this.blob = undefined; D.clearElement(this.dropDetails); Chat.e.inputFile.value = ""; } }; /** Updates the paste/drop zone with details of the pasted/dropped data. The argument must be a Blob or Blob-like object (File) or it can be falsy to reset/clear that state.*/ const updateDropZoneContent = bxs.updateDropZoneContent = function(blob){ //console.debug("updateDropZoneContent()",blob); const dd = bxs.dropDetails; bxs.blob = blob; D.clearElement(dd); if(!blob){ Chat.e.inputFile.value = ''; return; |
︙ | ︙ | |||
1204 1205 1206 1207 1208 1209 1210 | return bxs; })()/*drag/drop/paste*/; const tzOffsetToString = function(off){ const hours = Math.round(off/60), min = Math.round(off % 30); return ''+(hours + (min ? '.5' : '')); }; | < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 | return bxs; })()/*drag/drop/paste*/; const tzOffsetToString = function(off){ const hours = Math.round(off/60), min = Math.round(off % 30); return ''+(hours + (min ? '.5' : '')); }; const localTime8601 = function(d){ return [ d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()), 'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds()) ].join(''); }; /** Called by Chat.submitMessage() when message sending failed. Injects a fake message containing the content and attachment of the failed message and gives the user buttons to discard it or edit and retry. */ const recoverFailedMessage = function(state){ const w = D.addClass(D.div(), 'failed-message'); D.append(w, D.append( D.span(),"This message was not successfully sent to the server:" )); if(state.msg){ const ta = D.textarea(); ta.value = state.msg; D.append(w,ta); } if(state.blob){ D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed"))); //console.debug("blob = ",state.blob); } const buttons = D.addClass(D.div(), 'buttons'); D.append(w, buttons); D.append(buttons, D.button("Discard message?", function(){ let theMsg = findMessageWidgetParent(w); if(theMsg) Chat.deleteMessageElem(theMsg); })); D.append(buttons, D.button("Edit message and try again?", function(){ if(state.msg) Chat.inputValue(ta.value); if(state.blob) BlobXferState.updateDropZoneContent(state.blob); let theMsg = findMessageWidgetParent(w); if(theMsg) Chat.deleteMessageElem(theMsg); })); Chat.reportErrorAsMessage(w); }; /** Submits the contents of the message input field (if not empty) and/or the file attachment field to the server. If both are empty, this is a no-op. */ Chat.submitMessage = function f(){ if(!f.spaces){ f.spaces = /\s+$/; f.markdownContinuation = /\\\s+$/; } this.setCurrentView(this.e.viewMessages); const fd = new FormData(); const fallback = {msg: this.inputValue()}; var msg = fallback.msg.trim(); if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ /* Cosmetic: trim whitespace from the ends of lines to try to keep copy/paste from terminals, especially wide ones, from forcing a horizontal scrollbar on all clients. This breaks markdown's use of blackslash-space-space for paragraph continuation, but *not* doing this affects all clients every time someone pastes in console copy/paste from an affected |
︙ | ︙ | |||
1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 | }); msg = xmsg.join('\n'); } if(msg) fd.set('msg',msg); const file = BlobXferState.blob || this.e.inputFile.files[0]; if(file) fd.set("file", file); if( !msg && !file ) return; const self = this; fd.set("lmtime", localTime8601(new Date())); F.fetch("chat-send",{ payload: fd, responseType: 'text', | > > | > > < > | 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 | }); msg = xmsg.join('\n'); } if(msg) fd.set('msg',msg); const file = BlobXferState.blob || this.e.inputFile.files[0]; if(file) fd.set("file", file); if( !msg && !file ) return; fallback.blob = file; const self = this; fd.set("lmtime", localTime8601(new Date())); F.fetch("chat-send",{ payload: fd, responseType: 'text', onerror:function(err){ self.reportErrorAsMessage(err); recoverFailedMessage(fallback); }, onload:function(txt){ if(!txt) return/*success response*/; try{ const json = JSON.parse(txt); self.newContent({msgs:[json]}); }catch(e){ self.reportError(e); } recoverFailedMessage(fallback); } }); BlobXferState.clear(); Chat.inputValue("").inputFocus(); }; const inputWidgetKeydown = function f(ev){ |
︙ | ︙ |
Changes to src/style.chat.css.
︙ | ︙ | |||
54 55 56 57 58 59 60 61 62 63 64 65 66 67 | } body.chat .message-widget-content > .markdown > *:first-child { margin-top: 0; } body.chat .message-widget-content > .markdown > *:last-child { margin-bottom: 0; } /* User name and timestamp (a LEGEND-like element) */ body.chat .message-widget .message-widget-tab { border-radius: 0.25em 0.25em 0 0; margin: 0 0.25em 0em 0.15em; padding: 0 0.5em 0.15em 0.5em; cursor: pointer; white-space: nowrap; | > > > > > > > > > > > > > > > > > > > > > | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | } body.chat .message-widget-content > .markdown > *:first-child { margin-top: 0; } body.chat .message-widget-content > .markdown > *:last-child { margin-bottom: 0; } body.chat .message-widget-content.error .buttons { display: flex; flex-direction: row; justify-content: space-around; flex-wrap: wrap; } body.chat .message-widget-content.error .buttons > button { margin: 0.25em; } body.chat .message-widget-content.error a { color: inherit; } body.chat .message-widget-content.error .failed-message { display: flex; flex-direction: column; } body.chat .message-widget-content.error .failed-message textarea { min-height: 5rem; } /* User name and timestamp (a LEGEND-like element) */ body.chat .message-widget .message-widget-tab { border-radius: 0.25em 0.25em 0 0; margin: 0 0.25em 0em 0.15em; padding: 0 0.5em 0.15em 0.5em; cursor: pointer; white-space: nowrap; |
︙ | ︙ |
Changes to www/changes.wiki.
1 2 3 4 5 6 7 8 9 10 11 12 | <title>Change Log</title> <h2 id='v2_18'>Changes for version 2.18 (pending)</h2> * [/help?cmd=/chat|The /chat page] input options have been reworked again for better cross-browser portability. <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2> * Major improvements to the "diff" subsystem, including: <ul> <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl. <li> Partial-line matching for unified diffs <li> Better partial-line matching for side-by-side diffs | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <title>Change Log</title> <h2 id='v2_18'>Changes for version 2.18 (pending)</h2> * [/help?cmd=/chat|The /chat page] input options have been reworked again for better cross-browser portability. * When sending a [/help?cmd=/chat|/chat] message fails, it is no longer immediately lost and sending may optionally be retried. <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2> * Major improvements to the "diff" subsystem, including: <ul> <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl. <li> Partial-line matching for unified diffs <li> Better partial-line matching for side-by-side diffs |
︙ | ︙ |