mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 13:20:37 +02:00
add e2e tests
This commit is contained in:
parent
cc912db05a
commit
868227a4cc
5
.changeset/itchy-boats-help.md
Normal file
5
.changeset/itchy-boats-help.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add e2e tests
|
12
cypress.config.ts
Normal file
12
cypress.config.ts
Normal 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
3
cypress/e2e/embeds.cy.ts
Normal file
@ -0,0 +1,3 @@
|
||||
describe("Embeds", () => {
|
||||
it("embeds a video element", () => {});
|
||||
});
|
50
cypress/e2e/login.cy.ts
Normal file
50
cypress/e2e/login.cy.ts
Normal 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
65
cypress/e2e/search.cy.ts
Normal 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");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
53
cypress/support/commands.ts
Normal file
53
cypress/support/commands.ts
Normal 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
31
cypress/support/e2e.ts
Normal 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
8
cypress/tsconfig.json
Normal 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"]
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 |
@ -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}>
|
||||
|
@ -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 && (
|
||||
|
@ -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);
|
||||
}
|
||||
|
17
src/hooks/use-shareable-profile-id.ts
Normal file
17
src/hooks/use-shareable-profile-id.ts
Normal 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]);
|
||||
}
|
@ -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);
|
||||
|
@ -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>}
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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 ? (
|
||||
|
Loading…
x
Reference in New Issue
Block a user