/* * comment-reaction.js -- Simple emoji picker and glue for reaction endpoint * Copyright © 2023 Jakob L. Kreuze * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * . */ function encodeAsFormData(obj) { // Turn the data object into an array of URL-encoded key/value pairs. let urlEncodedDataPairs = []; for (let key in obj) { urlEncodedDataPairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])); } return urlEncodedDataPairs.join('&'); } function postReaction(reaction, callback) { let xhr = new XMLHttpRequest(); xhr.open('POST', "/api/comment/react", true); // Endpoint expects HTML form data. xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.onload = function(data) { switch (xhr.status) { case 200: callback(); break; case 429: alert("Failed -- you can react at most once an hour."); break; default: let resp = JSON.parse(xhr.response); alert("Failed -- " + resp.error); break; } } xhr.onerror = function(error) { throw new Error(`Request failed: ${error}`); } xhr.send(encodeAsFormData(reaction)); } let allReactions = null; async function promptForReaction() { const modal = document.getElementById('reaction-modal'); const searchInput = document.getElementById('reaction-search'); const listContainer = document.getElementById('reaction-list'); const closeBtn = document.getElementById('close-modal'); if (!allReactions) { try { const response = await fetch('/static/data-by-emoji.json'); allReactions = await response.json(); } catch (e) { console.error("Failed to load reactions", e); return null; } } return new Promise((resolve) => { const render = (data) => { listContainer.innerHTML = ''; Object.entries(data).forEach(([reaction, info]) => { const span = document.createElement('span'); span.className = 'reaction-item'; span.textContent = reaction; span.title = info.name; span.onclick = () => cleanup(reaction); listContainer.appendChild(span); }); }; const handleSearch = (e) => { const term = e.target.value.toLowerCase(); const filtered = Object.fromEntries( Object.entries(allReactions).filter(([_, info]) => info.name.toLowerCase().includes(term) || info.slug.toLowerCase().includes(term) ) ); render(filtered); }; const cleanup = (result) => { modal.style.display = 'none'; closeBtn.onclick = null; searchInput.oninput = null; resolve(result); }; searchInput.value = ''; render(allReactions); modal.style.display = 'flex'; closeBtn.onclick = () => cleanup(null); searchInput.oninput = handleSearch; modal.onclick = (e) => { if(e.target === modal) cleanup(null); }; }); } window.addEventListener("load", () => { const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); const reactButtons = document.querySelectorAll(".add-reaction"); for (const button of reactButtons) { button.removeAttribute("hidden"); button.addEventListener('click', async (e) => { let reaction = await promptForReaction(); console.log(reaction); postReaction({ "id": button.getAttribute("data-reply-to-id"), "reaction": reaction }, window.location.reload); e.preventDefault(); }); } });