service.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. const sendButton = document.querySelector("button#send");
  2. const username =
  3. new URLSearchParams(window.location.search).get("u") || "Anonymous";
  4. const isStreamMode = new URLSearchParams(window.location.search).get("stream") === "1";
  5. if (isStreamMode) {
  6. document.body.classList.add("stream-mode");
  7. }
  8. sendButton.addEventListener("click", send);
  9. let cooldownTime = 0;
  10. let cooldownInterval = null;
  11. function send() {
  12. if (cooldownTime > 0) return;
  13. const messageText = window.getCurrentText ? window.getCurrentText() : "";
  14. let data = canvas.toDataURL("image/png");
  15. // Crop white space top and bottom if there's no text preventing it
  16. if (!messageText || messageText.trim() === "") {
  17. const ctx = canvas.getContext("2d");
  18. const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  19. const data32 = new Uint32Array(imgData.data.buffer);
  20. let top = canvas.height;
  21. let bottom = -1;
  22. for (let y = 0; y < canvas.height; y++) {
  23. let rowHasPixel = false;
  24. for (let x = 0; x < canvas.width; x++) {
  25. const p = data32[y * canvas.width + x];
  26. // ABGR: 0xFFFFFFFF is white. 0x00000000 is transparent.
  27. if (p !== 0xFFFFFFFF && p !== 0) {
  28. rowHasPixel = true;
  29. break;
  30. }
  31. }
  32. if (rowHasPixel) {
  33. if (y < top) top = y;
  34. if (y > bottom) bottom = y;
  35. }
  36. }
  37. if (top <= bottom) {
  38. // Add a little padding
  39. const padding = 10;
  40. top = Math.max(0, top - padding);
  41. bottom = Math.min(canvas.height - 1, bottom + padding);
  42. const cropHeight = bottom - top + 1;
  43. const tempCanvas = document.createElement("canvas");
  44. tempCanvas.width = canvas.width;
  45. tempCanvas.height = cropHeight;
  46. const tempCtx = tempCanvas.getContext("2d");
  47. tempCtx.putImageData(ctx.getImageData(0, top, canvas.width, cropHeight), 0, 0);
  48. data = tempCanvas.toDataURL("image/png");
  49. } else {
  50. // Canvas is entirely white/blank
  51. const tempCanvas = document.createElement("canvas");
  52. tempCanvas.width = canvas.width;
  53. tempCanvas.height = 20; // extremely minimal height
  54. const tempCtx = tempCanvas.getContext("2d");
  55. tempCtx.fillStyle = "white";
  56. tempCtx.fillRect(0, 0, canvas.width, 20);
  57. data = tempCanvas.toDataURL("image/png");
  58. }
  59. }
  60. const payload = {
  61. from: username,
  62. text: messageText,
  63. doodle: data,
  64. color: userColor,
  65. timestamp: new Date().toISOString(),
  66. };
  67. fetch("/api/messages", {
  68. method: "POST",
  69. headers: {
  70. "Content-Type": "application/json",
  71. },
  72. body: JSON.stringify(payload),
  73. }).then(response => {
  74. if (response.status === 401) {
  75. window.location.href = "/";
  76. }
  77. });
  78. // clear canvas after sending
  79. if (window.resetCanvas) {
  80. window.resetCanvas();
  81. } else {
  82. canvas.width = canvas.width;
  83. window.clearCurrentText();
  84. }
  85. // start cooldown
  86. cooldownTime = 60;
  87. sendButton.disabled = true;
  88. sendButton.innerHTML = `wait ${cooldownTime}s`;
  89. cooldownInterval = setInterval(() => {
  90. cooldownTime--;
  91. if (cooldownTime <= 0) {
  92. clearInterval(cooldownInterval);
  93. sendButton.disabled = false;
  94. sendButton.innerHTML = "send";
  95. } else {
  96. sendButton.innerHTML = `wait ${cooldownTime}s`;
  97. }
  98. }, 1000);
  99. }
  100. function addToChatLog(msg) {
  101. const chatLog = document.getElementById("chat-log");
  102. const messageBox = document.createElement("div");
  103. messageBox.className = "message-box";
  104. if (msg.color && msg.color.startsWith('#')) {
  105. messageBox.style.borderColor = msg.color;
  106. messageBox.style.boxShadow = `0 4px 12px ${msg.color}33`;
  107. } else {
  108. messageBox.classList.add(`accent-${msg.color}`);
  109. }
  110. const messageHeader = document.createElement("div");
  111. messageHeader.className = "message-header";
  112. if (msg.color && msg.color.startsWith('#')) {
  113. messageHeader.style.color = msg.color;
  114. messageHeader.style.borderBottomColor = msg.color;
  115. }
  116. const usernameSpan = document.createElement("span");
  117. usernameSpan.className = "username";
  118. usernameSpan.textContent = msg.from;
  119. messageHeader.appendChild(usernameSpan);
  120. messageBox.appendChild(messageHeader);
  121. const canvasDiv = document.createElement("div");
  122. canvasDiv.className = "chat-canvas";
  123. const messageTextSpan = document.createElement("span");
  124. messageTextSpan.className = "message-text";
  125. messageTextSpan.innerHTML = msg.text.replace(/\n/g, "<br>");
  126. canvasDiv.appendChild(messageTextSpan);
  127. if (msg.doodle) {
  128. const img = document.createElement("img");
  129. img.src = msg.doodle;
  130. canvasDiv.appendChild(img);
  131. }
  132. messageBox.appendChild(canvasDiv);
  133. animatePreviousMessages(chatLog, false);
  134. chatLog.insertBefore(messageBox, chatLog.firstChild);
  135. }
  136. function addSystemMessageToChatLog(content) {
  137. const chatLog = document.getElementById("chat-log");
  138. const messageBox = document.createElement("div");
  139. messageBox.className = "system-message-box";
  140. messageBox.innerHTML = content;
  141. animatePreviousMessages(chatLog, true);
  142. chatLog.insertBefore(messageBox, chatLog.firstChild);
  143. }
  144. function animatePreviousMessages(chatLog, newIsSystem) {
  145. const previousMessages = chatLog.querySelectorAll(
  146. ".message-box, .system-message-box"
  147. );
  148. previousMessages.forEach((message) => {
  149. const prevIsSystem = message.classList.contains("system-message-box");
  150. const startTranslate = prevIsSystem
  151. ? newIsSystem
  152. ? "translateY(calc(100% + 5px))"
  153. : "translateY(calc(500% + 5px))"
  154. : newIsSystem
  155. ? "translateY(calc(20% + 5px))"
  156. : "translateY(calc(100% + 5px))";
  157. message.animate(
  158. [{ transform: startTranslate }, { transform: "translateY(0%)" }],
  159. {
  160. duration: 100,
  161. easing: "linear",
  162. fill: "forwards",
  163. }
  164. );
  165. });
  166. }
  167. // run when a new message is received from the server
  168. function handleMessage(msg) {
  169. const data = JSON.parse(msg);
  170. if (data.type === "system") {
  171. addSystemMessageToChatLog(data.content);
  172. return;
  173. }
  174. addToChatLog(data);
  175. }
  176. // listens for new messages from the server
  177. async function listen() {
  178. try {
  179. const url = isStreamMode ? "/api/messages?stream=1" : "/api/messages";
  180. const response = await fetch(url);
  181. if (response.status === 401 && !isStreamMode) {
  182. window.location.href = "/";
  183. return;
  184. }
  185. if (response.status === 200) {
  186. const msg = await response.text();
  187. handleMessage(msg);
  188. }
  189. } catch (err) {
  190. console.error("error listening messages:", err);
  191. } finally {
  192. // goes back to listening just after receiving a message or on error
  193. setTimeout(listen, 50); // small delay to avoid stack overflow
  194. }
  195. }
  196. // load backlog first, then start listening
  197. async function loadBacklog() {
  198. try {
  199. const url = isStreamMode ? "/api/backlog?stream=1" : "/api/backlog";
  200. const response = await fetch(url);
  201. if (response.status === 401 && !isStreamMode) {
  202. window.location.href = "/";
  203. return;
  204. }
  205. if (response.status === 200) {
  206. const messages = await response.json();
  207. // append chronologically
  208. for (const msg of messages) {
  209. if (msg.type === "system") {
  210. addSystemMessageToChatLog(msg.content);
  211. } else {
  212. addToChatLog(msg);
  213. }
  214. }
  215. }
  216. } catch (err) {
  217. console.error("error loading backlog:", err);
  218. }
  219. }
  220. // start listening for messages right after loading the app backlog
  221. loadBacklog().then(() => listen());
  222. // sets the username label in the user message box
  223. const usernameLabel = document.querySelector(
  224. "#user-message-box > .message-header > .username"
  225. );
  226. usernameLabel.innerHTML = username;