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.
242 lines
9.2 KiB
242 lines
9.2 KiB
import { getCachedThreadContextEvents } from '@/lib/navigation-related-events' |
|
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 { getMoneroTipInfo } from '@/lib/monero-tip' |
|
import { isMentioningMutedUsers, isNip18RepostKind, isNip25ReactionKind } from '@/lib/event' |
|
import { getWebExternalReactionTargetUrl } from '@/lib/rss-article' |
|
import { relayHintsFromEventTags } from '@/lib/relay-list-builder' |
|
import { toNote } from '@/lib/link' |
|
import { cn } from '@/lib/utils' |
|
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' |
|
import storage from '@/services/local-storage.service' |
|
import { useMuteList } from '@/contexts/mute-list-context' |
|
import { muteSetHas } from '@/lib/mute-set' |
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
|
import { Event, kinds } from 'nostr-tools' |
|
import { useMemo, useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import Collapsible from '../Collapsible' |
|
import MarkdownArticle from '../Note/MarkdownArticle/MarkdownArticle' |
|
import ReactionEmojiDisplay from '../Note/ReactionEmojiDisplay' |
|
import NoteAuthorMetaLine from '../NoteAuthorMetaLine' |
|
import NoteOptions from '../NoteOptions' |
|
import NoteStats from '../NoteStats' |
|
import ParentNotePreview from '../ParentNotePreview' |
|
import WebPreview from '../WebPreview' |
|
import UserAvatar from '../UserAvatar' |
|
import Superchat from '../Note/Superchat' |
|
import Zap from '../Note/Zap' |
|
import MoneroTip from '../Note/MoneroTip' |
|
|
|
export default function ReplyNote({ |
|
event, |
|
parentEventId, |
|
onClickParent = () => {}, |
|
onClickReply, |
|
highlight = false, |
|
duplicateWebPreviewCleanedUrlHints, |
|
foregroundStats = false |
|
}: { |
|
event: Event |
|
parentEventId?: string |
|
onClickParent?: () => void |
|
onClickReply?: (event: Event) => void |
|
highlight?: boolean |
|
duplicateWebPreviewCleanedUrlHints?: string[] |
|
foregroundStats?: boolean |
|
}) { |
|
const { t } = useTranslation() |
|
const { isSmallScreen } = useScreenSize() |
|
const { navigateToNote } = useSmartNoteNavigation() |
|
const { mutePubkeySet } = useMuteList() |
|
const hideContentMentioningMutedUsers = |
|
useContentPolicyOptional()?.hideContentMentioningMutedUsers ?? |
|
storage.getHideContentMentioningMutedUsers() |
|
const [showMuted, setShowMuted] = useState(false) |
|
const reactionDisplay = useNotificationReactionDisplay(event) |
|
const webReactionParentUrl = useMemo( |
|
() => |
|
event.kind === ExtendedKind.EXTERNAL_REACTION ? getWebExternalReactionTargetUrl(event) : undefined, |
|
[event] |
|
) |
|
const parentFetchRelayHints = useMemo(() => relayHintsFromEventTags(event), [event]) |
|
const headerUserId = useMemo(() => { |
|
if (event.kind === kinds.Zap) { |
|
const info = getZapInfoFromEvent(event) |
|
return info?.senderPubkey ?? event.pubkey |
|
} |
|
if ( |
|
event.kind === ExtendedKind.MONERO_TIP_DISCLOSURE || |
|
event.kind === ExtendedKind.MONERO_TIP_RECEIPT |
|
) { |
|
const info = getMoneroTipInfo(event) |
|
return info?.senderPubkey ?? event.pubkey |
|
} |
|
return event.pubkey |
|
}, [event]) |
|
|
|
const show = useMemo(() => { |
|
if (showMuted) { |
|
return true |
|
} |
|
if (muteSetHas(mutePubkeySet, event.pubkey)) { |
|
return false |
|
} |
|
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(event, mutePubkeySet)) { |
|
return false |
|
} |
|
return true |
|
}, [showMuted, mutePubkeySet, event, hideContentMentioningMutedUsers]) |
|
|
|
return ( |
|
<div |
|
className={`clickable border-b pb-3 transition-colors duration-500 ${highlight ? 'bg-primary/50' : ''}`} |
|
onClick={(e) => { |
|
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, getCachedThreadContextEvents(event)) |
|
} |
|
}} |
|
> |
|
<Collapsible> |
|
<div className="px-4 pt-3"> |
|
<div className="flex min-w-0 items-start justify-between gap-2"> |
|
<div className="flex min-w-0 flex-1 items-start gap-2"> |
|
<UserAvatar |
|
userId={headerUserId} |
|
size="medium" |
|
className="mt-0.5 shrink-0" |
|
maxFileSizeKb={2048} |
|
deferRemoteAvatar={false} |
|
/> |
|
<NoteAuthorMetaLine |
|
userId={headerUserId} |
|
timestamp={event.created_at} |
|
powEvent={event} |
|
usernameClassName="max-w-[min(12rem,40vw)] text-sm text-muted-foreground hover:text-foreground" |
|
skeletonClassName="h-3" |
|
timestampShort={isSmallScreen} |
|
/> |
|
</div> |
|
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" /> |
|
</div> |
|
{webReactionParentUrl ? ( |
|
<div className="not-prose mt-1.5 max-w-full" data-parent-note-preview> |
|
<WebPreview url={webReactionParentUrl} className="w-full" /> |
|
</div> |
|
) : parentEventId && |
|
event.kind !== kinds.Zap && |
|
event.kind !== ExtendedKind.PAYMENT_NOTIFICATION && |
|
event.kind !== ExtendedKind.ZAP_RECEIPT && |
|
event.kind !== ExtendedKind.MONERO_TIP_DISCLOSURE && |
|
event.kind !== ExtendedKind.MONERO_TIP_RECEIPT ? ( |
|
<ParentNotePreview |
|
appearance="subtle" |
|
className="mt-1.5" |
|
eventId={parentEventId} |
|
relayHints={parentFetchRelayHints} |
|
onClick={(e) => { |
|
e.stopPropagation() |
|
onClickParent() |
|
}} |
|
/> |
|
) : null} |
|
{show ? ( |
|
isNip25ReactionKind(event.kind) ? ( |
|
<div |
|
className={cn( |
|
'mt-2 flex min-h-0 min-w-0 flex-wrap items-end gap-x-2 gap-y-1 overflow-visible pb-1.5', |
|
reactionDisplay.status === 'default' |
|
? 'text-foreground' |
|
: 'text-muted-foreground text-sm' |
|
)} |
|
> |
|
{reactionDisplay.status === 'vote_up' ? ( |
|
<span className="text-sm leading-none opacity-90" aria-hidden> |
|
{DISCUSSION_UPVOTE_DISPLAY} |
|
</span> |
|
) : reactionDisplay.status === 'vote_down' ? ( |
|
<span className="text-sm leading-none opacity-90" aria-hidden> |
|
{DISCUSSION_DOWNVOTE_DISPLAY} |
|
</span> |
|
) : ( |
|
<ReactionEmojiDisplay event={event} variant="thread" maxRawLength={64} /> |
|
)} |
|
{reactionDisplay.status !== 'default' && ( |
|
<span className="text-sm text-foreground/85">{t(notificationReactionSummaryKey(reactionDisplay))}</span> |
|
)} |
|
</div> |
|
) : event.kind === kinds.Zap || event.kind === ExtendedKind.ZAP_RECEIPT ? ( |
|
<Zap className="mt-1.5" event={event} variant="thread" /> |
|
) : event.kind === ExtendedKind.MONERO_TIP_DISCLOSURE || |
|
event.kind === ExtendedKind.MONERO_TIP_RECEIPT ? ( |
|
<MoneroTip className="mt-1.5" event={event} variant="thread" /> |
|
) : event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? ( |
|
<Superchat className="mt-1.5" event={event} variant="thread" /> |
|
) : isNip18RepostKind(event.kind) ? null : ( |
|
<MarkdownArticle |
|
className="mt-2" |
|
event={event} |
|
hideMetadata={true} |
|
lazyMedia={false} |
|
duplicateWebPreviewCleanedUrlHints={duplicateWebPreviewCleanedUrlHints} |
|
/> |
|
) |
|
) : ( |
|
<Button |
|
variant="outline" |
|
className="mt-2 font-medium text-muted-foreground" |
|
onClick={(e) => { |
|
e.stopPropagation() |
|
setShowMuted(true) |
|
}} |
|
> |
|
{t('Temporarily display this reply')} |
|
</Button> |
|
)} |
|
</div> |
|
</Collapsible> |
|
{show && !isNip18RepostKind(event.kind) && ( |
|
<NoteStats |
|
className="mt-2 px-4" |
|
event={event} |
|
fetchIfNotExisting |
|
foregroundStats={foregroundStats} |
|
useIconOnlyLikeTrigger={isNip25ReactionKind(event.kind)} |
|
/> |
|
)} |
|
</div> |
|
) |
|
} |
|
|
|
export function ReplyNoteSkeleton() { |
|
return ( |
|
<div className="w-full px-4 py-3"> |
|
<div className="flex items-start gap-2"> |
|
<Skeleton className="mt-0.5 h-9 w-9 shrink-0 rounded-full" /> |
|
<div className="min-w-0 flex-1"> |
|
<Skeleton className="h-3 w-16" /> |
|
<Skeleton className="mt-2 h-3 w-24" /> |
|
</div> |
|
</div> |
|
<Skeleton className="mt-3 h-4 w-full" /> |
|
<Skeleton className="mt-2 h-4 w-2/3" /> |
|
</div> |
|
) |
|
}
|
|
|