diff options
| author | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2026-02-16 19:00:16 -0500 |
|---|---|---|
| committer | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2026-02-16 19:00:16 -0500 |
| commit | 3330177ae946da9f347f9ac95ac14025f578a089 (patch) | |
| tree | 727a4864dacab0599fcc983ec80ae2d5bd0975ff | |
| parent | 06bc7adb5e5daa152497efa2fec07334a0126398 (diff) | |
Fix comment reactions
| -rw-r--r-- | jakob/utils/comments.scm | 4 | ||||
| -rw-r--r-- | static/css/style.css | 109 | ||||
| -rw-r--r-- | static/js/comment-reaction.js | 140 |
3 files changed, 187 insertions, 66 deletions
diff --git a/jakob/utils/comments.scm b/jakob/utils/comments.scm index 15824b1..06c15b5 100644 --- a/jakob/utils/comments.scm +++ b/jakob/utils/comments.scm @@ -159,7 +159,9 @@ `(span (@ (class "reaction")) ,(format #f "~a ~a" emote count)))) (comment-reactions comment)) (button (@ (class "add-reaction") (hidden #t) - (aria-label "Add reaction")) "+"))) + (aria-label "Add reaction") + (data-reply-to-id ,(internal-comment-id comment))) + "+"))) '()) ,@(if (and (not reply?) (internal-comment? comment) diff --git a/static/css/style.css b/static/css/style.css index 2fee7bf..b096ca0 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -511,6 +511,115 @@ hr { background: #cc0000; } +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,.4); + justify-content: center; + align-items: center; + z-index: 1000 +} + +.modal-content { + background-color: #fff; + border: 1px solid #666; + box-shadow: 2px 2px 0#ccc; + width: 320px; + padding: 0; + font-family: "Bitstream Vera Sans","DejaVu Sans",Verdana,sans-serif +} + +.modal-header { + background: linear-gradient(to bottom,#333 0,#222 40%,#111 50%,#222 60%,#333 100%); + color: #fff; + padding: 5px 10px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px +} + +#close-modal,#reaction-search { + font-size: 9px; + border: 1px solid #ccc +} + +#close-modal { + background: #eee; + cursor: pointer; + padding: 0 4px +} + +#close-modal:hover { + background: #2e8b57; + color: #fff +} + +.search-container { + padding: 10px; + background: #f9f9f9; + border-bottom: 1px solid #ccc +} + +#reaction-search { + width: 100%; + box-sizing: border-box; + padding: 4px; + font-family: inherit +} + +#reaction-search:focus { + outline: 0; + border-color: #2e8b57 +} + +.reaction-grid { + display: grid; + grid-template-columns: repeat(auto-fill,minmax(35px,1fr)); + gap: 4px; + overflow-y: auto; + max-height: 250px; + padding: 10px; + background: #fff +} + +.reaction-item { + cursor: pointer; + font-size: 1.4rem; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid transparent +} + +.reaction-item:hover { + background-color: #eee; + border: 1px solid #2e8b57; + transform: none +} + +#reaction-trigger { + background: #eee; + border: 1px solid #ccc; + font-size: 9px; + padding: 5px 10px; + cursor: pointer; + text-transform: uppercase; + font-family: inherit +} + +#reaction-trigger:hover { + background: #e0e0e0; + border-color: #999 +} + .legacy-warning { background: linear-gradient(to bottom,#fff3e0 0,#ffe0b2 100%); border: 1px dotted #fb8c00; diff --git a/static/js/comment-reaction.js b/static/js/comment-reaction.js index 4536717..1723c64 100644 --- a/static/js/comment-reaction.js +++ b/static/js/comment-reaction.js @@ -54,80 +54,90 @@ function postReaction(reaction, callback) { xhr.send(encodeAsFormData(reaction)); } -function selectorWidgetName(id) { - return `emoji-picker-${id}`; -} +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'); -// Simple Emoji picker widget. -function selectorWidget(id) { - function emojiButton(codepoint) { - let span = document.createElement("span"); - span.innerHTML = String.fromCodePoint(codepoint); - return span; + 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; + } } - let selector = document.createElement("div"); - selector.setAttribute("id", selectorWidgetName(id)); - selector.setAttribute("class", "emoji-picker"); + 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); + }); + }; - for (let i = 0x1F600; i < 0x1F64F; i++) { - selector.appendChild(emojiButton(i)); - } - selector.appendChild(emojiButton(0x1F37B)); - selector.appendChild(emojiButton(0x1F440)); - selector.appendChild(emojiButton(0x1F446)); - selector.appendChild(emojiButton(0x1F447)); - selector.appendChild(emojiButton(0x1F448)); - selector.appendChild(emojiButton(0x1F449)); - selector.appendChild(emojiButton(0x1F450)); - selector.appendChild(emojiButton(0x270A)); - selector.appendChild(emojiButton(0x270B)); - selector.appendChild(emojiButton(0x270C)); - selector.appendChild(emojiButton(0x270D)); - for (let i = 0x1F90C; i < 0x1F9FF; i++) { - selector.appendChild(emojiButton(i)); - } - return selector; -} + 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); + }; -// Add a "react" button to every local comment. -window.addEventListener("load", () => { - // Unhide additional comment actions if Javascript is enabled. - let additionalActionsRows = document.querySelectorAll(".comment-additional-actions"); - for (let row of additionalActionsRows) { - row.removeAttribute("hidden"); - } + const cleanup = (result) => { + modal.style.display = 'none'; + closeBtn.onclick = null; + searchInput.oninput = null; + resolve(result); + }; - let reactButtons = document.querySelectorAll(".comment-react-button"); - for (let button of reactButtons) { - let id = button.getAttribute("data-reply-to-id"); + searchInput.value = ''; + render(allReactions); + modal.style.display = 'flex'; - button.addEventListener('click', (e) => { - // If we already made a selector widget for this comment, let's "hide" the - // one that exists. - let existingSelectorWidget = document.getElementById(selectorWidgetName(id)); - if (existingSelectorWidget !== null) { - existingSelectorWidget.remove(); - e.preventDefault(); - return; - } + closeBtn.onclick = () => cleanup(null); + searchInput.oninput = handleSearch; - let emojiPicker = selectorWidget(id); - button.parentNode.parentNode.insertBefore(emojiPicker, button.parentNode); - for (let button of emojiPicker.children) { - button.addEventListener('click', (e) => { - postReaction({ - "id": button.getAttribute("data-react-to-id"), - "reaction": button.innerHTML - }, () => { - let reactAcknowledgement = document.createElement("span"); - reactAcknowledgement.innerHTML = "successfully reacted!" - button.replaceWith(reactAcknowledgement); - }); - emojiPicker.remove(); - }); - } + modal.onclick = (e) => { if(e.target === modal) cleanup(null); }; + }); +} +window.addEventListener("load", () => { + const modalHTML = ` +<div id="reaction-modal" class="modal-overlay"> + <div class="modal-content"> + <div class="modal-header"> + <span>Select a reaction</span> + <button id="close-modal">X</button> + </div> + <div class="search-container"> + <input type="text" id="reaction-search" placeholder="Search reactions..." /> + </div> + <div id="reaction-list" class="reaction-grid"></div> + </div> +</div>`; + 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(); }); } |