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.
Commit
3c0c8954c73f7a523b9211d0882b000793eee4dec3d70ab5b605b0426c6ea4b6
Parent
b06442a621870ea…
2 files changed
+61
-4
+45
-19
+61
-4
| --- src/chat.c | ||
| +++ src/chat.c | ||
| @@ -219,22 +219,57 @@ | ||
| 219 | 219 | db_step(&s); |
| 220 | 220 | db_finalize(&s); |
| 221 | 221 | } |
| 222 | 222 | } |
| 223 | 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 | +} | |
| 224 | 250 | |
| 225 | 251 | /* |
| 226 | 252 | ** WEBPAGE: chat-send |
| 227 | 253 | ** |
| 228 | 254 | ** This page receives (via XHR) a new chat-message and/or a new file |
| 229 | 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. | |
| 230 | 262 | */ |
| 231 | 263 | void chat_send_webpage(void){ |
| 232 | 264 | int nByte; |
| 233 | 265 | const char *zMsg; |
| 234 | 266 | login_check_credentials(); |
| 235 | - if( !g.perm.Chat ) return; | |
| 267 | + if( !g.perm.Chat ) { | |
| 268 | + chat_emit_permissions_error(0); | |
| 269 | + return; | |
| 270 | + } | |
| 236 | 271 | chat_create_tables(); |
| 237 | 272 | nByte = atoi(PD("file:bytes",0)); |
| 238 | 273 | zMsg = PD("msg",""); |
| 239 | 274 | db_begin_write(); |
| 240 | 275 | chat_purge(); |
| @@ -394,11 +429,11 @@ | ||
| 394 | 429 | ** |
| 395 | 430 | ** The reply from this webpage is JSON that describes the new content. |
| 396 | 431 | ** Format of the json: |
| 397 | 432 | ** |
| 398 | 433 | ** | { |
| 399 | -** | "msg":[ | |
| 434 | +** | "msgs":[ | |
| 400 | 435 | ** | { |
| 401 | 436 | ** | "msgid": integer // message id |
| 402 | 437 | ** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC |
| 403 | 438 | ** | "lmtime: text // Localtime where the message was sent from |
| 404 | 439 | ** | "xfrom": text // Login name of sender |
| @@ -421,10 +456,29 @@ | ||
| 421 | 456 | ** |
| 422 | 457 | ** The "lmtime" value might be known, in which case it is omitted. |
| 423 | 458 | ** |
| 424 | 459 | ** The messages are ordered oldest first unless "before" is provided, in which |
| 425 | 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. | |
| 426 | 480 | */ |
| 427 | 481 | void chat_poll_webpage(void){ |
| 428 | 482 | Blob json; /* The json to be constructed and returned */ |
| 429 | 483 | sqlite3_int64 dataVersion; /* Data version. Used for polling. */ |
| 430 | 484 | const int iDelay = 1000; /* Delay until next poll (milliseconds) */ |
| @@ -434,13 +488,16 @@ | ||
| 434 | 488 | int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0; |
| 435 | 489 | Blob sql = empty_blob; |
| 436 | 490 | Stmt q1; |
| 437 | 491 | nDelay = db_get_int("chat-poll-timeout",420); /* Default about 7 minutes */ |
| 438 | 492 | login_check_credentials(); |
| 439 | - if( !g.perm.Chat ) return; | |
| 493 | + if( !g.perm.Chat ) { | |
| 494 | + chat_emit_permissions_error(1); | |
| 495 | + return; | |
| 496 | + } | |
| 440 | 497 | chat_create_tables(); |
| 441 | - cgi_set_content_type("text/json"); | |
| 498 | + cgi_set_content_type("application/json"); | |
| 442 | 499 | dataVersion = db_int64(0, "PRAGMA data_version"); |
| 443 | 500 | blob_append_sql(&sql, |
| 444 | 501 | "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file)," |
| 445 | 502 | " fname, fmime, %s, lmtime" |
| 446 | 503 | " FROM chat ", |
| 447 | 504 |
| --- 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 @@ | ||
| 497 | 497 | if(m.xfrom === Chat.me){ |
| 498 | 498 | D.addClass(this.e.body, 'mine'); |
| 499 | 499 | } |
| 500 | 500 | this.e.content.style.backgroundColor = m.uclr; |
| 501 | 501 | this.e.tab.style.backgroundColor = m.uclr; |
| 502 | - | |
| 503 | 502 | const d = new Date(m.mtime); |
| 504 | 503 | D.append( |
| 505 | 504 | D.clearElement(this.e.tab), |
| 506 | 505 | D.text( |
| 507 | - m.xfrom," #",m.msgid,' @ ',d.getHours(),":", | |
| 506 | + m.xfrom," #",(m.msgid||'???'),' @ ',d.getHours(),":", | |
| 508 | 507 | (d.getMinutes()+100).toString().slice(1,3) |
| 509 | 508 | ) |
| 510 | 509 | ); |
| 511 | 510 | 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 ){ | |
| 513 | 514 | if( m.fmime |
| 514 | 515 | && m.fmime.startsWith("image/") |
| 515 | 516 | && Chat.settings.getBool('images-inline',true) |
| 516 | 517 | ){ |
| 517 | 518 | contentTarget.appendChild(D.img("chat-download/" + m.msgid)); |
| @@ -524,11 +525,10 @@ | ||
| 524 | 525 | "(" + m.fname + " " + m.fsize + " bytes)" |
| 525 | 526 | ) |
| 526 | 527 | D.attr(a,'target','_blank'); |
| 527 | 528 | contentTarget.appendChild(a); |
| 528 | 529 | } |
| 529 | - ; | |
| 530 | 530 | } |
| 531 | 531 | if(m.xmsg){ |
| 532 | 532 | if(m.fsize>0){ |
| 533 | 533 | /* We have file/image content, so need another element for |
| 534 | 534 | the message text. */ |
| @@ -652,17 +652,28 @@ | ||
| 652 | 652 | segfaults, and i have no clue why! */; |
| 653 | 653 | const msg = this.inputValue(); |
| 654 | 654 | if(msg) fd.set('msg',msg); |
| 655 | 655 | const file = BlobXferState.blob || this.e.inputFile.files[0]; |
| 656 | 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 | - } | |
| 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)); | |
| 664 | 675 | BlobXferState.clear(); |
| 665 | 676 | Chat.inputValue("").inputFocus(); |
| 666 | 677 | }; |
| 667 | 678 | |
| 668 | 679 | Chat.e.inputSingle.addEventListener('keydown',function(ev){ |
| @@ -891,12 +902,12 @@ | ||
| 891 | 902 | }); |
| 892 | 903 | })(); |
| 893 | 904 | |
| 894 | 905 | /** Callback for poll() to inject new content into the page. jx == |
| 895 | 906 | 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). */ | |
| 898 | 909 | const newcontent = function f(jx,atEnd){ |
| 899 | 910 | if(!f.processPost){ |
| 900 | 911 | /** Processes chat message m, placing it either the start (if atEnd |
| 901 | 912 | is falsy) or end (if atEnd is truthy) of the chat history. atEnd |
| 902 | 913 | should only be true when loading older messages. */ |
| @@ -911,10 +922,13 @@ | ||
| 911 | 922 | } |
| 912 | 923 | const row = new MessageWidget() |
| 913 | 924 | row.setMessage(m); |
| 914 | 925 | row.setPopupCallback(handleLegendClicked); |
| 915 | 926 | Chat.injectMessageElem(row.e.body,atEnd); |
| 927 | + if(m.isError){ | |
| 928 | + Chat.gotServerError = m; | |
| 929 | + } | |
| 916 | 930 | }/*processPost()*/; |
| 917 | 931 | }/*end static init*/ |
| 918 | 932 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 919 | 933 | if('visible'===document.visibilityState){ |
| 920 | 934 | if(Chat.changesSincePageHidden){ |
| @@ -929,10 +943,11 @@ | ||
| 929 | 943 | } |
| 930 | 944 | if(jx.msgs.length && F.config.chat.pingTcp){ |
| 931 | 945 | fetch("http:/"+"/localhost:"+F.config.chat.pingTcp+"/chat-ping"); |
| 932 | 946 | } |
| 933 | 947 | }/*newcontent()*/; |
| 948 | + Chat.newContent = newcontent; | |
| 934 | 949 | |
| 935 | 950 | (function(){ |
| 936 | 951 | /** Add toolbar for loading older messages. We use a FIELDSET here |
| 937 | 952 | because a fieldset is the only parent element type which can |
| 938 | 953 | automatically enable/disable its children by |
| @@ -958,11 +973,16 @@ | ||
| 958 | 973 | }) |
| 959 | 974 | .catch(e=>Chat.reportError(e)) |
| 960 | 975 | .finally(function(){ |
| 961 | 976 | Chat.isBatchLoading = false; |
| 962 | 977 | 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*/ | |
| 964 | 984 | || 0===gotMessages/*we found no history*/ |
| 965 | 985 | || (n>0 && gotMessages<n /*we got fewer history entries than requested*/) |
| 966 | 986 | || (false!==gotMessages && n===0 && gotMessages<Chat.loadMessageCount |
| 967 | 987 | /*we asked for default amount and got fewer than that.*/)){ |
| 968 | 988 | /* We've loaded all history. Permanently disable the |
| @@ -975,15 +995,16 @@ | ||
| 975 | 995 | if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1); |
| 976 | 996 | Chat.e.loadOlderToolbar.disabled = true; |
| 977 | 997 | } |
| 978 | 998 | if(gotMessages > 0){ |
| 979 | 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 */ | |
| 980 | 1002 | Chat.e.messagesWrapper.scrollTo( |
| 981 | 1003 | 0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop |
| 982 | 1004 | ); |
| 983 | 1005 | } |
| 984 | - Chat.ajaxEnd(); | |
| 985 | 1006 | }); |
| 986 | 1007 | }; |
| 987 | 1008 | const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */; |
| 988 | 1009 | D.append(toolbar, wrapper); |
| 989 | 1010 | var btn = D.button("Previous "+Chat.loadMessageCount+" messages"); |
| @@ -1013,26 +1034,31 @@ | ||
| 1013 | 1034 | resumed, and reportError() produces a loud error message. */ |
| 1014 | 1035 | .finally(function(){ |
| 1015 | 1036 | if(isFirstCall){ |
| 1016 | 1037 | Chat.isBatchLoading = false; |
| 1017 | 1038 | Chat.ajaxEnd(); |
| 1039 | + Chat.e.messagesWrapper.classList.remove('loading'); | |
| 1018 | 1040 | setTimeout(function(){ |
| 1019 | 1041 | Chat.scrollMessagesTo(1); |
| 1020 | - Chat.e.messagesWrapper.classList.remove('loading'); | |
| 1021 | 1042 | }, 250); |
| 1043 | + } | |
| 1044 | + if(Chat.gotServerError && Chat.intervalTimer){ | |
| 1045 | + clearInterval(Chat.intervalTimer); | |
| 1046 | + delete Chat.intervalTimer; | |
| 1022 | 1047 | } |
| 1023 | 1048 | poll.running=false; |
| 1024 | 1049 | }); |
| 1025 | 1050 | } |
| 1026 | - poll.running = false; | |
| 1051 | + Chat.gotServerError = poll.running = false; | |
| 1027 | 1052 | poll(true); |
| 1028 | - setInterval(poll, 1000); | |
| 1029 | - | |
| 1053 | + if(!Chat.gotServerError){ | |
| 1054 | + Chat.intervalTimer = setInterval(poll, 1000); | |
| 1055 | + } | |
| 1030 | 1056 | if(/\bping=\d+/.test(window.location.search)){ |
| 1031 | 1057 | /* If we see the 'ping' parameter we're certain this was run via |
| 1032 | 1058 | the 'fossil chat' CLI command, in which case we start up in |
| 1033 | 1059 | chat-only mode. */ |
| 1034 | 1060 | Chat.chatOnlyMode(true); |
| 1035 | 1061 | } |
| 1036 | 1062 | |
| 1037 | 1063 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 1038 | 1064 | })(); |
| 1039 | 1065 |
| --- 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 |