- {events
- .filter((event) => listMode === 'postsAndReplies' || !isReplyNoteEvent(event))
- .map((event) => (
-
- ))}
+ {events.filter(eventFilter).map((event) => (
+
+ ))}
)}
diff --git a/src/components/NoteStats/NoteOptions/index.tsx b/src/components/NoteStats/NoteOptions/index.tsx
index e0734a8b..6c3aa6a8 100644
--- a/src/components/NoteStats/NoteOptions/index.tsx
+++ b/src/components/NoteStats/NoteOptions/index.tsx
@@ -5,7 +5,9 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { getSharableEventId } from '@/lib/event'
-import { Code, Copy, Ellipsis } from 'lucide-react'
+import { useMuteList } from '@/providers/MuteListProvider'
+import { useNostr } from '@/providers/NostrProvider'
+import { BellOff, Code, Copy, Ellipsis } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -13,7 +15,9 @@ import RawEventDialog from './RawEventDialog'
export default function NoteOptions({ event }: { event: Event }) {
const { t } = useTranslation()
+ const { pubkey } = useNostr()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
+ const { mutePubkey } = useMuteList()
return (
e.stopPropagation()}>
@@ -35,6 +39,15 @@ export default function NoteOptions({ event }: { event: Event }) {
{t('raw event')}
+ {pubkey && (
+
mutePubkey(event.pubkey)}
+ className="text-destructive focus:text-destructive"
+ >
+
+ {t('mute author')}
+
+ )}
+
+
+
+
+ navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))}
+ >
+
+ {t('copy embedded code')}
+
+ {mutePubkeys.includes(pubkey) ? (
+ unmutePubkey(pubkey)}
+ className="text-destructive focus:text-destructive"
+ >
+
+ {t('unmute user')}
+
+ ) : (
+ mutePubkey(pubkey)}
+ className="text-destructive focus:text-destructive"
+ >
+
+ {t('mute user')}
+
+ )}
+
+
+ )
+}
diff --git a/src/constants.ts b/src/constants.ts
index b6e7305d..b42fbda9 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -7,6 +7,7 @@ export const StorageKey = {
CURRENT_ACCOUNT: 'currentAccount',
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap',
ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap',
+ ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap',
ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap',
ADD_CLIENT_TAG: 'addClientTag'
}
diff --git a/src/hooks/useFetchFollowings.tsx b/src/hooks/useFetchFollowings.tsx
index a3fc0be7..cbdc72eb 100644
--- a/src/hooks/useFetchFollowings.tsx
+++ b/src/hooks/useFetchFollowings.tsx
@@ -1,4 +1,4 @@
-import { getFollowingsFromFollowListEvent } from '@/lib/event'
+import { extractPubkeysFromEventTags } from '@/lib/tag'
import client from '@/services/client.service'
import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
@@ -18,7 +18,7 @@ export function useFetchFollowings(pubkey?: string | null) {
if (!event) return
setFollowListEvent(event)
- setFollowings(getFollowingsFromFollowListEvent(event))
+ setFollowings(extractPubkeysFromEventTags(event.tags))
} finally {
setIsFetching(false)
}
diff --git a/src/hooks/useFetchProfile.tsx b/src/hooks/useFetchProfile.tsx
index 7c5fec1e..ba69e1eb 100644
--- a/src/hooks/useFetchProfile.tsx
+++ b/src/hooks/useFetchProfile.tsx
@@ -1,17 +1,21 @@
+import { getProfileFromProfileEvent } from '@/lib/event'
import { userIdToPubkey } from '@/lib/pubkey'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
+import storage from '@/services/storage.service'
import { TProfile } from '@/types'
-import { useEffect, useMemo, useState } from 'react'
+import { useEffect, useState } from 'react'
export function useFetchProfile(id?: string) {
const { profile: currentAccountProfile } = useNostr()
const [isFetching, setIsFetching] = useState(true)
const [error, setError] = useState(null)
const [profile, setProfile] = useState(null)
- const pubkey = useMemo(() => (id ? userIdToPubkey(id) : undefined), [id])
+ const [pubkey, setPubkey] = useState(null)
useEffect(() => {
+ setProfile(null)
+ setPubkey(null)
const fetchProfile = async () => {
setIsFetching(true)
try {
@@ -21,6 +25,16 @@ export function useFetchProfile(id?: string) {
return
}
+ const pubkey = userIdToPubkey(id)
+ setPubkey(pubkey)
+ const storedProfileEvent = storage.getAccountProfileEvent(pubkey)
+ if (storedProfileEvent) {
+ const profile = getProfileFromProfileEvent(storedProfileEvent)
+ setProfile(profile)
+ setIsFetching(false)
+ return
+ }
+
const profile = await client.fetchProfile(id)
if (profile) {
setProfile(profile)
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 99532dab..3480c63e 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -49,6 +49,7 @@ export default {
Note: 'Note',
"username's following": "{{username}}'s following",
"username's used relays": "{{username}}'s used relays",
+ "username's muted": "{{username}}'s muted",
Login: 'Login',
'Follows you': 'Follows you',
'Relay Settings': 'Relay Settings',
@@ -146,6 +147,12 @@ export default {
password: 'password',
'Save to': 'Save to',
'Enter a name for the new relay set': 'Enter a name for the new relay set',
- 'Save to a new relay set': 'Save to a new relay set'
+ 'Save to a new relay set': 'Save to a new relay set',
+ Mute: 'Mute',
+ Muted: 'Muted',
+ Unmute: 'Unmute',
+ 'mute author': 'mute author',
+ 'mute user': 'mute user',
+ 'unmute user': 'unmute user'
}
}
diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts
index d191ebcf..387f51bb 100644
--- a/src/i18n/zh.ts
+++ b/src/i18n/zh.ts
@@ -49,6 +49,7 @@ export default {
Note: '笔记',
"username's following": '{{username}} 的关注',
"username's used relays": '{{username}} 使用的服务器',
+ "username's muted": '{{username}} 屏蔽的用户',
Login: '登录',
'Follows you': '关注了你',
'Relay Settings': '服务器设置',
@@ -147,6 +148,12 @@ export default {
password: '密码',
'Save to': '保存到',
'Enter a name for the new relay set': '输入新服务器组的名称',
- 'Save to a new relay set': '保存到新服务器组'
+ 'Save to a new relay set': '保存到新服务器组',
+ Mute: '屏蔽',
+ Muted: '已屏蔽',
+ Unmute: '取消屏蔽',
+ 'mute author': '屏蔽作者',
+ 'mute user': '屏蔽用户',
+ 'unmute user': '取消屏蔽用户'
}
}
diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts
index a7198ce9..2a334cd3 100644
--- a/src/lib/draft-event.ts
+++ b/src/lib/draft-event.ts
@@ -202,6 +202,15 @@ export function createFollowListDraftEvent(tags: string[][], content?: string):
}
}
+export function createMuteListDraftEvent(tags: string[][], content?: string): TDraftEvent {
+ return {
+ kind: kinds.Mutelist,
+ content: content ?? '',
+ created_at: dayjs().unix(),
+ tags
+ }
+}
+
export function createProfileDraftEvent(content: string, tags: string[][] = []): TDraftEvent {
return {
kind: kinds.Metadata,
diff --git a/src/lib/event.ts b/src/lib/event.ts
index 60e859d4..1e386181 100644
--- a/src/lib/event.ts
+++ b/src/lib/event.ts
@@ -67,18 +67,6 @@ export function getUsingClient(event: Event) {
return event.tags.find(tagNameEquals('client'))?.[1]
}
-export function getFollowingsFromFollowListEvent(event: Event) {
- return Array.from(
- new Set(
- event.tags
- .filter(tagNameEquals('p'))
- .map(([, pubkey]) => pubkey)
- .filter(Boolean)
- .reverse()
- )
- )
-}
-
export function getRelayListFromRelayListEvent(event?: Event) {
if (!event) {
return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] }
@@ -292,3 +280,7 @@ export function extractEmbeddedNotesFromContent(content: string) {
return { embeddedNotes, contentWithoutEmbeddedNotes: c }
}
+
+export function getLatestEvent(events: Event[]) {
+ return events.sort((a, b) => b.created_at - a.created_at)[0]
+}
diff --git a/src/lib/link.ts b/src/lib/link.ts
index 130710b7..398e6f04 100644
--- a/src/lib/link.ts
+++ b/src/lib/link.ts
@@ -38,6 +38,7 @@ export const toRelaySettings = (tag?: 'mailbox' | 'relay-sets') => {
export const toSettings = () => '/settings'
export const toProfileEditor = () => '/profile-editor'
export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}`
+export const toMuteList = () => '/mutes'
export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}`
export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}`
diff --git a/src/lib/tag.ts b/src/lib/tag.ts
index 0c5d80e7..81339d8f 100644
--- a/src/lib/tag.ts
+++ b/src/lib/tag.ts
@@ -42,3 +42,23 @@ export function extractImageInfoFromTag(tag: string[]): TImageInfo | null {
}
return image
}
+
+export function extractPubkeysFromEventTags(tags: string[][]) {
+ return Array.from(
+ new Set(
+ tags
+ .filter(tagNameEquals('p'))
+ .map(([, pubkey]) => pubkey)
+ .filter(Boolean)
+ .reverse()
+ )
+ )
+}
+
+export function isSameTag(tag1: string[], tag2: string[]) {
+ if (tag1.length !== tag2.length) return false
+ for (let i = 0; i < tag1.length; i++) {
+ if (tag1[i] !== tag2[i]) return false
+ }
+ return true
+}
diff --git a/src/pages/secondary/MuteListPage/index.tsx b/src/pages/secondary/MuteListPage/index.tsx
new file mode 100644
index 00000000..bb652ce0
--- /dev/null
+++ b/src/pages/secondary/MuteListPage/index.tsx
@@ -0,0 +1,87 @@
+import MuteButton from '@/components/MuteButton'
+import Nip05 from '@/components/Nip05'
+import UserAvatar from '@/components/UserAvatar'
+import Username from '@/components/Username'
+import { useFetchProfile } from '@/hooks'
+import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
+import { useMuteList } from '@/providers/MuteListProvider'
+import { useNostr } from '@/providers/NostrProvider'
+import { useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import NotFoundPage from '../NotFoundPage'
+
+export default function MuteListPage({ index }: { index?: number }) {
+ const { t } = useTranslation()
+ const { profile } = useNostr()
+ const { mutePubkeys } = useMuteList()
+ const [visibleMutePubkeys, setVisibleMutePubkeys] = useState([])
+ const bottomRef = useRef(null)
+
+ useEffect(() => {
+ setVisibleMutePubkeys(mutePubkeys.slice(0, 10))
+ }, [mutePubkeys])
+
+ useEffect(() => {
+ const options = {
+ root: null,
+ rootMargin: '10px',
+ threshold: 1
+ }
+
+ const observerInstance = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && mutePubkeys.length > visibleMutePubkeys.length) {
+ setVisibleMutePubkeys((prev) => [
+ ...prev,
+ ...mutePubkeys.slice(prev.length, prev.length + 10)
+ ])
+ }
+ }, options)
+
+ const currentBottomRef = bottomRef.current
+ if (currentBottomRef) {
+ observerInstance.observe(currentBottomRef)
+ }
+
+ return () => {
+ if (observerInstance && currentBottomRef) {
+ observerInstance.unobserve(currentBottomRef)
+ }
+ }
+ }, [visibleMutePubkeys, mutePubkeys])
+
+ if (!profile) {
+ return
+ }
+
+ return (
+
+
+ {visibleMutePubkeys.map((pubkey, index) => (
+
+ ))}
+ {mutePubkeys.length > visibleMutePubkeys.length &&
}
+
+
+ )
+}
+
+function UserItem({ pubkey }: { pubkey: string }) {
+ const { profile } = useFetchProfile(pubkey)
+ const { nip05, about } = profile || {}
+
+ return (
+
+ )
+}
diff --git a/src/pages/secondary/ProfilePage/index.tsx b/src/pages/secondary/ProfilePage/index.tsx
index ec1aeea8..ee321d09 100644
--- a/src/pages/secondary/ProfilePage/index.tsx
+++ b/src/pages/secondary/ProfilePage/index.tsx
@@ -13,6 +13,7 @@ import { useFetchRelayList } from '@/hooks/useFetchRelayList'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import {
toFollowingList,
+ toMuteList,
toOthersRelaySettings,
toProfileEditor,
toRelaySettings
@@ -21,10 +22,12 @@ import { generateImageByPubkey } from '@/lib/pubkey'
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
import { useFeed } from '@/providers/FeedProvider'
import { useFollowList } from '@/providers/FollowListProvider'
+import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import NotFoundPage from '../NotFoundPage'
+import ProfileOptions from '@/components/ProfileOptions'
export default function ProfilePage({ id, index }: { id?: string; index?: number }) {
const { t } = useTranslation()
@@ -41,6 +44,7 @@ export default function ProfilePage({ id, index }: { id?: string; index?: number
)
const { pubkey: accountPubkey } = useNostr()
const { followings: selfFollowings } = useFollowList()
+ const { mutePubkeys } = useMuteList()
const { followings } = useFetchFollowings(profile?.pubkey)
const isFollowingYou = useMemo(() => {
return (
@@ -103,6 +107,7 @@ export default function ProfilePage({ id, index }: { id?: string; index?: number
) : (
)}
+
{username}
@@ -127,11 +132,22 @@ export default function ProfilePage({ id, index }: { id?: string; index?: number
{relayList.originalRelays.length}
{t('Relays')}
+ {isSelf && (
+
+ {mutePubkeys.length}
+ {t('Muted')}
+
+ )}