fix user metadata not removing pubkeys

small ui improvements
This commit is contained in:
hzrd149
2023-02-07 17:04:18 -06:00
parent 42c2eeca2f
commit d899250345
15 changed files with 101 additions and 69 deletions

View File

@@ -1,3 +1,12 @@
# TODO # TODO
- Adding loading state to `useUserMetadata` so views can show loading state - Adding loading state to `useUserMetadata` so views can show loading state
- Add a debounce to user metadata services so it dose not spam the relay when updating subscription
- user metadata service: remove author from subscription once metadata is returned
- create a stats page showing state of local db and info about app
- create user timeline service that caching events and supports loading older events on request
## Ideas
- come up with a clever name
- build support for DMs

View File

@@ -23,6 +23,7 @@
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-markdown": "^8.0.4", "react-markdown": "^8.0.4",
"react-router-dom": "^6.5.0", "react-router-dom": "^6.5.0",
"react-singleton-hook": "^4.0.1",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"rxjs": "^7.8.0" "rxjs": "^7.8.0"
}, },

View File

@@ -4,13 +4,13 @@ import { useNavigate } from "react-router-dom";
import { ErrorBoundary } from "./error-boundary"; import { ErrorBoundary } from "./error-boundary";
import { ConnectedRelays } from "./connected-relays"; import { ConnectedRelays } from "./connected-relays";
export const Page = ({ children }: {children: React.ReactNode}) => { export const Page = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<Container size="lg" padding={4}> <Container size="lg" padding={4}>
<HStack alignItems="flex-start" spacing={4} overflow="hidden"> <HStack alignItems="flex-start" spacing={4} overflow="hidden">
<VStack style={{ width: "20rem" }} alignItems="stretch" flexShrink={0}> <VStack style={{ width: "15rem" }} alignItems="stretch" flexShrink={0}>
<Button onClick={() => navigate("/")}>Home</Button> <Button onClick={() => navigate("/")}>Home</Button>
<Button onClick={() => navigate("/global")}>Global</Button> <Button onClick={() => navigate("/global")}>Global</Button>
<Button onClick={() => navigate("/settings")}>Settings</Button> <Button onClick={() => navigate("/settings")}>Settings</Button>
@@ -19,7 +19,7 @@ export const Page = ({ children }: {children: React.ReactNode}) => {
<Box flexGrow={1} overflow="auto"> <Box flexGrow={1} overflow="auto">
<ErrorBoundary>{children}</ErrorBoundary> <ErrorBoundary>{children}</ErrorBoundary>
</Box> </Box>
<VStack style={{ width: "20rem" }} alignItems="stretch" flexShrink={0}> <VStack style={{ width: "15rem" }} alignItems="stretch" flexShrink={0}>
<Button onClick={() => navigate("/")}>Manage Follows</Button> <Button onClick={() => navigate("/")}>Manage Follows</Button>
</VStack> </VStack>
</HStack> </HStack>

View File

@@ -5,6 +5,7 @@ import {
Card, Card,
CardBody, CardBody,
CardHeader, CardHeader,
Code,
Flex, Flex,
Heading, Heading,
HStack, HStack,
@@ -26,7 +27,7 @@ export type PostProps = {
}; };
export const Post = React.memo(({ event }: PostProps) => { export const Post = React.memo(({ event }: PostProps) => {
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const metadata = useUserMetadata(event.pubkey); const { metadata } = useUserMetadata(event.pubkey);
const isLong = event.content.length > 800; const isLong = event.content.length > 800;
const username = metadata const username = metadata
@@ -34,11 +35,11 @@ export const Post = React.memo(({ event }: PostProps) => {
: event.pubkey; : event.pubkey;
return ( return (
<Card> <Card padding="4" variant="outline">
<CardHeader> <CardHeader padding="0">
<HStack spacing="4"> <HStack spacing="4">
<Flex flex="1" gap="4" alignItems="center" flexWrap="wrap"> <Flex flex="1" gap="2" alignItems="center" flexWrap="wrap">
<UserAvatarLink pubkey={event.pubkey} /> <UserAvatarLink pubkey={event.pubkey} size="sm" />
<Box> <Box>
<Heading size="sm"> <Heading size="sm">
@@ -49,7 +50,7 @@ export const Post = React.memo(({ event }: PostProps) => {
</Flex> </Flex>
</HStack> </HStack>
</CardHeader> </CardHeader>
<CardBody pt={0}> <CardBody padding="0" pt={0}>
<VStack alignItems="flex-start" justifyContent="stretch"> <VStack alignItems="flex-start" justifyContent="stretch">
<Box maxHeight="20rem" overflow="hidden" width="100%"> <Box maxHeight="20rem" overflow="hidden" width="100%">
<ReactMarkdown> <ReactMarkdown>
@@ -64,6 +65,7 @@ export const Post = React.memo(({ event }: PostProps) => {
<PostModal event={event} isOpen={isOpen} onClose={onClose} /> <PostModal event={event} isOpen={isOpen} onClose={onClose} />
</> </>
)} )}
<Code>{event.id}</Code>
</VStack> </VStack>
</CardBody> </CardBody>
</Card> </Card>

View File

@@ -1,13 +1,10 @@
import { Tooltip } from "@chakra-ui/react"; import { Tooltip } from "@chakra-ui/react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useUserMetadata } from "../hooks/use-user-metadata"; import { useUserMetadata } from "../hooks/use-user-metadata";
import { UserAvatar } from "./user-avatar"; import { UserAvatar, UserAvatarProps } from "./user-avatar";
export type UserAvatarProps = { export const UserAvatarLink = ({ pubkey, ...props }: UserAvatarProps) => {
pubkey: string; const { metadata } = useUserMetadata(pubkey);
};
export const UserAvatarLink = ({ pubkey }: UserAvatarProps) => {
const metadata = useUserMetadata(pubkey);
let label = "Loading..."; let label = "Loading...";
if (metadata?.display_name && metadata?.name) { if (metadata?.display_name && metadata?.name) {
@@ -21,7 +18,7 @@ export const UserAvatarLink = ({ pubkey }: UserAvatarProps) => {
return ( return (
<Tooltip label={label}> <Tooltip label={label}>
<Link to={`/user/${pubkey}`}> <Link to={`/user/${pubkey}`}>
<UserAvatar pubkey={pubkey} /> <UserAvatar pubkey={pubkey} {...props} />
</Link> </Link>
</Tooltip> </Tooltip>
); );

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { Avatar } from "@chakra-ui/react"; import { Avatar, AvatarProps } from "@chakra-ui/react";
import Identicon from "identicon.js"; import Identicon from "identicon.js";
import { useUserMetadata } from "../hooks/use-user-metadata"; import { useUserMetadata } from "../hooks/use-user-metadata";
@@ -11,18 +11,20 @@ function getIdenticon(pubkey: string) {
return cache[pubkey]; return cache[pubkey];
} }
export type UserAvatarProps = { export type UserAvatarProps = Omit<AvatarProps, "src"> & {
pubkey: string; pubkey: string;
}; };
export const UserAvatar = React.memo(({ pubkey }: UserAvatarProps) => { export const UserAvatar = React.memo(
const metadata = useUserMetadata(pubkey); ({ pubkey, ...props }: UserAvatarProps) => {
const { metadata } = useUserMetadata(pubkey);
const url = useMemo(() => { const url = useMemo(() => {
return ( return (
metadata?.picture ?? metadata?.picture ??
`data:image/svg+xml;base64,${getIdenticon(pubkey).toString()}` `data:image/svg+xml;base64,${getIdenticon(pubkey).toString()}`
); );
}, [metadata]); }, [metadata]);
return <Avatar src={url} />; return <Avatar src={url} {...props} />;
}); }
);

View File

@@ -1,11 +1,16 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useObservable } from "react-use";
import userMetadata from "../services/user-metadata"; import userMetadata from "../services/user-metadata";
import useSubject from "./use-subject";
export function useUserMetadata(pubkey: string) { export function useUserMetadata(pubkey: string) {
const observable = useMemo( const observable = useMemo(
() => userMetadata.requestUserMetadata(pubkey), () => userMetadata.requestUserMetadata(pubkey),
[pubkey] [pubkey]
); );
return useObservable(observable); const metadata = useSubject(observable) ?? undefined;
return {
loading: !metadata,
metadata,
};
} }

View File

@@ -37,14 +37,14 @@ export class Relay {
this.onOpen.next(this); this.onOpen.next(this);
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.info(`Relay ${this.url} opened`); console.info(`Relay: ${this.url} connected`);
} }
}; };
this.ws.onclose = () => { this.ws.onclose = () => {
this.onClose.next(this); this.onClose.next(this);
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.info(`Relay ${this.url} closed`); console.info(`Relay: ${this.url} disconnected`);
} }
}; };
this.ws.onmessage = this.handleMessage.bind(this); this.ws.onmessage = this.handleMessage.bind(this);
@@ -94,7 +94,7 @@ export class Relay {
break; break;
} }
} catch (e) { } catch (e) {
console.log(`Failed to parse event from ${this.url}`); console.log(`Relay: Failed to parse event from ${this.url}`);
console.log(event.data); console.log(event.data);
} }
} }

View File

@@ -65,7 +65,7 @@ export class Subscription {
} }
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.info(`Subscription ${this.name || this.id} opened`); console.info(`Subscription: "${this.name || this.id}" opened`);
} }
} }
close() { close() {
@@ -80,7 +80,7 @@ export class Subscription {
} }
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.info(`Subscription ${this.name || this.id} closed`); console.info(`Subscription: "${this.name || this.id}" closed`);
} }
} }
} }

