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
- 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-markdown": "^8.0.4",
"react-router-dom": "^6.5.0",
"react-singleton-hook": "^4.0.1",
"react-use": "^17.4.0",
"rxjs": "^7.8.0"
},

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,16 @@
import { useMemo } from "react";
import { useObservable } from "react-use";
import userMetadata from "../services/user-metadata";
import useSubject from "./use-subject";
export function useUserMetadata(pubkey: string) {
const observable = useMemo(
() => userMetadata.requestUserMetadata(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);
if (import.meta.env.DEV) {
console.info(`Relay ${this.url} opened`);
console.info(`Relay: ${this.url} connected`);
}
};
this.ws.onclose = () => {
this.onClose.next(this);
if (import.meta.env.DEV) {
console.info(`Relay ${this.url} closed`);
console.info(`Relay: ${this.url} disconnected`);
}
};
this.ws.onmessage = this.handleMessage.bind(this);
@ -94,7 +94,7 @@ export class Relay {
break;
}
} 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);
}
}

View File

@ -65,7 +65,7 @@ export class Subscription {
}
if (import.meta.env.DEV) {
console.info(`Subscription ${this.name || this.id} opened`);
console.info(`Subscription: "${this.name || this.id}" opened`);
}
}
close() {
@ -80,7 +80,7 @@ export class Subscription {
}
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 { 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 {
requests = new Set<string>();
subjects = new Map<string, BehaviorSubject<Kind0ParsedContent | null>>();
@ -23,6 +32,10 @@ class UserMetadataService {
const metadata = JSON.parse(event.content);
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) {}
});
@ -49,25 +62,27 @@ class UserMetadataService {
const request = () => {
if (!this.requests.has(pubkey)) {
this.requests.add(pubkey);
this.updateSubscription();
this.update();
}
};
if (useCache && !subject.getValue()) {
db.get("user-metadata", pubkey).then((cachedEvent) => {
if (cachedEvent) {
try {
subject.next(JSON.parse(cachedEvent.content));
} catch (e) {
request();
}
} else request();
});
if (useCache) {
if (!subject.getValue()) {
db.get("user-metadata", pubkey).then((cachedEvent) => {
if (cachedEvent) {
try {
subject.next(JSON.parse(cachedEvent.content));
} catch (e) {
request();
}
} else request();
});
}
} else request();
return subject;
}
updateSubscription() {
private updateSubscription() {
const pubkeys = Array.from(this.requests.keys());
if (pubkeys.length === 0) {
@ -79,6 +94,7 @@ class UserMetadataService {
}
}
}
update = debounce(this.updateSubscription.bind(this), 500);
pruneRequests() {
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 { SkeletonText } from "@chakra-ui/react";
import { Flex, SkeletonText } from "@chakra-ui/react";
import { useSubscription } from "../../hooks/use-subscription";
import { Post } from "../../components/post";
import moment from "moment/moment";
@ -41,10 +41,10 @@ export const GlobalView = () => {
if (timeline.length > 20) timeline.length = 20;
return (
<>
<Flex direction="column" gap="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
))}
</>
</Flex>
);
};

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 { Post } from "../../components/post";
import { NostrEvent } from "../../types/nostr-event";
@ -43,10 +43,10 @@ export const UserPostsTab = ({ pubkey }: { pubkey: string }) => {
if (timeline.length > 20) timeline.length = 20;
return (
<>
<Flex direction="column" gap="2">
{timeline.map((event) => (
<Post key={event.id} event={event} />
))}
</>
</Flex>
);
};

View File

@ -4756,6 +4756,11 @@ react-router@6.5.0:
dependencies:
"@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:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"