mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
fix user metadata not removing pubkeys
small ui improvements
This commit is contained in:
parent
42c2eeca2f
commit
d899250345
@ -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
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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} />;
|
||||
}
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ export const HomeView = () => {
|
||||
<UserAvatarLink pubkey="e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42" />
|
||||
<UserAvatarLink pubkey="c5cfda98d01f152b3493d995eed4cdb4d9e55a973925f6f9ea24769a5a21e778" />
|
||||
<UserAvatarLink pubkey="3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" />
|
||||
<UserAvatarLink pubkey="04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9" />
|
||||
<UserAvatarLink pubkey="266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5" />
|
||||
</HStack>
|
||||
</>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user