Something went wrong. Try again.
SolidJS component to embed comments from your Leaflet post
Something went wrong. Try again.
6.1 kB · 170 lines · Created 10mo ago
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171/*Copyright (c) 2025 SharpMars
Permission to use, copy, modify, and/or distribute this software forany purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALLWARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIESOF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLEFOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANYDAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER INAN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUTOF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.*/import { Client, simpleFetchHandler } from "@atcute/client";import type { RecordKey } from "@atcute/lexicons";import { createResource, For, Show, Suspense } from "solid-js";import type { PubLeafletComment, PubLeafletRichtextFacet } from "@atcute/leaflet";import { formatDistance } from "date-fns";
export function LeafletComments(props: { rkey: RecordKey }) { const leafletUrl = "https://sharpmars.leaflet.pub/"; const atUriPrefix = "at://did:plc:irx36xprktslecsbopbwnh5w/pub.leaflet.document/";
const splitTextByFacet = (text: string, facets?: PubLeafletRichtextFacet.Main[]) => { if (!facets || facets.length == 0) return text;
const sortedFacets = facets.toSorted((a, b) => a.index.byteStart - b.index.byteStart); const encoder = new TextEncoder(); const decoder = new TextDecoder(); const encodedText = encoder.encode(text);
const indexSet = new Set<number>(); indexSet.add(0); indexSet.add(encodedText.length);
for (const facet of sortedFacets) { indexSet.add(facet.index.byteStart); indexSet.add(facet.index.byteEnd); }
const indexes = [...indexSet].sort((a, b) => a - b);
const ranges: { start: number; end: number }[] = [];
for (let i = 0; i < indexSet.size - 1; i++) { ranges.push({ start: indexes[i], end: indexes[i + 1] }); }
const segments = ranges.map((range) => { const segmentFacets = sortedFacets .filter((val) => val.index.byteStart >= range.start && val.index.byteEnd <= range.end) .flatMap((val) => val.features) .map((val) => val.$type);
return ( <span classList={{ "line-through decoration-[hsl(29,100%,65%)] decoration-1.5": segmentFacets.includes( "pub.leaflet.richtext.facet#strikethrough" ), italic: segmentFacets.includes("pub.leaflet.richtext.facet#italic"), "font-bold": segmentFacets.includes("pub.leaflet.richtext.facet#bold"), }} > {decoder.decode(encodedText.subarray(range.start, range.end))} </span> ); });
return segments; };
const [comments] = createResource(async () => { const atUri = atUriPrefix + props.rkey;
let cursor = null; const records = [];
do { const res = await ( await fetch( `https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks?subject=${atUri}&source=pub.leaflet.comment:subject&limit=100` ) ).json();
records.push(...res.records); cursor = res.cursor; } while (cursor);
const slingshot = new Client({ handler: simpleFetchHandler({ service: "https://slingshot.microcosm.blue" }) });
const comments: { handle: string; createdAt: string; text: ReturnType<typeof splitTextByFacet>; attachment: boolean; }[] = [];
for (const record of records) { const commentRes = await slingshot.get("com.atproto.repo.getRecord", { params: { rkey: record.rkey, collection: record.collection, repo: record.did }, headers: [["Accept", "application/json"]], });
const handleRes = await slingshot.get("com.bad-example.identity.resolveMiniDoc", { params: { identifier: record.did }, });
const handle = handleRes.ok ? handleRes.data.handle : record.did;
if (commentRes.ok) { const comment = commentRes.data.value as PubLeafletComment.Main; comments.push({ handle: handle ? handle : record.did, createdAt: comment.createdAt, text: splitTextByFacet(comment.plaintext, comment.facets), attachment: !!comment.attachment, }); } }
return comments; });
return ( <div class="flex gap-3 flex-col max-w-768px w-full"> <hr class="text-[hsl(29,100%,65%)]" /> <p class="text-6 font-700">Comments</p> <p> To comment or see them in their original form check out{" "} <a class="underline text-[hsl(29,100%,65%)] after:content-['↗'] hover:brightness-120 transition-all" href={leafletUrl + props.rkey} > leaflet.pub </a> </p> <Suspense fallback={"Loading..."}> <For each={comments()}> {(comment, i) => ( <div class="flex flex-col gap-1"> <hr class="m-t-1 m-b-1 text-neutral-700" /> <div class="flex justify-between items-center"> <p>{comment.handle}</p> <p>{formatDistance(new Date(comment.createdAt), new Date(), { addSuffix: true })}</p> </div> <Show when={comment.attachment}> <div class="flex justify-center w-full"> <div class="b-1 b-neutral-700 p-1 flex justify-center items-center max-w-98% w-full"> <p> Check quote on{" "} <a class="underline text-[hsl(29,100%,65%)] after:content-['↗'] hover:brightness-120 transition-all" href={leafletUrl + props.rkey} > leaflet.pub </a> </p> </div> </div> </Show> <p class="m-x-1%">{comment.text}</p> </div> )} </For> </Suspense> </div> );}