mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-09 20:33:03 +02:00
fix user metadata not removing pubkeys
small ui improvements
This commit is contained in:
@@ -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
|
||||||
|
@@ -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"
|
||||||
},
|
},
|
||||||
|
@@ -10,7 +10,7 @@ export const Page = ({ children }: {children: React.ReactNode}) => {
|
|||||||
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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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,11 +11,12 @@ 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 (
|
||||||
@@ -24,5 +25,6 @@ export const UserAvatar = React.memo(({ pubkey }: UserAvatarProps) => {
|
|||||||
);
|
);
|
||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
return <Avatar src={url} />;
|
return <Avatar src={url} {...props} />;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
@@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,10 +62,11 @@ 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) {
|
||||||
|
if (!subject.getValue()) {
|
||||||
db.get("user-metadata", pubkey).then((cachedEvent) => {
|
db.get("user-metadata", pubkey).then((cachedEvent) => {
|
||||||
if (cachedEvent) {
|
if (cachedEvent) {
|
||||||
try {
|
try {
|
||||||
@@ -62,12 +76,13 @@ class UserMetadataService {
|
|||||||
}
|
}
|
||||||
} 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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"
|
||||||
|
Reference in New Issue
Block a user