diff --git a/src/assets/css/oom-styles.css b/src/assets/css/oom-styles.css
new file mode 100644
index 0000000..c4cffdd
--- /dev/null
+++ b/src/assets/css/oom-styles.css
@@ -0,0 +1,93 @@
+/* https://github.com/oom-components/mastodon-comments, under MIT License */
+oom-comments {
+ display: block;
+ padding: 2em;
+}
+oom-comments ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+oom-comments li {
+ margin: 32px 0;
+}
+oom-comments article {
+ max-width: 600px;
+}
+oom-comments ul ul {
+ margin-left: 64px;
+}
+oom-comments .comment-avatar {
+ width: 50px;
+ height: 50px;
+ border-radius: 6px;
+ float: left;
+ margin-right: 14px;
+ box-shadow: 0 0 1px #0009;
+}
+oom-comments .comment-user {
+ color: currentColor;
+ text-decoration: none;
+ display: block;
+ position: relative;
+}
+oom-comments .comment-author {
+ position: absolute;
+ left: 35px;
+ top: 35px;
+ background: white;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ color: gray;
+}
+oom-comments .comment-user:hover .comment-username {
+ text-decoration: underline;
+}
+oom-comments .comment-username {
+ margin-right: 0.5em;
+}
+oom-comments .comment-useraddress {
+ color: gray;
+ font-size: small;
+ font-style: normal;
+}
+oom-comments .comment-time {
+ font-size: small;
+ display: flex;
+ align-items: center;
+ column-gap: 0.4em;
+}
+oom-comments .comment-time svg {
+ width: 1em;
+ height: 1em;
+ fill: gray;
+}
+oom-comments .comment-address {
+ color: currentColor;
+ text-decoration: none;
+ display: block;
+ margin-top: 0.25em;
+}
+oom-comments .comment-address:hover {
+ text-decoration: underline;
+}
+oom-comments .comment-body {
+ margin-top: 0.5em;
+ margin-left: 64px;
+ line-height: 1.5;
+}
+oom-comments .comment-body p {
+ margin: 0.5em 0;
+}
+oom-comments .comment-counts {
+ display: flex;
+ column-gap: 1em;
+ font-size: small;
+}
+oom-comments .comment-counts > span {
+ display: flex;
+ align-items: center;
+ column-gap: 0.3em;
+ color: gray;
+}
diff --git a/src/assets/js/oom-comments.js b/src/assets/js/oom-comments.js
new file mode 100644
index 0000000..404a483
--- /dev/null
+++ b/src/assets/js/oom-comments.js
@@ -0,0 +1,312 @@
+/* https://github.com/oom-components/mastodon-comments, under MIT License */
+// © https://phosphoricons.com/
+export const icons = {
+ reblog:
+ ``,
+ favourite:
+ ``,
+ author:
+ ``,
+
+ // @ https://simpleicons.org/
+ mastodon:
+ ``,
+
+ pleroma:
+ ``,
+
+ bluesky:
+ ``,
+};
+
+export default class SocialComments extends HTMLElement {
+ comments = {};
+
+ async connectedCallback() {
+ const lang = this.closest("[lang]")?.lang || navigator.language || "en";
+
+ this.dateTimeFormatter = new Intl.DateTimeFormat(lang, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+
+ const mastodon = this.getAttribute("mastodon") || this.getAttribute("src");
+ const bluesky = this.getAttribute("bluesky");
+
+ await Promise.all([
+ mastodon && this.#fetchMastodon(new URL(mastodon)),
+ bluesky && this.#fetchBluesky(new URL(bluesky)),
+ ]);
+
+ this.refresh();
+ }
+
+ refresh() {
+ const comments = [
+ ...this.comments.mastodon || [],
+ ...this.comments.bluesky || [],
+ ].sort(
+ (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
+ );
+
+ if (comments.length) {
+ this.innerHTML = "";
+ this.render(this, comments);
+ }
+ }
+
+ async #fetchBluesky(url) {
+ const { pathname } = url;
+
+ const [, handle, rkey] = pathname.match(
+ /\/profile\/([\w\.]+)\/post\/(\w+)/,
+ );
+
+ if (!handle || !rkey) {
+ return;
+ }
+
+ const options = {
+ ttl: Number(this.getAttribute("cache") || 0),
+ };
+
+ const didData = await fetchJSON(
+ `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`,
+ options,
+ );
+ const uri = `at://${didData.did}/app.bsky.feed.post/${rkey}`;
+
+ this.comments.bluesky = dataFromBluesky(
+ await fetchJSON(
+ `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${uri}`,
+ options,
+ ),
+ );
+ }
+
+ async #fetchMastodon(url) {
+ const { origin, pathname } = url;
+ let id;
+
+ const source = pathname.includes("/notice/") ? "pleroma" : "mastodon";
+
+ if (source === "pleroma") {
+ [, id] = pathname.match(/^\/notice\/([^\/?#]+)/);
+ } else {
+ [, id] = pathname.match(/\/(\d+)$/);
+ }
+
+ if (!id) {
+ return;
+ }
+
+ const token = this.getAttribute("token");
+ const options = {
+ ttl: Number(this.getAttribute("cache") || 0),
+ };
+ if (token) {
+ options.headers = {
+ Authorization: `Bearer ${token}`,
+ };
+ }
+
+ const user = url.pathname.split("/")[1];
+ const author = `${user}@${url.hostname}`;
+
+ const comments = dataFromMastodon(
+ await fetchJSON(
+ new URL(`${origin}/api/v1/statuses/${id}/context`),
+ options,
+ ),
+ author,
+ source,
+ );
+
+ this.comments.mastodon = comments.filter((comment) =>
+ comment.parent === id
+ );
+ }
+
+ render(container, replies) {
+ const ul = document.createElement("ul");
+
+ for (const reply of replies) {
+ const comment = document.createElement("li");
+ comment.innerHTML = this.renderComment(reply);
+
+ if (reply.replies.length) {
+ this.render(comment, reply.replies);
+ }
+ ul.appendChild(comment);
+ }
+
+ container.appendChild(ul);
+ }
+
+ renderComment(comment) {
+ return `
+
+ `;
+ }
+}
+
+function formatEmojis(html, emojis) {
+ emojis.forEach(({ shortcode, static_url, url }) => {
+ html = html.replace(
+ `:${shortcode}:`,
+ `
+
+
+ `,
+ );
+ });
+ return html;
+}
+
+async function fetchJSON(url, options = {}) {
+ const headers = new Headers();
+
+ if (options.headers) {
+ for (const [key, value] of Object.entries(options.headers)) {
+ headers.set(key, value);
+ }
+ }
+
+ if (typeof caches === "undefined") {
+ return await (await fetch(url), { headers }).json();
+ }
+
+ const cache = await caches.open("mastodon-comments");
+ let cached = await cache.match(url);
+
+ if (cached && options.ttl) {
+ const cacheTime = new Date(cached.headers.get("x-cached-at"));
+ const diff = Date.now() - cacheTime.getTime();
+
+ if (diff <= options.ttl * 1000) {
+ return await cached.json();
+ }
+ }
+
+ try {
+ const response = await fetch(url, { headers });
+ const body = await response.json();
+
+ cached = new Response(JSON.stringify(body));
+ cached.headers.set("x-cached-at", new Date());
+ cached.headers.set("content-type", "application/json; charset=utf-8");
+ await cache.put(url, cached);
+ return body;
+ } catch {
+ if (cached) {
+ return await cached.json();
+ }
+ }
+}
+
+function dataFromMastodon(data, author, source) {
+ const comments = new Map();
+
+ // Transform data to a more usable format
+ for (const comment of data.descendants) {
+ if (comment.visibility !== "public") {
+ continue;
+ }
+
+ const { account } = comment;
+ const handler = `@${account.username}@${new URL(account.url).hostname}`;
+ comments.set(comment.id, {
+ id: comment.id,
+ isMine: author === handler,
+ source,
+ url: comment.url,
+ parent: comment.in_reply_to_id,
+ createdAt: new Date(comment.created_at),
+ content: formatEmojis(comment.content, comment.emojis),
+ author: {
+ name: formatEmojis(account.display_name, account.emojis),
+ handler,
+ url: account.url,
+ avatar: account.avatar_static,
+ alt: account.display_name,
+ },
+ boosts: comment.reblogs_count,
+ likes: comment.favourites_count,
+ replies: [],
+ });
+ }
+
+ // Group comments by parent
+ for (const comment of comments.values()) {
+ if (comment.parent && comments.has(comment.parent)) {
+ comments.get(comment.parent).replies.push(comment);
+ }
+ }
+
+ return Array.from(comments.values());
+}
+
+function dataFromBluesky(data) {
+ const { thread } = data;
+
+ return blueskyComments(
+ thread.post.author.did,
+ thread.post.cid,
+ thread.replies,
+ );
+}
+
+function blueskyComments(author, parent, comments) {
+ return comments.map((reply) => {
+ const { post, replies } = reply;
+ const rkey = post.uri.split("/").pop();
+ return {
+ id: post.cid,
+ isMine: post.author.did === author,
+ source: "bluesky",
+ url: `https://bsky.app/profile/${post.author.handle}/post/${rkey}`,
+ parent,
+ createdAt: new Date(post.record.createdAt),
+ content: post.record.text,
+ author: {
+ name: post.author.displayName,
+ handler: post.author.handle,
+ url: `https://bsky.app/profile/${post.author.handle}`,
+ avatar: post.author.avatar,
+ alt: post.author.displayName,
+ },
+ boosts: post.repostCount,
+ likes: post.likeCount,
+ replies: blueskyComments(author, post.cid, replies || []),
+ };
+ });
+}
diff --git a/src/components/Comments.astro b/src/components/Comments.astro
index e6e6bd4..c0429bc 100644
--- a/src/components/Comments.astro
+++ b/src/components/Comments.astro
@@ -4,15 +4,23 @@ import FediverseComments from "./helper/comments/Fediverse.astro";
import HatsuComments from "./helper/comments/Hatsu.astro";
import Artalk from "./helper/comments/Artalk.astro";
import Giscus from "./helper/comments/Giscus.astro";
+import OomComments from "./helper/comments/OOM.astro";
const method = siteConfig.comments.type
const FediverseConfig = siteConfig.comments.fediverse
+interface Props {
+ mastodonLink?: string;
+ bskyLink?: string;
+}
+
+const { mastodonLink, bskyLink } = Astro.props;
---
{method === 'artalk' && }
{method === 'giscus' && }
{(method === 'fediverse' && !FediverseConfig.renderOnServer ) && }
{(method === 'fediverse' && FediverseConfig.renderOnServer ) && Loading comments...
}
-{method === 'hatsu' && }
\ No newline at end of file
+{method === 'hatsu' && }
+{method === 'oom' && }
\ No newline at end of file
diff --git a/src/components/helper/comments/OOM.astro b/src/components/helper/comments/OOM.astro
new file mode 100644
index 0000000..c410a64
--- /dev/null
+++ b/src/components/helper/comments/OOM.astro
@@ -0,0 +1,14 @@
+---
+import Comments from "../../../assets/js/oom-comments.js";
+import "../../../assets/css/oom-styles.css";
+
+//Register the custom element with your desired name
+customElements.define("oom-comments", Comments);
+const { mastodonLink, bskyLink } = Astro.props;
+---
+
+ No comments yet
+
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index c8cd0eb..666a0bd 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -44,7 +44,7 @@ export const siteConfig = {
contentPassword: 'p1easeChangeMe!',
// comments
comments: {
- type: 'artalk', // 'artalk','giscus','fediverse','email','hatsu'
+ type: 'artalk', // 'artalk','giscus','fediverse','email','hatsu','oom'
artalk: {
instanceDomain: '', // the domain of your artalk instance
},
+ ${ + comment.boosts ? `${icons.reblog} ${comment.boosts}` : "" + } + ${ + comment.likes ? `${icons.favourite} ${comment.likes}` : "" + } +
+