mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 03:18:02 +02:00
add simple list view
This commit is contained in:
parent
02b93843b2
commit
63474a7413
5
.changeset/tender-pumpkins-boil.md
Normal file
5
.changeset/tender-pumpkins-boil.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add delete button for lists
|
@ -1,17 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
MenuItem,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { Bech32Prefix, getSharableNoteId, normalizeToBech32 } from "../../helpers/nip19";
|
||||
@ -22,47 +9,19 @@ import { ClipboardIcon, CodeIcon, ExternalLinkIcon, LikeIcon, RepostIcon, TrashI
|
||||
import NoteReactionsModal from "./note-zaps-modal";
|
||||
import NoteDebugModal from "../debug-modals/note-debug-modal";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useCallback, useState } from "react";
|
||||
import QuoteNote from "./quote-note";
|
||||
import { buildDeleteEvent } from "../../helpers/nostr-event";
|
||||
import signingService from "../../services/signing";
|
||||
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr-apps";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
|
||||
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) => {
|
||||
const account = useCurrentAccount();
|
||||
const toast = useToast();
|
||||
const infoModal = useDisclosure();
|
||||
const reactionsModal = useDisclosure();
|
||||
const deleteModal = useDisclosure();
|
||||
const [reason, setReason] = useState("");
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
|
||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
|
||||
|
||||
const deleteNote = useCallback(async () => {
|
||||
try {
|
||||
if (!account) throw new Error("not logged in");
|
||||
setDeleting(true);
|
||||
const deleteEvent = buildDeleteEvent([event.id], reason);
|
||||
const signed = await signingService.requestSignature(deleteEvent, account);
|
||||
const results = nostrPostAction(clientRelaysService.getWriteUrls(), signed);
|
||||
await results.onComplete;
|
||||
deleteModal.onClose();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
}, [event]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuIconButton {...props}>
|
||||
@ -84,7 +43,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
||||
</MenuItem>
|
||||
)}
|
||||
{account?.pubkey === event.pubkey && (
|
||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={deleteModal.onOpen}>
|
||||
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(event)}>
|
||||
Delete Note
|
||||
</MenuItem>
|
||||
)}
|
||||
@ -100,37 +59,6 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
||||
{reactionsModal.isOpen && (
|
||||
<NoteReactionsModal noteId={event.id} isOpen={reactionsModal.isOpen} onClose={reactionsModal.onClose} />
|
||||
)}
|
||||
|
||||
{deleteModal.isOpen && (
|
||||
<Modal isOpen={deleteModal.isOpen} onClose={deleteModal.onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader px="4" py="2">
|
||||
Delete Note?
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody px="4" py="0">
|
||||
<QuoteNote noteId={event.id} />
|
||||
<Input
|
||||
name="reason"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
placeholder="Reason (optional)"
|
||||
mt="2"
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter px="4" py="4">
|
||||
<Button variant="ghost" size="sm" mr={2} onClick={deleteModal.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="red" variant="solid" onClick={deleteNote} size="sm" isLoading={deleting}>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -13,20 +13,20 @@ import {
|
||||
PopoverFooter,
|
||||
} from "@chakra-ui/react";
|
||||
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||
import { getEventRelays, handleEventFromRelay } from "../../services/event-relays";
|
||||
import { handleEventFromRelay } from "../../services/event-relays";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { RelayIcon, SearchIcon } from "../icons";
|
||||
import { RelayIcon } from "../icons";
|
||||
import { RelayFavicon } from "../relay-favicon";
|
||||
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||
import relayPoolService from "../../services/relay-pool";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import useEventRelays from "../../hooks/use-event-relays";
|
||||
|
||||
export type NoteRelaysProps = Omit<IconButtonProps, "icon" | "aria-label"> & {
|
||||
event: NostrEvent;
|
||||
};
|
||||
|
||||
export const NoteRelays = memo(({ event, ...props }: NoteRelaysProps) => {
|
||||
const eventRelays = useSubject(getEventRelays(event.id));
|
||||
const eventRelays = useEventRelays();
|
||||
const writeRelays = useWriteRelayUrls();
|
||||
|
||||
const [broadcasting, setBroadcasting] = useState(false);
|
||||
|
@ -43,7 +43,6 @@ export const UserFollowButton = ({
|
||||
}: { pubkey: string } & Omit<ButtonProps, "onClick" | "isLoading" | "isDisabled">) => {
|
||||
const account = useCurrentAccount();
|
||||
const following = useSubject(clientFollowingService.following) ?? [];
|
||||
const savingDraft = useSubject(clientFollowingService.savingDraft);
|
||||
|
||||
const readRelays = useReadRelayUrls(useAdditionalRelayContext());
|
||||
const userContacts = useUserContacts(pubkey, readRelays);
|
||||
@ -51,8 +50,7 @@ export const UserFollowButton = ({
|
||||
const isFollowing = following.some((t) => t[1] === pubkey);
|
||||
const isFollowingMe = account && userContacts?.contacts.includes(account.pubkey);
|
||||
|
||||
const userContacts = useUserContacts(pubkey, clientRelaysService.getReadUrls());
|
||||
const followLabel = account && userContacts?.contacts.includes(account.pubkey) ? "Follow Back" : "Follow";
|
||||
const followLabel = account && isFollowingMe ? "Follow Back" : "Follow";
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
|
8
src/hooks/use-event-relays.ts
Normal file
8
src/hooks/use-event-relays.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { useMemo } from "react";
|
||||
import { getEventRelays } from "../services/event-relays";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export default function useEventRelays(eventId?: string) {
|
||||
const sub = useMemo(() => (eventId ? getEventRelays(eventId) : undefined), [eventId]);
|
||||
return useSubject(sub) ?? [];
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import moment from "moment";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
import { Providers } from "./providers";
|
||||
@ -26,7 +25,3 @@ root.render(
|
||||
<App />
|
||||
</Providers>
|
||||
);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
window.moment = moment;
|
||||
}
|
||||
|
176
src/providers/delete-event-provider.tsx
Normal file
176
src/providers/delete-event-provider.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
Code,
|
||||
Flex,
|
||||
Input,
|
||||
Link,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Text,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { Event, Kind, nip19 } from "nostr-tools";
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { buildDeleteEvent } from "../helpers/nostr-event";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import signingService from "../services/signing";
|
||||
import { nostrPostAction } from "../classes/nostr-post-action";
|
||||
import QuoteNote from "../components/note/quote-note";
|
||||
import createDefer, { Deferred } from "../classes/deferred";
|
||||
import useEventRelays from "../hooks/use-event-relays";
|
||||
import { useWriteRelayUrls } from "../hooks/use-client-relays";
|
||||
import { RelayFavicon } from "../components/relay-favicon";
|
||||
import { ExternalLinkIcon } from "../components/icons";
|
||||
|
||||
type DeleteEventContextType = {
|
||||
isLoading: boolean;
|
||||
deleteEvent: (event: Event) => Promise<void>;
|
||||
};
|
||||
|
||||
const DeleteEventContext = createContext<DeleteEventContextType>({
|
||||
isLoading: false,
|
||||
deleteEvent: () => Promise.reject(),
|
||||
});
|
||||
|
||||
export function useDeleteEventContext() {
|
||||
return useContext(DeleteEventContext);
|
||||
}
|
||||
|
||||
function EventPreview({ event }: { event: Event }) {
|
||||
if (event.kind === Kind.Text) {
|
||||
return <QuoteNote noteId={event.id} />;
|
||||
}
|
||||
return <Code>{nip19.noteEncode(event.id)}</Code>;
|
||||
}
|
||||
|
||||
export default function DeleteEventProvider({ children }: PropsWithChildren) {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount();
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [event, setEvent] = useState<Event>();
|
||||
const [defer, setDefer] = useState<Deferred<void>>();
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
const eventRelays = useEventRelays(event?.id);
|
||||
const writeRelays = useWriteRelayUrls(eventRelays);
|
||||
|
||||
const deleteEvent = useCallback((event: Event) => {
|
||||
setEvent(event);
|
||||
const defer = createDefer<void>();
|
||||
setDefer(defer);
|
||||
return defer;
|
||||
}, []);
|
||||
const onClose = useCallback(() => setEvent(undefined), []);
|
||||
|
||||
const confirm = useCallback(async () => {
|
||||
try {
|
||||
if (!event) throw new Error("no event");
|
||||
if (!account) throw new Error("not logged in");
|
||||
setLoading(true);
|
||||
const deleteEvent = buildDeleteEvent([event.id], reason);
|
||||
const signed = await signingService.requestSignature(deleteEvent, account);
|
||||
|
||||
const results = nostrPostAction(writeRelays, signed);
|
||||
await results.onComplete;
|
||||
defer?.resolve();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
toast({
|
||||
status: "error",
|
||||
description: e.message,
|
||||
});
|
||||
}
|
||||
defer?.reject();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setReason("");
|
||||
setEvent(undefined);
|
||||
setDefer(undefined);
|
||||
}
|
||||
}, [defer, event, account]);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
isLoading,
|
||||
deleteEvent,
|
||||
}),
|
||||
[isLoading, deleteEvent]
|
||||
);
|
||||
|
||||
return (
|
||||
<DeleteEventContext.Provider value={context}>
|
||||
{children}
|
||||
{event && (
|
||||
<Modal isOpen={true} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader px="4" py="2">
|
||||
Delete Note?
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody px="4" py="0">
|
||||
<EventPreview event={event} />
|
||||
<Input
|
||||
name="reason"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
placeholder="Reason (optional)"
|
||||
mt="2"
|
||||
/>
|
||||
|
||||
<Accordion allowToggle my="2">
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
Deleting from relays
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
<Flex wrap="wrap" gap="2" py="2">
|
||||
{writeRelays.map((url) => (
|
||||
<Box alignItems="center" key={url} px="2" borderRadius="lg" display="flex" borderWidth="1px">
|
||||
<RelayFavicon relay={url} size="2xs" mr="2" />
|
||||
<Text isTruncated>{url}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter px="4" pb="4" pt="0">
|
||||
<Button
|
||||
as={Link}
|
||||
leftIcon={<ExternalLinkIcon />}
|
||||
isExternal
|
||||
href="https://nostr-delete.vercel.app/"
|
||||
variant="link"
|
||||
mr="auto"
|
||||
size="sm"
|
||||
>
|
||||
Nostr Event Deletion
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" mr={2} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="red" variant="solid" onClick={confirm} size="sm" isLoading={isLoading}>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</DeleteEventContext.Provider>
|
||||
);
|
||||
}
|
@ -3,6 +3,8 @@ import { ChakraProvider, localStorageManager } from "@chakra-ui/react";
|
||||
import { SigningProvider } from "./signing-provider";
|
||||
import createTheme from "../theme";
|
||||
import useAppSettings from "../hooks/use-app-settings";
|
||||
import DeleteEventProvider from "./delete-event-provider";
|
||||
import { ErrorBoundary } from "../components/error-boundary";
|
||||
|
||||
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
const { primaryColor } = useAppSettings();
|
||||
@ -10,7 +12,11 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
return (
|
||||
<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
|
||||
<SigningProvider>{children}</SigningProvider>
|
||||
<SigningProvider>
|
||||
<ErrorBoundary>
|
||||
<DeleteEventProvider>{children}</DeleteEventProvider>
|
||||
</ErrorBoundary>
|
||||
</SigningProvider>
|
||||
</ChakraProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import moment from "moment";
|
||||
import dayjs from "dayjs";
|
||||
import { NostrRequest } from "../classes/nostr-request";
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import { DraftNostrEvent, NostrEvent, isPTag } from "../types/nostr-event";
|
||||
@ -53,7 +53,7 @@ export class List {
|
||||
if (this.event.tags.some((t) => t[0] === "p" && t[1] === pubkey)) throw new Error("person already in list");
|
||||
|
||||
const draft: DraftNostrEvent = {
|
||||
created_at: moment().unix(),
|
||||
created_at: dayjs().unix(),
|
||||
kind: this.event.kind,
|
||||
content: this.event.content,
|
||||
tags: [...this.event.tags, relay ? ["p", pubkey, relay] : ["p", pubkey]],
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
@ -38,7 +37,7 @@ export default function HashTagView() {
|
||||
`${hashtag}-hashtag`,
|
||||
selectedRelay ? [selectedRelay] : defaultRelays,
|
||||
{ kinds: [1], "#t": [hashtag] },
|
||||
{ pageSize: moment.duration(5, "minutes").asSeconds() }
|
||||
{ pageSize: 60 * 10 }
|
||||
);
|
||||
|
||||
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Button, Flex, FormControl, FormLabel, Select, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Note } from "../../components/note";
|
||||
import { unique } from "../../helpers/array";
|
||||
@ -26,7 +25,7 @@ export default function GlobalTab() {
|
||||
`global`,
|
||||
selectedRelay ? [selectedRelay] : [],
|
||||
{ kinds: [1] },
|
||||
{ pageSize: moment.duration(5, "minutes").asSeconds() }
|
||||
{ pageSize: 60 * 10 }
|
||||
);
|
||||
|
||||
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
|
||||
|
@ -9,7 +9,7 @@ function UsersLists() {
|
||||
const account = useCurrentAccount()!;
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const lists = useUserLists(account.pubkey, readRelays);
|
||||
const lists = useUserLists(account.pubkey, readRelays, true);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Link as RouterList, useParams } from "react-router-dom";
|
||||
import { Link as RouterList, useNavigate, useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useUserLists from "../../hooks/use-user-lists";
|
||||
@ -9,6 +9,7 @@ import { UserCard } from "../user/components/user-card";
|
||||
import { ArrowLeftSIcon, ExternalLinkIcon } from "../../components/icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr-apps";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
|
||||
function useListPointer() {
|
||||
const { addr } = useParams() as { addr: string };
|
||||
@ -26,6 +27,8 @@ function useListPointer() {
|
||||
export default function ListView() {
|
||||
const pointer = useListPointer();
|
||||
const account = useCurrentAccount();
|
||||
const navigate = useNavigate();
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
|
||||
const readRelays = useReadRelayUrls(pointer.relays);
|
||||
const lists = useUserLists(pointer.pubkey, readRelays, true);
|
||||
@ -53,7 +56,11 @@ export default function ListView() {
|
||||
{list.name}
|
||||
</Heading>
|
||||
|
||||
{isAuthor && <Button colorScheme="red">Delete</Button>}
|
||||
{isAuthor && (
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(list.event).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button as={Link} href={buildAppSelectUrl(list.getAddress())} target="_blank" leftIcon={<ExternalLinkIcon />}>
|
||||
Open in app
|
||||
</Button>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Avatar, Button, Flex, Heading, Image, Link } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { ToolsIcon } from "../../components/icons";
|
||||
import { ExternalLinkIcon, ToolsIcon } from "../../components/icons";
|
||||
|
||||
export default function ToolsHomeView() {
|
||||
return (
|
||||
@ -18,6 +18,15 @@ export default function ToolsHomeView() {
|
||||
>
|
||||
nostr army knife
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://nostr-delete.vercel.app/"
|
||||
isExternal
|
||||
target="_blank"
|
||||
leftIcon={<ExternalLinkIcon />}
|
||||
>
|
||||
Nostr Event Deletion
|
||||
</Button>
|
||||
<Button as={RouterLink} to="./nip19">
|
||||
Nip-19 encode/decode
|
||||
</Button>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Button, Flex, Spinner, Text } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { NoteLink } from "../../components/note-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
@ -46,7 +45,7 @@ export default function UserReportsTab() {
|
||||
`${truncatedId(pubkey)}-reports`,
|
||||
contextRelays,
|
||||
{ authors: [pubkey], kinds: [1984] },
|
||||
{ pageSize: moment.duration(1, "week").asSeconds() }
|
||||
{ pageSize: 60 * 60 * 24 * 7 }
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -4934,11 +4934,6 @@ mixme@^0.5.1:
|
||||
resolved "https://registry.yarnpkg.com/mixme/-/mixme-0.5.9.tgz#a5a58e17354632179ff3ce5b0fc130899c8ba81c"
|
||||
integrity sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==
|
||||
|
||||
moment@^2.29.4:
|
||||
version "2.29.4"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
|
Loading…
x
Reference in New Issue
Block a user