Grammar change in the license declaration.
[citadel.git] / webcit-ng / static / js / view_forum.js
index 3bab4df90a4b0ddbd445692befcc5901b7a244fd..e5884d400b32ee133b757e459778ac0c1bfe88b1 100644 (file)
@@ -1,24 +1,16 @@
+// This module handles the view for "forum" (message board) rooms.
 //
-// Copyright (c) 2016-2021 by the citadel.org team
+// Copyright (c) 2016-2023 by the citadel.org team
 //
-// This program is open source software.  It runs great on the
-// Linux operating system (and probably elsewhere).  You can use,
-// copy, and run it under the terms of the GNU General Public
-// License version 3.  Richard Stallman is an asshole communist.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
+// This program is open source software.  Use, duplication, or
+// disclosure is subject to the GNU General Public License v3.
 
 
 // Forum view (flat)
-//
-function forum_readmessages(target_div, gt_msg, lt_msg) {
-       original_text = document.getElementById(target_div).innerHTML;          // in case we need to replace it after an error
-       document.getElementById(target_div).innerHTML = 
-               "<div class=\"ctdl-forum-nav\"><i class=\"fas fa-spinner fa-spin\"></i>&nbsp;&nbsp;"
-               + _("Loading messages from server, please wait") + "</div>";
+function forum_readmessages(target_div_name, gt_msg, lt_msg) {
+       target_div = document.getElementById(target_div_name);
+       original_text = target_div.innerHTML;                                   // in case we need to replace it after an error
+       target_div.innerHTML = ""
 
        if (lt_msg < 9999999999) {
                url = "/ctdl/r/" + escapeHTMLURI(current_room) + "/msgs.lt|" + lt_msg;
@@ -31,7 +23,7 @@ function forum_readmessages(target_div, gt_msg, lt_msg) {
                response = await fetch(url);
                msgs = await(response.json());
                if (response.ok) {
-                       document.getElementById(target_div).innerHTML = "" ;
+                       target_div.innerHTML = "" ;
 
                        // If we were given an explicit starting point, by all means start there.
                        // Note that we don't have to remove them from the array because we did a 'msgs gt|xxx' command to Citadel.
@@ -44,40 +36,40 @@ function forum_readmessages(target_div, gt_msg, lt_msg) {
                                if (msgs.length > messages_per_page) {
                                        msgs = msgs.slice(msgs.length - messages_per_page);
                                }
-                               new_old_div_name = randomString(5);
+                               new_old_div_name = randomString();
                                if (msgs.length < 1) {
                                        newlt = lt_msg;
                                }
                                else {
                                        newlt = msgs[0];
                                }
-                               document.getElementById(target_div).innerHTML +=
+                               target_div.innerHTML +=
                                        "<div id=\"" + new_old_div_name + "\">" +
-                                       "<div class=\"ctdl-forum-nav\">" +
-                                       "<a href=\"javascript:forum_readmessages('" + new_old_div_name + "', 0, " + newlt + ");\">" +
+                                       "<div class=\"ctdl-forum-nav\" " +
+                                       "onclick=\"javascript:forum_readmessages('" + new_old_div_name + "', 0, " + newlt + ");\">" +
                                        "<i class=\"fa fa-arrow-circle-up\"></i>&nbsp;&nbsp;" +
-                                       _("Older posts") + "&nbsp;&nbsp;<i class=\"fa fa-arrow-circle-up\"></a></div></div></a></div></div>" ;
+                                       _("Older posts") + "&nbsp;&nbsp;<i class=\"fa fa-arrow-circle-up\"></div></div></a></div></div>" ;
                        }
 
-                       // Render an empty div for each message.  We will fill them in later.
-                       for (var i in msgs) {
-                               document.getElementById(target_div).innerHTML += "<div id=\"ctdl_msg_" + msgs[i] + "\"> </div>" ;
-                               document.getElementById("ctdl_msg_"+msgs[i]).style.display = "none";
-                       }
+                       // The messages will go here.
+                       let msgs_div_name = randomString();
+                       target_div.innerHTML += "<div id=\"" + msgs_div_name + "\"> </div>" ;
+
                        if (lt_msg == 9999999999) {
-                               new_new_div_name = randomString(5);
+                               new_new_div_name = randomString();
                                if (msgs.length <= 0) {
                                        newgt = gt_msg;
                                }
                                else {
                                        newgt = msgs[msgs.length-1];
                                }
-                               document.getElementById(target_div).innerHTML +=
+                               target_div.innerHTML +=
                                        "<div id=\"" + new_new_div_name + "\">" +
-                                       "<div class=\"ctdl-forum-nav\">" +
-                                       "<a href=\"javascript:forum_readmessages('" + new_new_div_name + "', " + newgt + ", 9999999999);\">" +
+                                       "<div id=\"ctdl-newmsg-here\"></div>" +
+                                       "<div class=\"ctdl-forum-nav\" " +
+                                       "onClick=\"javascript:forum_readmessages('" + new_new_div_name + "', " + newgt + ", 9999999999);\">" +
                                        "<i class=\"fa fa-arrow-circle-down\"></i>&nbsp;&nbsp;" +
-                                       _("Newer posts") + "&nbsp;&nbsp;<i class=\"fa fa-arrow-circle-down\"></a></div></div>" ;
+                                       _("Newer posts") + "&nbsp;&nbsp;<i class=\"fa fa-arrow-circle-down\"></div></div>" ;
                        }
 
                        // Now figure out where to scroll to after rendering.
@@ -95,163 +87,272 @@ function forum_readmessages(target_div, gt_msg, lt_msg) {
                        }
 
                        // Render the individual messages in the divs
-                       forum_render_messages(msgs, "ctdl_msg_", scroll_to)
+                       forum_render_messages(msgs, msgs_div_name, scroll_to)
                }
                else {
                        // if xhr fails, this will make the link reappear so the user can try again
-                       document.getElementById(target_div).innerHTML = original_text;
+                       target_div.innerHTML = original_text;
                }
        }
        fetch_msg_list();
+
+       // make the nav buttons appear (post a new message, skip this room, goto next room)
+
+       document.getElementById("ctdl-newmsg-button").innerHTML = `<i class="fa fa-edit"></i>&nbsp;` + _("Post message");
+       document.getElementById("ctdl-newmsg-button").style.display = "block";
+
+       document.getElementById("ctdl-skip-button").innerHTML = _("Skip this room") + `&nbsp;<i class="fa fa-arrow-alt-circle-right"></i>`;
+       document.getElementById("ctdl-skip-button").style.display = "block";
+
+       document.getElementById("ctdl-goto-button").innerHTML = _("Goto next room") + `&nbsp;<i class="fa fa-arrow-circle-right"></i>`;
+       document.getElementById("ctdl-goto-button").style.display = "block";
 }
 
 
-// Render a range of messages, with the div prefix specified
-//
-function forum_render_messages(msgs, prefix, scroll_to) {
-       for (i=0; i<msgs.length; ++i) {
-               forum_render_one(prefix, msgs[i], scroll_to);
+// Render a range of messages into the specified target div
+function forum_render_messages(message_numbers, msgs_div_name, scroll_to) {
+
+       // Build an array of Promises and then wait for them all to resolve.
+       let num_msgs = message_numbers.length;
+       let msg_promises = Array.apply(null, Array(num_msgs));
+       for (i=0; i<num_msgs; ++i) {
+               msg_promises[i] = fetch("/ctdl/r/" + escapeHTMLURI(current_room) + "/" + message_numbers[i] + "/json")
+                       .then(response => response.json())
+                       .catch((error) => {
+                               response => null;
+                       })
+               ;
+       }
+
+       // Here is the async function that waits for all the messages to be loaded, and then renders them.
+       fetch_msg_list = async() => {
+               document.body.style.cursor = "wait";
+               activate_loading_modal();
+               await Promise.all(msg_promises);
+               deactivate_loading_modal();
+               document.body.style.cursor = "default";
+               
+               // At this point all of the Promises are resolved and we can render.
+               // Note: "let" keeps "i" in scope even through the .then scope
+               let scroll_to_div = null;
+               for (let i=0; i<num_msgs; ++i) {
+                       msg_promises[i].then((one_message) => {
+                               let new_msg_div = forum_render_one(one_message, null);
+                               document.getElementById(msgs_div_name).append(new_msg_div);
+                               if (message_numbers[i] == scroll_to) {
+                                       scroll_to_div = new_msg_div;
+                               }
+                               if (i == num_msgs - 1) {
+                                       scroll_to_div.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});
+                               }
+                       });
+               }
+       }
+
+       fetch_msg_list();
+
+       // Make a note of the highest message number we saw, so we can mark it when we "Goto next room"
+       // (Compared to the text client, this is actually more like <A>bandon than <G>oto)
+       if ((num_msgs > 0) && (message_numbers[num_msgs-1] > last_seen)) {
+               last_seen = message_numbers[num_msgs-1];
        }
 }
 
 
-// We have to put each XHR for forum_render_messages() into its own stack frame, otherwise it jumbles them together.  I don't know why.
-function forum_render_one(prefix, msgnum, scroll_to) {
-       fetch_message = async() => {
-               response = await fetch("/ctdl/r/" + escapeHTMLURI(current_room) + "/" + msgs[i] + "/json");
-               msg = await response.json();
-               if (response.ok) {
-                       outmsg =
-                         "<div class=\"ctdl-msg-wrapper\">"                            // begin message wrapper
-                       + "<div class=\"ctdl-avatar\">"                                 // begin avatar
-                       + "<img src=\"/ctdl/u/" + msg.from + "/userpic\" width=\"32\" "
-                       + "onerror=\"this.parentNode.innerHTML='&lt;i class=&quot;fa fa-user-circle fa-2x&quot;&gt;&lt;/i&gt; '\">"
-                       + "</div>"                                                      // end avatar
-                       + "<div class=\"ctdl-msg-content\">"                            // begin content
-                       + "<div class=\"ctdl-msg-header\">"                             // begin header
-                       + "<span class=\"ctdl-msg-header-info\">"                       // begin header info on left side
-                       + "<span class=\"ctdl-username\"><a href=\"#\">"                // FIXME link to user profile
-                       + msg.from
-                       + "</a></span>"                                                 // end username
-                       + "<span class=\"ctdl-msgdate\">"
-                       + msg.time
-                       + "</span>"                                                     // end msgdate
-                       + "</span>"                                                     // end header info on left side
-                       + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
-
-                       + "<span class=\"ctdl-msg-button\">"                            // Reply button FIXME make this work
-                       + "<a href=\"javascript:open_reply_box('"+prefix+"',"+msgnum+",false);\">"
-                       + "<i class=\"fa fa-reply\"></i> " 
-                       + _("Reply")
-                       + "</a></span>"
-
-                       + "<span class=\"ctdl-msg-button\">"                            // ReplyQuoted , only show in forums FIXME
-                       + "<a href=\"javascript:open_reply_box('"+prefix+"',"+msgnum+",true);\">"
-                       + "<i class=\"fa fa-comment\"></i> " 
-                       + _("ReplyQuoted")
-                       + "</a></span>"
-
-                       + "<span class=\"ctdl-msg-button\"><a href=\"#\">"              // Delete , show only with permission FIXME
-                       + "<i class=\"fa fa-trash\"></i> " 
-                       + _("Delete")
-                       + "</a></span>"
+// Render a message.  Returns a div object.
+function forum_render_one(msg, existing_div) {
+       let div = null;
+       if (existing_div != null) {                                             // If an existing div was supplied, render into it
+               div = existing_div;
+       }
+       else {                                                                  // Otherwise, create a new one
+               div = document.createElement("div");
+       }
 
-                       + "</span>";                                                    // end buttons on right side
-                       if (msg.subj) {
-                               outmsg +=
-                               "<br><span class=\"ctdl-msgsubject\">" + msg.subj + "</span>";
-                       }
+       mdiv = randomString();                                                  // Give the div a new name
+       div.id = mdiv;
+
+       try {
+               outmsg =
+                 "<div class=\"ctdl-fmsg-wrapper\">"                           // begin message wrapper
+               + render_userpic(msg.from)                                      // user avatar
+               + "<div class=\"ctdl-fmsg-content\">"                           // begin content
+               + "<div class=\"ctdl-msg-header\">"                             // begin header
+               + "<span class=\"ctdl-msg-header-info\">"                       // begin header info on left side
+               + render_msg_author(msg, views.VIEW_BBS)                        // author
+               + "<span class=\"ctdl-msgdate\">"
+               + string_timestamp(msg.time,0)
+               + "</span>"                                                     // end msgdate
+               + "</span>"                                                     // end header info on left side
+               + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
+       
+               + "<span class=\"ctdl-msg-button\">"                            // Reply
+               + "<a href=\"javascript:open_reply_box('"+mdiv+"',false,'"+msg.wefw+"','"+msg.msgn+"');\">"
+               + "<i class=\"fa fa-reply\"></i> " 
+               + _("Reply")
+               + "</a></span>"
+       
+               + "<span class=\"ctdl-msg-button\">"                            // ReplyQuoted
+               + "<a href=\"javascript:open_reply_box('"+mdiv+"',true,'"+msg.wefw+"','"+msg.msgn+"');\">"
+               + "<i class=\"fa fa-comment\"></i> " 
+               + _("ReplyQuoted")
+               + "</a></span>";
+       
+               if (can_delete_messages) {
                        outmsg +=
-                         "</div><br>"                                                  // end header
-                       + "<div class=\"ctdl-msg-body\">"                               // begin body
-                       + msg.text
-                       + "</div>"                                                      // end body
-                       + "</div>"                                                      // end content
-                       + "</div>"                                                      // end wrapper
-                       ;
-                       document.getElementById(prefix+msgnum).innerHTML = outmsg;
+                       "<span class=\"ctdl-msg-button\">"
+                       + "<a href=\"javascript:forum_delete_message('"+mdiv+"','"+msg.msgnum+"');\">"
+                       + "<i class=\"fa fa-trash\"></i> " 
+                       + _("Delete")
+                       + "</a></span>";
                }
-               else {
-                       document.getElementById(prefix+msgnum).innerHTML = "ERROR";
+       
+               outmsg +=
+                 "</span>";                                                    // end buttons on right side
+               if (msg.subj) {
+                       outmsg +=
+                       "<br><span class=\"ctdl-msgsubject\">" + msg.subj + "</span>";
                }
-               document.getElementById(prefix+msgnum).style.display  = "inline";
-               if (msgnum == scroll_to) {
-                       document.getElementById(prefix+msgnum).scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});
+               outmsg +=
+                 "</div>"                                                      // end header
+               + "<div class=\"ctdl-msg-body\" id=\"" + mdiv + "_body\">"      // begin body
+               + msg.text
+               + "</div>"                                                      // end body
+               + "</div>"                                                      // end content
+               + "</div>"                                                      // end wrapper
+               ;
+       }
+       catch(err) {
+               outmsg = "<div class=\"ctdl-fmsg-wrapper\">" + err.message + "</div>";
+       }
+
+       div.innerHTML = outmsg;
+       return(div);
+}
+
+
+// Compose a references string using existing references plus the message being replied to
+function compose_references(references, msgid) {
+       if (references.includes("@")) {
+               refs = references + "|";
+       }
+       else {
+               refs = "";
+       }
+       refs += msgid;
+
+       // If the resulting string is too big, we can trim it here
+       while (refs.length > 900) {
+               r = refs.split("|");
+               r.splice(1,1);          // remove the second element so we keep the root
+               refs = r.join("|");
+       }
+       return refs;
+}
+
+// Delete a message.
+// We don't bother checking for permission because the button only appears if we have permission,
+// and even if someone hacks the client, the server will deny any unauthorized deletes.
+function forum_delete_message(message_div, message_number) {
+       if (confirm(_("Delete this message?")) == true) {
+               async_forum_delete_message = async() => {
+                       response = await fetch(
+                               "/ctdl/r/" + escapeHTMLURI(current_room) + "/" + message_number,
+                               { method: "DELETE" }
+                       );
+                       if (response.ok) {              // If the server accepted the delete, blank out the message div.
+                               document.getElementById(message_div).outerHTML = "";
+                       }
                }
+               async_forum_delete_message();
        }
-       fetch_message();
 }
 
 
 // Open a reply box directly below a specific message
-function open_reply_box(prefix, msgnum, is_quoted) {
-       target_div_name = prefix+msgnum;
-       new_div_name = prefix + "_reply_to_" + msgnum;
-       document.getElementById(target_div_name).outerHTML += "<div id=\"" + new_div_name + "\">reply box put here</div>";
+function open_reply_box(parent_div, is_quoted, references, msgid) {
+       let new_div = document.createElement("div");
+       let new_div_name = randomString();
+       new_div.id = new_div_name;
+
+       document.getElementById(parent_div).append(new_div);
 
        replybox =
-         "<div class=\"ctdl-msg-wrapper\">"                            // begin message wrapper
+         "<div class=\"ctdl-fmsg-wrapper ctdl-msg-reply\">"            // begin message wrapper
        + "<div class=\"ctdl-avatar\">"                                 // begin avatar
-       + "<img src=\"/ctdl/u/" + "FIXME my name" + "/userpic\" width=\"32\" "
+       + "<img src=\"/ctdl/u/" + current_user + "/userpic\" width=\"32\" "
        + "onerror=\"this.parentNode.innerHTML='&lt;i class=&quot;fa fa-user-circle fa-2x&quot;&gt;&lt;/i&gt; '\">"
        + "</div>"                                                      // end avatar
-       + "<div class=\"ctdl-msg-content\">"                            // begin content
+       + "<div class=\"ctdl-fmsg-content\">"                           // begin content
        + "<div class=\"ctdl-msg-header\">"                             // begin header
        + "<span class=\"ctdl-msg-header-info\">"                       // begin header info on left side
-       + "<span class=\"ctdl-username\"><a href=\"#\">"                // FIXME link to user profile
-       + "FIXME my name"
-       + "</a></span>"                                                 // end username
+       + "<span class=\"ctdl-username\">"
+       + current_user                                                  // user = me !
+       + "</span>"
        + "<span class=\"ctdl-msgdate\">"
-       + "FIXME now time"
-       + "</span>"                                                     // end msgdate
+       + string_timestamp((Date.now() / 1000),0)                       // the current date/time (temporary for display)
+       + "</span>"
        + "</span>"                                                     // end header info on left side
        + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
 
-       + "<span class=\"ctdl-msg-button\">"                            // bold button FIXME make this work
-       + "<a href=\"#\">"
+       + "<span class=\"ctdl-msg-button\">"                            // bold button
+       + "<a href=\"javascript:void(0)\" onclick=\"forum_format('bold')\">"
        + "<i class=\"fa fa-bold fa-fw\"></i>" 
        + "</a></span>"
 
-       + "<span class=\"ctdl-msg-button\">"                            // italic button FIXME make this work
-       + "<a href=\"#\">"
+       + "<span class=\"ctdl-msg-button\">"                            // italic button
+       + "<a href=\"javascript:void(0)\" onclick=\"forum_format('italic')\">" 
        + "<i class=\"fa fa-italic fa-fw\"></i>" 
        + "</a></span>"
 
-       + "<span class=\"ctdl-msg-button\">"                            // list button FIXME make this work
-       + "<a href=\"#\">"
+       + "<span class=\"ctdl-msg-button\">"                            // list button
+       + "<a href=\"javascript:void(0)\" onclick=\"forum_format('insertunorderedlist')\">"
        + "<i class=\"fa fa-list fa-fw\"></i>" 
        + "</a></span>"
 
-       + "<span class=\"ctdl-msg-button\">"                            // link button FIXME make this work
-       + "<a href=\"#\">"
+       + "<span class=\"ctdl-msg-button\">"                            // link button
+       + "<a href=\"javascript:void(0)\" onclick=\"forum_display_urlbox()\">"
        + "<i class=\"fa fa-link fa-fw\"></i>" 
        + "</a></span>"
 
        + "</span>";                                                    // end buttons on right side
-       if (msg.subj) {
-               replybox +=
-               "<br><span class=\"ctdl-msgsubject\">" + "FIXME subject" + "</span>";
-       }
+       //if (msg.subj) {
+               //replybox +=
+               //"<br><span id=\"ctdl-subject\" class=\"ctdl-msgsubject\">" + "FIXME subject" + "</span>";
+       //}
+       //else {                                                                // hidden filed for empty subject
+               replybox += "<span id=\"ctdl-subject\" style=\"display:none\"></span>";
+       //}
        replybox +=
          "</div><br>"                                                  // end header
 
+       + "<span id=\"ctdl-replyreferences\" style=\"display:none\">"   // hidden field for references
+       + compose_references(references,msgid) + "</span>"
                                                                        // begin body
-       + "<div class=\"ctdl-msg-body\" id=\"ctdl-editor-body\" style=\"padding:5px;\" contenteditable=\"true\">"
-       + "\n"                                                          // empty initial content
-       + "</div>"                                                      // end body
+       + "<div class=\"ctdl-msg-body ctdl-forum-editor-body\" id=\"ctdl-editor-body\" contenteditable=\"true\">"
+       + "\n";                                                         // empty initial content
+
+       if (is_quoted) {
+               replybox += "<br><blockquote>"
+                       + document.getElementById(parent_div+"_body").innerHTML
+                       + "</blockquote>";
+       }
+
+       replybox +=
+         "</div>"                                                      // end body
 
        + "<div class=\"ctdl-msg-header\">"                             // begin footer
        + "<span class=\"ctdl-msg-header-info\">"                       // begin footer info on left side
-       + "x"
+       + "&nbsp;"                                                      // (nothing here for now)
        + "</span>"                                                     // end footer info on left side
        + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
 
-       + "<span class=\"ctdl-msg-button\"><a href=\"javascript:save_message('" +  new_div_name + "', '" + msgnum + "');\">"
+       + "<span class=\"ctdl-msg-button\"><a href=\"javascript:forum_save_message('" + new_div_name + "');\">"
        + "<i class=\"fa fa-check\" style=\"color:green\"></i> "        // save button
        + _("Post message")
        + "</a></span>"
 
-       + "<span class=\"ctdl-msg-button\"><a href=\"javascript:cancel_post('" +  new_div_name + "');\">"
+       + "<span class=\"ctdl-msg-button\"><a href=\"javascript:forum_cancel_post('" +  new_div_name + "');\">"
        + "<i class=\"fa fa-trash\" style=\"color:red\"></i> "          // cancel button
        + _("Cancel")
        + "</a></span>"
@@ -262,6 +363,27 @@ function open_reply_box(prefix, msgnum, is_quoted) {
 
        + "</div>"                                                      // end content
        + "</div>"                                                      // end wrapper
+
+       + "<div id=\"forum_url_entry_box\" class=\"ctdl-modal ctdl-forum-urlmodal\">"           // begin URL entry modal
+       + "     <div class=\"ctdl-modal-header\">" 
+       + "             <p>URL:</p>"
+       + "     </div>"
+       + "     <div class=\"ctdl-modal-main\">"
+       + "             <input id=\"ctdl-forum-urlbox\" placeholder=\"http://\" style=\"width:100%\">"
+       + "     </div>"
+       + "     <div class=\"ctdl-modal-footer\">"
+       + "             <p><span class=\"ctdl-msg-button\"><a href=\"javascript:forum_close_urlbox(true);\">"
+       + "                     <i class=\"fa fa-check\" style=\"color:green\"></i> "
+       +                       _("Save")
+       +               "</a></span>"
+       + "             <span class=\"ctdl-msg-button\"><a href=\"javascript:forum_close_urlbox(false);\">"
+       + "                     <i class=\"fa fa-trash\" style=\"color:red\"></i> "
+       +                       _("Cancel")
+       +               "</a></span></p>"
+       + "     </div>"
+       + "     <input id=\"forum_selection_start\" style=\"display:none\"></input>"    // hidden fields
+       + "     <input id=\"forum_selection_end\" style=\"display:none\"></input>"      // to store selection range
+       + "</div>"                                                      // end URL entry modal
        ;
 
        document.getElementById(new_div_name).innerHTML = replybox;
@@ -277,45 +399,55 @@ function open_reply_box(prefix, msgnum, is_quoted) {
 
 
 // Abort a message post (it simply destroys the div)
-function cancel_post(div_name) {
+function forum_cancel_post(div_name) {
        document.getElementById(div_name).outerHTML = "";               // make it cease to exist
 }
 
 
 // Save the posted message to the server
-function save_message(div_name, reply_to_msgnum) {
+function forum_save_message(editor_div_name) {
 
        document.body.style.cursor = "wait";
+       wefw = (document.getElementById("ctdl-replyreferences").innerHTML).replaceAll("|","!"); // references (if present)
+       subj = document.getElementById("ctdl-subject").innerHTML;                               // subject (if present)
 
-       url = "/ctdl/r/" + escapeHTMLURI(current_room) + "/dummy_name_for_new_message";
-       boundary = randomString(20);
-       body_text =
-               "--" + boundary + "\r\n"
-               + "Content-type: text/html\r\n"
-               + "Content-transfer-encoding: quoted-printable\r\n"
-               + "\r\n"
-               + quoted_printable_encode(
-                       "<html><body>" + document.getElementById("ctdl-editor-body").innerHTML + "</body></html>"
-               ) + "\r\n"
-               + "--" + boundary + "--\r\n"
-       ;
+       url = "/ctdl/r/" + escapeHTMLURI(current_room)
+               + "/dummy_name_for_new_message"
+               + "?wefw=" + wefw
+               + "&subj=" + subj
+       body_text = "<html><body>" + document.getElementById("ctdl-editor-body").innerHTML + "</body></html>\r\n";
 
        var request = new XMLHttpRequest();
        request.open("PUT", url, true);
-       request.setRequestHeader("Content-type", "multipart/mixed; boundary=\"" + boundary + "\"");
+       request.setRequestHeader("Content-type", "text/html");
        request.onreadystatechange = function() {
                if (request.readyState == 4) {
                        document.body.style.cursor = "default";
                        if (Math.trunc(request.status / 100) == 2) {
-                               alert("headers: " + request.getAllResponseHeaders());
-                               document.getElementById(div_name).outerHTML = "";               // close the editor
+                               headers = request.getAllResponseHeaders().split("\n");
+                               for (var i in headers) {
+                                       if (headers[i].startsWith("etag: ")) {
+                                               new_msg_num = headers[i].split(" ")[1];
+                                       }
+                               }
+
+                               // After saving the message, load it back from the server and replace the editor div with it.
+                               replace_editor_with_final_message = async() => {
+                                       response = await fetch("/ctdl/r/" + escapeHTMLURI(current_room) + "/" + new_msg_num + "/json");
+                                       if (response.ok) {
+                                               newly_posted_message = await(response.json());
+                                               forum_render_one(newly_posted_message, document.getElementById(editor_div_name));
+                                       }
+                               }
+                               replace_editor_with_final_message();
+
                        }
                        else {
                                error_message = request.responseText;
                                if (error_message.length == 0) {
                                        error_message = _("An error has occurred.");
                                }
-                               alert(error_message);                                           // FIXME make this pretty
+                               alert(error_message);                                           // editor remains open
                        }
                }
        };
@@ -323,33 +455,47 @@ function save_message(div_name, reply_to_msgnum) {
 }
 
 
-// Function to encode data in quoted-printable format
-// Written by Theriault and Brett Zamir [https://locutus.io/php/quoted_printable_encode/]
-function quoted_printable_encode (str) { // eslint-disable-line camelcase
-       const hexChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
-       const RFC2045Encode1IN = / \r\n|\r\n|[^!-<>-~ ]/gm
-       const RFC2045Encode1OUT = function (sMatch) {
-               // Encode space before CRLF sequence to prevent spaces from being stripped
-               // Keep hard line breaks intact; CRLF sequences
-               if (sMatch.length > 1) {
-                       return sMatch.replace(' ', '=20');
-               }
-               // Encode matching character
-               const chr = sMatch.charCodeAt(0);
-               return '=' + hexChars[((chr >>> 4) & 15)] + hexChars[(chr & 15)];
-       }
-       // Split lines to 75 characters; the reason it's 75 and not 76 is because softline breaks are
-       // preceeded by an equal sign; which would be the 76th character. However, if the last line/string
-       // was exactly 76 characters, then a softline would not be needed. PHP currently softbreaks
-       // anyway; so this function replicates PHP.
-       const RFC2045Encode2IN = /.{1,72}(?!\r\n)[^=]{0,3}/g
-       const RFC2045Encode2OUT = function (sMatch) {
-               if (sMatch.substr(sMatch.length - 2) === '\r\n') {
-                       return sMatch;
-               }
-               return sMatch + '=\r\n';
+// Bold, italics, etc.
+function forum_format(command, value) {
+       document.execCommand(command, false, value);
+}
+
+
+// Make the URL entry box appear.
+// When the user clicks into the URL box it will make the previous focus disappear, so we have to save it.
+function forum_display_urlbox() {
+       document.getElementById("forum_selection_start").value = window.getSelection().anchorOffset;
+       document.getElementById("forum_selection_end").value = window.getSelection().focusOffset;
+       document.getElementById("forum_url_entry_box").style.display = "block";
+}
+
+
+// When the URL box is closed, this gets called.  do_save is true for Save, false for Cancel.
+function forum_close_urlbox(do_save) {
+       if (do_save) {
+               var tag = document.getElementById("ctdl-editor-body");
+               var start_replace = document.getElementById("forum_selection_start").value;     // use saved selection range
+               var end_replace = document.getElementById("forum_selection_end").value;
+               new_text = tag.innerHTML.substring(0, start_replace)
+                       + "<a href=\"" + document.getElementById("ctdl-forum-urlbox").value + "\">"
+                       + tag.innerHTML.substring(start_replace, end_replace)
+                       + "</a>"
+                       + tag.innerHTML.substring(end_replace);
+               tag.innerHTML = new_text;
        }
-       str = str.replace(RFC2045Encode1IN, RFC2045Encode1OUT).replace(RFC2045Encode2IN, RFC2045Encode2OUT);
-       // Strip last softline break
-       return str.substr(0, str.length - 3)
+       document.getElementById("ctdl-forum-urlbox").value = "";                                // clear url box for next time
+       document.getElementById("forum_url_entry_box").style.display = "none";
+}
+
+
+// User has clicked the "Post message" button.  This is roughly the same as "reply" except there is no parent message.
+function forum_entmsg() {
+       open_reply_box("ctdl-newmsg-here", false, "", "");
+}
+
+
+// RENDERER FOR THIS VIEW
+function view_render_forums() {
+       document.getElementById("ctdl-main").innerHTML = `<div id="ctdl-mrp" class="ctdl-forum-reading-pane"></div>`;
+       forum_readmessages("ctdl-mrp", 0, 9999999999);
 }