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 { html } from 'lit';
import { outbox, resource } from '@napplet/sdk';
import { editor } from '../editor-controller';
import { runtimeHasDomain } from '../domain-availability';
import { LightElement } from './light-element';
type Profile = { name: string; picture?: string };
const profiles = new Map<string, Promise<Profile>>();
function loadProfile(viewer: string): Promise<Profile> {
const cached = profiles.get(viewer);
if (cached) return cached;
const request = (async () => {
const result = await outbox.query([{ kinds: [0], authors: [viewer], limit: 1 }], {
authors: [viewer],
timeoutMs: 5000,
});
if (result.error) throw new Error(result.error);
const event = result.events
.map((r) => r.event)
.filter((e) => e.kind === 0 && e.pubkey === viewer)
.sort((a, b) => b.created_at - a.created_at || a.id.localeCompare(b.id))[0];
const profile = event ? JSON.parse(event.content) : {};
const text = (value: unknown) =>
typeof value === 'string' ? value.trim().slice(0, 100) : '';
return {
name: text(profile?.display_name) || text(profile?.name),
picture: typeof profile?.picture === 'string' ? profile.picture : undefined,
};
})();
if (profiles.size >= 16) profiles.delete(profiles.keys().next().value!);
profiles.set(viewer, request);
request.catch(() => profiles.delete(viewer));
return request;
}
/** Public profile metadata via OUTBOX; avatar bytes stay behind RESOURCE. */
export class BsAccount extends LightElement {
static properties = { name: { state: true }, avatar: { state: true } };
declare name: string;
declare avatar?: string;
#viewer?: string;
#epoch = 0;
constructor() {
super();
this.name = '';
this.watch((s) => [s.viewer, s.identityLabel]);
}
updated() {
const viewer = editor.state.viewer;
if (viewer === this.#viewer) return;
this.#viewer = viewer;
const epoch = ++this.#epoch;
this.name = '';
this.#clearAvatar();
if (viewer) void this.#load(viewer, epoch);
}
disconnectedCallback() {
super.disconnectedCallback();
this.#epoch++;
this.#viewer = undefined;
this.#clearAvatar();
}
#clearAvatar() {
if (this.avatar) URL.revokeObjectURL(this.avatar);
this.avatar = undefined;
}
async #load(viewer: string, epoch: number) {
try {
if (!runtimeHasDomain('outbox')) return;
const profile = await loadProfile(viewer);
if (epoch !== this.#epoch || !this.isConnected) return;
this.name = profile.name;
if (!profile.picture || !runtimeHasDomain('resource')) return;
const blob = await resource.bytes(profile.picture);
if (epoch !== this.#epoch || !this.isConnected) return;
if (
blob.size > 5 * 1024 * 1024 ||
!['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/avif'].includes(
blob.type,
)
)
return;
this.avatar = URL.createObjectURL(blob);
} catch {
/* Profile and image failures retain a readable account fallback. */
}
}
render() {
const { viewer, identityLabel } = editor.state;
if (!viewer) return html`<span class="account-status">${identityLabel}</span>`;
const current = viewer === this.#viewer;
const name = (current && this.name) || `${viewer.slice(0, 8)}…${viewer.slice(-4)}`;
const initials =
current && this.name
? this.name
.split(/\s+/)
.slice(0, 2)
.map((s) => Array.from(s)[0])
.join('')
.toUpperCase()
: '?';
return html`<span class="account-profile" title=${`Signed in as ${name}`}>
<span class="account-avatar" aria-hidden="true"
>${current && this.avatar ? html`<img src=${this.avatar} alt="" @error=${() => this.#clearAvatar()} />` : initials}</span
>
<span class="account-name">${name}</span>
</span>`;
}
}
customElements.define('bs-account', BsAccount);
