add: chat message reactions

This commit is contained in:
trisua 2025-06-21 03:11:29 -04:00
parent a4298f95f6
commit a37312fecf
20 changed files with 557 additions and 25 deletions

View file

@ -109,6 +109,23 @@
("title" "Send")
(text "{{ icon \"send-horizontal\" }}"))))
(text "{%- endif %}")
; emoji picker
(text "{{ components::emoji_picker(element_id=\"\", render_dialog=true, render_button=false) }}")
(input ("id" "react_emoji_picker_field") ("class" "hidden") ("type" "hidden"))
(script
(text "window.EMOJI_PICKER_MODE = \"replace\";
document.getElementById(\"react_emoji_picker_field\").addEventListener(\"change\", (e) => {
if (!EMOJI_PICKER_REACTION_MESSAGE_ID) {
return;
}
const emoji = e.target.value === \"::\" ? \":heart:\" : e.target.value;
trigger(\"me::message_react\", [document.getElementById(`message-${EMOJI_PICKER_REACTION_MESSAGE_ID}`), EMOJI_PICKER_REACTION_MESSAGE_ID, emoji]);
});"))
; ...
(script
(text "window.CURRENT_PAGE = Number.parseInt(\"{{ page }}\");
window.VIEWING_SINGLE = \"{{ message }}\".length > 0;
@ -434,6 +451,7 @@
const clean_text = () => {
trigger(\"atto::clean_date_codes\");
trigger(\"atto::hooks::online_indicator\");
trigger(\"atto::hooks::check_message_reactions\");
};
document.addEventListener(

View file

@ -1026,9 +1026,29 @@
(text "{%- endif %}")
(div
("class" "flex w-full gap-2 justify-between")
(span
("class" "no_p_margin")
(text "{{ message.content|markdown|safe }}"))
(div
("class" "flex flex-col gap-2")
(span
("class" "no_p_margin")
(text "{{ message.content|markdown|safe }}"))
(div
("class" "flex w-full gap-1 flex-wrap")
("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
("hook" "check_message_reactions")
("hook-arg:id" "{{ message.id }}")
(text "{% for emoji,num in message.reactions -%}")
(button
("class" "small lowered")
("ui_ident" "emoji_{{ emoji }}")
("onclick" "trigger('me::message_react', [event.target.parentElement.parentElement, '{{ message.id }}', '{{ emoji }}'])")
(span (text "{{ emoji|emojis|safe }} {{ num }}")))
(text "{%- endfor %}")
(div
("class" "hidden")
(text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}"))))
(text "{% if grouped -%}")
(div
("class" "hidden")
@ -1185,13 +1205,15 @@
(text "{{ text \"chats:action.kick_member\" }}")))))
(text "{%- endif %}"))
(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false) -%}")
(text "{%- endmacro %} {% macro emoji_picker(element_id, render_dialog=false, render_button=true, small=false) -%}")
(text "{% if render_button -%}")
(button
("class" "button small square lowered")
("class" "button small {% if not small -%} square {%- endif %} lowered")
("onclick" "window.EMOJI_PICKER_TEXT_ID = '{{ element_id }}'; document.getElementById('emoji_dialog').showModal()")
("title" "Emojis")
("type" "button")
(text "{{ icon \"smile-plus\" }}"))
(text "{%- endif %}")
(text "{% if render_dialog -%}")
(dialog
@ -1237,20 +1259,41 @@
}
if (event.detail.unicode) {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value += ` :${await (
await fetch(\"/api/v1/lookup_emoji\", {
method: \"POST\",
body: event.detail.unicode,
})
).text()}:`;
if (window.EMOJI_PICKER_MODE === \"replace\") {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value = `:${await (
await fetch(\"/api/v1/lookup_emoji\", {
method: \"POST\",
body: event.detail.unicode,
})
).text()}:`;
} else {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value += ` :${await (
await fetch(\"/api/v1/lookup_emoji\", {
method: \"POST\",
body: event.detail.unicode,
})
).text()}:`;
}
} else {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value += ` :${event.detail.emoji.shortcodes[0]}:`;
if (window.EMOJI_PICKER_MODE === \"replace\") {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value = `:${event.detail.emoji.shortcodes[0]}:`;
} else {
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).value += ` :${event.detail.emoji.shortcodes[0]}:`;
}
}
document.getElementById(
window.EMOJI_PICKER_TEXT_ID,
).dispatchEvent(new Event(\"change\"));
document.getElementById(\"emoji_dialog\").close();
});"))
(div

View file

@ -688,6 +688,36 @@ media_theme_pref();
$.OBSERVERS.push(observer);
});
self.define("hooks::check_message_reactions", async ({ $ }) => {
const observer = $.offload_work_to_client_when_in_view(
async (element) => {
const reactions = await (
await fetch(
`/api/v1/message_reactions/${element.getAttribute("hook-arg:id")}`,
)
).json();
if (reactions.ok) {
for (const reaction of reactions.payload) {
element
.querySelector(
`[ui_ident=emoji_${reaction.emoji.replaceAll(":", "\\:")}]`,
)
.classList.remove("lowered");
}
}
},
);
for (const element of Array.from(
document.querySelectorAll("[hook=check_message_reactions]") || [],
)) {
observer.observe(element);
}
$.OBSERVERS.push(observer);
});
self.define("hooks::tabs:switch", (_, tab) => {
tab = tab.split("?")[0];

View file

@ -204,6 +204,47 @@
});
});
self.define("message_react", async (_, element, message, emoji) => {
await trigger("atto::debounce", ["reactions::toggle"]);
fetch("/api/v1/message_reactions", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message,
emoji,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
if (res.message.includes("created")) {
const x = element.querySelector(
`[ui_ident=emoji_${emoji.replaceAll(":", "\\:")}]`,
);
if (x) {
x.classList.remove("lowered");
}
} else {
const x = element.querySelector(
`[ui_ident=emoji_${emoji.replaceAll(":", "\\:")}]`,
);
if (x) {
x.classList.add("lowered");
}
}
}
});
});
self.define("remove_notification", (_, id) => {
fetch(`/api/v1/notifications/${id}`, {
method: "DELETE",