Fossil SCM

Add new page /chat-search, for searching chat history.

dan 2024-06-24 20:52 trunk
Commit 89f82e66035840016ce390e480fb89aa3e3c7a923233a50c75c3b28b50228f23
+179 -55
--- src/chat.c
+++ src/chat.c
@@ -270,10 +270,50 @@
270270
@ }, false);
271271
@ </script>
272272
builtin_request_js("fossil.page.chat.js");
273273
style_finish_page();
274274
}
275
+
276
+/*
277
+** WEBPAGE: chat-search hidden loadavg-exempt
278
+**
279
+** Webpage allowing users to search the archive of chat messages using fts5.
280
+*/
281
+void chat_search_webpage(void){
282
+ login_check_credentials();
283
+ if( !g.perm.Chat ){
284
+ login_needed(g.anon.Chat);
285
+ return;
286
+ }
287
+
288
+ style_set_current_feature("chat");
289
+ style_header("Chat Search");
290
+ @
291
+ @ <div id=results>
292
+ @ </div>
293
+ @ <div class='searchForm'>
294
+ @ <input id=textinput type="text" name="s" size="40">
295
+ @ <input id=searchbutton type="submit" value="Search">
296
+ @ </div>
297
+ builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch",
298
+ "pikchr", "confirmer", "copybutton",
299
+ NULL);
300
+ /* Always in-line the javascript for the chat page */
301
+ @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
302
+ /* We need an onload handler to ensure that window.fossil is
303
+ initialized before the chat init code runs. */
304
+ @ window.addEventListener('load', function(){
305
+ @ document.body.classList.add('chat');
306
+ @ /*^^^for skins which add their own BODY tag */;
307
+ // ajax_emit_js_preview_modes(0);
308
+ // chat_emit_alert_list();
309
+ @ }, false);
310
+ @ </script>
311
+
312
+ builtin_request_js("fossil.page.chatsearch.js");
313
+ style_finish_page();
314
+}
275315
276316
/* Definition of repository tables used by chat
277317
*/
278318
static const char zChatSchema1[] =
279319
@ CREATE TABLE repository.chat(
@@ -301,10 +341,26 @@
301341
if( !db_table_has_column("repository","chat","mdel") ){
302342
db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT");
303343
}
304344
db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT");
305345
}
346
+
347
+ if( !db_table_exists("repository", "chatfts1") ){
348
+ db_multi_exec(
349
+ "CREATE VIRTUAL TABLE chatfts1 USING fts5("
350
+ " xmsg, content=chat, content_rowid=msgid, tokenize=porter"
351
+ ");"
352
+ "CREATE TRIGGER chat_ai AFTER INSERT ON chat BEGIN "
353
+ " INSERT INTO chatfts1(rowid, xmsg) VALUES(new.msgid, new.xmsg);"
354
+ "END;"
355
+ "CREATE TRIGGER chat_ad AFTER DELETE ON chat BEGIN "
356
+ " INSERT INTO chatfts1(chatfts1, rowid, xmsg) "
357
+ " VALUES('delete', old.msgid, old.xmsg);"
358
+ "END;"
359
+ "INSERT INTO chatfts1(chatfts1) VALUES('rebuild');"
360
+ );
361
+ }
306362
}
307363
308364
/*
309365
** Delete old content from the chat table.
310366
*/
@@ -470,10 +526,78 @@
470526
zOut = chat_format_to_html(g.argv[i], 0);
471527
fossil_print("[%d]: %s\n", i, zOut);
472528
fossil_free(zOut);
473529
}
474530
}
531
+
532
+/*
533
+**
534
+*/
535
+static int chat_poll_rowstojson(
536
+ Stmt *p, /* Statement to read rows from */
537
+ const char *zChatUser, /* Current user */
538
+ int bRaw, /* True to return raw format xmsg */
539
+ Blob *pJson /* Append json array entries here */
540
+){
541
+ int cnt = 0;
542
+ while( db_step(p)==SQLITE_ROW ){
543
+ int isWiki = 0; /* True if chat message is x-fossil-wiki */
544
+ int id = db_column_int(p, 0);
545
+ const char *zDate = db_column_text(p, 1);
546
+ const char *zFrom = db_column_text(p, 2);
547
+ const char *zRawMsg = db_column_text(p, 3);
548
+ int nByte = db_column_int(p, 4);
549
+ const char *zFName = db_column_text(p, 5);
550
+ const char *zFMime = db_column_text(p, 6);
551
+ int iToDel = db_column_int(p, 7);
552
+ const char *zLMtime = db_column_text(p, 8);
553
+ char *zMsg;
554
+ if(cnt++){
555
+ blob_append(pJson, ",\n", 2);
556
+ }
557
+ blob_appendf(pJson, "{\"msgid\":%d,", id);
558
+ blob_appendf(pJson, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
559
+ if( zLMtime && zLMtime[0] ){
560
+ blob_appendf(pJson, "\"lmtime\":%!j,", zLMtime);
561
+ }
562
+ blob_append(pJson, "\"xfrom\":", -1);
563
+ if(zFrom){
564
+ blob_appendf(pJson, "%!j,", zFrom);
565
+ isWiki = fossil_strcmp(zFrom,zChatUser)==0;
566
+ }else{
567
+ /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
568
+ blob_appendf(pJson, "null,");
569
+ isWiki = 0;
570
+ }
571
+ blob_appendf(pJson, "\"uclr\":%!j,",
572
+ isWiki ? "transparent" : user_color(zFrom ? zFrom : "nobody"));
573
+
574
+ if(bRaw){
575
+ blob_appendf(pJson, "\"xmsg\":%!j,", zRawMsg);
576
+ }else{
577
+ zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "", isWiki);
578
+ blob_appendf(pJson, "\"xmsg\":%!j,", zMsg);
579
+ fossil_free(zMsg);
580
+ }
581
+
582
+ if( nByte==0 ){
583
+ blob_appendf(pJson, "\"fsize\":0");
584
+ }else{
585
+ blob_appendf(pJson, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
586
+ nByte, zFName, zFMime);
587
+ }
588
+
589
+ if( iToDel ){
590
+ blob_appendf(pJson, ",\"mdel\":%d}", iToDel);
591
+ }else{
592
+ blob_append(pJson, "}", 1);
593
+ }
594
+ }
595
+ db_reset(p);
596
+
597
+ return cnt;
598
+}
475599
476600
/*
477601
** WEBPAGE: chat-poll hidden loadavg-exempt
478602
**
479603
** The chat page generated by /chat using an XHR to this page to
@@ -569,11 +693,10 @@
569693
Blob json; /* The json to be constructed and returned */
570694
sqlite3_int64 dataVersion; /* Data version. Used for polling. */
571695
const int iDelay = 1000; /* Delay until next poll (milliseconds) */
572696
int nDelay; /* Maximum delay.*/
573697
const char *zChatUser; /* chat-timeline-user */
574
- int isWiki = 0; /* True if chat message is x-fossil-wiki */
575698
int msgid = atoi(PD("name","0"));
576699
const int msgBefore = atoi(PD("before","0"));
577700
int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
578701
const int bRaw = P("raw")!=0;
579702
@@ -623,64 +746,11 @@
623746
}
624747
db_prepare(&q1, "%s", blob_sql_text(&sql));
625748
blob_reset(&sql);
626749
blob_init(&json, "{\"msgs\":[\n", -1);
627750
while( nDelay>0 ){
628
- int cnt = 0;
629
- while( db_step(&q1)==SQLITE_ROW ){
630
- int id = db_column_int(&q1, 0);
631
- const char *zDate = db_column_text(&q1, 1);
632
- const char *zFrom = db_column_text(&q1, 2);
633
- const char *zRawMsg = db_column_text(&q1, 3);
634
- int nByte = db_column_int(&q1, 4);
635
- const char *zFName = db_column_text(&q1, 5);
636
- const char *zFMime = db_column_text(&q1, 6);
637
- int iToDel = db_column_int(&q1, 7);
638
- const char *zLMtime = db_column_text(&q1, 8);
639
- char *zMsg;
640
- if(cnt++){
641
- blob_append(&json, ",\n", 2);
642
- }
643
- blob_appendf(&json, "{\"msgid\":%d,", id);
644
- blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
645
- if( zLMtime && zLMtime[0] ){
646
- blob_appendf(&json, "\"lmtime\":%!j,", zLMtime);
647
- }
648
- blob_append(&json, "\"xfrom\":", -1);
649
- if(zFrom){
650
- blob_appendf(&json, "%!j,", zFrom);
651
- isWiki = fossil_strcmp(zFrom,zChatUser)==0;
652
- }else{
653
- /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
654
- blob_appendf(&json, "null,");
655
- isWiki = 0;
656
- }
657
- blob_appendf(&json, "\"uclr\":%!j,",
658
- isWiki ? "transparent" : user_color(zFrom ? zFrom : "nobody"));
659
-
660
- if(bRaw){
661
- blob_appendf(&json, "\"xmsg\":%!j,", zRawMsg);
662
- }else{
663
- zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "", isWiki);
664
- blob_appendf(&json, "\"xmsg\":%!j,", zMsg);
665
- fossil_free(zMsg);
666
- }
667
-
668
- if( nByte==0 ){
669
- blob_appendf(&json, "\"fsize\":0");
670
- }else{
671
- blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
672
- nByte, zFName, zFMime);
673
- }
674
-
675
- if( iToDel ){
676
- blob_appendf(&json, ",\"mdel\":%d}", iToDel);
677
- }else{
678
- blob_append(&json, "}", 1);
679
- }
680
- }
681
- db_reset(&q1);
751
+ int cnt = chat_poll_rowstojson(&q1, zChatUser, bRaw, &json);
682752
if( cnt || msgBefore>0 ){
683753
break;
684754
}
685755
sqlite3_sleep(iDelay); nDelay--;
686756
while( nDelay>0 ){
@@ -696,10 +766,64 @@
696766
blob_append(&json, "\n]}", 3);
697767
cgi_set_content(&json);
698768
return;
699769
}
700770
771
+
772
+/*
773
+** WEBPAGE: chat-query hidden loadavg-exempt
774
+*/
775
+void chat_query_webpage(void){
776
+ Blob json; /* The json to be constructed and returned */
777
+ int nLimit = atoi(PD("n","500"));
778
+ const char *zQuery = PD("q", "");
779
+ int iFirst = atoi(PD("i","0"));
780
+
781
+ Blob sql = empty_blob;
782
+ Stmt q1;
783
+ i64 iMin = 0;
784
+ i64 iMax = 0;
785
+
786
+ login_check_credentials();
787
+ if( !g.perm.Chat ) {
788
+ chat_emit_permissions_error(1);
789
+ return;
790
+ }
791
+ chat_create_tables();
792
+ cgi_set_content_type("application/json");
793
+
794
+ if( zQuery[0] ){
795
+ iMax = db_int64(0, "SELECT max(msgid) FROM chat");
796
+ iMin = db_int64(0, "SELECT min(msgid) FROM chat");
797
+ blob_append_sql(&sql,
798
+ "SELECT * FROM ("
799
+ "SELECT c.msgid, datetime(c.mtime), c.xfrom, "
800
+ " highlight(chatfts1, 0, '<span class=match>', '</span>'), "
801
+ " octet_length(c.file), c.fname, c.fmime, c.mdel, c.lmtime"
802
+ " FROM chatfts1(%Q) f, chat c WHERE f.rowid=c.msgid "
803
+ " ORDER BY f.rowid DESC LIMIT %d"
804
+ ") ORDER BY 1 ASC", zQuery, nLimit
805
+ );
806
+ }else{
807
+ blob_append_sql(&sql,
808
+ "SELECT msgid, datetime(mtime), xfrom, "
809
+ " xmsg, octet_length(file), fname, fmime, mdel, lmtime"
810
+ " FROM chat WHERE msgid>=%d LIMIT %d",
811
+ iFirst, nLimit
812
+ );
813
+ }
814
+
815
+ db_prepare(&q1, "%s", blob_sql_text(&sql));
816
+ blob_reset(&sql);
817
+ blob_init(&json, "{\"msgs\":[\n", -1);
818
+ chat_poll_rowstojson(&q1, "", 0, &json);
819
+ db_finalize(&q1);
820
+ blob_appendf(&json, "\n], \"first\":%lld, \"last\":%lld}", iMin, iMax);
821
+ cgi_set_content(&json);
822
+ return;
823
+}
824
+
701825
/*
702826
** WEBPAGE: chat-fetch-one hidden loadavg-exempt
703827
**
704828
** /chat-fetch-one/N
705829
**
706830
707831
ADDED src/fossil.page.chatsearch.js
--- src/chat.c
+++ src/chat.c
@@ -270,10 +270,50 @@
270 @ }, false);
271 @ </script>
272 builtin_request_js("fossil.page.chat.js");
273 style_finish_page();
274 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
276 /* Definition of repository tables used by chat
277 */
278 static const char zChatSchema1[] =
279 @ CREATE TABLE repository.chat(
@@ -301,10 +341,26 @@
301 if( !db_table_has_column("repository","chat","mdel") ){
302 db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT");
303 }
304 db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT");
305 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306 }
307
308 /*
309 ** Delete old content from the chat table.
310 */
@@ -470,10 +526,78 @@
470 zOut = chat_format_to_html(g.argv[i], 0);
471 fossil_print("[%d]: %s\n", i, zOut);
472 fossil_free(zOut);
473 }
474 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
476 /*
477 ** WEBPAGE: chat-poll hidden loadavg-exempt
478 **
479 ** The chat page generated by /chat using an XHR to this page to
@@ -569,11 +693,10 @@
569 Blob json; /* The json to be constructed and returned */
570 sqlite3_int64 dataVersion; /* Data version. Used for polling. */
571 const int iDelay = 1000; /* Delay until next poll (milliseconds) */
572 int nDelay; /* Maximum delay.*/
573 const char *zChatUser; /* chat-timeline-user */
574 int isWiki = 0; /* True if chat message is x-fossil-wiki */
575 int msgid = atoi(PD("name","0"));
576 const int msgBefore = atoi(PD("before","0"));
577 int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
578 const int bRaw = P("raw")!=0;
579
@@ -623,64 +746,11 @@
623 }
624 db_prepare(&q1, "%s", blob_sql_text(&sql));
625 blob_reset(&sql);
626 blob_init(&json, "{\"msgs\":[\n", -1);
627 while( nDelay>0 ){
628 int cnt = 0;
629 while( db_step(&q1)==SQLITE_ROW ){
630 int id = db_column_int(&q1, 0);
631 const char *zDate = db_column_text(&q1, 1);
632 const char *zFrom = db_column_text(&q1, 2);
633 const char *zRawMsg = db_column_text(&q1, 3);
634 int nByte = db_column_int(&q1, 4);
635 const char *zFName = db_column_text(&q1, 5);
636 const char *zFMime = db_column_text(&q1, 6);
637 int iToDel = db_column_int(&q1, 7);
638 const char *zLMtime = db_column_text(&q1, 8);
639 char *zMsg;
640 if(cnt++){
641 blob_append(&json, ",\n", 2);
642 }
643 blob_appendf(&json, "{\"msgid\":%d,", id);
644 blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
645 if( zLMtime && zLMtime[0] ){
646 blob_appendf(&json, "\"lmtime\":%!j,", zLMtime);
647 }
648 blob_append(&json, "\"xfrom\":", -1);
649 if(zFrom){
650 blob_appendf(&json, "%!j,", zFrom);
651 isWiki = fossil_strcmp(zFrom,zChatUser)==0;
652 }else{
653 /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
654 blob_appendf(&json, "null,");
655 isWiki = 0;
656 }
657 blob_appendf(&json, "\"uclr\":%!j,",
658 isWiki ? "transparent" : user_color(zFrom ? zFrom : "nobody"));
659
660 if(bRaw){
661 blob_appendf(&json, "\"xmsg\":%!j,", zRawMsg);
662 }else{
663 zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "", isWiki);
664 blob_appendf(&json, "\"xmsg\":%!j,", zMsg);
665 fossil_free(zMsg);
666 }
667
668 if( nByte==0 ){
669 blob_appendf(&json, "\"fsize\":0");
670 }else{
671 blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
672 nByte, zFName, zFMime);
673 }
674
675 if( iToDel ){
676 blob_appendf(&json, ",\"mdel\":%d}", iToDel);
677 }else{
678 blob_append(&json, "}", 1);
679 }
680 }
681 db_reset(&q1);
682 if( cnt || msgBefore>0 ){
683 break;
684 }
685 sqlite3_sleep(iDelay); nDelay--;
686 while( nDelay>0 ){
@@ -696,10 +766,64 @@
696 blob_append(&json, "\n]}", 3);
697 cgi_set_content(&json);
698 return;
699 }
700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701 /*
702 ** WEBPAGE: chat-fetch-one hidden loadavg-exempt
703 **
704 ** /chat-fetch-one/N
705 **
706
707 DDED src/fossil.page.chatsearch.js
--- src/chat.c
+++ src/chat.c
@@ -270,10 +270,50 @@
270 @ }, false);
271 @ </script>
272 builtin_request_js("fossil.page.chat.js");
273 style_finish_page();
274 }
275
276 /*
277 ** WEBPAGE: chat-search hidden loadavg-exempt
278 **
279 ** Webpage allowing users to search the archive of chat messages using fts5.
280 */
281 void chat_search_webpage(void){
282 login_check_credentials();
283 if( !g.perm.Chat ){
284 login_needed(g.anon.Chat);
285 return;
286 }
287
288 style_set_current_feature("chat");
289 style_header("Chat Search");
290 @
291 @ <div id=results>
292 @ </div>
293 @ <div class='searchForm'>
294 @ <input id=textinput type="text" name="s" size="40">
295 @ <input id=searchbutton type="submit" value="Search">
296 @ </div>
297 builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch",
298 "pikchr", "confirmer", "copybutton",
299 NULL);
300 /* Always in-line the javascript for the chat page */
301 @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
302 /* We need an onload handler to ensure that window.fossil is
303 initialized before the chat init code runs. */
304 @ window.addEventListener('load', function(){
305 @ document.body.classList.add('chat');
306 @ /*^^^for skins which add their own BODY tag */;
307 // ajax_emit_js_preview_modes(0);
308 // chat_emit_alert_list();
309 @ }, false);
310 @ </script>
311
312 builtin_request_js("fossil.page.chatsearch.js");
313 style_finish_page();
314 }
315
316 /* Definition of repository tables used by chat
317 */
318 static const char zChatSchema1[] =
319 @ CREATE TABLE repository.chat(
@@ -301,10 +341,26 @@
341 if( !db_table_has_column("repository","chat","mdel") ){
342 db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT");
343 }
344 db_multi_exec("ALTER TABLE chat ADD COLUMN lmtime TEXT");
345 }
346
347 if( !db_table_exists("repository", "chatfts1") ){
348 db_multi_exec(
349 "CREATE VIRTUAL TABLE chatfts1 USING fts5("
350 " xmsg, content=chat, content_rowid=msgid, tokenize=porter"
351 ");"
352 "CREATE TRIGGER chat_ai AFTER INSERT ON chat BEGIN "
353 " INSERT INTO chatfts1(rowid, xmsg) VALUES(new.msgid, new.xmsg);"
354 "END;"
355 "CREATE TRIGGER chat_ad AFTER DELETE ON chat BEGIN "
356 " INSERT INTO chatfts1(chatfts1, rowid, xmsg) "
357 " VALUES('delete', old.msgid, old.xmsg);"
358 "END;"
359 "INSERT INTO chatfts1(chatfts1) VALUES('rebuild');"
360 );
361 }
362 }
363
364 /*
365 ** Delete old content from the chat table.
366 */
@@ -470,10 +526,78 @@
526 zOut = chat_format_to_html(g.argv[i], 0);
527 fossil_print("[%d]: %s\n", i, zOut);
528 fossil_free(zOut);
529 }
530 }
531
532 /*
533 **
534 */
535 static int chat_poll_rowstojson(
536 Stmt *p, /* Statement to read rows from */
537 const char *zChatUser, /* Current user */
538 int bRaw, /* True to return raw format xmsg */
539 Blob *pJson /* Append json array entries here */
540 ){
541 int cnt = 0;
542 while( db_step(p)==SQLITE_ROW ){
543 int isWiki = 0; /* True if chat message is x-fossil-wiki */
544 int id = db_column_int(p, 0);
545 const char *zDate = db_column_text(p, 1);
546 const char *zFrom = db_column_text(p, 2);
547 const char *zRawMsg = db_column_text(p, 3);
548 int nByte = db_column_int(p, 4);
549 const char *zFName = db_column_text(p, 5);
550 const char *zFMime = db_column_text(p, 6);
551 int iToDel = db_column_int(p, 7);
552 const char *zLMtime = db_column_text(p, 8);
553 char *zMsg;
554 if(cnt++){
555 blob_append(pJson, ",\n", 2);
556 }
557 blob_appendf(pJson, "{\"msgid\":%d,", id);
558 blob_appendf(pJson, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
559 if( zLMtime && zLMtime[0] ){
560 blob_appendf(pJson, "\"lmtime\":%!j,", zLMtime);
561 }
562 blob_append(pJson, "\"xfrom\":", -1);
563 if(zFrom){
564 blob_appendf(pJson, "%!j,", zFrom);
565 isWiki = fossil_strcmp(zFrom,zChatUser)==0;
566 }else{
567 /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
568 blob_appendf(pJson, "null,");
569 isWiki = 0;
570 }
571 blob_appendf(pJson, "\"uclr\":%!j,",
572 isWiki ? "transparent" : user_color(zFrom ? zFrom : "nobody"));
573
574 if(bRaw){
575 blob_appendf(pJson, "\"xmsg\":%!j,", zRawMsg);
576 }else{
577 zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "", isWiki);
578 blob_appendf(pJson, "\"xmsg\":%!j,", zMsg);
579 fossil_free(zMsg);
580 }
581
582 if( nByte==0 ){
583 blob_appendf(pJson, "\"fsize\":0");
584 }else{
585 blob_appendf(pJson, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
586 nByte, zFName, zFMime);
587 }
588
589 if( iToDel ){
590 blob_appendf(pJson, ",\"mdel\":%d}", iToDel);
591 }else{
592 blob_append(pJson, "}", 1);
593 }
594 }
595 db_reset(p);
596
597 return cnt;
598 }
599
600 /*
601 ** WEBPAGE: chat-poll hidden loadavg-exempt
602 **
603 ** The chat page generated by /chat using an XHR to this page to
@@ -569,11 +693,10 @@
693 Blob json; /* The json to be constructed and returned */
694 sqlite3_int64 dataVersion; /* Data version. Used for polling. */
695 const int iDelay = 1000; /* Delay until next poll (milliseconds) */
696 int nDelay; /* Maximum delay.*/
697 const char *zChatUser; /* chat-timeline-user */
 
