SOURCE / PINNED RELEASE
Made of little things.
Black Site Map Editor
- Release
- 32a57733b7e9…
- Author-recorded commit
- 3f54890f542f…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/black-site-map-editor
Archive hash verified: 7a4b1abbac7db5b0…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { outbox } from '@napplet/sdk';
import type { NostrEvent } from '@napplet/sdk';
import type { MapRecord } from './map-host';
import { parseMapEvent } from './vendor/map-event.mjs';
const tagged = (e: NostrEvent, name: string, value: string) =>
e.tags.some((t) => t[0] === name && t[1] === value);
export async function followedAuthors(viewer: string): Promise<string[]> {
const result = await outbox.query([{ kinds: [3], authors: [viewer], limit: 1 }], {
authors: [viewer],
timeoutMs: 8000,
});
if (result.error || result.incomplete)
throw new Error('Could not load your follows. Please retry.');
const latest = result.events
.map((r) => r.event)
.filter((e) => e.kind === 3 && e.pubkey === viewer)
.sort((a, b) => b.created_at - a.created_at || a.id.localeCompare(b.id))[0];
return [
...new Set(
(latest?.tags ?? [])
.filter((t) => t[0] === 'p' && /^[a-f0-9]{64}$/.test(t[1]))
.map((t) => t[1]),
),
];
}
export interface MapDiscussion {
comments: NostrEvent[];
reactions: { label: string; count: number }[];
remixes: MapRecord[];
notice: string;
}
export async function mapDiscussion(source: MapRecord): Promise<MapDiscussion> {
const address = source.metadata.address;
const queries = await Promise.allSettled([
outbox.query(
[
{ kinds: [1111], '#A': [address], limit: 100 },
{ kinds: [1111], '#E': [source.event.id], limit: 100 },
],
{ limit: 200, timeoutMs: 8000 },
),
outbox.query(
[
{ kinds: [7], '#a': [address], limit: 200 },
{ kinds: [7], '#e': [source.event.id], limit: 200 },
],
{ limit: 400, timeoutMs: 8000 },
),
outbox.query([{ kinds: [35858], '#a': [address], limit: 100 }], {
limit: 100,
timeoutMs: 8000,
}),
]);
let partial = false;
const rows = queries.map((q) => {
if (q.status === 'rejected') {
partial = true;
return [];
}
if (q.value.error || q.value.incomplete) partial = true;
return [...new Map(q.value.events.map((r) => [r.event.id, r])).values()];
});
const comments = rows[0]
.map((r) => r.event)
.filter(
(e) =>
e.kind === 1111 &&
tagged(e, 'K', '35858') &&
(tagged(e, 'A', address) || tagged(e, 'E', source.event.id)),
)
.sort((a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id));
// Count each person's latest vote per reaction value; repeated relay copies never inflate it.
const votes = new Map<string, NostrEvent>();
for (const { event: e } of rows[1]) {
if (
e.kind !== 7 ||
!(
tagged(e, 'a', address) ||
e.tags.filter((t) => t[0] === 'e').at(-1)?.[1] === source.event.id
)
)
continue;
const value = e.content === '' ? '+' : e.content;
const key = `${e.pubkey}:${value}`;
const prior = votes.get(key);
if (!prior || e.created_at > prior.created_at) votes.set(key, e);
}
const totals = new Map<string, number>();
for (const e of votes.values()) {
const label =
e.content === '' || e.content === '+'
? 'Upvotes'
: e.content === '-'
? 'Downvotes'
: e.content;
totals.set(label, (totals.get(label) ?? 0) + 1);
}
const newest = new Map<string, MapRecord>();
for (const r of rows[2]) {
const parsed = parseMapEvent(r.event);
if (!parsed.ok || parsed.map.lineage?.parent.address !== address) continue;
const prior = newest.get(parsed.map.address);
if (
!prior ||
r.event.created_at > prior.event.created_at ||
(r.event.created_at === prior.event.created_at && r.event.id < prior.event.id)
)
newest.set(parsed.map.address, {
event: r.event,
metadata: parsed.map,
relayHints: r.sidecar?.relayHints ?? [],
});
}
return {
comments,
reactions: [...totals].map(([label, count]) => ({ label, count })),
remixes: [...newest.values()].sort((a, b) => b.event.created_at - a.event.created_at),
notice: `${partial ? 'Some activity could not be loaded. Retry to refresh. ' : ''}Activity reflects available relays and may be incomplete (up to 200 comments, 400 reactions and 100 remixes).`,
};
}
export async function postMapComment(source: MapRecord, content: string) {
const text = content.trim();
if (!text || text.length > 4000) throw new Error('Write a comment of 1–4000 characters.');
const result = await outbox.publish({
kind: 1111,
content: text,
created_at: Math.floor(Date.now() / 1000),
tags: [
['A', source.metadata.address],
['K', '35858'],
['P', source.event.pubkey],
['a', source.metadata.address],
['e', source.event.id],
['k', '35858'],
['p', source.event.pubkey],
],
});
if (!result.ok || !result.event)
throw new Error(result.error ?? 'Comment could not be published.');
return result.event;
}
