Fossil SCM
/chat now uses full-fledged markdown instead of its former special-case markup. Added a message preview option to support that.
Commit
6c1ac839e3591d02a6fb2f348badb2dcd2f138fe1dfea842b500cabacbe7a007
Parent
898b8f208286eb1…
11 files changed
+5
-3
+112
-76
+156
-36
+1
-299
+1
+1
+9
+1
+2
+5
+10
-21
+5
-3
| --- src/ajax.c | ||
| +++ src/ajax.c | ||
| @@ -264,16 +264,18 @@ | ||
| 264 | 264 | } |
| 265 | 265 | return rc; |
| 266 | 266 | } |
| 267 | 267 | |
| 268 | 268 | /* |
| 269 | -** AJAX route /ajax/preview-wiki | |
| 269 | +** AJAX route /ajax/preview-text | |
| 270 | 270 | ** |
| 271 | 271 | ** Required query parameters: |
| 272 | 272 | ** |
| 273 | 273 | ** filename=name of content, for use in determining the |
| 274 | -** mimetype/render mode. content=text | |
| 274 | +** mimetype/render mode. | |
| 275 | +** | |
| 276 | +** content=text | |
| 275 | 277 | ** |
| 276 | 278 | ** Optional query parameters: |
| 277 | 279 | ** |
| 278 | 280 | ** render_mode=integer (AJAX_RENDER_xxx) (default=AJAX_RENDER_GUESS) |
| 279 | 281 | ** |
| @@ -365,11 +367,11 @@ | ||
| 365 | 367 | const AjaxRoute * rB = (const AjaxRoute*)b; |
| 366 | 368 | return fossil_strcmp(rA->zName, rB->zName); |
| 367 | 369 | } |
| 368 | 370 | |
| 369 | 371 | /* |
| 370 | -** WEBPAGE: ajax | |
| 372 | +** WEBPAGE: ajax hidden | |
| 371 | 373 | ** |
| 372 | 374 | ** The main dispatcher for shared ajax-served routes. Requires the |
| 373 | 375 | ** 'name' parameter be the main route's name (as defined in a list in |
| 374 | 376 | ** this function), noting that fossil automatically assigns all path |
| 375 | 377 | ** parts after "ajax" to "name", e.g. /ajax/foo/bar assigns |
| 376 | 378 |
| --- 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 @@ | ||
| 125 | 125 | */ |
| 126 | 126 | /* |
| 127 | 127 | ** SETTING: chat-alert-sound width=10 |
| 128 | 128 | ** |
| 129 | 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. | |
| 130 | +** The value must be the name of a builtin WAV file. | |
| 131 | 131 | */ |
| 132 | 132 | /* |
| 133 | 133 | ** WEBPAGE: chat |
| 134 | 134 | ** |
| 135 | 135 | ** Start up a browser-based chat session. |
| @@ -154,19 +154,25 @@ | ||
| 154 | 154 | zProjectName = db_get("project-name","Unnamed project"); |
| 155 | 155 | style_set_current_feature("chat"); |
| 156 | 156 | style_header("Chat"); |
| 157 | 157 | @ <form accept-encoding="utf-8" id="chat-form" autocomplete="off"> |
| 158 | 158 | @ <div id='chat-input-area'> |
| 159 | - @ <div id='chat-input-line'> | |
| 159 | + @ <div id='chat-input-line' class='single-line'> | |
| 160 | 160 | @ <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"> | |
| 162 | 163 | @ <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." \ | |
| 164 | 165 | @ 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">👁</button> | |
| 169 | + @ <button id="chat-settings-button" \ | |
| 170 | + @ title="Configure chat">⚙</button> | |
| 171 | + @ <button id="chat-message-submit" \ | |
| 172 | + @ title="Send message">📤</button> | |
| 173 | + @ </div> | |
| 168 | 174 | @ </div> |
| 169 | 175 | @ <div id='chat-input-file-area'> |
| 170 | 176 | @ <div class='file-selection-wrapper'> |
| 171 | 177 | @ <div class='help-buttonlet'> |
| 172 | 178 | @ Select a file to upload, drag/drop a file into this spot, |
| @@ -177,21 +183,27 @@ | ||
| 177 | 183 | @ </div> |
| 178 | 184 | @ <div id="chat-drop-details"></div> |
| 179 | 185 | @ </div> |
| 180 | 186 | @ </div> |
| 181 | 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> | |
| 182 | 193 | @ <div id='chat-config' class='hidden'> |
| 183 | 194 | @ <div id='chat-config-options'></div> |
| 184 | 195 | /* ^^^populated client-side */ |
| 185 | - @ <button>Close</button> | |
| 196 | + @ <button>Close Settings</button> | |
| 186 | 197 | @ </div> |
| 187 | 198 | @ <div id='chat-messages-wrapper'> |
| 188 | 199 | /* New chat messages get inserted immediately after this element */ |
| 189 | 200 | @ <span id='message-inject-point'></span> |
| 190 | 201 | @ </div> |
| 191 | 202 | 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); | |
| 193 | 205 | /* Always in-line the javascript for the chat page */ |
| 194 | 206 | @ <script nonce="%h(style_nonce())">/* chat.c:%d(__LINE__) */ |
| 195 | 207 | /* We need an onload handler to ensure that window.fossil is |
| 196 | 208 | initialized before the chat init code runs. */ |
| 197 | 209 | @ window.addEventListener('load', function(){ |
| @@ -201,10 +213,11 @@ | ||
| 201 | 213 | @ fromcli: %h(PB("cli")?"true":"false"), |
| 202 | 214 | @ alertSound: "%h(zAlert)", |
| 203 | 215 | @ initSize: %d(db_get_int("chat-initial-history",50)), |
| 204 | 216 | @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)) |
| 205 | 217 | @ }; |
| 218 | + ajax_emit_js_preview_modes(0); | |
| 206 | 219 | chat_emit_alert_list(); |
| 207 | 220 | cgi_append_content(builtin_text("chat.js"),-1); |
| 208 | 221 | @ }, false); |
| 209 | 222 | @ </script> |
| 210 | 223 | |
| @@ -294,11 +307,11 @@ | ||
| 294 | 307 | } |
| 295 | 308 | fossil_free(zTime); |
| 296 | 309 | } |
| 297 | 310 | |
| 298 | 311 | /* |
| 299 | -** WEBPAGE: chat-send | |
| 312 | +** WEBPAGE: chat-send hidden | |
| 300 | 313 | ** |
| 301 | 314 | ** This page receives (via XHR) a new chat-message and/or a new file |
| 302 | 315 | ** to be entered into the chat history. |
| 303 | 316 | ** |
| 304 | 317 | ** On success it responds with an empty response: the new message |
| @@ -347,79 +360,25 @@ | ||
| 347 | 360 | db_commit_transaction(); |
| 348 | 361 | } |
| 349 | 362 | |
| 350 | 363 | /* |
| 351 | 364 | ** 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. | |
| 360 | 368 | ** |
| 361 | 369 | ** Space to hold the returned string is obtained from fossil_malloc() |
| 362 | 370 | ** and must be freed by the caller. |
| 363 | 371 | */ |
| 364 | 372 | static char *chat_format_to_html(const char *zMsg){ |
| 365 | - char *zSafe = mprintf("%h", zMsg); | |
| 366 | - int i, j, k; | |
| 367 | 373 | 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 | + } | |
| 421 | 380 | return blob_str(&out); |
| 422 | 381 | } |
| 423 | 382 | |
| 424 | 383 | /* |
| 425 | 384 | ** COMMAND: test-chat-formatter |
| @@ -442,11 +401,11 @@ | ||
| 442 | 401 | fossil_free(zOut); |
| 443 | 402 | } |
| 444 | 403 | } |
| 445 | 404 | |
| 446 | 405 | /* |
| 447 | -** WEBPAGE: chat-poll | |
| 406 | +** WEBPAGE: chat-poll hidden | |
| 448 | 407 | ** |
| 449 | 408 | ** The chat page generated by /chat using an XHR to this page to |
| 450 | 409 | ** request new chat content. A typical invocation is: |
| 451 | 410 | ** |
| 452 | 411 | ** /chat-poll/N |
| @@ -651,11 +610,88 @@ | ||
| 651 | 610 | cgi_set_content(&json); |
| 652 | 611 | return; |
| 653 | 612 | } |
| 654 | 613 | |
| 655 | 614 | /* |
| 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 | |
| 657 | 693 | ** |
| 658 | 694 | ** Download the CHAT.FILE attachment associated with a single chat |
| 659 | 695 | ** entry. The "name" query parameter begins with an integer that |
| 660 | 696 | ** identifies the particular chat message. The integer may be followed |
| 661 | 697 | ** by a / and a filename, which will indicate to the browser to use |
| @@ -684,11 +720,11 @@ | ||
| 684 | 720 | cgi_set_content(&r); |
| 685 | 721 | } |
| 686 | 722 | |
| 687 | 723 | |
| 688 | 724 | /* |
| 689 | -** WEBPAGE: chat-delete | |
| 725 | +** WEBPAGE: chat-delete hidden | |
| 690 | 726 | ** |
| 691 | 727 | ** Delete the chat entry identified by the name query parameter. |
| 692 | 728 | ** Invoking fetch("chat-delete/"+msgid) from javascript in the client |
| 693 | 729 | ** will delete a chat entry from the CHAT table. |
| 694 | 730 | ** |
| 695 | 731 |
| --- 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">👁</button> |
| 169 | @ <button id="chat-settings-button" \ |
| 170 | @ title="Configure chat">⚙</button> |
| 171 | @ <button id="chat-message-submit" \ |
| 172 | @ title="Send message">📤</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 @@ | ||
| 101 | 101 | e:{/*map of certain DOM elements.*/ |
| 102 | 102 | messageInjectPoint: E1('#message-inject-point'), |
| 103 | 103 | pageTitle: E1('head title'), |
| 104 | 104 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 105 | 105 | inputWrapper: E1("#chat-input-area"), |
| 106 | + inputLine: E1('#chat-input-line'), | |
| 106 | 107 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 107 | 108 | messagesWrapper: E1('#chat-messages-wrapper'), |
| 108 | 109 | inputForm: E1('#chat-form'), |
| 109 | 110 | btnSubmit: E1('#chat-message-submit'), |
| 110 | 111 | inputSingle: E1('#chat-input-single'), |
| 111 | 112 | inputMulti: E1('#chat-input-multi'), |
| 112 | 113 | inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 113 | 114 | inputFile: E1('#chat-input-file'), |
| 114 | 115 | 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') | |
| 116 | 120 | }, |
| 117 | 121 | me: F.user.name, |
| 118 | 122 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 119 | 123 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 120 | 124 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -147,12 +151,14 @@ | ||
| 147 | 151 | /** Toggles between single- and multi-line edit modes. Returns this. */ |
| 148 | 152 | inputToggleSingleMulti: function(){ |
| 149 | 153 | const old = this.e.inputCurrent; |
| 150 | 154 | if(this.e.inputCurrent === this.e.inputSingle){ |
| 151 | 155 | this.e.inputCurrent = this.e.inputMulti; |
| 156 | + this.e.inputLine.classList.remove('single-line'); | |
| 152 | 157 | }else{ |
| 153 | 158 | this.e.inputCurrent = this.e.inputSingle; |
| 159 | + this.e.inputLine.classList.add('single-line'); | |
| 154 | 160 | } |
| 155 | 161 | const m = this.e.messagesWrapper, |
| 156 | 162 | sTop = m.scrollTop, |
| 157 | 163 | mh1 = m.clientHeight; |
| 158 | 164 | D.addClass(old, 'hidden'); |
| @@ -485,10 +491,67 @@ | ||
| 485 | 491 | F.toast.message("Deleted message "+id+"."); |
| 486 | 492 | } |
| 487 | 493 | return !!e; |
| 488 | 494 | }; |
| 489 | 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 | + | |
| 490 | 553 | /** Given a .message-row element, this function returns whethe the |
| 491 | 554 | current user may, at least hypothetically, delete the message |
| 492 | 555 | globally. A user may always delete a local copy of a |
| 493 | 556 | post. The server may trump this, e.g. if the login has been |
| 494 | 557 | cancelled after this page was loaded. |
| @@ -681,10 +744,13 @@ | ||
| 681 | 744 | if(m.xmsg instanceof Array){ |
| 682 | 745 | // Used by Chat.reportErrorAsMessage() |
| 683 | 746 | D.append(contentTarget, m.xmsg); |
| 684 | 747 | }else{ |
| 685 | 748 | contentTarget.innerHTML = m.xmsg; |
| 749 | + if(F.pikchr){ | |
| 750 | + F.pikchr.addSrcView(contentTarget.querySelectorAll('svg.pikchr')); | |
| 751 | + } | |
| 686 | 752 | } |
| 687 | 753 | } |
| 688 | 754 | this.e.tab.addEventListener('click', this._handleLegendClicked, false); |
| 689 | 755 | if(eXFrom){ |
| 690 | 756 | eXFrom.addEventListener('click', ()=>this.e.tab.click(), false); |
| @@ -739,14 +805,20 @@ | ||
| 739 | 805 | btnDeleteGlobal.addEventListener('click', function(){ |
| 740 | 806 | self.hide(); |
| 741 | 807 | Chat.deleteMessage(eMsg); |
| 742 | 808 | }); |
| 743 | 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); | |
| 744 | 818 | if(eMsg.dataset.xfrom){ |
| 745 | 819 | /* 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 | 820 | const timelineLink = D.attr( |
| 749 | 821 | D.a(F.repoUrl('timeline',{ |
| 750 | 822 | u: eMsg.dataset.xfrom, |
| 751 | 823 | y: 'a' |
| 752 | 824 | }), "User's Timeline"), |
| @@ -881,10 +953,11 @@ | ||
| 881 | 953 | */ |
| 882 | 954 | Chat.submitMessage = function f(){ |
| 883 | 955 | if(!f.spaces){ |
| 884 | 956 | f.spaces = /\s+$/; |
| 885 | 957 | } |
| 958 | + this.revealPreview(false); | |
| 886 | 959 | const fd = new FormData(this.e.inputForm) |
| 887 | 960 | /* ^^^^ we don't really want/need the FORM element, but when |
| 888 | 961 | FormData() is default-constructed here then the server |
| 889 | 962 | segfaults, and i have no clue why! */; |
| 890 | 963 | var msg = this.inputValue().trim(); |
| @@ -952,12 +1025,21 @@ | ||
| 952 | 1025 | }; |
| 953 | 1026 | |
| 954 | 1027 | (function(){/*Set up #chat-settings-button */ |
| 955 | 1028 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 956 | 1029 | 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; | |
| 959 | 1041 | }; |
| 960 | 1042 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 961 | 1043 | Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false); |
| 962 | 1044 | /* Settings menu entries... */ |
| 963 | 1045 | const settingsOps = [{ |
| @@ -995,11 +1077,11 @@ | ||
| 995 | 1077 | F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+"."); |
| 996 | 1078 | } |
| 997 | 1079 | }]; |
| 998 | 1080 | |
| 999 | 1081 | /** Set up selection list of notification sounds. */ |
| 1000 | - if(false/*flip this to false to enable selection of audio files*/){ | |
| 1082 | + if(1){ | |
| 1001 | 1083 | settingsOps.push({ |
| 1002 | 1084 | label: "Audible alerts", |
| 1003 | 1085 | boolValue: ()=>Chat.settings.getBool('audible-alert'), |
| 1004 | 1086 | callback: function(){ |
| 1005 | 1087 | const v = Chat.settings.toggle('audible-alert'); |
| @@ -1009,40 +1091,10 @@ | ||
| 1009 | 1091 | } |
| 1010 | 1092 | }); |
| 1011 | 1093 | Chat.setNewMessageSound( |
| 1012 | 1094 | Chat.settings.getBool('audible-alert') ? F.config.chat.alertSound : false |
| 1013 | 1095 | ); |
| 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 | 1096 | }/*audio notification config*/ |
| 1045 | 1097 | /** |
| 1046 | 1098 | Build list of options... |
| 1047 | 1099 | */ |
| 1048 | 1100 | settingsOps.forEach(function f(op){ |
| @@ -1076,10 +1128,78 @@ | ||
| 1076 | 1128 | if(settingsOps.selectSound){ |
| 1077 | 1129 | D.append(optionsMenu, settingsOps.selectSound); |
| 1078 | 1130 | } |
| 1079 | 1131 | //settingsButton.click()/*for for development*/; |
| 1080 | 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*/; | |
| 1081 | 1201 | |
| 1082 | 1202 | /** Callback for poll() to inject new content into the page. jx == |
| 1083 | 1203 | the response from /chat-poll. If atEnd is true, the message is |
| 1084 | 1204 | appended to the end of the chat list (for loading older |
| 1085 | 1205 | messages), else the beginning (the default). */ |
| 1086 | 1206 |
| --- 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 @@ | ||
| 1596 | 1596 | opacity: 0 !important; |
| 1597 | 1597 | pointer-events: none !important; |
| 1598 | 1598 | display: none !important; |
| 1599 | 1599 | } |
| 1600 | 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 | 1601 | |
| 1707 | 1602 | /* An icon element intended for use as a button/menu for |
| 1708 | 1603 | accessing app-specific settings. */ |
| 1709 | 1604 | .settings-icon { |
| 1710 | 1605 | /* Icon source: https://de.wikipedia.org/wiki/Datei:OOjs_UI_icon_settings.svg |
| @@ -1734,204 +1629,11 @@ | ||
| 1734 | 1629 | border: 1px outset rgba(127,127,127,1); |
| 1735 | 1630 | } |
| 1736 | 1631 | body.fossil-dark-style .settings-icon { |
| 1737 | 1632 | filter: invert(100%); |
| 1738 | 1633 | } |
| 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 | + | |
| 1933 | 1635 | input[type="checkbox"].diff-toggle { |
| 1934 | 1636 | float: right; |
| 1935 | 1637 | } |
| 1936 | 1638 | |
| 1937 | 1639 | body.branch .brlist > table > tbody > tr:hover:not(.selected), |
| 1938 | 1640 |
| --- 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 |
+1
| --- src/main.mk | ||
| +++ src/main.mk | ||
| @@ -265,10 +265,11 @@ | ||
| 265 | 265 | $(SRCDIR)/sounds/c.wav \ |
| 266 | 266 | $(SRCDIR)/sounds/d.wav \ |
| 267 | 267 | $(SRCDIR)/sounds/e.wav \ |
| 268 | 268 | $(SRCDIR)/sounds/f.wav \ |
| 269 | 269 | $(SRCDIR)/style.admin_log.css \ |
| 270 | + $(SRCDIR)/style.chat.css \ | |
| 270 | 271 | $(SRCDIR)/style.fileedit.css \ |
| 271 | 272 | $(SRCDIR)/style.wikiedit.css \ |
| 272 | 273 | $(SRCDIR)/tree.js \ |
| 273 | 274 | $(SRCDIR)/useredit.js \ |
| 274 | 275 | $(SRCDIR)/wiki.wiki |
| 275 | 276 |
| --- 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 |
+1
| --- src/markdown.c | ||
| +++ src/markdown.c | ||
| @@ -1665,10 +1665,11 @@ | ||
| 1665 | 1665 | |
| 1666 | 1666 | if( !size || data[0]!='#' ) return 0; |
| 1667 | 1667 | |
| 1668 | 1668 | while( level<size && level<6 && data[level]=='#' ){ level++; } |
| 1669 | 1669 | for(i=level; i<size && (data[i]==' ' || data[i]=='\t'); i++); |
| 1670 | + if ( i == level ) return parse_paragraph(ob, rndr, data, size); | |
| 1670 | 1671 | span_beg = i; |
| 1671 | 1672 | |
| 1672 | 1673 | for(end=i; end<size && data[end]!='\n'; end++); |
| 1673 | 1674 | skip = end; |
| 1674 | 1675 | if( end<=i ) return parse_paragraph(ob, rndr, data, size); |
| 1675 | 1676 | |
| 1676 | 1677 | 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 |
+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 | +} |
| --- 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 | } |
+1
| --- win/Makefile.mingw | ||
| +++ win/Makefile.mingw | ||
| @@ -674,10 +674,11 @@ | ||
| 674 | 674 | $(SRCDIR)/sounds/c.wav \ |
| 675 | 675 | $(SRCDIR)/sounds/d.wav \ |
| 676 | 676 | $(SRCDIR)/sounds/e.wav \ |
| 677 | 677 | $(SRCDIR)/sounds/f.wav \ |
| 678 | 678 | $(SRCDIR)/style.admin_log.css \ |
| 679 | + $(SRCDIR)/style.chat.css \ | |
| 679 | 680 | $(SRCDIR)/style.fileedit.css \ |
| 680 | 681 | $(SRCDIR)/style.wikiedit.css \ |
| 681 | 682 | $(SRCDIR)/tree.js \ |
| 682 | 683 | $(SRCDIR)/useredit.js \ |
| 683 | 684 | $(SRCDIR)/wiki.wiki |
| 684 | 685 |
| --- 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 |
+2
| --- win/Makefile.msc | ||
| +++ win/Makefile.msc | ||
| @@ -616,10 +616,11 @@ | ||
| 616 | 616 | "$(SRCDIR)\sounds\c.wav" \ |
| 617 | 617 | "$(SRCDIR)\sounds\d.wav" \ |
| 618 | 618 | "$(SRCDIR)\sounds\e.wav" \ |
| 619 | 619 | "$(SRCDIR)\sounds\f.wav" \ |
| 620 | 620 | "$(SRCDIR)\style.admin_log.css" \ |
| 621 | + "$(SRCDIR)\style.chat.css" \ | |
| 621 | 622 | "$(SRCDIR)\style.fileedit.css" \ |
| 622 | 623 | "$(SRCDIR)\style.wikiedit.css" \ |
| 623 | 624 | "$(SRCDIR)\tree.js" \ |
| 624 | 625 | "$(SRCDIR)\useredit.js" \ |
| 625 | 626 | "$(SRCDIR)\wiki.wiki" |
| @@ -1224,10 +1225,11 @@ | ||
| 1224 | 1225 | echo "$(SRCDIR)\sounds/c.wav" >> $@ |
| 1225 | 1226 | echo "$(SRCDIR)\sounds/d.wav" >> $@ |
| 1226 | 1227 | echo "$(SRCDIR)\sounds/e.wav" >> $@ |
| 1227 | 1228 | echo "$(SRCDIR)\sounds/f.wav" >> $@ |
| 1228 | 1229 | echo "$(SRCDIR)\style.admin_log.css" >> $@ |
| 1230 | + echo "$(SRCDIR)\style.chat.css" >> $@ | |
| 1229 | 1231 | echo "$(SRCDIR)\style.fileedit.css" >> $@ |
| 1230 | 1232 | echo "$(SRCDIR)\style.wikiedit.css" >> $@ |
| 1231 | 1233 | echo "$(SRCDIR)\tree.js" >> $@ |
| 1232 | 1234 | echo "$(SRCDIR)\useredit.js" >> $@ |
| 1233 | 1235 | echo "$(SRCDIR)\wiki.wiki" >> $@ |
| 1234 | 1236 |
| --- 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 |
+5
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -39,10 +39,15 @@ | ||
| 39 | 39 | * The [/help?cmd=all|fossil all git status] command only shows reports for |
| 40 | 40 | the subset of repositories that have a configured Git export. |
| 41 | 41 | * Enhanced the [/help?cmd=/chat|/chat page] configuration and added the ability |
| 42 | 42 | for a repository administrator to [./chat.md#notifications|extend the |
| 43 | 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. | |
| 44 | 49 | |
| 45 | 50 | <h2 id='v2_16'>Changes for Version 2.16 (2021-07-02)</h2> |
| 46 | 51 | * <b>Security:</b> Fix the client-side TLS so that it verifies that the |
| 47 | 52 | server hostname matches its certificate. |
| 48 | 53 | * The default "ssh" command on Windows is changed to "ssh" instead of the |
| 49 | 54 |
| --- 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 @@ | ||
| 4 | 4 | |
| 5 | 5 | As of version 2.14, |
| 6 | 6 | Fossil supports a developer chatroom feature. The chatroom provides an |
| 7 | 7 | ephemeral discussion venue for insiders. Design goals include: |
| 8 | 8 | |
| 9 | - * **Simple but functional** → 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** → | |
| 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 | 14 | |
| 15 | 15 | * **Low administration** → |
| 16 | 16 | You can activate the chatroom in seconds without having to |
| 17 | 17 | mess with configuration files or install new software. |
| 18 | 18 | In an existing [server setup](./server/), |
| @@ -53,34 +53,23 @@ | ||
| 53 | 53 | behavior of chat, though the default settings are reasonable so in most |
| 54 | 54 | cases those settings can be ignored. The settings control things like |
| 55 | 55 | the amount of time that chat messages are retained before being purged |
| 56 | 56 | from the repository database. |
| 57 | 57 | |
| 58 | -## Usage | |
| 58 | +## <a id="usage"></a>Usage | |
| 59 | 59 | |
| 60 | 60 | For users with appropriate permissions, simply browse to the |
| 61 | 61 | [/chat](/help?cmd=/chat) to start up a chat session. The default |
| 62 | 62 | skin includes a "Chat" entry on the menu bar on wide screens for |
| 63 | 63 | people with chat privilege. There is also a "Chat" option on |
| 64 | 64 | the [Sitemap page](/sitemap), which means that chat will appear |
| 65 | 65 | as an option under the hamburger menu for many [skins](./customskin.md). |
| 66 | 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. | |
| 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. | |
| 82 | 71 | |
| 83 | 72 | Files may be sent via chat using the file selection element at the |
| 84 | 73 | bottom of the page. If the desktop environment system supports it, |
| 85 | 74 | files may be dragged and dropped onto that element. Files are not |
| 86 | 75 | automatically sent - selection of a file can be cancelled using the |
| 87 | 76 |
| --- 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** → 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** → |
| 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** → |
| 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** → |
| 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 |