Fossil SCM
Integrated client-local timestamp into chat so that participants can see the local time in their colleagues' time zones.
Commit
3c5e2badc8254dc72c400f6e557a5667f61b173cb6abdaa5e6dd41957842e71c
Parent
152ac599e8a4162…
2 files changed
+21
-9
+25
-4
+21
-9
| --- src/chat.c | ||
| +++ src/chat.c | ||
| @@ -167,10 +167,11 @@ | ||
| 167 | 167 | */ |
| 168 | 168 | static const char zChatSchema1[] = |
| 169 | 169 | @ CREATE TABLE repository.chat( |
| 170 | 170 | @ msgid INTEGER PRIMARY KEY AUTOINCREMENT, |
| 171 | 171 | @ mtime JULIANDAY, -- Time for this entry - Julianday Zulu |
| 172 | +@ lmtime TEXT, -- Localtime when message originall sent | |
| 172 | 173 | @ xfrom TEXT, -- Login of the sender |
| 173 | 174 | @ xmsg TEXT, -- Raw, unformatted text of the message |
| 174 | 175 | @ file BLOB, -- Text of the uploaded file, or NULL |
| 175 | 176 | @ fname TEXT, -- Filename of the uploaded file, or NULL |
| 176 | 177 | @ fmime TEXT, -- MIMEType of the upload file, or NULL |
| @@ -184,12 +185,15 @@ | ||
| 184 | 185 | ** if they do not. |
| 185 | 186 | */ |
| 186 | 187 | static void chat_create_tables(void){ |
| 187 | 188 | if( !db_table_exists("repository","chat") ){ |
| 188 | 189 | db_multi_exec(zChatSchema1/*works-like:""*/); |
| 189 | - }else if( !db_table_has_column("repository","chat","mdel") ){ | |
| 190 | - db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT"); | |
| 190 | + }else if( !db_table_has_column("repository","chat","lmtime") ){ | |
| 191 | + if( !db_table_has_column("repository","chat","mdel") ){ | |
| 192 | + db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT"); | |
| 193 | + } | |
| 194 | + db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT"); | |
| 191 | 195 | } |
| 192 | 196 | } |
| 193 | 197 | |
| 194 | 198 | /* |
| 195 | 199 | ** Delete old content from the chat table. |
| @@ -233,22 +237,22 @@ | ||
| 233 | 237 | db_begin_write(); |
| 234 | 238 | chat_purge(); |
| 235 | 239 | if( nByte==0 ){ |
| 236 | 240 | if( zMsg[0] ){ |
| 237 | 241 | db_multi_exec( |
| 238 | - "INSERT INTO chat(mtime,xfrom,xmsg)" | |
| 239 | - "VALUES(julianday('now'),%Q,%Q)", | |
| 240 | - g.zLogin, zMsg | |
| 242 | + "INSERT INTO chat(mtime,lmtime,xfrom,xmsg)" | |
| 243 | + "VALUES(julianday('now'),%Q,%Q,%Q)", | |
| 244 | + P("lmtime"), g.zLogin, zMsg | |
| 241 | 245 | ); |
| 242 | 246 | } |
| 243 | 247 | }else{ |
| 244 | 248 | Stmt q; |
| 245 | 249 | Blob b; |
| 246 | 250 | db_prepare(&q, |
| 247 | - "INSERT INTO chat(mtime, xfrom,xmsg,file,fname,fmime)" | |
| 248 | - "VALUES(julianday('now'),%Q,%Q,:file,%Q,%Q)", | |
| 249 | - g.zLogin, zMsg, PD("file:filename",""), | |
| 251 | + "INSERT INTO chat(mtime,lmtime,xfrom,xmsg,file,fname,fmime)" | |
| 252 | + "VALUES(julianday('now'),%Q,%Q,%Q,:file,%Q,%Q)", | |
| 253 | + P("lmtime"), g.zLogin, zMsg, PD("file:filename",""), | |
| 250 | 254 | PD("file:mimetype","application/octet-stream")); |
| 251 | 255 | blob_init(&b, P("file"), nByte); |
| 252 | 256 | db_bind_blob(&q, ":file", &b); |
| 253 | 257 | db_step(&q); |
| 254 | 258 | db_finalize(&q); |
| @@ -392,10 +396,11 @@ | ||
| 392 | 396 | ** | { |
| 393 | 397 | ** | "msg":[ |
| 394 | 398 | ** | { |
| 395 | 399 | ** | "msgid": integer // message id |
| 396 | 400 | ** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC |
| 401 | +** | "lmtime: text // Localtime where the message was sent from | |
| 397 | 402 | ** | "xfrom": text // Login name of sender |
| 398 | 403 | ** | "uclr": text // Color string associated with the user |
| 399 | 404 | ** | "xmsg": text // HTML text of the message |
| 400 | 405 | ** | "fsize": integer // file attachment size in bytes |
| 401 | 406 | ** | "fname": text // Name of file attachment |
| @@ -409,10 +414,12 @@ | ||
| 409 | 414 | ** than zero. The "xmsg" field may be an empty string if "fsize" is zero. |
| 410 | 415 | ** |
| 411 | 416 | ** The "msgid" values will be in increasing order. |
| 412 | 417 | ** |
| 413 | 418 | ** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero. |
| 419 | +** | |
| 420 | +** The "lmtime" value might be known, in which case it is omitted. | |
| 414 | 421 | ** |
| 415 | 422 | ** The messages are ordered oldest first unless "before" is provided, in which |
| 416 | 423 | ** case they are sorted newest first (to facilitate the client-side UI update). |
| 417 | 424 | */ |
| 418 | 425 | void chat_poll_webpage(void){ |
| @@ -431,11 +438,11 @@ | ||
| 431 | 438 | chat_create_tables(); |
| 432 | 439 | cgi_set_content_type("text/json"); |
| 433 | 440 | dataVersion = db_int64(0, "PRAGMA data_version"); |
| 434 | 441 | blob_append_sql(&sql, |
| 435 | 442 | "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file)," |
| 436 | - " fname, fmime, %s" | |
| 443 | + " fname, fmime, %s, lmtime" | |
| 437 | 444 | " FROM chat ", |
| 438 | 445 | msgBefore>0 ? "0 as mdel" : "mdel"); |
| 439 | 446 | if( msgid<=0 || msgBefore>0 ){ |
| 440 | 447 | db_begin_write(); |
| 441 | 448 | chat_purge(); |
| @@ -475,16 +482,20 @@ | ||
| 475 | 482 | const char *zRawMsg = db_column_text(&q1, 3); |
| 476 | 483 | int nByte = db_column_int(&q1, 4); |
| 477 | 484 | const char *zFName = db_column_text(&q1, 5); |
| 478 | 485 | const char *zFMime = db_column_text(&q1, 6); |
| 479 | 486 | int iToDel = db_column_int(&q1, 7); |
| 487 | + const char *zLMtime = db_column_text(&q1, 8); | |
| 480 | 488 | char *zMsg; |
| 481 | 489 | if(cnt++){ |
| 482 | 490 | blob_append(&json, ",\n", 2); |
| 483 | 491 | } |
| 484 | 492 | blob_appendf(&json, "{\"msgid\":%d,", id); |
| 485 | 493 | blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11); |
| 494 | + if( zLMtime && zLMtime[0] ){ | |
| 495 | + blob_appendf(&json, "\"lmtime\":%!j,", zLMtime); | |
| 496 | + } | |
| 486 | 497 | blob_appendf(&json, "\"xfrom\":%!j,", zFrom); |
| 487 | 498 | blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom)); |
| 488 | 499 | |
| 489 | 500 | zMsg = chat_format_to_html(zRawMsg ? zRawMsg : ""); |
| 490 | 501 | blob_appendf(&json, "\"xmsg\":%!j,", zMsg); |
| @@ -494,10 +505,11 @@ | ||
| 494 | 505 | blob_appendf(&json, "\"fsize\":0"); |
| 495 | 506 | }else{ |
| 496 | 507 | blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j", |
| 497 | 508 | nByte, zFName, zFMime); |
| 498 | 509 | } |
| 510 | + | |
| 499 | 511 | if( iToDel ){ |
| 500 | 512 | blob_appendf(&json, ",\"mdel\":%d}", iToDel); |
| 501 | 513 | }else{ |
| 502 | 514 | blob_append(&json, "}", 1); |
| 503 | 515 | } |
| 504 | 516 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -167,10 +167,11 @@ | |
| 167 | */ |
| 168 | static const char zChatSchema1[] = |
| 169 | @ CREATE TABLE repository.chat( |
| 170 | @ msgid INTEGER PRIMARY KEY AUTOINCREMENT, |
| 171 | @ mtime JULIANDAY, -- Time for this entry - Julianday Zulu |
| 172 | @ xfrom TEXT, -- Login of the sender |
| 173 | @ xmsg TEXT, -- Raw, unformatted text of the message |
| 174 | @ file BLOB, -- Text of the uploaded file, or NULL |
| 175 | @ fname TEXT, -- Filename of the uploaded file, or NULL |
| 176 | @ fmime TEXT, -- MIMEType of the upload file, or NULL |
| @@ -184,12 +185,15 @@ | |
| 184 | ** if they do not. |
| 185 | */ |
| 186 | static void chat_create_tables(void){ |
| 187 | if( !db_table_exists("repository","chat") ){ |
| 188 | db_multi_exec(zChatSchema1/*works-like:""*/); |
| 189 | }else if( !db_table_has_column("repository","chat","mdel") ){ |
| 190 | db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT"); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | /* |
| 195 | ** Delete old content from the chat table. |
| @@ -233,22 +237,22 @@ | |
| 233 | db_begin_write(); |
| 234 | chat_purge(); |
| 235 | if( nByte==0 ){ |
| 236 | if( zMsg[0] ){ |
| 237 | db_multi_exec( |
| 238 | "INSERT INTO chat(mtime,xfrom,xmsg)" |
| 239 | "VALUES(julianday('now'),%Q,%Q)", |
| 240 | g.zLogin, zMsg |
| 241 | ); |
| 242 | } |
| 243 | }else{ |
| 244 | Stmt q; |
| 245 | Blob b; |
| 246 | db_prepare(&q, |
| 247 | "INSERT INTO chat(mtime, xfrom,xmsg,file,fname,fmime)" |
| 248 | "VALUES(julianday('now'),%Q,%Q,:file,%Q,%Q)", |
| 249 | g.zLogin, zMsg, PD("file:filename",""), |
| 250 | PD("file:mimetype","application/octet-stream")); |
| 251 | blob_init(&b, P("file"), nByte); |
| 252 | db_bind_blob(&q, ":file", &b); |
| 253 | db_step(&q); |
| 254 | db_finalize(&q); |
| @@ -392,10 +396,11 @@ | |
| 392 | ** | { |
| 393 | ** | "msg":[ |
| 394 | ** | { |
| 395 | ** | "msgid": integer // message id |
| 396 | ** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC |
| 397 | ** | "xfrom": text // Login name of sender |
| 398 | ** | "uclr": text // Color string associated with the user |
| 399 | ** | "xmsg": text // HTML text of the message |
| 400 | ** | "fsize": integer // file attachment size in bytes |
| 401 | ** | "fname": text // Name of file attachment |
| @@ -409,10 +414,12 @@ | |
| 409 | ** than zero. The "xmsg" field may be an empty string if "fsize" is zero. |
| 410 | ** |
| 411 | ** The "msgid" values will be in increasing order. |
| 412 | ** |
| 413 | ** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero. |
| 414 | ** |
| 415 | ** The messages are ordered oldest first unless "before" is provided, in which |
| 416 | ** case they are sorted newest first (to facilitate the client-side UI update). |
| 417 | */ |
| 418 | void chat_poll_webpage(void){ |
| @@ -431,11 +438,11 @@ | |
| 431 | chat_create_tables(); |
| 432 | cgi_set_content_type("text/json"); |
| 433 | dataVersion = db_int64(0, "PRAGMA data_version"); |
| 434 | blob_append_sql(&sql, |
| 435 | "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file)," |
| 436 | " fname, fmime, %s" |
| 437 | " FROM chat ", |
| 438 | msgBefore>0 ? "0 as mdel" : "mdel"); |
| 439 | if( msgid<=0 || msgBefore>0 ){ |
| 440 | db_begin_write(); |
| 441 | chat_purge(); |
| @@ -475,16 +482,20 @@ | |
| 475 | const char *zRawMsg = db_column_text(&q1, 3); |
| 476 | int nByte = db_column_int(&q1, 4); |
| 477 | const char *zFName = db_column_text(&q1, 5); |
| 478 | const char *zFMime = db_column_text(&q1, 6); |
| 479 | int iToDel = db_column_int(&q1, 7); |
| 480 | char *zMsg; |
| 481 | if(cnt++){ |
| 482 | blob_append(&json, ",\n", 2); |
| 483 | } |
| 484 | blob_appendf(&json, "{\"msgid\":%d,", id); |
| 485 | blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11); |
| 486 | blob_appendf(&json, "\"xfrom\":%!j,", zFrom); |
| 487 | blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom)); |
| 488 | |
| 489 | zMsg = chat_format_to_html(zRawMsg ? zRawMsg : ""); |
| 490 | blob_appendf(&json, "\"xmsg\":%!j,", zMsg); |
| @@ -494,10 +505,11 @@ | |
| 494 | blob_appendf(&json, "\"fsize\":0"); |
| 495 | }else{ |
| 496 | blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j", |
| 497 | nByte, zFName, zFMime); |
| 498 | } |
| 499 | if( iToDel ){ |
| 500 | blob_appendf(&json, ",\"mdel\":%d}", iToDel); |
| 501 | }else{ |
| 502 | blob_append(&json, "}", 1); |
| 503 | } |
| 504 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -167,10 +167,11 @@ | |
| 167 | */ |
| 168 | static const char zChatSchema1[] = |
| 169 | @ CREATE TABLE repository.chat( |
| 170 | @ msgid INTEGER PRIMARY KEY AUTOINCREMENT, |
| 171 | @ mtime JULIANDAY, -- Time for this entry - Julianday Zulu |
| 172 | @ lmtime TEXT, -- Localtime when message originall sent |
| 173 | @ xfrom TEXT, -- Login of the sender |
| 174 | @ xmsg TEXT, -- Raw, unformatted text of the message |
| 175 | @ file BLOB, -- Text of the uploaded file, or NULL |
| 176 | @ fname TEXT, -- Filename of the uploaded file, or NULL |
| 177 | @ fmime TEXT, -- MIMEType of the upload file, or NULL |
| @@ -184,12 +185,15 @@ | |
| 185 | ** if they do not. |
| 186 | */ |
| 187 | static void chat_create_tables(void){ |
| 188 | if( !db_table_exists("repository","chat") ){ |
| 189 | db_multi_exec(zChatSchema1/*works-like:""*/); |
| 190 | }else if( !db_table_has_column("repository","chat","lmtime") ){ |
| 191 | if( !db_table_has_column("repository","chat","mdel") ){ |
| 192 | db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT"); |
| 193 | } |
| 194 | db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT"); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | /* |
| 199 | ** Delete old content from the chat table. |
| @@ -233,22 +237,22 @@ | |
| 237 | db_begin_write(); |
| 238 | chat_purge(); |
| 239 | if( nByte==0 ){ |
| 240 | if( zMsg[0] ){ |
| 241 | db_multi_exec( |
| 242 | "INSERT INTO chat(mtime,lmtime,xfrom,xmsg)" |
| 243 | "VALUES(julianday('now'),%Q,%Q,%Q)", |
| 244 | P("lmtime"), g.zLogin, zMsg |
| 245 | ); |
| 246 | } |
| 247 | }else{ |
| 248 | Stmt q; |
| 249 | Blob b; |
| 250 | db_prepare(&q, |
| 251 | "INSERT INTO chat(mtime,lmtime,xfrom,xmsg,file,fname,fmime)" |
| 252 | "VALUES(julianday('now'),%Q,%Q,%Q,:file,%Q,%Q)", |
| 253 | P("lmtime"), g.zLogin, zMsg, PD("file:filename",""), |
| 254 | PD("file:mimetype","application/octet-stream")); |
| 255 | blob_init(&b, P("file"), nByte); |
| 256 | db_bind_blob(&q, ":file", &b); |
| 257 | db_step(&q); |
| 258 | db_finalize(&q); |
| @@ -392,10 +396,11 @@ | |
| 396 | ** | { |
| 397 | ** | "msg":[ |
| 398 | ** | { |
| 399 | ** | "msgid": integer // message id |
| 400 | ** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC |
| 401 | ** | "lmtime: text // Localtime where the message was sent from |
| 402 | ** | "xfrom": text // Login name of sender |
| 403 | ** | "uclr": text // Color string associated with the user |
| 404 | ** | "xmsg": text // HTML text of the message |
| 405 | ** | "fsize": integer // file attachment size in bytes |
| 406 | ** | "fname": text // Name of file attachment |
| @@ -409,10 +414,12 @@ | |
| 414 | ** than zero. The "xmsg" field may be an empty string if "fsize" is zero. |
| 415 | ** |
| 416 | ** The "msgid" values will be in increasing order. |
| 417 | ** |
| 418 | ** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero. |
| 419 | ** |
| 420 | ** The "lmtime" value might be known, in which case it is omitted. |
| 421 | ** |
| 422 | ** The messages are ordered oldest first unless "before" is provided, in which |
| 423 | ** case they are sorted newest first (to facilitate the client-side UI update). |
| 424 | */ |
| 425 | void chat_poll_webpage(void){ |
| @@ -431,11 +438,11 @@ | |
| 438 | chat_create_tables(); |
| 439 | cgi_set_content_type("text/json"); |
| 440 | dataVersion = db_int64(0, "PRAGMA data_version"); |
| 441 | blob_append_sql(&sql, |
| 442 | "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file)," |
| 443 | " fname, fmime, %s, lmtime" |
| 444 | " FROM chat ", |
| 445 | msgBefore>0 ? "0 as mdel" : "mdel"); |
| 446 | if( msgid<=0 || msgBefore>0 ){ |
| 447 | db_begin_write(); |
| 448 | chat_purge(); |
| @@ -475,16 +482,20 @@ | |
| 482 | const char *zRawMsg = db_column_text(&q1, 3); |
| 483 | int nByte = db_column_int(&q1, 4); |
| 484 | const char *zFName = db_column_text(&q1, 5); |
| 485 | const char *zFMime = db_column_text(&q1, 6); |
| 486 | int iToDel = db_column_int(&q1, 7); |
| 487 | const char *zLMtime = db_column_text(&q1, 8); |
| 488 | char *zMsg; |
| 489 | if(cnt++){ |
| 490 | blob_append(&json, ",\n", 2); |
| 491 | } |
| 492 | blob_appendf(&json, "{\"msgid\":%d,", id); |
| 493 | blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11); |
| 494 | if( zLMtime && zLMtime[0] ){ |
| 495 | blob_appendf(&json, "\"lmtime\":%!j,", zLMtime); |
| 496 | } |
| 497 | blob_appendf(&json, "\"xfrom\":%!j,", zFrom); |
| 498 | blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom)); |
| 499 | |
| 500 | zMsg = chat_format_to_html(zRawMsg ? zRawMsg : ""); |
| 501 | blob_appendf(&json, "\"xmsg\":%!j,", zMsg); |
| @@ -494,10 +505,11 @@ | |
| 505 | blob_appendf(&json, "\"fsize\":0"); |
| 506 | }else{ |
| 507 | blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j", |
| 508 | nByte, zFName, zFMime); |
| 509 | } |
| 510 | |
| 511 | if( iToDel ){ |
| 512 | blob_appendf(&json, ",\"mdel\":%d}", iToDel); |
| 513 | }else{ |
| 514 | blob_append(&json, "}", 1); |
| 515 | } |
| 516 |
+25
-4
| --- src/chat.js | ||
| +++ src/chat.js | ||
| @@ -352,10 +352,11 @@ | ||
| 352 | 352 | return this; |
| 353 | 353 | }, |
| 354 | 354 | setMessage: function(m){ |
| 355 | 355 | const ds = this.e.body.dataset; |
| 356 | 356 | ds.timestamp = m.mtime; |
| 357 | + ds.lmtime = m.lmtime; | |
| 357 | 358 | ds.msgid = m.msgid; |
| 358 | 359 | ds.xfrom = m.xfrom; |
| 359 | 360 | if(m.xfrom === Chat.me){ |
| 360 | 361 | D.addClass(this.e.body, 'mine'); |
| 361 | 362 | } |
| @@ -492,21 +493,34 @@ | ||
| 492 | 493 | (k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true) |
| 493 | 494 | ); |
| 494 | 495 | return bxs; |
| 495 | 496 | })()/*drag/drop*/; |
| 496 | 497 | |
| 498 | + const tzOffsetToString = function(off){ | |
| 499 | + const hours = Math.round(off/60), min = Math.round(off % 30); | |
| 500 | + return ''+(hours + (min ? '.5' : '')); | |
| 501 | + }; | |
| 502 | + const pad2 = (x)=>('0'+x).substr(-2); | |
| 503 | + const localTime8601 = function(d){ | |
| 504 | + return [ | |
| 505 | + d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()+1), | |
| 506 | + 'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds()), | |
| 507 | + 'Z', tzOffsetToString(d.getTimezoneOffset()) | |
| 508 | + ].join(''); | |
| 509 | + }; | |
| 510 | + | |
| 497 | 511 | Chat.submitMessage = function(){ |
| 498 | 512 | const fd = new FormData(this.e.inputForm) |
| 499 | 513 | /* ^^^^ we don't really want/need the FORM element, but when |
| 500 | 514 | FormData() is default-constructed here then the server |
| 501 | 515 | segfaults, and i have no clue why! */; |
| 502 | 516 | const msg = this.inputValue(); |
| 503 | 517 | if(msg) fd.set('msg',msg); |
| 504 | 518 | const file = BlobXferState.blob || this.e.inputFile.files[0]; |
| 505 | 519 | if(file) fd.set("file", file); |
| 506 | - fd.set("lmtime", new Date().toISOString()); | |
| 507 | 520 | if( msg || file ){ |
| 521 | + fd.set("lmtime", localTime8601(new Date())); | |
| 508 | 522 | fetch("chat-send",{ |
| 509 | 523 | method: 'POST', |
| 510 | 524 | body: fd |
| 511 | 525 | }); |
| 512 | 526 | } |
| @@ -552,11 +566,11 @@ | ||
| 552 | 566 | ].join(''); |
| 553 | 567 | }; |
| 554 | 568 | /* Returns an almost-ISO8601 form of Date object d. */ |
| 555 | 569 | const iso8601ish = function(d){ |
| 556 | 570 | return d.toISOString() |
| 557 | - .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' GMT'); | |
| 571 | + .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' Zulu'); | |
| 558 | 572 | }; |
| 559 | 573 | /* Event handler for clicking .message-user elements to show their |
| 560 | 574 | timestamps. */ |
| 561 | 575 | const handleLegendClicked = function f(ev){ |
| 562 | 576 | if(!f.popup){ |
| @@ -568,16 +582,23 @@ | ||
| 568 | 582 | if(!eMsg) return; |
| 569 | 583 | D.clearElement(this.e); |
| 570 | 584 | const d = new Date(eMsg.dataset.timestamp); |
| 571 | 585 | if(d.getMinutes().toString()!=="NaN"){ |
| 572 | 586 | // Date works, render informative timestamps |
| 587 | + const xfrom = eMsg.dataset.xfrom; | |
| 573 | 588 | D.append(this.e, |
| 574 | - D.append(D.span(), localTimeString(d)," client-local"), | |
| 589 | + D.append(D.span(), localTimeString(d)," ",Chat.me," time"), | |
| 575 | 590 | D.append(D.span(), iso8601ish(d))); |
| 591 | + if(xfrom!==Chat.me){ | |
| 592 | + D.append(this.e, | |
| 593 | + D.append(D.span(), localTime8601( | |
| 594 | + new Date(eMsg.dataset.lmtime) | |
| 595 | + )," ",xfrom," time")); | |
| 596 | + } | |
| 576 | 597 | }else{ |
| 577 | 598 | // Date doesn't work, so dumb it down... |
| 578 | - D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," GMT")); | |
| 599 | + D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," Zulu")); | |
| 579 | 600 | } |
| 580 | 601 | const toolbar = D.addClass(D.div(), 'toolbar'); |
| 581 | 602 | D.append(this.e, toolbar); |
| 582 | 603 | const btnDeleteLocal = D.button("Delete locally"); |
| 583 | 604 | D.append(toolbar, btnDeleteLocal); |
| 584 | 605 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -352,10 +352,11 @@ | |
| 352 | return this; |
| 353 | }, |
| 354 | setMessage: function(m){ |
| 355 | const ds = this.e.body.dataset; |
| 356 | ds.timestamp = m.mtime; |
| 357 | ds.msgid = m.msgid; |
| 358 | ds.xfrom = m.xfrom; |
| 359 | if(m.xfrom === Chat.me){ |
| 360 | D.addClass(this.e.body, 'mine'); |
| 361 | } |
| @@ -492,21 +493,34 @@ | |
| 492 | (k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true) |
| 493 | ); |
| 494 | return bxs; |
| 495 | })()/*drag/drop*/; |
| 496 | |
| 497 | Chat.submitMessage = function(){ |
| 498 | const fd = new FormData(this.e.inputForm) |
| 499 | /* ^^^^ we don't really want/need the FORM element, but when |
| 500 | FormData() is default-constructed here then the server |
| 501 | segfaults, and i have no clue why! */; |
| 502 | const msg = this.inputValue(); |
| 503 | if(msg) fd.set('msg',msg); |
| 504 | const file = BlobXferState.blob || this.e.inputFile.files[0]; |
| 505 | if(file) fd.set("file", file); |
| 506 | fd.set("lmtime", new Date().toISOString()); |
| 507 | if( msg || file ){ |
| 508 | fetch("chat-send",{ |
| 509 | method: 'POST', |
| 510 | body: fd |
| 511 | }); |
| 512 | } |
| @@ -552,11 +566,11 @@ | |
| 552 | ].join(''); |
| 553 | }; |
| 554 | /* Returns an almost-ISO8601 form of Date object d. */ |
| 555 | const iso8601ish = function(d){ |
| 556 | return d.toISOString() |
| 557 | .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' GMT'); |
| 558 | }; |
| 559 | /* Event handler for clicking .message-user elements to show their |
| 560 | timestamps. */ |
| 561 | const handleLegendClicked = function f(ev){ |
| 562 | if(!f.popup){ |
| @@ -568,16 +582,23 @@ | |
| 568 | if(!eMsg) return; |
| 569 | D.clearElement(this.e); |
| 570 | const d = new Date(eMsg.dataset.timestamp); |
| 571 | if(d.getMinutes().toString()!=="NaN"){ |
| 572 | // Date works, render informative timestamps |
| 573 | D.append(this.e, |
| 574 | D.append(D.span(), localTimeString(d)," client-local"), |
| 575 | D.append(D.span(), iso8601ish(d))); |
| 576 | }else{ |
| 577 | // Date doesn't work, so dumb it down... |
| 578 | D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," GMT")); |
| 579 | } |
| 580 | const toolbar = D.addClass(D.div(), 'toolbar'); |
| 581 | D.append(this.e, toolbar); |
| 582 | const btnDeleteLocal = D.button("Delete locally"); |
| 583 | D.append(toolbar, btnDeleteLocal); |
| 584 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -352,10 +352,11 @@ | |
| 352 | return this; |
| 353 | }, |
| 354 | setMessage: function(m){ |
| 355 | const ds = this.e.body.dataset; |
| 356 | ds.timestamp = m.mtime; |
| 357 | ds.lmtime = m.lmtime; |
| 358 | ds.msgid = m.msgid; |
| 359 | ds.xfrom = m.xfrom; |
| 360 | if(m.xfrom === Chat.me){ |
| 361 | D.addClass(this.e.body, 'mine'); |
| 362 | } |
| @@ -492,21 +493,34 @@ | |
| 493 | (k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true) |
| 494 | ); |
| 495 | return bxs; |
| 496 | })()/*drag/drop*/; |
| 497 | |
| 498 | const tzOffsetToString = function(off){ |
| 499 | const hours = Math.round(off/60), min = Math.round(off % 30); |
| 500 | return ''+(hours + (min ? '.5' : '')); |
| 501 | }; |
| 502 | const pad2 = (x)=>('0'+x).substr(-2); |
| 503 | const localTime8601 = function(d){ |
| 504 | return [ |
| 505 | d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()+1), |
| 506 | 'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds()), |
| 507 | 'Z', tzOffsetToString(d.getTimezoneOffset()) |
| 508 | ].join(''); |
| 509 | }; |
| 510 | |
| 511 | Chat.submitMessage = function(){ |
| 512 | const fd = new FormData(this.e.inputForm) |
| 513 | /* ^^^^ we don't really want/need the FORM element, but when |
| 514 | FormData() is default-constructed here then the server |
| 515 | segfaults, and i have no clue why! */; |
| 516 | const msg = this.inputValue(); |
| 517 | if(msg) fd.set('msg',msg); |
| 518 | const file = BlobXferState.blob || this.e.inputFile.files[0]; |
| 519 | if(file) fd.set("file", file); |
| 520 | if( msg || file ){ |
| 521 | fd.set("lmtime", localTime8601(new Date())); |
| 522 | fetch("chat-send",{ |
| 523 | method: 'POST', |
| 524 | body: fd |
| 525 | }); |
| 526 | } |
| @@ -552,11 +566,11 @@ | |
| 566 | ].join(''); |
| 567 | }; |
| 568 | /* Returns an almost-ISO8601 form of Date object d. */ |
| 569 | const iso8601ish = function(d){ |
| 570 | return d.toISOString() |
| 571 | .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' Zulu'); |
| 572 | }; |
| 573 | /* Event handler for clicking .message-user elements to show their |
| 574 | timestamps. */ |
| 575 | const handleLegendClicked = function f(ev){ |
| 576 | if(!f.popup){ |
| @@ -568,16 +582,23 @@ | |
| 582 | if(!eMsg) return; |
| 583 | D.clearElement(this.e); |
| 584 | const d = new Date(eMsg.dataset.timestamp); |
| 585 | if(d.getMinutes().toString()!=="NaN"){ |
| 586 | // Date works, render informative timestamps |
| 587 | const xfrom = eMsg.dataset.xfrom; |
| 588 | D.append(this.e, |
| 589 | D.append(D.span(), localTimeString(d)," ",Chat.me," time"), |
| 590 | D.append(D.span(), iso8601ish(d))); |
| 591 | if(xfrom!==Chat.me){ |
| 592 | D.append(this.e, |
| 593 | D.append(D.span(), localTime8601( |
| 594 | new Date(eMsg.dataset.lmtime) |
| 595 | )," ",xfrom," time")); |
| 596 | } |
| 597 | }else{ |
| 598 | // Date doesn't work, so dumb it down... |
| 599 | D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," Zulu")); |
| 600 | } |
| 601 | const toolbar = D.addClass(D.div(), 'toolbar'); |
| 602 | D.append(this.e, toolbar); |
| 603 | const btnDeleteLocal = D.button("Delete locally"); |
| 604 | D.append(toolbar, btnDeleteLocal); |
| 605 |