diff --git a/02.md b/02.md new file mode 100644 index 0000000..8354bf0 --- /dev/null +++ b/02.md @@ -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>,
, ]`. + +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. diff --git a/components/FollowButton.tsx b/components/FollowButton.tsx index 9fb534e..497eec5 100644 --- a/components/FollowButton.tsx +++ b/components/FollowButton.tsx @@ -1,10 +1,11 @@ - import React, { useEffect, useState } from 'react'; import { Button } from './ui/button'; import { useNostr, useNostrEvents } from 'nostr-react'; -import { finalizeEvent } from 'nostr-tools'; +import { finalizeEvent, NostrEvent } from 'nostr-tools'; import { sign } from 'crypto'; import { SignalMedium } from 'lucide-react'; +import { ReloadIcon } from '@radix-ui/react-icons'; +import { signEvent } from '@/utils/utils'; interface FollowButtonProps { pubkey: string; @@ -12,17 +13,10 @@ interface FollowButtonProps { } const FollowButton: React.FC = ({ pubkey, userPubkey }) => { - // const { publish } = useNostr(); - const [isFollowing, setIsFollowing] = useState(false); + const { publish } = useNostr() - let storedPubkey: string | null = null; - let storedNsec: string | null = null; - let isLoggedIn = false; - if (typeof window !== 'undefined') { - storedPubkey = window.localStorage.getItem('pubkey'); - storedNsec = window.localStorage.getItem('nsec'); - isLoggedIn = storedPubkey !== null; - } + const [isFollowing, setIsFollowing] = useState(false); + const loginType = typeof window !== "undefined" ? window.localStorage.getItem("loginType") : null const { events, isLoading } = useNostrEvents({ filter: { @@ -38,58 +32,60 @@ const FollowButton: React.FC = ({ pubkey, userPubkey }) => { useEffect(() => { - if (followingPubkeys.includes(pubkey)) { - setIsFollowing(true); - } - }, [followingPubkeys, isFollowing, setIsFollowing]); + // Reset the following state when receiving new data + setIsFollowing(followingPubkeys.includes(pubkey)); + }, [followingPubkeys, pubkey]); const handleFollow = async () => { - // if (isLoggedIn) { + if (userPubkey) { + // Get a unique set of pubkeys to follow + let uniqueFollows = new Set(followingPubkeys); + + // Add or remove the target pubkey + if (isFollowing) { + uniqueFollows.delete(pubkey); + } else { + uniqueFollows.add(pubkey); + } + + // Convert to array and create properly formatted p tags + const formattedTags = Array.from(uniqueFollows).map(pk => ["p", pk]); - // let eventTemplate = { - // kind: 3, - // created_at: Math.floor(Date.now() / 1000), - // tags: [followingPubkeys], - // content: '', - // } + let eventTemplate = { + kind: 3, + created_at: Math.floor(Date.now() / 1000), + tags: formattedTags, + content: '', + pubkey: '', // Placeholder + id: '', // Placeholder + sig: '', // Placeholder + } - // console.log(eventTemplate); + console.log('Sending event template:', eventTemplate); - // if (isFollowing) { - // eventTemplate.tags = eventTemplate.tags.filter(tag => tag[1] !== pubkey); - // } else { - // eventTemplate.tags[0].push(pubkey); - // } + let signedEvent = null; + signedEvent = await signEvent(loginType, eventTemplate) as NostrEvent; - // console.log(eventTemplate); - - // let signedEvent = null; - // if (storedNsec != null) { - // // TODO: Sign Nostr Event with nsec - // const nsecArray = storedNsec ? new TextEncoder().encode(storedNsec) : new Uint8Array(); - // signedEvent = finalizeEvent(eventTemplate, nsecArray); - // console.log(signedEvent); - // } else if (storedPubkey != null) { - // // TODO: Request Extension to sign Nostr Event - // console.log('Requesting Extension to sign Nostr Event..'); - // try { - // signedEvent = await window.nostr.signEvent(eventTemplate); - // } catch (error) { - // console.error('Nostr Extension not found or aborted.'); - // } - // } - - // if (signedEvent !== null) { - // console.log(signedEvent); - // publish(signedEvent); - // setIsFollowing(!isFollowing); - // } - // } + if (signedEvent !== null) { + console.log('Publishing signed event:', signedEvent); + publish(signedEvent); + + // Update UI state immediately + setIsFollowing(!isFollowing); + + // Optionally, store this change in a local state if needed + // This could help with rapid UI interactions before the relay responds + } + } }; return ( - ); };