You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
215 lines
8.0 KiB
215 lines
8.0 KiB
import { useSmartNoteNavigation } from '@/PageManager' |
|
import { ExtendedKind } from '@/constants' |
|
import { Button } from '@/components/ui/button' |
|
import { Skeleton } from '@/components/ui/skeleton' |
|
import { |
|
notificationReactionSummaryKey, |
|
useNotificationReactionDisplay |
|
} from '@/hooks/useNotificationReactionDisplay' |
|
import { |
|
DISCUSSION_DOWNVOTE_DISPLAY, |
|
DISCUSSION_UPVOTE_DISPLAY |
|
} from '@/lib/discussion-votes' |
|
import { getZapInfoFromEvent } from '@/lib/event-metadata' |
|
import { isMentioningMutedUsers, isNip25ReactionKind } from '@/lib/event' |
|
import { getWebExternalReactionTargetUrl } from '@/lib/rss-article' |
|
import { toNote } from '@/lib/link' |
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider' |
|
import { useMuteList } from '@/contexts/mute-list-context' |
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
|
import { Event, kinds } from 'nostr-tools' |
|
import { useMemo, useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import ClientTag from '../ClientTag' |
|
import Collapsible from '../Collapsible' |
|
import MarkdownArticle from '../Note/MarkdownArticle/MarkdownArticle' |
|
import ReactionEmojiDisplay from '../Note/ReactionEmojiDisplay' |
|
import { FormattedTimestamp } from '../FormattedTimestamp' |
|
import Nip05 from '../Nip05' |
|
import NoteOptions from '../NoteOptions' |
|
import NoteStats from '../NoteStats' |
|
import ParentNotePreview from '../ParentNotePreview' |
|
import WebPreview from '../WebPreview' |
|
import UserAvatar from '../UserAvatar' |
|
import Username from '../Username' |
|
import NoteKindLabel from '../Note/NoteKindLabel' |
|
import Zap from '../Note/Zap' |
|
|
|
export default function ReplyNote({ |
|
event, |
|
parentEventId, |
|
onClickParent = () => {}, |
|
onClickReply, |
|
highlight = false, |
|
duplicateWebPreviewCleanedUrlHints |
|
}: { |
|
event: Event |
|
parentEventId?: string |
|
onClickParent?: () => void |
|
onClickReply?: (event: Event) => void |
|
highlight?: boolean |
|
duplicateWebPreviewCleanedUrlHints?: string[] |
|
}) { |
|
const { t } = useTranslation() |
|
const { isSmallScreen } = useScreenSize() |
|
const { navigateToNote } = useSmartNoteNavigation() |
|
const { mutePubkeySet } = useMuteList() |
|
const { hideContentMentioningMutedUsers } = useContentPolicy() |
|
const [showMuted, setShowMuted] = useState(false) |
|
const reactionDisplay = useNotificationReactionDisplay(event) |
|
const webReactionParentUrl = useMemo( |
|
() => |
|
event.kind === ExtendedKind.EXTERNAL_REACTION ? getWebExternalReactionTargetUrl(event) : undefined, |
|
[event] |
|
) |
|
const headerUserId = useMemo(() => { |
|
if (event.kind !== kinds.Zap) return event.pubkey |
|
const info = getZapInfoFromEvent(event) |
|
return info?.senderPubkey ?? event.pubkey |
|
}, [event]) |
|
|
|
const show = useMemo(() => { |
|
if (showMuted) { |
|
return true |
|
} |
|
if (mutePubkeySet.has(event.pubkey)) { |
|
return false |
|
} |
|
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(event, mutePubkeySet)) { |
|
return false |
|
} |
|
return true |
|
}, [showMuted, mutePubkeySet, event, hideContentMentioningMutedUsers]) |
|
|
|
|
|
return ( |
|
<div |
|
className={`pb-3 border-b transition-colors duration-500 clickable ${highlight ? 'bg-primary/50' : ''}`} |
|
onClick={(e) => { |
|
// Don't navigate if clicking on interactive elements |
|
const target = e.target as HTMLElement |
|
if (target.closest('button') || target.closest('[role="button"]') || target.closest('a') || target.closest('[data-parent-note-preview]')) { |
|
return |
|
} |
|
if (onClickReply) { |
|
onClickReply(event) |
|
} else { |
|
navigateToNote(toNote(event), event) |
|
} |
|
}} |
|
> |
|
<Collapsible> |
|
<div className="flex space-x-2 items-start px-4 pt-3"> |
|
<UserAvatar userId={headerUserId} size="medium" className="shrink-0 mt-0.5" /> |
|
<div className="w-full overflow-hidden"> |
|
<div className="flex items-start justify-between gap-2"> |
|
<div className="flex-1 w-0"> |
|
<div className="flex gap-1 items-center"> |
|
<Username |
|
userId={headerUserId} |
|
className="text-sm font-semibold text-muted-foreground hover:text-foreground truncate" |
|
skeletonClassName="h-3" |
|
/> |
|
<ClientTag event={event} /> |
|
</div> |
|
<div className="flex items-center gap-1 text-sm text-muted-foreground"> |
|
<Nip05 pubkey={headerUserId} append="·" /> |
|
<FormattedTimestamp |
|
timestamp={event.created_at} |
|
className="shrink-0" |
|
short={isSmallScreen} |
|
/> |
|
</div> |
|
</div> |
|
<div className="flex items-center shrink-0"> |
|
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" /> |
|
</div> |
|
</div> |
|
<NoteKindLabel kind={event.kind} event={event} size="small" className="mt-0.5" /> |
|
{webReactionParentUrl ? ( |
|
<div className="mt-2 not-prose max-w-full" data-parent-note-preview> |
|
<WebPreview url={webReactionParentUrl} className="w-full" /> |
|
</div> |
|
) : parentEventId ? ( |
|
<ParentNotePreview |
|
className="mt-2" |
|
eventId={parentEventId} |
|
onClick={(e) => { |
|
e.stopPropagation() |
|
onClickParent() |
|
}} |
|
/> |
|
) : null} |
|
{show ? ( |
|
isNip25ReactionKind(event.kind) ? ( |
|
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-muted-foreground"> |
|
{reactionDisplay.status === 'pending' ? ( |
|
<Skeleton className="size-4 shrink-0 rounded-sm" aria-hidden /> |
|
) : reactionDisplay.status === 'vote_up' ? ( |
|
<span className="text-base leading-none" aria-hidden> |
|
{DISCUSSION_UPVOTE_DISPLAY} |
|
</span> |
|
) : reactionDisplay.status === 'vote_down' ? ( |
|
<span className="text-base leading-none" aria-hidden> |
|
{DISCUSSION_DOWNVOTE_DISPLAY} |
|
</span> |
|
) : ( |
|
<ReactionEmojiDisplay event={event} variant="compact" maxRawLength={64} /> |
|
)} |
|
<span>{t(notificationReactionSummaryKey(reactionDisplay))}</span> |
|
</div> |
|
) : event.kind === kinds.Zap ? ( |
|
<Zap className="mt-2" event={event} omitSenderHeading variant="compact" /> |
|
) : ( |
|
<MarkdownArticle |
|
className="mt-2" |
|
event={event} |
|
hideMetadata={true} |
|
duplicateWebPreviewCleanedUrlHints={duplicateWebPreviewCleanedUrlHints} |
|
/> |
|
) |
|
) : ( |
|
<Button |
|
variant="outline" |
|
className="text-muted-foreground font-medium mt-2" |
|
onClick={(e) => { |
|
e.stopPropagation() |
|
setShowMuted(true) |
|
}} |
|
> |
|
{t('Temporarily display this reply')} |
|
</Button> |
|
)} |
|
</div> |
|
</div> |
|
</Collapsible> |
|
{show && !isNip25ReactionKind(event.kind) && ( |
|
<NoteStats |
|
className="ml-14 pl-1 mr-4 mt-2" |
|
event={event} |
|
displayTopZapsAndLikes={event.kind !== kinds.Zap} |
|
fetchIfNotExisting |
|
/> |
|
)} |
|
</div> |
|
) |
|
} |
|
|
|
export function ReplyNoteSkeleton() { |
|
return ( |
|
<div className="px-4 py-3 flex items-start space-x-2 w-full"> |
|
<Skeleton className="w-9 h-9 rounded-full shrink-0 mt-0.5" /> |
|
<div className="w-full"> |
|
<div className="py-1"> |
|
<Skeleton className="h-3 w-16" /> |
|
</div> |
|
<div className="my-1"> |
|
<Skeleton className="w-full h-4 my-1 mt-2" /> |
|
</div> |
|
<div className="my-1"> |
|
<Skeleton className="w-2/3 h-4 my-1" /> |
|
</div> |
|
</div> |
|
</div> |
|
) |
|
}
|
|
|