A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

209 lines
5.6 KiB

  1. const textarea = document.querySelector("textarea.body");
  2. if (window.location.pathname.endsWith("/reply")) {
  3. // Auto-focus body and scroll to bottom
  4. textarea.focus();
  5. textarea.setSelectionRange(textarea.value.length, textarea.value.length);
  6. textarea.scrollTop = textarea.scrollHeight;
  7. }
  8. const sendButton = document.getElementById("send-button"),
  9. saveButton = document.getElementById("save-button");
  10. const composeForm = document.getElementById("compose-form");
  11. const sendProgress = document.getElementById("send-progress");
  12. composeForm.addEventListener("submit", ev => {
  13. [...document.querySelectorAll("input, textarea")].map(
  14. i => i.setAttribute("readonly", "readonly"));
  15. sendProgress.style.display = 'flex';
  16. });
  17. saveButton.addEventListener("click", ev => {
  18. sendProgress.querySelector(".info").innerText = "Saving draft...";
  19. });
  20. let attachments = [];
  21. const headers = document.querySelector(".create-update .headers");
  22. headers.classList.remove("no-js");
  23. const attachmentsNode = document.getElementById("attachment-list");
  24. attachmentsNode.style.display = '';
  25. const helpNode = attachmentsNode.querySelector(".help");
  26. const attachmentsInput = headers.querySelector("input[type='file']");
  27. attachmentsInput.removeAttribute("name");
  28. attachmentsInput.addEventListener("input", ev => {
  29. const files = attachmentsInput.files;
  30. for (let i = 0; i < files.length; i++) {
  31. attachFile(files[i]);
  32. }
  33. });
  34. window.addEventListener("dragenter", dragNOP);
  35. window.addEventListener("dragleave", dragNOP);
  36. window.addEventListener("dragover", dragNOP);
  37. window.addEventListener("drop", ev => {
  38. ev.preventDefault();
  39. const files = ev.dataTransfer.files;
  40. for (let i = 0; i < files.length; i++) {
  41. attachFile(files[i]);
  42. }
  43. });
  44. function dragNOP(e) {
  45. e.stopPropagation();
  46. e.preventDefault();
  47. }
  48. const attachmentUUIDsNode = document.getElementById("attachment-uuids");
  49. function updateState() {
  50. let complete = true;
  51. for (let i = 0; i < attachments.length; i++) {
  52. const a = attachments[i];
  53. const progress = a.node.querySelector(".progress");
  54. progress.style.width = `${Math.floor(a.progress * 100)}%`;
  55. complete &= a.progress === 1.0;
  56. if (a.progress === 1.0) {
  57. progress.style.display = 'none';
  58. }
  59. }
  60. if (complete) {
  61. sendButton.removeAttribute("disabled");
  62. saveButton.removeAttribute("disabled");
  63. } else {
  64. sendButton.setAttribute("disabled", "disabled");
  65. saveButton.setAttribute("disabled", "disabled");
  66. }
  67. attachmentUUIDsNode.value = attachments.
  68. filter(a => a.progress === 1.0).
  69. map(a => a.uuid).
  70. join(",");
  71. }
  72. function attachFile(file) {
  73. helpNode.remove();
  74. const xhr = new XMLHttpRequest();
  75. const node = attachmentNodeFor(file);
  76. const attachment = {
  77. node: node,
  78. progress: 0,
  79. xhr: xhr,
  80. };
  81. attachments.push(attachment);
  82. attachmentsNode.appendChild(node);
  83. node.querySelector("button").addEventListener("click", ev => {
  84. attachment.xhr.abort();
  85. attachments = attachments.filter(a => a !== attachment);
  86. node.remove();
  87. updateState();
  88. if (typeof attachment.uuid !== "undefined") {
  89. const cancel = new XMLHttpRequest();
  90. cancel.open("POST", `/compose/attachment/${attachment.uuid}/remove`);
  91. cancel.send();
  92. }
  93. });
  94. let formData = new FormData();
  95. formData.append("attachments", file);
  96. const handleError = msg => {
  97. attachments = attachments.filter(a => a !== attachment);
  98. node.classList.add("error");
  99. node.querySelector(".progress").remove();
  100. node.querySelector(".size").remove();
  101. node.querySelector("button").remove();
  102. node.querySelector(".error").innerText = "Error: " + msg;
  103. updateState();
  104. };
  105. xhr.open("POST", "/compose/attachment");
  106. xhr.upload.addEventListener("progress", ev => {
  107. attachment.progress = ev.loaded / ev.total;
  108. updateState();
  109. });
  110. xhr.addEventListener("load", () => {
  111. let resp;
  112. try {
  113. resp = JSON.parse(xhr.responseText);
  114. } catch {
  115. resp = { "error": "Error: invalid response" };
  116. }
  117. if (xhr.status !== 200) {
  118. handleError(resp["error"]);
  119. return;
  120. }
  121. attachment.uuid = resp[0];
  122. updateState();
  123. });
  124. xhr.addEventListener("error", () => {
  125. handleError("an unexpected problem occured");
  126. });
  127. xhr.send(formData);
  128. updateState();
  129. }
  130. function attachmentNodeFor(file) {
  131. const node = document.createElement("div"),
  132. progress = document.createElement("span"),
  133. filename = document.createElement("span"),
  134. error = document.createElement("span"),
  135. size = document.createElement("span"),
  136. button = document.createElement("button");
  137. node.classList.add("upload");
  138. progress.classList.add("progress");
  139. node.appendChild(progress);
  140. filename.classList.add("filename");
  141. filename.innerText = file.name;
  142. node.appendChild(filename);
  143. error.classList.add("error");
  144. node.appendChild(error);
  145. size.classList.add("size");
  146. size.innerText = formatSI(file.size) + "B";
  147. node.appendChild(size);
  148. button.innerHTML = "&times";
  149. node.appendChild(button);
  150. return node;
  151. }
  152. // via https://github.com/ThomWright/format-si-prefix; MIT license
  153. // Copyright (c) 2015 Thom Wright
  154. const PREFIXES = {
  155. '24': 'Y', '21': 'Z', '18': 'E', '15': 'P', '12': 'T', '9': 'G', '6': 'M',
  156. '3': 'k', '0': '', '-3': 'm', '-6': 'µ', '-9': 'n', '-12': 'p', '-15': 'f',
  157. '-18': 'a', '-21': 'z', '-24': 'y'
  158. };
  159. function formatSI(num) {
  160. if (num === 0) {
  161. return '0';
  162. }
  163. let sig = Math.abs(num); // significand
  164. let exponent = 0;
  165. while (sig >= 1000 && exponent < 24) {
  166. sig /= 1000;
  167. exponent += 3;
  168. }
  169. while (sig < 1 && exponent > -24) {
  170. sig *= 1000;
  171. exponent -= 3;
  172. }
  173. const signPrefix = num < 0 ? '-' : '';
  174. if (sig > 1000) {
  175. return signPrefix + sig.toFixed(0) + PREFIXES[exponent];
  176. }
  177. return signPrefix + parseFloat(sig.toPrecision(3)) + PREFIXES[exponent];
  178. }