Fossil SCM

/chat now uses full-fledged markdown instead of its former special-case markup. Added a message preview option to support that.

stephan 2021-09-21 13:25 trunk merge
Commit 6c1ac839e3591d02a6fb2f348badb2dcd2f138fe1dfea842b500cabacbe7a007
+5 -3
--- src/ajax.c
+++ src/ajax.c
@@ -264,16 +264,18 @@
264264
}
265265
return rc;
266266
}
267267
268268
/*
269
-** AJAX route /ajax/preview-wiki
269
+** AJAX route /ajax/preview-text
270270
**
271271
** Required query parameters:
272272
**
273273
** filename=name of content, for use in determining the
274
-** mimetype/render mode. content=text
274
+** mimetype/render mode.
275
+**
276
+** content=text
275277
**
276278
** Optional query parameters:
277279
**
278280
** render_mode=integer (AJAX_RENDER_xxx) (default=AJAX_RENDER_GUESS)
279281
**
@@ -365,11 +367,11 @@
365367
const AjaxRoute * rB = (const AjaxRoute*)b;
366368
return fossil_strcmp(rA->zName, rB->zName);
367369
}
368370
369371
/*
370
-** WEBPAGE: ajax
372
+** WEBPAGE: ajax hidden
371373
**
372374
** The main dispatcher for shared ajax-served routes. Requires the
373375
** 'name' parameter be the main route's name (as defined in a list in
374376
** this function), noting that fossil automatically assigns all path
375377
** parts after "ajax" to "name", e.g. /ajax/foo/bar assigns
376378
--- src/ajax.c
+++ src/ajax.c
@@ -264,16 +264,18 @@
264 }
265 return rc;
266 }
267
268 /*
269 ** AJAX route /ajax/preview-wiki
270 **
271 ** Required query parameters:
272 **
273 ** filename=name of content, for use in determining the
274 ** mimetype/render mode. content=text
 
 
275 **
276 ** Optional query parameters:
277 **
278 ** render_mode=integer (AJAX_RENDER_xxx) (default=AJAX_RENDER_GUESS)
279 **
@@ -365,11 +367,11 @@
365 const AjaxRoute * rB = (const AjaxRoute*)b;
366 return fossil_strcmp(rA->zName, rB->zName);
367 }
368
369 /*
370 ** WEBPAGE: ajax
371 **
372 ** The main dispatcher for shared ajax-served routes. Requires the
373 ** 'name' parameter be the main route's name (as defined in a list in
374 ** this function), noting that fossil automatically assigns all path
375 ** parts after "ajax" to "name", e.g. /ajax/foo/bar assigns
376
--- src/ajax.c
+++ src/ajax.c
@@ -264,16 +264,18 @@
264 }
265 return rc;
266 }
267
268 /*
269 ** AJAX route /ajax/preview-text
270 **
271 ** Required query parameters:
272 **
273 ** filename=name of content, for use in determining the
274 ** mimetype/render mode.
275 **
276 ** content=text
277 **
278 ** Optional query parameters:
279 **
280 ** render_mode=integer (AJAX_RENDER_xxx) (default=AJAX_RENDER_GUESS)
281 **
@@ -365,11 +367,11 @@
367 const AjaxRoute * rB = (const AjaxRoute*)b;
368 return fossil_strcmp(rA->zName, rB->zName);
369 }
370
371 /*
372 ** WEBPAGE: ajax hidden
373 **
374 ** The main dispatcher for shared ajax-served routes. Requires the
375 ** 'name' parameter be the main route's name (as defined in a list in
376 ** this function), noting that fossil automatically assigns all path
377 ** parts after "ajax" to "name", e.g. /ajax/foo/bar assigns
378
+112 -76
--- src/chat.c
+++ src/chat.c
@@ -125,11 +125,11 @@
125125
*/
126126
/*
127127
** SETTING: chat-alert-sound width=10
128128
**
129129
** This is the name of the builtin sound file to use for the alert tone.
130
-** The value must be the name of one of a builtin WAV file.
130
+** The value must be the name of a builtin WAV file.
131131
*/
132132
/*
133133
** WEBPAGE: chat
134134
**
135135
** Start up a browser-based chat session.
@@ -154,19 +154,25 @@
154154
zProjectName = db_get("project-name","Unnamed project");
155155
style_set_current_feature("chat");
156156
style_header("Chat");
157157
@ <form accept-encoding="utf-8" id="chat-form" autocomplete="off">
158158
@ <div id='chat-input-area'>
159
- @ <div id='chat-input-line'>
159
+ @ <div id='chat-input-line' class='single-line'>
160160
@ <input type="text" name="msg" id="chat-input-single" \
161
- @ placeholder="Type message for %h(zProjectName)." autocomplete="off">
161
+ @ placeholder="Type markdown-formatted message for %h(zProjectName)." \
162
+ @ autocomplete="off">
162163
@ <textarea rows="8" id="chat-input-multi" \
163
- @ placeholder="Type message for %h(zProjectName). Ctrl-Enter sends it." \
164
+ @ placeholder="Type markdown-formatted message for %h(zProjectName). Ctrl-Enter sends it." \
164165
@ class="hidden"></textarea>
165
- @ <input type="submit" value="Send" id="chat-message-submit">
166
- @ <span id="chat-settings-button" class="settings-icon" \
167
- @ aria-label="Settings..." aria-haspopup="true" ></span>
166
+ @ <div id='chat-edit-buttons'>
167
+ @ <button id="chat-preview-button" \
168
+ @ title="Preview message">&#128065;</button>
169
+ @ <button id="chat-settings-button" \
170
+ @ title="Configure chat">&#9881;</button>
171
+ @ <button id="chat-message-submit" \
172
+ @ title="Send message">&#128228;</button>
173
+ @ </div>
168174
@ </div>
169175
@ <div id='chat-input-file-area'>
170176
@ <div class='file-selection-wrapper'>
171177
@ <div class='help-buttonlet'>
172178
@ Select a file to upload, drag/drop a file into this spot,
@@ -177,21 +183,27 @@
177183
@ </div>
178184
@ <div id="chat-drop-details"></div>
179185
@ </div>
180186
@ </div>
181187
@ </form>
188
+ @ <div id='chat-preview' class='hidden'>
189
+ @ <header>Preview:</header>
190
+ @ <div id='chat-preview-content' class='message-widget-content'></div>
191
+ @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
192
+ @ </div>
182193
@ <div id='chat-config' class='hidden'>
183194
@ <div id='chat-config-options'></div>
184195
/* ^^^populated client-side */
185
- @ <button>Close</button>
196
+ @ <button>Close Settings</button>
186197
@ </div>
187198
@ <div id='chat-messages-wrapper'>
188199
/* New chat messages get inserted immediately after this element */
189200
@ <span id='message-inject-point'></span>
190201
@ </div>
191202
fossil_free(zProjectName);
192
- builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", NULL);
203
+ builtin_fossil_js_bundle_or("popupwidget", "storage",
204
+ "fetch", "pikchr", NULL);
193205
/* Always in-line the javascript for the chat page */
194206
@ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
195207
/* We need an onload handler to ensure that window.fossil is
196208
initialized before the chat init code runs. */
197209
@ window.addEventListener('load', function(){
@@ -201,10 +213,11 @@
201213
@ fromcli: %h(PB("cli")?"true":"false"),
202214
@ alertSound: "%h(zAlert)",
203215
@ initSize: %d(db_get_int("chat-initial-history",50)),
204216
@ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
205217
@ };
218
+ ajax_emit_js_preview_modes(0);
206219
chat_emit_alert_list();
207220
cgi_append_content(builtin_text("chat.js"),-1);
208221
@ }, false);
209222
@ </script>
210223
@@ -294,11 +307,11 @@
294307
}
295308
fossil_free(zTime);
296309
}
297310
298311
/*
299
-** WEBPAGE: chat-send
312
+** WEBPAGE: chat-send hidden
300313
**
301314
** This page receives (via XHR) a new chat-message and/or a new file
302315
** to be entered into the chat history.
303316
**
304317
** On success it responds with an empty response: the new message
@@ -347,79 +360,25 @@
347360
db_commit_transaction();
348361
}
349362
350363
/*
351364
** This routine receives raw (user-entered) message text and transforms
352
-** it into HTML that is safe to insert using innerHTML.
353
-**
354
-** * HTML in the original text is escaped.
355
-**
356
-** * Hyperlinks are identified and tagged. Hyperlinks are:
357
-**
358
-** - Undelimited text of the form https:... or http:...
359
-** - Any text enclosed within [...]
365
+** it into HTML that is safe to insert using innerHTML. As of 2021-09-19,
366
+** it does so by using markdown_to_html() to convert markdown-formatted
367
+** zMsg to HTML.
360368
**
361369
** Space to hold the returned string is obtained from fossil_malloc()
362370
** and must be freed by the caller.
363371
*/
364372
static char *chat_format_to_html(const char *zMsg){
365
- char *zSafe = mprintf("%h", zMsg);
366
- int i, j, k;
367373
Blob out;
368
- char zClose[20];
369
- blob_init(&out, 0, 0);
370
- for(i=j=0; zSafe[i]; i++){
371
- if( zSafe[i]=='[' ){
372
- for(k=i+1; zSafe[k] && zSafe[k]!=']'; k++){}
373
- if( zSafe[k]==']' ){
374
- zSafe[k] = 0;
375
- if( j<i ){
376
- blob_append(&out, zSafe + j, i-j);
377
- j = i;
378
- }
379
- blob_append_char(&out, '[');
380
- wiki_resolve_hyperlink(&out,
381
- WIKI_NOBADLINKS|WIKI_TARGET_BLANK|WIKI_NOBRACKET,
382
- zSafe+i+1, zClose, sizeof(zClose), zSafe, 0);
383
- zSafe[k] = ']';
384
- j++;
385
- blob_append(&out, zSafe + j, k - j);
386
- blob_append(&out, zClose, -1);
387
- blob_append_char(&out, ']');
388
- i = k;
389
- j = k+1;
390
- continue;
391
- }
392
- }else if( zSafe[i]=='h'
393
- && (strncmp(zSafe+i,"http:",5)==0
394
- || strncmp(zSafe+i,"https:",6)==0) ){
395
- for(k=i+1; zSafe[k] && !fossil_isspace(zSafe[k]); k++){}
396
- if( k>i+7 ){
397
- char c = zSafe[k];
398
- if( !fossil_isalnum(zSafe[k-1]) && zSafe[k-1]!='/' ){
399
- k--;
400
- c = zSafe[k];
401
- }
402
- if( j<i ){
403
- blob_append(&out, zSafe + j, i-j);
404
- j = i;
405
- }
406
- zSafe[k] = 0;
407
- wiki_resolve_hyperlink(&out, WIKI_NOBADLINKS|WIKI_TARGET_BLANK,
408
- zSafe+i, zClose, sizeof(zClose), zSafe, 0);
409
- zSafe[k] = c;
410
- blob_append(&out, zSafe + j, k - j);
411
- blob_append(&out, zClose, -1);
412
- i = j = k;
413
- continue;
414
- }
415
- }
416
- }
417
- if( j<i ){
418
- blob_append(&out, zSafe+j, j-i);
419
- }
420
- fossil_free(zSafe);
374
+ blob_init(&out, "", 0);
375
+ if(*zMsg){
376
+ Blob bIn;
377
+ blob_init(&bIn, zMsg, (int)strlen(zMsg));
378
+ markdown_to_html(&bIn, NULL, &out);
379
+ }
421380
return blob_str(&out);
422381
}
423382
424383
/*
425384
** COMMAND: test-chat-formatter
@@ -442,11 +401,11 @@
442401
fossil_free(zOut);
443402
}
444403
}
445404
446405
/*
447
-** WEBPAGE: chat-poll
406
+** WEBPAGE: chat-poll hidden
448407
**
449408
** The chat page generated by /chat using an XHR to this page to
450409
** request new chat content. A typical invocation is:
451410
**
452411
** /chat-poll/N
@@ -651,11 +610,88 @@
651610
cgi_set_content(&json);
652611
return;
653612
}
654613
655614
/*
656
-** WEBPAGE: chat-download
615
+** WEBPAGE: chat-fetch-one hidden
616
+**
617
+** /chat-fetch-one/N
618
+**
619
+** Fetches a single message with the given ID, if available.
620
+**
621
+** Options:
622
+**
623
+** raw = the xmsg field will be returned unparsed.
624
+**
625
+** Response is either a single object in the format returned by
626
+** /chat-poll (without the wrapper array) or a JSON-format error
627
+** response, as documented for ajax_route_error().
628
+*/
629
+void chat_fetch_one(void){
630
+ Blob json = empty_blob; /* The json to be constructed and returned */
631
+ const int fRaw = PD("raw",0)!=0;
632
+ const int msgid = atoi(PD("name","0"));
633
+ Stmt q;
634
+ login_check_credentials();
635
+ if( !g.perm.Chat ) {
636
+ chat_emit_permissions_error(0);
637
+ return;
638
+ }
639
+ chat_create_tables();
640
+ cgi_set_content_type("application/json");
641
+ db_prepare(&q,
642
+ "SELECT datetime(mtime), xfrom, xmsg, length(file),"
643
+ " fname, fmime, lmtime"
644
+ " FROM chat WHERE msgid=%d AND mdel IS NULL",
645
+ msgid);
646
+ if(SQLITE_ROW==db_step(&q)){
647
+ const char *zDate = db_column_text(&q, 0);
648
+ const char *zFrom = db_column_text(&q, 1);
649
+ const char *zRawMsg = db_column_text(&q, 2);
650
+ const int nByte = db_column_int(&q, 3);
651
+ const char *zFName = db_column_text(&q, 4);
652
+ const char *zFMime = db_column_text(&q, 5);
653
+ const char *zLMtime = db_column_text(&q, 7);
654
+ blob_appendf(&json,"{\"msgid\": %d,", msgid);
655
+
656
+ blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
657
+ if( zLMtime && zLMtime[0] ){
658
+ blob_appendf(&json, "\"lmtime\":%!j,", zLMtime);
659
+ }
660
+ blob_append(&json, "\"xfrom\":", -1);
661
+ if(zFrom){
662
+ blob_appendf(&json, "%!j,", zFrom);
663
+ }else{
664
+ /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
665
+ blob_appendf(&json, "null,");
666
+ }
667
+ blob_appendf(&json, "\"uclr\":%!j,",
668
+ user_color(zFrom ? zFrom : "nobody"));
669
+ blob_append(&json,"\"xmsg\":", 7);
670
+ if(fRaw){
671
+ blob_appendf(&json, "%!j,", zRawMsg);
672
+ }else{
673
+ char * zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
674
+ blob_appendf(&json, "%!j,", zMsg);
675
+ fossil_free(zMsg);
676
+ }
677
+ if( nByte==0 ){
678
+ blob_appendf(&json, "\"fsize\":0");
679
+ }else{
680
+ blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
681
+ nByte, zFName, zFMime);
682
+ }
683
+ blob_append(&json,"}",1);
684
+ cgi_set_content(&json);
685
+ }else{
686
+ ajax_route_error(404,"Chat message #%d not found.", msgid);
687
+ }
688
+ db_finalize(&q);
689
+}
690
+
691
+/*
692
+** WEBPAGE: chat-download hidden
657693
**
658694
** Download the CHAT.FILE attachment associated with a single chat
659695
** entry. The "name" query parameter begins with an integer that
660696
** identifies the particular chat message. The integer may be followed
661697
** by a / and a filename, which will indicate to the browser to use
@@ -684,11 +720,11 @@
684720
cgi_set_content(&r);
685721
}
686722
687723
688724
/*
689
-** WEBPAGE: chat-delete
725
+** WEBPAGE: chat-delete hidden
690726
**
691727
** Delete the chat entry identified by the name query parameter.
692728
** Invoking fetch("chat-delete/"+msgid) from javascript in the client
693729
** will delete a chat entry from the CHAT table.
694730
**
695731
--- src/chat.c
+++ src/chat.c
@@ -125,11 +125,11 @@
125 */
126 /*
127 ** SETTING: chat-alert-sound width=10
128 **
129 ** This is the name of the builtin sound file to use for the alert tone.
130 ** The value must be the name of one of a builtin WAV file.
131 */
132 /*
133 ** WEBPAGE: chat
134 **
135 ** Start up a browser-based chat session.
@@ -154,19 +154,25 @@
154 zProjectName = db_get("project-name","Unnamed project");
155 style_set_current_feature("chat");
156 style_header("Chat");
157 @ <form accept-encoding="utf-8" id="chat-form" autocomplete="off">
158 @ <div id='chat-input-area'>
159 @ <div id='chat-input-line'>
160 @ <input type="text" name="msg" id="chat-input-single" \
161 @ placeholder="Type message for %h(zProjectName)." autocomplete="off">
 
162 @ <textarea rows="8" id="chat-input-multi" \
163 @ placeholder="Type message for %h(zProjectName). Ctrl-Enter sends it." \
164 @ class="hidden"></textarea>
165 @ <input type="submit" value="Send" id="chat-message-submit">
166 @ <span id="chat-settings-button" class="settings-icon" \
167 @ aria-label="Settings..." aria-haspopup="true" ></span>
 
 
 
 
 
168 @ </div>
169 @ <div id='chat-input-file-area'>
170 @ <div class='file-selection-wrapper'>
171 @ <div class='help-buttonlet'>
172 @ Select a file to upload, drag/drop a file into this spot,
@@ -177,21 +183,27 @@
177 @ </div>
178 @ <div id="chat-drop-details"></div>
179 @ </div>
180 @ </div>
181 @ </form>
 
 
 
 
 
182 @ <div id='chat-config' class='hidden'>
183 @ <div id='chat-config-options'></div>
184 /* ^^^populated client-side */
185 @ <button>Close</button>
186 @ </div>
187 @ <div id='chat-messages-wrapper'>
188 /* New chat messages get inserted immediately after this element */
189 @ <span id='message-inject-point'></span>
190 @ </div>
191 fossil_free(zProjectName);
192 builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", NULL);
 
