feat: 💨
This commit is contained in:
@@ -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} />
|
||||
|
||||
63
src/components/NoteCard/MutedNoteCard.tsx
Normal file
63
src/components/NoteCard/MutedNoteCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '选择要追加的服务器',
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user