Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Integrated client-local timestamp into chat so that participants can see the local time in their colleagues' time zones. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
3c5e2badc8254dc72c400f6e557a5667 |
| User & Date: | stephan 2020-12-26 20:24:14.258 |
Context
|
2020-12-26
| ||
| 20:26 | Only apply sender-local time if it's in the message (older messages do not have it). ... (check-in: f2a58f10bf user: stephan tags: trunk) | |
| 20:24 | Integrated client-local timestamp into chat so that participants can see the local time in their colleagues' time zones. ... (check-in: 3c5e2badc8 user: stephan tags: trunk) | |
| 19:43 | Added lmtime (local mtime) parameter to /chat-send calls, per chat discussion. ... (check-in: 152ac599e8 user: stephan tags: trunk) | |
Changes
Changes to src/chat.c.
| ︙ | ︙ | |||
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
/* Definition of repository tables used by chat
*/
static const char zChatSchema1[] =
@ CREATE TABLE repository.chat(
@ msgid INTEGER PRIMARY KEY AUTOINCREMENT,
@ mtime JULIANDAY, -- Time for this entry - Julianday Zulu
@ xfrom TEXT, -- Login of the sender
@ xmsg TEXT, -- Raw, unformatted text of the message
@ file BLOB, -- Text of the uploaded file, or NULL
@ fname TEXT, -- Filename of the uploaded file, or NULL
@ fmime TEXT, -- MIMEType of the upload file, or NULL
@ mdel INT -- msgid of another message to delete
@ );
;
/*
** Make sure the repository data tables used by chat exist. Create them
** if they do not.
*/
static void chat_create_tables(void){
if( !db_table_exists("repository","chat") ){
db_multi_exec(zChatSchema1/*works-like:""*/);
| > | > | > > | 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
/* Definition of repository tables used by chat
*/
static const char zChatSchema1[] =
@ CREATE TABLE repository.chat(
@ msgid INTEGER PRIMARY KEY AUTOINCREMENT,
@ mtime JULIANDAY, -- Time for this entry - Julianday Zulu
@ lmtime TEXT, -- Localtime when message originall sent
@ xfrom TEXT, -- Login of the sender
@ xmsg TEXT, -- Raw, unformatted text of the message
@ file BLOB, -- Text of the uploaded file, or NULL
@ fname TEXT, -- Filename of the uploaded file, or NULL
@ fmime TEXT, -- MIMEType of the upload file, or NULL
@ mdel INT -- msgid of another message to delete
@ );
;
/*
** Make sure the repository data tables used by chat exist. Create them
** if they do not.
*/
static void chat_create_tables(void){
if( !db_table_exists("repository","chat") ){
db_multi_exec(zChatSchema1/*works-like:""*/);
}else if( !db_table_has_column("repository","chat","lmtime") ){
if( !db_table_has_column("repository","chat","mdel") ){
db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT");
}
db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT");
}
}
/*
** Delete old content from the chat table.
*/
static void chat_purge(void){
|
| ︙ | ︙ | |||
231 232 233 234 235 236 237 |
nByte = atoi(PD("file:bytes",0));
zMsg = PD("msg","");
db_begin_write();
chat_purge();
if( nByte==0 ){
if( zMsg[0] ){
db_multi_exec(
| | | | | | | | 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
nByte = atoi(PD("file:bytes",0));
zMsg = PD("msg","");
db_begin_write();
chat_purge();
if( nByte==0 ){
if( zMsg[0] ){
db_multi_exec(
"INSERT INTO chat(mtime,lmtime,xfrom,xmsg)"
"VALUES(julianday('now'),%Q,%Q,%Q)",
P("lmtime"), g.zLogin, zMsg
);
}
}else{
Stmt q;
Blob b;
db_prepare(&q,
"INSERT INTO chat(mtime,lmtime,xfrom,xmsg,file,fname,fmime)"
"VALUES(julianday('now'),%Q,%Q,%Q,:file,%Q,%Q)",
P("lmtime"), g.zLogin, zMsg, PD("file:filename",""),
PD("file:mimetype","application/octet-stream"));
blob_init(&b, P("file"), nByte);
db_bind_blob(&q, ":file", &b);
db_step(&q);
db_finalize(&q);
blob_reset(&b);
}
|
| ︙ | ︙ | |||
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
** Format of the json:
**
** | {
** | "msg":[
** | {
** | "msgid": integer // message id
** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC
** | "xfrom": text // Login name of sender
** | "uclr": text // Color string associated with the user
** | "xmsg": text // HTML text of the message
** | "fsize": integer // file attachment size in bytes
** | "fname": text // Name of file attachment
** | "fmime": text // MIME-type of file attachment
** | "mdel": integer // message id of prior message to delete
** | }
** | ]
** | }
**
** The "fname" and "fmime" fields are only present if "fsize" is greater
** than zero. The "xmsg" field may be an empty string if "fsize" is zero.
**
** The "msgid" values will be in increasing order.
**
** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero.
**
** The messages are ordered oldest first unless "before" is provided, in which
** case they are sorted newest first (to facilitate the client-side UI update).
*/
void chat_poll_webpage(void){
Blob json; /* The json to be constructed and returned */
sqlite3_int64 dataVersion; /* Data version. Used for polling. */
| > > > | 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
** Format of the json:
**
** | {
** | "msg":[
** | {
** | "msgid": integer // message id
** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC
** | "lmtime: text // Localtime where the message was sent from
** | "xfrom": text // Login name of sender
** | "uclr": text // Color string associated with the user
** | "xmsg": text // HTML text of the message
** | "fsize": integer // file attachment size in bytes
** | "fname": text // Name of file attachment
** | "fmime": text // MIME-type of file attachment
** | "mdel": integer // message id of prior message to delete
** | }
** | ]
** | }
**
** The "fname" and "fmime" fields are only present if "fsize" is greater
** than zero. The "xmsg" field may be an empty string if "fsize" is zero.
**
** The "msgid" values will be in increasing order.
**
** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero.
**
** The "lmtime" value might be known, in which case it is omitted.
**
** The messages are ordered oldest first unless "before" is provided, in which
** case they are sorted newest first (to facilitate the client-side UI update).
*/
void chat_poll_webpage(void){
Blob json; /* The json to be constructed and returned */
sqlite3_int64 dataVersion; /* Data version. Used for polling. */
|
| ︙ | ︙ | |||
429 430 431 432 433 434 435 |
login_check_credentials();
if( !g.perm.Chat ) return;
chat_create_tables();
cgi_set_content_type("text/json");
dataVersion = db_int64(0, "PRAGMA data_version");
blob_append_sql(&sql,
"SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
| | | 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 |
login_check_credentials();
if( !g.perm.Chat ) return;
chat_create_tables();
cgi_set_content_type("text/json");
dataVersion = db_int64(0, "PRAGMA data_version");
blob_append_sql(&sql,
"SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
" fname, fmime, %s, lmtime"
" FROM chat ",
msgBefore>0 ? "0 as mdel" : "mdel");
if( msgid<=0 || msgBefore>0 ){
db_begin_write();
chat_purge();
db_commit_transaction();
}
|
| ︙ | ︙ | |||
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 |
const char *zDate = db_column_text(&q1, 1);
const char *zFrom = db_column_text(&q1, 2);
const char *zRawMsg = db_column_text(&q1, 3);
int nByte = db_column_int(&q1, 4);
const char *zFName = db_column_text(&q1, 5);
const char *zFMime = db_column_text(&q1, 6);
int iToDel = db_column_int(&q1, 7);
char *zMsg;
if(cnt++){
blob_append(&json, ",\n", 2);
}
blob_appendf(&json, "{\"msgid\":%d,", id);
blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
blob_appendf(&json, "\"xfrom\":%!j,", zFrom);
blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom));
zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
blob_appendf(&json, "\"xmsg\":%!j,", zMsg);
fossil_free(zMsg);
if( nByte==0 ){
blob_appendf(&json, "\"fsize\":0");
}else{
blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
nByte, zFName, zFMime);
}
if( iToDel ){
blob_appendf(&json, ",\"mdel\":%d}", iToDel);
}else{
blob_append(&json, "}", 1);
}
}
db_reset(&q1);
| > > > > > | 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 |
const char *zDate = db_column_text(&q1, 1);
const char *zFrom = db_column_text(&q1, 2);
const char *zRawMsg = db_column_text(&q1, 3);
int nByte = db_column_int(&q1, 4);
const char *zFName = db_column_text(&q1, 5);
const char *zFMime = db_column_text(&q1, 6);
int iToDel = db_column_int(&q1, 7);
const char *zLMtime = db_column_text(&q1, 8);
char *zMsg;
if(cnt++){
blob_append(&json, ",\n", 2);
}
blob_appendf(&json, "{\"msgid\":%d,", id);
blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
if( zLMtime && zLMtime[0] ){
blob_appendf(&json, "\"lmtime\":%!j,", zLMtime);
}
blob_appendf(&json, "\"xfrom\":%!j,", zFrom);
blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom));
zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
blob_appendf(&json, "\"xmsg\":%!j,", zMsg);
fossil_free(zMsg);
if( nByte==0 ){
blob_appendf(&json, "\"fsize\":0");
}else{
blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
nByte, zFName, zFMime);
}
if( iToDel ){
blob_appendf(&json, ",\"mdel\":%d}", iToDel);
}else{
blob_append(&json, "}", 1);
}
}
db_reset(&q1);
|
| ︙ | ︙ |
Changes to src/chat.js.
| ︙ | ︙ | |||
350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
setPopupCallback: function(callback){
this.e.tab.addEventListener('click', callback, false);
return this;
},
setMessage: function(m){
const ds = this.e.body.dataset;
ds.timestamp = m.mtime;
ds.msgid = m.msgid;
ds.xfrom = m.xfrom;
if(m.xfrom === Chat.me){
D.addClass(this.e.body, 'mine');
}
this.e.content.style.backgroundColor = m.uclr;
this.e.tab.style.backgroundColor = m.uclr;
| > | 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
setPopupCallback: function(callback){
this.e.tab.addEventListener('click', callback, false);
return this;
},
setMessage: function(m){
const ds = this.e.body.dataset;
ds.timestamp = m.mtime;
ds.lmtime = m.lmtime;
ds.msgid = m.msgid;
ds.xfrom = m.xfrom;
if(m.xfrom === Chat.me){
D.addClass(this.e.body, 'mine');
}
this.e.content.style.backgroundColor = m.uclr;
this.e.tab.style.backgroundColor = m.uclr;
|
| ︙ | ︙ | |||
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 |
};
Object.keys(dropEvents).forEach(
(k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true)
);
return bxs;
})()/*drag/drop*/;
Chat.submitMessage = function(){
const fd = new FormData(this.e.inputForm)
/* ^^^^ we don't really want/need the FORM element, but when
FormData() is default-constructed here then the server
segfaults, and i have no clue why! */;
const msg = this.inputValue();
if(msg) fd.set('msg',msg);
const file = BlobXferState.blob || this.e.inputFile.files[0];
if(file) fd.set("file", file);
| > > > > > > > > > > > > > < > | 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 |
};
Object.keys(dropEvents).forEach(
(k)=>Chat.e.inputFile.addEventListener(k, dropEvents[k], true)
);
return bxs;
})()/*drag/drop*/;
const tzOffsetToString = function(off){
const hours = Math.round(off/60), min = Math.round(off % 30);
return ''+(hours + (min ? '.5' : ''));
};
const pad2 = (x)=>('0'+x).substr(-2);
const localTime8601 = function(d){
return [
d.getYear()+1900, '-', pad2(d.getMonth()+1), '-', pad2(d.getDate()+1),
'T', pad2(d.getHours()),':', pad2(d.getMinutes()),':',pad2(d.getSeconds()),
'Z', tzOffsetToString(d.getTimezoneOffset())
].join('');
};
Chat.submitMessage = function(){
const fd = new FormData(this.e.inputForm)
/* ^^^^ we don't really want/need the FORM element, but when
FormData() is default-constructed here then the server
segfaults, and i have no clue why! */;
const msg = this.inputValue();
if(msg) fd.set('msg',msg);
const file = BlobXferState.blob || this.e.inputFile.files[0];
if(file) fd.set("file", file);
if( msg || file ){
fd.set("lmtime", localTime8601(new Date()));
fetch("chat-send",{
method: 'POST',
body: fd
});
}
BlobXferState.clear();
Chat.inputValue("").inputFocus();
|
| ︙ | ︙ | |||
550 551 552 553 554 555 556 |
' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()),
':',ff.pad(d.getSeconds())
].join('');
};
/* Returns an almost-ISO8601 form of Date object d. */
const iso8601ish = function(d){
return d.toISOString()
| | > | > > > > > > | | 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 |
' ',ff.pad(d.getHours()),':',ff.pad(d.getMinutes()),
':',ff.pad(d.getSeconds())
].join('');
};
/* Returns an almost-ISO8601 form of Date object d. */
const iso8601ish = function(d){
return d.toISOString()
.replace('T',' ').replace(/\.\d+/,'').replace('Z', ' Zulu');
};
/* Event handler for clicking .message-user elements to show their
timestamps. */
const handleLegendClicked = function f(ev){
if(!f.popup){
/* Timestamp popup widget */
f.popup = new F.PopupWidget({
cssClass: ['fossil-tooltip', 'chat-message-popup'],
refresh:function(){
const eMsg = this._eMsg;
if(!eMsg) return;
D.clearElement(this.e);
const d = new Date(eMsg.dataset.timestamp);
if(d.getMinutes().toString()!=="NaN"){
// Date works, render informative timestamps
const xfrom = eMsg.dataset.xfrom;
D.append(this.e,
D.append(D.span(), localTimeString(d)," ",Chat.me," time"),
D.append(D.span(), iso8601ish(d)));
if(xfrom!==Chat.me){
D.append(this.e,
D.append(D.span(), localTime8601(
new Date(eMsg.dataset.lmtime)
)," ",xfrom," time"));
}
}else{
// Date doesn't work, so dumb it down...
D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," Zulu"));
}
const toolbar = D.addClass(D.div(), 'toolbar');
D.append(this.e, toolbar);
const btnDeleteLocal = D.button("Delete locally");
D.append(toolbar, btnDeleteLocal);
const self = this;
btnDeleteLocal.addEventListener('click', function(){
|
| ︙ | ︙ |