feature: reactions

This commit is contained in:
Andy Burke 2025-10-15 17:50:48 -07:00
parent b8467ec870
commit 7046bb0389
11 changed files with 371 additions and 133 deletions

40
public/js/_utils.js Normal file
View file

@ -0,0 +1,40 @@
function get_best_coords_for_popup(_options) {
const viewport_width = document.body.getBoundingClientRect().width;
const viewport_height = document.body.getBoundingClientRect().height;
const options = {
offset: {
x: 0,
y: 0,
},
popup: {
width: 200,
height: 200,
},
..._options,
};
const target = options.target ?? {
x:
options.target_element?.getBoundingClientRect().left ??
viewport_width / 2 - options.popup.width / 2,
y:
options.target_element?.getBoundingClientRect().top ??
viewport_height / 2 - options.popup.height / 2,
};
const best_coords = {
x: target.x + options.offset.x,
y: target.y + options.offset.y,
};
if (target.x + options.offset.x + options.popup.width + options.offset.x > viewport_width) {
best_coords.x = Math.max(0, target.x - options.popup.width + options.offset.x);
}
if (target.y + options.offset.y + options.popup.height + options.offset.y > viewport_height) {
best_coords.y = Math.max(0, target.y - options.popup.height + options.offset.y);
}
return best_coords;
}

134
public/js/eventactions.js Normal file
View file

@ -0,0 +1,134 @@
const event_actions_popup_width = 100;
const event_actions_popup_height = 260;
const event_actions_popup_styling = `
#eventactionspopup {
position: fixed;
max-width: 90%;
overflow-x: auto;
z-index: 100;
background: inherit;
overflow: hidden;
border: 1px solid var(--border-normal);
padding: 0.5rem;
}
#eventactionspopup .icon.close {
float: right;
margin: 0.1rem;
}
#eventactionspopup button.event-action {
display: block;
cursor: pointer;
width: 3rem;
height: 3rem;
vertical-align: top;
margin: 0.5rem;
}
#eventactionspopup button .action-name {
display: block;
margin-top: 0.33rem;
text-transform: uppercase;
font-size: xx-small;
}
`;
let event_actions_popup;
let event_actions_popup_form;
let event_actions_popup_search_input;
let event_actions_popup_parent_id_input;
let event_actions_popup_emojis_list;
let event_actions_popup_reaction_input;
function open_event_actions_popup(event) {
const event_id = event.target?.closest("[data-event_id]")?.dataset?.event_id;
const position = get_best_coords_for_popup({
target_element: event.target,
popup: {
width: event_actions_popup_width,
height: event_actions_popup_height,
},
offset: {
x: 4,
y: 4,
},
});
event_actions_popup.dataset.current_event_id = event_id;
event_actions_popup.style.left = position.x + "px";
event_actions_popup.style.top = position.y + "px";
event_actions_popup.style.visibility = "visible";
event_actions_popup.style.opacity = "1";
event_actions_popup.style.display = "block";
}
function clear_event_actions_popup() {
if (!event_actions_popup) {
return;
}
event_actions_popup.style.visibility = "hidden";
event_actions_popup.style.opacity = "0";
event_actions_popup.style.display = "none";
}
document.addEventListener("DOMContentLoaded", () => {
if (!document.getElementById("event-actions-styling")) {
const style = document.createElement("style");
style.id = "event-actions-styling";
style.innerHTML = event_actions_popup_styling;
document.head.appendChild(style);
}
event_actions_popup = document.createElement("div");
event_actions_popup.id = "eventactionspopup";
event_actions_popup.innerHTML = `
<div class="event-actions-container">
<button
class="event-action"
data-action="react"
data-reactions
data-smart
>
<i class="icon circle"></i><span class="action-name">React</span>
</button>
<button
class="event-action"
data-action="reply"
>
<i class="icon reply"></i><span class="action-name">Reply</span>
</button>
<button class="event-action mockup" data-action="forward_copy">
<i class="icon forward-copy"></i
><span class="action-name">Copy Link</span>
</button>
<button class="event-action mockup" data-action="delete">
<i class="icon trash"></i><span class="action-name">Delete</span>
</button>
</div>
`;
document.body.appendChild(event_actions_popup);
document.querySelector("body").addEventListener("click", (event) => {
const is_in_the_event_actions_popup = event?.target?.closest("#eventactionspopup");
if (is_in_the_event_actions_popup) {
return;
}
const is_an_event_actions_button =
event?.target?.matches("button[commandfor]") ||
event?.target?.closest("button[commandfor]");
if (!is_an_event_actions_button) {
clear_event_actions_popup();
return;
}
event.preventDefault();
open_event_actions_popup(event);
});
});

View file

