1357929f1808791f9b7ccef8d2490230ef3e3510
[citadel.git] / webcit-ng / static / js / view_forum.js
1 // This module handles the view for "forum" (message board) rooms.
2 //
3 // Copyright (c) 2016-2023 by the citadel.org team
4 //
5 // This program is open source software.  Use, duplication, or
6 // disclosure are subject to the GNU General Public License v3.
7
8
9 // Forum view (flat)
10 function forum_readmessages(target_div_name, gt_msg, lt_msg) {
11         target_div = document.getElementById(target_div_name);
12         original_text = target_div.innerHTML;                                   // in case we need to replace it after an error
13         target_div.innerHTML = ""
14
15         if (lt_msg < 9999999999) {
16                 url = "/ctdl/r/" + escapeHTMLURI(current_room) + "/msgs.lt|" + lt_msg;
17         }
18         else {
19                 url = "/ctdl/r/" + escapeHTMLURI(current_room) + "/msgs.gt|" + gt_msg;
20         }
21
22         fetch_msg_list = async() => {
23                 response = await fetch(url);
24                 msgs = await(response.json());
25                 if (response.ok) {
26                         target_div.innerHTML = "" ;
27
28                         // If we were given an explicit starting point, by all means start there.
29                         // Note that we don't have to remove them from the array because we did a 'msgs gt|xxx' command to Citadel.
30                         if (gt_msg > 0) {
31                                 msgs = msgs.slice(0, messages_per_page);
32                         }
33
34                         // Otherwise, show us the last 20 messages
35                         else {
36                                 if (msgs.length > messages_per_page) {
37                                         msgs = msgs.slice(msgs.length - messages_per_page);
38                                 }
39                                 new_old_div_name = randomString();
40                                 if (msgs.length < 1) {
41                                         newlt = lt_msg;
42                                 }
43                                 else {
44                                         newlt = msgs[0];
45                                 }
46                                 target_div.innerHTML +=
47                                         "<div id=\"" + new_old_div_name + "\">" +
48                                         "<div class=\"ctdl-forum-nav\" " +
49                                         "onclick=\"javascript:forum_readmessages('" + new_old_div_name + "', 0, " + newlt + ");\">" +
50                                         "<i class=\"fa fa-arrow-circle-up\"></i>&nbsp;&nbsp;" +
51                                         _("Older posts") + "&nbsp;&nbsp;<i class=\"fa fa-arrow-circle-up\"></div></div></a></div></div>" ;
52                         }
53
54                         // The messages will go here.
55                         let msgs_div_name = randomString();
56                         target_div.innerHTML += "<div id=\"" + msgs_div_name + "\"> </div>" ;
57
58                         if (lt_msg == 9999999999) {
59                                 new_new_div_name = randomString();
60                                 if (msgs.length <= 0) {
61                                         newgt = gt_msg;
62                                 }
63                                 else {
64                                         newgt = msgs[msgs.length-1];
65                                 }
66                                 target_div.innerHTML +=
67                                         "<div id=\"" + new_new_div_name + "\">" +
68                                         "<div id=\"ctdl-newmsg-here\"></div>" +
69                                         "<div class=\"ctdl-forum-nav\" " +
70                                         "onClick=\"javascript:forum_readmessages('" + new_new_div_name + "', " + newgt + ", 9999999999);\">" +
71                                         "<i class=\"fa fa-arrow-circle-down\"></i>&nbsp;&nbsp;" +
72                                         _("Newer posts") + "&nbsp;&nbsp;<i class=\"fa fa-arrow-circle-down\"></div></div>" ;
73                         }
74
75                         // Now figure out where to scroll to after rendering.
76                         if (gt_msg > 0) {
77                                 scroll_to = msgs[0];
78                         }
79                         else if (lt_msg < 9999999999) {
80                                 scroll_to = msgs[msgs.length-1];
81                         }
82                         else if ( (logged_in) && (gt_msg == 0) && (lt_msg == 9999999999) ) {
83                                 scroll_to = msgs[msgs.length-1];
84                         }
85                         else {
86                                 scroll_to = msgs[0];            // FIXME this is too naive
87                         }
88
89                         // Render the individual messages in the divs
90                         forum_render_messages(msgs, msgs_div_name, scroll_to)
91                 }
92                 else {
93                         // if xhr fails, this will make the link reappear so the user can try again
94                         target_div.innerHTML = original_text;
95                 }
96         }
97         fetch_msg_list();
98
99         // make the nav buttons appear (post a new message, skip this room, goto next room)
100
101         document.getElementById("ctdl-newmsg-button").innerHTML = `<i class="fa fa-edit"></i>&nbsp;` + _("Post message");
102         document.getElementById("ctdl-newmsg-button").style.display = "block";
103
104         document.getElementById("ctdl-skip-button").innerHTML = _("Skip this room") + `&nbsp;<i class="fa fa-arrow-alt-circle-right"></i>`;
105         document.getElementById("ctdl-skip-button").style.display = "block";
106
107         document.getElementById("ctdl-goto-button").innerHTML = _("Goto next room") + `&nbsp;<i class="fa fa-arrow-circle-right"></i>`;
108         document.getElementById("ctdl-goto-button").style.display = "block";
109 }
110
111
112 // Render a range of messages into the specified target div
113 function forum_render_messages(message_numbers, msgs_div_name, scroll_to) {
114
115         // Build an array of Promises and then wait for them all to resolve.
116         let num_msgs = message_numbers.length;
117         let msg_promises = Array.apply(null, Array(num_msgs));
118         for (i=0; i<num_msgs; ++i) {
119                 msg_promises[i] = fetch("/ctdl/r/" + escapeHTMLURI(current_room) + "/" + message_numbers[i] + "/json")
120                         .then(response => response.json())
121                         .catch((error) => {
122                                 response => null;
123                         })
124                 ;
125         }
126
127         // Here is the async function that waits for all the messages to be loaded, and then renders them.
128         fetch_msg_list = async() => {
129                 document.body.style.cursor = "wait";
130                 activate_loading_modal();
131                 await Promise.all(msg_promises);
132                 deactivate_loading_modal();
133                 document.body.style.cursor = "default";
134                 
135                 // At this point all of the Promises are resolved and we can render.
136                 // Note: "let" keeps "i" in scope even through the .then scope
137                 let scroll_to_div = null;
138                 for (let i=0; i<num_msgs; ++i) {
139                         msg_promises[i].then((one_message) => {
140                                 let new_msg_div = forum_render_one(one_message, null);
141                                 document.getElementById(msgs_div_name).append(new_msg_div);
142                                 if (message_numbers[i] == scroll_to) {
143                                         scroll_to_div = new_msg_div;
144                                 }
145                                 if (i == num_msgs - 1) {
146                                         scroll_to_div.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});
147                                 }
148                         });
149                 }
150         }
151
152         fetch_msg_list();
153
154         // Make a note of the highest message number we saw, so we can mark it when we "Goto next room"
155         // (Compared to the text client, this is actually more like <A>bandon than <G>oto)
156         if ((num_msgs > 0) && (message_numbers[num_msgs-1] > last_seen)) {
157                 last_seen = message_numbers[num_msgs-1];
158         }
159 }
160
161
162 // Render a message.  Returns a div object.
163 function forum_render_one(msg, existing_div) {
164         let div = null;
165         if (existing_div != null) {                                             // If an existing div was supplied, render into it
166                 div = existing_div;
167         }
168         else {                                                                  // Otherwise, create a new one
169                 div = document.createElement("div");
170         }
171
172         mdiv = randomString();                                                  // Give the div a new name
173         div.id = mdiv;
174
175         try {
176                 outmsg =
177                   "<div class=\"ctdl-fmsg-wrapper\">"                           // begin message wrapper
178                 + render_userpic(msg.from)                                      // user avatar
179                 + "<div class=\"ctdl-fmsg-content\">"                           // begin content
180                 + "<div class=\"ctdl-msg-header\">"                             // begin header
181                 + "<span class=\"ctdl-msg-header-info\">"                       // begin header info on left side
182                 + render_msg_author(msg, views.VIEW_BBS)                        // author
183                 + "<span class=\"ctdl-msgdate\">"
184                 + string_timestamp(msg.time,0)
185                 + "</span>"                                                     // end msgdate
186                 + "</span>"                                                     // end header info on left side
187                 + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
188         
189                 + "<span class=\"ctdl-msg-button\">"                            // Reply
190                 + "<a href=\"javascript:open_reply_box('"+mdiv+"',false,'"+msg.wefw+"','"+msg.msgn+"');\">"
191                 + "<i class=\"fa fa-reply\"></i> " 
192                 + _("Reply")
193                 + "</a></span>"
194         
195                 + "<span class=\"ctdl-msg-button\">"                            // ReplyQuoted
196                 + "<a href=\"javascript:open_reply_box('"+mdiv+"',true,'"+msg.wefw+"','"+msg.msgn+"');\">"
197                 + "<i class=\"fa fa-comment\"></i> " 
198                 + _("ReplyQuoted")
199                 + "</a></span>";
200         
201                 if (can_delete_messages) {
202                         outmsg +=
203                         "<span class=\"ctdl-msg-button\">"
204                         + "<a href=\"javascript:forum_delete_message('"+mdiv+"','"+msg.msgnum+"');\">"
205                         + "<i class=\"fa fa-trash\"></i> " 
206                         + _("Delete")
207                         + "</a></span>";
208                 }
209         
210                 outmsg +=
211                   "</span>";                                                    // end buttons on right side
212                 if (msg.subj) {
213                         outmsg +=
214                         "<br><span class=\"ctdl-msgsubject\">" + msg.subj + "</span>";
215                 }
216                 outmsg +=
217                   "</div>"                                                      // end header
218                 + "<div class=\"ctdl-msg-body\" id=\"" + mdiv + "_body\">"      // begin body
219                 + msg.text
220                 + "</div>"                                                      // end body
221                 + "</div>"                                                      // end content
222                 + "</div>"                                                      // end wrapper
223                 ;
224         }
225         catch(err) {
226                 outmsg = "<div class=\"ctdl-fmsg-wrapper\">" + err.message + "</div>";
227         }
228
229         div.innerHTML = outmsg;
230         return(div);
231 }
232
233
234 // Compose a references string using existing references plus the message being replied to
235 function compose_references(references, msgid) {
236         if (references.includes("@")) {
237                 refs = references + "|";
238         }
239         else {
240                 refs = "";
241         }
242         refs += msgid;
243
244         // If the resulting string is too big, we can trim it here
245         while (refs.length > 900) {
246                 r = refs.split("|");
247                 r.splice(1,1);          // remove the second element so we keep the root
248                 refs = r.join("|");
249         }
250         return refs;
251 }
252
253 // Delete a message.
254 // We don't bother checking for permission because the button only appears if we have permission,
255 // and even if someone hacks the client, the server will deny any unauthorized deletes.
256 function forum_delete_message(message_div, message_number) {
257         if (confirm(_("Delete this message?")) == true) {
258                 async_forum_delete_message = async() => {
259                         response = await fetch(
260                                 "/ctdl/r/" + escapeHTMLURI(current_room) + "/" + message_number,
261                                 { method: "DELETE" }
262                         );
263                         if (response.ok) {              // If the server accepted the delete, blank out the message div.
264                                 document.getElementById(message_div).outerHTML = "";
265                         }
266                 }
267                 async_forum_delete_message();
268         }
269 }
270
271
272 // Open a reply box directly below a specific message
273 function open_reply_box(parent_div, is_quoted, references, msgid) {
274         let new_div = document.createElement("div");
275         let new_div_name = randomString();
276         new_div.id = new_div_name;
277
278         document.getElementById(parent_div).append(new_div);
279
280         replybox =
281           "<div class=\"ctdl-fmsg-wrapper ctdl-msg-reply\">"            // begin message wrapper
282         + "<div class=\"ctdl-avatar\">"                                 // begin avatar
283         + "<img src=\"/ctdl/u/" + current_user + "/userpic\" width=\"32\" "
284         + "onerror=\"this.parentNode.innerHTML='&lt;i class=&quot;fa fa-user-circle fa-2x&quot;&gt;&lt;/i&gt; '\">"
285         + "</div>"                                                      // end avatar
286         + "<div class=\"ctdl-fmsg-content\">"                           // begin content
287         + "<div class=\"ctdl-msg-header\">"                             // begin header
288         + "<span class=\"ctdl-msg-header-info\">"                       // begin header info on left side
289         + "<span class=\"ctdl-username\">"
290         + current_user                                                  // user = me !
291         + "</span>"
292         + "<span class=\"ctdl-msgdate\">"
293         + string_timestamp((Date.now() / 1000),0)                       // the current date/time (temporary for display)
294         + "</span>"
295         + "</span>"                                                     // end header info on left side
296         + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
297
298         + "<span class=\"ctdl-msg-button\">"                            // bold button
299         + "<a href=\"javascript:void(0)\" onclick=\"forum_format('bold')\">"
300         + "<i class=\"fa fa-bold fa-fw\"></i>" 
301         + "</a></span>"
302
303         + "<span class=\"ctdl-msg-button\">"                            // italic button
304         + "<a href=\"javascript:void(0)\" onclick=\"forum_format('italic')\">" 
305         + "<i class=\"fa fa-italic fa-fw\"></i>" 
306         + "</a></span>"
307
308         + "<span class=\"ctdl-msg-button\">"                            // list button
309         + "<a href=\"javascript:void(0)\" onclick=\"forum_format('insertunorderedlist')\">"
310         + "<i class=\"fa fa-list fa-fw\"></i>" 
311         + "</a></span>"
312
313         + "<span class=\"ctdl-msg-button\">"                            // link button
314         + "<a href=\"javascript:void(0)\" onclick=\"forum_display_urlbox()\">"
315         + "<i class=\"fa fa-link fa-fw\"></i>" 
316         + "</a></span>"
317
318         + "</span>";                                                    // end buttons on right side
319         //if (msg.subj) {
320                 //replybox +=
321                 //"<br><span id=\"ctdl-subject\" class=\"ctdl-msgsubject\">" + "FIXME subject" + "</span>";
322         //}
323         //else {                                                                // hidden filed for empty subject
324                 replybox += "<span id=\"ctdl-subject\" style=\"display:none\"></span>";
325         //}
326         replybox +=
327           "</div><br>"                                                  // end header
328
329         + "<span id=\"ctdl-replyreferences\" style=\"display:none\">"   // hidden field for references
330         + compose_references(references,msgid) + "</span>"
331                                                                         // begin body
332         + "<div class=\"ctdl-msg-body ctdl-forum-editor-body\" id=\"ctdl-editor-body\" contenteditable=\"true\">"
333         + "\n";                                                         // empty initial content
334
335         if (is_quoted) {
336                 replybox += "<br><blockquote>"
337                         + document.getElementById(parent_div+"_body").innerHTML
338                         + "</blockquote>";
339         }
340
341         replybox +=
342           "</div>"                                                      // end body
343
344         + "<div class=\"ctdl-msg-header\">"                             // begin footer
345         + "<span class=\"ctdl-msg-header-info\">"                       // begin footer info on left side
346         + "&nbsp;"                                                      // (nothing here for now)
347         + "</span>"                                                     // end footer info on left side
348         + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
349
350         + "<span class=\"ctdl-msg-button\"><a href=\"javascript:forum_save_message('" + new_div_name + "');\">"
351         + "<i class=\"fa fa-check\" style=\"color:green\"></i> "        // save button
352         + _("Post message")
353         + "</a></span>"
354
355         + "<span class=\"ctdl-msg-button\"><a href=\"javascript:forum_cancel_post('" +  new_div_name + "');\">"
356         + "<i class=\"fa fa-trash\" style=\"color:red\"></i> "          // cancel button
357         + _("Cancel")
358         + "</a></span>"
359
360         + "</span>"                                                     // end buttons on right side
361         + "</div><br>"                                                  // end footer
362
363
364         + "</div>"                                                      // end content
365         + "</div>"                                                      // end wrapper
366
367         + "<div id=\"forum_url_entry_box\" class=\"ctdl-modal ctdl-forum-urlmodal\">"           // begin URL entry modal
368         + "     <div class=\"ctdl-modal-header\">" 
369         + "             <p>URL:</p>"
370         + "     </div>"
371         + "     <div class=\"ctdl-modal-main\">"
372         + "             <input id=\"ctdl-forum-urlbox\" placeholder=\"http://\" style=\"width:100%\">"
373         + "     </div>"
374         + "     <div class=\"ctdl-modal-footer\">"
375         + "             <p><span class=\"ctdl-msg-button\"><a href=\"javascript:forum_close_urlbox(true);\">"
376         + "                     <i class=\"fa fa-check\" style=\"color:green\"></i> "
377         +                       _("Save")
378         +               "</a></span>"
379         + "             <span class=\"ctdl-msg-button\"><a href=\"javascript:forum_close_urlbox(false);\">"
380         + "                     <i class=\"fa fa-trash\" style=\"color:red\"></i> "
381         +                       _("Cancel")
382         +               "</a></span></p>"
383         + "     </div>"
384         + "     <input id=\"forum_selection_start\" style=\"display:none\"></input>"    // hidden fields
385         + "     <input id=\"forum_selection_end\" style=\"display:none\"></input>"      // to store selection range
386         + "</div>"                                                      // end URL entry modal
387         ;
388
389         document.getElementById(new_div_name).innerHTML = replybox;
390         document.getElementById(new_div_name).scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
391
392         // These actions must happen *after* the initial render loop completes.
393         setTimeout(function() {
394                 var tag = document.getElementById("ctdl-editor-body");
395                 tag.focus();                                            // sets the focus
396                 window.getSelection().collapse(tag.firstChild, 0);      // positions the cursor
397         }, 0);
398 }
399
400
401 // Abort a message post (it simply destroys the div)
402 function forum_cancel_post(div_name) {
403         document.getElementById(div_name).outerHTML = "";               // make it cease to exist
404 }
405
406
407 // Save the posted message to the server
408 function forum_save_message(editor_div_name) {
409
410         document.body.style.cursor = "wait";
411         wefw = (document.getElementById("ctdl-replyreferences").innerHTML).replaceAll("|","!"); // references (if present)
412         subj = document.getElementById("ctdl-subject").innerHTML;                               // subject (if present)
413
414         url = "/ctdl/r/" + escapeHTMLURI(current_room)
415                 + "/dummy_name_for_new_message"
416                 + "?wefw=" + wefw
417                 + "&subj=" + subj
418         body_text = "<html><body>" + document.getElementById("ctdl-editor-body").innerHTML + "</body></html>\r\n";
419
420         var request = new XMLHttpRequest();
421         request.open("PUT", url, true);
422         request.setRequestHeader("Content-type", "text/html");
423         request.onreadystatechange = function() {
424                 if (request.readyState == 4) {
425                         document.body.style.cursor = "default";
426                         if (Math.trunc(request.status / 100) == 2) {
427                                 headers = request.getAllResponseHeaders().split("\n");
428                                 for (var i in headers) {
429                                         if (headers[i].startsWith("etag: ")) {
430                                                 new_msg_num = headers[i].split(" ")[1];
431                                         }
432                                 }
433
434                                 // After saving the message, load it back from the server and replace the editor div with it.
435                                 replace_editor_with_final_message = async() => {
436                                         response = await fetch("/ctdl/r/" + escapeHTMLURI(current_room) + "/" + new_msg_num + "/json");
437                                         if (response.ok) {
438                                                 newly_posted_message = await(response.json());
439                                                 forum_render_one(newly_posted_message, document.getElementById(editor_div_name));
440                                         }
441                                 }
442                                 replace_editor_with_final_message();
443
444                         }
445                         else {
446                                 error_message = request.responseText;
447                                 if (error_message.length == 0) {
448                                         error_message = _("An error has occurred.");
449                                 }
450                                 alert(error_message);                                           // editor remains open
451                         }
452                 }
453         };
454         request.send(body_text);
455 }
456
457
458 // Bold, italics, etc.
459 function forum_format(command, value) {
460         document.execCommand(command, false, value);
461 }
462
463
464 // Make the URL entry box appear.
465 // When the user clicks into the URL box it will make the previous focus disappear, so we have to save it.
466 function forum_display_urlbox() {
467         document.getElementById("forum_selection_start").value = window.getSelection().anchorOffset;
468         document.getElementById("forum_selection_end").value = window.getSelection().focusOffset;
469         document.getElementById("forum_url_entry_box").style.display = "block";
470 }
471
472
473 // When the URL box is closed, this gets called.  do_save is true for Save, false for Cancel.
474 function forum_close_urlbox(do_save) {
475         if (do_save) {
476                 var tag = document.getElementById("ctdl-editor-body");
477                 var start_replace = document.getElementById("forum_selection_start").value;     // use saved selection range
478                 var end_replace = document.getElementById("forum_selection_end").value;
479                 new_text = tag.innerHTML.substring(0, start_replace)
480                         + "<a href=\"" + document.getElementById("ctdl-forum-urlbox").value + "\">"
481                         + tag.innerHTML.substring(start_replace, end_replace)
482                         + "</a>"
483                         + tag.innerHTML.substring(end_replace);
484                 tag.innerHTML = new_text;
485         }
486         document.getElementById("ctdl-forum-urlbox").value = "";                                // clear url box for next time
487         document.getElementById("forum_url_entry_box").style.display = "none";
488 }
489
490
491 // User has clicked the "Post message" button.  This is roughly the same as "reply" except there is no parent message.
492 function forum_entmsg() {
493         open_reply_box("ctdl-newmsg-here", false, "", "");
494 }
495
496
497 // RENDERER FOR THIS VIEW
498 function view_render_forums() {
499         document.getElementById("ctdl-main").innerHTML = `<div id="ctdl-mrp" class="ctdl-forum-reading-pane"></div>`;
500         forum_readmessages("ctdl-mrp", 0, 9999999999);
501 }