diff --git a/src/components/connected-relays.tsx b/src/components/connected-relays.tsx index 336c219fa..d060e47a3 100644 --- a/src/components/connected-relays.tsx +++ b/src/components/connected-relays.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; -import { Button, Text, useDisclosure } from "@chakra-ui/react"; -import { useInterval } from "react-use"; +import { Button, useDisclosure } from "@chakra-ui/react"; import { Relay } from "../services/relays"; import relayPool from "../services/relays/relay-pool"; import { DevModel } from "./dev-modal"; +import { useInterval } from "react-use"; export const ConnectedRelays = () => { const [relays, setRelays] = useState(relayPool.getRelays()); diff --git a/src/components/icons.tsx b/src/components/icons.tsx index ac433d280..962fe7741 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -54,3 +54,8 @@ export const TrashIcon = createIcon({ displayName: "delete-bin-line", d: "M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v3zm1 2H6v12h12V8zm-9 3h2v6H9v-6zm4 0h2v6h-2v-6zM9 4v2h6V4H9z", }); + +export const AddIcon = createIcon({ + displayName: "delete-bin-line", + d: "M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z", +}); diff --git a/src/hooks/use-user-contacts.ts b/src/hooks/use-user-contacts.ts index 7c7a2d0b4..a57f9ea7a 100644 --- a/src/hooks/use-user-contacts.ts +++ b/src/hooks/use-user-contacts.ts @@ -1,11 +1,9 @@ import { useMemo } from "react"; -import settings from "../services/settings"; import userContactsService from "../services/user-contacts"; import useSubject from "./use-subject"; -export function useUserContacts(pubkey: string) { - const relays = useSubject(settings.relays); - const observable = useMemo(() => userContactsService.requestContacts(pubkey, relays), [pubkey, relays]); +export function useUserContacts(pubkey: string, relays: string[] = [], alwaysRequest = false) { + const observable = useMemo(() => userContactsService.requestContacts(pubkey, relays, alwaysRequest), [pubkey]); const contacts = useSubject(observable) ?? undefined; return { diff --git a/src/hooks/use-user-metadata.ts b/src/hooks/use-user-metadata.ts index 31fd98332..21630b281 100644 --- a/src/hooks/use-user-metadata.ts +++ b/src/hooks/use-user-metadata.ts @@ -1,10 +1,10 @@ import { useMemo } from "react"; -import { useObservable } from "react-use"; import userMetadataService from "../services/user-metadata"; +import useSubject from "./use-subject"; -export function useUserMetadata(pubkey: string, relays?: string[], alwaysRequest = false) { +export function useUserMetadata(pubkey: string, relays: string[] = [], alwaysRequest = false) { const observable = useMemo(() => userMetadataService.requestMetadata(pubkey, relays, alwaysRequest), [pubkey]); - const metadata = useObservable(observable) ?? undefined; + const metadata = useSubject(observable) ?? undefined; return metadata; } diff --git a/src/services/user-contacts.ts b/src/services/user-contacts.ts index 77839b215..2e118676a 100644 --- a/src/services/user-contacts.ts +++ b/src/services/user-contacts.ts @@ -68,7 +68,7 @@ function flushRequests() { function pruneMemoryCache() { const keys = userSubjects.prune(); - for (const [key] of keys) { + for (const key of keys) { pendingRequests.removePubkey(key); } } @@ -96,7 +96,7 @@ setInterval(() => { pruneMemoryCache(); }, 1000); -const userContactsService = { requestContacts, flushRequests }; +const userContactsService = { requestContacts, flushRequests, pendingRequests }; if (import.meta.env.DEV) { // @ts-ignore diff --git a/src/services/user-metadata.ts b/src/services/user-metadata.ts index bbbb7d277..76c093332 100644 --- a/src/services/user-metadata.ts +++ b/src/services/user-metadata.ts @@ -8,19 +8,20 @@ import { Kind0ParsedContent, NostrEvent } from "../types/nostr-event"; import { NostrQuery } from "../types/nostr-query"; import { unique } from "../helpers/array"; +type Metadata = Kind0ParsedContent & { created_at: number }; + const subscription = new NostrSubscription([], undefined, "user-metadata"); -const userMetadataSubjects = new PubkeySubjectCache(); +const userMetadataSubjects = new PubkeySubjectCache(); const pendingRequests = new PubkeyRequestList(); -function requestMetadataEvent( - pubkey: string, - relays: string[], - alwaysRequest = false -): BehaviorSubject { +function requestMetadata(pubkey: string, relays: string[], alwaysRequest = false) { let subject = userMetadataSubjects.getSubject(pubkey); db.get("user-metadata", pubkey).then((cached) => { - if (cached) subject.next(cached); + if (cached) { + const parsed = parseMetadata(cached); + if (parsed) subject.next(parsed); + } if (alwaysRequest || !cached) { pendingRequests.addPubkey(pubkey, relays); @@ -30,17 +31,11 @@ function requestMetadataEvent( return subject; } -function isEvent(e: NostrEvent | null): e is NostrEvent { - return !!e; -} -function parseMetadata(event: NostrEvent): Kind0ParsedContent | undefined { +function parseMetadata(event: NostrEvent): Metadata | undefined { try { - return JSON.parse(event.content); + return { ...JSON.parse(event.content), created_at: event.created_at }; } catch (e) {} } -function requestMetadata(pubkey: string, relays: string[] = [], alwaysRequest = false) { - return requestMetadataEvent(pubkey, relays, alwaysRequest).pipe(filter(isEvent), map(parseMetadata)); -} function flushRequests() { if (!pendingRequests.needsFlush) return; @@ -59,7 +54,7 @@ function flushRequests() { function pruneMemoryCache() { const keys = userMetadataSubjects.prune(); - for (const [key] of keys) { + for (const key of keys) { pendingRequests.removePubkey(key); } } @@ -86,7 +81,7 @@ setInterval(() => { pruneMemoryCache(); }, 1000); -const userMetadataService = { requestMetadata, requestMetadataEvent, flushRequests }; +const userMetadataService = { requestMetadata, flushRequests, pendingRequests }; if (import.meta.env.DEV) { // @ts-ignore diff --git a/src/views/user/following.tsx b/src/views/user/following.tsx index 6a0f0ad2d..4c7456614 100644 --- a/src/views/user/following.tsx +++ b/src/views/user/following.tsx @@ -1,10 +1,31 @@ import { useMemo } from "react"; -import { Flex, SkeletonText, Text } from "@chakra-ui/react"; -import settings from "../../services/settings"; +import moment from "moment"; +import { Box, Flex, Grid, Heading, IconButton, SkeletonText, Text, Link } from "@chakra-ui/react"; +import { Link as ReactRouterLink } from "react-router-dom"; + import useSubject from "../../hooks/use-subject"; import userContactsService from "../../services/user-contacts"; -import { UserAvatarLink } from "../../components/user-avatar-link"; -import moment from "moment"; +import { useUserMetadata } from "../../hooks/use-user-metadata"; +import { getUserDisplayName } from "../../helpers/user-metadata"; +import { AddIcon } from "../../components/icons"; +import { UserAvatar } from "../../components/user-avatar"; +import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19"; + +const UserCard = ({ pubkey }: { pubkey: string }) => { + const metadata = useUserMetadata(pubkey); + + return ( + + + + + {getUserDisplayName(metadata, pubkey)} + + } aria-label="Follow user" title="Follow" ml="auto" /> + + + ); +}; export const UserFollowingTab = ({ pubkey }: { pubkey: string }) => { const observable = useMemo(() => userContactsService.requestContacts(pubkey, [], true), [pubkey]); @@ -14,12 +35,12 @@ export const UserFollowingTab = ({ pubkey }: { pubkey: string }) => { {contacts ? ( <> - + {contacts.contacts.map((contact, i) => ( - + ))} - - {`Updated ${moment(contacts?.created_at).fromNow()}`} + + {`Updated ${moment(contacts?.created_at * 1000).fromNow()}`} ) : (