244 lines
6.7 KiB
JavaScript
244 lines
6.7 KiB
JavaScript
let smarten_feeds_debounce_timeout;
|
|
function smarten_feeds() {
|
|
if (smarten_feeds_debounce_timeout) {
|
|
clearTimeout(smarten_feeds_debounce_timeout);
|
|
}
|
|
|
|
smarten_feeds_debounce_timeout = setTimeout(() => {
|
|
smarten_feeds_debounce_timeout = undefined;
|
|
|
|
const feeds = document?.body?.querySelectorAll("[data-feed]:not([data-smartened])") ?? [];
|
|
for (const feed of feeds) {
|
|
if (!feed.dataset.source) {
|
|
console.warn("No source url for smart feed: " + feed);
|
|
continue;
|
|
}
|
|
|
|
const feed_item_template = feed.querySelector("template");
|
|
if (!feed_item_template) {
|
|
console.warn("No template for smart feed: " + feed);
|
|
continue;
|
|
}
|
|
|
|
feed.__start = () => {
|
|
feed.__started = true;
|
|
feed.__update();
|
|
};
|
|
|
|
feed.__stop = async () => {
|
|
feed.__started = false;
|
|
|
|
if (feed.__request_abort_controller) {
|
|
await feed.__request_abort_controller.abort("smartfeed:stopped");
|
|
delete feed.__request_abort_controller;
|
|
}
|
|
|
|
if (feed.__refresh_interval) {
|
|
clearInterval(feed.__refresh_interval);
|
|
delete feed.__refresh_interval;
|
|
}
|
|
};
|
|
|
|
feed.__clear = () => {
|
|
const children_to_remove = Array.from(feed.childNodes).filter(
|
|
(node) => !["script", "template"].includes(node.tagName),
|
|
);
|
|
children_to_remove.forEach((element) => feed.removeChild(element));
|
|
|
|
feed.__newest_id = undefined;
|
|
feed.__oldest_id = undefined;
|
|
};
|
|
|
|
feed.__reset = async () => {
|
|
const was_started = feed.__started;
|
|
|
|
await feed.__stop();
|
|
feed.__clear();
|
|
|
|
if (was_started) {
|
|
setTimeout(feed.__start, 1);
|
|
}
|
|
};
|
|
|
|
feed.__precheck = () => {
|
|
return feed.dataset.precheck ? eval(feed.dataset.precheck) : true;
|
|
};
|
|
|
|
feed.__target = (item) => {
|
|
return feed.__target_element?.(item) ?? feed;
|
|
};
|
|
|
|
feed.__autoscroll_debounce_timeout = undefined;
|
|
feed.__render = async (item) => {
|
|
feed.__context =
|
|
feed.__context ??
|
|
(feed.dataset.context ? new Function(feed.dataset.context) : undefined);
|
|
const context = feed.__context ? await feed.__context(item, feed) : {};
|
|
|
|
const rendered_html = eval("`" + feed_item_template.innerHTML.trim() + "`");
|
|
|
|
const existing_element =
|
|
feed.querySelector("#" + item.id?.replace(/([:\.])/g, "\\$1")) ??
|
|
feed.querySelector("#" + item.temp_id?.replace(/([:\.])/g, "\\$1")) ??
|
|
feed.querySelector("#" + item.meta?.temp_id?.replace(/([:\.])/g, "\\$1")) ??
|
|
feed.querySelector(
|
|
`[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 ||
|
|
rendered_html !== existing_element.outerHTML
|
|
) {
|
|
existing_element.outerHTML = rendered_html;
|
|
}
|
|
} else {
|
|
const target = feed.__target(item);
|
|
switch (feed.dataset.insert ?? "append") {
|
|
case "prepend":
|
|
target.insertAdjacentHTML("afterbegin", rendered_html);
|
|
break;
|
|
case "append":
|
|
target.insertAdjacentHTML("beforeend", rendered_html);
|
|
break;
|
|
default:
|
|
throw new Error(
|
|
'data-insert must be "append" or "prepend" for smart feeds',
|
|
);
|
|
break;
|
|
}
|
|
|
|
if (feed.dataset.autoscroll) {
|
|
if (feed.__autoscroll_debounce_timeout) {
|
|
clearTimeout(feed.__autoscroll_debounce_timeout);
|
|
}
|
|
|
|
feed.__autoscroll_debounce_timeout = setTimeout(() => {
|
|
feed.scrollTo({
|
|
behavior: "auto",
|
|
left: 0,
|
|
top:
|
|
(feed.dataset.insert ?? "append") === "append"
|
|
? feed.scrollHeight
|
|
: 0,
|
|
});
|
|
feed.__autoscroll_debounce_timeout = undefined;
|
|
}, 15);
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: if the feed is scrolled to one extreme, load more elements?
|
|
feed.__update = function () {
|
|
if (!feed.__started) {
|
|
return;
|
|
}
|
|
|
|
feed.__attempts_since_last_successful_update =
|
|
1 + (feed.__attempts_since_last_successful_update ?? 0);
|
|
|
|
if (!feed.__precheck()) {
|
|
return;
|
|
}
|
|
|
|
feed.__request_abort_controller =
|
|
feed.__request_abort_controller || new AbortController();
|
|
|
|
const url = eval("`" + feed.dataset.source + "`");
|
|
|
|
api.fetch(url, {
|
|
signal: feed.__request_abort_controller.signal,
|
|
})
|
|
.then(async (new_items_response) => {
|
|
if (!new_items_response.ok) {
|
|
const body = await new_items_response.json();
|
|
throw new Error(body.message ?? body.error?.message ?? "Uknown error.");
|
|
}
|
|
|
|
const new_items = await new_items_response.json();
|
|
|
|
const sorted_items = feed.dataset.reverse ? new_items.reverse() : new_items;
|
|
for await (const item of sorted_items) {
|
|
await feed.__render(item);
|
|
}
|
|
|
|
if (/^\d+$/.test(feed.dataset.maxlength ?? "")) {
|
|
const maxlength = parseInt(feed.dataset.maxlength, 10);
|
|
switch (feed.dataset.insert ?? "append") {
|
|
case "prepend":
|
|
/* remove children at the end */
|
|
target.children.forEach((element, index) =>
|
|
index > maxlength ? element.remove() : null,
|
|
);
|
|
break;
|
|
case "append":
|
|
/* remove children at the beginning */
|
|
const cutoff = target.children.length - maxlength;
|
|
target.children.forEach((element, index) =>
|
|
index < cutoff ? element.remove() : null,
|
|
);
|
|
break;
|
|
default:
|
|
throw new Error(
|
|
'data-insert must be "append" or "prepend" for smart feeds',
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
feed.dataset.hydrated = true;
|
|
feed.dataset.last_update = new Date().toISOString();
|
|
feed.__attempts_since_last_successful_update = 0;
|
|
})
|
|
.catch((error) => {
|
|
if (error === "smartfeed:stopped") {
|
|
return;
|
|
}
|
|
|
|
feed.dataset.error = JSON.stringify(error);
|
|
console.trace(error);
|
|
})
|
|
.finally(() => {
|
|
if (feed.__started && feed.dataset.longpolling) {
|
|
setTimeout(
|
|
feed.__update,
|
|
/* logarithmic backoff */
|
|
Math.log(feed.__attempts_since_last_successful_update || 1) *
|
|
10 /* scale it a bit */ *
|
|
1_000 /* 1s in ms */,
|
|
);
|
|
}
|
|
});
|
|
};
|
|
|
|
if (feed.dataset.refresh) {
|
|
const refresh_frequency = parseInt(feed.dataset.refresh, 10);
|
|
feed.__refresh_interval = setInterval(() => {
|
|
feed.__update();
|
|
}, refresh_frequency);
|
|
}
|
|
|
|
feed.dataset.smartened = true;
|
|
|
|
feed.__start();
|
|
}
|
|
}, 10);
|
|
}
|
|
|
|
const smarten_feeds_observer = new MutationObserver(smarten_feeds);
|
|
smarten_feeds_observer.observe(document, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|