c74dbb6a3b
Extends the feed.* namespace with five new sub-namespaces:
compose.placeholder, tabs (Follows / Following / Global), empty (the
six per-tab + per-mode empty-state messages plus the three CTA labels
that show under them), modeSwitcher (the home feed's mode-picker dropdown
with Agora / All Nostr / Following options, the dropdown trigger's aria
label, and the disabled-Following tooltip), replyContext (Replying to,
the and-joiner, plus a pluralized andOthers_one/andOthers_other), and
actions (Reply / Repost / Undo repost / React / Zap / Share / More + the
Link-copied toast).
PostActionBar's replyLabel prop default went from 'Reply' to undefined;
the component falls back to t('feed.actions.reply') at render time so
existing call sites that pass an already-translated string keep working
unchanged. FeedModeSwitcher's OPTIONS array swapped its hard-coded
label string for an i18nKey suffix, materialized through t() inside the
component.
ar / es / zh / fa / ps / km / sn translations drafted inline; km, sn,
ps, fa still pending native-speaker review.
99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
import { Link } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { EmbeddedNote } from '@/components/EmbeddedNote';
|
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { ProfileHoverCard } from '@/components/ProfileHoverCard';
|
|
import { useAuthor } from '@/hooks/useAuthor';
|
|
import { genUserName } from '@/lib/genUserName';
|
|
import { useProfileUrl } from '@/hooks/useProfileUrl';
|
|
|
|
interface ReplyContextProps {
|
|
pubkeys: string[];
|
|
/** Hex event ID of the parent post being replied to. */
|
|
parentEventId?: string;
|
|
/** Relay URL hint for fetching the parent event. */
|
|
parentRelayHint?: string;
|
|
/** Author pubkey hint for NIP-65 outbox resolution of the parent event. */
|
|
parentAuthorHint?: string;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Displays "Replying to @username" or "Replying to @user1 and @user2" context for reply posts.
|
|
* When parentEventId is provided, hovering over the line shows an embedded preview of the parent post.
|
|
* Used consistently across NoteCard and notification views.
|
|
*/
|
|
export function ReplyContext({ pubkeys, parentEventId, parentRelayHint, parentAuthorHint, className }: ReplyContextProps) {
|
|
const { t } = useTranslation();
|
|
// Filter out any undefined/empty pubkeys defensively
|
|
const validPubkeys = pubkeys.filter(Boolean);
|
|
// Show max 2 authors for cleaner UI
|
|
const displayPubkeys = validPubkeys.slice(0, 2);
|
|
|
|
const replyingToLabel = parentEventId ? (
|
|
<HoverCard openDelay={300} closeDelay={150}>
|
|
<HoverCardTrigger asChild>
|
|
<span className="shrink-0 cursor-pointer hover:underline">{t('feed.replyContext.replyingTo')}</span>
|
|
</HoverCardTrigger>
|
|
<HoverCardContent
|
|
side="bottom"
|
|
align="start"
|
|
sideOffset={4}
|
|
className="w-80 p-0 rounded-2xl shadow-lg"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<EmbeddedNote
|
|
eventId={parentEventId}
|
|
relays={parentRelayHint ? [parentRelayHint] : undefined}
|
|
authorHint={parentAuthorHint}
|
|
className="border-0 rounded-none"
|
|
disableHoverCards
|
|
/>
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
) : (
|
|
<span className="shrink-0">{t('feed.replyContext.replyingTo')}</span>
|
|
);
|
|
|
|
return (
|
|
<div className={className || 'flex items-center flex-wrap gap-x-1 text-sm text-muted-foreground mt-2 mb-1 min-w-0 overflow-hidden'}>
|
|
{replyingToLabel}
|
|
{displayPubkeys.map((pubkey, index) => (
|
|
<span key={pubkey} className="inline-flex items-center gap-1 min-w-0">
|
|
<ReplyAuthor pubkey={pubkey} />
|
|
{index < displayPubkeys.length - 1 && <span className="shrink-0">{t('feed.replyContext.and')}</span>}
|
|
</span>
|
|
))}
|
|
{validPubkeys.length > 2 && (
|
|
<span className="text-muted-foreground shrink-0">
|
|
{t('feed.replyContext.andOthers', { count: validPubkeys.length - 2 })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ReplyAuthor({ pubkey }: { pubkey: string }) {
|
|
const author = useAuthor(pubkey);
|
|
const name = author.data?.metadata?.name || author.data?.metadata?.display_name || genUserName(pubkey);
|
|
const profileUrl = useProfileUrl(pubkey, author.data?.metadata);
|
|
|
|
if (author.isLoading) {
|
|
return <Skeleton className="h-3.5 w-20 inline-block" />;
|
|
}
|
|
|
|
return (
|
|
<ProfileHoverCard pubkey={pubkey} asChild>
|
|
<Link
|
|
to={profileUrl}
|
|
className="text-primary hover:underline truncate max-w-[200px] inline-block align-bottom"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
@{name}
|
|
</Link>
|
|
</ProfileHoverCard>
|
|
);
|
|
}
|