Began CSS stylizing the mailbox view
[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-2022 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                                         "<a href=\"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\"></a></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                                         "<a href=\"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\"></a></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>" + _("Post message");
102         document.getElementById("ctdl-newmsg-button").style.display = "block";
103
104         document.getElementById("ctdl-skip-button").innerHTML = "<i class=\"fa fa-arrow-alt-circle-right\"></i>" + _("Skip this room");
105         document.getElementById("ctdl-skip-button").style.display = "block";
106
107         document.getElementById("ctdl-goto-button").innerHTML = "<i class=\"fa fa-arrow-circle-right\"></i>" + _("Goto next room");
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                                 console.error('Error: ', error);
124                         })
125                 ;
126         }
127
128         // Here is the async function that waits for all the messages to be loaded, and then renders them.
129         fetch_msg_list = async() => {
130                 document.body.style.cursor = "wait";
131                 activate_loading_modal();
132                 await Promise.all(msg_promises);
133                 deactivate_loading_modal();
134                 document.body.style.cursor = "default";
135                 
136                 // At this point all of the Promises are resolved and we can render.
137                 // Note: "let" keeps "i" in scope even through the .then scope
138                 let scroll_to_div = null;
139                 for (let i=0; i<num_msgs; ++i) {
140                         msg_promises[i].then((one_message) => {
141                                 let new_msg_div = forum_render_one(one_message, null);
142                                 document.getElementById(msgs_div_name).append(new_msg_div);
143                                 if (message_numbers[i] == scroll_to) {
144                                         scroll_to_div = new_msg_div;
145                                 }
146                                 if (i == num_msgs - 1) {
147                                         scroll_to_div.scrollIntoView({behavior: "smooth", block: "start", inline: "nearest"});
148                                 }
149                         });
150                 }
151         }
152
153         fetch_msg_list();
154
155         // Make a note of the highest message number we saw, so we can mark it when we "Goto next room"
156         // (Compared to the text client, this is actually more like <A>bandon than <G>oto)
157         if ((num_msgs > 0) && (message_numbers[num_msgs-1] > last_seen)) {
158                 last_seen = message_numbers[num_msgs-1];
159         }
160 }
161
162
163 // Render a message.  Returns a div object.
164 function forum_render_one(msg, existing_div) {
165         let div = null;
166         if (existing_div != null) {                                             // If an existing div was supplied, render into it
167                 div = existing_div;
168         }
169         else {                                                                  // Otherwise, create a new one
170                 div = document.createElement("div");
171         }
172
173         mdiv = randomString();                                                  // Give the div a new name
174         div.id = mdiv;
175
176         try {
177                 outmsg =
178                   "<div class=\"ctdl-fmsg-wrapper\">"                           // begin message wrapper
179                 + "<div class=\"ctdl-avatar\" onClick=\"javascript:user_profile('" + msg.from + "');\">"
180                 + "<img src=\"/ctdl/u/" + msg.from + "/userpic\" width=\"32\" "
181                 + "onerror=\"this.parentNode.innerHTML='&lt;i class=&quot;fa fa-user-circle fa-2x&quot;&gt;&lt;/i&gt; '\">"
182                 + "</div>"                                                      // end avatar
183                 + "<div class=\"ctdl-fmsg-content\">"                           // begin content
184                 + "<div class=\"ctdl-msg-header\">"                             // begin header
185                 + "<span class=\"ctdl-msg-header-info\">"                       // begin header info on left side
186                 + "<span class=\"ctdl-username\" onClick=\"javascript:user_profile('" + msg.from + "');\">"
187                 + msg.from
188                 + "</a></span>"                                                 // end username
189                 + "<span class=\"ctdl-msgdate\">"
190                 + convertTimestamp(msg.time)
191                 + "</span>"                                                     // end msgdate
192                 + "</span>"                                                     // end header info on left side
193                 + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
194         
195                 + "<span class=\"ctdl-msg-button\">"                            // Reply
196                 + "<a href=\"javascript:open_reply_box('"+mdiv+"',false,'"+msg.wefw+"','"+msg.msgn+"');\">"
197                 + "<i class=\"fa fa-reply\"></i> " 
198                 + _("Reply")
199                 + "</a></span>"
200         
201                 + "<span class=\"ctdl-msg-button\">"                            // ReplyQuoted
202                 + "<a href=\"javascript:open_reply_box('"+mdiv+"',true,'"+msg.wefw+"','"+msg.msgn+"');\">"
203                 + "<i class=\"fa fa-comment\"></i> " 
204                 + _("ReplyQuoted")
205                 + "</a></span>";
206         
207                 if (can_delete_messages) {
208                         outmsg +=
209                         "<span class=\"ctdl-msg-button\">"
210                         + "<a href=\"javascript:forum_delete_message('"+mdiv+"','"+msg.msgnum+"');\">"
211                         + "<i class=\"fa fa-trash\"></i> " 
212                         + _("Delete")
213                         + "</a></span>";
214                 }
215         
216                 outmsg +=
217                   "</span>";                                                    // end buttons on right side
218                 if (msg.subj) {
219                         outmsg +=
220                         "<br><span class=\"ctdl-msgsubject\">" + msg.subj + "</span>";
221                 }
222                 outmsg +=
223                   "</div><br>"                                                  // end header
224                 + "<div class=\"ctdl-msg-body\" id=\"" + mdiv + "_body\">"      // begin body
225                 + msg.text
226                 + "</div>"                                                      // end body
227                 + "</div>"                                                      // end content
228                 + "</div>"                                                      // end wrapper
229                 ;
230         }
231         catch(err) {
232                 outmsg = "<div class=\"ctdl-fmsg-wrapper\">" + err.message + "</div>";
233         }
234
235         div.innerHTML = outmsg;
236         return(div);
237 }
238
239
240 // Compose a references string using existing references plus the message being replied to
241 function compose_references(references, msgid) {
242         if (references.includes("@")) {
243                 refs = references + "|";
244         }
245         else {
246                 refs = "";
247         }
248         refs += msgid;
249
250         // If the resulting string is too big, we can trim it here
251         while (refs.length > 900) {
252                 r = refs.split("|");
253                 r.splice(1,1);          // remove the second element so we keep the root
254                 refs = r.join("|");
255         }
256         return refs;
257 }
258
259 // Delete a message.
260 // We don't bother checking for permission because the button only appears if we have permission,
261 // and even if someone hacks the client, the server will deny any unauthorized deletes.
262 function forum_delete_message(message_div, message_number) {
263         if (confirm(_("Delete this message?")) == true) {
264                 async_forum_delete_message = async() => {
265                         response = await fetch(
266                                 "/ctdl/r/" + escapeHTMLURI(current_room) + "/" + message_number,
267                                 { method: "DELETE" }
268                         );
269                         if (response.ok) {              // If the server accepted the delete, blank out the message div.
270                                 document.getElementById(message_div).outerHTML = "";
271                         }
272                 }
273                 async_forum_delete_message();
274         }
275 }
276
277
278 // Open a reply box directly below a specific message
279 function open_reply_box(parent_div, is_quoted, references, msgid) {
280         let new_div = document.createElement("div");
281         let new_div_name = randomString();
282         new_div.id = new_div_name;
283
284         document.getElementById(parent_div).append(new_div);
285
286         replybox =
287           "<div class=\"ctdl-fmsg-wrapper ctdl-msg-reply\">"            // begin message wrapper
288         + "<div class=\"ctdl-avatar\">"                                 // begin avatar
289         + "<img src=\"/ctdl/u/" + current_user + "/userpic\" width=\"32\" "
290         + "onerror=\"this.parentNode.innerHTML='&lt;i class=&quot;fa fa-user-circle fa-2x&quot;&gt;&lt;/i&gt; '\">"
291         + "</div>"                                                      // end avatar
292         + "<div class=\"ctdl-fmsg-content\">"                           // begin content
293         + "<div class=\"ctdl-msg-header\">"                             // begin header
294         + "<span class=\"ctdl-msg-header-info\">"                       // begin header info on left side
295         + "<span class=\"ctdl-username\">"
296         + current_user                                                  // user = me !
297         + "</span>"
298         + "<span class=\"ctdl-msgdate\">"
299         + convertTimestamp(Date.now() / 1000)                           // the current date/time (temporary for display)
300         + "</span>"
301         + "</span>"                                                     // end header info on left side
302         + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
303
304         + "<span class=\"ctdl-msg-button\">"                            // bold button
305         + "<a href=\"javascript:void(0)\" onclick=\"forum_format('bold')\">"
306         + "<i class=\"fa fa-bold fa-fw\"></i>" 
307         + "</a></span>"
308
309         + "<span class=\"ctdl-msg-button\">"                            // italic button
310         + "<a href=\"javascript:void(0)\" onclick=\"forum_format('italic')\">" 
311         + "<i class=\"fa fa-italic fa-fw\"></i>" 
312         + "</a></span>"
313
314         + "<span class=\"ctdl-msg-button\">"                            // list button
315         + "<a href=\"javascript:void(0)\" onclick=\"forum_format('insertunorderedlist')\">"
316         + "<i class=\"fa fa-list fa-fw\"></i>" 
317         + "</a></span>"
318
319         + "<span class=\"ctdl-msg-button\">"                            // link button
320         + "<a href=\"javascript:void(0)\" onclick=\"forum_display_urlbox()\">"
321         + "<i class=\"fa fa-link fa-fw\"></i>" 
322         + "</a></span>"
323
324         + "</span>";                                                    // end buttons on right side
325         //if (msg.subj) {
326                 //replybox +=
327                 //"<br><span id=\"ctdl-subject\" class=\"ctdl-msgsubject\">" + "FIXME subject" + "</span>";
328         //}
329         //else {                                                                // hidden filed for empty subject
330                 replybox += "<span id=\"ctdl-subject\" style=\"display:none\"></span>";
331         //}
332         replybox +=
333           "</div><br>"                                                  // end header
334
335         + "<span id=\"ctdl-replyreferences\" style=\"display:none\">"   // hidden field for references
336         + compose_references(references,msgid) + "</span>"
337                                                                         // begin body
338         + "<div class=\"ctdl-msg-body\" id=\"ctdl-editor-body\" style=\"padding:5px;\" contenteditable=\"true\">"
339         + "\n";                                                         // empty initial content
340
341         if (is_quoted) {
342                 replybox += "<br><blockquote>"
343                         + document.getElementById(parent_div+"_body").innerHTML
344                         + "</blockquote>";
345         }
346
347         replybox +=
348           "</div>"                                                      // end body
349
350         + "<div class=\"ctdl-msg-header\">"                             // begin footer
351         + "<span class=\"ctdl-msg-header-info\">"                       // begin footer info on left side
352         + "&nbsp;"                                                      // (nothing here for now)
353         + "</span>"                                                     // end footer info on left side
354         + "<span class=\"ctdl-msg-header-buttons\">"                    // begin buttons on right side
355
356         + "<span class=\"ctdl-msg-button\"><a href=\"javascript:forum_save_message('" + new_div_name + "');\">"
357         + "<i class=\"fa fa-check\" style=\"color:green\"></i> "        // save button
358         + _("Post message")
359         + "</a></span>"
360
361         + "<span class=\"ctdl-msg-button\"><a href=\"javascript:forum_cancel_post('" +  new_div_name + "');\">"
362         + "<i class=\"fa fa-trash\" style=\"color:red\"></i> "          // cancel button
363         + _("Cancel")
364         + "</a></span>"
365
366         + "</span>"                                                     // end buttons on right side
367         + "</div><br>"                                                  // end footer
368
369
370         + "</div>"                                                      // end content
371         + "</div>"                                                      // end wrapper
372
373         + "<div id=\"forum_url_entry_box\" class=\"w3-modal\">"         // begin URL entry modal
374         + "     <div class=\"w3-modal-content w3-animate-top w3-card-4\">"
375         + "             <header class=\"w3-container w3-blue\"> "
376         + "                     <p><span>URL:</span></p>"
377         + "             </header>"
378         + "             <div class=\"w3-container w3-blue\">"
379         + "                     <input id=\"forum_txtFormatUrl\" placeholder=\"http://\" style=\"width:100%\">"
380         + "             </div>"
381         + "             <footer class=\"w3-container w3-blue\">"
382         + "                     <p><span class=\"ctdl-msg-button\"><a href=\"javascript:forum_close_urlbox(true);\">"
383         + "                             <i class=\"fa fa-check\" style=\"color:green\"></i> "
384         +                               _("Save")
385         +                       "</a></span>"
386         + "                     <span class=\"ctdl-msg-button\"><a href=\"javascript:forum_close_urlbox(false);\">"
387         + "                             <i class=\"fa fa-trash\" style=\"color:red\"></i> "
388         +                               _("Cancel")
389         +                       "</a></span></p>"
390         + "             </footer>"
391         + "             </div>"
392         + "     </div>"
393         + "     <input id=\"forum_selection_start\" style=\"display:none\"></input>"    // hidden fields
394         + "     <input id=\"forum_selection_end\" style=\"display:none\"></input>"      // to store selection range
395         + "</div>"                                                      // end URL entry modal
396         ;
397
398         document.getElementById(new_div_name).innerHTML = replybox;
399         document.getElementById(new_div_name).scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
400
401         // These actions must happen *after* the initial render loop completes.
402         setTimeout(function() {
403                 var tag = document.getElementById("ctdl-editor-body");
404                 tag.focus();                                            // sets the focus
405                 window.getSelection().collapse(tag.firstChild, 0);      // positions the cursor
406         }, 0);
407 }
408
409
410 // Abort a message post (it simply destroys the div)
411 function forum_cancel_post(div_name) {
412         document.getElementById(div_name).outerHTML = "";               // make it cease to exist
413 }
414
415
416 // Save the posted message to the server
417 function forum_save_message(editor_div_name) {
418
419         document.body.style.cursor = "wait";
420         wefw = (document.getElementById("ctdl-replyreferences").innerHTML).replaceAll("|","!"); // references (if present)
421         subj = document.getElementById("ctdl-subject").innerHTML;                               // subject (if present)
422
423         url = "/ctdl/r/" + escapeHTMLURI(current_room)
424                 + "/dummy_name_for_new_message"
425                 + "?wefw=" + wefw
426                 + "&subj=" + subj
427         boundary = randomString();
428         body_text =
429                 "--" + boundary + "\r\n"
430                 + "Content-type: text/html\r\n"
431                 + "Content-transfer-encoding: quoted-printable\r\n"
432                 + "\r\n"
433                 + quoted_printable_encode(
434                         "<html><body>" + document.getElementById("ctdl-editor-body").innerHTML + "</body></html>"
435                 ) + "\r\n"
436                 + "--" + boundary + "--\r\n"
437         ;
438
439         var request = new XMLHttpRequest();
440         request.open("PUT", url, true);
441         request.setRequestHeader("Content-type", "multipart/mixed; boundary=\"" + boundary + "\"");
442         request.onreadystatechange = function() {
443                 if (request.readyState == 4) {
444                         document.body.style.cursor = "default";
445                         if (Math.trunc(request.status / 100) == 2) {
446                                 headers = request.getAllResponseHeaders().split("\n");
447                                 for (var i in headers) {
448                                         if (headers[i].startsWith("etag: ")) {
449                                                 new_msg_num = headers[i].split(" ")[1];
450                                         }
451                                 }
452
453                                 // After saving the message, load it back from the server and replace the editor div with it.
454                                 replace_editor_with_final_message = async() => {
455                                         response = await fetch("/ctdl/r/" + escapeHTMLURI(current_room) + "/" + new_msg_num + "/json");
456                                         if (response.ok) {
457                                                 newly_posted_message = await(response.json());
458                                                 forum_render_one(newly_posted_message, document.getElementById(editor_div_name));
459                                         }
460                                 }
461                                 replace_editor_with_final_message();
462
463                         }
464                         else {
465                                 error_message = request.responseText;
466                                 if (error_message.length == 0) {
467                                         error_message = _("An error has occurred.");
468                                 }
469                                 alert(error_message);                                           // editor remains open
470                         }
471                 }
472         };
473         request.send(body_text);
474 }
475
476
477 // Bold, italics, etc.
478 function forum_format(command, value) {
479         document.execCommand(command, false, value);
480 }
481
482
483 // Make the URL entry box appear.
484 // When the user clicks into the URL box it will make the previous focus disappear, so we have to save it.
485 function forum_display_urlbox() {
486         document.getElementById("forum_selection_start").value = window.getSelection().anchorOffset;
487         document.getElementById("forum_selection_end").value = window.getSelection().focusOffset;
488         document.getElementById("forum_url_entry_box").style.display = "block";
489 }
490
491
492 // When the URL box is closed, this gets called.  do_save is true for Save, false for Cancel.
493 function forum_close_urlbox(do_save) {
494         if (do_save) {
495                 var tag = document.getElementById("ctdl-editor-body");
496                 var start_replace = document.getElementById("forum_selection_start").value;     // use saved selection range
497                 var end_replace = document.getElementById("forum_selection_end").value;
498                 new_text = tag.innerHTML.substring(0, start_replace)
499                         + "<a href=\"" + document.getElementById("forum_txtFormatUrl").value + "\">"
500                         + tag.innerHTML.substring(start_replace, end_replace)
501                         + "</a>"
502                         + tag.innerHTML.substring(end_replace);
503                 tag.innerHTML = new_text;
504         }
505         document.getElementById("forum_txtFormatUrl").value = "";                               // clear url box for next time
506         document.getElementById("forum_url_entry_box").style.display = "none";
507 }
508
509
510 // User has clicked the "Post message" button.  This is roughly the same as "reply" except there is no parent message.
511 function forum_entmsg() {
512         open_reply_box("ctdl-newmsg-here", false, "", "");
513 }