698 int msgid = atoi(PD("name","0"));
699 const int msgBefore = atoi(PD("before","0"));
700 int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
701 const int bRaw = P("raw")!=0;
702
@@ -623,64 +746,11 @@
746 }
747 db_prepare(&q1, "%s", blob_sql_text(&sql));
748 blob_reset(&sql);
749 blob_init(&json, "{\"msgs\":[\n", -1);
750 while( nDelay>0 ){
751 int cnt = chat_poll_rowstojson(&q1, zChatUser, bRaw, &json);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
752 if( cnt || msgBefore>0 ){
753 break;
754 }
755 sqlite3_sleep(iDelay); nDelay--;
756 while( nDelay>0 ){
@@ -696,10 +766,64 @@
766 blob_append(&json, "\n]}", 3);
767 cgi_set_content(&json);
768 return;
769 }
770
771
772 /*
773 ** WEBPAGE: chat-query hidden loadavg-exempt
774 */
775 void chat_query_webpage(void){
776 Blob json; /* The json to be constructed and returned */
777 int nLimit = atoi(PD("n","500"));
778 const char *zQuery = PD("q", "");
779 int iFirst = atoi(PD("i","0"));
780
781 Blob sql = empty_blob;
782 Stmt q1;
783 i64 iMin = 0;
784 i64 iMax = 0;
785
786 login_check_credentials();
787 if( !g.perm.Chat ) {
788 chat_emit_permissions_error(1);
789 return;
790 }
791 chat_create_tables();
792 cgi_set_content_type("application/json");
793
794 if( zQuery[0] ){
795 iMax = db_int64(0, "SELECT max(msgid) FROM chat");
796 iMin = db_int64(0, "SELECT min(msgid) FROM chat");
797 blob_append_sql(&sql,
798 "SELECT * FROM ("
799 "SELECT c.msgid, datetime(c.mtime), c.xfrom, "
800 " highlight(chatfts1, 0, '<span class=match>', '</span>'), "
801 " octet_length(c.file), c.fname, c.fmime, c.mdel, c.lmtime"
802 " FROM chatfts1(%Q) f, chat c WHERE f.rowid=c.msgid "
803 " ORDER BY f.rowid DESC LIMIT %d"
804 ") ORDER BY 1 ASC", zQuery, nLimit
805 );
806 }else{
807 blob_append_sql(&sql,
808 "SELECT msgid, datetime(mtime), xfrom, "
809 " xmsg, octet_length(file), fname, fmime, mdel, lmtime"
810 " FROM chat WHERE msgid>=%d LIMIT %d",
811 iFirst, nLimit
812 );
813 }
814
815 db_prepare(&q1, "%s", blob_sql_text(&sql));
816 blob_reset(&sql);
817 blob_init(&json, "{\"msgs\":[\n", -1);
818 chat_poll_rowstojson(&q1, "", 0, &json);
819 db_finalize(&q1);
820 blob_appendf(&json, "\n], \"first\":%lld, \"last\":%lld}", iMin, iMax);
821 cgi_set_content(&json);
822 return;
823 }
824
825 /*
826 ** WEBPAGE: chat-fetch-one hidden loadavg-exempt
827 **
828 ** /chat-fetch-one/N
829 **
830
831 DDED src/fossil.page.chatsearch.js
--- a/src/fossil.page.chatsearch.js
+++ b/src/fossil.page.chatsearch.js
@@ -0,0 +1,299 @@
1
+/*
2
+** This file contains the client-side implementation of fossil's
3
+** /chat-search application.
4
+*/
5
+window.fossil.onPageLoad(function(){
6
+
7
+ const F = window.fossil, D = F.dom;
8
+ const E1 = function(selector){
9
+ const e = document.querySelector(selector);
10
+ if(!e) throw new Error("missing required DOM element: "+selector);
11
+ return e;
12
+ };
13
+
14
+/************************************************************************/
15
+/************************************************************************/
16
+/************************************************************************/
17
+
18
+ /**
19
+ Custom widget type for rendering messages (one message per
20
+ instance). These are modelled after FIELDSET elements but we
21
+ don't use FIELDSET because of cross-browser inconsistencies in
22
+ features of the FIELDSET/LEGEND combination, e.g. inability to
23
+ align legends via CSS in Firefox and clicking-related
24
+ deficiencies in Safari.
25
+ */
26
+ var MessageWidget = (function(){
27
+ /**
28
+ Constructor. If passed an argument, it is passed to
29
+ this.setMessage() after initialization.
30
+ */
31
+ const cf = function(){
32
+ this.e = {
33
+ body: D.addClass(D.div(), 'message-widget'),
34
+ tab: D.addClass(D.div(), 'message-widget-tab'),
35
+ content: D.addClass(D.div(), 'message-widget-content')
36
+ };
37
+ D.append(this.e.body, this.e.tab, this.e.content);
38
+ this.e.tab.setAttribute('role', 'button');
39
+ if(arguments.length){
40
+ this.setMessage(arguments[0]);
41
+ }
42
+ };
43
+
44
+ /**
45
+ Returns true if this page believes it can embed a view of the
46
+ file wrapped by the given message object, else returns false.
47
+ */
48
+ const canEmbedFile = function f(msg){
49
+ if(!f.$rx){
50
+ f.$rx = /\.((html?)|(txt)|(md)|(wiki)|(pikchr))$/i;
51
+ f.$specificTypes = [
52
+ 'text/plain',
53
+ 'text/html',
54
+ 'text/x-markdown',
55
+ /* Firefox sends text/markdown when uploading .md files */
56
+ 'text/markdown',
57
+ 'text/x-pikchr',
58
+ 'text/x-fossil-wiki'
59
+ // add more as we discover which ones Firefox won't
60
+ // force the user to try to download.
61
+ ];
62
+ }
63
+ if(msg.fmime){
64
+ if(msg.fmime.startsWith("image/")
65
+ || f.$specificTypes.indexOf(msg.fmime)>=0){
66
+ return true;
67
+ }
68
+ }
69
+ return (msg.fname && f.$rx.test(msg.fname));
70
+ };
71
+
72
+ /**
73
+ Returns true if the given message object "should"
74
+ be embedded in fossil-rendered form instead of
75
+ raw content form. This is only intended to be passed
76
+ message objects for which canEmbedFile() returns true.
77
+ */
78
+ const shouldWikiRenderEmbed = function f(msg){
79
+ if(!f.$rx){
80
+ f.$rx = /\.((md)|(wiki)|(pikchr))$/i;
81
+ f.$specificTypes = [
82
+ 'text/x-markdown',
83
+ 'text/markdown' /* Firefox-uploaded md files */,
84
+ 'text/x-pikchr',
85
+ 'text/x-fossil-wiki'
86
+ // add more as we discover which ones Firefox won't
87
+ // force the user to try to download.
88
+ ];
89
+ }
90
+ if(msg.fmime){
91
+ if(f.$specificTypes.indexOf(msg.fmime)>=0) return true;
92
+ }
93
+ return msg.fname && f.$rx.test(msg.fname);
94
+ };
95
+
96
+ const adjustIFrameSize = function(msgObj){
97
+ const iframe = msgObj.e.iframe;
98
+ const body = iframe.contentWindow.document.querySelector('body');
99
+ if(body && !body.style.fontSize){
100
+ /** _Attempt_ to force the iframe to inherit the message's text size
101
+ if the body has no explicit size set. On desktop systems
102
+ the size is apparently being inherited in that case, but on mobile
103
+ not. */
104
+ body.style.fontSize = window.getComputedStyle(msgObj.e.content);
105
+ }
106
+ if('' === iframe.style.maxHeight){
107
+ /* Resize iframe height to fit the content. Workaround: if we
108
+ adjust the iframe height while it's hidden then its height
109
+ is 0, so we must briefly unhide it. */
110
+ const isHidden = iframe.classList.contains('hidden');
111
+ if(isHidden) D.removeClass(iframe, 'hidden');
112
+ iframe.style.maxHeight = iframe.style.height
113
+ = iframe.contentWindow.document.documentElement.scrollHeight + 'px';
114
+ if(isHidden) D.addClass(iframe, 'hidden');
115
+ }
116
+ };
117
+
118
+ cf.prototype = {
119
+ scrollIntoView: function(){
120
+ this.e.content.scrollIntoView();
121
+ },
122
+ setMessage: function(m){
123
+ const ds = this.e.body.dataset;
124
+ ds.timestamp = m.mtime;
125
+ ds.lmtime = m.lmtime;
126
+ ds.msgid = m.msgid;
127
+ ds.xfrom = m.xfrom || '';
128
+
129
+ if(m.uclr){
130
+ this.e.content.style.backgroundColor = m.uclr;
131
+ this.e.tab.style.backgroundColor = m.uclr;
132
+ }
133
+ const d = new Date(m.mtime);
134
+ D.clearElement(this.e.tab);
135
+ var contentTarget = this.e.content;
136
+ var eXFrom /* element holding xfrom name */;
137
+ var eXFrom = D.append(D.addClass(D.span(), 'xfrom'), m.xfrom);
138
+ const wrapper = D.append(
139
+ D.span(), eXFrom,
140
+ D.text(" #",(m.msgid||'???'),' @ ',d.toLocaleString()));
141
+ D.append(this.e.tab, wrapper);
142
+
143
+ if( m.xfrom && m.fsize>0 ){
144
+ if( m.fmime
145
+ && m.fmime.startsWith("image/")
146
+ /* && Chat.settings.getBool('images-inline',true) */
147
+ ){
148
+ const extension = m.fname.split('.').pop();
149
+ contentTarget.appendChild(D.img("chat-download/" + m.msgid +(
150
+ extension ? ('.'+extension) : ''/*So that IMG tag mimetype guessing works*/
151
+ )));
152
+ ds.hasImage = 1;
153
+ }else{
154
+ // Add a download link.
155
+ const downloadUri = window.fossil.rootPath+
156
+ 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname);
157
+ const w = D.addClass(D.div(), 'attachment-link');
158
+ const a = D.a(downloadUri,
159
+ // ^^^ add m.fname to URL to cause downloaded file to have that name.
160
+ "(" + m.fname + " " + m.fsize + " bytes)"
161
+ )
162
+ D.attr(a,'target','_blank');
163
+ D.append(w, a);
164
+ if(canEmbedFile(m)){
165
+ /* Add an option to embed HTML attachments in an iframe. The primary
166
+ use case is attached diffs. */
167
+ const shouldWikiRender = shouldWikiRenderEmbed(m);
168
+ const downloadArgs = shouldWikiRender ? '?render' : '';
169
+ D.addClass(contentTarget, 'wide');
170
+ const embedTarget = this.e.content;
171
+ const self = this;
172
+ const btnEmbed = D.attr(D.checkbox("1", false), 'id',
173
+ 'embed-'+ds.msgid);
174
+ const btnLabel = D.label(btnEmbed, shouldWikiRender
175
+ ? "Embed (fossil-rendered)" : "Embed");
176
+ /* Maintenance reminder: do not disable the toggle
177
+ button while the content is loading because that will
178
+ cause it to get stuck in disabled mode if the browser
179
+ decides that loading the content should prompt the
180
+ user to download it, rather than embed it in the
181
+ iframe. */
182
+ btnEmbed.addEventListener('change',function(){
183
+ if(self.e.iframe){
184
+ if(btnEmbed.checked){
185
+ D.removeClass(self.e.iframe, 'hidden');
186
+ if(self.e.$iframeLoaded) adjustIFrameSize(self);
187
+ }
188
+ else D.addClass(self.e.iframe, 'hidden');
189
+ return;
190
+ }
191
+ const iframe = self.e.iframe = document.createElement('iframe');
192
+ D.append(embedTarget, iframe);
193
+ iframe.addEventListener('load', function(){
194
+ self.e.$iframeLoaded = true;
195
+ adjustIFrameSize(self);
196
+ });
197
+ iframe.setAttribute('src', downloadUri + downloadArgs);
198
+ });
199
+ D.append(w, btnEmbed, btnLabel);
200
+ }
201
+ contentTarget.appendChild(w);
202
+ }
203
+ }
204
+ if(m.xmsg){
205
+ if(m.fsize>0){
206
+ /* We have file/image content, so need another element for
207
+ the message text. */
208
+ contentTarget = D.div();
209
+ D.append(this.e.content, contentTarget);
210
+ }
211
+ D.addClass(contentTarget, 'content-target'
212
+ /*target element for the 'toggle text mode' feature*/);
213
+ // The m.xmsg text comes from the same server as this script and
214
+ // is guaranteed by that server to be "safe" HTML - safe in the
215
+ // sense that it is not possible for a malefactor to inject HTML
216
+ // or javascript or CSS. The m.xmsg content might contain
217
+ // hyperlinks, but otherwise it will be markup-free. See the
218
+ // chat_format_to_html() routine in the server for details.
219
+ //
220
+ // Hence, even though innerHTML is normally frowned upon, it is
221
+ // perfectly safe to use in this context.
222
+ if(m.xmsg && 'string' !== typeof m.xmsg){
223
+ // Used by Chat.reportErrorAsMessage()
224
+ D.append(contentTarget, m.xmsg);
225
+ }else{
226
+ contentTarget.innerHTML = m.xmsg;
227
+ // contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
228
+ if(F.pikchr){
229
+ F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
230
+ }
231
+ }
232
+ }
233
+ //console.debug("tab",this.e.tab);
234
+ //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild);
235
+ // this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
236
+ /*if(eXFrom){
237
+ eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
238
+ }*/
239
+ return this;
240
+ }
241
+ };
242
+ return cf;
243
+ })()/*MessageWidget*/;
244
+
245
+/************************************************************************/
246
+/************************************************************************/
247
+/************************************************************************/
248
+
249
+ var MessageSpacer = (function(){
250
+ const nMsgContext = 5;
251
+ const zUpArrow = '\u25B2';
252
+ const zDownArrow = '\u25BC';
253
+
254
+ const cf = function(o){
255
+
256
+ /* iFirstInTable: es.inde** msgid of first
257
+ ** msgid of first es.inde ** iLastInTable:
258
+ ** msgid of last row in chatfts table.
259
+ **
260
+ ** iPrevId:
261
+ ** msgid of message immediately above this spacer. Or 0 if this
262
+ ** spacer is above all results.
263
+ **
264
+ ** iNextId:
265
+ ** msgid of message immediately below this spacer. Or 0 if this
266
+ ** spacer is below all results.
267
+ **
268
+ ** bIgnoreClick:
269
+ ** ignore any clicks if this is true. This is used to ensure there
270
+ ** is only ever one request belonging to this widget outstanding
271
+
272
+ }else{
273
+ contentTarget.innerHTML = m.xmsg;
274
+ // contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
275
+ if(F.pikchr){
276
+ F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
277
+ }
278
+ }
279
+ }
280
+ //console.debug("tab",this.e.tab);
281
+ //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild);
282
+ // this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
283
+ /*if(eXFrom){
284
+ eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
285
+ }*/
286
+ return this;
287
+ }
288
+ };
289
+ return cf;
290
+ })()/*MessageWidget*/;
291
+
292
+/************************************************************************/
293
+/************************************************************************/
294
+/******************************************this.e.up.style.float = 'left'ossil, D = F.dom;
295
+ /*
296
+** This file contains the client-side implementation of fossil's
297
+** /chat
298
+;
299
+ cons
--- a/src/fossil.page.chatsearch.js
+++ b/src/fossil.page.chatsearch.js
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/fossil.page.chatsearch.js
+++ b/src/fossil.page.chatsearch.js
@@ -0,0 +1,299 @@
1 /*
2 ** This file contains the client-side implementation of fossil's
3 ** /chat-search application.
4 */
5 window.fossil.onPageLoad(function(){
6
7 const F = window.fossil, D = F.dom;
8 const E1 = function(selector){
9 const e = document.querySelector(selector);
10 if(!e) throw new Error("missing required DOM element: "+selector);
11 return e;
12 };
13
14 /************************************************************************/
15 /************************************************************************/
16 /************************************************************************/
17
18 /**
19 Custom widget type for rendering messages (one message per
20 instance). These are modelled after FIELDSET elements but we
21 don't use FIELDSET because of cross-browser inconsistencies in
22 features of the FIELDSET/LEGEND combination, e.g. inability to
23 align legends via CSS in Firefox and clicking-related
24 deficiencies in Safari.
25 */
26 var MessageWidget = (function(){
27 /**
28 Constructor. If passed an argument, it is passed to
29 this.setMessage() after initialization.
30 */
31 const cf = function(){
32 this.e = {
33 body: D.addClass(D.div(), 'message-widget'),
34 tab: D.addClass(D.div(), 'message-widget-tab'),
35 content: D.addClass(D.div(), 'message-widget-content')
36 };
37 D.append(this.e.body, this.e.tab, this.e.content);
38 this.e.tab.setAttribute('role', 'button');
39 if(arguments.length){
40 this.setMessage(arguments[0]);
41 }
42 };
43
44 /**
45 Returns true if this page believes it can embed a view of the
46 file wrapped by the given message object, else returns false.
47 */
48 const canEmbedFile = function f(msg){
49 if(!f.$rx){
50 f.$rx = /\.((html?)|(txt)|(md)|(wiki)|(pikchr))$/i;
51 f.$specificTypes = [
52 'text/plain',
53 'text/html',
54 'text/x-markdown',
55 /* Firefox sends text/markdown when uploading .md files */
56 'text/markdown',
57 'text/x-pikchr',
58 'text/x-fossil-wiki'
59 // add more as we discover which ones Firefox won't
60 // force the user to try to download.
61 ];
62 }
63 if(msg.fmime){
64 if(msg.fmime.startsWith("image/")
65 || f.$specificTypes.indexOf(msg.fmime)>=0){
66 return true;
67 }
68 }
69 return (msg.fname && f.$rx.test(msg.fname));
70 };
71
72 /**
73 Returns true if the given message object "should"
74 be embedded in fossil-rendered form instead of
75 raw content form. This is only intended to be passed
76 message objects for which canEmbedFile() returns true.
77 */
78 const shouldWikiRenderEmbed = function f(msg){
79 if(!f.$rx){
80 f.$rx = /\.((md)|(wiki)|(pikchr))$/i;
81 f.$specificTypes = [
82 'text/x-markdown',
83 'text/markdown' /* Firefox-uploaded md files */,
84 'text/x-pikchr',
85 'text/x-fossil-wiki'
86 // add more as we discover which ones Firefox won't
87 // force the user to try to download.
88 ];
89 }
90 if(msg.fmime){
91 if(f.$specificTypes.indexOf(msg.fmime)>=0) return true;
92 }
93 return msg.fname && f.$rx.test(msg.fname);
94 };
95
96 const adjustIFrameSize = function(msgObj){
97 const iframe = msgObj.e.iframe;
98 const body = iframe.contentWindow.document.querySelector('body');
99 if(body && !body.style.fontSize){
100 /** _Attempt_ to force the iframe to inherit the message's text size
101 if the body has no explicit size set. On desktop systems
102 the size is apparently being inherited in that case, but on mobile
103 not. */
104 body.style.fontSize = window.getComputedStyle(msgObj.e.content);
105 }
106 if('' === iframe.style.maxHeight){
107 /* Resize iframe height to fit the content. Workaround: if we
108 adjust the iframe height while it's hidden then its height
109 is 0, so we must briefly unhide it. */
110 const isHidden = iframe.classList.contains('hidden');
111 if(isHidden) D.removeClass(iframe, 'hidden');
112 iframe.style.maxHeight = iframe.style.height
113 = iframe.contentWindow.document.documentElement.scrollHeight + 'px';
114 if(isHidden) D.addClass(iframe, 'hidden');
115 }
116 };
117
118 cf.prototype = {
119 scrollIntoView: function(){
120 this.e.content.scrollIntoView();
121 },
122 setMessage: function(m){
123 const ds = this.e.body.dataset;
124 ds.timestamp = m.mtime;
125 ds.lmtime = m.lmtime;
126 ds.msgid = m.msgid;
127 ds.xfrom = m.xfrom || '';
128
129 if(m.uclr){
130 this.e.content.style.backgroundColor = m.uclr;
131 this.e.tab.style.backgroundColor = m.uclr;
132 }
133 const d = new Date(m.mtime);
134 D.clearElement(this.e.tab);
135 var contentTarget = this.e.content;
136 var eXFrom /* element holding xfrom name */;
137 var eXFrom = D.append(D.addClass(D.span(), 'xfrom'), m.xfrom);
138 const wrapper = D.append(
139 D.span(), eXFrom,
140 D.text(" #",(m.msgid||'???'),' @ ',d.toLocaleString()));
141 D.append(this.e.tab, wrapper);
142
143 if( m.xfrom && m.fsize>0 ){
144 if( m.fmime
145 && m.fmime.startsWith("image/")
146 /* && Chat.settings.getBool('images-inline',true) */
147 ){
148 const extension = m.fname.split('.').pop();
149 contentTarget.appendChild(D.img("chat-download/" + m.msgid +(
150 extension ? ('.'+extension) : ''/*So that IMG tag mimetype guessing works*/
151 )));
152 ds.hasImage = 1;
153 }else{
154 // Add a download link.
155 const downloadUri = window.fossil.rootPath+
156 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname);
157 const w = D.addClass(D.div(), 'attachment-link');
158 const a = D.a(downloadUri,
159 // ^^^ add m.fname to URL to cause downloaded file to have that name.
160 "(" + m.fname + " " + m.fsize + " bytes)"
161 )
162 D.attr(a,'target','_blank');
163 D.append(w, a);
164 if(canEmbedFile(m)){
165 /* Add an option to embed HTML attachments in an iframe. The primary
166 use case is attached diffs. */
167 const shouldWikiRender = shouldWikiRenderEmbed(m);
168 const downloadArgs = shouldWikiRender ? '?render' : '';
169 D.addClass(contentTarget, 'wide');
170 const embedTarget = this.e.content;
171 const self = this;
172 const btnEmbed = D.attr(D.checkbox("1", false), 'id',
173 'embed-'+ds.msgid);
174 const btnLabel = D.label(btnEmbed, shouldWikiRender
175 ? "Embed (fossil-rendered)" : "Embed");
176 /* Maintenance reminder: do not disable the toggle
177 button while the content is loading because that will
178 cause it to get stuck in disabled mode if the browser
179 decides that loading the content should prompt the
180 user to download it, rather than embed it in the
181 iframe. */
182 btnEmbed.addEventListener('change',function(){
183 if(self.e.iframe){
184 if(btnEmbed.checked){
185 D.removeClass(self.e.iframe, 'hidden');
186 if(self.e.$iframeLoaded) adjustIFrameSize(self);
187 }
188 else D.addClass(self.e.iframe, 'hidden');
189 return;
190 }
191 const iframe = self.e.iframe = document.createElement('iframe');
192 D.append(embedTarget, iframe);
193 iframe.addEventListener('load', function(){
194 self.e.$iframeLoaded = true;
195 adjustIFrameSize(self);
196 });
197 iframe.setAttribute('src', downloadUri + downloadArgs);
198 });
199 D.append(w, btnEmbed, btnLabel);
200 }
201 contentTarget.appendChild(w);
202 }
203 }
204 if(m.xmsg){
205 if(m.fsize>0){
206 /* We have file/image content, so need another element for
207 the message text. */
208 contentTarget = D.div();
209 D.append(this.e.content, contentTarget);
210 }
211 D.addClass(contentTarget, 'content-target'
212 /*target element for the 'toggle text mode' feature*/);
213 // The m.xmsg text comes from the same server as this script and
214 // is guaranteed by that server to be "safe" HTML - safe in the
215 // sense that it is not possible for a malefactor to inject HTML
216 // or javascript or CSS. The m.xmsg content might contain
217 // hyperlinks, but otherwise it will be markup-free. See the
218 // chat_format_to_html() routine in the server for details.
219 //
220 // Hence, even though innerHTML is normally frowned upon, it is
221 // perfectly safe to use in this context.
222 if(m.xmsg && 'string' !== typeof m.xmsg){
223 // Used by Chat.reportErrorAsMessage()
224 D.append(contentTarget, m.xmsg);
225 }else{
226 contentTarget.innerHTML = m.xmsg;
227 // contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
228 if(F.pikchr){
229 F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
230 }
231 }
232 }
233 //console.debug("tab",this.e.tab);
234 //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild);
235 // this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
236 /*if(eXFrom){
237 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
238 }*/
239 return this;
240 }
241 };
242 return cf;
243 })()/*MessageWidget*/;
244
245 /************************************************************************/
246 /************************************************************************/
247 /************************************************************************/
248
249 var MessageSpacer = (function(){
250 const nMsgContext = 5;
251 const zUpArrow = '\u25B2';
252 const zDownArrow = '\u25BC';
253
254 const cf = function(o){
255
256 /* iFirstInTable: es.inde** msgid of first
257 ** msgid of first es.inde ** iLastInTable:
258 ** msgid of last row in chatfts table.
259 **
260 ** iPrevId:
261 ** msgid of message immediately above this spacer. Or 0 if this
262 ** spacer is above all results.
263 **
264 ** iNextId:
265 ** msgid of message immediately below this spacer. Or 0 if this
266 ** spacer is below all results.
267 **
268 ** bIgnoreClick:
269 ** ignore any clicks if this is true. This is used to ensure there
270 ** is only ever one request belonging to this widget outstanding
271
272 }else{
273 contentTarget.innerHTML = m.xmsg;
274 // contentTarget.querySelectorAll('a').forEach(addAnchorTargetBlank);
275 if(F.pikchr){
276 F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
277 }
278 }
279 }
280 //console.debug("tab",this.e.tab);
281 //console.debug("this.e.tab.firstElementChild",this.e.tab.firstElementChild);
282 // this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
283 /*if(eXFrom){
284 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
285 }*/
286 return this;
287 }
288 };
289 return cf;
290 })()/*MessageWidget*/;
291
292 /************************************************************************/
293 /************************************************************************/
294 /******************************************this.e.up.style.float = 'left'ossil, D = F.dom;
295 /*
296 ** This file contains the client-side implementation of fossil's
297 ** /chat
298 ;
299 cons
--- src/main.mk
+++ src/main.mk
@@ -231,10 +231,11 @@
231231
$(SRCDIR)/fossil.dom.js \
232232
$(SRCDIR)/fossil.fetch.js \
233233
$(SRCDIR)/fossil.numbered-lines.js \
234234
$(SRCDIR)/fossil.page.brlist.js \
235235
$(SRCDIR)/fossil.page.chat.js \
236
+ $(SRCDIR)/fossil.page.chatsearch.js \
236237
$(SRCDIR)/fossil.page.fileedit.js \
237238
$(SRCDIR)/fossil.page.forumpost.js \
238239
$(SRCDIR)/fossil.page.pikchrshow.js \
239240
$(SRCDIR)/fossil.page.pikchrshowasm.js \
240241
$(SRCDIR)/fossil.page.whistory.js \
@@ -269,10 +270,11 @@
269270
$(SRCDIR)/sounds/d.wav \
270271
$(SRCDIR)/sounds/e.wav \
271272
$(SRCDIR)/sounds/f.wav \
272273
$(SRCDIR)/style.admin_log.css \
273274
$(SRCDIR)/style.chat.css \
275
+ $(SRCDIR)/style.chat-search.css \
274276
$(SRCDIR)/style.fileedit.css \
275277
$(SRCDIR)/style.pikchrshow.css \
276278
$(SRCDIR)/style.wikiedit.css \
277279
$(SRCDIR)/tree.js \
278280
$(SRCDIR)/useredit.js \
279281
280282
ADDED src/style.chat-search.css
--- src/main.mk
+++ src/main.mk
@@ -231,10 +231,11 @@
231 $(SRCDIR)/fossil.dom.js \
232 $(SRCDIR)/fossil.fetch.js \
233 $(SRCDIR)/fossil.numbered-lines.js \
234 $(SRCDIR)/fossil.page.brlist.js \
235 $(SRCDIR)/fossil.page.chat.js \
 
236 $(SRCDIR)/fossil.page.fileedit.js \
237 $(SRCDIR)/fossil.page.forumpost.js \
238 $(SRCDIR)/fossil.page.pikchrshow.js \
239 $(SRCDIR)/fossil.page.pikchrshowasm.js \
240 $(SRCDIR)/fossil.page.whistory.js \
@@ -269,10 +270,11 @@
269 $(SRCDIR)/sounds/d.wav \
270 $(SRCDIR)/sounds/e.wav \
271 $(SRCDIR)/sounds/f.wav \
272 $(SRCDIR)/style.admin_log.css \
273 $(SRCDIR)/style.chat.css \
 
274 $(SRCDIR)/style.fileedit.css \
275 $(SRCDIR)/style.pikchrshow.css \
276 $(SRCDIR)/style.wikiedit.css \
277 $(SRCDIR)/tree.js \
278 $(SRCDIR)/useredit.js \
279
280 DDED src/style.chat-search.css
--- src/main.mk
+++ src/main.mk
@@ -231,10 +231,11 @@
231 $(SRCDIR)/fossil.dom.js \
232 $(SRCDIR)/fossil.fetch.js \
233 $(SRCDIR)/fossil.numbered-lines.js \
234 $(SRCDIR)/fossil.page.brlist.js \
235 $(SRCDIR)/fossil.page.chat.js \
236 $(SRCDIR)/fossil.page.chatsearch.js \
237 $(SRCDIR)/fossil.page.fileedit.js \
238 $(SRCDIR)/fossil.page.forumpost.js \
239 $(SRCDIR)/fossil.page.pikchrshow.js \
240 $(SRCDIR)/fossil.page.pikchrshowasm.js \
241 $(SRCDIR)/fossil.page.whistory.js \
@@ -269,10 +270,11 @@
270 $(SRCDIR)/sounds/d.wav \
271 $(SRCDIR)/sounds/e.wav \
272 $(SRCDIR)/sounds/f.wav \
273 $(SRCDIR)/style.admin_log.css \
274 $(SRCDIR)/style.chat.css \
275 $(SRCDIR)/style.chat-search.css \
276 $(SRCDIR)/style.fileedit.css \
277 $(SRCDIR)/style.pikchrshow.css \
278 $(SRCDIR)/style.wikiedit.css \
279 $(SRCDIR)/tree.js \
280 $(SRCDIR)/useredit.js \
281
282 DDED src/style.chat-search.css
--- a/src/style.chat-search.css
+++ b/src/style.chat-search.css
@@ -0,0 +1,677 @@
1
+/* Chat-related */
2
+body.chat span.at-name { /* for @USERNAME references */
3
+ text-decoration: underline;
4
+ font-weight: bold;
5
+}
6
+/* A wrapper for a single single chat message (one row of the UI) */
7
+body.chat .message-widget {
8
+ margin-bottom: 0.75em;
9
+ border: none;
10
+ display: flex;
11
+ flex-direction: column;
12
+ border: none;
13
+ align-items: flex-start;
14
+}
15
+body.chat button,
16
+body.chat input[type=button] {
17
+ line-height: inherit/*undo skin-specific funkiness*/;
18
+}
19
+
20
+body.chat.my-messages-right .message-widget.mine {
21
+ /* Right-aligns a user's own chat messages, similar to how
22
+ most/some mobile messaging apps do it. */
23
+ align-items: flex-end;
24
+}
25
+body.chat.my-messages-right .message-widget.notification {
26
+ /* Center-aligns a system-level notification message. */
27
+ align-items: center;
28
+}
29
+/* The content area of a message. */
30
+body.chat .message-widget-content {
31
+ border-radius: 0.25em;
32
+ border: 1px solid rgba(0,0,0,0.2);
33
+ box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
34
+ padding: 0.25em 0.5em;
35
+ margin-top: 0;
36
+ min-width: 9em /*avoid unsightly "underlap" with the neighboring
37
+ .message-widget-tab element*/;
38
+ white-space: normal;
39
+ word-break: break-word /* so that full hashes wrap on narrow screens */;
40
+}
41
+
42
+body.chat .message-widget-content.wide {
43
+ /* Special case for when embedding content which we really want to
44
+ expand, namely iframes. */
45
+ width: 98%;
46
+}
47
+body.chat .message-widget-content label[for] {
48
+ margin-left: 0.25em;
49
+ cursor: pointer;
50
+}
51
+body.chat .message-widget-content > .attachment-link {
52
+ display: flex;
53
+ flex-direction: row;
54
+}
55
+body.chat .message-widget-content > .attachment-link > a {
56
+ margin-right: 1em;
57
+}
58
+body.chat .message-widget-content > iframe {
59
+ width: 100%;
60
+ max-width: 100%;
61
+ resize: both;
62
+}
63
+body.chat .message-widget-content> a {
64
+ /* Cosmetic: keep skin-induced on-hover underlining from shifting
65
+ content placed below this. */
66
+ border-bottom: 1px transparent;
67
+}
68
+body.chat.monospace-messages .message-widget-content,
69
+body.chat.monospace-messages .chat-input-field{
70
+ font-family: monospace;
71
+}
72
+body.chat .message-widget-content > * {
73
+ margin: 0;
74
+ padding: 0;
75
+}
76
+body.chat .message-widget-content > pre {
77
+ white-space: pre-wrap;
78
+}
79
+body.chat .message-widget-content > .markdown > *:first-child {
80
+ margin-top: 0;
81
+}
82
+body.chat .message-widget-content > .markdown > *:last-child {
83
+ margin-bottom: 0;
84
+}
85
+body.chat .message-widget-content.error .buttons {
86
+ display: flex;
87
+ flex-direction: row;
88
+ justify-content: space-around;
89
+ flex-wrap: wrap;
90
+}
91
+body.chat .message-widget-content.error .buttons > button {
92
+ margin: 0.25em;
93
+}
94
+
95
+body.chat .message-widget-content.error a {
96
+ color: inherit;
97
+}
98
+body.chat .message-widget-content.error .failed-message {
99
+ display: flex;
100
+ flex-direction: column;
101
+}
102
+body.chat .message-widget-content.error .failed-message textarea {
103
+ min-height: 5rem;
104
+}
105
+
106
+/* User name and timestamp (a LEGEND-like element) */
107
+body.chat .message-widget .message-widget-tab {
108
+ border-radius: 0.25em 0.25em 0 0;
109
+ margin: 0 0.25em 0em 0.15em;
110
+ padding: 0 0.5em 0.15em 0.5em;
111
+ cursor: pointer;
112
+ white-space: nowrap;
113
+}
114
+body.chat .fossil-tooltip.help-buttonlet-content {
115
+ font-size: 80%;
116
+}
117
+body.chat .message-widget .message-widget-tab .xfrom {
118
+ /* Element which holds the "this message is from user X" part
119
+ of the message banner. */
120
+ font-style: italic;
121
+ font-weight: bold;
122
+}
123
+/* The popup element for displaying message timestamps
124
+ and deletion controls. */
125
+body.chat .chat-message-popup {
126
+ font-family: monospace;
127
+ font-size: 0.9em;
128
+ text-align: left;
129
+ display: flex;
130
+ flex-direction: column;
131
+ align-items: stretch;
132
+ padding: 0.25em;
133
+ margin-top: 0.25em;
134
+ border: 1px outset;
135
+ border-radius: 0.5em;
136
+}
137
+/* Full message timestamps. */
138
+body.chat .chat-message-popup > span { white-space: nowrap; }
139
+/* Container for the message deletion buttons. */
140
+body.chat .chat-message-popup > .toolbar {
141
+ padding: 0;
142
+ margin: 0;
143
+ border: 2px inset rgba(0,0,0,0.3);
144
+ border-radius: 0.25em;
145
+ display: flex;
146
+ flex-direction: row;
147
+ justify-content: stretch;
148
+ flex-wrap: wrap;
149
+ align-items: center;
150
+}
151
+body.chat .chat-message-popup > .toolbar > * {
152
+ margin: 0.35em;
153
+}
154
+body.chat .chat-message-popup > .toolbar > button {
155
+ flex: 1 1 auto;
156
+}
157
+/* The widget for loading more/older chat messages. */
158
+body.chat #load-msg-toolbar {
159
+ border-radius: 0.25em;
160
+ padding: 0.1em 0.2em;
161
+ margin-bottom: 1em;
162
+}
163
+/* .all-done is set when chat has loaded all of the available
164
+ historical messages */
165
+body.chat #load-msg-toolbar.all-done {
166
+ opacity: 0.5;
167
+}
168
+body.chat #load-msg-toolbar > div {
169
+ display: flex;
170
+ flex-direction: row;
171
+ justify-content: stretch;
172
+ flex-wrap: wrap;
173
+}
174
+body.chat #load-msg-toolbar > div > button {
175
+ flex: 1 1 auto;
176
+}
177
+/* "Chat-only mode" hides the site header/footer, showing only
178
+ the chat app. */
179
+body.chat.chat-only-mode{
180
+ padding: 0;
181
+ margin: 0 auto;
182
+}
183
+body.chat #chat-button-settings {}
184
+/** Popup widget for the /chat settings. */
185
+body.chat .chat-settings-popup {
186
+ font-size: 0.8em;
187
+ text-align: left;
188
+ display: flex;
189
+ flex-direction: column;
190
+ align-items: stretch;
191
+ padding: 0.25em;
192
+ z-index: 200;
193
+}
194
+
195
+/** Container for the list of /chat messages. */
196
+body.chat #chat-messages-wrapper {
197
+ overflow: auto;
198
+ padding: 0 0.25em;
199
+}
200
+body.chat #chat-messages-wrapper.loading > * {
201
+ /* An attempt at reducing flicker when loading lots of messages. */
202
+ visibility: hidden;
203
+}
204
+body.chat div.content {
205
+ margin: 0;
206
+ padding: 0;
207
+ display: block;
208
+ flex-direction: column-reverse;
209
+ /* ^^^^ In order to get good automatic scrolling of new messages on
210
+ the BOTTOM in bottom-up chat mode, such that they scroll up
211
+ instead of down, we have to use column-reverse layout, which
212
+ changes #chat-messages-wrapper's "gravity" for purposes of
213
+ scrolling! If we instead use flex-direction:column then each new
214
+ message pushes #chat-input-area down further off the screen!
215
+ */
216
+ align-items: stretch;
217
+}
218
+/* Wrapper for /chat user input controls */
219
+body.chat #chat-input-area {
220
+ display: flex;
221
+ flex-direction: column;
222
+ padding: 0;
223
+ margin: 0;
224
+ flex: 0 1 auto;
225
+}
226
+body.chat:not(.chat-only-mode) #chat-input-area{
227
+ /* Safari user reports that 2em is necessary to keep the file selection
228
+ widget from overlapping the page footer, whereas a margin of 0 is fine
229
+ for FF/Chrome (and 2em is a *huge* waste of space for those). */
230
+ margin-bottom: 0;
231
+}
232
+.chat-input-field {
233
+ flex: 10 1 auto;
234
+ margin: 0;
235
+}
236
+#chat-input-field-x,
237
+#chat-input-field-multi {
238
+ overflow: auto;
239
+ resize: vertical;
240
+}
241
+#chat-input-field-x {
242
+ display: inline-block/*supposed workaround for Chrome weirdness*/;
243
+ padding: 0.2em;
244
+ background-color: rgba(156,156,156,0.3);
245
+ white-space: pre-wrap;
246
+ /* ^^^ Firefox, when pasting plain text into a contenteditable field,
247
+ loses all newlines unless we explicitly set this. Chrome does not. */
248
+ cursor: text;
249
+ /* ^^^ In some browsers the cursor may not change for a contenteditable
250
+ element until it has focus, causing potential confusion. */
251
+}
252
+#chat-input-field-x:empty::before {
253
+ content: attr(data-placeholder);
254
+ opacity: 0.6;
255
+}
256
+.chat-input-field:not(:focus){
257
+ border-width: 1px;
258
+ border-style: solid;
259
+ border-radius: 0.25em;
260
+}
261
+.chat-input-field:focus{
262
+ /* This transparent border helps avoid the text shifting around
263
+ when the contenteditable attribute causes a border (which we
264
+ apparently cannot style) to be added. */
265
+ border-width: 1px;
266
+ border-style: solid;
267
+ border-color: transparent;
268
+ border-radius: 0.25em;
269
+}
270
+/* Widget holding the chat message input field, send button, and
271
+ settings button. */
272
+body.chat #chat-input-line-wrapper {
273
+ display: flex;
274
+ flex-direction: row;
275
+ align-items: stretch;
276
+ flex-wrap: nowrap;
277
+}
278
+body.chat.chat-only-mode #chat-input-line-wrapper {
279
+ padding: 0 0.25em;
280
+}
281
+
282
+/*body.chat #chat-input-line-wrapper:not(.compact) {
283
+ flex-wrap: nowrap;
284
+}*/
285
+body.chat #chat-input-line-wrapper.compact {
286
+ /* "The problem" with wrapping, together with a contenteditable input
287
+ field, is that the latter grows as the user types, so causes
288
+ wrapping to happen while they type, then to unwrap as soon as the
289
+ input field is cleared (when the message is sent). When we stay
290
+ wrapped in compact mode, the wrapped buttons simply take up too
291
+ much space. */
292
+ /*flex-wrap: wrap;
293
+ justify-content: flex-end;*/
294
+ flex-direction: column;
295
+ /**
296
+ We "really do" need column orientation here because it's the
297
+ only way to eliminate the possibility that (A) the buttons
298
+ get truncated in very narrow windows and (B) that they keep
299
+ stable positions.
300
+ */
301
+}
302
+body.chat #chat-input-line-wrapper.compact #chat-input-field-x {
303
+}
304
+
305
+body.chat #chat-buttons-wrapper {
306
+ flex: 0 1 auto;
307
+ display: flex;
308
+ flex-direction: column;
309
+ align-items: center;
310
+ min-width: 4em;
311
+ min-height: 1.5em;
312
+ align-self: flex-end
313
+ /*keep buttons stable at bottom/right even when input field
314
+ resizes */;
315
+}
316
+body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper {
317
+ flex-direction: row;
318
+ flex: 1 1 auto;
319
+ align-self: stretch;
320
+ justify-content: flex-end;
321
+ /*flex-wrap: wrap;*/
322
+ /* Wrapping would be ideal except that the edit widget
323
+ grows in width as the user types, moving the buttons
324
+ around */
325
+}
326
+body.chat #chat-buttons-wrapper > .cbutton {
327
+ padding: 0;
328
+ display: inline-block;
329
+ border-width: 1px;
330
+ border-style: solid;
331
+ border-radius: 0.25em;
332
+ min-width: 4ex;
333
+ max-width: 4ex;
334
+ min-height: 3ex;
335
+ max-height: 3ex;
336
+ margin: 0.125em;
337
+ display: inline-flex;
338
+ justify-content: center;
339
+ align-items: center;
340
+ cursor: pointer;
341
+ font-size: 130%;
342
+}
343
+body.chat #chat-buttons-wrapper > .cbutton:hover {
344
+ background-color: rgba(200,200,200,0.3);
345
+}
346
+body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper > .cbutton {
347
+ margin: 2px 0.125em 0 0.125em;
348
+ min-width: 6ex;
349
+ max-width: 6ex;
350
+ min-height: 2.3ex;
351
+ max-height: 2.3ex;
352
+ font-size: 120%;
353
+}
354
+body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper #chat-button-submit {
355
+ min-width: 12ex;
356
+}
357
+.chat-input-field {
358
+ font-family: inherit
359
+}
360
+body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-multi,
361
+body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-x {
362
+ min-height: 4rem;
363
+/*
364
+ Problems related to max-height:
365
+
366
+ - If we do NOT set a max-height then pasting/typing a large amount
367
+ of text can cause this element to grow without bounds, larger than
368
+ the window, and there's no way to navigate it sensibly. In this
369
+ case, manually resizing the element (desktop only - mobile doesn't
370
+ offer that) will force it to stay at the selected size even if more
371
+ content is added to it later.
372
+
373
+ - If we DO set a max-height then its growth is bounded but it also
374
+ cannot manually expanded by the user.
375
+
376
+ The lesser of the two evils seems to be to rely on the browser
377
+ feature that a manual resize of the element will pin its size.
378
+*/
379
+}
380
+
381
+body.chat #chat-input-line-wrapper > #chat-button-settings{
382
+ margin: 0 0 0 0.25em;
383
+ max-width: 2em;
384
+}
385
+body.chat #chat-input-line-wrapper > input[type=text],
386
+body.chat #chat-input-line-wrapper > textarea {
387
+ flex: 20 1 auto;
388
+ max-width: revert;
389
+ min-width: 20em;
390
+}
391
+body.chat #chat-input-line-wrapper.compact > input[type=text] {
392
+ margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
393
+}
394
+/* Widget holding the file selection control and preview */
395
+body.chat #chat-input-file-area {
396
+ display: flex;
397
+ flex-direction: row;
398
+ margin: 0;
399
+}
400
+body.chat #chat-input-file-area > .file-selection-wrapper {
401
+ align-self: flex-start;
402
+ margin-right: 0.5em;
403
+ flex: 0 1 auto;
404
+ padding: 0.25em 0.5em;
405
+ white-space: nowrap;
406
+}
407
+body.chat #chat-input-file {
408
+ border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
409
+ border-radius: 0.25em;
410
+ padding: 0.25em;
411
+}
412
+body.chat #chat-input-file > input {
413
+ flex: 1 0 auto;
414
+}
415
+/* Indicator when a drag/drop is in progress */
416
+body.chat #chat-input-file.dragover {
417
+ border: 1px dashed green;
418
+}
419
+/* Widget holding the details of a selected/dropped file/image. */
420
+body.chat #chat-drop-details {
421
+ padding: 0 1em;
422
+ white-space: pre;
423
+ font-family: monospace;
424
+ margin: auto;
425
+ flex: 0;
426
+}
427
+body.chat #chat-drop-details:empty {
428
+ padding: 0;
429
+ margin: 0;
430
+}
431
+body.chat #chat-drop-details img {
432
+ max-width: 45%;
433
+ max-height: 45%;
434
+}
435
+body.chat .chat-view {
436
+ flex: 20 1 auto
437
+ /*ensure that these grow more than the non-.chat-view elements.
438
+ Note that setting flex shrink to 0 breaks/disables scrolling!*/;
439
+ margin-bottom: 0.2em;
440
+}
441
+body.chat #chat-config,
442
+body.chat #chat-preview {
443
+ /* /chat configuration widget */
444
+ display: flex;
445
+ flex-direction: column;
446
+ overflow: auto;
447
+ padding: 0;
448
+ margin: 0;
449
+ align-items: stretch;
450
+ min-height: 6em;
451
+}
452
+body.chat #chat-config #chat-config-options {
453
+ /* /chat config options go here */
454
+ flex: 1 1 auto;
455
+ display: flex;
456
+ flex-direction: column;
457
+ overflow: auto;
458
+ align-items: stretch;
459
+}
460
+body.chat #chat-config #chat-config-options .menu-entry {
461
+ display: flex;
462
+ align-items: center;
463
+ flex-direction: row-reverse;
464
+ flex-wrap: nowrap;
465
+ padding: 1em;
466
+ flex: 1 1 auto;
467
+ align-self: stretch;
468
+}
469
+body.chat #chat-config #chat-config-options .menu-entry.parent{
470
+ border-radius: 1em 1em 0 1em;
471
+ margin-top: 1em;
472
+}
473
+body.chat #chat-config #chat-config-options .menu-entry.child {
474
+ /*padding-left: 2.5em;*/
475
+ margin-left: 2em;
476
+}
477
+body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(even){
478
+ background-color: rgba(175,175,175,0.15);
479
+}
480
+body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(odd){
481
+ background-color: rgba(175,175,175,0.35);
482
+}
483
+body.chat #chat-config #chat-config-options .menu-entry:first-child {
484
+ /* Config list header */
485
+ border-radius: 0 0 1em 1em;
486
+}
487
+body.chat #chat-config #chat-config-options .menu-entry:first-child .label-wrapper {
488
+ align-items: start;
489
+}
490
+body.chat #chat-config #chat-config-options .menu-entry > .toggle-wrapper {
491
+ /* Holder for a checkbox, if any */
492
+ min-width: 1.5rem;
493
+ margin-right: 1rem;
494
+}
495
+body.chat #chat-config #chat-config-options .menu-entry .label-wrapper {
496
+ /* Wrapper for a LABEL and a .hint element. */
497
+ display: flex;
498
+ flex-direction: column;
499
+ align-self: baseline;
500
+ flex: 1 1 auto;
501
+}
502
+body.chat #chat-config #chat-config-options .menu-entry label {
503
+ /* Config option label. */
504
+ font-weight: bold;
505
+ white-space: initial;
506
+}
507
+body.chat #chat-config #chat-config-options .menu-entry label[for] {
508
+ cursor: pointer;
509
+}
510
+body.chat #chat-config #chat-config-options .menu-entry .hint {
511
+ /* Config menu hint text */
512
+ font-size: 85%;
513
+ font-weight: normal;
514
+ white-space: pre-wrap;
515
+ display: inline-block;
516
+ opacity: 0.85;
517
+}
518
+body.chat #chat-config #chat-config-options .menu-entry select {
519
+}
520
+body.chat #chat-preview #chat-preview-content {
521
+ overflow: auto;
522
+ flex: 1 1 auto;
523
+ padding: 0.5em;
524
+ border: 1px dotted;
525
+}
526
+body.chat #chat-preview #chat-preview-content > * {
527
+ margin: 0;
528
+ padding: 0;
529
+}
530
+body.chat #chat-preview #chat-preview-buttons {
531
+ flex: 0 1 auto;
532
+ display: flex;
533
+ flex-direction: column;
534
+}
535
+body.chat #chat-config > button,
536
+body.chat #chat-preview #chat-preview-buttons > button {
537
+ padding: 0.5em;
538
+ flex: 0 1 auto;
539
+ margin: 0.25em 0;
540
+}
541
+
542
+body.chat #chat-user-list-wrapper {
543
+ /* Safari can't do fieldsets right, so we emulate one. */
544
+ border-radius: 0.5em;
545
+ margin: 1em 0 0.2em 0;
546
+ padding: 0 0.5em;
547
+ border-style: inset;
548
+ border-width: 0 1px 1px 1px/*else collides with the LEGEND*/;
549
+}
550
+body.chat #chat-user-list-wrapper.collapsed {
551
+ padding: 0;
552
+}
553
+body.chat #chat-user-list-wrapper > .legend {
554
+ font-weight: initial;
555
+ padding: 0 0.5em 0 0.5em;
556
+ position: relative;
557
+ top: -1.75ex/* place it like a fieldset legend */;
558
+ cursor: pointer;
559
+}
560
+body.chat #chat-user-list-wrapper > .legend > * {
561
+ vertical-align: middle;
562
+}
563
+body.chat #chat-user-list-wrapper > .legend > *:nth-child(2){
564
+ /* Title label */
565
+ opacity: 0.6;
566
+ font-size: 0.8em;
567
+}
568
+body.chat #chat-user-list-wrapper.collapsed > .legend > *:nth-child(2)::after {
569
+ content: " (tap to toggle)";
570
+}
571
+body.chat #chat-user-list-wrapper .help-buttonlet {
572
+ margin: 0;
573
+}
574
+body.chat #chat-user-list-wrapper.collapsed #chat-user-list {
575
+ position: absolute !important;
576
+ opacity: 0 !important;
577
+ pointer-events: none !important;
578
+ display: none !important;
579
+}
580
+body.chat #chat-user-list {
581
+ margin-top: -1.25ex;
582
+ display: flex;
583
+ flex-direction: row;
584
+ flex-wrap: wrap;
585
+ align-items: center;
586
+}
587
+body.chat #chat-user-list .chat-user {
588
+ margin: 0.2em;
589
+ padding: 0.1em 0.5em 0.2em 0.5em;
590
+ border-radius: 0.5em;
591
+ cursor: pointer;
592
+ text-align: center;
593
+ white-space: pre;
594
+}
595
+body.chat #chat-user-list .timestamp {
596
+ font-size: 85%;
597
+ font-family: monospace;
598
+}
599
+body.chat #chat-user-list:not(.timestamps) .timestamp {
600
+ display: none;
601
+}
602
+body.chat #chat-user-list .chat-user.selected {
603
+ font-weight: bold;
604
+ text-decoration: underline;
605
+}
606
+
607
+body.chat.fossil-dark-style #chat-button-attach > svg {
608
+ /* The black paperclip is barely visible in dark-mode
609
+ skins when they have dark buttons */
610
+ filter: invert(0.8);
611
+}
612
+
613
+body.chat .anim-rotate-360 {
614
+ animation: rotate-360 750ms linear;
615
+}
616
+@keyframes rotate-360 {
617
+ from { transform: rotate(0deg); }
618
+ to { transform: rotate(360deg); }
619
+}
620
+body.chat .anim-flip-h {
621
+ animation: flip-h 750ms linear;
622
+}
623
+@keyframes flip-h{
624
+ from { transform: rotateY(0deg); }
625
+ to { transform: rotateY(360deg); }
626
+}
627
+body.chat .anim-flip-v {
628
+ animation: flip-v 750ms linear;
629
+}
630
+@keyframes flip-v{
631
+ from { transform: rotateX(0deg); }
632
+ to { transform: rotateX(360deg); }
633
+}
634
+body.chat .anim-fade-in {
635
+ animation: fade-in 750ms linear;
636
+}
637
+body.chat .anim-fade-in-fast {
638
+ animation: fade-in 350ms linear;
639
+}
640
+@keyframes fade-in {
641
+ from { opacity: 0; }
642
+ to { opacity: 1; }
643
+}
644
+body.chat .anim-fade-out-fast {
645
+ animation: fade-out 250ms linear;
646
+}
647
+@keyframes fade-out {
648
+ from { opacity: 1; }
649
+ to { opacity: 0; }
650
+}
651
+
652
+/***********************/
653
+
654
+body.chat .message-widget .match {
655
+ font-weight: bold;
656
+ background-color: yellow;
657
+}
658
+
659
+body.chat .searchForm {
660
+ margin-top: 1em;
661
+}
662
+body.chat .spacer-widget button {
663
+ margin-left: 1ex;
664
+ margin-right: 1ex;
665
+}
666
+
667
+body.chat .spacer-widget-buttons .up {
668
+ margin-top: -0.75em;
669
+ margin-bottom: 1em;
670
+}
671
+body.chat .spacer-widget-buttons .down {
672
+ margin-top: 1em;
673
+}
674
+body.chat .spacer-widget-buttons .all {
675
+ margin-bottom: 0.75em;
676
+}
677
+
--- a/src/style.chat-search.css
+++ b/src/style.chat-search.css
@@ -0,0 +1,677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/style.chat-search.css
+++ b/src/style.chat-search.css
@@ -0,0 +1,677 @@
1 /* Chat-related */
2 body.chat span.at-name { /* for @USERNAME references */
3 text-decoration: underline;
4 font-weight: bold;
5 }
6 /* A wrapper for a single single chat message (one row of the UI) */
7 body.chat .message-widget {
8 margin-bottom: 0.75em;
9 border: none;
10 display: flex;
11 flex-direction: column;
12 border: none;
13 align-items: flex-start;
14 }
15 body.chat button,
16 body.chat input[type=button] {
17 line-height: inherit/*undo skin-specific funkiness*/;
18 }
19
20 body.chat.my-messages-right .message-widget.mine {
21 /* Right-aligns a user's own chat messages, similar to how
22 most/some mobile messaging apps do it. */
23 align-items: flex-end;
24 }
25 body.chat.my-messages-right .message-widget.notification {
26 /* Center-aligns a system-level notification message. */
27 align-items: center;
28 }
29 /* The content area of a message. */
30 body.chat .message-widget-content {
31 border-radius: 0.25em;
32 border: 1px solid rgba(0,0,0,0.2);
33 box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
34 padding: 0.25em 0.5em;
35 margin-top: 0;
36 min-width: 9em /*avoid unsightly "underlap" with the neighboring
37 .message-widget-tab element*/;
38 white-space: normal;
39 word-break: break-word /* so that full hashes wrap on narrow screens */;
40 }
41
42 body.chat .message-widget-content.wide {
43 /* Special case for when embedding content which we really want to
44 expand, namely iframes. */
45 width: 98%;
46 }
47 body.chat .message-widget-content label[for] {
48 margin-left: 0.25em;
49 cursor: pointer;
50 }
51 body.chat .message-widget-content > .attachment-link {
52 display: flex;
53 flex-direction: row;
54 }
55 body.chat .message-widget-content > .attachment-link > a {
56 margin-right: 1em;
57 }
58 body.chat .message-widget-content > iframe {
59 width: 100%;
60 max-width: 100%;
61 resize: both;
62 }
63 body.chat .message-widget-content> a {
64 /* Cosmetic: keep skin-induced on-hover underlining from shifting
65 content placed below this. */
66 border-bottom: 1px transparent;
67 }
68 body.chat.monospace-messages .message-widget-content,
69 body.chat.monospace-messages .chat-input-field{
70 font-family: monospace;
71 }
72 body.chat .message-widget-content > * {
73 margin: 0;
74 padding: 0;
75 }
76 body.chat .message-widget-content > pre {
77 white-space: pre-wrap;
78 }
79 body.chat .message-widget-content > .markdown > *:first-child {
80 margin-top: 0;
81 }
82 body.chat .message-widget-content > .markdown > *:last-child {
83 margin-bottom: 0;
84 }
85 body.chat .message-widget-content.error .buttons {
86 display: flex;
87 flex-direction: row;
88 justify-content: space-around;
89 flex-wrap: wrap;
90 }
91 body.chat .message-widget-content.error .buttons > button {
92 margin: 0.25em;
93 }
94
95 body.chat .message-widget-content.error a {
96 color: inherit;
97 }
98 body.chat .message-widget-content.error .failed-message {
99 display: flex;
100 flex-direction: column;
101 }
102 body.chat .message-widget-content.error .failed-message textarea {
103 min-height: 5rem;
104 }
105
106 /* User name and timestamp (a LEGEND-like element) */
107 body.chat .message-widget .message-widget-tab {
108 border-radius: 0.25em 0.25em 0 0;
109 margin: 0 0.25em 0em 0.15em;
110 padding: 0 0.5em 0.15em 0.5em;
111 cursor: pointer;
112 white-space: nowrap;
113 }
114 body.chat .fossil-tooltip.help-buttonlet-content {
115 font-size: 80%;
116 }
117 body.chat .message-widget .message-widget-tab .xfrom {
118 /* Element which holds the "this message is from user X" part
119 of the message banner. */
120 font-style: italic;
121 font-weight: bold;
122 }
123 /* The popup element for displaying message timestamps
124 and deletion controls. */
125 body.chat .chat-message-popup {
126 font-family: monospace;
127 font-size: 0.9em;
128 text-align: left;
129 display: flex;
130 flex-direction: column;
131 align-items: stretch;
132 padding: 0.25em;
133 margin-top: 0.25em;
134 border: 1px outset;
135 border-radius: 0.5em;
136 }
137 /* Full message timestamps. */
138 body.chat .chat-message-popup > span { white-space: nowrap; }
139 /* Container for the message deletion buttons. */
140 body.chat .chat-message-popup > .toolbar {
141 padding: 0;
142 margin: 0;
143 border: 2px inset rgba(0,0,0,0.3);
144 border-radius: 0.25em;
145 display: flex;
146 flex-direction: row;
147 justify-content: stretch;
148 flex-wrap: wrap;
149 align-items: center;
150 }
151 body.chat .chat-message-popup > .toolbar > * {
152 margin: 0.35em;
153 }
154 body.chat .chat-message-popup > .toolbar > button {
155 flex: 1 1 auto;
156 }
157 /* The widget for loading more/older chat messages. */
158 body.chat #load-msg-toolbar {
159 border-radius: 0.25em;
160 padding: 0.1em 0.2em;
161 margin-bottom: 1em;
162 }
163 /* .all-done is set when chat has loaded all of the available
164 historical messages */
165 body.chat #load-msg-toolbar.all-done {
166 opacity: 0.5;
167 }
168 body.chat #load-msg-toolbar > div {
169 display: flex;
170 flex-direction: row;
171 justify-content: stretch;
172 flex-wrap: wrap;
173 }
174 body.chat #load-msg-toolbar > div > button {
175 flex: 1 1 auto;
176 }
177 /* "Chat-only mode" hides the site header/footer, showing only
178 the chat app. */
179 body.chat.chat-only-mode{
180 padding: 0;
181 margin: 0 auto;
182 }
183 body.chat #chat-button-settings {}
184 /** Popup widget for the /chat settings. */
185 body.chat .chat-settings-popup {
186 font-size: 0.8em;
187 text-align: left;
188 display: flex;
189 flex-direction: column;
190 align-items: stretch;
191 padding: 0.25em;
192 z-index: 200;
193 }
194
195 /** Container for the list of /chat messages. */
196 body.chat #chat-messages-wrapper {
197 overflow: auto;
198 padding: 0 0.25em;
199 }
200 body.chat #chat-messages-wrapper.loading > * {
201 /* An attempt at reducing flicker when loading lots of messages. */
202 visibility: hidden;
203 }
204 body.chat div.content {
205 margin: 0;
206 padding: 0;
207 display: block;
208 flex-direction: column-reverse;
209 /* ^^^^ In order to get good automatic scrolling of new messages on
210 the BOTTOM in bottom-up chat mode, such that they scroll up
211 instead of down, we have to use column-reverse layout, which
212 changes #chat-messages-wrapper's "gravity" for purposes of
213 scrolling! If we instead use flex-direction:column then each new
214 message pushes #chat-input-area down further off the screen!
215 */
216 align-items: stretch;
217 }
218 /* Wrapper for /chat user input controls */
219 body.chat #chat-input-area {
220 display: flex;
221 flex-direction: column;
222 padding: 0;
223 margin: 0;
224 flex: 0 1 auto;
225 }
226 body.chat:not(.chat-only-mode) #chat-input-area{
227 /* Safari user reports that 2em is necessary to keep the file selection
228 widget from overlapping the page footer, whereas a margin of 0 is fine
229 for FF/Chrome (and 2em is a *huge* waste of space for those). */
230 margin-bottom: 0;
231 }
232 .chat-input-field {
233 flex: 10 1 auto;
234 margin: 0;
235 }
236 #chat-input-field-x,
237 #chat-input-field-multi {
238 overflow: auto;
239 resize: vertical;
240 }
241 #chat-input-field-x {
242 display: inline-block/*supposed workaround for Chrome weirdness*/;
243 padding: 0.2em;
244 background-color: rgba(156,156,156,0.3);
245 white-space: pre-wrap;
246 /* ^^^ Firefox, when pasting plain text into a contenteditable field,
247 loses all newlines unless we explicitly set this. Chrome does not. */
248 cursor: text;
249 /* ^^^ In some browsers the cursor may not change for a contenteditable
250 element until it has focus, causing potential confusion. */
251 }
252 #chat-input-field-x:empty::before {
253 content: attr(data-placeholder);
254 opacity: 0.6;
255 }
256 .chat-input-field:not(:focus){
257 border-width: 1px;
258 border-style: solid;
259 border-radius: 0.25em;
260 }
261 .chat-input-field:focus{
262 /* This transparent border helps avoid the text shifting around
263 when the contenteditable attribute causes a border (which we
264 apparently cannot style) to be added. */
265 border-width: 1px;
266 border-style: solid;
267 border-color: transparent;
268 border-radius: 0.25em;
269 }
270 /* Widget holding the chat message input field, send button, and
271 settings button. */
272 body.chat #chat-input-line-wrapper {
273 display: flex;
274 flex-direction: row;
275 align-items: stretch;
276 flex-wrap: nowrap;
277 }
278 body.chat.chat-only-mode #chat-input-line-wrapper {
279 padding: 0 0.25em;
280 }
281
282 /*body.chat #chat-input-line-wrapper:not(.compact) {
283 flex-wrap: nowrap;
284 }*/
285 body.chat #chat-input-line-wrapper.compact {
286 /* "The problem" with wrapping, together with a contenteditable input
287 field, is that the latter grows as the user types, so causes
288 wrapping to happen while they type, then to unwrap as soon as the
289 input field is cleared (when the message is sent). When we stay
290 wrapped in compact mode, the wrapped buttons simply take up too
291 much space. */
292 /*flex-wrap: wrap;
293 justify-content: flex-end;*/
294 flex-direction: column;
295 /**
296 We "really do" need column orientation here because it's the
297 only way to eliminate the possibility that (A) the buttons
298 get truncated in very narrow windows and (B) that they keep
299 stable positions.
300 */
301 }
302 body.chat #chat-input-line-wrapper.compact #chat-input-field-x {
303 }
304
305 body.chat #chat-buttons-wrapper {
306 flex: 0 1 auto;
307 display: flex;
308 flex-direction: column;
309 align-items: center;
310 min-width: 4em;
311 min-height: 1.5em;
312 align-self: flex-end
313 /*keep buttons stable at bottom/right even when input field
314 resizes */;
315 }
316 body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper {
317 flex-direction: row;
318 flex: 1 1 auto;
319 align-self: stretch;
320 justify-content: flex-end;
321 /*flex-wrap: wrap;*/
322 /* Wrapping would be ideal except that the edit widget
323 grows in width as the user types, moving the buttons
324 around */
325 }
326 body.chat #chat-buttons-wrapper > .cbutton {
327 padding: 0;
328 display: inline-block;
329 border-width: 1px;
330 border-style: solid;
331 border-radius: 0.25em;
332 min-width: 4ex;
333 max-width: 4ex;
334 min-height: 3ex;
335 max-height: 3ex;
336 margin: 0.125em;
337 display: inline-flex;
338 justify-content: center;
339 align-items: center;
340 cursor: pointer;
341 font-size: 130%;
342 }
343 body.chat #chat-buttons-wrapper > .cbutton:hover {
344 background-color: rgba(200,200,200,0.3);
345 }
346 body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper > .cbutton {
347 margin: 2px 0.125em 0 0.125em;
348 min-width: 6ex;
349 max-width: 6ex;
350 min-height: 2.3ex;
351 max-height: 2.3ex;
352 font-size: 120%;
353 }
354 body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper #chat-button-submit {
355 min-width: 12ex;
356 }
357 .chat-input-field {
358 font-family: inherit
359 }
360 body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-multi,
361 body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-x {
362 min-height: 4rem;
363 /*
364 Problems related to max-height:
365
366 - If we do NOT set a max-height then pasting/typing a large amount
367 of text can cause this element to grow without bounds, larger than
368 the window, and there's no way to navigate it sensibly. In this
369 case, manually resizing the element (desktop only - mobile doesn't
370 offer that) will force it to stay at the selected size even if more
371 content is added to it later.
372
373 - If we DO set a max-height then its growth is bounded but it also
374 cannot manually expanded by the user.
375
376 The lesser of the two evils seems to be to rely on the browser
377 feature that a manual resize of the element will pin its size.
378 */
379 }
380
381 body.chat #chat-input-line-wrapper > #chat-button-settings{
382 margin: 0 0 0 0.25em;
383 max-width: 2em;
384 }
385 body.chat #chat-input-line-wrapper > input[type=text],
386 body.chat #chat-input-line-wrapper > textarea {
387 flex: 20 1 auto;
388 max-width: revert;
389 min-width: 20em;
390 }
391 body.chat #chat-input-line-wrapper.compact > input[type=text] {
392 margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
393 }
394 /* Widget holding the file selection control and preview */
395 body.chat #chat-input-file-area {
396 display: flex;
397 flex-direction: row;
398 margin: 0;
399 }
400 body.chat #chat-input-file-area > .file-selection-wrapper {
401 align-self: flex-start;
402 margin-right: 0.5em;
403 flex: 0 1 auto;
404 padding: 0.25em 0.5em;
405 white-space: nowrap;
406 }
407 body.chat #chat-input-file {
408 border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
409 border-radius: 0.25em;
410 padding: 0.25em;
411 }
412 body.chat #chat-input-file > input {
413 flex: 1 0 auto;
414 }
415 /* Indicator when a drag/drop is in progress */
416 body.chat #chat-input-file.dragover {
417 border: 1px dashed green;
418 }
419 /* Widget holding the details of a selected/dropped file/image. */
420 body.chat #chat-drop-details {
421 padding: 0 1em;
422 white-space: pre;
423 font-family: monospace;
424 margin: auto;
425 flex: 0;
426 }
427 body.chat #chat-drop-details:empty {
428 padding: 0;
429 margin: 0;
430 }
431 body.chat #chat-drop-details img {
432 max-width: 45%;
433 max-height: 45%;
434 }
435 body.chat .chat-view {
436 flex: 20 1 auto
437 /*ensure that these grow more than the non-.chat-view elements.
438 Note that setting flex shrink to 0 breaks/disables scrolling!*/;
439 margin-bottom: 0.2em;
440 }
441 body.chat #chat-config,
442 body.chat #chat-preview {
443 /* /chat configuration widget */
444 display: flex;
445 flex-direction: column;
446 overflow: auto;
447 padding: 0;
448 margin: 0;
449 align-items: stretch;
450 min-height: 6em;
451 }
452 body.chat #chat-config #chat-config-options {
453 /* /chat config options go here */
454 flex: 1 1 auto;
455 display: flex;
456 flex-direction: column;
457 overflow: auto;
458 align-items: stretch;
459 }
460 body.chat #chat-config #chat-config-options .menu-entry {
461 display: flex;
462 align-items: center;
463 flex-direction: row-reverse;
464 flex-wrap: nowrap;
465 padding: 1em;
466 flex: 1 1 auto;
467 align-self: stretch;
468 }
469 body.chat #chat-config #chat-config-options .menu-entry.parent{
470 border-radius: 1em 1em 0 1em;
471 margin-top: 1em;
472 }
473 body.chat #chat-config #chat-config-options .menu-entry.child {
474 /*padding-left: 2.5em;*/
475 margin-left: 2em;
476 }
477 body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(even){
478 background-color: rgba(175,175,175,0.15);
479 }
480 body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(odd){
481 background-color: rgba(175,175,175,0.35);
482 }
483 body.chat #chat-config #chat-config-options .menu-entry:first-child {
484 /* Config list header */
485 border-radius: 0 0 1em 1em;
486 }
487 body.chat #chat-config #chat-config-options .menu-entry:first-child .label-wrapper {
488 align-items: start;
489 }
490 body.chat #chat-config #chat-config-options .menu-entry > .toggle-wrapper {
491 /* Holder for a checkbox, if any */
492 min-width: 1.5rem;
493 margin-right: 1rem;
494 }
495 body.chat #chat-config #chat-config-options .menu-entry .label-wrapper {
496 /* Wrapper for a LABEL and a .hint element. */
497 display: flex;
498 flex-direction: column;
499 align-self: baseline;
500 flex: 1 1 auto;
501 }
502 body.chat #chat-config #chat-config-options .menu-entry label {
503 /* Config option label. */
504 font-weight: bold;
505 white-space: initial;
506 }
507 body.chat #chat-config #chat-config-options .menu-entry label[for] {
508 cursor: pointer;
509 }
510 body.chat #chat-config #chat-config-options .menu-entry .hint {
511 /* Config menu hint text */
512 font-size: 85%;
513 font-weight: normal;
514 white-space: pre-wrap;
515 display: inline-block;
516 opacity: 0.85;
517 }
518 body.chat #chat-config #chat-config-options .menu-entry select {
519 }
520 body.chat #chat-preview #chat-preview-content {
521 overflow: auto;
522 flex: 1 1 auto;
523 padding: 0.5em;
524 border: 1px dotted;
525 }
526 body.chat #chat-preview #chat-preview-content > * {
527 margin: 0;
528 padding: 0;
529 }
530 body.chat #chat-preview #chat-preview-buttons {
531 flex: 0 1 auto;
532 display: flex;
533 flex-direction: column;
534 }
535 body.chat #chat-config > button,
536 body.chat #chat-preview #chat-preview-buttons > button {
537 padding: 0.5em;
538 flex: 0 1 auto;
539 margin: 0.25em 0;
540 }
541
542 body.chat #chat-user-list-wrapper {
543 /* Safari can't do fieldsets right, so we emulate one. */
544 border-radius: 0.5em;
545 margin: 1em 0 0.2em 0;
546 padding: 0 0.5em;
547 border-style: inset;
548 border-width: 0 1px 1px 1px/*else collides with the LEGEND*/;
549 }
550 body.chat #chat-user-list-wrapper.collapsed {
551 padding: 0;
552 }
553 body.chat #chat-user-list-wrapper > .legend {
554 font-weight: initial;
555 padding: 0 0.5em 0 0.5em;
556 position: relative;
557 top: -1.75ex/* place it like a fieldset legend */;
558 cursor: pointer;
559 }
560 body.chat #chat-user-list-wrapper > .legend > * {
561 vertical-align: middle;
562 }
563 body.chat #chat-user-list-wrapper > .legend > *:nth-child(2){
564 /* Title label */
565 opacity: 0.6;
566 font-size: 0.8em;
567 }
568 body.chat #chat-user-list-wrapper.collapsed > .legend > *:nth-child(2)::after {
569 content: " (tap to toggle)";
570 }
571 body.chat #chat-user-list-wrapper .help-buttonlet {
572 margin: 0;
573 }
574 body.chat #chat-user-list-wrapper.collapsed #chat-user-list {
575 position: absolute !important;
576 opacity: 0 !important;
577 pointer-events: none !important;
578 display: none !important;
579 }
580 body.chat #chat-user-list {
581 margin-top: -1.25ex;
582 display: flex;
583 flex-direction: row;
584 flex-wrap: wrap;
585 align-items: center;
586 }
587 body.chat #chat-user-list .chat-user {
588 margin: 0.2em;
589 padding: 0.1em 0.5em 0.2em 0.5em;
590 border-radius: 0.5em;
591 cursor: pointer;
592 text-align: center;
593 white-space: pre;
594 }
595 body.chat #chat-user-list .timestamp {
596 font-size: 85%;
597 font-family: monospace;
598 }
599 body.chat #chat-user-list:not(.timestamps) .timestamp {
600 display: none;
601 }
602 body.chat #chat-user-list .chat-user.selected {
603 font-weight: bold;
604 text-decoration: underline;
605 }
606
607 body.chat.fossil-dark-style #chat-button-attach > svg {
608 /* The black paperclip is barely visible in dark-mode
609 skins when they have dark buttons */
610 filter: invert(0.8);
611 }
612
613 body.chat .anim-rotate-360 {
614 animation: rotate-360 750ms linear;
615 }
616 @keyframes rotate-360 {
617 from { transform: rotate(0deg); }
618 to { transform: rotate(360deg); }
619 }
620 body.chat .anim-flip-h {
621 animation: flip-h 750ms linear;
622 }
623 @keyframes flip-h{
624 from { transform: rotateY(0deg); }
625 to { transform: rotateY(360deg); }
626 }
627 body.chat .anim-flip-v {
628 animation: flip-v 750ms linear;
629 }
630 @keyframes flip-v{
631 from { transform: rotateX(0deg); }
632 to { transform: rotateX(360deg); }
633 }
634 body.chat .anim-fade-in {
635 animation: fade-in 750ms linear;
636 }
637 body.chat .anim-fade-in-fast {
638 animation: fade-in 350ms linear;
639 }
640 @keyframes fade-in {
641 from { opacity: 0; }
642 to { opacity: 1; }
643 }
644 body.chat .anim-fade-out-fast {
645 animation: fade-out 250ms linear;
646 }
647 @keyframes fade-out {
648 from { opacity: 1; }
649 to { opacity: 0; }
650 }
651
652 /***********************/
653
654 body.chat .message-widget .match {
655 font-weight: bold;
656 background-color: yellow;
657 }
658
659 body.chat .searchForm {
660 margin-top: 1em;
661 }
662 body.chat .spacer-widget button {
663 margin-left: 1ex;
664 margin-right: 1ex;
665 }
666
667 body.chat .spacer-widget-buttons .up {
668 margin-top: -0.75em;
669 margin-bottom: 1em;
670 }
671 body.chat .spacer-widget-buttons .down {
672 margin-top: 1em;
673 }
674 body.chat .spacer-widget-buttons .all {
675 margin-bottom: 0.75em;
676 }
677

Keyboard Shortcuts

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