View File

@@ -4,6 +4,15 @@ import db from "./db";
import settings from "./settings"; import settings from "./settings";
import { Subscription } from "./subscriptions"; import { Subscription } from "./subscriptions";
function debounce<T>(func: T, timeout = 300) {
let timer: number | undefined;
return (...args: any[]) => {
clearTimeout(timer);
// @ts-ignore
timer = setTimeout(() => func(args), timeout);
};
}
class UserMetadataService { class UserMetadataService {
requests = new Set<string>(); requests = new Set<string>();
subjects = new Map<string, BehaviorSubject<Kind0ParsedContent | null>>(); subjects = new Map<string, BehaviorSubject<Kind0ParsedContent | null>>();
@@ -23,6 +32,10 @@ class UserMetadataService {
const metadata = JSON.parse(event.content); const metadata = JSON.parse(event.content);
this.getUserSubject(event.pubkey).next(metadata); this.getUserSubject(event.pubkey).next(metadata);
// remove the pubkey from requests since is have the data
this.requests.delete(event.pubkey);
this.update();
} catch (e) {} } catch (e) {}
}); });
@@ -49,25 +62,27 @@ class UserMetadataService {
const request = () => { const request = () => {
if (!this.requests.has(pubkey)) { if (!this.requests.has(pubkey)) {
this.requests.add(pubkey); this.requests.add(pubkey);
this.updateSubscription(); this.update();
} }
}; };
if (useCache && !subject.getValue()) { if (useCache) {
db.get("user-metadata", pubkey).then((cachedEvent) => { if (!subject.getValue()) {
if (cachedEvent) { db.get("user-metadata", pubkey).then((cachedEvent) => {
try { if (cachedEvent) {
subject.next(JSON.parse(cachedEvent.content)); try {
} catch (e) { subject.next(JSON.parse(cachedEvent.content));
request(); } catch (e) {
} request();
} else request(); }
}); } else request();
});
}
} else request(); } else request();
return subject; return subject;
} }
updateSubscription() { private updateSubscription() {
const pubkeys = Array.from(this.requests.keys()); const pubkeys = Array.from(this.requests.keys());
if (pubkeys.length === 0) { if (pubkeys.length === 0) {
@@ -79,6 +94,7 @@ class UserMetadataService {
} }
} }
} }
update = debounce(this.updateSubscription.bind(this), 500);
pruneRequests() { pruneRequests() {
let removed = false; let removed = false;
@@ -91,7 +107,7 @@ class UserMetadataService {
} }
} }
if (removed) this.updateSubscription(); if (removed) this.update();
} }
} }

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { SkeletonText } from "@chakra-ui/react"; import { Flex, SkeletonText } from "@chakra-ui/react";
import { useSubscription } from "../../hooks/use-subscription"; import { useSubscription } from "../../hooks/use-subscription";
import { Post } from "../../components/post"; import { Post } from "../../components/post";
import moment from "moment/moment"; import moment from "moment/moment";
@@ -41,10 +41,10 @@ export const GlobalView = () => {
if (timeline.length > 20) timeline.length = 20; if (timeline.length > 20) timeline.length = 20;
return ( return (
<> <Flex direction="column" gap="2">
{timeline.map((event) => ( {timeline.map((event) => (
<Post key={event.id} event={event} /> <Post key={event.id} event={event} />
))} ))}
</> </Flex>
); );
}; };

