Fossil SCM

/chat: add a button to the user/timestamp popup to toggle between parsed and plain-text rendering for that message, per /chat request.

stephan 2021-09-21 04:09 chat-markdown
Commit 9a17e76ebf6eb6095f5665ee63bd27dffe1dbf6631b844b864bf798049e72de0
+77
--- src/chat.c
+++ src/chat.c
@@ -597,10 +597,87 @@
597597
db_finalize(&q1);
598598
blob_append(&json, "\n]}", 3);
599599
cgi_set_content(&json);
600600
return;
601601
}
602
+
603
+/*
604
+** WEBPAGE: chat-fetch-one hidden
605
+**
606
+** /chat-fetch-one/N
607
+**
608
+** Fetches a single message with the given ID, if available.
609
+**
610
+** Options:
611
+**
612
+** raw = the xmsg field will be returned unparsed.
613
+**
614
+** Response is either a single object in the format returned by
615
+** /chat-poll (without the wrapper array) or a JSON-format error
616
+** response, as documented for ajax_route_error().
617
+*/
618
+void chat_fetch_one(void){
619
+ Blob json = empty_blob; /* The json to be constructed and returned */
620
+ const int fRaw = PD("raw",0)!=0;
621
+ const int msgid = atoi(PD("name","0"));
622
+ Stmt q;
623
+ login_check_credentials();
624
+ if( !g.perm.Chat ) {
625
+ chat_emit_permissions_error(0);
626
+ return;
627
+ }
628
+ chat_create_tables();
629
+ cgi_set_content_type("application/json");
630
+ db_prepare(&q,
631
+ "SELECT datetime(mtime), xfrom, xmsg, length(file),"
632
+ " fname, fmime, lmtime"
633
+ " FROM chat WHERE msgid=%d AND mdel IS NULL",
634
+ msgid);
635
+ if(SQLITE_ROW==db_step(&q)){
636
+ const char *zDate = db_column_text(&q, 0);
637
+ const char *zFrom = db_column_text(&q, 1);
638
+ const char *zRawMsg = db_column_text(&q, 2);
639
+ const int nByte = db_column_int(&q, 3);
640
+ const char *zFName = db_column_text(&q, 4);
641
+ const char *zFMime = db_column_text(&q, 5);
642
+ const char *zLMtime = db_column_text(&q, 7);
643
+ blob_appendf(&json,"{\"msgid\": %d,", msgid);
644
+
645
+ blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
646
+ if( zLMtime && zLMtime[0] ){
647
+ blob_appendf(&json, "\"lmtime\":%!j,", zLMtime);
648
+ }
649
+ blob_append(&json, "\"xfrom\":", -1);
650
+ if(zFrom){
651
+ blob_appendf(&json, "%!j,", zFrom);
652
+ }else{
653
+ /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
654
+ blob_appendf(&json, "null,");
655
+ }
656
+ blob_appendf(&json, "\"uclr\":%!j,",
657
+ user_color(zFrom ? zFrom : "nobody"));
658
+ blob_append(&json,"\"xmsg\":", 7);
659
+ if(fRaw){
660
+ blob_appendf(&json, "%!j,", zRawMsg);
661
+ }else{
662
+ char * zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
663
+ blob_appendf(&json, "%!j,", zMsg);
664
+ fossil_free(zMsg);
665
+ }
666
+ if( nByte==0 ){
667
+ blob_appendf(&json, "\"fsize\":0");
668
+ }else{
669
+ blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
670
+ nByte, zFName, zFMime);
671
+ }
672
+ blob_append(&json,"}",1);
673
+ cgi_set_content(&json);
674
+ }else{
675
+ ajax_route_error(404,"Chat message #%d not found.", msgid);
676
+ }
677
+ db_finalize(&q);
678
+}
602679
603680
/*
604681
** WEBPAGE: chat-download hidden
605682
**
606683
** Download the CHAT.FILE attachment associated with a single chat
607684
--- src/chat.c
+++ src/chat.c
@@ -597,10 +597,87 @@
597 db_finalize(&q1);
598 blob_append(&json, "\n]}", 3);
599 cgi_set_content(&json);
600 return;
601 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
603 /*
604 ** WEBPAGE: chat-download hidden
605 **
606 ** Download the CHAT.FILE attachment associated with a single chat
607
--- src/chat.c
+++ src/chat.c
@@ -597,10 +597,87 @@
597 db_finalize(&q1);
598 blob_append(&json, "\n]}", 3);
599 cgi_set_content(&json);
600 return;
601 }
602
603 /*
604 ** WEBPAGE: chat-fetch-one hidden
605 **
606 ** /chat-fetch-one/N
607 **
608 ** Fetches a single message with the given ID, if available.
609 **
610 ** Options:
611 **
612 ** raw = the xmsg field will be returned unparsed.
613 **
614 ** Response is either a single object in the format returned by
615 ** /chat-poll (without the wrapper array) or a JSON-format error
616 ** response, as documented for ajax_route_error().
617 */
618 void chat_fetch_one(void){
619 Blob json = empty_blob; /* The json to be constructed and returned */
620 const int fRaw = PD("raw",0)!=0;
621 const int msgid = atoi(PD("name","0"));
622 Stmt q;
623 login_check_credentials();
624 if( !g.perm.Chat ) {
625 chat_emit_permissions_error(0);
626 return;
627 }
628 chat_create_tables();
629 cgi_set_content_type("application/json");
630 db_prepare(&q,
631 "SELECT datetime(mtime), xfrom, xmsg, length(file),"
632 " fname, fmime, lmtime"
633 " FROM chat WHERE msgid=%d AND mdel IS NULL",
634 msgid);
635 if(SQLITE_ROW==db_step(&q)){
636 const char *zDate = db_column_text(&q, 0);
637 const char *zFrom = db_column_text(&q, 1);
638 const char *zRawMsg = db_column_text(&q, 2);
639 const int nByte = db_column_int(&q, 3);
640 const char *zFName = db_column_text(&q, 4);
641 const char *zFMime = db_column_text(&q, 5);
642 const char *zLMtime = db_column_text(&q, 7);
643 blob_appendf(&json,"{\"msgid\": %d,", msgid);
644
645 blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
646 if( zLMtime && zLMtime[0] ){
647 blob_appendf(&json, "\"lmtime\":%!j,", zLMtime);
648 }
649 blob_append(&json, "\"xfrom\":", -1);
650 if(zFrom){
651 blob_appendf(&json, "%!j,", zFrom);
652 }else{
653 /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
654 blob_appendf(&json, "null,");
655 }
656 blob_appendf(&json, "\"uclr\":%!j,",
657 user_color(zFrom ? zFrom : "nobody"));
658 blob_append(&json,"\"xmsg\":", 7);
659 if(fRaw){
660 blob_appendf(&json, "%!j,", zRawMsg);
661 }else{
662 char * zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
663 blob_appendf(&json, "%!j,", zMsg);
664 fossil_free(zMsg);
665 }
666 if( nByte==0 ){
667 blob_appendf(&json, "\"fsize\":0");
668 }else{
669 blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
670 nByte, zFName, zFMime);
671 }
672 blob_append(&json,"}",1);
673 cgi_set_content(&json);
674 }else{
675 ajax_route_error(404,"Chat message #%d not found.", msgid);
676 }
677 db_finalize(&q);
678 }
679
680 /*
681 ** WEBPAGE: chat-download hidden
682 **
683 ** Download the CHAT.FILE attachment associated with a single chat
684
+65 -2
--- src/chat.js
+++ src/chat.js
@@ -485,10 +485,67 @@
485485
F.toast.message("Deleted message "+id+".");
486486
}
487487
return !!e;
488488
};
489489
490
+ /**
491
+ Toggles the given message between its parsed and plain-text
492
+ representations. It requires a server round-trip to collect the
493
+ plain-text form but caches it for subsequent toggles.
494
+
495
+ Expects the ID of a currently-loaded message or a
496
+ message-widget DOM elment from which it can extract an id.
497
+ This is an aync operation the first time it's passed a given
498
+ message and synchronous on subsequent calls for that
499
+ message. It is a no-op if id does not resolve to a loaded
500
+ message.
501
+ */
502
+ cs.toggleTextMode = function(id){
503
+ var e;
504
+ if(id instanceof HTMLElement){
505
+ e = id;
506
+ id = e.dataset.msgid;
507
+ }else{
508
+ e = this.getMessageElemById(id);
509
+ }
510
+ if(!e || !id) return false;
511
+ else if(e.$isToggling) return;
512
+ e.$isToggling = true;
513
+ const content = e.querySelector('.message-widget-content');
514
+ if(!content.$elems){
515
+ content.$elems = [
516
+ content.firstElementChild, // parsed elem
517
+ undefined // plaintext elem
518
+ ];
519
+ }else if(content.$elems[1]){
520
+ // We have both content types. Simply toggle them.
521
+ const child = (
522
+ content.firstElementChild===content.$elems[0]
523
+ ? content.$elems[1]
524
+ : content.$elems[0]
525
+ );
526
+ delete e.$isToggling;
527
+ D.append(D.clearElement(content), child);
528
+ return;
529
+ }
530
+ // We need to fetch the plain-text version...
531
+ const self = this;
532
+ F.fetch('chat-fetch-one',{
533
+ urlParams:{ name: id, raw: true},
534
+ responseType: 'json',
535
+ onload: function(msg){
536
+ content.$elems[1] = D.append(D.pre(),msg.xmsg);
537
+ self.toggleTextMode(e);
538
+ },
539
+ aftersend:function(){
540
+ delete e.$isToggling;
541
+ Chat.ajaxEnd();
542
+ }
543
+ });
544
+ return true;
545
+ };
546
+
490547
/** Given a .message-row element, this function returns whethe the
491548
current user may, at least hypothetically, delete the message
492549
globally. A user may always delete a local copy of a
493550
post. The server may trump this, e.g. if the login has been
494551
cancelled after this page was loaded.
@@ -742,14 +799,20 @@
742799
btnDeleteGlobal.addEventListener('click', function(){
743800
self.hide();
744801
Chat.deleteMessage(eMsg);
745802
});
746803
}
804
+ const toolbar2 = D.addClass(D.div(), 'toolbar');
805
+ D.append(this.e, toolbar2);
806
+ const btnToggleText = D.button("Toggle text mode");
807
+ btnToggleText.addEventListener('click', function(){
808
+ self.hide();
809
+ Chat.toggleTextMode(eMsg);
810
+ });
811
+ D.append(toolbar2, btnToggleText);
747812
if(eMsg.dataset.xfrom){
748813
/* Add a link to the /timeline filtered on this user. */
749
- const toolbar2 = D.addClass(D.div(), 'toolbar');
750
- D.append(this.e, toolbar2);
751814
const timelineLink = D.attr(
752815
D.a(F.repoUrl('timeline',{
753816
u: eMsg.dataset.xfrom,
754817
y: 'a'
755818
}), "User's Timeline"),
756819
--- src/chat.js
+++ src/chat.js
@@ -485,10 +485,67 @@
485 F.toast.message("Deleted message "+id+".");
486 }
487 return !!e;
488 };
489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490 /** Given a .message-row element, this function returns whethe the
491 current user may, at least hypothetically, delete the message
492 globally. A user may always delete a local copy of a
493 post. The server may trump this, e.g. if the login has been
494 cancelled after this page was loaded.
@@ -742,14 +799,20 @@
742 btnDeleteGlobal.addEventListener('click', function(){
743 self.hide();
744 Chat.deleteMessage(eMsg);
745 });
746 }
 
 
 
 
 
 
 
 
747 if(eMsg.dataset.xfrom){
748 /* Add a link to the /timeline filtered on this user. */
749 const toolbar2 = D.addClass(D.div(), 'toolbar');
750 D.append(this.e, toolbar2);
751 const timelineLink = D.attr(
752 D.a(F.repoUrl('timeline',{
753 u: eMsg.dataset.xfrom,
754 y: 'a'
755 }), "User's Timeline"),
756
--- src/chat.js
+++ src/chat.js
@@ -485,10 +485,67 @@
485 F.toast.message("Deleted message "+id+".");
486 }
487 return !!e;
488 };
489
490 /**
491 Toggles the given message between its parsed and plain-text
492 representations. It requires a server round-trip to collect the
493 plain-text form but caches it for subsequent toggles.
494
495 Expects the ID of a currently-loaded message or a
496 message-widget DOM elment from which it can extract an id.
497 This is an aync operation the first time it's passed a given
498 message and synchronous on subsequent calls for that
499 message. It is a no-op if id does not resolve to a loaded
500 message.
501 */
502 cs.toggleTextMode = function(id){
503 var e;
504 if(id instanceof HTMLElement){
505 e = id;
506 id = e.dataset.msgid;
507 }else{
508 e = this.getMessageElemById(id);
509 }
510 if(!e || !id) return false;
511 else if(e.$isToggling) return;
512 e.$isToggling = true;
513 const content = e.querySelector('.message-widget-content');
514 if(!content.$elems){
515 content.$elems = [
516 content.firstElementChild, // parsed elem
517 undefined // plaintext elem
518 ];
519 }else if(content.$elems[1]){
520 // We have both content types. Simply toggle them.
521 const child = (
522 content.firstElementChild===content.$elems[0]
523 ? content.$elems[1]
524 : content.$elems[0]
525 );
526 delete e.$isToggling;
527 D.append(D.clearElement(content), child);
528 return;
529 }
530 // We need to fetch the plain-text version...
531 const self = this;
532 F.fetch('chat-fetch-one',{
533 urlParams:{ name: id, raw: true},
534 responseType: 'json',
535 onload: function(msg){
536 content.$elems[1] = D.append(D.pre(),msg.xmsg);
537 self.toggleTextMode(e);
538 },
539 aftersend:function(){
540 delete e.$isToggling;
541 Chat.ajaxEnd();
542 }
543 });
544 return true;
545 };
546
547 /** Given a .message-row element, this function returns whethe the
548 current user may, at least hypothetically, delete the message
549 globally. A user may always delete a local copy of a
550 post. The server may trump this, e.g. if the login has been
551 cancelled after this page was loaded.
@@ -742,14 +799,20 @@
799 btnDeleteGlobal.addEventListener('click', function(){
800 self.hide();
801 Chat.deleteMessage(eMsg);
802 });
803 }
804 const toolbar2 = D.addClass(D.div(), 'toolbar');
805 D.append(this.e, toolbar2);
806 const btnToggleText = D.button("Toggle text mode");
807 btnToggleText.addEventListener('click', function(){
808 self.hide();
809 Chat.toggleTextMode(eMsg);
810 });
811 D.append(toolbar2, btnToggleText);
812 if(eMsg.dataset.xfrom){
813 /* Add a link to the /timeline filtered on this user. */
 
 
814 const timelineLink = D.attr(
815 D.a(F.repoUrl('timeline',{
816 u: eMsg.dataset.xfrom,
817 y: 'a'
818 }), "User's Timeline"),
819
+6 -2
--- src/default.css
+++ src/default.css
@@ -1636,11 +1636,11 @@
16361636
body.chat.monospace-messages .message-widget-content,
16371637
body.chat.monospace-messages textarea,
16381638
body.chat.monospace-messages input[type=text]{
16391639
font-family: monospace;
16401640
}
1641
-body.chat .message-widget-content > .markdown {
1641
+body.chat .message-widget-content > * {
16421642
margin: 0;
16431643
padding: 0;
16441644
}
16451645
body.chat .message-widget-content > .markdown > *:first-child {
16461646
margin-top: 0;
@@ -1679,18 +1679,22 @@
16791679
}
16801680
/* Full message timestamps. */
16811681
body.chat .chat-message-popup > span { white-space: nowrap; }
16821682
/* Container for the message deletion buttons. */
16831683
body.chat .chat-message-popup > .toolbar {
1684
- padding: 0.2em;
1684
+ padding: 0;
16851685
margin: 0;
16861686
border: 2px inset rgba(0,0,0,0.3);
16871687
border-radius: 0.25em;
16881688
display: flex;
16891689
flex-direction: row;
16901690
justify-content: stretch;
16911691
flex-wrap: wrap;
1692
+ align-items: center;
1693
+}
1694
+body.chat .chat-message-popup > .toolbar > * {
1695
+ margin: 0.35em;
16921696
}
16931697
body.chat .chat-message-popup > .toolbar > button {
16941698
flex: 1 1 auto;
16951699
}
16961700
/* The widget for loading more/older chat messages. */
16971701
--- src/default.css
+++ src/default.css
@@ -1636,11 +1636,11 @@
1636 body.chat.monospace-messages .message-widget-content,
1637 body.chat.monospace-messages textarea,
1638 body.chat.monospace-messages input[type=text]{
1639 font-family: monospace;
1640 }
1641 body.chat .message-widget-content > .markdown {
1642 margin: 0;
1643 padding: 0;
1644 }
1645 body.chat .message-widget-content > .markdown > *:first-child {
1646 margin-top: 0;
@@ -1679,18 +1679,22 @@
1679 }
1680 /* Full message timestamps. */
1681 body.chat .chat-message-popup > span { white-space: nowrap; }
1682 /* Container for the message deletion buttons. */
1683 body.chat .chat-message-popup > .toolbar {
1684 padding: 0.2em;
1685 margin: 0;
1686 border: 2px inset rgba(0,0,0,0.3);
1687 border-radius: 0.25em;
1688 display: flex;
1689 flex-direction: row;
1690 justify-content: stretch;
1691 flex-wrap: wrap;
 
 
 
 
1692 }
1693 body.chat .chat-message-popup > .toolbar > button {
1694 flex: 1 1 auto;
1695 }
1696 /* The widget for loading more/older chat messages. */
1697
--- src/default.css
+++ src/default.css
@@ -1636,11 +1636,11 @@
1636 body.chat.monospace-messages .message-widget-content,
1637 body.chat.monospace-messages textarea,
1638 body.chat.monospace-messages input[type=text]{
1639 font-family: monospace;
1640 }
1641 body.chat .message-widget-content > * {
1642 margin: 0;
1643 padding: 0;
1644 }
1645 body.chat .message-widget-content > .markdown > *:first-child {
1646 margin-top: 0;
@@ -1679,18 +1679,22 @@
1679 }
1680 /* Full message timestamps. */
1681 body.chat .chat-message-popup > span { white-space: nowrap; }
1682 /* Container for the message deletion buttons. */
1683 body.chat .chat-message-popup > .toolbar {
1684 padding: 0;
1685 margin: 0;
1686 border: 2px inset rgba(0,0,0,0.3);
1687 border-radius: 0.25em;
1688 display: flex;
1689 flex-direction: row;
1690 justify-content: stretch;
1691 flex-wrap: wrap;
1692 align-items: center;
1693 }
1694 body.chat .chat-message-popup > .toolbar > * {
1695 margin: 0.35em;
1696 }
1697 body.chat .chat-message-popup > .toolbar > button {
1698 flex: 1 1 auto;
1699 }
1700 /* The widget for loading more/older chat messages. */
1701

Keyboard Shortcuts

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