193 /* Always in-line the javascript for the chat page */
194 @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
195 /* We need an onload handler to ensure that window.fossil is
196 initialized before the chat init code runs. */
197 @ window.addEventListener('load', function(){
@@ -201,10 +213,11 @@
201 @ fromcli: %h(PB("cli")?"true":"false"),
202 @ alertSound: "%h(zAlert)",
203 @ initSize: %d(db_get_int("chat-initial-history",50)),
204 @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
205 @ };
 
206 chat_emit_alert_list();
207 cgi_append_content(builtin_text("chat.js"),-1);
208 @ }, false);
209 @ </script>
210
@@ -294,11 +307,11 @@
294 }
295 fossil_free(zTime);
296 }
297
298 /*
299 ** WEBPAGE: chat-send
300 **
301 ** This page receives (via XHR) a new chat-message and/or a new file
302 ** to be entered into the chat history.
303 **
304 ** On success it responds with an empty response: the new message
@@ -347,79 +360,25 @@
347 db_commit_transaction();
348 }
349
350 /*
351 ** This routine receives raw (user-entered) message text and transforms
352 ** it into HTML that is safe to insert using innerHTML.
353 **
354 ** * HTML in the original text is escaped.
355 **
356 ** * Hyperlinks are identified and tagged. Hyperlinks are:
357 **
358 ** - Undelimited text of the form https:... or http:...
359 ** - Any text enclosed within [...]
360 **
361 ** Space to hold the returned string is obtained from fossil_malloc()
362 ** and must be freed by the caller.
363 */
364 static char *chat_format_to_html(const char *zMsg){
365 char *zSafe = mprintf("%h", zMsg);
366 int i, j, k;
367 Blob out;
368 char zClose[20];
369 blob_init(&out, 0, 0);
370 for(i=j=0; zSafe[i]; i++){
371 if( zSafe[i]=='[' ){
372 for(k=i+1; zSafe[k] && zSafe[k]!=']'; k++){}
373 if( zSafe[k]==']' ){
374 zSafe[k] = 0;
375 if( j<i ){
376 blob_append(&out, zSafe + j, i-j);
377 j = i;
378 }
379 blob_append_char(&out, '[');
380 wiki_resolve_hyperlink(&out,
381 WIKI_NOBADLINKS|WIKI_TARGET_BLANK|WIKI_NOBRACKET,
382 zSafe+i+1, zClose, sizeof(zClose), zSafe, 0);
383 zSafe[k] = ']';
384 j++;
385 blob_append(&out, zSafe + j, k - j);
386 blob_append(&out, zClose, -1);
387 blob_append_char(&out, ']');
388 i = k;
389 j = k+1;
390 continue;
391 }
392 }else if( zSafe[i]=='h'
393 && (strncmp(zSafe+i,"http:",5)==0
394 || strncmp(zSafe+i,"https:",6)==0) ){
395 for(k=i+1; zSafe[k] && !fossil_isspace(zSafe[k]); k++){}
396 if( k>i+7 ){
397 char c = zSafe[k];
398 if( !fossil_isalnum(zSafe[k-1]) && zSafe[k-1]!='/' ){
399 k--;
400 c = zSafe[k];
401 }
402 if( j<i ){
403 blob_append(&out, zSafe + j, i-j);
404 j = i;
405 }
406 zSafe[k] = 0;
407 wiki_resolve_hyperlink(&out, WIKI_NOBADLINKS|WIKI_TARGET_BLANK,
408 zSafe+i, zClose, sizeof(zClose), zSafe, 0);
409 zSafe[k] = c;
410 blob_append(&out, zSafe + j, k - j);
411 blob_append(&out, zClose, -1);
412 i = j = k;
413 continue;
414 }
415 }
416 }
417 if( j<i ){
418 blob_append(&out, zSafe+j, j-i);
419 }
420 fossil_free(zSafe);
421 return blob_str(&out);
422 }
423
424 /*
425 ** COMMAND: test-chat-formatter
@@ -442,11 +401,11 @@
442 fossil_free(zOut);
443 }
444 }
445
446 /*
447 ** WEBPAGE: chat-poll
448 **
449 ** The chat page generated by /chat using an XHR to this page to
450 ** request new chat content. A typical invocation is:
451 **
452 ** /chat-poll/N
@@ -651,11 +610,88 @@
651 cgi_set_content(&json);
652 return;
653 }
654
655 /*
656 ** WEBPAGE: chat-download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657 **
658 ** Download the CHAT.FILE attachment associated with a single chat
659 ** entry. The "name" query parameter begins with an integer that
660 ** identifies the particular chat message. The integer may be followed
661 ** by a / and a filename, which will indicate to the browser to use
@@ -684,11 +720,11 @@
684 cgi_set_content(&r);
685 }
686
687
688 /*
689 ** WEBPAGE: chat-delete
690 **
691 ** Delete the chat entry identified by the name query parameter.
692 ** Invoking fetch("chat-delete/"+msgid) from javascript in the client
693 ** will delete a chat entry from the CHAT table.
694 **
695
--- src/chat.c
+++ src/chat.c
@@ -125,11 +125,11 @@
125 */
126 /*
127 ** SETTING: chat-alert-sound width=10
128 **
129 ** This is the name of the builtin sound file to use for the alert tone.
130 ** The value must be the name of a builtin WAV file.
131 */
132 /*
133 ** WEBPAGE: chat
134 **
135 ** Start up a browser-based chat session.
@@ -154,19 +154,25 @@
154 zProjectName = db_get("project-name","Unnamed project");
155 style_set_current_feature("chat");
156 style_header("Chat");
157 @ <form accept-encoding="utf-8" id="chat-form" autocomplete="off">
158 @ <div id='chat-input-area'>
159 @ <div id='chat-input-line' class='single-line'>
160 @ <input type="text" name="msg" id="chat-input-single" \
161 @ placeholder="Type markdown-formatted message for %h(zProjectName)." \
162 @ autocomplete="off">
163 @ <textarea rows="8" id="chat-input-multi" \
164 @ placeholder="Type markdown-formatted message for %h(zProjectName). Ctrl-Enter sends it." \
165 @ class="hidden"></textarea>
166 @ <div id='chat-edit-buttons'>
167 @ <button id="chat-preview-button" \
168 @ title="Preview message">&#128065;</button>
169 @ <button id="chat-settings-button" \
170 @ title="Configure chat">&#9881;</button>
171 @ <button id="chat-message-submit" \
172 @ title="Send message">&#128228;</button>
173 @ </div>
174 @ </div>
175 @ <div id='chat-input-file-area'>
176 @ <div class='file-selection-wrapper'>
177 @ <div class='help-buttonlet'>
178 @ Select a file to upload, drag/drop a file into this spot,
@@ -177,21 +183,27 @@
183 @ </div>
184 @ <div id="chat-drop-details"></div>
185 @ </div>
186 @ </div>
187 @ </form>
188 @ <div id='chat-preview' class='hidden'>
189 @ <header>Preview:</header>
190 @ <div id='chat-preview-content' class='message-widget-content'></div>
191 @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div>
192 @ </div>
193 @ <div id='chat-config' class='hidden'>
194 @ <div id='chat-config-options'></div>
195 /* ^^^populated client-side */
196 @ <button>Close Settings</button>
197 @ </div>
198 @ <div id='chat-messages-wrapper'>
199 /* New chat messages get inserted immediately after this element */
200 @ <span id='message-inject-point'></span>
201 @ </div>
202 fossil_free(zProjectName);
203 builtin_fossil_js_bundle_or("popupwidget", "storage",
204 "fetch", "pikchr", NULL);
205 /* Always in-line the javascript for the chat page */
206 @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */
207 /* We need an onload handler to ensure that window.fossil is
208 initialized before the chat init code runs. */
209 @ window.addEventListener('load', function(){
@@ -201,10 +213,11 @@
213 @ fromcli: %h(PB("cli")?"true":"false"),
214 @ alertSound: "%h(zAlert)",
215 @ initSize: %d(db_get_int("chat-initial-history",50)),
216 @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
217 @ };
218 ajax_emit_js_preview_modes(0);
219 chat_emit_alert_list();
220 cgi_append_content(builtin_text("chat.js"),-1);
221 @ }, false);
222 @ </script>
223
@@ -294,11 +307,11 @@
307 }
308 fossil_free(zTime);
309 }
310
311 /*
312 ** WEBPAGE: chat-send hidden
313 **
314 ** This page receives (via XHR) a new chat-message and/or a new file
315 ** to be entered into the chat history.
316 **
317 ** On success it responds with an empty response: the new message
@@ -347,79 +360,25 @@
360 db_commit_transaction();
361 }
362
363 /*
364 ** This routine receives raw (user-entered) message text and transforms
365 ** it into HTML that is safe to insert using innerHTML. As of 2021-09-19,
366 ** it does so by using markdown_to_html() to convert markdown-formatted
367 ** zMsg to HTML.
 
 
 
 
 
368 **
369 ** Space to hold the returned string is obtained from fossil_malloc()
370 ** and must be freed by the caller.
371 */
372 static char *chat_format_to_html(const char *zMsg){
 
 
373 Blob out;
374 blob_init(&out, "", 0);
375 if(*zMsg){
376 Blob bIn;
377 blob_init(&bIn, zMsg, (int)strlen(zMsg));
378 markdown_to_html(&bIn, NULL, &out);
379 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380 return blob_str(&out);
381 }
382
383 /*
384 ** COMMAND: test-chat-formatter
@@ -442,11 +401,11 @@
401 fossil_free(zOut);
402 }
403 }
404
405 /*
406 ** WEBPAGE: chat-poll hidden
407 **
408 ** The chat page generated by /chat using an XHR to this page to
409 ** request new chat content. A typical invocation is:
410 **
411 ** /chat-poll/N
@@ -651,11 +610,88 @@
610 cgi_set_content(&json);
611 return;
612 }
613
614 /*
615 ** WEBPAGE: chat-fetch-one hidden
616 **
617 ** /chat-fetch-one/N
618 **
619 ** Fetches a single message with the given ID, if available.
620 **
621 ** Options:
622 **
623 ** raw = the xmsg field will be returned unparsed.
624 **
625 ** Response is either a single object in the format returned by
626 ** /chat-poll (without the wrapper array) or a JSON-format error
627 ** response, as documented for ajax_route_error().
628 */
629 void chat_fetch_one(void){
630 Blob json = empty_blob; /* The json to be constructed and returned */
631 const int fRaw = PD("raw",0)!=0;
632 const int msgid = atoi(PD("name","0"));
633 Stmt q;
634 login_check_credentials();
635 if( !g.perm.Chat ) {
636 chat_emit_permissions_error(0);
637 return;
638 }
639 chat_create_tables();
640 cgi_set_content_type("application/json");
641 db_prepare(&q,
642 "SELECT datetime(mtime), xfrom, xmsg, length(file),"
643 " fname, fmime, lmtime"
644 " FROM chat WHERE msgid=%d AND mdel IS NULL",
645 msgid);
646 if(SQLITE_ROW==db_step(&q)){
647 const char *zDate = db_column_text(&q, 0);
648 const char *zFrom = db_column_text(&q, 1);
649 const char *zRawMsg = db_column_text(&q, 2);
650 const int nByte = db_column_int(&q, 3);
651 const char *zFName = db_column_text(&q, 4);
652 const char *zFMime = db_column_text(&q, 5);
653 const char *zLMtime = db_column_text(&q, 7);
654 blob_appendf(&json,"{\"msgid\": %d,", msgid);
655
656 blob_appendf(&json, "\"mtime\":\"%.10sT%sZ\",", zDate, zDate+11);
657 if( zLMtime && zLMtime[0] ){
658 blob_appendf(&json, "\"lmtime\":%!j,", zLMtime);
659 }
660 blob_append(&json, "\"xfrom\":", -1);
661 if(zFrom){
662 blob_appendf(&json, "%!j,", zFrom);
663 }else{
664 /* see https://fossil-scm.org/forum/forumpost/e0be0eeb4c */
665 blob_appendf(&json, "null,");
666 }
667 blob_appendf(&json, "\"uclr\":%!j,",
668 user_color(zFrom ? zFrom : "nobody"));
669 blob_append(&json,"\"xmsg\":", 7);
670 if(fRaw){
671 blob_appendf(&json, "%!j,", zRawMsg);
672 }else{
673 char * zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
674 blob_appendf(&json, "%!j,", zMsg);
675 fossil_free(zMsg);
676 }
677 if( nByte==0 ){
678 blob_appendf(&json, "\"fsize\":0");
679 }else{
680 blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j",
681 nByte, zFName, zFMime);
682 }
683 blob_append(&json,"}",1);
684 cgi_set_content(&json);
685 }else{
686 ajax_route_error(404,"Chat message #%d not found.", msgid);
687 }
688 db_finalize(&q);
689 }
690
691 /*
692 ** WEBPAGE: chat-download hidden
693 **
694 ** Download the CHAT.FILE attachment associated with a single chat
695 ** entry. The "name" query parameter begins with an integer that
696 ** identifies the particular chat message. The integer may be followed
697 ** by a / and a filename, which will indicate to the browser to use
@@ -684,11 +720,11 @@
720 cgi_set_content(&r);
721 }
722
723
724 /*
725 ** WEBPAGE: chat-delete hidden
726 **
727 ** Delete the chat entry identified by the name query parameter.
728 ** Invoking fetch("chat-delete/"+msgid) from javascript in the client
729 ** will delete a chat entry from the CHAT table.
730 **
731
+156 -36
--- src/chat.js
+++ src/chat.js
@@ -101,20 +101,24 @@
101101
e:{/*map of certain DOM elements.*/
102102
messageInjectPoint: E1('#message-inject-point'),
103103
pageTitle: E1('head title'),
104104
loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
105105
inputWrapper: E1("#chat-input-area"),
106
+ inputLine: E1('#chat-input-line'),
106107
fileSelectWrapper: E1('#chat-input-file-area'),
107108
messagesWrapper: E1('#chat-messages-wrapper'),
108109
inputForm: E1('#chat-form'),
109110
btnSubmit: E1('#chat-message-submit'),
110111
inputSingle: E1('#chat-input-single'),
111112
inputMulti: E1('#chat-input-multi'),
112113
inputCurrent: undefined/*one of inputSingle or inputMulti*/,
113114
inputFile: E1('#chat-input-file'),
114115
contentDiv: E1('div.content'),
115
- configArea: E1('#chat-config')
116
+ configArea: E1('#chat-config'),
117
+ previewArea: E1('#chat-preview'),
118
+ previewContent: E1('#chat-preview-content'),
119
+ btnPreview: E1('#chat-preview-button')
116120
},
117121
me: F.user.name,
118122
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
119123
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
120124
pageIsActive: 'visible'===document.visibilityState,
@@ -147,12 +151,14 @@
147151
/** Toggles between single- and multi-line edit modes. Returns this. */
148152
inputToggleSingleMulti: function(){
149153
const old = this.e.inputCurrent;
150154
if(this.e.inputCurrent === this.e.inputSingle){
151155
this.e.inputCurrent = this.e.inputMulti;
156
+ this.e.inputLine.classList.remove('single-line');
152157
}else{
153158
this.e.inputCurrent = this.e.inputSingle;
159
+ this.e.inputLine.classList.add('single-line');
154160
}
155161
const m = this.e.messagesWrapper,
156162
sTop = m.scrollTop,
157163
mh1 = m.clientHeight;
158164
D.addClass(old, 'hidden');
@@ -485,10 +491,67 @@
485491
F.toast.message("Deleted message "+id+".");
486492
}
487493
return !!e;
488494
};
489495
496
+ /**
497
+ Toggles the given message between its parsed and plain-text
498
+ representations. It requires a server round-trip to collect the
499
+ plain-text form but caches it for subsequent toggles.
500
+
501
+ Expects the ID of a currently-loaded message or a
502
+ message-widget DOM elment from which it can extract an id.
503
+ This is an aync operation the first time it's passed a given
504
+ message and synchronous on subsequent calls for that
505
+ message. It is a no-op if id does not resolve to a loaded
506
+ message.
507
+ */
508
+ cs.toggleTextMode = function(id){
509
+ var e;
510
+ if(id instanceof HTMLElement){
511
+ e = id;
512
+ id = e.dataset.msgid;
513
+ }else{
514
+ e = this.getMessageElemById(id);
515
+ }
516
+ if(!e || !id) return false;
517
+ else if(e.$isToggling) return;
518
+ e.$isToggling = true;
519
+ const content = e.querySelector('.message-widget-content');
520
+ if(!content.$elems){
521
+ content.$elems = [
522
+ content.firstElementChild, // parsed elem
523
+ undefined // plaintext elem
524
+ ];
525
+ }else if(content.$elems[1]){
526
+ // We have both content types. Simply toggle them.
527
+ const child = (
528
+ content.firstElementChild===content.$elems[0]
529
+ ? content.$elems[1]
530
+ : content.$elems[0]
531
+ );
532
+ delete e.$isToggling;
533
+ D.append(D.clearElement(content), child);
534
+ return;
535
+ }
536
+ // We need to fetch the plain-text version...
537
+ const self = this;
538
+ F.fetch('chat-fetch-one',{
539
+ urlParams:{ name: id, raw: true},
540
+ responseType: 'json',
541
+ onload: function(msg){
542
+ content.$elems[1] = D.append(D.pre(),msg.xmsg);
543
+ self.toggleTextMode(e);
544
+ },
545
+ aftersend:function(){
546
+ delete e.$isToggling;
547
+ Chat.ajaxEnd();
548
+ }
549
+ });
550
+ return true;
551
+ };
552
+
490553
/** Given a .message-row element, this function returns whethe the
491554
current user may, at least hypothetically, delete the message
492555
globally. A user may always delete a local copy of a
493556
post. The server may trump this, e.g. if the login has been
494557
cancelled after this page was loaded.
@@ -681,10 +744,13 @@
681744
if(m.xmsg instanceof Array){
682745
// Used by Chat.reportErrorAsMessage()
683746
D.append(contentTarget, m.xmsg);
684747
}else{
685748
contentTarget.innerHTML = m.xmsg;
749
+ if(F.pikchr){
750
+ F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
751
+ }
686752
}
687753
}
688754
this.e.tab.addEventListener('click', this._handleLegendClicked, false);
689755
if(eXFrom){
690756
eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
@@ -739,14 +805,20 @@
739805
btnDeleteGlobal.addEventListener('click', function(){
740806
self.hide();
741807
Chat.deleteMessage(eMsg);
742808
});
743809
}
810
+ const toolbar2 = D.addClass(D.div(), 'toolbar');
811
+ D.append(this.e, toolbar2);
812
+ const btnToggleText = D.button("Toggle text mode");
813
+ btnToggleText.addEventListener('click', function(){
814
+ self.hide();
815
+ Chat.toggleTextMode(eMsg);
816
+ });
817
+ D.append(toolbar2, btnToggleText);
744818
if(eMsg.dataset.xfrom){
745819
/* Add a link to the /timeline filtered on this user. */
746
- const toolbar2 = D.addClass(D.div(), 'toolbar');
747
- D.append(this.e, toolbar2);
748820
const timelineLink = D.attr(
749821
D.a(F.repoUrl('timeline',{
750822
u: eMsg.dataset.xfrom,
751823
y: 'a'
752824
}), "User's Timeline"),
@@ -881,10 +953,11 @@
881953
*/
882954
Chat.submitMessage = function f(){
883955
if(!f.spaces){
884956
f.spaces = /\s+$/;
885957
}
958
+ this.revealPreview(false);
886959
const fd = new FormData(this.e.inputForm)
887960
/* ^^^^ we don't really want/need the FORM element, but when
888961
FormData() is default-constructed here then the server
889962
segfaults, and i have no clue why! */;
890963
var msg = this.inputValue().trim();
@@ -952,12 +1025,21 @@
9521025
};
9531026
9541027
(function(){/*Set up #chat-settings-button */
9551028
const settingsButton = document.querySelector('#chat-settings-button');
9561029
const optionsMenu = E1('#chat-config-options');
957
- const cbToggle = function(){
958
- D.toggleClass([Chat.e.messagesWrapper, Chat.e.configArea], 'hidden');
1030
+ const cbToggle = function(ev){
1031
+ ev.preventDefault();
1032
+ ev.stopPropagation();
1033
+ if(Chat.e.configArea.classList.contains('hidden')){
1034
+ D.removeClass(Chat.e.configArea, 'hidden');
1035
+ D.addClass([Chat.e.messagesWrapper, Chat.e.previewArea], 'hidden');
1036
+ }else{
1037
+ D.addClass(Chat.e.configArea, 'hidden');
1038
+ D.removeClass(Chat.e.messagesWrapper, 'hidden');
1039
+ }
1040
+ return false;
9591041
};
9601042
D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
9611043
Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false);
9621044
/* Settings menu entries... */
9631045
const settingsOps = [{
@@ -995,11 +1077,11 @@
9951077
F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
9961078
}
9971079
}];
9981080
9991081
/** Set up selection list of notification sounds. */
1000
- if(false/*flip this to false to enable selection of audio files*/){
1082
+ if(1){
10011083
settingsOps.push({
10021084
label: "Audible alerts",
10031085
boolValue: ()=>Chat.settings.getBool('audible-alert'),
10041086
callback: function(){
10051087
const v = Chat.settings.toggle('audible-alert');
@@ -1009,40 +1091,10 @@
10091091
}
10101092
});
10111093
Chat.setNewMessageSound(
10121094
Chat.settings.getBool('audible-alert') ? F.config.chat.alertSound : false
10131095
);
1014
- }else{
1015
- /* Disabled per chatroom discussion: selection list of audio files for
1016
- chat notification. */
1017
- settingsOps.selectSound = D.addClass(D.div(), 'menu-entry');
1018
- const selectSound = D.select();
1019
- D.append(settingsOps.selectSound,
1020
- D.append(D.span(),"Audio alert"),
1021
- selectSound);
1022
- D.option(selectSound, "", "(no audio)");
1023
- const firstSoundIndex = selectSound.options.length;
1024
- F.config.chat.alerts.forEach(function(a){
1025
- D.option(selectSound, a);
1026
- });
1027
- if(true===Chat.settings.getBool('audible-alert')){
1028
- selectSound.selectedIndex = firstSoundIndex;
1029
- }else{
1030
- selectSound.value = Chat.settings.get('audible-alert','');
1031
- if(selectSound.selectedIndex<0){
1032
- /*Missing file - removed after this setting was applied. Fall back
1033
- to the first sound in the list. */
1034
- selectSound.selectedIndex = firstSoundIndex;
1035
- }
1036
- }
1037
- selectSound.addEventListener('change',function(){
1038
- const v = this.value;
1039
- Chat.setNewMessageSound(v);
1040
- F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1041
- if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1042
- }, false);
1043
- Chat.setNewMessageSound(selectSound.value);
10441096
}/*audio notification config*/
10451097
/**
10461098
Build list of options...
10471099
*/
10481100
settingsOps.forEach(function f(op){
@@ -1076,10 +1128,78 @@
10761128
if(settingsOps.selectSound){
10771129
D.append(optionsMenu, settingsOps.selectSound);
10781130
}
10791131
//settingsButton.click()/*for for development*/;
10801132
})()/*#chat-settings-button setup*/;
1133
+
1134
+ (function(){/*set up message preview*/
1135
+ const btnPreview = Chat.e.btnPreview;
1136
+ Chat.setPreviewText = function(t){
1137
+ this.revealPreview(true).e.previewContent.innerHTML = t;
1138
+ };
1139
+ /**
1140
+ Reveals preview area if showIt is true, else hides it.
1141
+ This also shows/hides other elements, "as appropriate."
1142
+ */
1143
+ Chat.revealPreview = function(showIt){
1144
+ if(showIt){
1145
+ D.removeClass(Chat.e.previewArea, 'hidden');
1146
+ D.addClass([Chat.e.messagesWrapper, Chat.e.configArea],
1147
+ 'hidden');
1148
+ }else{
1149
+ D.addClass(Chat.e.previewArea, 'hidden');
1150
+ D.removeClass(Chat.e.messagesWrapper, 'hidden');
1151
+ }
1152
+ return this;
1153
+ };
1154
+ Chat.e.previewArea.querySelector('#chat-preview-close').
1155
+ addEventListener('click', ()=>Chat.revealPreview(false), false);
1156
+ let previewPending = false;
1157
+ const elemsToEnable = [
1158
+ btnPreview, Chat.e.btnSubmit,
1159
+ Chat.e.inputSingle, Chat.e.inputMulti];
1160
+ Chat.disableDuringAjax.push(btnPreview);
1161
+ const submit = function(ev){
1162
+ ev.preventDefault();
1163
+ ev.stopPropagation();
1164
+ if(previewPending) return false;
1165
+ const txt = Chat.e.inputCurrent.value;
1166
+ if(!txt){
1167
+ Chat.setPreviewText('');
1168
+ previewPending = false;
1169
+ return false;
1170
+ }
1171
+ const fd = new FormData();
1172
+ fd.append('content', txt);
1173
+ fd.append('filename','chat.md'
1174
+ /*filename needed for mimetype determination*/);
1175
+ fd.append('render_mode',F.page.previewModes.wiki);
1176
+ F.fetch('ajax/preview-text',{
1177
+ payload: fd,
1178
+ onload: (html)=>Chat.setPreviewText(html),
1179
+ onerror: function(e){
1180
+ F.fetch.onerror(e);
1181
+ Chat.setPreviewText("ERROR: "+(
1182
+ e.message || 'Unknown error fetching preview!'
1183
+ ));
1184
+ },
1185
+ beforesend: function(){
1186
+ D.disable(elemsToEnable);
1187
+ Chat.ajaxStart();
1188
+ previewPending = true;
1189
+ Chat.setPreviewText("Loading preview...");
1190
+ },
1191
+ aftersend:function(){
1192
+ previewPending = false;
1193
+ D.enable(elemsToEnable);
1194
+ Chat.ajaxEnd();
1195
+ }
1196
+ });
1197
+ return false;
1198
+ };
1199
+ btnPreview.addEventListener('click', submit, false);
1200
+ })()/*message preview setup*/;
10811201
10821202
/** Callback for poll() to inject new content into the page. jx ==
10831203
the response from /chat-poll. If atEnd is true, the message is
10841204
appended to the end of the chat list (for loading older
10851205
messages), else the beginning (the default). */
10861206
--- src/chat.js
+++ src/chat.js
@@ -101,20 +101,24 @@
101 e:{/*map of certain DOM elements.*/
102 messageInjectPoint: E1('#message-inject-point'),
103 pageTitle: E1('head title'),
104 loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
105 inputWrapper: E1("#chat-input-area"),
 
106 fileSelectWrapper: E1('#chat-input-file-area'),
107 messagesWrapper: E1('#chat-messages-wrapper'),
108 inputForm: E1('#chat-form'),
109 btnSubmit: E1('#chat-message-submit'),
110 inputSingle: E1('#chat-input-single'),
111 inputMulti: E1('#chat-input-multi'),
112 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
113 inputFile: E1('#chat-input-file'),
114 contentDiv: E1('div.content'),
115 configArea: E1('#chat-config')
 
 
 
116 },
117 me: F.user.name,
118 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
119 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
120 pageIsActive: 'visible'===document.visibilityState,
@@ -147,12 +151,14 @@
147 /** Toggles between single- and multi-line edit modes. Returns this. */
148 inputToggleSingleMulti: function(){
149 const old = this.e.inputCurrent;
150 if(this.e.inputCurrent === this.e.inputSingle){
151 this.e.inputCurrent = this.e.inputMulti;
 
152 }else{
153 this.e.inputCurrent = this.e.inputSingle;
 
154 }
155 const m = this.e.messagesWrapper,
156 sTop = m.scrollTop,
157 mh1 = m.clientHeight;
158 D.addClass(old, 'hidden');
@@ -485,10 +491,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.
@@ -681,10 +744,13 @@
681 if(m.xmsg instanceof Array){
682 // Used by Chat.reportErrorAsMessage()
683 D.append(contentTarget, m.xmsg);
684 }else{
685 contentTarget.innerHTML = m.xmsg;
 
 
 
686 }
687 }
688 this.e.tab.addEventListener('click', this._handleLegendClicked, false);
689 if(eXFrom){
690 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
@@ -739,14 +805,20 @@
739 btnDeleteGlobal.addEventListener('click', function(){
740 self.hide();
741 Chat.deleteMessage(eMsg);
742 });
743 }
 
 
 
 
 
 
 
 
744 if(eMsg.dataset.xfrom){
745 /* Add a link to the /timeline filtered on this user. */
746 const toolbar2 = D.addClass(D.div(), 'toolbar');
747 D.append(this.e, toolbar2);
748 const timelineLink = D.attr(
749 D.a(F.repoUrl('timeline',{
750 u: eMsg.dataset.xfrom,
751 y: 'a'
752 }), "User's Timeline"),
@@ -881,10 +953,11 @@
881 */
882 Chat.submitMessage = function f(){
883 if(!f.spaces){
884 f.spaces = /\s+$/;
885 }
 
886 const fd = new FormData(this.e.inputForm)
887 /* ^^^^ we don't really want/need the FORM element, but when
888 FormData() is default-constructed here then the server
889 segfaults, and i have no clue why! */;
890 var msg = this.inputValue().trim();
@@ -952,12 +1025,21 @@
952 };
953
954 (function(){/*Set up #chat-settings-button */
955 const settingsButton = document.querySelector('#chat-settings-button');
956 const optionsMenu = E1('#chat-config-options');
957 const cbToggle = function(){
958 D.toggleClass([Chat.e.messagesWrapper, Chat.e.configArea], 'hidden');
 
 
 
 
 
 
 
 
 
959 };
960 D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
961 Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false);
962 /* Settings menu entries... */
963 const settingsOps = [{
@@ -995,11 +1077,11 @@
995 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
996 }
997 }];
998
999 /** Set up selection list of notification sounds. */
1000 if(false/*flip this to false to enable selection of audio files*/){
1001 settingsOps.push({
1002 label: "Audible alerts",
1003 boolValue: ()=>Chat.settings.getBool('audible-alert'),
1004 callback: function(){
1005 const v = Chat.settings.toggle('audible-alert');
@@ -1009,40 +1091,10 @@
1009 }
1010 });
1011 Chat.setNewMessageSound(
1012 Chat.settings.getBool('audible-alert') ? F.config.chat.alertSound : false
1013 );
1014 }else{
1015 /* Disabled per chatroom discussion: selection list of audio files for
1016 chat notification. */
1017 settingsOps.selectSound = D.addClass(D.div(), 'menu-entry');
1018 const selectSound = D.select();
1019 D.append(settingsOps.selectSound,
1020 D.append(D.span(),"Audio alert"),
1021 selectSound);
1022 D.option(selectSound, "", "(no audio)");
1023 const firstSoundIndex = selectSound.options.length;
1024 F.config.chat.alerts.forEach(function(a){
1025 D.option(selectSound, a);
1026 });
1027 if(true===Chat.settings.getBool('audible-alert')){
1028 selectSound.selectedIndex = firstSoundIndex;
1029 }else{
1030 selectSound.value = Chat.settings.get('audible-alert','');
1031 if(selectSound.selectedIndex<0){
1032 /*Missing file - removed after this setting was applied. Fall back
1033 to the first sound in the list. */
1034 selectSound.selectedIndex = firstSoundIndex;
1035 }
1036 }
1037 selectSound.addEventListener('change',function(){
1038 const v = this.value;
1039 Chat.setNewMessageSound(v);
1040 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1041 if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1042 }, false);
1043 Chat.setNewMessageSound(selectSound.value);
1044 }/*audio notification config*/
1045 /**
1046 Build list of options...
1047 */
1048 settingsOps.forEach(function f(op){
@@ -1076,10 +1128,78 @@
1076 if(settingsOps.selectSound){
1077 D.append(optionsMenu, settingsOps.selectSound);
1078 }
1079 //settingsButton.click()/*for for development*/;
1080 })()/*#chat-settings-button setup*/;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1081
1082 /** Callback for poll() to inject new content into the page. jx ==
1083 the response from /chat-poll. If atEnd is true, the message is
1084 appended to the end of the chat list (for loading older
1085 messages), else the beginning (the default). */
1086
--- src/chat.js
+++ src/chat.js
@@ -101,20 +101,24 @@
101 e:{/*map of certain DOM elements.*/
102 messageInjectPoint: E1('#message-inject-point'),
103 pageTitle: E1('head title'),
104 loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
105 inputWrapper: E1("#chat-input-area"),
106 inputLine: E1('#chat-input-line'),
107 fileSelectWrapper: E1('#chat-input-file-area'),
108 messagesWrapper: E1('#chat-messages-wrapper'),
109 inputForm: E1('#chat-form'),
110 btnSubmit: E1('#chat-message-submit'),
111 inputSingle: E1('#chat-input-single'),
112 inputMulti: E1('#chat-input-multi'),
113 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
114 inputFile: E1('#chat-input-file'),
115 contentDiv: E1('div.content'),
116 configArea: E1('#chat-config'),
117 previewArea: E1('#chat-preview'),
118 previewContent: E1('#chat-preview-content'),
119 btnPreview: E1('#chat-preview-button')
120 },
121 me: F.user.name,
122 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
123 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
124 pageIsActive: 'visible'===document.visibilityState,
@@ -147,12 +151,14 @@
151 /** Toggles between single- and multi-line edit modes. Returns this. */
152 inputToggleSingleMulti: function(){
153 const old = this.e.inputCurrent;
154 if(this.e.inputCurrent === this.e.inputSingle){
155 this.e.inputCurrent = this.e.inputMulti;
156 this.e.inputLine.classList.remove('single-line');
157 }else{
158 this.e.inputCurrent = this.e.inputSingle;
159 this.e.inputLine.classList.add('single-line');
160 }
161 const m = this.e.messagesWrapper,
162 sTop = m.scrollTop,
163 mh1 = m.clientHeight;
164 D.addClass(old, 'hidden');
@@ -485,10 +491,67 @@
491 F.toast.message("Deleted message "+id+".");
492 }
493 return !!e;
494 };
495
496 /**
497 Toggles the given message between its parsed and plain-text
498 representations. It requires a server round-trip to collect the
499 plain-text form but caches it for subsequent toggles.
500
501 Expects the ID of a currently-loaded message or a
502 message-widget DOM elment from which it can extract an id.
503 This is an aync operation the first time it's passed a given
504 message and synchronous on subsequent calls for that
505 message. It is a no-op if id does not resolve to a loaded
506 message.
507 */
508 cs.toggleTextMode = function(id){
509 var e;
510 if(id instanceof HTMLElement){
511 e = id;
512 id = e.dataset.msgid;
513 }else{
514 e = this.getMessageElemById(id);
515 }
516 if(!e || !id) return false;
517 else if(e.$isToggling) return;
518 e.$isToggling = true;
519 const content = e.querySelector('.message-widget-content');
520 if(!content.$elems){
521 content.$elems = [
522 content.firstElementChild, // parsed elem
523 undefined // plaintext elem
524 ];
525 }else if(content.$elems[1]){
526 // We have both content types. Simply toggle them.
527 const child = (
528 content.firstElementChild===content.$elems[0]
529 ? content.$elems[1]
530 : content.$elems[0]
531 );
532 delete e.$isToggling;
533 D.append(D.clearElement(content), child);
534 return;
535 }
536 // We need to fetch the plain-text version...
537 const self = this;
538 F.fetch('chat-fetch-one',{
539 urlParams:{ name: id, raw: true},
540 responseType: 'json',
541 onload: function(msg){
542 content.$elems[1] = D.append(D.pre(),msg.xmsg);
543 self.toggleTextMode(e);
544 },
545 aftersend:function(){
546 delete e.$isToggling;
547 Chat.ajaxEnd();
548 }
549 });
550 return true;
551 };
552
553 /** Given a .message-row element, this function returns whethe the
554 current user may, at least hypothetically, delete the message
555 globally. A user may always delete a local copy of a
556 post. The server may trump this, e.g. if the login has been
557 cancelled after this page was loaded.
@@ -681,10 +744,13 @@
744 if(m.xmsg instanceof Array){
745 // Used by Chat.reportErrorAsMessage()
746 D.append(contentTarget, m.xmsg);
747 }else{
748 contentTarget.innerHTML = m.xmsg;
749 if(F.pikchr){
750 F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr'));
751 }
752 }
753 }
754 this.e.tab.addEventListener('click', this._handleLegendClicked, false);
755 if(eXFrom){
756 eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
@@ -739,14 +805,20 @@
805 btnDeleteGlobal.addEventListener('click', function(){
806 self.hide();
807 Chat.deleteMessage(eMsg);
808 });
809 }
810 const toolbar2 = D.addClass(D.div(), 'toolbar');
811 D.append(this.e, toolbar2);
812 const btnToggleText = D.button("Toggle text mode");
813 btnToggleText.addEventListener('click', function(){
814 self.hide();
815 Chat.toggleTextMode(eMsg);
816 });
817 D.append(toolbar2, btnToggleText);
818 if(eMsg.dataset.xfrom){
819 /* Add a link to the /timeline filtered on this user. */
 
 
820 const timelineLink = D.attr(
821 D.a(F.repoUrl('timeline',{
822 u: eMsg.dataset.xfrom,
823 y: 'a'
824 }), "User's Timeline"),
@@ -881,10 +953,11 @@
953 */
954 Chat.submitMessage = function f(){
955 if(!f.spaces){
956 f.spaces = /\s+$/;
957 }
958 this.revealPreview(false);
959 const fd = new FormData(this.e.inputForm)
960 /* ^^^^ we don't really want/need the FORM element, but when
961 FormData() is default-constructed here then the server
962 segfaults, and i have no clue why! */;
963 var msg = this.inputValue().trim();
@@ -952,12 +1025,21 @@
1025 };
1026
1027 (function(){/*Set up #chat-settings-button */
1028 const settingsButton = document.querySelector('#chat-settings-button');
1029 const optionsMenu = E1('#chat-config-options');
1030 const cbToggle = function(ev){
1031 ev.preventDefault();
1032 ev.stopPropagation();
1033 if(Chat.e.configArea.classList.contains('hidden')){
1034 D.removeClass(Chat.e.configArea, 'hidden');
1035 D.addClass([Chat.e.messagesWrapper, Chat.e.previewArea], 'hidden');
1036 }else{
1037 D.addClass(Chat.e.configArea, 'hidden');
1038 D.removeClass(Chat.e.messagesWrapper, 'hidden');
1039 }
1040 return false;
1041 };
1042 D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
1043 Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false);
1044 /* Settings menu entries... */
1045 const settingsOps = [{
@@ -995,11 +1077,11 @@
1077 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1078 }
1079 }];
1080
1081 /** Set up selection list of notification sounds. */
1082 if(1){
1083 settingsOps.push({
1084 label: "Audible alerts",
1085 boolValue: ()=>Chat.settings.getBool('audible-alert'),
1086 callback: function(){
1087 const v = Chat.settings.toggle('audible-alert');
@@ -1009,40 +1091,10 @@
1091 }
1092 });
1093 Chat.setNewMessageSound(
1094 Chat.settings.getBool('audible-alert') ? F.config.chat.alertSound : false
1095 );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096 }/*audio notification config*/
1097 /**
1098 Build list of options...
1099 */
1100 settingsOps.forEach(function f(op){
@@ -1076,10 +1128,78 @@
1128 if(settingsOps.selectSound){
1129 D.append(optionsMenu, settingsOps.selectSound);
1130 }
1131 //settingsButton.click()/*for for development*/;
1132 })()/*#chat-settings-button setup*/;
1133
1134 (function(){/*set up message preview*/
1135 const btnPreview = Chat.e.btnPreview;
1136 Chat.setPreviewText = function(t){
1137 this.revealPreview(true).e.previewContent.innerHTML = t;
1138 };
1139 /**
1140 Reveals preview area if showIt is true, else hides it.
1141 This also shows/hides other elements, "as appropriate."
1142 */
1143 Chat.revealPreview = function(showIt){
1144 if(showIt){
1145 D.removeClass(Chat.e.previewArea, 'hidden');
1146 D.addClass([Chat.e.messagesWrapper, Chat.e.configArea],
1147 'hidden');
1148 }else{
1149 D.addClass(Chat.e.previewArea, 'hidden');
1150 D.removeClass(Chat.e.messagesWrapper, 'hidden');
1151 }
1152 return this;
1153 };
1154 Chat.e.previewArea.querySelector('#chat-preview-close').
1155 addEventListener('click', ()=>Chat.revealPreview(false), false);
1156 let previewPending = false;
1157 const elemsToEnable = [
1158 btnPreview, Chat.e.btnSubmit,
1159 Chat.e.inputSingle, Chat.e.inputMulti];
1160 Chat.disableDuringAjax.push(btnPreview);
1161 const submit = function(ev){
1162 ev.preventDefault();
1163 ev.stopPropagation();
1164 if(previewPending) return false;
1165 const txt = Chat.e.inputCurrent.value;
1166 if(!txt){
1167 Chat.setPreviewText('');
1168 previewPending = false;
1169 return false;
1170 }
1171 const fd = new FormData();
1172 fd.append('content', txt);
1173 fd.append('filename','chat.md'
1174 /*filename needed for mimetype determination*/);
1175 fd.append('render_mode',F.page.previewModes.wiki);
1176 F.fetch('ajax/preview-text',{
1177 payload: fd,
1178 onload: (html)=>Chat.setPreviewText(html),
1179 onerror: function(e){
1180 F.fetch.onerror(e);
1181 Chat.setPreviewText("ERROR: "+(
1182 e.message || 'Unknown error fetching preview!'
1183 ));
1184 },
1185 beforesend: function(){
1186 D.disable(elemsToEnable);
1187 Chat.ajaxStart();
1188 previewPending = true;
1189 Chat.setPreviewText("Loading preview...");
1190 },
1191 aftersend:function(){
1192 previewPending = false;
1193 D.enable(elemsToEnable);
1194 Chat.ajaxEnd();
1195 }
1196 });
1197 return false;
1198 };
1199 btnPreview.addEventListener('click', submit, false);
1200 })()/*message preview setup*/;
1201
1202 /** Callback for poll() to inject new content into the page. jx ==
1203 the response from /chat-poll. If atEnd is true, the message is
1204 appended to the end of the chat list (for loading older
1205 messages), else the beginning (the default). */
1206
+1 -299
--- src/default.css
+++ src/default.css
@@ -1596,115 +1596,10 @@
15961596
opacity: 0 !important;
15971597
pointer-events: none !important;
15981598
display: none !important;
15991599
}
16001600
1601
-/* Chat-related */
1602
-body.chat span.at-name { /* for @USERNAME references */
1603
- text-decoration: underline;
1604
- font-weight: bold;
1605
-}
1606
-/* A wrapper for a single single chat message (one row of the UI) */
1607
-body.chat .message-widget {
1608
- margin-bottom: 0.75em;
1609
- border: none;
1610
- display: flex;
1611
- flex-direction: column;
1612
- border: none;
1613
- align-items: flex-start;
1614
-}
1615
-body.chat.my-messages-right .message-widget.mine {
1616
- /* Right-aligns a user's own chat messages, similar to how
1617
- most mobile messaging apps do it. */
1618
- align-items: flex-end;
1619
-}
1620
-body.chat.my-messages-right .message-widget.notification {
1621
- /* Center-aligns a system-level notification message. */
1622
- align-items: center;
1623
-}
1624
-/* The content area of a message. */
1625
-body.chat .message-widget-content {
1626
- display: inline-block;
1627
- border-radius: 0.25em;
1628
- border: 1px solid rgba(0,0,0,0.2);
1629
- box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
1630
- padding: 0.25em 0.5em;
1631
- margin-top: 0;
1632
- min-width: 9em /*avoid unsightly "underlap" with the neighboring
1633
- .message-widget-tab element*/;
1634
- white-space: pre-wrap/*needed for multi-line edits*/;
1635
-}
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
-/* User name and timestamp (a LEGEND-like element) */
1642
-body.chat .message-widget .message-widget-tab {
1643
- border-radius: 0.25em 0.25em 0 0;
1644
- margin: 0 0.25em 0em 0.15em;
1645
- padding: 0 0.5em 0.15em 0.5em;
1646
- cursor: pointer;
1647
- white-space: nowrap;
1648
-}
1649
-body.chat .fossil-tooltip.help-buttonlet-content {
1650
- font-size: 80%;
1651
-}
1652
-body.chat .message-widget .message-widget-tab .xfrom {
1653
- /* Element which holds the "this message is from user X" part
1654
- of the message banner. */
1655
- font-style: italic;
1656
- font-weight: bold;
1657
-}
1658
-/* The popup element for displaying message timestamps
1659
- and deletion controls. */
1660
-body.chat .chat-message-popup {
1661
- font-family: monospace;
1662
- font-size: 0.8em;
1663
- text-align: left;
1664
- display: flex;
1665
- flex-direction: column;
1666
- align-items: stretch;
1667
- padding: 0.25em;
1668
- z-index: 200;
1669
-}
1670
-/* Full message timestamps. */
1671
-body.chat .chat-message-popup > span { white-space: nowrap; }
1672
-/* Container for the message deletion buttons. */
1673
-body.chat .chat-message-popup > .toolbar {
1674
- padding: 0.2em;
1675
- margin: 0;
1676
- border: 2px inset rgba(0,0,0,0.3);
1677
- border-radius: 0.25em;
1678
- display: flex;
1679
- flex-direction: row;
1680
- justify-content: stretch;
1681
- flex-wrap: wrap;
1682
-}
1683
-body.chat .chat-message-popup > .toolbar > button {
1684
- flex: 1 1 auto;
1685
-}
1686
-/* The widget for loading more/older chat messages. */
1687
-body.chat #load-msg-toolbar {
1688
- border-radius: 0.25em;
1689
- padding: 0.1em 0.2em;
1690
- margin-bottom: 1em;
1691
-}
1692
-/* .all-done is set when chat has loaded all of the available
1693
- historical messages */
1694
-body.chat #load-msg-toolbar.all-done {
1695
- opacity: 0.5;
1696
-}
1697
-body.chat #load-msg-toolbar > div {
1698
- display: flex;
1699
- flex-direction: row;
1700
- justify-content: stretch;
1701
- flex-wrap: wrap;
1702
-}
1703
-body.chat #load-msg-toolbar > div > button {
1704
- flex: 1 1 auto;
1705
-}
17061601
17071602
/* An icon element intended for use as a button/menu for
17081603
accessing app-specific settings. */
17091604
.settings-icon {
17101605
/* Icon source: https://de.wikipedia.org/wiki/Datei:OOjs_UI_icon_settings.svg
@@ -1734,204 +1629,11 @@
17341629
border: 1px outset rgba(127,127,127,1);
17351630
}
17361631
body.fossil-dark-style .settings-icon {
17371632
filter: invert(100%);
17381633
}
1739
-/* "Chat-only mode" hides the site header/footer, showing only
1740
- the chat app. */
1741
-body.chat.chat-only-mode{}
1742
-body.chat #chat-settings-button {}
1743
-/** Popup widget for the /chat settings. */
1744
-body.chat .chat-settings-popup {
1745
- font-size: 0.8em;
1746
- text-align: left;
1747
- display: flex;
1748
- flex-direction: column;
1749
- align-items: stretch;
1750
- padding: 0.25em;
1751
- z-index: 200;
1752
-}
1753
-body.chat .chat-settings-popup > span {
1754
- vertical-align: middle;
1755
-}
1756
-body.chat .chat-settings-popup > span.menu-entry{
1757
- white-space: nowrap;
1758
- cursor: pointer;
1759
- border: 1px solid;
1760
- border-radius: 0.25em;
1761
- padding: 0.25em 0.5em;
1762
- display: flex;
1763
- flex-direction: row;
1764
- align-items: center;
1765
- justify-content: space-between;
1766
-}
1767
-body.chat .chat-settings-popup > span.menu-entry:hover {
1768
-}
1769
-body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
1770
- vertical-align: middle;
1771
-}
1772
-body.chat .chat-settings-popup > span.menu-entry > span.button {
1773
- margin: 0.25em 0.2em;
1774
- padding: 0.25em;
1775
- flex: 1 1 auto/*eliminates dead no-click zones on the right*/;
1776
-}
1777
-body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] {
1778
- cursor: inherit;
1779
-}
1780
-body.chat .chat-settings-popup > select.menu-entry {
1781
- flex: 1 1 auto;
1782
- padding: 0;
1783
- cursor: pointer;
1784
- min-height: 2.5em;
1785
- border-radius: 0.25em;
1786
-}
1787
-body.chat .chat-settings-popup > select.menu-entry > option {
1788
- /*Recall that many browsers don't allow styling of OPTION
1789
- elements, or allow only very limited styling.*/
1790
-}
1791
-
1792
-/** Container for the list of /chat messages. */
1793
-body.chat #chat-messages-wrapper {
1794
- overflow: auto;
1795
- flex: 2 1 auto;
1796
- padding: 0 0.25em;
1797
-}
1798
-body.chat #chat-messages-wrapper.loading > * {
1799
- /* An attempt at reducing flicker when loading lots of messages. */
1800
- visibility: hidden;
1801
-}
1802
-body.chat div.content {
1803
- margin: 0;
1804
- padding: 0;
1805
- display: flex;
1806
- flex-direction: column-reverse;
1807
- /* ^^^^ In order to get good automatic scrolling of new messages on
1808
- the BOTTOM in bottom-up chat mode, such that they scroll up
1809
- instead of down, we have to use column-reverse layout, which
1810
- changes #chat-messages-wrapper's "gravity" for purposes of
1811
- scrolling! If we instead use flex-direction:column then each new
1812
- message pushes #chat-input-area down further off the screen!
1813
- */
1814
- align-items: stretch;
1815
-}
1816
-/* Wrapper for /chat user input controls */
1817
-body.chat #chat-input-area {
1818
- display: flex;
1819
- flex-direction: column;
1820
- padding: 0.5em 1em;
1821
- border-bottom: none;
1822
- border-top: 1px solid black;
1823
- margin: 0.5em 1em 0 1em;
1824
- position: initial /*sticky currently disabled due to scrolling-related issues*/;
1825
- bottom: 0;
1826
-}
1827
-body.chat:not(.chat-only-mode) #chat-input-area{
1828
- /* Safari user reports that 2em is necessary to keep the file selection
1829
- widget from overlapping the page footer, whereas a margin of 0 is fine
1830
- for FF/Chrome (and 2em is a *huge* waste of space for those). */
1831
- margin-bottom: 0;
1832
-}
1833
-
1834
-/* Widget holding the chat message input field, send button, and
1835
- settings button. */
1836
-body.chat #chat-input-line {
1837
- display: flex;
1838
- flex-direction: row;
1839
- margin-bottom: 0.25em;
1840
- align-items: self-start;
1841
-}
1842
-body.chat #chat-input-line > input[type=submit],
1843
-body.chat #chat-input-line > #chat-settings-button,
1844
-body.chat #chat-input-line > button {
1845
- flex: 1 5 auto;
1846
- max-width: 6em;
1847
- margin: 0 0.25em;
1848
-}
1849
-body.chat #chat-input-line > button {
1850
- max-width: 4em;
1851
-}
1852
-body.chat #chat-input-line > #chat-settings-button{
1853
- margin: 0 0 0 0.25em;
1854
- max-width: 2em;
1855
-}
1856
-body.chat #chat-input-line > input[type=text],
1857
-body.chat #chat-input-line > textarea {
1858
- flex: 5 1 auto;
1859
-}
1860
-/* Widget holding the file selection control and preview */
1861
-body.chat #chat-input-file-area {
1862
- display: flex;
1863
- flex-direction: row;
1864
- align-items: center;
1865
- flex-wrap: wrap;
1866
-}
1867
-body.chat #chat-input-file-area > .file-selection-wrapper {
1868
- align-self: flex-start;
1869
- margin-right: 0.5em;
1870
- flex: 0 1 auto;
1871
- padding: 0.25em 0.25em 0.25em 0;
1872
-}
1873
-body.chat #chat-input-file-area .file-selection-wrapper > * {
1874
- vertical-align: middle;
1875
- margin: 0;
1876
-}
1877
-body.chat #chat-input-file {
1878
- border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
1879
- border-radius: 0.25em;
1880
- padding: 0.25em;
1881
-}
1882
-body.chat #chat-input-file > input {
1883
- flex: 1 0 auto;
1884
-}
1885
-/* Indicator when a drag/drop is in progress */
1886
-body.chat #chat-input-file.dragover {
1887
- border: 1px dashed green;
1888
-}
1889
-/* Widget holding the details of a selected/dropped file/image. */
1890
-body.chat #chat-drop-details {
1891
- flex: 0 1 auto;
1892
- padding: 0.5em 1em;
1893
- margin-left: 0.5em;
1894
- white-space: pre;
1895
- font-family: monospace;
1896
-}
1897
-
1898
-body.chat #chat-drop-details img {
1899
- max-width: 45%;
1900
- max-height: 45%;
1901
-}
1902
-body.chat #chat-config {
1903
- /* /chat configuration widget */
1904
- display: flex;
1905
- flex-direction: column;
1906
- flex: 1 0 auto;
1907
- overflow: auto;
1908
- flex: 2 1 auto;
1909
- padding: 0 0.25em;
1910
-}
1911
-body.chat #chat-config > button {
1912
- padding: 0.5em;
1913
- flex: 0 1 auto;
1914
- margin: 0.25em 0;
1915
-}
1916
-body.chat #chat-config #chat-config-options {
1917
- /* /chat config options go here */
1918
- flex: 1 1 auto;
1919
- display: flex;
1920
- flex-direction: column;
1921
- overflow: auto;
1922
-}
1923
-body.chat #chat-config #chat-config-options .menu-entry {
1924
- display: flex;
1925
- align-items: center;
1926
- flex-direction: row;
1927
- flex-wrap: wrap;
1928
- padding: 1em;
1929
-}
1930
-body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
1931
- margin-right: 1em;
1932
-}
1634
+
19331635
input[type="checkbox"].diff-toggle {
19341636
float: right;
19351637
}
19361638
19371639
body.branch .brlist > table > tbody > tr:hover:not(.selected),
19381640
--- src/default.css
+++ src/default.css
@@ -1596,115 +1596,10 @@
1596 opacity: 0 !important;
1597 pointer-events: none !important;
1598 display: none !important;
1599 }
1600
1601 /* Chat-related */
1602 body.chat span.at-name { /* for @USERNAME references */
1603 text-decoration: underline;
1604 font-weight: bold;
1605 }
1606 /* A wrapper for a single single chat message (one row of the UI) */
1607 body.chat .message-widget {
1608 margin-bottom: 0.75em;
1609 border: none;
1610 display: flex;
1611 flex-direction: column;
1612 border: none;
1613 align-items: flex-start;
1614 }
1615 body.chat.my-messages-right .message-widget.mine {
1616 /* Right-aligns a user's own chat messages, similar to how
1617 most mobile messaging apps do it. */
1618 align-items: flex-end;
1619 }
1620 body.chat.my-messages-right .message-widget.notification {
1621 /* Center-aligns a system-level notification message. */
1622 align-items: center;
1623 }
1624 /* The content area of a message. */
1625 body.chat .message-widget-content {
1626 display: inline-block;
1627 border-radius: 0.25em;
1628 border: 1px solid rgba(0,0,0,0.2);
1629 box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
1630 padding: 0.25em 0.5em;
1631 margin-top: 0;
1632 min-width: 9em /*avoid unsightly "underlap" with the neighboring
1633 .message-widget-tab element*/;
1634 white-space: pre-wrap/*needed for multi-line edits*/;
1635 }
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 /* User name and timestamp (a LEGEND-like element) */
1642 body.chat .message-widget .message-widget-tab {
1643 border-radius: 0.25em 0.25em 0 0;
1644 margin: 0 0.25em 0em 0.15em;
1645 padding: 0 0.5em 0.15em 0.5em;
1646 cursor: pointer;
1647 white-space: nowrap;
1648 }
1649 body.chat .fossil-tooltip.help-buttonlet-content {
1650 font-size: 80%;
1651 }
1652 body.chat .message-widget .message-widget-tab .xfrom {
1653 /* Element which holds the "this message is from user X" part
1654 of the message banner. */
1655 font-style: italic;
1656 font-weight: bold;
1657 }
1658 /* The popup element for displaying message timestamps
1659 and deletion controls. */
1660 body.chat .chat-message-popup {
1661 font-family: monospace;
1662 font-size: 0.8em;
1663 text-align: left;
1664 display: flex;
1665 flex-direction: column;
1666 align-items: stretch;
1667 padding: 0.25em;
1668 z-index: 200;
1669 }
1670 /* Full message timestamps. */
1671 body.chat .chat-message-popup > span { white-space: nowrap; }
1672 /* Container for the message deletion buttons. */
1673 body.chat .chat-message-popup > .toolbar {
1674 padding: 0.2em;
1675 margin: 0;
1676 border: 2px inset rgba(0,0,0,0.3);
1677 border-radius: 0.25em;
1678 display: flex;
1679 flex-direction: row;
1680 justify-content: stretch;
1681 flex-wrap: wrap;
1682 }
1683 body.chat .chat-message-popup > .toolbar > button {
1684 flex: 1 1 auto;
1685 }
1686 /* The widget for loading more/older chat messages. */
1687 body.chat #load-msg-toolbar {
1688 border-radius: 0.25em;
1689 padding: 0.1em 0.2em;
1690 margin-bottom: 1em;
1691 }
1692 /* .all-done is set when chat has loaded all of the available
1693 historical messages */
1694 body.chat #load-msg-toolbar.all-done {
1695 opacity: 0.5;
1696 }
1697 body.chat #load-msg-toolbar > div {
1698 display: flex;
1699 flex-direction: row;
1700 justify-content: stretch;
1701 flex-wrap: wrap;
1702 }
1703 body.chat #load-msg-toolbar > div > button {
1704 flex: 1 1 auto;
1705 }
1706
1707 /* An icon element intended for use as a button/menu for
1708 accessing app-specific settings. */
1709 .settings-icon {
1710 /* Icon source: https://de.wikipedia.org/wiki/Datei:OOjs_UI_icon_settings.svg
@@ -1734,204 +1629,11 @@
1734 border: 1px outset rgba(127,127,127,1);
1735 }
1736 body.fossil-dark-style .settings-icon {
1737 filter: invert(100%);
1738 }
1739 /* "Chat-only mode" hides the site header/footer, showing only
1740 the chat app. */
1741 body.chat.chat-only-mode{}
1742 body.chat #chat-settings-button {}
1743 /** Popup widget for the /chat settings. */
1744 body.chat .chat-settings-popup {
1745 font-size: 0.8em;
1746 text-align: left;
1747 display: flex;
1748 flex-direction: column;
1749 align-items: stretch;
1750 padding: 0.25em;
1751 z-index: 200;
1752 }
1753 body.chat .chat-settings-popup > span {
1754 vertical-align: middle;
1755 }
1756 body.chat .chat-settings-popup > span.menu-entry{
1757 white-space: nowrap;
1758 cursor: pointer;
1759 border: 1px solid;
1760 border-radius: 0.25em;
1761 padding: 0.25em 0.5em;
1762 display: flex;
1763 flex-direction: row;
1764 align-items: center;
1765 justify-content: space-between;
1766 }
1767 body.chat .chat-settings-popup > span.menu-entry:hover {
1768 }
1769 body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet {
1770 vertical-align: middle;
1771 }
1772 body.chat .chat-settings-popup > span.menu-entry > span.button {
1773 margin: 0.25em 0.2em;
1774 padding: 0.25em;
1775 flex: 1 1 auto/*eliminates dead no-click zones on the right*/;
1776 }
1777 body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] {
1778 cursor: inherit;
1779 }
1780 body.chat .chat-settings-popup > select.menu-entry {
1781 flex: 1 1 auto;
1782 padding: 0;
1783 cursor: pointer;
1784 min-height: 2.5em;
1785 border-radius: 0.25em;
1786 }
1787 body.chat .chat-settings-popup > select.menu-entry > option {
1788 /*Recall that many browsers don't allow styling of OPTION
1789 elements, or allow only very limited styling.*/
1790 }
1791
1792 /** Container for the list of /chat messages. */
1793 body.chat #chat-messages-wrapper {
1794 overflow: auto;
1795 flex: 2 1 auto;
1796 padding: 0 0.25em;
1797 }
1798 body.chat #chat-messages-wrapper.loading > * {
1799 /* An attempt at reducing flicker when loading lots of messages. */
1800 visibility: hidden;
1801 }
1802 body.chat div.content {
1803 margin: 0;
1804 padding: 0;
1805 display: flex;
1806 flex-direction: column-reverse;
1807 /* ^^^^ In order to get good automatic scrolling of new messages on
1808 the BOTTOM in bottom-up chat mode, such that they scroll up
1809 instead of down, we have to use column-reverse layout, which
1810 changes #chat-messages-wrapper's "gravity" for purposes of
1811 scrolling! If we instead use flex-direction:column then each new
1812 message pushes #chat-input-area down further off the screen!
1813 */
1814 align-items: stretch;
1815 }
1816 /* Wrapper for /chat user input controls */
1817 body.chat #chat-input-area {
1818 display: flex;
1819 flex-direction: column;
1820 padding: 0.5em 1em;
1821 border-bottom: none;
1822 border-top: 1px solid black;
1823 margin: 0.5em 1em 0 1em;
1824 position: initial /*sticky currently disabled due to scrolling-related issues*/;
1825 bottom: 0;
1826 }
1827 body.chat:not(.chat-only-mode) #chat-input-area{
1828 /* Safari user reports that 2em is necessary to keep the file selection
1829 widget from overlapping the page footer, whereas a margin of 0 is fine
1830 for FF/Chrome (and 2em is a *huge* waste of space for those). */
1831 margin-bottom: 0;
1832 }
1833
1834 /* Widget holding the chat message input field, send button, and
1835 settings button. */
1836 body.chat #chat-input-line {
1837 display: flex;
1838 flex-direction: row;
1839 margin-bottom: 0.25em;
1840 align-items: self-start;
1841 }
1842 body.chat #chat-input-line > input[type=submit],
1843 body.chat #chat-input-line > #chat-settings-button,
1844 body.chat #chat-input-line > button {
1845 flex: 1 5 auto;
1846 max-width: 6em;
1847 margin: 0 0.25em;
1848 }
1849 body.chat #chat-input-line > button {
1850 max-width: 4em;
1851 }
1852 body.chat #chat-input-line > #chat-settings-button{
1853 margin: 0 0 0 0.25em;
1854 max-width: 2em;
1855 }
1856 body.chat #chat-input-line > input[type=text],
1857 body.chat #chat-input-line > textarea {
1858 flex: 5 1 auto;
1859 }
1860 /* Widget holding the file selection control and preview */
1861 body.chat #chat-input-file-area {
1862 display: flex;
1863 flex-direction: row;
1864 align-items: center;
1865 flex-wrap: wrap;
1866 }
1867 body.chat #chat-input-file-area > .file-selection-wrapper {
1868 align-self: flex-start;
1869 margin-right: 0.5em;
1870 flex: 0 1 auto;
1871 padding: 0.25em 0.25em 0.25em 0;
1872 }
1873 body.chat #chat-input-file-area .file-selection-wrapper > * {
1874 vertical-align: middle;
1875 margin: 0;
1876 }
1877 body.chat #chat-input-file {
1878 border:1px solid rgba(0,0,0,0);/*avoid UI shift during drop-targeting*/
1879 border-radius: 0.25em;
1880 padding: 0.25em;
1881 }
1882 body.chat #chat-input-file > input {
1883 flex: 1 0 auto;
1884 }
1885 /* Indicator when a drag/drop is in progress */
1886 body.chat #chat-input-file.dragover {
1887 border: 1px dashed green;
1888 }
1889 /* Widget holding the details of a selected/dropped file/image. */
1890 body.chat #chat-drop-details {
1891 flex: 0 1 auto;
1892 padding: 0.5em 1em;
1893 margin-left: 0.5em;
1894 white-space: pre;
1895 font-family: monospace;
1896 }
1897
1898 body.chat #chat-drop-details img {
1899 max-width: 45%;
1900 max-height: 45%;
1901 }
1902 body.chat #chat-config {
1903 /* /chat configuration widget */
1904 display: flex;
1905 flex-direction: column;
1906 flex: 1 0 auto;
1907 overflow: auto;
1908 flex: 2 1 auto;
1909 padding: 0 0.25em;
1910 }
1911 body.chat #chat-config > button {
1912 padding: 0.5em;
1913 flex: 0 1 auto;
1914 margin: 0.25em 0;
1915 }
1916 body.chat #chat-config #chat-config-options {
1917 /* /chat config options go here */
1918 flex: 1 1 auto;
1919 display: flex;
1920 flex-direction: column;
1921 overflow: auto;
1922 }
1923 body.chat #chat-config #chat-config-options .menu-entry {
1924 display: flex;
1925 align-items: center;
1926 flex-direction: row;
1927 flex-wrap: wrap;
1928 padding: 1em;
1929 }
1930 body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
1931 margin-right: 1em;
1932 }
1933 input[type="checkbox"].diff-toggle {
1934 float: right;
1935 }
1936
1937 body.branch .brlist > table > tbody > tr:hover:not(.selected),
1938
--- src/default.css
+++ src/default.css
@@ -1596,115 +1596,10 @@
1596 opacity: 0 !important;
1597 pointer-events: none !important;
1598 display: none !important;
1599 }
1600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1601
1602 /* An icon element intended for use as a button/menu for
1603 accessing app-specific settings. */
1604 .settings-icon {
1605 /* Icon source: https://de.wikipedia.org/wiki/Datei:OOjs_UI_icon_settings.svg
@@ -1734,204 +1629,11 @@
1629 border: 1px outset rgba(127,127,127,1);
1630 }
1631 body.fossil-dark-style .settings-icon {
1632 filter: invert(100%);
1633 }
1634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1635 input[type="checkbox"].diff-toggle {
1636 float: right;
1637 }
1638
1639 body.branch .brlist > table > tbody > tr:hover:not(.selected),
1640
--- src/main.mk
+++ src/main.mk
@@ -265,10 +265,11 @@
265265
$(SRCDIR)/sounds/c.wav \
266266
$(SRCDIR)/sounds/d.wav \
267267
$(SRCDIR)/sounds/e.wav \
268268
$(SRCDIR)/sounds/f.wav \
269269
$(SRCDIR)/style.admin_log.css \
270
+ $(SRCDIR)/style.chat.css \
270271
$(SRCDIR)/style.fileedit.css \
271272
$(SRCDIR)/style.wikiedit.css \
272273
$(SRCDIR)/tree.js \
273274
$(SRCDIR)/useredit.js \
274275
$(SRCDIR)/wiki.wiki
275276
--- src/main.mk
+++ src/main.mk
@@ -265,10 +265,11 @@
265 $(SRCDIR)/sounds/c.wav \
266 $(SRCDIR)/sounds/d.wav \
267 $(SRCDIR)/sounds/e.wav \
268 $(SRCDIR)/sounds/f.wav \
269 $(SRCDIR)/style.admin_log.css \
 
270 $(SRCDIR)/style.fileedit.css \
271 $(SRCDIR)/style.wikiedit.css \
272 $(SRCDIR)/tree.js \
273 $(SRCDIR)/useredit.js \
274 $(SRCDIR)/wiki.wiki
275
--- src/main.mk
+++ src/main.mk
@@ -265,10 +265,11 @@
265 $(SRCDIR)/sounds/c.wav \
266 $(SRCDIR)/sounds/d.wav \
267 $(SRCDIR)/sounds/e.wav \
268 $(SRCDIR)/sounds/f.wav \
269 $(SRCDIR)/style.admin_log.css \
270 $(SRCDIR)/style.chat.css \
271 $(SRCDIR)/style.fileedit.css \
272 $(SRCDIR)/style.wikiedit.css \
273 $(SRCDIR)/tree.js \
274 $(SRCDIR)/useredit.js \
275 $(SRCDIR)/wiki.wiki
276
--- src/markdown.c
+++ src/markdown.c
@@ -1665,10 +1665,11 @@
16651665
16661666
if( !size || data[0]!='#' ) return 0;
16671667
16681668
while( level<size && level<6 && data[level]=='#' ){ level++; }
16691669
for(i=level; i<size && (data[i]==' ' || data[i]=='\t'); i++);
1670
+ if ( i == level ) return parse_paragraph(ob, rndr, data, size);
16701671
span_beg = i;
16711672
16721673
for(end=i; end<size && data[end]!='\n'; end++);
16731674
skip = end;
16741675
if( end<=i ) return parse_paragraph(ob, rndr, data, size);
16751676
16761677
ADDED src/style.chat.css
--- src/markdown.c
+++ src/markdown.c
@@ -1665,10 +1665,11 @@
1665
1666 if( !size || data[0]!='#' ) return 0;
1667
1668 while( level<size && level<6 && data[level]=='#' ){ level++; }
1669 for(i=level; i<size && (data[i]==' ' || data[i]=='\t'); i++);
 
1670 span_beg = i;
1671
1672 for(end=i; end<size && data[end]!='\n'; end++);
1673 skip = end;
1674 if( end<=i ) return parse_paragraph(ob, rndr, data, size);
1675
1676 DDED src/style.chat.css
--- src/markdown.c
+++ src/markdown.c
@@ -1665,10 +1665,11 @@
1665
1666 if( !size || data[0]!='#' ) return 0;
1667
1668 while( level<size && level<6 && data[level]=='#' ){ level++; }
1669 for(i=level; i<size && (data[i]==' ' || data[i]=='\t'); i++);
1670 if ( i == level ) return parse_paragraph(ob, rndr, data, size);
1671 span_beg = i;
1672
1673 for(end=i; end<size && data[end]!='\n'; end++);
1674 skip = end;
1675 if( end<=i ) return parse_paragraph(ob, rndr, data, size);
1676
1677 DDED src/style.chat.css
--- a/src/style.chat.css
+++ b/src/style.chat.css
@@ -0,0 +1,9 @@
1
+em;
2
+ font-size: 85% 2;
3
+ ser-list2.5 2,0.55.25em 0. 2.5 2,0em 0. 2.5 2,0.55.25em 0edit-buttons {user-list2.5 2,0.55.25em 0. 2.5 2,0.55.25em 0.5em;
4
+ font-size: 85% 2;
5
+ opacity: 0.6.2em 0.5em 0.2em 0
6
+ (0deg);-::before {
7
+ /*cont "Rec 1 5 auto;*/
8
+ max-width: 6emn@1eT,3V@1gI,1:2g@1jo,4:16emQ@1zj,2R@1mo,O@1iG,VB@1q_,aZZoL;524em;
9
+}
--- a/src/style.chat.css
+++ b/src/style.chat.css
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
--- a/src/style.chat.css
+++ b/src/style.chat.css
@@ -0,0 +1,9 @@
1 em;
2 font-size: 85% 2;
3 ser-list2.5 2,0.55.25em 0. 2.5 2,0em 0. 2.5 2,0.55.25em 0edit-buttons {user-list2.5 2,0.55.25em 0. 2.5 2,0.55.25em 0.5em;
4 font-size: 85% 2;
5 opacity: 0.6.2em 0.5em 0.2em 0
6 (0deg);-::before {
7 /*cont "Rec 1 5 auto;*/
8 max-width: 6emn@1eT,3V@1gI,1:2g@1jo,4:16emQ@1zj,2R@1mo,O@1iG,VB@1q_,aZZoL;524em;
9 }
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -674,10 +674,11 @@
674674
$(SRCDIR)/sounds/c.wav \
675675
$(SRCDIR)/sounds/d.wav \
676676
$(SRCDIR)/sounds/e.wav \
677677
$(SRCDIR)/sounds/f.wav \
678678
$(SRCDIR)/style.admin_log.css \
679
+ $(SRCDIR)/style.chat.css \
679680
$(SRCDIR)/style.fileedit.css \
680681
$(SRCDIR)/style.wikiedit.css \
681682
$(SRCDIR)/tree.js \
682683
$(SRCDIR)/useredit.js \
683684
$(SRCDIR)/wiki.wiki
684685
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -674,10 +674,11 @@
674 $(SRCDIR)/sounds/c.wav \
675 $(SRCDIR)/sounds/d.wav \
676 $(SRCDIR)/sounds/e.wav \
677 $(SRCDIR)/sounds/f.wav \
678 $(SRCDIR)/style.admin_log.css \
 
679 $(SRCDIR)/style.fileedit.css \
680 $(SRCDIR)/style.wikiedit.css \
681 $(SRCDIR)/tree.js \
682 $(SRCDIR)/useredit.js \
683 $(SRCDIR)/wiki.wiki
684
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -674,10 +674,11 @@
674 $(SRCDIR)/sounds/c.wav \
675 $(SRCDIR)/sounds/d.wav \
676 $(SRCDIR)/sounds/e.wav \
677 $(SRCDIR)/sounds/f.wav \
678 $(SRCDIR)/style.admin_log.css \
679 $(SRCDIR)/style.chat.css \
680 $(SRCDIR)/style.fileedit.css \
681 $(SRCDIR)/style.wikiedit.css \
682 $(SRCDIR)/tree.js \
683 $(SRCDIR)/useredit.js \
684 $(SRCDIR)/wiki.wiki
685
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -616,10 +616,11 @@
616616
"$(SRCDIR)\sounds\c.wav" \
617617
"$(SRCDIR)\sounds\d.wav" \
618618
"$(SRCDIR)\sounds\e.wav" \
619619
"$(SRCDIR)\sounds\f.wav" \
620620
"$(SRCDIR)\style.admin_log.css" \
621
+ "$(SRCDIR)\style.chat.css" \
621622
"$(SRCDIR)\style.fileedit.css" \
622623
"$(SRCDIR)\style.wikiedit.css" \
623624
"$(SRCDIR)\tree.js" \
624625
"$(SRCDIR)\useredit.js" \
625626
"$(SRCDIR)\wiki.wiki"
@@ -1224,10 +1225,11 @@
12241225
echo "$(SRCDIR)\sounds/c.wav" >> $@
12251226
echo "$(SRCDIR)\sounds/d.wav" >> $@
12261227
echo "$(SRCDIR)\sounds/e.wav" >> $@
12271228
echo "$(SRCDIR)\sounds/f.wav" >> $@
12281229
echo "$(SRCDIR)\style.admin_log.css" >> $@
1230
+ echo "$(SRCDIR)\style.chat.css" >> $@
12291231
echo "$(SRCDIR)\style.fileedit.css" >> $@
12301232
echo "$(SRCDIR)\style.wikiedit.css" >> $@
12311233
echo "$(SRCDIR)\tree.js" >> $@
12321234
echo "$(SRCDIR)\useredit.js" >> $@
12331235
echo "$(SRCDIR)\wiki.wiki" >> $@
12341236
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -616,10 +616,11 @@
616 "$(SRCDIR)\sounds\c.wav" \
617 "$(SRCDIR)\sounds\d.wav" \
618 "$(SRCDIR)\sounds\e.wav" \
619 "$(SRCDIR)\sounds\f.wav" \
620 "$(SRCDIR)\style.admin_log.css" \
 
621 "$(SRCDIR)\style.fileedit.css" \
622 "$(SRCDIR)\style.wikiedit.css" \
623 "$(SRCDIR)\tree.js" \
624 "$(SRCDIR)\useredit.js" \
625 "$(SRCDIR)\wiki.wiki"
@@ -1224,10 +1225,11 @@
1224 echo "$(SRCDIR)\sounds/c.wav" >> $@
1225 echo "$(SRCDIR)\sounds/d.wav" >> $@
1226 echo "$(SRCDIR)\sounds/e.wav" >> $@
1227 echo "$(SRCDIR)\sounds/f.wav" >> $@
1228 echo "$(SRCDIR)\style.admin_log.css" >> $@
 
1229 echo "$(SRCDIR)\style.fileedit.css" >> $@
1230 echo "$(SRCDIR)\style.wikiedit.css" >> $@
1231 echo "$(SRCDIR)\tree.js" >> $@
1232 echo "$(SRCDIR)\useredit.js" >> $@
1233 echo "$(SRCDIR)\wiki.wiki" >> $@
1234
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -616,10 +616,11 @@
616 "$(SRCDIR)\sounds\c.wav" \
617 "$(SRCDIR)\sounds\d.wav" \
618 "$(SRCDIR)\sounds\e.wav" \
619 "$(SRCDIR)\sounds\f.wav" \
620 "$(SRCDIR)\style.admin_log.css" \
621 "$(SRCDIR)\style.chat.css" \
622 "$(SRCDIR)\style.fileedit.css" \
623 "$(SRCDIR)\style.wikiedit.css" \
624 "$(SRCDIR)\tree.js" \
625 "$(SRCDIR)\useredit.js" \
626 "$(SRCDIR)\wiki.wiki"
@@ -1224,10 +1225,11 @@
1225 echo "$(SRCDIR)\sounds/c.wav" >> $@
1226 echo "$(SRCDIR)\sounds/d.wav" >> $@
1227 echo "$(SRCDIR)\sounds/e.wav" >> $@
1228 echo "$(SRCDIR)\sounds/f.wav" >> $@
1229 echo "$(SRCDIR)\style.admin_log.css" >> $@
1230 echo "$(SRCDIR)\style.chat.css" >> $@
1231 echo "$(SRCDIR)\style.fileedit.css" >> $@
1232 echo "$(SRCDIR)\style.wikiedit.css" >> $@
1233 echo "$(SRCDIR)\tree.js" >> $@
1234 echo "$(SRCDIR)\useredit.js" >> $@
1235 echo "$(SRCDIR)\wiki.wiki" >> $@
1236
--- www/changes.wiki
+++ www/changes.wiki
@@ -39,10 +39,15 @@
3939
* The [/help?cmd=all|fossil all git status] command only shows reports for
4040
the subset of repositories that have a configured Git export.
4141
* Enhanced the [/help?cmd=/chat|/chat page] configuration and added the ability
4242
for a repository administrator to [./chat.md#notifications|extend the
4343
selection of notification sounds via unversioned files].
44
+ * The [/help?cmd=/chat|/chat] messages now use fossil's full complement of
45
+ markdown features, instead of the prior small subset of markup it
46
+ previously supported. This retroactively applies to all chat messages,
47
+ as they are markdown-processed when they are sent instead of when they
48
+ are saved. See [./chat.md#usage|the chat docs] for more details.
4449
4550
<h2 id='v2_16'>Changes for Version 2.16 (2021-07-02)</h2>
4651
* <b>Security:</b> Fix the client-side TLS so that it verifies that the
4752
server hostname matches its certificate.
4853
* The default "ssh" command on Windows is changed to "ssh" instead of the
4954
--- www/changes.wiki
+++ www/changes.wiki
@@ -39,10 +39,15 @@
39 * The [/help?cmd=all|fossil all git status] command only shows reports for
40 the subset of repositories that have a configured Git export.
41 * Enhanced the [/help?cmd=/chat|/chat page] configuration and added the ability
42 for a repository administrator to [./chat.md#notifications|extend the
43 selection of notification sounds via unversioned files].
 
 
 
 
 
44
45 <h2 id='v2_16'>Changes for Version 2.16 (2021-07-02)</h2>
46 * <b>Security:</b> Fix the client-side TLS so that it verifies that the
47 server hostname matches its certificate.
48 * The default "ssh" command on Windows is changed to "ssh" instead of the
49
--- www/changes.wiki
+++ www/changes.wiki
@@ -39,10 +39,15 @@
39 * The [/help?cmd=all|fossil all git status] command only shows reports for
40 the subset of repositories that have a configured Git export.
41 * Enhanced the [/help?cmd=/chat|/chat page] configuration and added the ability
42 for a repository administrator to [./chat.md#notifications|extend the
43 selection of notification sounds via unversioned files].
44 * The [/help?cmd=/chat|/chat] messages now use fossil's full complement of
45 markdown features, instead of the prior small subset of markup it
46 previously supported. This retroactively applies to all chat messages,
47 as they are markdown-processed when they are sent instead of when they
48 are saved. See [./chat.md#usage|the chat docs] for more details.
49
50 <h2 id='v2_16'>Changes for Version 2.16 (2021-07-02)</h2>
51 * <b>Security:</b> Fix the client-side TLS so that it verifies that the
52 server hostname matches its certificate.
53 * The default "ssh" command on Windows is changed to "ssh" instead of the
54
+10 -21
--- www/chat.md
+++ www/chat.md
@@ -4,15 +4,15 @@
44
55
As of version 2.14,
66
Fossil supports a developer chatroom feature. The chatroom provides an
77
ephemeral discussion venue for insiders. Design goals include:
88
9
- * **Simple but functional** &rarr; Fossil chat is designed to provide a
10
- convenient real-time communication mechanism for geographically
11
- dispersed developers. Fossil chat is *not* intended
12
- as a replacement or
13
- competitor for IRC, Slack, Discord, Telegram, Google Hangouts, etc.
9
+ * **Simple but functional** &rarr;
10
+ Fossil chat is designed to provide a convenient real-time
11
+ communication mechanism for geographically dispersed developers.
12
+ Fossil chat is *not* intended as a replacement or competitor for
13
+ IRC, Slack, Discord, Telegram, Google Hangouts, etc.
1414
1515
* **Low administration** &rarr;
1616
You can activate the chatroom in seconds without having to
1717
mess with configuration files or install new software.
1818
In an existing [server setup](./server/),
@@ -53,34 +53,23 @@
5353
behavior of chat, though the default settings are reasonable so in most
5454
cases those settings can be ignored. The settings control things like
5555
the amount of time that chat messages are retained before being purged
5656
from the repository database.
5757
58
-## Usage
58
+## <a id="usage"></a>Usage
5959
6060
For users with appropriate permissions, simply browse to the
6161
[/chat](/help?cmd=/chat) to start up a chat session. The default
6262
skin includes a "Chat" entry on the menu bar on wide screens for
6363
people with chat privilege. There is also a "Chat" option on
6464
the [Sitemap page](/sitemap), which means that chat will appear
6565
as an option under the hamburger menu for many [skins](./customskin.md).
6666
67
-Message text is delivered verbatim. There is no markup. However,
68
-the chat system does try to identify and tag hyperlinks, as follows:
69
-
70
- * Any word that begins with "http://" or "https://" is assumed
71
- to be a hyperlink and is tagged.
72
-
73
- * Text within `[...]` is parsed, and it if is a valid hyperlink
74
- target (according to the way that [Fossil Wiki](/wiki_rules) or
75
- [Markdown](/md_rules) understand hyperlinks), then that text is
76
- tagged. Note that only URLs and Fossil-internal constructs such
77
- as checkin hashes and wiki pages names are recognized here, not
78
- constructs such as `[URL | label]` or `[label](URL)`.
79
-
80
-Apart from adding hyperlink anchor tags to bits of text that look
81
-like hyperlinks, no changes are made to the input text.
67
+As of version 2.17, chat messages are subject to [fossil's
68
+full range of markdown processing](/md_rules). Because chat messages are
69
+stored as-is when they arrive from a client, this change applies
70
+retroactively to messages stored by previous fossil versions.
8271
8372
Files may be sent via chat using the file selection element at the
8473
bottom of the page. If the desktop environment system supports it,
8574
files may be dragged and dropped onto that element. Files are not
8675
automatically sent - selection of a file can be cancelled using the
8776
--- www/chat.md
+++ www/chat.md
@@ -4,15 +4,15 @@
4
5 As of version 2.14,
6 Fossil supports a developer chatroom feature. The chatroom provides an
7 ephemeral discussion venue for insiders. Design goals include:
8
9 * **Simple but functional** &rarr; Fossil chat is designed to provide a
10 convenient real-time communication mechanism for geographically
11 dispersed developers. Fossil chat is *not* intended
12 as a replacement or
13 competitor for IRC, Slack, Discord, Telegram, Google Hangouts, etc.
14
15 * **Low administration** &rarr;
16 You can activate the chatroom in seconds without having to
17 mess with configuration files or install new software.
18 In an existing [server setup](./server/),
@@ -53,34 +53,23 @@
53 behavior of chat, though the default settings are reasonable so in most
54 cases those settings can be ignored. The settings control things like
55 the amount of time that chat messages are retained before being purged
56 from the repository database.
57
58 ## Usage
59
60 For users with appropriate permissions, simply browse to the
61 [/chat](/help?cmd=/chat) to start up a chat session. The default
62 skin includes a "Chat" entry on the menu bar on wide screens for
63 people with chat privilege. There is also a "Chat" option on
64 the [Sitemap page](/sitemap), which means that chat will appear
65 as an option under the hamburger menu for many [skins](./customskin.md).
66
67 Message text is delivered verbatim. There is no markup. However,
68 the chat system does try to identify and tag hyperlinks, as follows:
69
70 * Any word that begins with "http://" or "https://" is assumed
71 to be a hyperlink and is tagged.
72
73 * Text within `[...]` is parsed, and it if is a valid hyperlink
74 target (according to the way that [Fossil Wiki](/wiki_rules) or
75 [Markdown](/md_rules) understand hyperlinks), then that text is
76 tagged. Note that only URLs and Fossil-internal constructs such
77 as checkin hashes and wiki pages names are recognized here, not
78 constructs such as `[URL | label]` or `[label](URL)`.
79
80 Apart from adding hyperlink anchor tags to bits of text that look
81 like hyperlinks, no changes are made to the input text.
82
83 Files may be sent via chat using the file selection element at the
84 bottom of the page. If the desktop environment system supports it,
85 files may be dragged and dropped onto that element. Files are not
86 automatically sent - selection of a file can be cancelled using the
87
--- www/chat.md
+++ www/chat.md
@@ -4,15 +4,15 @@
4
5 As of version 2.14,
6 Fossil supports a developer chatroom feature. The chatroom provides an
7 ephemeral discussion venue for insiders. Design goals include:
8
9 * **Simple but functional** &rarr;
10 Fossil chat is designed to provide a convenient real-time
11 communication mechanism for geographically dispersed developers.
12 Fossil chat is *not* intended as a replacement or competitor for
13 IRC, Slack, Discord, Telegram, Google Hangouts, etc.
14
15 * **Low administration** &rarr;
16 You can activate the chatroom in seconds without having to
17 mess with configuration files or install new software.
18 In an existing [server setup](./server/),
@@ -53,34 +53,23 @@
53 behavior of chat, though the default settings are reasonable so in most
54 cases those settings can be ignored. The settings control things like
55 the amount of time that chat messages are retained before being purged
56 from the repository database.
57
58 ## <a id="usage"></a>Usage
59
60 For users with appropriate permissions, simply browse to the
61 [/chat](/help?cmd=/chat) to start up a chat session. The default
62 skin includes a "Chat" entry on the menu bar on wide screens for
63 people with chat privilege. There is also a "Chat" option on
64 the [Sitemap page](/sitemap), which means that chat will appear
65 as an option under the hamburger menu for many [skins](./customskin.md).
66
67 As of version 2.17, chat messages are subject to [fossil's
68 full range of markdown processing](/md_rules). Because chat messages are
69 stored as-is when they arrive from a client, this change applies
70 retroactively to messages stored by previous fossil versions.
 
 
 
 
 
 
 
 
 
 
 
71
72 Files may be sent via chat using the file selection element at the
73 bottom of the page. If the desktop environment system supports it,
74 files may be dragged and dropped onto that element. Files are not
75 automatically sent - selection of a file can be cancelled using the
76

Keyboard Shortcuts

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