feat: add pinned post functionality

This commit is contained in:
codytseng 2025-10-12 21:39:16 +08:00
parent 9c554da2da
commit d131026af9
31 changed files with 563 additions and 56 deletions

View file

@ -1,10 +1,12 @@
import { Separator } from '@/components/ui/separator'
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { Event } from 'nostr-tools'
import Collapsible from '../Collapsible'
import Note from '../Note'
import NoteStats from '../NoteStats'
import PinnedButton from './PinnedButton'
import RepostDescription from './RepostDescription'
export default function MainNoteCard({
@ -12,13 +14,15 @@ export default function MainNoteCard({
className,
reposter,
embedded,
originalNoteId
originalNoteId,
pinned = false
}: {
event: Event
className?: string
reposter?: string
embedded?: boolean
originalNoteId?: string
pinned?: boolean
}) {
const { push } = useSecondaryPage()
@ -30,8 +34,9 @@ export default function MainNoteCard({
push(toNote(originalNoteId ?? event))
}}
>
<div className={`clickable ${embedded ? 'p-2 sm:p-3 border rounded-lg' : 'py-3'}`}>
<div className={cn('clickable', embedded ? 'p-2 sm:p-3 border rounded-lg' : 'py-3')}>
<Collapsible alwaysExpand={embedded}>
{pinned && <PinnedButton event={event} />}
<RepostDescription className={embedded ? '' : 'px-4'} reposter={reposter} />
<Note
className={embedded ? '' : 'px-4'}

View file

@ -0,0 +1,46 @@
import { Button } from '@/components/ui/button'
import { useNostr } from '@/providers/NostrProvider'
import { usePinList } from '@/providers/PinListProvider'
import { Loader, Pin } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function PinnedButton({ event }: { event: NostrEvent }) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { unpin } = usePinList()
const [hovered, setHovered] = useState(false)
const [unpinning, setUnpinning] = useState(false)
if (event.pubkey !== pubkey) {
return (
<div className="flex gap-1 text-sm items-center text-primary mb-1 px-4 py-0 h-fit">
<Pin size={16} className="shrink-0" />
{t('Pinned')}
</div>
)
}
return (
<Button
className="flex gap-1 text-sm text-muted-foreground items-center mb-1 px-4 py-0.5 h-fit hover:text-foreground"
variant="link"
onClick={(e) => {
e.stopPropagation()
setUnpinning(true)
unpin(event).finally(() => setUnpinning(false))
}}
disabled={unpinning}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{unpinning ? (
<Loader size={16} className="animate-spin shrink-0" />
) : (
<Pin size={16} className="shrink-0" />
)}
{unpinning ? t('Unpinning') : hovered ? t('Unpin') : t('Pinned')}
</Button>
)
}

View file

@ -10,11 +10,13 @@ import MainNoteCard from './MainNoteCard'
export default function RepostNoteCard({
event,
className,
filterMutedNotes = true
filterMutedNotes = true,
pinned = false
}: {
event: Event
className?: string
filterMutedNotes?: boolean
pinned?: boolean
}) {
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
@ -71,5 +73,12 @@ export default function RepostNoteCard({
if (!targetEvent || shouldHide) return null
return <MainNoteCard className={className} reposter={event.pubkey} event={targetEvent} />
return (
<MainNoteCard
className={className}
reposter={event.pubkey}
event={targetEvent}
pinned={pinned}
/>
)
}

View file

@ -10,11 +10,13 @@ import RepostNoteCard from './RepostNoteCard'
export default function NoteCard({
event,
className,
filterMutedNotes = true
filterMutedNotes = true,
pinned = false
}: {
event: Event
className?: string
filterMutedNotes?: boolean
pinned?: boolean
}) {
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
@ -31,10 +33,15 @@ export default function NoteCard({
if (event.kind === kinds.Repost) {
return (
<RepostNoteCard event={event} className={className} filterMutedNotes={filterMutedNotes} />
<RepostNoteCard
event={event}
className={className}
filterMutedNotes={filterMutedNotes}
pinned={pinned}
/>
)
}
return <MainNoteCard event={event} className={className} />
return <MainNoteCard event={event} className={className} pinned={pinned} />
}
export function NoteCardLoadingSkeleton() {