diff --git a/src/renderer/src/components/FollowButton/index.tsx b/src/renderer/src/components/FollowButton/index.tsx
new file mode 100644
index 00000000..7a88cd18
--- /dev/null
+++ b/src/renderer/src/components/FollowButton/index.tsx
@@ -0,0 +1,77 @@
+import { TDraftEvent } from '@common/types'
+import { Button } from '@renderer/components/ui/button'
+import { useFetchFollowings } from '@renderer/hooks'
+import { useNostr } from '@renderer/providers/NostrProvider'
+import dayjs from 'dayjs'
+import { Loader } from 'lucide-react'
+import { kinds } from 'nostr-tools'
+import { useMemo, useState } from 'react'
+
+export default function FollowButton({ pubkey }: { pubkey: string }) {
+ const { pubkey: accountPubkey, publish } = useNostr()
+ const { followings, followListEvent, refresh } = useFetchFollowings(accountPubkey)
+ const [updating, setUpdating] = useState(false)
+ const isFollowing = useMemo(() => followings.includes(pubkey), [followings, pubkey])
+
+ if (!accountPubkey || pubkey === accountPubkey) return null
+
+ const follow = async (e: React.MouseEvent) => {
+ e.stopPropagation()
+ if (isFollowing) return
+
+ setUpdating(true)
+ const newFollowListEvent: TDraftEvent = {
+ kind: kinds.Contacts,
+ content: followListEvent?.content ?? '',
+ created_at: dayjs().unix(),
+ tags: (followListEvent?.tags ?? []).concat([['p', pubkey]])
+ }
+ console.log(newFollowListEvent)
+ try {
+ await publish(newFollowListEvent)
+ await refresh()
+ } catch (error) {
+ console.error(error)
+ } finally {
+ setUpdating(false)
+ }
+ }
+
+ const unfollow = async (e: React.MouseEvent) => {
+ e.stopPropagation()
+ if (!isFollowing || !followListEvent) return
+
+ setUpdating(true)
+ const newFollowListEvent: TDraftEvent = {
+ kind: kinds.Contacts,
+ content: followListEvent.content ?? '',
+ created_at: dayjs().unix(),
+ tags: followListEvent.tags.filter(
+ ([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey
+ )
+ }
+ try {
+ await publish(newFollowListEvent)
+ await refresh()
+ } catch (error) {
+ console.error(error)
+ } finally {
+ setUpdating(false)
+ }
+ }
+
+ return isFollowing ? (
+
+ ) : (
+
+ )
+}
diff --git a/src/renderer/src/components/ProfileBanner/index.tsx b/src/renderer/src/components/ProfileBanner/index.tsx
new file mode 100644
index 00000000..d30d5ee7
--- /dev/null
+++ b/src/renderer/src/components/ProfileBanner/index.tsx
@@ -0,0 +1,32 @@
+import { generateImageByPubkey } from '@renderer/lib/pubkey'
+import { useEffect, useMemo, useState } from 'react'
+
+export default function ProfileBanner({
+ pubkey,
+ banner,
+ className
+}: {
+ pubkey: string
+ banner?: string
+ className?: string
+}) {
+ const defaultBanner = useMemo(() => generateImageByPubkey(pubkey), [pubkey])
+ const [bannerUrl, setBannerUrl] = useState(banner || defaultBanner)
+
+ useEffect(() => {
+ if (banner) {
+ setBannerUrl(banner)
+ } else {
+ setBannerUrl(defaultBanner)
+ }
+ }, [defaultBanner, banner])
+
+ return (
+
setBannerUrl(defaultBanner)}
+ />
+ )
+}
diff --git a/src/renderer/src/components/ProfileCard/index.tsx b/src/renderer/src/components/ProfileCard/index.tsx
index 3abf14a7..2be32d26 100644
--- a/src/renderer/src/components/ProfileCard/index.tsx
+++ b/src/renderer/src/components/ProfileCard/index.tsx
@@ -2,26 +2,28 @@ import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/ava
import { useFetchProfile } from '@renderer/hooks'
import { generateImageByPubkey } from '@renderer/lib/pubkey'
import { useMemo } from 'react'
+import FollowButton from '../FollowButton'
import Nip05 from '../Nip05'
import ProfileAbout from '../ProfileAbout'
export default function ProfileCard({ pubkey }: { pubkey: string }) {
const { avatar = '', username, nip05, about } = useFetchProfile(pubkey)
- const defaultAvatar = useMemo(() => generateImageByPubkey(pubkey), [pubkey])
+ const defaultImage = useMemo(() => generateImageByPubkey(pubkey), [pubkey])
return (
-
+
-
+
-
+
-
-
{username}
- {nip05 &&
}
-
+
+
+
+
{username}
+ {nip05 &&
}
{about && (
(null)
const [followings, setFollowings] = useState
([])
useEffect(() => {
const init = async () => {
if (!pubkey) return
- const followListEvent = await client.fetchEventByFilter({
+ const event = await client.fetchEventByFilter({
authors: [pubkey],
- kinds: [kinds.Contacts],
- limit: 1
+ kinds: [kinds.Contacts]
})
- if (!followListEvent) return
+ if (!event) return
+ setFollowListEvent(event)
setFollowings(
- followListEvent.tags
+ event.tags
.filter(tagNameEquals('p'))
.map(([, pubkey]) => pubkey)
.filter(Boolean)
@@ -29,5 +30,27 @@ export function useFetchFollowings(pubkey?: string) {
init()
}, [pubkey])
- return followings
+ const refresh = async () => {
+ if (!pubkey) return
+
+ const filter = {
+ authors: [pubkey],
+ kinds: [kinds.Contacts]
+ }
+
+ client.deleteEventCacheByFilter(filter)
+ const event = await client.fetchEventByFilter(filter)
+ if (!event) return
+
+ setFollowListEvent(event)
+ setFollowings(
+ event.tags
+ .filter(tagNameEquals('p'))
+ .map(([, pubkey]) => pubkey)
+ .filter(Boolean)
+ .reverse()
+ )
+ }
+
+ return { followings, followListEvent, refresh }
}
diff --git a/src/renderer/src/pages/secondary/FollowingListPage/index.tsx b/src/renderer/src/pages/secondary/FollowingListPage/index.tsx
index cb096f9e..5c9c33bb 100644
--- a/src/renderer/src/pages/secondary/FollowingListPage/index.tsx
+++ b/src/renderer/src/pages/secondary/FollowingListPage/index.tsx
@@ -1,3 +1,4 @@
+import FollowButton from '@renderer/components/FollowButton'
import Nip05 from '@renderer/components/Nip05'
import UserAvatar from '@renderer/components/UserAvatar'
import Username from '@renderer/components/Username'
@@ -7,7 +8,7 @@ import { useEffect, useRef, useState } from 'react'
export default function ProfilePage({ pubkey }: { pubkey?: string }) {
const { username } = useFetchProfile(pubkey)
- const followings = useFetchFollowings(pubkey)
+ const { followings } = useFetchFollowings(pubkey)
const [visibleFollowings, setVisibleFollowings] = useState([])
const observer = useRef(null)
const bottomRef = useRef(null)
@@ -47,7 +48,7 @@ export default function ProfilePage({ pubkey }: { pubkey?: string }) {
{visibleFollowings.map((pubkey, index) => (
-
+
))}
{followings.length > visibleFollowings.length &&
}
@@ -64,8 +65,9 @@ function FollowingItem({ pubkey }: { pubkey: string }) {
+
)
}
diff --git a/src/renderer/src/pages/secondary/ProfilePage/index.tsx b/src/renderer/src/pages/secondary/ProfilePage/index.tsx
index 3d8438b2..e5143ab5 100644
--- a/src/renderer/src/pages/secondary/ProfilePage/index.tsx
+++ b/src/renderer/src/pages/secondary/ProfilePage/index.tsx
@@ -1,6 +1,8 @@
+import FollowButton from '@renderer/components/FollowButton'
import Nip05 from '@renderer/components/Nip05'
import NoteList from '@renderer/components/NoteList'
import ProfileAbout from '@renderer/components/ProfileAbout'
+import ProfileBanner from '@renderer/components/ProfileBanner'
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar'
import { Separator } from '@renderer/components/ui/separator'
import { useFetchFollowings, useFetchProfile } from '@renderer/hooks'
@@ -9,15 +11,21 @@ import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout'
import { toFollowingList } from '@renderer/lib/link'
import { formatNpub, generateImageByPubkey } from '@renderer/lib/pubkey'
import { SecondaryPageLink } from '@renderer/PageManager'
+import { useNostr } from '@renderer/providers/NostrProvider'
import { Copy } from 'lucide-react'
import { nip19 } from 'nostr-tools'
-import { useEffect, useMemo, useState } from 'react'
+import { useMemo, useState } from 'react'
export default function ProfilePage({ pubkey }: { pubkey?: string }) {
const { banner, username, nip05, about, avatar } = useFetchProfile(pubkey)
const relayList = useFetchRelayList(pubkey)
const [copied, setCopied] = useState(false)
- const followings = useFetchFollowings(pubkey)
+ const { pubkey: accountPubkey } = useNostr()
+ const { followings } = useFetchFollowings(pubkey)
+ const isFollowingYou = useMemo(
+ () => !!accountPubkey && accountPubkey !== pubkey && followings.includes(accountPubkey),
+ [followings, pubkey]
+ )
const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : undefined), [pubkey])
const defaultImage = useMemo(() => (pubkey ? generateImageByPubkey(pubkey) : ''), [pubkey])
@@ -31,10 +39,9 @@ export default function ProfilePage({ pubkey }: { pubkey?: string }) {
return (
-
+
@@ -45,7 +52,15 @@ export default function ProfilePage({ pubkey }: { pubkey?: string }) {
-
+
+ {isFollowingYou && (
+
+ Follows you
+
+ )}
+
+
+
{username}
{nip05 &&
}
)
}
-
-function ProfileBanner({
- defaultBanner,
- pubkey,
- banner,
- className
-}: {
- defaultBanner: string
- pubkey: string
- banner?: string
- className?: string
-}) {
- const [bannerUrl, setBannerUrl] = useState(banner || defaultBanner)
-
- useEffect(() => {
- if (banner) {
- setBannerUrl(banner)
- } else {
- setBannerUrl(defaultBanner)
- }
- }, [defaultBanner, banner])
-
- return (
-

setBannerUrl(defaultBanner)}
- />
- )
-}
diff --git a/src/renderer/src/services/client.service.ts b/src/renderer/src/services/client.service.ts
index 73e198c8..d6840e88 100644
--- a/src/renderer/src/services/client.service.ts
+++ b/src/renderer/src/services/client.service.ts
@@ -125,6 +125,10 @@ class ClientService {
return this.eventCache.fetch(JSON.stringify({ ...filter, limit: 1 }))
}
+ deleteEventCacheByFilter(filter: Filter) {
+ this.eventCache.delete(JSON.stringify({ ...filter, limit: 1 }))
+ }
+
async fetchEventById(id: string): Promise
{
return this.eventDataloader.load(id)
}