v4: a description shows as text where a list quotes it — a playlist's card printed its description as written, so one holding an image showed "", and so did a user's line in the search results. Both now go through markdownToPlainText (shared/markdown.ts), as the chat's reply previews do: the markup goes, a link keeps its text, an image is left out, and a description of nothing but an image leaves no line. The playlist's page and the profile render the Markdown as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Flattens the Markdown users write into one line of plain text, where a
|
* Flattens the Markdown users write into one line of plain text, where a
|
||||||
* quote of it can't render markup: the chat's reply previews and the digest
|
* quote of it can't render markup: the chat's reply previews, the digest
|
||||||
* email's quotes of a comment or a note.
|
* email's quotes of a comment or a note, and the line of a description on a
|
||||||
|
* playlist's card and on a user in the search results.
|
||||||
*
|
*
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { Tooltip } from "../shared/Tooltip.tsx";
|
import { Tooltip } from "../shared/Tooltip.tsx";
|
||||||
import { CategoryChips } from "../category/CategoryChips.tsx";
|
import { CategoryChips } from "../category/CategoryChips.tsx";
|
||||||
import { userPath } from "../../../shared/paths.ts";
|
import { userPath } from "../../../shared/paths.ts";
|
||||||
|
import { markdownToPlainText } from "../../../shared/markdown.ts";
|
||||||
|
|
||||||
/** Passed to {@link PlaylistCard} by each list of playlists. */
|
/** Passed to {@link PlaylistCard} by each list of playlists. */
|
||||||
export interface PlaylistCardProps {
|
export interface PlaylistCardProps {
|
||||||
@@ -53,6 +54,10 @@ export function PlaylistCard(
|
|||||||
const unread = !isOwner && isRecent(playlist.createdAt) &&
|
const unread = !isOwner && isRecent(playlist.createdAt) &&
|
||||||
!isPlaylistVisited(playlist.id);
|
!isPlaylistVisited(playlist.id);
|
||||||
const showBadge = !privateBadgeOnly || (!!isOwner && !playlist.isPublic);
|
const showBadge = !privateBadgeOnly || (!!isOwner && !playlist.isPublic);
|
||||||
|
// The playlist's page renders the description's Markdown; the card has
|
||||||
|
// room for its text only, and none at all when it is a lone image.
|
||||||
|
const description = playlist.description &&
|
||||||
|
markdownToPlainText(playlist.description, "");
|
||||||
|
|
||||||
function handleNavigate() {
|
function handleNavigate() {
|
||||||
markPlaylistVisited(playlist.id);
|
markPlaylistVisited(playlist.id);
|
||||||
@@ -91,8 +96,8 @@ export function PlaylistCard(
|
|||||||
{unread && <span className="unread-dot" aria-hidden="true" />}
|
{unread && <span className="unread-dot" aria-hidden="true" />}
|
||||||
{playlist.title}
|
{playlist.title}
|
||||||
</Link>
|
</Link>
|
||||||
{playlist.description && (
|
{description && (
|
||||||
<p className="playlist-card-description">{playlist.description}</p>
|
<p className="playlist-card-description">{description}</p>
|
||||||
)}
|
)}
|
||||||
<div className="playlist-card-meta">
|
<div className="playlist-card-meta">
|
||||||
{showBadge && (
|
{showBadge && (
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
userPath,
|
userPath,
|
||||||
} from "../../shared/paths.ts";
|
} from "../../shared/paths.ts";
|
||||||
import { NotFound } from "./NotFound.tsx";
|
import { NotFound } from "./NotFound.tsx";
|
||||||
|
import { markdownToPlainText } from "../../shared/markdown.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Held by `Search` for a query, from the first page of `SearchResults`. The
|
* Held by `Search` for a query, from the first page of `SearchResults`. The
|
||||||
@@ -297,21 +298,27 @@ export function Search(): React.JSX.Element {
|
|||||||
? <p className="search-page-empty">{t`No users match "${q}".`}</p>
|
? <p className="search-page-empty">{t`No users match "${q}".`}</p>
|
||||||
: (
|
: (
|
||||||
<ul className="user-results">
|
<ul className="user-results">
|
||||||
{results.users.map((u) => (
|
{results.users.map((u) => {
|
||||||
|
// The profile renders the description's Markdown; this
|
||||||
|
// line has room for its text only.
|
||||||
|
const description = u.description &&
|
||||||
|
markdownToPlainText(u.description, "");
|
||||||
|
return (
|
||||||
<li key={u.id}>
|
<li key={u.id}>
|
||||||
<Link
|
<Link
|
||||||
to={userPath(u.username)}
|
to={userPath(u.username)}
|
||||||
className="user-result-item"
|
className="user-result-item"
|
||||||
>
|
>
|
||||||
<span className="user-result-name">@{u.username}</span>
|
<span className="user-result-name">@{u.username}</span>
|
||||||
{u.description && (
|
{description && (
|
||||||
<span className="user-result-description">
|
<span className="user-result-description">
|
||||||
{u.description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,6 +38,34 @@ describe("PlaylistCard", () => {
|
|||||||
expect(currentPath()).toBe("/@ann/playlists/mix");
|
expect(currentPath()).toBe("/@ann/playlists/mix");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the description shows as text, without its Markdown or its images", () => {
|
||||||
|
const withText = renderWithContexts(
|
||||||
|
<ul>
|
||||||
|
<PlaylistCard
|
||||||
|
playlist={makePlaylist({
|
||||||
|
description:
|
||||||
|
"**Late** [tunes](https://x.y) ",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</ul>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
withText.container.querySelector(".playlist-card-description"),
|
||||||
|
).toHaveTextContent(/^Late tunes$/);
|
||||||
|
withText.unmount();
|
||||||
|
|
||||||
|
const imageOnly = renderWithContexts(
|
||||||
|
<ul>
|
||||||
|
<PlaylistCard
|
||||||
|
playlist={makePlaylist({ description: "" })}
|
||||||
|
/>
|
||||||
|
</ul>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
imageOnly.container.querySelector(".playlist-card-description"),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
test("the owner doesn't see their own name, or an unread dot", () => {
|
test("the owner doesn't see their own name, or an unread dot", () => {
|
||||||
const p = makePlaylist({ ownerUsername: "me", createdAt: new Date() });
|
const p = makePlaylist({ ownerUsername: "me", createdAt: new Date() });
|
||||||
const { container } = renderWithContexts(
|
const { container } = renderWithContexts(
|
||||||
|
|||||||
Reference in New Issue
Block a user