The GeoCities marquee stops only for a link the keyboard focused: a link pressed with the pointer took the focus as well, which put the line back at its start and scrolled it, so the link slipped from under the pointer before the button came up and the click was lost. The next page's marquee also carries on from the very point the last one had reached, with the loop it showed: it worked its place out from when the loop began, which ran on while the crawl was paused under the pointer, and showed the feed as it was by then, of another width. The chat's message field rests on one line, as tall as the Send button: it rested on two, 54 to 62 pixels against the button's 38, and the button sat low beside it. Both take --chat-composer-height. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
/**
|
|
* Renders a profile's description, which the signed-in user edits in place on
|
|
* their own profile.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { API_URL } from "../../config/api.ts";
|
|
import { VALIDATION } from "../../../shared/validation.ts";
|
|
import type { Wire } from "../../../shared/api.ts";
|
|
import type { PublicUser, UpdateUserRequest } from "../../../shared/user.ts";
|
|
import { expectOk } from "../../utils/shared/api.ts";
|
|
import { useAuth } from "../../hooks/auth/useAuth.ts";
|
|
import { InlineEdit } from "../form/InlineEdit.tsx";
|
|
import { Markdown } from "../editor/Markdown.tsx";
|
|
|
|
/** Passed to {@link ProfileDescription} by `UserPublicProfile`. */
|
|
export interface ProfileDescriptionProps {
|
|
description: string | undefined;
|
|
isOwnProfile: boolean;
|
|
/** Called with the saved description, undefined once cleared. */
|
|
onSaved: (description: string | undefined) => void;
|
|
}
|
|
|
|
/**
|
|
* Renders the description in Markdown, or nothing when there is none. On
|
|
* their own profile, users get a "Who am I?" placeholder instead, and edit it
|
|
* in place, saved to `PATCH /api/me`. Other browsers see the change through
|
|
* the socket's `user_updated` event.
|
|
*/
|
|
export function ProfileDescription(
|
|
{ description, isOwnProfile, onSaved }: ProfileDescriptionProps,
|
|
): React.JSX.Element | null {
|
|
const { authFetch } = useAuth();
|
|
|
|
if (!description && !isOwnProfile) return null;
|
|
|
|
return (
|
|
<div className="profile-description">
|
|
<InlineEdit
|
|
value={description ?? ""}
|
|
editable={isOwnProfile}
|
|
label={t`Edit description`}
|
|
onSave={async (saved) => {
|
|
// Sent even when empty, since the server keeps the old one when it
|
|
// is left out.
|
|
await expectOk<Wire<PublicUser>>(
|
|
await authFetch(`${API_URL}/api/me`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(
|
|
{ description: saved } satisfies UpdateUserRequest,
|
|
),
|
|
}),
|
|
);
|
|
onSaved(saved || undefined);
|
|
}}
|
|
kind="markdown"
|
|
maxLength={VALIDATION.USER_DESCRIPTION_MAX}
|
|
placeholder={t`Who am I?`}
|
|
>
|
|
{description
|
|
? (
|
|
<Markdown className="profile-description-text">
|
|
{description}
|
|
</Markdown>
|
|
)
|
|
: (
|
|
<p className="inline-edit-empty">
|
|
<Trans>Who am I?</Trans>
|
|
</p>
|
|
)}
|
|
</InlineEdit>
|
|
</div>
|
|
);
|
|
}
|