add e2e tests

This commit is contained in:
hzrd149 2023-06-04 11:58:57 -04:00
parent cc912db05a
commit 868227a4cc
23 changed files with 1405 additions and 142 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add e2e tests

12
cypress.config.ts Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from "cypress";
export default defineConfig({
viewportWidth: 1200,
viewportHeight: 800,
e2e: {
baseUrl: "http://localhost:5173",
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

3
cypress/e2e/embeds.cy.ts Normal file
View File

@ -0,0 +1,3 @@
describe("Embeds", () => {
it("embeds a video element", () => {});
});

50
cypress/e2e/login.cy.ts Normal file
View File

@ -0,0 +1,50 @@
describe("Login view", () => {
beforeEach(() => {
cy.visit("/login");
cy.window().then(($win) => {
cy.stub($win, "prompt").returns("pass");
});
});
it("login with nip05", () => {
cy.intercept("get", "https://hzrd149.com/.well-known/nostr.json?name=_", {
names: {
_: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
},
relays: {
"266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5": ["wss://nostrue.com"],
},
});
cy.findByRole("button", { name: /nip-05/i }).click();
cy.findByRole("textbox", { name: /nip-05/i }).type("_@hzrd149.com");
cy.contains(/found 1 relays/i);
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});
it("login with npub", () => {
cy.findByRole("button", { name: /npub/i }).click();
cy.findByRole("textbox", { name: /npub/i }).type("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr");
cy.findByRole("combobox", { name: /bootstrap relay/i })
.clear()
.type("wss://nostrue.com");
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});
it("login with new nsec", () => {
cy.findByRole("button", { name: /nsec/i }).click();
cy.findByRole("button", { name: /generate/i }).click();
cy.findByRole("combobox", { name: /bootstrap relay/i })
.clear()
.type("wss://nostrue.com");
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});
});

65
cypress/e2e/search.cy.ts Normal file
View File

@ -0,0 +1,65 @@
describe("Search", () => {
beforeEach(() => {
cy.loginWithNewUser();
});
describe("Events", () => {
const links: [string, RegExp][] = [
[
"nostr:nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpp4mhxue69uhkummn9ekx7mqpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet59dl66z",
/Nostr zaps - a guide/i,
],
// [
// "nostr:nevent1qqsymds0vlpp4f5s0dckjf4qz283pdsen0rmx8lu7ct6hpnxag2hpacpremhxue69uhkummnw3ez6un9d3shjtnwda4k7arpwfhjucm0d5q3qamnwvaz7tmwdaehgu3wwa5kueghxyq76",
// /testing/i,
// ],
];
for (const [link, regexp] of links) {
it(`should handle ${link}`, () => {
cy.visit("/search");
cy.findByRole("searchbox").type(link, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
for (const [link, regexp] of links) {
const withoutPrefix = link.replace("nostr:", "");
it(`should handle ${withoutPrefix}`, () => {
cy.visit("/search");
cy.findByRole("searchbox").type(link, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
});
describe("Profiles", () => {
const profiles: [string, RegExp][] = [
[
"nostr:nprofile1qqsp2alytxwazryxxjv0u0pqhkp247hc9xjetn5rch8c4s6xx5cmpxcpzpmhxue69uhkummnw3ezuamfdejsz9nhwden5te0v96xcctn9ehx7um5wghxcctwvs6ymk33",
/npub1z4m7g\.\.\.kzdsxana6p/i,
],
];
for (const [search, regexp] of profiles) {
it(`should handle ${search}`, () => {
cy.visit("/search");
cy.findByRole("searchbox").type(search, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
for (const [search, regexp] of profiles) {
const withoutPrefix = search.replace("nostr:", "");
it(`should handle ${withoutPrefix}`, () => {
cy.visit("/search");
cy.findByRole("searchbox").type(search, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
});
});

View File

@ -0,0 +1,53 @@
/// <reference types="cypress" />
import "@testing-library/cypress/add-commands";
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
declare global {
namespace Cypress {
interface Chainable {
loginWithNewUser(): Chainable<void>;
}
}
}
Cypress.Commands.add("loginWithNewUser", () => {
cy.visit("/login");
cy.window().then(($win) => {
cy.stub($win, "prompt").returns("pass");
});
cy.findByRole("button", { name: /nsec/i }).click();
cy.findByRole("button", { name: /generate/i }).click();
cy.findByRole("combobox", { name: /bootstrap relay/i })
.clear()
.type("wss://nostrue.com", { delay: 0 });
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});

31
cypress/support/e2e.ts Normal file
View File

@ -0,0 +1,31 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
beforeEach(async () => {
cy.clearAllLocalStorage();
// remove the database for every test
await new Promise((res, rej) => {
const request = window.indexedDB.deleteDatabase("storage");
request.onsuccess = res;
request.onerror = rej;
});
});

8
cypress/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../tsconfig.json",
"include": ["../next-env.d.ts", "../cypress.config.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"types": ["cypress", "@testing-library/cypress", "next"]
}
}

View File

@ -6,7 +6,8 @@
"scripts": {
"start": "vite serve",
"build": "tsc --project tsconfig.json && vite build",
"format": "prettier --ignore-path .prettierignore -w ."
"format": "prettier --ignore-path .prettierignore -w .",
"e2e": "cypress open"
},
"dependencies": {
"@chakra-ui/icons": "^2.0.19",
@ -32,6 +33,8 @@
"webln": "^0.3.2"
},
"devDependencies": {
"@testing-library/cypress": "^9.0.0",
"cypress": "^12.13.0",
"@changesets/cli": "^2.26.1",
"@types/identicon.js": "^2.3.1",
"@types/react": "^18.2.7",

View File

@ -22,7 +22,7 @@ const EmbeddedImage = ({ src, blue }: { src: string; blue: boolean }) => {
return (
<ImageGalleryLink href={src} target="_blank" display="block" mx="-2">
<ImageComponent src={thumbnail} cursor="pointer" maxH={isMobile ? "80vh" : "25vh"} mx={isMobile ? "auto" : "0"} />
<ImageComponent src={thumbnail} cursor="pointer" maxH={isMobile ? "80vh" : "35vh"} mx={isMobile ? "auto" : "0"} />
</ImageGalleryLink>
);
};

View File

@ -1,13 +1,5 @@
import { createIcon, IconProps } from "@chakra-ui/icons";
import nostrGuruIcon from "./icons/nostr-guru.jpg";
import snortSocialIcon from "./icons/snort-social.png";
export const IMAGE_ICONS = {
nostrGuruIcon,
snortSocialIcon,
};
const defaultProps: IconProps = { fontSize: "1.2em" };
export const GlobalIcon = createIcon({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

View File

@ -1,27 +1,16 @@
import { useMemo } from "react";
import { Link, LinkProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { truncatedId } from "../helpers/nostr-event";
import { nip19 } from "nostr-tools";
import { getEventRelays } from "../services/event-relays";
import relayScoreboardService from "../services/relay-scoreboard";
import { useMemo } from "react";
export function getSharableEncodedNoteId(eventId: string) {
const relays = getEventRelays(eventId).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const onlyTwo = ranked.slice(0, 2);
if (onlyTwo.length > 0) {
return nip19.neventEncode({ id: eventId, relays: onlyTwo });
} else return nip19.noteEncode(eventId);
}
import { getSharableNoteId } from "../helpers/nip19";
export type NoteLinkProps = LinkProps & {
noteId: string;
};
export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) => {
const encoded = useMemo(() => getSharableEncodedNoteId(noteId), [noteId]);
const encoded = useMemo(() => getSharableNoteId(noteId), [noteId]);
return (
<Link as={RouterLink} to={`/n/${encoded}`} color={color} {...props}>

View File

@ -14,7 +14,7 @@ import {
} from "@chakra-ui/react";
import { useCopyToClipboard } from "react-use";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { Bech32Prefix, getSharableNoteId, normalizeToBech32 } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
@ -28,7 +28,6 @@ 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 { getSharableEncodedNoteId } from "../note-link";
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) => {
const account = useCurrentAccount();
@ -68,7 +67,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Zaps/Reactions
</MenuItem>
<MenuItem onClick={() => copyToClipboard("nostr:" + getSharableEncodedNoteId(event.id))} icon={<RepostIcon />}>
<MenuItem onClick={() => copyToClipboard("nostr:" + getSharableNoteId(event.id))} icon={<RepostIcon />}>
Copy Share Link
</MenuItem>
{noteId && (

View File

@ -1,5 +1,7 @@
import { bech32 } from "bech32";
import { nip19 } from "nostr-tools";
import { getEventRelays } from "../services/event-relays";
import relayScoreboardService from "../services/relay-scoreboard";
export function isHex(key?: string) {
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
@ -77,3 +79,13 @@ export function normalizeToHex(hex: string) {
if (isBech32Key(hex)) return bech32ToHex(hex);
return null;
}
export function getSharableNoteId(eventId: string) {
const relays = getEventRelays(eventId).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const onlyTwo = ranked.slice(0, 2);
if (onlyTwo.length > 0) {
return nip19.neventEncode({ id: eventId, relays: onlyTwo });
} else return nip19.noteEncode(eventId);
}

View File

@ -0,0 +1,17 @@
import { useMemo } from "react";
import useFallbackUserRelays from "./use-fallback-user-relays";
import relayScoreboardService from "../services/relay-scoreboard";
import { RelayMode } from "../classes/relay";
import { nip19 } from "nostr-tools";
export function useSharableProfileId(pubkey: string) {
const userRelays = useFallbackUserRelays(pubkey);
return useMemo(() => {
const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
const ranked = relayScoreboardService.getRankedRelays(writeUrls);
const onlyTwo = ranked.slice(0, 2);
return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey);
}, [userRelays]);
}

View File

@ -46,6 +46,9 @@ export default function LoginNip05View() {
const id = await dnsIdentityService.getIdentity(nip05, true);
setPubkey(id?.pubkey);
setRelays(id?.relays);
if (id?.relays[0]) {
setRelayUrl(id.relays[0]);
}
} catch (e) {}
}
setLoading(false);

View File

@ -32,7 +32,13 @@ function useUserShareLink(pubkey: string) {
}, [userRelays]);
}
export default function Header({ pubkey }: { pubkey: string }) {
export default function Header({
pubkey,
showRelaySelectionModal,
}: {
pubkey: string;
showRelaySelectionModal: () => void;
}) {
const isMobile = useIsMobile();
const navigate = useNavigate();
const metadata = useUserMetadata(pubkey);
@ -41,8 +47,6 @@ export default function Header({ pubkey }: { pubkey: string }) {
const account = useCurrentAccount();
const isSelf = pubkey === account.pubkey;
const shareLink = useUserShareLink(pubkey);
return (
<Flex direction="column" gap="2" px="2" pt="2">
<Flex gap="4">
@ -55,7 +59,13 @@ export default function Header({ pubkey }: { pubkey: string }) {
</Flex>
<Flex gap="2">
<UserTipButton pubkey={pubkey} size="sm" variant="link" />
<UserProfileMenu pubkey={pubkey} aria-label="More Options" size="sm" variant="link" />
<UserProfileMenu
pubkey={pubkey}
aria-label="More Options"
size="sm"
variant="link"
showRelaySelectionModal={showRelaySelectionModal}
/>
</Flex>
</Flex>
{!metadata ? <SkeletonText /> : <Text>{metadata?.about}</Text>}

View File

@ -1,33 +1,27 @@
import {
Avatar,
Code,
Flex,
Heading,
MenuItem,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalOverlay,
useDisclosure,
} from "@chakra-ui/react";
import { MenuItem, useDisclosure } from "@chakra-ui/react";
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
import { CodeIcon, IMAGE_ICONS, SpyIcon } from "../../../components/icons";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
import { ClipboardIcon, CodeIcon, RelayIcon, SpyIcon } from "../../../components/icons";
import accountService from "../../../services/account";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { useUserRelays } from "../../../hooks/use-user-relays";
import { RelayMode } from "../../../classes/relay";
import { CopyIconButton } from "../../../components/copy-icon-button";
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
import { useCopyToClipboard } from "react-use";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<MenuIconButtonProps, "children">) => {
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
export const UserProfileMenu = ({
pubkey,
showRelaySelectionModal,
...props
}: { pubkey: string; showRelaySelectionModal: () => void } & Omit<MenuIconButtonProps, "children">) => {
const metadata = useUserMetadata(pubkey);
const userRelays = useUserRelays(pubkey);
const infoModal = useDisclosure();
const sharableId = useSharableProfileId(pubkey);
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
const loginAsUser = () => {
const readRelays = userRelays?.relays.filter((r) => r.mode === RelayMode.READ).map((r) => r.url) ?? [];
@ -47,24 +41,14 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
<MenuItem icon={<SpyIcon fontSize="1.5em" />} onClick={() => loginAsUser()}>
Login as {getUserDisplayName(metadata, pubkey)}
</MenuItem>
<MenuItem onClick={() => copyToClipboard("nostr:" + sharableId)} icon={<ClipboardIcon />}>
Copy share link
</MenuItem>
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
View Raw
</MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
href={`https://www.nostr.guru/p/${pubkey}`}
target="_blank"
>
Open in Nostr.guru
</MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.snortSocialIcon} size="xs" />}
href={`https://snort.social/p/${npub}`}
target="_blank"
>
Open in snort.social
<MenuItem icon={<RelayIcon />} onClick={showRelaySelectionModal}>
Relay selection
</MenuItem>
</MenuIconButton>
{infoModal.isOpen && (

View File

@ -1,4 +1,29 @@
import { Flex, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import {
Flex,
FormControl,
FormHelperText,
FormLabel,
List,
ListItem,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Spinner,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
useDisclosure,
} from "@chakra-ui/react";
import { Outlet, useMatches, useNavigate, useParams } from "react-router-dom";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../helpers/user-metadata";
@ -6,7 +31,7 @@ import { useIsMobile } from "../../hooks/use-is-mobile";
import { Bech32Prefix, isHex, normalizeToBech32 } from "../../helpers/nip19";
import { useAppTitle } from "../../hooks/use-app-title";
import Header from "./components/header";
import { Suspense } from "react";
import { Suspense, useState } from "react";
import useFallbackUserRelays from "../../hooks/use-fallback-user-relays";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
@ -14,6 +39,7 @@ import { RelayMode } from "../../classes/relay";
import { AdditionalRelayProvider } from "../../providers/additional-relay-context";
import { nip19 } from "nostr-tools";
import { unique } from "../../helpers/array";
import { RelayFavicon } from "../../components/relay-favicon";
const tabs = [
{ label: "Notes", path: "notes" },
@ -57,7 +83,9 @@ const UserView = () => {
const { pubkey, relays: pointerRelays } = useUserPointer();
const isMobile = useIsMobile();
const navigate = useNavigate();
const userTopRelays = useUserTopRelays(pubkey);
const [relayCount, setRelayCount] = useState(4);
const userTopRelays = useUserTopRelays(pubkey, relayCount);
const relayModal = useDisclosure();
const matches = useMatches();
const lastMatch = matches[matches.length - 1];
@ -70,38 +98,70 @@ const UserView = () => {
useAppTitle(getUserDisplayName(metadata, npub ?? pubkey));
return (
<AdditionalRelayProvider relays={unique([...userTopRelays, ...pointerRelays])}>
<Flex direction="column" alignItems="stretch" gap="2" overflow={isMobile ? "auto" : "hidden"} height="100%">
{/* {metadata?.banner && <Image src={metadata.banner} mb={-120} />} */}
<Header pubkey={pubkey} />
<Tabs
display="flex"
flexDirection="column"
flexGrow="1"
overflow={isMobile ? undefined : "hidden"}
isLazy
index={activeTab}
onChange={(v) => navigate(tabs[v].path)}
colorScheme="brand"
>
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
{tabs.map(({ label }) => (
<Tab key={label}>{label}</Tab>
))}
</TabList>
<>
<AdditionalRelayProvider relays={unique([...userTopRelays, ...pointerRelays])}>
<Flex direction="column" alignItems="stretch" gap="2" overflow={isMobile ? "auto" : "hidden"} height="100%">
{/* {metadata?.banner && <Image src={metadata.banner} mb={-120} />} */}
<Header pubkey={pubkey} showRelaySelectionModal={relayModal.onOpen} />
<Tabs
display="flex"
flexDirection="column"
flexGrow="1"
overflow={isMobile ? undefined : "hidden"}
isLazy
index={activeTab}
onChange={(v) => navigate(tabs[v].path)}
colorScheme="brand"
>
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
{tabs.map(({ label }) => (
<Tab key={label}>{label}</Tab>
))}
</TabList>
<TabPanels overflow={isMobile ? undefined : "auto"} height="100%">
{tabs.map(({ label }) => (
<TabPanel key={label} pr={0} pl={0}>
<Suspense fallback={<Spinner />}>
<Outlet context={{ pubkey }} />
</Suspense>
</TabPanel>
))}
</TabPanels>
</Tabs>
</Flex>
</AdditionalRelayProvider>
<TabPanels overflow={isMobile ? undefined : "auto"} height="100%">
{tabs.map(({ label }) => (
<TabPanel key={label} pr={0} pl={0}>
<Suspense fallback={<Spinner />}>
<Outlet context={{ pubkey, setRelayCount }} />
</Suspense>
</TabPanel>
))}
</TabPanels>
</Tabs>
</Flex>
</AdditionalRelayProvider>
<Modal isOpen={relayModal.isOpen} onClose={relayModal.onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader pb="1">Relay selection</ModalHeader>
<ModalCloseButton />
<ModalBody>
<List spacing="2">
{userTopRelays.map((url) => (
<ListItem key={url}>
<RelayFavicon relay={url} size="xs" mr="2" />
{url}
</ListItem>
))}
</List>
<FormControl>
<FormLabel>Max relays</FormLabel>
<NumberInput min={0} step={1} value={relayCount} onChange={(v) => setRelayCount(parseInt(v) || 0)}>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<FormHelperText>set to 0 to connect to all relays</FormHelperText>
</FormControl>
</ModalBody>
</ModalContent>
</Modal>
</>
);
};

View File

@ -4,21 +4,12 @@ import {
Flex,
FormControl,
FormLabel,
ListItem,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverTrigger,
Spinner,
Switch,
UnorderedList,
useDisclosure,
} from "@chakra-ui/react";
import moment from "moment";
import { useOutletContext } from "react-router-dom";
import { RelayIcon } from "../../components/icons";
import { Note } from "../../components/note";
import RepostNote from "../../components/note/repost-note";
import { isReply, isRepost, truncatedId } from "../../helpers/nostr-event";
@ -56,24 +47,6 @@ const UserNotesTab = () => {
Reposts
</FormLabel>
<Box flexGrow={1} />
<Popover>
<PopoverTrigger>
<Button variant="link" leftIcon={<RelayIcon />}>
Using Relays
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>
<UnorderedList>
{contextRelays.map((url) => (
<ListItem key={url}>{url}</ListItem>
))}
</UnorderedList>
</PopoverBody>
</PopoverContent>
</Popover>
</FormControl>
{timeline.map((event) =>
event.kind === 6 ? (

1028
yarn.lock

File diff suppressed because it is too large Load Diff