add better reaction popup
All checks were successful
Android Build / publish (push) Successful in 32s
Linux Build / publish (push) Successful in 50s

This commit is contained in:
olcxja 2026-06-18 08:11:38 +02:00
commit ecf7e3b4ce
14 changed files with 31442 additions and 14 deletions

Binary file not shown.

View file

@ -124,6 +124,7 @@
"messages.decrypt.failed": "failed to decrypt meow :c",
"title.reply.message": "meow back",
"title.react.message": "purr",
"title.text.react": "text purr",
"title.delete.message": "hiss away",
"title.replied.to": "meowed back to",
"title.replying.to": "meowing back to",

View file

@ -125,6 +125,7 @@
"messages.decrypt.failed": "Failed to decrypt message",
"title.reply.message": "Reply",
"title.react.message": "React",
"title.text.react": "Text React",
"title.delete.message": "Delete",
"title.replied.to": "Replied to",
"title.replying.to": "Replying to",

15644
webroot/emotes.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1057,8 +1057,10 @@ async function ensureUserKeys() {
async function mainJS() {
await initBlahs();
await loadEmotes();
passwordHash = await hashSHA3_512(password);
if (localStorage.getItem('lang') != null) {
lang = localStorage.getItem('lang');
}
@ -2503,13 +2505,76 @@ async function replyMessage(msgId) {
input.focus();
}
var reactionPickerMenuHtml = "";
var loadedEmotes = [];
async function loadEmotes() {
try {
let res = await fetch("emotes.json");
let emotesObj = await res.json();
let skinTones = ['\u{1F3FB}', '\u{1F3FC}', '\u{1F3FD}', '\u{1F3FE}', '\u{1F3FF}'];
loadedEmotes = Object.keys(emotesObj).flatMap(char => {
let baseEmote = { char: char, name: emotesObj[char].name, slug: emotesObj[char].slug };
if (emotesObj[char].skin_tone_support) {
let variants = skinTones.map(tone => ({
char: char + tone,
name: emotesObj[char].name + " skin tone",
slug: emotesObj[char].slug
}));
return [baseEmote, ...variants];
}
return [baseEmote];
});
reactionPickerMenuHtml = `
<div style="display: flex; flex-direction: column; gap: 0.4rem; padding: 0.2rem; min-width: 14rem;">
<input type="text" id="reaction-picker-search" placeholder="Type text / emoji..." oninput="filterReactions(this.value)" onkeydown="if(event.key === 'Enter') { if(this.value.trim() !== '') reactMessagePrompt(currentContextMenuMsgId, encodeURIComponent(this.value)); }" style="width: 100%; box-sizing: border-box; padding: 0.5rem 0.6rem; border-radius: 0.65rem; border: var(--border-width) solid var(--light-border-color); background: transparent; color: var(--text-color); font-family: inherit;">
<div id="reaction-picker-grid" style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.2rem; max-height: 12rem; overflow-y: auto; overflow-x: hidden;">
${loadedEmotes.map(emoji => `<button onclick="reactMessagePrompt(currentContextMenuMsgId, encodeURIComponent('${emoji.char}'))" data-name="${emoji.name}" style="min-width: unset; width: 100%; aspect-ratio: 1; display: flex; justify-content: center; align-items: center; padding: 0; font-size: 1.2rem; border-radius: 0.65rem;" class="reaction-emoji-btn">${emoji.char}</button>`).join('')}
</div>
<button onclick="reactMessagePrompt(currentContextMenuMsgId, encodeURIComponent(document.getElementById('reaction-picker-search').value))" style="margin: 0; padding: 0.5rem; justify-content: center; border-radius: 0.65rem; width: 100%;">
<span class="blah">title.text.react</span>
</button>
</div>
`;
} catch (e) {
console.error("Failed to load emotes", e);
}
}
function filterReactions(query) {
let grid = document.getElementById("reaction-picker-grid");
if (!grid) return;
let buttons = grid.querySelectorAll(".reaction-emoji-btn");
let lowerQuery = query.toLowerCase();
buttons.forEach(btn => {
let name = btn.getAttribute("data-name");
if (name && name.toLowerCase().indexOf(lowerQuery) > -1) {
btn.style.display = "flex";
} else {
btn.style.display = "none";
}
});
}
function openReactionPicker(e) {
if (e) {
e.stopPropagation();
e.preventDefault();
}
if (!reactionPickerMenuHtml) return;
let rect = fixedContextMenu.getBoundingClientRect();
showFixedContextMenu({top: rect.top, right: rect.left, bottom: rect.top, left: rect.left}, reactionPickerMenuHtml);
let searchInput = document.getElementById('reaction-picker-search');
if (searchInput) searchInput.focus();
}
async function reactMessagePrompt(msgId, quickReaction = null) {
if (fixedContextMenu) fixedContextMenu.classList.remove("show");
if (typeof clearContextMenuStyles === "function") clearContextMenuStyles();
let reaction = quickReaction;
if (!reaction) {
reaction = prompt("Enter reaction (emoji or text):");
if (!reaction) return;
return;
} else {
reaction = decodeURIComponent(reaction);
}

View file

@ -225,15 +225,13 @@ var addSpaceMenu = `
<button onclick="gotoCreateSpace()">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M80-120v-720h400v160h400v320h-80v-240H480v80h80v80h-80v80h80v80h-80v80h160v80H80Zm80-80h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm160 480h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80ZM800-40v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM640-440v-80h80v80h-80Zm0 160v-80h80v80h-80Z"/></svg>
<span class="blah">title.create.space</span>
</button>
`;
</button>`
var messageContextMenu = `
<button onclick="replyMessage(currentContextMenuMsgId)">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M760-200v-160q0-50-35-85t-85-35H273l144 144-57 56-240-240 240-240 57 56-144 144h367q83 0 141.5 58.5T840-360v160h-80Z"/></svg>
<span class="blah">title.reply.message</span>
</button>
<button onclick="reactMessagePrompt(currentContextMenuMsgId)">
<button onclick="openReactionPicker(event)">
<svg xmlns="http://www.w3.org/2000/svg" width="1.4rem" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M480-260q70 0 126.5-40.5T682-400H278q19 59 75.5 99.5T480-260Zm-160-220q25 0 42.5-17.5T380-540q0-25-17.5-42.5T320-600q-25 0-42.5 17.5T260-540q0 25 17.5 42.5T320-480Zm320 0q25 0 42.5-17.5T700-540q0-25-17.5-42.5T640-600q-25 0-42.5 17.5T580-540q0 25 17.5 42.5T640-480ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
<span class="blah">title.react.message</span>
</button>
@ -246,3 +244,4 @@ var messageContextMenu = `
//elements
var detailsBtn = `<button class="mobile-nav-btn right" onclick="mobileNavDetails()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg></button>`;
var backBtnHtml = `<button class="mobile-nav-btn" onclick="mobileNavBack()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--text-color)"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg></button>`;

View file

@ -2,6 +2,10 @@
font-family: 'Nunito';
src: URL('Nunito-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'NotoColorEmoji';
src: URL('NotoColorEmoji-Regular.ttf') format('truetype');
}
:root {
--main-bg-color: rgb(20, 20, 20);
--text-color: rgb(240, 240, 245);
@ -66,7 +70,7 @@ loading {
padding: 0;
transition: all 0.2s ease;
color: var(--text-color);
font-family: "Nunito", sans-serif;
font-family: "Nunito", "NotoColorEmoji", sans-serif;
-webkit-tap-highlight-color: transparent;
user-select: none !important;
-webkit-user-drag: none !important;