Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useEffect, useSyncExternalStore } from "react";
import { Link } from "react-router";
import { keyTextColor } from "~/lib/color";
import type { Copy } from "~/lib/copies";
import { diffPath } from "~/lib/paths";
import { authorName } from "~/lib/profile";
import { authorsState, serverAuthorsState, subscribeAuthors, wantAuthors } from "~/lib/profiles";
import type { Standing } from "~/lib/spots";
import type { Variant } from "~/lib/variants";
import { type Standing, standingKey } from "~/lib/spots";
import { AuthorAvatar } from "./author-avatar";

export const VARIANTS_ID = "under-this-name";
export const COPIES_ID = "other-copies";

const asDate = (seconds: number): string => new Date(seconds * 1000).toISOString().slice(0, 10);

Expand All @@ -24,77 +24,85 @@ const said = (standing: Standing): string => {
};

/**
* The other documents published under this page's identifier. Drawn dashed like
* everything on this site that is available rather than settled: nobody vouches
* for these, they exist, and existing is the whole point.
* The other documents that are copies of this one.
*
* One list, not two. Some of these were found publishing at this address and
* some by the tag their author wrote saying they started here, and that
* difference is how they were found rather than anything about them. What
* separates a copy worth reading from a document that only shares a name is the
* standing on its row, which is measured against the text.
*
* No sentence under the heading. The heading names the list, each row carries
* its author, its standing and its address, and a paragraph saying this site
* vouches for none of them would answer a suspicion nobody arrived with.
*
* Drawn dashed like everything on this site that is available rather than
* settled: these exist, and existing is the whole point.
*/
export const Variants = ({
variants,
export const Copies = ({
copies,
standings,
from,
}: {
variants: Variant[];
copies: Copy[];
standings: Record<string, Standing>;
/** The document whose page this section sits on, and a comparison's base side. */
from: { npub: string; identifier: string };
}) => {
const authors = useSyncExternalStore(subscribeAuthors, authorsState, serverAuthorsState);

useEffect(() => {
if (variants.length > 0) wantAuthors(variants.map((variant) => variant.pubkey));
}, [variants]);
if (copies.length > 0) wantAuthors(copies.map((copy) => copy.pubkey));
}, [copies]);

if (variants.length === 0) return null;
if (copies.length === 0) return null;

return (
<section id={VARIANTS_ID} className="mt-16 border-t border-rule pt-8">
<section id={COPIES_ID} className="mt-16 border-t border-rule pt-8">
<h2 className="font-mono text-[0.6875rem] uppercase tracking-[0.18em] text-muted">
Under this name
Other copies
</h2>
<p className="mt-3 font-serif text-[0.9375rem] leading-relaxed text-muted">
Anyone may publish a document under this name. These are the other keys that have.
</p>
<ul className="mt-5 space-y-3">
{variants.map((variant) => {
const author = authors[variant.pubkey] ?? null;
const standing = standings[variant.pubkey];
{copies.map((copy) => {
const author = authors[copy.pubkey] ?? null;
const standing = standings[standingKey(copy)];
return (
<li
key={variant.pubkey}
key={standingKey(copy)}
className="flex gap-4 rounded-sm border border-dashed border-rule p-4 hover:border-muted"
>
<span className="mt-0.5 shrink-0">
<AuthorAvatar pubkey={variant.pubkey} picture={author?.picture ?? null} size={28} />
<AuthorAvatar pubkey={copy.pubkey} picture={author?.picture ?? null} size={28} />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-4">
<span
className="truncate font-mono text-sm font-medium"
style={{ color: keyTextColor(variant.pubkey) }}
style={{ color: keyTextColor(copy.pubkey) }}
>
{authorName(author, variant.npub)}
{authorName(author, copy.npub)}
</span>
<time
dateTime={new Date(variant.revisedAt * 1000).toISOString()}
dateTime={new Date(copy.revisedAt * 1000).toISOString()}
className="shrink-0 font-mono text-xs text-muted"
>
{asDate(variant.revisedAt)}
{asDate(copy.revisedAt)}
</time>
</div>
<Link
to={variant.path}
to={copy.path}
className="mt-1.5 block font-mono text-base font-medium hover:underline hover:decoration-1 hover:underline-offset-4"
>
{variant.title}
{copy.title}
</Link>
{variant.summary !== "" && (
{copy.summary !== "" && (
<p className="mt-1.5 line-clamp-2 max-w-[38rem] font-serif text-sm leading-snug text-muted">
{variant.summary}
{copy.summary}
</p>
)}
<div className="mt-3 flex flex-wrap items-baseline gap-x-3 gap-y-1">
<Link
to={diffPath(from.npub, from.identifier, variant.npub)}
to={diffPath(from.npub, from.identifier, copy.npub, copy.identifier)}
title="This document, marked with what their copy changes"
className="inline-block rounded-sm border border-rule px-2 py-1 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-muted hover:border-muted hover:text-ink"
>
Expand All @@ -103,6 +111,11 @@ export const Variants = ({
{standing !== undefined && (
<span className="font-mono text-xs text-muted">{said(standing)}</span>
)}
{/* Only where it differs. A copy at this document's own address
would be repeating the line at the top of the page. */}
{!copy.sameName && (
<span className="font-mono text-xs text-muted">{copy.identifier}</span>
)}
</div>
</div>
</li>
Expand Down
10 changes: 4 additions & 6 deletions apps/web/app/components/copy-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ export const CopyButton = ({
value,
label,
title,
className = "rounded-sm border border-rule px-2 py-1 text-muted hover:border-muted hover:text-ink",
}: {
/** A string, or a way to go and get one: the event is fetched, not held. */
value: string | (() => Promise<string>);
label: string;
title?: string;
/** The face it wears, so the same button reads as a row inside a menu. */
className?: string;
}) => {
const [state, setState] = useState<State>("idle");
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
Expand All @@ -35,12 +38,7 @@ export const CopyButton = ({
};

return (
<button
type="button"
onClick={copy}
title={title}
className="rounded-sm border border-rule px-2 py-1 text-muted hover:border-muted hover:text-ink"
>
<button type="button" onClick={copy} title={title} className={className}>
<span aria-live="polite">
{state === "idle" ? label : state === "copied" ? "Copied" : "Copy failed"}
</span>
Expand Down
Loading