feat: Show "Follows you" when logged in (#125)

* feat: add follow list functionality (NIP-02) to ProfileInfoCard component

* feat: enhance avatar display with follow
indication in ProfileInfoCard component

* fix: add aria-label for accessibility on follow indicator in ProfileInfoCard

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
mroxso
2025-05-25 00:44:27 +02:00
committed by GitHub
parent d1802df8fc
commit 206fb1681b
2 changed files with 110 additions and 4 deletions

78
.github/prompts/nostr-nip02.prompt.md vendored Normal file
View File

@@ -0,0 +1,78 @@
NIP-02
======
Follow List
-----------
`final` `optional`
A special event with kind `3`, meaning "follow list" is defined as having a list of `p` tags, one for each of the followed/known profiles one is following.
Each tag entry should contain the key for the profile, a relay URL where events from that key can be found (can be set to an empty string if not needed), and a local name (or "petname") for that profile (can also be set to an empty string or not provided), i.e., `["p", <32-bytes hex key>, <main relay URL>, <petname>]`.
The `.content` is not used.
For example:
```jsonc
{
"kind": 3,
"tags": [
["p", "91cf9..4e5ca", "wss://alicerelay.com/", "alice"],
["p", "14aeb..8dad4", "wss://bobrelay.com/nostr", "bob"],
["p", "612ae..e610f", "ws://carolrelay.com/ws", "carol"]
],
"content": "",
// other fields...
}
```
Every new following list that gets published overwrites the past ones, so it should contain all entries. Relays and clients SHOULD delete past following lists as soon as they receive a new one.
Whenever new follows are added to an existing list, clients SHOULD append them to the end of the list, so they are stored in chronological order.
## Uses
### Follow list backup
If one believes a relay will store their events for sufficient time, they can use this kind-3 event to backup their following list and recover on a different device.
### Profile discovery and context augmentation
A client may rely on the kind-3 event to display a list of followed people by profiles one is browsing; make lists of suggestions on who to follow based on the follow lists of other people one might be following or browsing; or show the data in other contexts.
### Relay sharing
A client may publish a follow list with good relays for each of their follows so other clients may use these to update their internal relay lists if needed, increasing censorship-resistance.
### Petname scheme
The data from these follow lists can be used by clients to construct local ["petname"](http://www.skyhunter.com/marcs/petnames/IntroPetNames.html) tables derived from other people's follow lists. This alleviates the need for global human-readable names. For example:
A user has an internal follow list that says
```json
[
["p", "21df6d143fb96c2ec9d63726bf9edc71", "", "erin"]
]
```
And receives two follow lists, one from `21df6d143fb96c2ec9d63726bf9edc71` that says
```json
[
["p", "a8bb3d884d5d90b413d9891fe4c4e46d", "", "david"]
]
```
and another from `a8bb3d884d5d90b413d9891fe4c4e46d` that says
```json
[
["p", "f57f54057d2a7af0efecc8b0b66f5708", "", "frank"]
]
```
When the user sees `21df6d143fb96c2ec9d63726bf9edc71` the client can show _erin_ instead;
When the user sees `a8bb3d884d5d90b413d9891fe4c4e46d` the client can show _david.erin_ instead;
When the user sees `f57f54057d2a7af0efecc8b0b66f5708` the client can show _frank.david.erin_ instead.

View File

@@ -22,7 +22,7 @@ import {
import { Input } from './ui/input';
import { Share1Icon, LightningBoltIcon, GlobeIcon } from '@radix-ui/react-icons';
import { toast } from './ui/use-toast';
import { Globe } from 'lucide-react';
import { Globe, UserCheck } from 'lucide-react';
import { Badge } from './ui/badge';
import { MusicIcon, ActivityIcon } from 'lucide-react';
@@ -60,6 +60,27 @@ const ProfileInfoCard: React.FC<ProfileInfoCardProps> = React.memo(({ pubkey })
},
});
// Fetch follow list (NIP-02) to check if profile owner follows the logged-in user
const { events: followListEvents } = useNostrEvents({
filter: {
authors: [pubkey],
kinds: [3], // NIP-02 follow list event kind
limit: 1,
},
enabled: !!userPubkey, // Only fetch if a user is logged in
});
// Check if the profile owner follows the logged-in user
const isFollowingUser = useMemo(() => {
if (!userPubkey || followListEvents.length === 0) return false;
const followList = followListEvents[0];
if (!followList) return false;
// Look for a 'p' tag with the logged-in user's pubkey
return followList.tags.some(tag => tag[0] === 'p' && tag[1] === userPubkey);
}, [followListEvents, userPubkey]);
// Get the latest status events by type
const userStatuses = useMemo(() => {
const statuses: StatusMap = {};
@@ -217,9 +238,16 @@ const ProfileInfoCard: React.FC<ProfileInfoCardProps> = React.memo(({ pubkey })
<Card>
<CardHeader>
<div className="flex items-center gap-6">
<Avatar className="h-24 w-24">
<AvatarImage className="object-cover w-full h-full" src={userData?.picture} alt={title} />
</Avatar>
<div className="relative">
<Avatar className={`h-24 w-24 ${userPubkey && userPubkey !== pubkey && isFollowingUser ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' : ''}`}>
<AvatarImage className="object-cover w-full h-full" src={userData?.picture} alt={title} />
</Avatar>
{userPubkey && userPubkey !== pubkey && isFollowingUser && (
<div className="absolute -top-1 -right-1 bg-primary text-primary-foreground rounded-full p-1" title="Follows you">
<UserCheck className="h-4 w-4" aria-label='Follows you' />
</div>
)}
</div>
<div className="flex flex-col gap-1.5">
<Link href={`/profile/${nip19.npubEncode(pubkey)}`}>
<div className="text-2xl">{title}</div>