feat: 💨

This commit is contained in:
codytseng
2025-02-18 14:55:58 +08:00
parent e61fd2e172
commit fe7d3a8b32
11 changed files with 158 additions and 31 deletions

View File

@@ -1,10 +1,13 @@
import { GROUP_METADATA_EVENT_KIND } from '@/constants'
import { isSupportedKind } from '@/lib/event'
import { useMuteList } from '@/providers/MuteListProvider'
import { Event, kinds } from 'nostr-tools'
import { useState } from 'react'
import GroupMetadataCard from './GroupMetadataCard'
import LiveEventCard from './LiveEventCard'
import LongFormArticleCard from './LongFormArticleCard'
import MainNoteCard from './MainNoteCard'
import MutedNoteCard from './MutedNoteCard'
import UnknownNoteCard from './UnknownNoteCard'
export default function GenericNoteCard({
@@ -20,6 +23,21 @@ export default function GenericNoteCard({
embedded?: boolean
originalNoteId?: string
}) {
const [showMuted, setShowMuted] = useState(false)
const { mutePubkeys } = useMuteList()
if (mutePubkeys.includes(event.pubkey) && !showMuted) {
return (
<MutedNoteCard
event={event}
className={className}
reposter={reposter}
embedded={embedded}
show={() => setShowMuted(true)}
/>
)
}
if (isSupportedKind(event.kind)) {
return (
<MainNoteCard event={event} className={className} reposter={reposter} embedded={embedded} />

View File

@@ -0,0 +1,63 @@
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import { Eye } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
import { FormattedTimestamp } from '../FormattedTimestamp'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import RepostDescription from './RepostDescription'
export default function MutedNoteCard({
event,
show,
reposter,
embedded,
className
}: {
event: Event
show: () => void
reposter?: string
embedded?: boolean
className?: string
}) {
const { t } = useTranslation()
return (
<div className={className}>
<div className={cn(embedded ? 'p-2 sm:p-3 border rounded-lg' : 'px-4 py-3')}>
<RepostDescription reposter={reposter} />
<div className="flex items-center space-x-2">
<UserAvatar userId={event.pubkey} size={embedded ? 'small' : 'normal'} />
<div
className={`flex-1 w-0 ${embedded ? 'flex space-x-2 items-center overflow-hidden' : ''}`}
>
<Username
userId={event.pubkey}
className={cn('font-semibold flex truncate', embedded ? 'text-sm' : '')}
skeletonClassName={embedded ? 'h-3' : 'h-4'}
/>
<div className="text-xs text-muted-foreground line-clamp-1">
<FormattedTimestamp timestamp={event.created_at} />
</div>
</div>
</div>
<div className="flex flex-col gap-2 items-center text-muted-foreground font-medium my-4">
<div>{t('This user is muted')}</div>
<Button
onClick={(e) => {
e.stopPropagation()
show()
}}
variant="outline"
>
<Eye />
{t('Temporarily display this note')}
</Button>
</div>
</div>
{!embedded && <Separator />}
</div>
)
}

View File

@@ -5,11 +5,11 @@ import { cn } from '@/lib/utils'
import { Check, Copy } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import RepostDescription from './RepostDescription'
import { useTranslation } from 'react-i18next'
import { FormattedTimestamp } from '../FormattedTimestamp'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import { FormattedTimestamp } from '../FormattedTimestamp'
import { useTranslation } from 'react-i18next'
import RepostDescription from './RepostDescription'
export default function UnknownNoteCard({
event,
@@ -44,7 +44,7 @@ export default function UnknownNoteCard({
</div>
</div>
</div>
<div className="flex flex-col gap-2 items-center text-muted-foreground font-medium mt-2">
<div className="flex flex-col gap-2 items-center text-muted-foreground font-medium my-4">
<div>{t('Cannot handle event of kind k', { k: event.kind })}</div>
<Button
onClick={(e) => {
@@ -53,7 +53,7 @@ export default function UnknownNoteCard({
setIsCopied(true)
setTimeout(() => setIsCopied(false), 2000)
}}
variant="ghost"
variant="outline"
>
{isCopied ? <Check /> : <Copy />} Copy event ID
</Button>

View File

@@ -5,11 +5,12 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { getSharableEventId } from '@/lib/event'
import { pubkeyToNpub } from '@/lib/pubkey'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { BellOff, Code, Copy, Ellipsis } from 'lucide-react'
import { Bell, BellOff, Code, Copy, Ellipsis } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import RawEventDialog from './RawEventDialog'
@@ -17,7 +18,8 @@ export default function NoteOptions({ event }: { event: Event }) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
const { mutePubkey } = useMuteList()
const { mutePubkey, unmutePubkey, mutePubkeys } = useMuteList()
const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event])
return (
<div className="h-4" onClick={(e) => e.stopPropagation()}>
@@ -30,22 +32,28 @@ export default function NoteOptions({ event }: { event: Event }) {
</DropdownMenuTrigger>
<DropdownMenuContent collisionPadding={8}>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText('nostr:' + getSharableEventId(event))}
onClick={() => navigator.clipboard.writeText(getSharableEventId(event))}
>
<Copy />
{t('copy embedded code')}
{t('Copy event ID')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(pubkeyToNpub(event.pubkey) ?? '')}
>
<Copy />
{t('Copy user ID')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsRawEventDialogOpen(true)}>
<Code />
{t('raw event')}
{t('View raw event')}
</DropdownMenuItem>
{pubkey && (
<DropdownMenuItem
onClick={() => mutePubkey(event.pubkey)}
onClick={() => (isMuted ? unmutePubkey(event.pubkey) : mutePubkey(event.pubkey))}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('mute author')}
{isMuted ? <Bell /> : <BellOff />}
{isMuted ? t('Unmute user') : t('Mute user')}
</DropdownMenuItem>
)}
</DropdownMenuContent>

View File

@@ -5,6 +5,7 @@ import { extractEmbeddedNotesFromContent, extractImagesFromContent } from '@/lib
import { toNote } from '@/lib/link'
import { tagNameEquals } from '@/lib/tag'
import { useSecondaryPage } from '@/PageManager'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import dayjs from 'dayjs'
@@ -185,6 +186,10 @@ NotificationList.displayName = 'NotificationList'
export default NotificationList
function NotificationItem({ notification }: { notification: Event }) {
const { mutePubkeys } = useMuteList()
if (mutePubkeys.includes(notification.pubkey)) {
return null
}
if (notification.kind === kinds.Reaction) {
return <ReactionNotification notification={notification} />
}

View File

@@ -1,5 +1,7 @@
import { cn } from '@/lib/utils'
import { useMuteList } from '@/providers/MuteListProvider'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import UserAvatar from '../UserAvatar'
@@ -13,6 +15,9 @@ export default function ParentNotePreview({
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
}) {
const { t } = useTranslation()
const { mutePubkeys } = useMuteList()
const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event])
return (
<div
className={cn(
@@ -23,7 +28,11 @@ export default function ParentNotePreview({
>
<div className="shrink-0">{t('reply to')}</div>
<UserAvatar className="shrink-0" userId={event.pubkey} size="tiny" />
<div className="truncate">{event.content}</div>
{isMuted ? (
<div className="truncate">{t('[muted]')}</div>
) : (
<div className="truncate">{event.content}</div>
)}
</div>
)
}

View File

@@ -30,7 +30,7 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) {
onClick={() => navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))}
>
<Copy />
{t('copy embedded code')}
{t('Copy user ID')}
</DropdownMenuItem>
{mutePubkeys.includes(pubkey) ? (
<DropdownMenuItem
@@ -38,7 +38,7 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) {
className="text-destructive focus:text-destructive"
>
<Bell />
{t('unmute user')}
{t('Unmute user')}
</DropdownMenuItem>
) : (
<DropdownMenuItem
@@ -46,7 +46,7 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) {
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('mute user')}
{t('Mute user')}
</DropdownMenuItem>
)}
</DropdownMenuContent>

View File

@@ -1,4 +1,8 @@
import { Button } from '@/components/ui/button'
import { useMuteList } from '@/providers/MuteListProvider'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Content from '../Content'
import { FormattedTimestamp } from '../FormattedTimestamp'
import NoteStats from '../NoteStats'
@@ -17,6 +21,14 @@ export default function ReplyNote({
onClickParent?: (eventId: string) => void
highlight?: boolean
}) {
const { t } = useTranslation()
const { mutePubkeys } = useMuteList()
const [showMuted, setShowMuted] = useState(false)
const show = useMemo(
() => showMuted || !mutePubkeys.includes(event.pubkey),
[showMuted, mutePubkeys, event]
)
return (
<div
className={`flex space-x-2 items-start rounded-lg p-2 transition-colors duration-500 ${highlight ? 'bg-highlight/50' : ''}`}
@@ -43,8 +55,20 @@ export default function ReplyNote({
}}
/>
)}
<Content className="mt-1" event={event} size="small" />
<NoteStats className="mt-2" event={event} variant="reply" />
{show ? (
<>
<Content className="mt-1" event={event} size="small" />
<NoteStats className="mt-2" event={event} variant="reply" />
</>
) : (
<Button
variant="outline"
className="text-muted-foreground font-medium mt-2"
onClick={() => setShowMuted(true)}
>
{t('Temporarily display this reply')}
</Button>
)}
</div>
</div>
)

View File

@@ -41,8 +41,9 @@ export default {
'Your post has been published': 'Your post has been published',
Repost: 'Repost',
Quote: 'Quote',
'copy embedded code': 'copy embedded code',
'raw event': 'raw event',
'Copy event ID': 'Copy event ID',
'Copy user ID': 'Copy user ID',
'View raw event': 'View raw event',
Like: 'Like',
'switch to light theme': 'switch to light theme',
'switch to dark theme': 'switch to dark theme',
@@ -153,9 +154,8 @@ export default {
Mute: 'Mute',
Muted: 'Muted',
Unmute: 'Unmute',
'mute author': 'mute author',
'mute user': 'mute user',
'unmute user': 'unmute user',
'Mute user': 'Mute user',
'Unmute user': 'Unmute user',
'Append n relays': 'Append {{n}} relays',
Append: 'Append',
'Select relays to append': 'Select relays to append',

View File

@@ -41,8 +41,9 @@ export default {
'Your post has been published': '您的笔记已发布',
Repost: '转发',
Quote: '引用',
'copy embedded code': '复制嵌入代码',
'raw event': '原始事件',
'Copy event ID': '复制事件 ID',
'Copy user ID': '复制用户 ID',
'View raw event': '查看原始事件',
Like: '点赞',
'switch to light theme': '切换到浅色主题',
'switch to dark theme': '切换到深色主题',
@@ -154,9 +155,8 @@ export default {
Mute: '屏蔽',
Muted: '已屏蔽',
Unmute: '取消屏蔽',
'mute author': '屏蔽作者',
'mute user': '屏蔽用户',
'unmute user': '取消屏蔽用户',
'Mute user': '屏蔽用户',
'Unmute user': '取消屏蔽用户',
'Append n relays': '追加 {{n}} 个服务器',
Append: '追加',
'Select relays to append': '选择要追加的服务器',

View File

@@ -38,7 +38,7 @@ export function createReactionDraftEvent(event: Event): TDraftEvent {
export function createRepostDraftEvent(event: Event): TDraftEvent {
const isProtected = isProtectedEvent(event)
const tags = [
['e', event.id, client.getEventHint(event.id), 'mentions', event.pubkey],
['e', event.id, client.getEventHint(event.id), '', event.pubkey],
['p', event.pubkey]
]