View File

@@ -18,6 +18,7 @@ export const HomeView = () => {
<UserAvatarLink pubkey="e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42" /> <UserAvatarLink pubkey="e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42" />
<UserAvatarLink pubkey="c5cfda98d01f152b3493d995eed4cdb4d9e55a973925f6f9ea24769a5a21e778" /> <UserAvatarLink pubkey="c5cfda98d01f152b3493d995eed4cdb4d9e55a973925f6f9ea24769a5a21e778" />
<UserAvatarLink pubkey="3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" /> <UserAvatarLink pubkey="3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" />
<UserAvatarLink pubkey="04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9" />
<UserAvatarLink pubkey="266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5" /> <UserAvatarLink pubkey="266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5" />
</HStack> </HStack>
</> </>

View File

@@ -1,19 +1,18 @@
import { import {
Box, Flex,
Heading, Heading,
HStack,
SkeletonText, SkeletonText,
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
TabPanels, TabPanels,
Tabs, Tabs,
Text,
VStack, VStack,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { UserPostsTab } from "./posts"; import { UserPostsTab } from "./posts";
import { useUserMetadata } from "../../hooks/use-user-metadata"; import { useUserMetadata } from "../../hooks/use-user-metadata";
import ReactMarkdown from "react-markdown";
import { UserAvatar } from "../../components/user-avatar"; import { UserAvatar } from "../../components/user-avatar";
import { getUserFullName } from "../../helpers/user-metadata"; import { getUserFullName } from "../../helpers/user-metadata";
@@ -24,24 +23,19 @@ export const UserView = () => {
throw new Error("No pubkey"); throw new Error("No pubkey");
} }
const metadata = useUserMetadata(pubkey); const { metadata, loading: loadingMetadata } = useUserMetadata(pubkey);
const label = metadata ? getUserFullName(metadata) || pubkey : pubkey; const label = metadata ? getUserFullName(metadata) || pubkey : pubkey;
return ( return (
<VStack alignItems="stretch" spacing={4}> <VStack alignItems="stretch" spacing={4}>
{" "} {" "}
<HStack spacing={4}> <Flex gap="4">
<UserAvatar pubkey={pubkey} /> <UserAvatar pubkey={pubkey} size="xl" />
<Box> <Flex direction="column" gap="2">
<Heading>{label}</Heading> <Heading>{label}</Heading>
{/* <Text>{metadata?.name}</Text> */} {loadingMetadata ? <SkeletonText /> : <Text>{metadata?.about}</Text>}
</Box> </Flex>
</HStack> </Flex>
{metadata?.about ? (
<ReactMarkdown>{metadata.about}</ReactMarkdown>
) : (
<SkeletonText />
)}
<Tabs> <Tabs>
<TabList> <TabList>
<Tab>Posts</Tab> <Tab>Posts</Tab>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { SkeletonText } from "@chakra-ui/react"; import { Flex, SkeletonText } from "@chakra-ui/react";
import { useSubscription } from "../../hooks/use-subscription"; import { useSubscription } from "../../hooks/use-subscription";
import { Post } from "../../components/post"; import { Post } from "../../components/post";
import { NostrEvent } from "../../types/nostr-event"; import { NostrEvent } from "../../types/nostr-event";
@@ -43,10 +43,10 @@ export const UserPostsTab = ({ pubkey }: { pubkey: string }) => {
if (timeline.length > 20) timeline.length = 20; if (timeline.length > 20) timeline.length = 20;
return ( return (
<> <Flex direction="column" gap="2">
{timeline.map((event) => ( {timeline.map((event) => (
<Post key={event.id} event={event} /> <Post key={event.id} event={event} />
))} ))}
</> </Flex>
); );
}; };

View File

@@ -4756,6 +4756,11 @@ react-router@6.5.0:
dependencies: dependencies:
"@remix-run/router" "1.1.0" "@remix-run/router" "1.1.0"
react-singleton-hook@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/react-singleton-hook/-/react-singleton-hook-4.0.1.tgz#98082b37be06b22845ced85eb11e02bacc3b2580"
integrity sha512-fWuk8VxcZPChrkQasDLM8pgd/7kyi+Cr/5FfCiD99FicjEru+JmtEZNnN4lJ8Z7KbqAST5CYPlpz6lmNsZFGNw==
react-style-singleton@^2.2.1: react-style-singleton@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"