Fossil SCM

chat: send and poll can now report if the session is logged out, and client-side poll stops looping if that condition is detected. Both cases emit a message in the message area, from user 'fossil', with the CSS class 'error' and a link to the login page with a redirect back to the chat page.

stephan 2020-12-29 04:18 trunk
Commit 3c0c8954c73f7a523b9211d0882b000793eee4dec3d70ab5b605b0426c6ea4b6
2 files changed +61 -4 +45 -19
+61 -4
--- src/chat.c
+++ src/chat.c
@@ -219,22 +219,57 @@
219219
db_step(&s);
220220
db_finalize(&s);
221221
}
222222
}
223223
}
224
+
225
+/*
226
+** Sets the current CGI response type to application/json then emits a
227
+** JSON-format error message object. If fAsMessageList is true then
228
+** the object is output using the list format described for chat-post,
229
+** else it is emitted as a single object in that same format.
230
+*/
231
+static void chat_emit_permissions_error(int fAsMessageList){
232
+ char * zTime = cgi_iso8601_datestamp();
233
+ cgi_set_content_type("application/json");
234
+ if(fAsMessageList){
235
+ CX("{\"msgs\":[{");
236
+ }else{
237
+ CX("{");
238
+ }
239
+ CX("\"isError\": true, \"xfrom\": \"fossil\",");
240
+ CX("\"mtime\": %!j, \"lmtime\": %!j,", zTime, zTime);
241
+ CX("\"xmsg\": \"Missing permissions or not logged in. "
242
+ "Try <a href='%R/login?g=%R/chat'>logging in</a>.\"");
243
+ if(fAsMessageList){
244
+ CX("}]}");
245
+ }else{
246
+ CX("}");
247
+ }
248
+ fossil_free(zTime);
249
+}
224250
225251
/*
226252
** WEBPAGE: chat-send
227253
**
228254
** This page receives (via XHR) a new chat-message and/or a new file
229255
** to be entered into the chat history.
256
+**
257
+** On success it responds with an empty response: the new message
258
+** should be fetched via /chat-poll. On error, e.g. login expiry,
259
+** it emits a JSON response in the same form as described for
260
+** /chat-poll errors, but as a standalone object instead of a
261
+** list of objects.
230262
*/
231263
void chat_send_webpage(void){
232264
int nByte;
233265
const char *zMsg;
234266
login_check_credentials();
235
- if( !g.perm.Chat ) return;
267
+ if( !g.perm.Chat ) {
268
+ chat_emit_permissions_error(0);
269
+ return;
270
+ }
236271
chat_create_tables();
237272
nByte = atoi(PD("file:bytes",0));
238273
zMsg = PD("msg","");
239274
db_begin_write();
240275
chat_purge();
@@ -394,11 +429,11 @@
394429
**
395430
** The reply from this webpage is JSON that describes the new content.
396431
** Format of the json:
397432
**
398433
** | {
399
-** | "msg":[
434
+** | "msgs":[
400435
** | {
401436
** | "msgid": integer // message id
402437
** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC
403438
** | "lmtime: text // Localtime where the message was sent from
404439
** | "xfrom": text // Login name of sender
@@ -421,10 +456,29 @@
421456
**
422457
** The "lmtime" value might be known, in which case it is omitted.
423458
**
424459
** The messages are ordered oldest first unless "before" is provided, in which
425460
** case they are sorted newest first (to facilitate the client-side UI update).
461
+**
462
+** As a special case, if this routine encounters an error, e.g. the user's
463
+** permissions cannot be verified because their login cookie expired, the
464
+** request returns a slightly modified structure:
465
+**
466
+** | {
467
+** | "msgs":[
468
+** | {
469
+** | "isError": true,
470
+** | "xfrom": "fossil",
471
+** | "xmsg": "error details"
472
+** | "mtime": as above,
473
+** | "ltime": same as mtime
474
+** | }
475
+** | ]
476
+** | }
477
+**
478
+** If the client gets such a response, it should display the message
479
+** in a prominent manner and then stop polling for new messages.
426480
*/
427481
void chat_poll_webpage(void){
428482
Blob json; /* The json to be constructed and returned */
429483
sqlite3_int64 dataVersion; /* Data version. Used for polling. */
430484
const int iDelay = 1000; /* Delay until next poll (milliseconds) */
@@ -434,13 +488,16 @@
434488
int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
435489
Blob sql = empty_blob;
436490
Stmt q1;
437491
nDelay = db_get_int("chat-poll-timeout",420); /* Default about 7 minutes */
438492
login_check_credentials();
439
- if( !g.perm.Chat ) return;
493
+ if( !g.perm.Chat ) {
494
+ chat_emit_permissions_error(1);
495
+ return;
496
+ }
440497
chat_create_tables();
441
- cgi_set_content_type("text/json");
498
+ cgi_set_content_type("application/json");
442499
dataVersion = db_int64(0, "PRAGMA data_version");
443500
blob_append_sql(&sql,
444501
"SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
445502
" fname, fmime, %s, lmtime"
446503
" FROM chat ",
447504
--- src/chat.c
+++ src/chat.c
@@ -219,22 +219,57 @@
219 db_step(&s);
220 db_finalize(&s);
221 }
222 }
223 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
225 /*
226 ** WEBPAGE: chat-send
227 **
228 ** This page receives (via XHR) a new chat-message and/or a new file
229 ** to be entered into the chat history.
 
 
 
 
 
 
230 */
231 void chat_send_webpage(void){
232 int nByte;
233 const char *zMsg;
234 login_check_credentials();
235 if( !g.perm.Chat ) return;
 
 
 
236 chat_create_tables();
237 nByte = atoi(PD("file:bytes",0));
238 zMsg = PD("msg","");
239 db_begin_write();
240 chat_purge();
@@ -394,11 +429,11 @@
394 **
395 ** The reply from this webpage is JSON that describes the new content.
396 ** Format of the json:
397 **
398 ** | {
399 ** | "msg":[
400 ** | {
401 ** | "msgid": integer // message id
402 ** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC
403 ** | "lmtime: text // Localtime where the message was sent from
404 ** | "xfrom": text // Login name of sender
@@ -421,10 +456,29 @@
421 **
422 ** The "lmtime" value might be known, in which case it is omitted.
423 **
424 ** The messages are ordered oldest first unless "before" is provided, in which
425 ** case they are sorted newest first (to facilitate the client-side UI update).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426 */
427 void chat_poll_webpage(void){
428 Blob json; /* The json to be constructed and returned */
429 sqlite3_int64 dataVersion; /* Data version. Used for polling. */
430 const int iDelay = 1000; /* Delay until next poll (milliseconds) */
@@ -434,13 +488,16 @@
434 int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
435 Blob sql = empty_blob;
436 Stmt q1;
437 nDelay = db_get_int("chat-poll-timeout",420); /* Default about 7 minutes */
438 login_check_credentials();
439 if( !g.perm.Chat ) return;
 
 
 
440 chat_create_tables();
441 cgi_set_content_type("text/json");
442 dataVersion = db_int64(0, "PRAGMA data_version");
443 blob_append_sql(&sql,
444 "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
445 " fname, fmime, %s, lmtime"
446 " FROM chat ",
447
--- src/chat.c
+++ src/chat.c
@@ -219,22 +219,57 @@
219 db_step(&s);
220 db_finalize(&s);
221 }
222 }
223 }
224
225 /*
226 ** Sets the current CGI response type to application/json then emits a
227 ** JSON-format error message object. If fAsMessageList is true then
228 ** the object is output using the list format described for chat-post,
229 ** else it is emitted as a single object in that same format.
230 */
231 static void chat_emit_permissions_error(int fAsMessageList){
232 char * zTime = cgi_iso8601_datestamp();
233 cgi_set_content_type("application/json");
234 if(fAsMessageList){
235 CX("{\"msgs\":[{");
236 }else{
237 CX("{");
238 }
239 CX("\"isError\": true, \"xfrom\": \"fossil\",");
240 CX("\"mtime\": %!j, \"lmtime\": %!j,", zTime, zTime);
241 CX("\"xmsg\": \"Missing permissions or not logged in. "
242 "Try <a href='%R/login?g=%R/chat'>logging in</a>.\"");
243 if(fAsMessageList){
244 CX("}]}");
245 }else{
246 CX("}");
247 }
248 fossil_free(zTime);
249 }
250
251 /*
252 ** WEBPAGE: chat-send
253 **
254 ** This page receives (via XHR) a new chat-message and/or a new file
255 ** to be entered into the chat history.
256 **
257 ** On success it responds with an empty response: the new message
258 ** should be fetched via /chat-poll. On error, e.g. login expiry,
259 ** it emits a JSON response in the same form as described for
260 ** /chat-poll errors, but as a standalone object instead of a
261 ** list of objects.
262 */
263 void chat_send_webpage(void){
264 int nByte;
265 const char *zMsg;
266 login_check_credentials();
267 if( !g.perm.Chat ) {
268 chat_emit_permissions_error(0);
269 return;
270 }
271 chat_create_tables();
272 nByte = atoi(PD("file:bytes",0));
273 zMsg = PD("msg","");
274 db_begin_write();
275 chat_purge();
@@ -394,11 +429,11 @@
429 **
430 ** The reply from this webpage is JSON that describes the new content.
431 ** Format of the json:
432 **
433 ** | {
434 ** | "msgs":[
435 ** | {
436 ** | "msgid": integer // message id
437 ** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC
438 ** | "lmtime: text // Localtime where the message was sent from
439 ** | "xfrom": text // Login name of sender
@@ -421,10 +456,29 @@
456 **
457 ** The "lmtime" value might be known, in which case it is omitted.
458 **
459 ** The messages are ordered oldest first unless "before" is provided, in which
460 ** case they are sorted newest first (to facilitate the client-side UI update).
461 **
462 ** As a special case, if this routine encounters an error, e.g. the user's
463 ** permissions cannot be verified because their login cookie expired, the
464 ** request returns a slightly modified structure:
465 **
466 ** | {
467 ** | "msgs":[
468 ** | {
469 ** | "isError": true,
470 ** | "xfrom": "fossil",
471 ** | "xmsg": "error details"
472 ** | "mtime": as above,
473 ** | "ltime": same as mtime
474 ** | }
475 ** | ]
476 ** | }
477 **
478 ** If the client gets such a response, it should display the message
479 ** in a prominent manner and then stop polling for new messages.
480 */
481 void chat_poll_webpage(void){
482 Blob json; /* The json to be constructed and returned */
483 sqlite3_int64 dataVersion; /* Data version. Used for polling. */
484 const int iDelay = 1000; /* Delay until next poll (milliseconds) */
@@ -434,13 +488,16 @@
488 int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
489 Blob sql = empty_blob;
490 Stmt q1;
491 nDelay = db_get_int("chat-poll-timeout",420); /* Default about 7 minutes */
492 login_check_credentials();
493 if( !g.perm.Chat ) {
494 chat_emit_permissions_error(1);
495 return;
496 }
497 chat_create_tables();
498 cgi_set_content_type("application/json");
499 dataVersion = db_int64(0, "PRAGMA data_version");
500 blob_append_sql(&sql,
501 "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
502 " fname, fmime, %s, lmtime"
503 " FROM chat ",
504
+45 -19
--- src/chat.js
+++ src/chat.js
@@ -497,21 +497,22 @@
497497
if(m.xfrom === Chat.me){
498498
D.addClass(this.e.body, 'mine');
499499
}
500500
this.e.content.style.backgroundColor = m.uclr;
501501
this.e.tab.style.backgroundColor = m.uclr;
502
-
503502
const d = new Date(m.mtime);
504503
D.append(
505504
D.clearElement(this.e.tab),
506505
D.text(
507
- m.xfrom," #",m.msgid,' @ ',d.getHours(),":",
506
+ m.xfrom," #",(m.msgid||'???'),' @ ',d.getHours(),":",
508507
(d.getMinutes()+100).toString().slice(1,3)
509508
)
510509
);
511510
var contentTarget = this.e.content;
512
- if( m.fsize>0 ){
511
+ if(m.isError){
512
+ D.addClass([contentTarget, this.e.tab], 'error');
513
+ }else if( m.fsize>0 ){
513514
if( m.fmime
514515
&& m.fmime.startsWith("image/")
515516
&& Chat.settings.getBool('images-inline',true)
516517
){
517518
contentTarget.appendChild(D.img("chat-download/" + m.msgid));
@@ -524,11 +525,10 @@
524525
"(" + m.fname + " " + m.fsize + " bytes)"
525526
)
526527
D.attr(a,'target','_blank');
527528
contentTarget.appendChild(a);
528529
}
529
- ;
530530
}
531531
if(m.xmsg){
532532
if(m.fsize>0){
533533
/* We have file/image content, so need another element for
534534
the message text. */
@@ -652,17 +652,28 @@
652652
segfaults, and i have no clue why! */;
653653
const msg = this.inputValue();
654654
if(msg) fd.set('msg',msg);
655655
const file = BlobXferState.blob || this.e.inputFile.files[0];
656656
if(file) fd.set("file", file);
657
- if( msg || file ){
658
- fd.set("lmtime", localTime8601(new Date()));
659
- fetch("chat-send",{
660
- method: 'POST',
661
- body: fd
662
- });
663
- }
657
+ if( !msg && !file ) return;
658
+ const self = this;
659
+ fd.set("lmtime", localTime8601(new Date()));
660
+ fetch("chat-send",{
661
+ method: 'POST',
662
+ body: fd
663
+ }).then((x)=>x.text())
664
+ .then(function(txt){
665
+ if(!txt) return/*success response*/;
666
+ try{
667
+ const json = JSON.parse(txt);
668
+ self.newContent({msgs:[json]});
669
+ }catch(e){
670
+ self.reportError(e);
671
+ return;
672
+ }
673
+ })
674
+ .catch((e)=>this.reportError(e));
664675
BlobXferState.clear();
665676
Chat.inputValue("").inputFocus();
666677
};
667678
668679
Chat.e.inputSingle.addEventListener('keydown',function(ev){
@@ -891,12 +902,12 @@
891902
});
892903
})();
893904
894905
/** Callback for poll() to inject new content into the page. jx ==
895906
the response from /chat-poll. If atEnd is true, the message is
896
- appended to the end of the chat list, else the beginning (the
897
- default). */
907
+ appended to the end of the chat list (for loading older
908
+ messages), else the beginning (the default). */
898909
const newcontent = function f(jx,atEnd){
899910
if(!f.processPost){
900911
/** Processes chat message m, placing it either the start (if atEnd
901912
is falsy) or end (if atEnd is truthy) of the chat history. atEnd
902913
should only be true when loading older messages. */
@@ -911,10 +922,13 @@
911922
}
912923
const row = new MessageWidget()
913924
row.setMessage(m);
914925
row.setPopupCallback(handleLegendClicked);
915926
Chat.injectMessageElem(row.e.body,atEnd);
927
+ if(m.isError){
928
+ Chat.gotServerError = m;
929
+ }
916930
}/*processPost()*/;
917931
}/*end static init*/
918932
jx.msgs.forEach((m)=>f.processPost(m,atEnd));
919933
if('visible'===document.visibilityState){
920934
if(Chat.changesSincePageHidden){
@@ -929,10 +943,11 @@
929943
}
930944
if(jx.msgs.length && F.config.chat.pingTcp){
931945
fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
932946
}
933947
}/*newcontent()*/;
948
+ Chat.newContent = newcontent;
934949
935950
(function(){
936951
/** Add toolbar for loading older messages. We use a FIELDSET here
937952
because a fieldset is the only parent element type which can
938953
automatically enable/disable its children by
@@ -958,11 +973,16 @@
958973
})
959974
.catch(e=>Chat.reportError(e))
960975
.finally(function(){
961976
Chat.isBatchLoading = false;
962977
Chat.e.messagesWrapper.classList.remove('loading');
963
- if(n<0/*we asked for all history*/
978
+ Chat.ajaxEnd();
979
+ if(Chat.gotServerError){
980
+ F.toast.error("Got an error response from the server. ",
981
+ "See message for details");
982
+ return;
983
+ }else if(n<0/*we asked for all history*/
964984
|| 0===gotMessages/*we found no history*/
965985
|| (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
966986
|| (false!==gotMessages && n===0 && gotMessages<Chat.loadMessageCount
967987
/*we asked for default amount and got fewer than that.*/)){
968988
/* We've loaded all history. Permanently disable the
@@ -975,15 +995,16 @@
975995
if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1);
976996
Chat.e.loadOlderToolbar.disabled = true;
977997
}
978998
if(gotMessages > 0){
979999
F.toast.message("Loaded "+gotMessages+" older messages.");
1000
+ /* Return scroll position to where it was when the history load
1001
+ was requested, per user request */
9801002
Chat.e.messagesWrapper.scrollTo(
9811003
0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop
9821004
);
9831005
}
984
- Chat.ajaxEnd();
9851006
});
9861007
};
9871008
const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */;
9881009
D.append(toolbar, wrapper);
9891010
var btn = D.button("Previous "+Chat.loadMessageCount+" messages");
@@ -1013,26 +1034,31 @@
10131034
resumed, and reportError() produces a loud error message. */
10141035
.finally(function(){
10151036
if(isFirstCall){
10161037
Chat.isBatchLoading = false;
10171038
Chat.ajaxEnd();
1039
+ Chat.e.messagesWrapper.classList.remove('loading');
10181040
setTimeout(function(){
10191041
Chat.scrollMessagesTo(1);
1020
- Chat.e.messagesWrapper.classList.remove('loading');
10211042
}, 250);
1043
+ }
1044
+ if(Chat.gotServerError && Chat.intervalTimer){
1045
+ clearInterval(Chat.intervalTimer);
1046
+ delete Chat.intervalTimer;
10221047
}
10231048
poll.running=false;
10241049
});
10251050
}
1026
- poll.running = false;
1051
+ Chat.gotServerError = poll.running = false;
10271052
poll(true);
1028
- setInterval(poll, 1000);
1029
-
1053
+ if(!Chat.gotServerError){
1054
+ Chat.intervalTimer = setInterval(poll, 1000);
1055
+ }
10301056
if(/\bping=\d+/.test(window.location.search)){
10311057
/* If we see the 'ping' parameter we're certain this was run via
10321058
the 'fossil chat' CLI command, in which case we start up in
10331059
chat-only mode. */
10341060
Chat.chatOnlyMode(true);
10351061
}
10361062
10371063
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
10381064
})();
10391065
--- src/chat.js
+++ src/chat.js
@@ -497,21 +497,22 @@
497 if(m.xfrom === Chat.me){
498 D.addClass(this.e.body, 'mine');
499 }
500 this.e.content.style.backgroundColor = m.uclr;
501 this.e.tab.style.backgroundColor = m.uclr;
502
503 const d = new Date(m.mtime);
504 D.append(
505 D.clearElement(this.e.tab),
506 D.text(
507 m.xfrom," #",m.msgid,' @ ',d.getHours(),":",
508 (d.getMinutes()+100).toString().slice(1,3)
509 )
510 );
511 var contentTarget = this.e.content;
512 if( m.fsize>0 ){
 
 
513 if( m.fmime
514 && m.fmime.startsWith("image/")
515 && Chat.settings.getBool('images-inline',true)
516 ){
517 contentTarget.appendChild(D.img("chat-download/" + m.msgid));
@@ -524,11 +525,10 @@
524 "(" + m.fname + " " + m.fsize + " bytes)"
525 )
526 D.attr(a,'target','_blank');
527 contentTarget.appendChild(a);
528 }
529 ;
530 }
531 if(m.xmsg){
532 if(m.fsize>0){
533 /* We have file/image content, so need another element for
534 the message text. */
@@ -652,17 +652,28 @@
652 segfaults, and i have no clue why! */;
653 const msg = this.inputValue();
654 if(msg) fd.set('msg',msg);
655 const file = BlobXferState.blob || this.e.inputFile.files[0];
656 if(file) fd.set("file", file);
657 if( msg || file ){
658 fd.set("lmtime", localTime8601(new Date()));
659 fetch("chat-send",{
660 method: 'POST',
661 body: fd
662 });
663 }
 
 
 
 
 
 
 
 
 
 
 
664 BlobXferState.clear();
665 Chat.inputValue("").inputFocus();
666 };
667
668 Chat.e.inputSingle.addEventListener('keydown',function(ev){
@@ -891,12 +902,12 @@
891 });
892 })();
893
894 /** Callback for poll() to inject new content into the page. jx ==
895 the response from /chat-poll. If atEnd is true, the message is
896 appended to the end of the chat list, else the beginning (the
897 default). */
898 const newcontent = function f(jx,atEnd){
899 if(!f.processPost){
900 /** Processes chat message m, placing it either the start (if atEnd
901 is falsy) or end (if atEnd is truthy) of the chat history. atEnd
902 should only be true when loading older messages. */
@@ -911,10 +922,13 @@
911 }
912 const row = new MessageWidget()
913 row.setMessage(m);
914 row.setPopupCallback(handleLegendClicked);
915 Chat.injectMessageElem(row.e.body,atEnd);
 
 
 
916 }/*processPost()*/;
917 }/*end static init*/
918 jx.msgs.forEach((m)=>f.processPost(m,atEnd));
919 if('visible'===document.visibilityState){
920 if(Chat.changesSincePageHidden){
@@ -929,10 +943,11 @@
929 }
930 if(jx.msgs.length && F.config.chat.pingTcp){
931 fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
932 }
933 }/*newcontent()*/;
 
934
935 (function(){
936 /** Add toolbar for loading older messages. We use a FIELDSET here
937 because a fieldset is the only parent element type which can
938 automatically enable/disable its children by
@@ -958,11 +973,16 @@
958 })
959 .catch(e=>Chat.reportError(e))
960 .finally(function(){
961 Chat.isBatchLoading = false;
962 Chat.e.messagesWrapper.classList.remove('loading');
963 if(n<0/*we asked for all history*/
 
 
 
 
 
964 || 0===gotMessages/*we found no history*/
965 || (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
966 || (false!==gotMessages && n===0 && gotMessages<Chat.loadMessageCount
967 /*we asked for default amount and got fewer than that.*/)){
968 /* We've loaded all history. Permanently disable the
@@ -975,15 +995,16 @@
975 if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1);
976 Chat.e.loadOlderToolbar.disabled = true;
977 }
978 if(gotMessages > 0){
979 F.toast.message("Loaded "+gotMessages+" older messages.");
 
 
980 Chat.e.messagesWrapper.scrollTo(
981 0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop
982 );
983 }
984 Chat.ajaxEnd();
985 });
986 };
987 const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */;
988 D.append(toolbar, wrapper);
989 var btn = D.button("Previous "+Chat.loadMessageCount+" messages");
@@ -1013,26 +1034,31 @@
1013 resumed, and reportError() produces a loud error message. */
1014 .finally(function(){
1015 if(isFirstCall){
1016 Chat.isBatchLoading = false;
1017 Chat.ajaxEnd();
 
1018 setTimeout(function(){
1019 Chat.scrollMessagesTo(1);
1020 Chat.e.messagesWrapper.classList.remove('loading');
1021 }, 250);
 
 
 
 
1022 }
1023 poll.running=false;
1024 });
1025 }
1026 poll.running = false;
1027 poll(true);
1028 setInterval(poll, 1000);
1029
 
1030 if(/\bping=\d+/.test(window.location.search)){
1031 /* If we see the 'ping' parameter we're certain this was run via
1032 the 'fossil chat' CLI command, in which case we start up in
1033 chat-only mode. */
1034 Chat.chatOnlyMode(true);
1035 }
1036
1037 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
1038 })();
1039
--- src/chat.js
+++ src/chat.js
@@ -497,21 +497,22 @@
497 if(m.xfrom === Chat.me){
498 D.addClass(this.e.body, 'mine');
499 }
500 this.e.content.style.backgroundColor = m.uclr;
501 this.e.tab.style.backgroundColor = m.uclr;
 
502 const d = new Date(m.mtime);
503 D.append(
504 D.clearElement(this.e.tab),
505 D.text(
506 m.xfrom," #",(m.msgid||'???'),' @ ',d.getHours(),":",
507 (d.getMinutes()+100).toString().slice(1,3)
508 )
509 );
510 var contentTarget = this.e.content;
511 if(m.isError){
512 D.addClass([contentTarget, this.e.tab], 'error');
513 }else if( m.fsize>0 ){
514 if( m.fmime
515 && m.fmime.startsWith("image/")
516 && Chat.settings.getBool('images-inline',true)
517 ){
518 contentTarget.appendChild(D.img("chat-download/" + m.msgid));
@@ -524,11 +525,10 @@
525 "(" + m.fname + " " + m.fsize + " bytes)"
526 )
527 D.attr(a,'target','_blank');
528 contentTarget.appendChild(a);
529 }
 
530 }
531 if(m.xmsg){
532 if(m.fsize>0){
533 /* We have file/image content, so need another element for
534 the message text. */
@@ -652,17 +652,28 @@
652 segfaults, and i have no clue why! */;
653 const msg = this.inputValue();
654 if(msg) fd.set('msg',msg);
655 const file = BlobXferState.blob || this.e.inputFile.files[0];
656 if(file) fd.set("file", file);
657 if( !msg && !file ) return;
658 const self = this;
659 fd.set("lmtime", localTime8601(new Date()));
660 fetch("chat-send",{
661 method: 'POST',
662 body: fd
663 }).then((x)=>x.text())
664 .then(function(txt){
665 if(!txt) return/*success response*/;
666 try{
667 const json = JSON.parse(txt);
668 self.newContent({msgs:[json]});
669 }catch(e){
670 self.reportError(e);
671 return;
672 }
673 })
674 .catch((e)=>this.reportError(e));
675 BlobXferState.clear();
676 Chat.inputValue("").inputFocus();
677 };
678
679 Chat.e.inputSingle.addEventListener('keydown',function(ev){
@@ -891,12 +902,12 @@
902 });
903 })();
904
905 /** Callback for poll() to inject new content into the page. jx ==
906 the response from /chat-poll. If atEnd is true, the message is
907 appended to the end of the chat list (for loading older
908 messages), else the beginning (the default). */
909 const newcontent = function f(jx,atEnd){
910 if(!f.processPost){
911 /** Processes chat message m, placing it either the start (if atEnd
912 is falsy) or end (if atEnd is truthy) of the chat history. atEnd
913 should only be true when loading older messages. */
@@ -911,10 +922,13 @@
922 }
923 const row = new MessageWidget()
924 row.setMessage(m);
925 row.setPopupCallback(handleLegendClicked);
926 Chat.injectMessageElem(row.e.body,atEnd);
927 if(m.isError){
928 Chat.gotServerError = m;
929 }
930 }/*processPost()*/;
931 }/*end static init*/
932 jx.msgs.forEach((m)=>f.processPost(m,atEnd));
933 if('visible'===document.visibilityState){
934 if(Chat.changesSincePageHidden){
@@ -929,10 +943,11 @@
943 }
944 if(jx.msgs.length && F.config.chat.pingTcp){
945 fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping");
946 }
947 }/*newcontent()*/;
948 Chat.newContent = newcontent;
949
950 (function(){
951 /** Add toolbar for loading older messages. We use a FIELDSET here
952 because a fieldset is the only parent element type which can
953 automatically enable/disable its children by
@@ -958,11 +973,16 @@
973 })
974 .catch(e=>Chat.reportError(e))
975 .finally(function(){
976 Chat.isBatchLoading = false;
977 Chat.e.messagesWrapper.classList.remove('loading');
978 Chat.ajaxEnd();
979 if(Chat.gotServerError){
980 F.toast.error("Got an error response from the server. ",
981 "See message for details");
982 return;
983 }else if(n<0/*we asked for all history*/
984 || 0===gotMessages/*we found no history*/
985 || (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
986 || (false!==gotMessages && n===0 && gotMessages<Chat.loadMessageCount
987 /*we asked for default amount and got fewer than that.*/)){
988 /* We've loaded all history. Permanently disable the
@@ -975,15 +995,16 @@
995 if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1);
996 Chat.e.loadOlderToolbar.disabled = true;
997 }
998 if(gotMessages > 0){
999 F.toast.message("Loaded "+gotMessages+" older messages.");
1000 /* Return scroll position to where it was when the history load
1001 was requested, per user request */
1002 Chat.e.messagesWrapper.scrollTo(
1003 0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop
1004 );
1005 }
 
1006 });
1007 };
1008 const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */;
1009 D.append(toolbar, wrapper);
1010 var btn = D.button("Previous "+Chat.loadMessageCount+" messages");
@@ -1013,26 +1034,31 @@
1034 resumed, and reportError() produces a loud error message. */
1035 .finally(function(){
1036 if(isFirstCall){
1037 Chat.isBatchLoading = false;
1038 Chat.ajaxEnd();
1039 Chat.e.messagesWrapper.classList.remove('loading');
1040 setTimeout(function(){
1041 Chat.scrollMessagesTo(1);
 
1042 }, 250);
1043 }
1044 if(Chat.gotServerError && Chat.intervalTimer){
1045 clearInterval(Chat.intervalTimer);
1046 delete Chat.intervalTimer;
1047 }
1048 poll.running=false;
1049 });
1050 }
1051 Chat.gotServerError = poll.running = false;
1052 poll(true);
1053 if(!Chat.gotServerError){
1054 Chat.intervalTimer = setInterval(poll, 1000);
1055 }
1056 if(/\bping=\d+/.test(window.location.search)){
1057 /* If we see the 'ping' parameter we're certain this was run via
1058 the 'fossil chat' CLI command, in which case we start up in
1059 chat-only mode. */
1060 Chat.chatOnlyMode(true);
1061 }
1062
1063 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
1064 })();
1065

Keyboard Shortcuts

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