feat: following badge
This commit is contained in:
@@ -20,10 +20,10 @@ import { toast } from 'sonner'
|
|||||||
export default function FollowButton({ pubkey }: { pubkey: string }) {
|
export default function FollowButton({ pubkey }: { pubkey: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pubkey: accountPubkey, checkLogin } = useNostr()
|
const { pubkey: accountPubkey, checkLogin } = useNostr()
|
||||||
const { followings, follow, unfollow } = useFollowList()
|
const { followingSet, follow, unfollow } = useFollowList()
|
||||||
const [updating, setUpdating] = useState(false)
|
const [updating, setUpdating] = useState(false)
|
||||||
const [hover, setHover] = useState(false)
|
const [hover, setHover] = useState(false)
|
||||||
const isFollowing = useMemo(() => followings.includes(pubkey), [followings, pubkey])
|
const isFollowing = useMemo(() => followingSet.has(pubkey), [followingSet, pubkey])
|
||||||
|
|
||||||
if (!accountPubkey || (pubkey && pubkey === accountPubkey)) return null
|
if (!accountPubkey || (pubkey && pubkey === accountPubkey)) return null
|
||||||
|
|
||||||
|
|||||||
23
src/components/FollowingBadge/index.tsx
Normal file
23
src/components/FollowingBadge/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { userIdToPubkey } from '@/lib/pubkey'
|
||||||
|
import { useFollowList } from '@/providers/FollowListProvider'
|
||||||
|
import { UserRoundCheck } from 'lucide-react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function FollowingBadge({ pubkey, userId }: { pubkey?: string; userId?: string }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { followingSet } = useFollowList()
|
||||||
|
const isFollowing = useMemo(() => {
|
||||||
|
if (pubkey) return followingSet.has(pubkey)
|
||||||
|
|
||||||
|
return userId ? followingSet.has(userIdToPubkey(userId)) : false
|
||||||
|
}, [followingSet, pubkey, userId])
|
||||||
|
|
||||||
|
if (!isFollowing) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-full bg-muted px-2 py-0.5 flex items-center" title={t('Following')}>
|
||||||
|
<UserRoundCheck className="!size-3" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { useMemo, useState } from 'react'
|
|||||||
import AudioPlayer from '../AudioPlayer'
|
import AudioPlayer from '../AudioPlayer'
|
||||||
import ClientTag from '../ClientTag'
|
import ClientTag from '../ClientTag'
|
||||||
import Content from '../Content'
|
import Content from '../Content'
|
||||||
|
import FollowingBadge from '../FollowingBadge'
|
||||||
import { FormattedTimestamp } from '../FormattedTimestamp'
|
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||||
import Nip05 from '../Nip05'
|
import Nip05 from '../Nip05'
|
||||||
import NoteOptions from '../NoteOptions'
|
import NoteOptions from '../NoteOptions'
|
||||||
@@ -28,9 +29,9 @@ import MutedNote from './MutedNote'
|
|||||||
import NsfwNote from './NsfwNote'
|
import NsfwNote from './NsfwNote'
|
||||||
import PictureNote from './PictureNote'
|
import PictureNote from './PictureNote'
|
||||||
import Poll from './Poll'
|
import Poll from './Poll'
|
||||||
|
import RelayReview from './RelayReview'
|
||||||
import UnknownNote from './UnknownNote'
|
import UnknownNote from './UnknownNote'
|
||||||
import VideoNote from './VideoNote'
|
import VideoNote from './VideoNote'
|
||||||
import RelayReview from './RelayReview'
|
|
||||||
|
|
||||||
export default function Note({
|
export default function Note({
|
||||||
event,
|
event,
|
||||||
@@ -117,6 +118,7 @@ export default function Note({
|
|||||||
className={`font-semibold flex truncate ${size === 'small' ? 'text-sm' : ''}`}
|
className={`font-semibold flex truncate ${size === 'small' ? 'text-sm' : ''}`}
|
||||||
skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
|
skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
|
||||||
/>
|
/>
|
||||||
|
<FollowingBadge pubkey={event.pubkey} />
|
||||||
<ClientTag event={event} />
|
<ClientTag event={event} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import FollowingBadge from '@/components/FollowingBadge'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { formatNpub, userIdToPubkey } from '@/lib/pubkey'
|
import { formatNpub, userIdToPubkey } from '@/lib/pubkey'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -87,7 +88,10 @@ const MentionList = forwardRef<MentionListHandle, MentionListProps>((props, ref)
|
|||||||
<div className="flex gap-2 w-80 items-center truncate pointer-events-none">
|
<div className="flex gap-2 w-80 items-center truncate pointer-events-none">
|
||||||
<SimpleUserAvatar userId={item} />
|
<SimpleUserAvatar userId={item} />
|
||||||
<div className="flex-1 w-0">
|
<div className="flex-1 w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<SimpleUsername userId={item} className="font-semibold truncate" />
|
<SimpleUsername userId={item} className="font-semibold truncate" />
|
||||||
|
<FollowingBadge userId={item} />
|
||||||
|
</div>
|
||||||
<Nip05 pubkey={userIdToPubkey(item)} />
|
<Nip05 pubkey={userIdToPubkey(item)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
export default function Followings({ pubkey }: { pubkey: string }) {
|
export default function Followings({ pubkey }: { pubkey: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pubkey: accountPubkey } = useNostr()
|
const { pubkey: accountPubkey } = useNostr()
|
||||||
const { followings: selfFollowings } = useFollowList()
|
const { followingSet: selfFollowingSet } = useFollowList()
|
||||||
const { followings, isFetching } = useFetchFollowings(pubkey)
|
const { followings, isFetching } = useFetchFollowings(pubkey)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -18,7 +18,7 @@ export default function Followings({ pubkey }: { pubkey: string }) {
|
|||||||
className="flex gap-1 hover:underline w-fit items-center"
|
className="flex gap-1 hover:underline w-fit items-center"
|
||||||
>
|
>
|
||||||
{accountPubkey === pubkey ? (
|
{accountPubkey === pubkey ? (
|
||||||
selfFollowings.length
|
selfFollowingSet.size
|
||||||
) : isFetching ? (
|
) : isFetching ? (
|
||||||
<Loader className="animate-spin size-4" />
|
<Loader className="animate-spin size-4" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function ProfileList({ pubkeys }: { pubkeys: string[] }) {
|
|||||||
return (
|
return (
|
||||||
<div className="px-4 pt-2">
|
<div className="px-4 pt-2">
|
||||||
{visiblePubkeys.map((pubkey, index) => (
|
{visiblePubkeys.map((pubkey, index) => (
|
||||||
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
<UserItem key={`${index}-${pubkey}`} userId={pubkey} />
|
||||||
))}
|
))}
|
||||||
{pubkeys.length > visiblePubkeys.length && <div ref={bottomRef} />}
|
{pubkeys.length > visiblePubkeys.length && <div ref={bottomRef} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function ProfileListBySearch({ search }: { search: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
{Array.from(pubkeySet).map((pubkey, index) => (
|
{Array.from(pubkeySet).map((pubkey, index) => (
|
||||||
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
<UserItem key={`${index}-${pubkey}`} userId={pubkey} />
|
||||||
))}
|
))}
|
||||||
{hasMore && <UserItemSkeleton />}
|
{hasMore && <UserItemSkeleton />}
|
||||||
{hasMore && <div ref={bottomRef} />}
|
{hasMore && <div ref={bottomRef} />}
|
||||||
|
|||||||
@@ -378,7 +378,12 @@ function ProfileItem({
|
|||||||
className={cn('px-2 hover:bg-accent rounded-md cursor-pointer', selected && 'bg-accent')}
|
className={cn('px-2 hover:bg-accent rounded-md cursor-pointer', selected && 'bg-accent')}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<UserItem pubkey={userId} hideFollowButton className="pointer-events-none" />
|
<UserItem
|
||||||
|
userId={userId}
|
||||||
|
className="pointer-events-none"
|
||||||
|
hideFollowButton
|
||||||
|
showFollowingBadge
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,39 @@ import Nip05 from '@/components/Nip05'
|
|||||||
import UserAvatar from '@/components/UserAvatar'
|
import UserAvatar from '@/components/UserAvatar'
|
||||||
import Username from '@/components/Username'
|
import Username from '@/components/Username'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { userIdToPubkey } from '@/lib/pubkey'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import FollowingBadge from '../FollowingBadge'
|
||||||
|
|
||||||
export default function UserItem({
|
export default function UserItem({
|
||||||
pubkey,
|
userId,
|
||||||
hideFollowButton,
|
hideFollowButton,
|
||||||
|
showFollowingBadge = false,
|
||||||
className
|
className
|
||||||
}: {
|
}: {
|
||||||
pubkey: string
|
userId: string
|
||||||
hideFollowButton?: boolean
|
hideFollowButton?: boolean
|
||||||
|
showFollowingBadge?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const pubkey = useMemo(() => userIdToPubkey(userId), [userId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex gap-2 items-center h-14', className)}>
|
<div className={cn('flex gap-2 items-center h-14', className)}>
|
||||||
<UserAvatar userId={pubkey} className="shrink-0" />
|
<UserAvatar userId={userId} className="shrink-0" />
|
||||||
<div className="w-full overflow-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Username
|
<Username
|
||||||
userId={pubkey}
|
userId={userId}
|
||||||
className="font-semibold truncate max-w-full w-fit"
|
className="font-semibold truncate max-w-full w-fit"
|
||||||
skeletonClassName="h-4"
|
skeletonClassName="h-4"
|
||||||
/>
|
/>
|
||||||
<Nip05 pubkey={pubkey} />
|
{showFollowingBadge && <FollowingBadge pubkey={pubkey} />}
|
||||||
</div>
|
</div>
|
||||||
{!hideFollowButton && <FollowButton pubkey={pubkey} />}
|
<Nip05 pubkey={userId} />
|
||||||
|
</div>
|
||||||
|
{!hideFollowButton && <FollowButton pubkey={userId} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useNostr } from './NostrProvider'
|
import { useNostr } from './NostrProvider'
|
||||||
|
|
||||||
type TFollowListContext = {
|
type TFollowListContext = {
|
||||||
followings: string[]
|
followingSet: Set<string>
|
||||||
follow: (pubkey: string) => Promise<void>
|
follow: (pubkey: string) => Promise<void>
|
||||||
unfollow: (pubkey: string) => Promise<void>
|
unfollow: (pubkey: string) => Promise<void>
|
||||||
}
|
}
|
||||||
@@ -24,8 +24,8 @@ export const useFollowList = () => {
|
|||||||
export function FollowListProvider({ children }: { children: React.ReactNode }) {
|
export function FollowListProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pubkey: accountPubkey, followListEvent, publish, updateFollowListEvent } = useNostr()
|
const { pubkey: accountPubkey, followListEvent, publish, updateFollowListEvent } = useNostr()
|
||||||
const followings = useMemo(
|
const followingSet = useMemo(
|
||||||
() => (followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []),
|
() => new Set(followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []),
|
||||||
[followListEvent]
|
[followListEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
|
|||||||
return (
|
return (
|
||||||
<FollowListContext.Provider
|
<FollowListContext.Provider
|
||||||
value={{
|
value={{
|
||||||
followings,
|
followingSet,
|
||||||
follow,
|
follow,
|
||||||
unfollow
|
unfollow
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user