feat(polls): add rich text support to poll renderers

- Add RichText rendering for poll questions in both feed and detail views
- Add RichText rendering for poll options with support for mentions, links, emojis
- Add RichText rendering for poll response option names
- Fix icon shrinking by adding shrink-0 flex-shrink-0 classes to all icons
- Pass poll event context to RichText for proper mention resolution
This commit is contained in:
Claude
2026-01-22 21:51:12 +00:00
parent 7838b0ab98
commit eb2bffd233
3 changed files with 56 additions and 25 deletions

View File

@@ -26,6 +26,7 @@ import {
countVotes,
getUniqueVoterCount,
} from "@/lib/nip88-helpers";
import { RichText } from "../RichText";
/**
* Detail renderer for Kind 1068 - Poll (NIP-88)
@@ -105,9 +106,9 @@ export function PollDetailRenderer({ event }: { event: NostrEvent }) {
{/* Poll Type Badge */}
<div className="flex items-center gap-2 text-muted-foreground">
{pollType === "multiplechoice" ? (
<ListChecks className="size-5" />
<ListChecks className="size-5 shrink-0 flex-shrink-0" />
) : (
<ListCheck className="size-5" />
<ListCheck className="size-5 shrink-0 flex-shrink-0" />
)}
<span className="text-sm uppercase tracking-wide">
{pollType === "multiplechoice"
@@ -124,7 +125,11 @@ export function PollDetailRenderer({ event }: { event: NostrEvent }) {
{/* Question */}
<h1 className="text-2xl font-bold text-foreground">
{question || "Poll"}
<RichText
content={question || "Poll"}
event={event}
options={{ showMedia: false, showEventEmbeds: false }}
/>
</h1>
{/* Author */}
@@ -137,7 +142,7 @@ export function PollDetailRenderer({ event }: { event: NostrEvent }) {
{/* Stats */}
<div className="flex items-center gap-6 py-3 border-y border-border">
<div className="flex items-center gap-2">
<Users className="size-4 text-muted-foreground" />
<Users className="size-4 shrink-0 flex-shrink-0 text-muted-foreground" />
<span className="font-medium">{voterCount}</span>
<span className="text-muted-foreground">
{voterCount === 1 ? "voter" : "voters"}
@@ -145,7 +150,7 @@ export function PollDetailRenderer({ event }: { event: NostrEvent }) {
</div>
{endsAt && (
<div className="flex items-center gap-2">
<Clock className="size-4 text-muted-foreground" />
<Clock className="size-4 shrink-0 flex-shrink-0 text-muted-foreground" />
<span className="text-muted-foreground">
{ended ? "Ended" : "Ends"} {endTimeText}
</span>
@@ -187,23 +192,27 @@ export function PollDetailRenderer({ event }: { event: NostrEvent }) {
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 min-w-0 flex-1">
{pollType === "multiplechoice" ? (
<CheckCircle2
className={`size-4 ${isWinner ? "text-primary" : "text-muted-foreground"}`}
className={`size-4 shrink-0 flex-shrink-0 ${isWinner ? "text-primary" : "text-muted-foreground"}`}
/>
) : (
<CircleDot
className={`size-4 ${isWinner ? "text-primary" : "text-muted-foreground"}`}
className={`size-4 shrink-0 flex-shrink-0 ${isWinner ? "text-primary" : "text-muted-foreground"}`}
/>
)}
<span
className={`font-medium ${isWinner ? "text-foreground" : ""}`}
<div
className={`font-medium min-w-0 flex-1 ${isWinner ? "text-foreground" : ""}`}
>
{option.label}
</span>
<RichText
content={option.label}
event={event}
options={{ showMedia: false, showEventEmbeds: false }}
/>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="flex items-center gap-2 text-sm shrink-0">
<span
className={`font-mono ${isWinner ? "font-bold" : "text-muted-foreground"}`}
>

View File

@@ -19,6 +19,7 @@ import {
getPollEndsAt,
isPollEnded,
} from "@/lib/nip88-helpers";
import { RichText } from "../RichText";
/**
* Renderer for Kind 1068 - Poll (NIP-88)
@@ -43,9 +44,9 @@ export function PollRenderer({ event }: BaseEventProps) {
{/* Poll Header */}
<div className="flex items-center gap-2 text-muted-foreground">
{pollType === "multiplechoice" ? (
<ListChecks className="size-4" />
<ListChecks className="size-4 shrink-0 flex-shrink-0" />
) : (
<ListCheck className="size-4" />
<ListCheck className="size-4 shrink-0 flex-shrink-0" />
)}
<span className="text-xs uppercase tracking-wide">
{pollType === "multiplechoice"
@@ -60,7 +61,11 @@ export function PollRenderer({ event }: BaseEventProps) {
event={event}
className="text-base font-semibold text-foreground leading-tight"
>
{question || "Poll"}
<RichText
content={question || "Poll"}
event={event}
options={{ showMedia: false, showEventEmbeds: false }}
/>
</ClickableEventTitle>
{/* Options Preview */}
@@ -72,11 +77,18 @@ export function PollRenderer({ event }: BaseEventProps) {
className="flex items-center gap-2 text-sm text-muted-foreground"
>
{pollType === "multiplechoice" ? (
<CheckCircle2 className="size-3.5 shrink-0" />
<CheckCircle2 className="size-3.5 shrink-0 flex-shrink-0" />
) : (
<CircleDot className="size-3.5 shrink-0" />
<CircleDot className="size-3.5 shrink-0 flex-shrink-0" />
)}
<span className="truncate">{option.label}</span>
<div className="truncate min-w-0 flex-1">
<RichText
content={option.label}
event={event}
options={{ showMedia: false, showEventEmbeds: false }}
className="truncate"
/>
</div>
</div>
))}
{options.length > 4 && (
@@ -90,7 +102,7 @@ export function PollRenderer({ event }: BaseEventProps) {
{/* Deadline */}
{endsAt && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Clock className="size-3" />
<Clock className="size-3 shrink-0 flex-shrink-0" />
{ended ? (
<span>Ended {endTimeText}</span>
) : (

View File

@@ -11,6 +11,7 @@ import {
getPollOptions,
getPollType,
} from "@/lib/nip88-helpers";
import { RichText } from "../RichText";
/**
* Renderer for Kind 1018 - Poll Response (NIP-88)
@@ -50,19 +51,28 @@ export function PollResponseRenderer({ event, depth = 0 }: BaseEventProps) {
return pollType === "singlechoice" ? labels.slice(0, 1) : labels;
}, [selectedOptions, pollEvent, pollType]);
const displayText =
displayedLabels.length > 0 ? displayedLabels.join(", ") : "unknown option";
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
{/* Vote indicator */}
<div className="flex items-center gap-2 text-muted-foreground">
<Vote className="size-4" />
<Vote className="size-4 shrink-0 flex-shrink-0" />
<span className="text-sm">
Voted for:{" "}
{displayedLabels.length > 0 ? (
<span className="text-foreground font-medium">{displayText}</span>
<span className="text-foreground font-medium">
{displayedLabels.map((label, idx) => (
<span key={idx}>
<RichText
content={label}
event={pollEvent || event}
options={{ showMedia: false, showEventEmbeds: false }}
className="inline"
/>
{idx < displayedLabels.length - 1 && ", "}
</span>
))}
</span>
) : (
<span className="italic">unknown option</span>
)}