@ -54,44 +54,50 @@ const reactions_popup_styling = `
background: inherit;
}
`;
function get_best_coords_for_popup(target, offset = { x: 10, y: 10 }) {
const target_x = target?.getBoundingClientRect().left ?? 0;
const target_y = target?.getBoundingClientRect().top ?? 0;
const viewport_width = document.body.getBoundingClientRect().width;
const viewport_height = document.body.getBoundingClientRect().height;
const best_coords = {
x: target_x + offset.x,
y: target_y + offset.y,
};
if (target_x + offset.x + reactions_popup_width + offset.x > viewport_width) {
best_coords.x = Math.max(0, target_x - reactions_popup_width);
}
if (target_y + offset.y + reactions_popup_height + offset.y > viewport_height) {
best_coords.y = Math.max(0, target_y - reactions_popup_height);
}
return best_coords;
.reaction-container {
display: inline-block;
border: 1px solid var(--border-subtle);
border-radius: var(--border-radius);
margin-right: 0.5rem;
padding: 0.25rem;
font-size: large;
}
@media screen and (max-width: 480px) {
#reactionspopup {
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
top: auto !important;
width: 100% !important;
}
}
`;
let reactions_popup;
let reactions_popup_form;
let reactions_popup_search_input;
let reactions_popup_parent_id_input;
let reactions_popup_emojis_list;
let reactions_popup_reaction_input;
function open_reactions_popup(event) {
const parent_event_id = event.target?.closest("[data-event_id]")?.dataset?.event_id;
const parent_event_id =
event.target?.closest("[data-current_event_id]")?.dataset?.current_event_id;
reactions_popup_parent_id_input.value = parent_event_id ?? "";
const position = get_best_coords_for_popup(event.target.closest("[data-reactions]"), {
x: 25,
y: 25,
const position = get_best_coords_for_popup({
target_element: event.target.closest("[data-reactions]"),
popup: {
width: reactions_popup_width,
height: reactions_popup_height,
},
offset: {
x: 25,
y: 25,
},
});
reactions_popup.style.left = position.x + "px";
@ -100,6 +106,8 @@ function open_reactions_popup(event) {
reactions_popup.style.visibility = "visible";
reactions_popup.style.opacity = "1";
reactions_popup.style.display = "block";
reactions_popup_search_input.focus();
}
function clear_reactions_popup() {
@ -129,8 +137,8 @@ document.addEventListener("DOMContentLoaded", () => {
id="reactions-selection-form"
data-smart="true"
method="POST"
on_reply="async (event) => { await document.querySelectorAll( '[data-feed]' ).forEach((feed) => feed.__render(event)); }"
on_parsed="async (event) => { await document.querySelectorAll( '[data-feed]' ).forEach((feed) => feed.__render(event)); }"
on_reply="async (event) => { clear_reactions_popup(); await document.querySelectorAll( '[data-feed]' ).forEach((feed) => feed.__render(event)); }"
on_parsed="async (event) => { clear_reactions_popup(); await document.querySelectorAll( '[data-feed]' ).forEach((feed) => feed.__render(event)); }"
>
<input id="reactions-search-input" name="search" type="text" placeholder="Search..." data-skip="true" />
@ -193,6 +201,7 @@ document.addEventListener("DOMContentLoaded", () => {
: "";
});
reactions_popup_search_input = document.getElementById("reactions-search-input");
reactions_popup_parent_id_input = reactions_popup_form.querySelector('[name="parent_id"]');
reactions_popup_emojis_list = document.getElementById("reactions-emojis-list");
reactions_popup_reaction_input = reactions_popup_form.querySelector('[name="data.reaction"]');
@ -216,14 +225,14 @@ document.addEventListener("DOMContentLoaded", () => {
: "";
});
const reactions_popup_search = debounce((event) => {
const debounced_search = debounce((event) => {
const prompt = event.target?.value;
const filtered = EMOJIS.autocomplete(prompt);
delete emojis_list.dataset.filtered;
delete reactions_popup_emojis_list.dataset.filtered;
if (filtered.length) {
emojis_list.dataset.filtered = true;
emojis_list.querySelectorAll("li").forEach((li) => {
reactions_popup_emojis_list.dataset.filtered = true;
reactions_popup_emojis_list.querySelectorAll("li").forEach((li) => {
if (filtered.some((entry) => entry[0] === li.dataset.emoji)) {
li.dataset.filtered = true;
} else {
@ -233,17 +242,16 @@ document.addEventListener("DOMContentLoaded", () => {
}
}, 200);
document
.getElementById("reactions-search-input")
.addEventListener("input", reactions_popup_search);
document
.getElementById("reactions-search-input")
.addEventListener("paste", reactions_popup_search);
document
.getElementById("reactions-search-input")
.addEventListener("change", reactions_popup_search);
reactions_popup_search_input.addEventListener("input", debounced_search);
reactions_popup_search_input.addEventListener("paste", debounced_search);
reactions_popup_search_input.addEventListener("change", debounced_search);
document.querySelector("body").addEventListener("click", (event) => {
const is_in_the_reactions_form = event?.target?.closest("#reactionspopup");
if (is_in_the_reactions_form) {
return;
}
const is_a_data_reactions_child = event?.target?.closest("[data-reactions]");
if (!is_a_data_reactions_child) {
clear_reactions_popup();

View file

@ -14,6 +14,10 @@ function smarten_feeds() {
continue;
}
feed.__started = false;
feed.__newest_id = undefined;
feed.__oldest_id = undefined;
feed.__templates = feed
.querySelectorAll("template[data-for_type]")
.values()
@ -76,6 +80,20 @@ function smarten_feeds() {
feed.__autoscroll_debounce_timeout = undefined;
feed.__render = async (item) => {
const [item_type, item_id] = item.id?.split(":", 2) ?? [];
feed.__newest_id =
typeof item_id === "string" && item_id > (feed.__newest_id ?? "")
? item_id
: feed.__newest_id;
feed.__oldest_id =
typeof item_id === "string" &&
item_id <
(feed.__oldest_id ??
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")
? item_id
: feed.__oldest_id;
const template = feed.__templates[item.type];
if (!template) {
return;
@ -96,18 +114,6 @@ function smarten_feeds() {
`[data-temp_id='${item.temp_id ?? item.meta?.temp_id ?? ""}']`,
);
feed.__newest_id =
typeof item.id === "string" && item.id > (feed.__newest_id ?? "")
? item.id
: feed.__newest_id;
feed.__oldest_id =
typeof item.id === "string" &&
item.id <
(feed.__oldest_id ??
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")
? item.id
: feed.__oldest_id;
if (existing_element) {
if (
existing_element.id !== item.id ||