This commit is contained in:
hzrd149 2023-08-05 18:26:41 -05:00
parent 035faf1b5f
commit 1c759c0d20
69 changed files with 185 additions and 207 deletions

View File

@ -1,6 +1,6 @@
# noStrudel
> NOTE: This client is still in development and is buggy
> NOTE: This client is still in development and will have bugs
## noStrudel is my personal nostr client.
@ -10,7 +10,7 @@ There are many features missing from this client and I wont get around to implem
Live Instance: [nostrudel.ninja](https://nostrudel.ninja)
You can find better clients with more features in the [awesome-nostr](https://github.com/aljazceru/awesome-nostr) repo.
You can find better clients with more features on [nostrapps.com](https://www.nostrapps.com/) or in the [awesome-nostr](https://github.com/aljazceru/awesome-nostr) repo.
## Please don't trust my app with your nsec
@ -30,7 +30,7 @@ docker run --rm -p 8080:80 ghcr.io/hzrd149/nostrudel
git clone git@github.com:hzrd149/nostrudel.git
cd nostrudel
yarn install
yarn start
yarn dev
```
## Contributing

View File

@ -2,7 +2,7 @@ describe("Embeds", () => {
describe("hashtags", () => {
it('should handle uppercase hashtags and ","', () => {
cy.visit(
"#/n/nevent1qqsrj5ns6wva3fcghlyx0hp7hhajqtqk3kuckp7xhhscrm4jl7futegpz9mhxue69uhkummnw3e82efwvdhk6qgswaehxw309ahx7um5wgh8w6twv5pkpt8l",
"#/n/nevent1qqsrj5ns6wva3fcghlyx0hp7hhajqtqk3kuckp7xhhscrm4jl7futegpz9mhxue69uhkummnw3e82efwvdhk6qgswaehxw309ahx7um5wgh8w6twv5pkpt8l"
);
cy.findByRole("link", { name: "#Japan" }).should("be.visible");
@ -15,18 +15,18 @@ describe("Embeds", () => {
describe("links", () => {
it("embed trustless.computer links", () => {
cy.visit(
"#/n/nevent1qqsfn2mv3pe2v7jak4r5wnyengt36t0rx26w04hgysrmtpml8jnlk5cprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2qgawaehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2aq2wry06",
"#/n/nevent1qqsfn2mv3pe2v7jak4r5wnyengt36t0rx26w04hgysrmtpml8jnlk5cprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2qgawaehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2aq2wry06"
);
cy.get('[href="https://trustless.computer/"]').should("be.visible");
cy.get(
'[href="https://mempool.space/tx/461c6f56015c94d74837b68c9d08f4b80e7db7ca1e5ac4c53d9aa8c76b667672"]',
'[href="https://mempool.space/tx/461c6f56015c94d74837b68c9d08f4b80e7db7ca1e5ac4c53d9aa8c76b667672"]'
).should("be.visible");
});
it("embeds links", () => {
cy.visit(
"#/n/nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpz9mhxue69uhkummnw3e82efwvdhk6qgjwaehxw309aex2mrp0yhxvdm69e5k7r3xlpe",
"#/n/nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpz9mhxue69uhkummnw3e82efwvdhk6qgjwaehxw309aex2mrp0yhxvdm69e5k7r3xlpe"
);
cy.get('[href="https://getalby.com/"]').should("exist");
@ -38,11 +38,11 @@ describe("Embeds", () => {
it("embeds simplex.chat links", () => {
cy.visit(
"#/n/nevent1qqsymds0vlpp4f5s0dckjf4qz283pdsen0rmx8lu7ct6hpnxag2hpacpremhxue69uhkummnw3ez6un9d3shjtnwda4k7arpwfhjucm0d5q3qamnwvaz7tmwdaehgu3wwa5kueghxyq76",
"#/n/nevent1qqsymds0vlpp4f5s0dckjf4qz283pdsen0rmx8lu7ct6hpnxag2hpacpremhxue69uhkummnw3ez6un9d3shjtnwda4k7arpwfhjucm0d5q3qamnwvaz7tmwdaehgu3wwa5kueghxyq76"
);
cy.get(
'[href="https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FVlHiRmia02CDgga7w-uNb2FQZTZsj3UR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAd2GEWU9Zjrljhw8O4FldcxrqehkDWezXl-cWD-VkeEw%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion"]',
'[href="https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FVlHiRmia02CDgga7w-uNb2FQZTZsj3UR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAd2GEWU9Zjrljhw8O4FldcxrqehkDWezXl-cWD-VkeEw%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion"]'
).should("be.visible");
});
});
@ -50,7 +50,7 @@ describe("Embeds", () => {
describe("Nostr links", () => {
it("should embed noub1...", () => {
cy.visit(
"#/n/nevent1qqsd5yw7sntqfc4e7u4aempvgctry2plz653t9gpf97ctk5vc0ftskgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmdfxdj3a",
"#/n/nevent1qqsd5yw7sntqfc4e7u4aempvgctry2plz653t9gpf97ctk5vc0ftskgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmdfxdj3a"
);
cy.contains("Alby team");
@ -69,7 +69,7 @@ describe("Embeds", () => {
describe("youtube", () => {
it("should embed playlists", () => {
cy.visit(
"#/n/nevent1qqs8w6e63smpr5ccmz4l0w5pvnkp6r7z2fxaadjwu2g74y95pl9xv0cpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqqkgf54",
"#/n/nevent1qqs8w6e63smpr5ccmz4l0w5pvnkp6r7z2fxaadjwu2g74y95pl9xv0cpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqqkgf54"
);
cy.findByTitle(/youtube video player/i).should("be.visible");
@ -80,31 +80,31 @@ describe("Embeds", () => {
describe("Music", () => {
it("should handle wavlake links", () => {
cy.visit(
"#/n/nevent1qqsve4ud5v8gjds2f2h7exlmjvhqayu4s520pge7frpwe22wezny0pcpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2mxs3z0",
"#/n/nevent1qqsve4ud5v8gjds2f2h7exlmjvhqayu4s520pge7frpwe22wezny0pcpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2mxs3z0"
);
cy.findByTitle("Wavlake Embed").should("be.visible");
});
it("should handle spotify links", () => {
cy.visit(
"#/n/nevent1qqsx0lz7m72qzq499exwhnfszvgwea8tv38x9wkv32yhkmwwmhgs7jgprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk25m3sln",
"#/n/nevent1qqsx0lz7m72qzq499exwhnfszvgwea8tv38x9wkv32yhkmwwmhgs7jgprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk25m3sln"
);
cy.findByTitle("Spotify List Embed").should("exist");
cy.visit(
"#/n/nevent1qqsqxkmz49hydf8ppa9k6x6zrcq7m4evhhlye0j3lcnz8hrl2q6np4spz3mhxue69uhhyetvv9ujuerpd46hxtnfdult02qz",
"#/n/nevent1qqsqxkmz49hydf8ppa9k6x6zrcq7m4evhhlye0j3lcnz8hrl2q6np4spz3mhxue69uhhyetvv9ujuerpd46hxtnfdult02qz"
);
cy.findByTitle("Spotify Embed").should("exist");
});
it("should handle apple music links", () => {
cy.visit(
"#/n/nevent1qqs9kqt9d7r4zjpawcyl82x5qsn4hals4wn294dv95knrahs4mggwasprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2whhzvz",
"#/n/nevent1qqs9kqt9d7r4zjpawcyl82x5qsn4hals4wn294dv95knrahs4mggwasprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2whhzvz"
);
cy.findByTitle("Apple Music Embed").should("exist");
cy.visit(
"#/n/nevent1qqszyrz4uug75j4086kj4f8peg3g0v8g9f04zjxplnpq0uxljtthggqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2aeexmq",
"#/n/nevent1qqszyrz4uug75j4086kj4f8peg3g0v8g9f04zjxplnpq0uxljtthggqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2aeexmq"
);
cy.findByTitle("Apple Music List Embed").should("exist");
});
@ -118,7 +118,7 @@ describe("Embeds", () => {
describe("Emoji", () => {
it("should embed emojis", () => {
cy.visit(
"#/n/nevent1qqsdj7k47uh4z0ypl2m29lvd4ar9zpf6dcy7ls0q6g6qctnxfj5n3pcpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqdyqlpq",
"#/n/nevent1qqsdj7k47uh4z0ypl2m29lvd4ar9zpf6dcy7ls0q6g6qctnxfj5n3pcpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqdyqlpq"
);
cy.findByRole("img", { name: /pepeD/i }).should("be.visible");

View File

@ -50,7 +50,7 @@ describe("Login view", () => {
it("should redirect after login", () => {
cy.visit(
"#/n/nevent1qqs88gdxv36qsjfwr66k7wxuq9r2tg8rsdcnfkcqdg4sc6vlnsma98qpzpmhxue69uhkummnw3ezuamfdejsz9rhwden5te0wfjkccte9ejxzmt4wvhxjmccew89d",
"#/n/nevent1qqs88gdxv36qsjfwr66k7wxuq9r2tg8rsdcnfkcqdg4sc6vlnsma98qpzpmhxue69uhkummnw3ezuamfdejsz9rhwden5te0wfjkccte9ejxzmt4wvhxjmccew89d"
);
cy.findByRole("link", { name: /login/i }).click();

View File

@ -1,7 +1,7 @@
describe("Profile view", () => {
it("should load a rss feed profile", () => {
cy.visit(
"#/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un",
"#/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un"
);
cy.contains("fjsmu");

View File

@ -2,7 +2,7 @@ describe("No account", () => {
describe("note view", () => {
it("should fetch and render note", () => {
cy.visit(
"#/n/nevent1qqs84hwdlls703w4yf66qsszxjqfc0xselfxrzr6n4qp40vzdnczragpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5jcwczn",
"#/n/nevent1qqs84hwdlls703w4yf66qsszxjqfc0xselfxrzr6n4qp40vzdnczragpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5jcwczn"
);
cy.get(".chakra-card")

View File

@ -1,7 +1,7 @@
describe("Thread", () => {
it("should handle quote notes with e tags correctly", () => {
cy.visit(
"#/n/nevent1qqsx2lnyuke6vmsrz9fdrd6uwjy0g0e9l6menfgdj5truugkh9qmkkgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqgc9md6",
"#/n/nevent1qqsx2lnyuke6vmsrz9fdrd6uwjy0g0e9l6menfgdj5truugkh9qmkkgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqgc9md6"
);
// find first note

View File

@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View File

@ -5,6 +5,7 @@
"license": "MIT",
"scripts": {
"start": "vite serve",
"dev": "vite serve",
"build": "tsc --project tsconfig.json && vite build",
"format": "prettier --ignore-path .prettierignore -w .",
"e2e": "cypress open",

View File

@ -21,7 +21,6 @@ import {
import relayPoolService from "../services/relay-pool";
import { useInterval } from "react-use";
import { RelayStatus } from "./relay-status";
import { useIsMobile } from "../hooks/use-is-mobile";
import { RelayIcon } from "./icons";
import { Relay } from "../classes/relay";
import { RelayFavicon } from "./relay-favicon";
@ -29,7 +28,6 @@ import relayScoreboardService from "../services/relay-scoreboard";
import { RelayScoreBreakdown } from "./relay-score-breakdown";
export const ConnectedRelays = () => {
const isMobile = useIsMobile();
const { isOpen, onOpen, onClose } = useDisclosure();
const [relays, setRelays] = useState<Relay[]>(relayPoolService.getRelays());
const sortedRelays = useMemo(() => relayScoreboardService.getRankedRelays(relays.map((r) => r.url)), [relays]);

View File

@ -7,7 +7,7 @@ export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNo
regexp: /:([a-zA-Z0-9_]+):/i,
render: (match) => {
const emojiTag = note.tags.find(
(tag) => tag[0] === "emoji" && tag[1].toLowerCase() === match[1].toLowerCase() && tag[2],
(tag) => tag[0] === "emoji" && tag[1].toLowerCase() === match[1].toLowerCase() && tag[2]
);
if (emojiTag) {
return (

View File

@ -53,7 +53,7 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
<ExpandProvider>
<Card variant={variant} ref={ref} data-event-id={event.id}>
<CardHeader padding="2">
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
<Flex flex="1" gap="2" alignItems="center">
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />

View File

@ -3,16 +3,16 @@ import { getEventRelays } from "../../services/event-relays";
import { NostrEvent } from "../../types/nostr-event";
import useSubject from "../../hooks/use-subject";
import { RelayIconStack } from "../relay-icon-stack";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { getEventUID } from "../../helpers/nostr/event";
import { useBreakpointValue } from "@chakra-ui/react";
export type NoteRelaysProps = {
event: NostrEvent;
};
export const EventRelays = memo(({ event }: NoteRelaysProps) => {
const isMobile = useIsMobile();
const maxRelays = useBreakpointValue({ base: 3, md: undefined });
const eventRelays = useSubject(getEventRelays(getEventUID(event)));
return <RelayIconStack relays={eventRelays} direction="row-reverse" maxRelays={isMobile ? 4 : undefined} />;
return <RelayIconStack relays={eventRelays} direction="row-reverse" maxRelays={maxRelays} />;
});

View File

@ -17,11 +17,10 @@ import { UserAvatarLink } from "../user-avatar-link";
import { UserLink } from "../user-link";
import dayjs from "dayjs";
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
import { ParsedZap, parseZapEvent } from "../../helpers/zaps";
import { ParsedZap } from "../../helpers/zaps";
import { readablizeSats } from "../../helpers/bolt11";
import useEventReactions from "../../hooks/use-event-reactions";
import useEventZaps from "../../hooks/use-event-zaps";
import { useIsMobile } from "../../hooks/use-is-mobile";
function getReactionIcon(content: string) {
switch (content) {
@ -48,12 +47,10 @@ const ReactionEvent = React.memo(({ event }: { event: NostrEvent }) => (
));
const ZapEvent = React.memo(({ zap }: { zap: ParsedZap }) => {
const isMobile = useIsMobile();
if (!zap.payment.amount) return null;
return (
<Box borderWidth="1px" borderRadius="lg" py="2" px={isMobile ? "2" : "4"}>
<Box borderWidth="1px" borderRadius="lg" py="2" px={["2", "4"]}>
<Flex gap="2" justifyContent="space-between">
<Box>
<UserAvatarLink pubkey={zap.request.pubkey} size="xs" mr="2" />
@ -80,14 +77,13 @@ export default function NoteReactionsModal({
const zaps = useEventZaps(noteId, [], true) ?? [];
const reactions = useEventReactions(noteId, [], true) ?? [];
const [selected, setSelected] = useState("zaps");
const isMobile = useIsMobile();
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalBody p={isMobile ? "2" : "4"}>
<ModalBody p={["2", "4"]}>
<Flex direction="column" gap="2">
<ButtonGroup>
<Button size="sm" variant={selected === "zaps" ? "solid" : "outline"} onClick={() => setSelected("zaps")}>

View File

@ -67,7 +67,7 @@ export default function PeopleListProvider({ children }: PropsWithChildren) {
list,
setList,
}),
[list, setList],
[list, setList]
);
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;

View File

@ -20,7 +20,6 @@ import { normalizeToHex } from "../../helpers/nip19";
import { getReferences } from "../../helpers/nostr/event";
import { matchHashtag, mentionNpubOrNote } from "../../helpers/regexp";
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { useSigningContext } from "../../providers/signing-provider";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import { ImageIcon } from "../icons";
@ -79,7 +78,6 @@ type PostModalProps = {
};
export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) => {
const isMobile = useIsMobile();
const toast = useToast();
const { requestSignature } = useSigningContext();
const writeRelays = useWriteRelayUrls();
@ -98,7 +96,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
const payload = new FormData();
payload.append("fileToUpload", imageFile);
const response = await fetch("https://nostr.build/upload.php", { body: payload, method: "POST" }).then((res) =>
res.text(),
res.text()
);
const imageUrl = response.match(/https:\/\/nostr\.build\/i\/[\w.]+/)?.[0];
if (imageUrl) {
@ -193,7 +191,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
<Modal isOpen={isOpen} onClose={onClose} size="4xl" closeOnOverlayClick={false}>
<ModalOverlay />
<ModalContent>
<ModalBody padding={isMobile ? "2" : "4"}>{renderContent()}</ModalBody>
<ModalBody padding={["2", "2", "4"]}>{renderContent()}</ModalBody>
</ModalContent>
</Modal>
);

View File

@ -28,7 +28,7 @@ function RelayPickerModal({
}: { onSelect: (relay: string) => void } & Omit<ModalProps, "children">) {
const [search, setSearch] = useState("");
const { value: onlineRelays } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>),
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>)
);
const relayList = unique(onlineRelays ?? []);
@ -78,7 +78,7 @@ export const RelayUrlInput = forwardRef(
({ onChange, ...props }: Omit<RelayUrlInputProps, "onChange"> & { onChange: (url: string) => void }, ref) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const { value: relaysJson } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>),
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>)
);
const relaySuggestions = unique(relaysJson ?? []);
@ -100,5 +100,5 @@ export const RelayUrlInput = forwardRef(
<RelayPickerModal onClose={onClose} isOpen={isOpen} onSelect={(url) => onChange(url)} size="2xl" />
</>
);
},
}
);

View File

@ -1,12 +1,21 @@
import { Alert, AlertDescription, AlertIcon, AlertProps, AlertTitle, Button, Spacer, useModal } from "@chakra-ui/react";
import { useIsMobile } from "../hooks/use-is-mobile";
import {
Alert,
AlertDescription,
AlertIcon,
AlertProps,
AlertTitle,
Button,
Spacer,
useBreakpointValue,
useModal,
} from "@chakra-ui/react";
import { useExpand } from "./note/expanded";
export default function SensitiveContentWarning({ description }: { description: string } & AlertProps) {
const isMobile = useIsMobile();
const expand = useExpand();
const smallScreen = useBreakpointValue({ base: true, md: false });
if (isMobile) {
if (smallScreen) {
return (
<Alert
status="warning"

View File

@ -1,10 +1,9 @@
import { useCallback, useRef } from "react";
import { Flex, Grid, SimpleGrid } from "@chakra-ui/react";
import { useCallback } from "react";
import { Flex, SimpleGrid } from "@chakra-ui/react";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import GenericNoteTimeline from "./generic-note-timeline";
import { ImageGalleryProvider } from "../image-gallery";
import MediaTimeline from "./media-timeline";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { TimelineLoader } from "../../classes/timeline-loader";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "./timeline-action-and-status";
@ -21,7 +20,7 @@ export function useTimelinePageEventFilter() {
if (view === "images" && !event.content.match(matchImageUrls)) return false;
return true;
},
[view],
[view]
);
}
@ -41,7 +40,7 @@ export default function TimelinePage({ timeline, header }: { timeline: TimelineL
case "images":
return (
<ImageGalleryProvider>
<SimpleGrid minChildWidth={["full", "15rem"]} gap="4">
<SimpleGrid columns={[1, 2, 2, 3, 4, 5]} gap="4">
<MediaTimeline timeline={timeline} />
</SimpleGrid>
</ImageGalleryProvider>

View File

@ -4,7 +4,6 @@ import useSubject from "../../../hooks/use-subject";
import { matchImageUrls } from "../../../helpers/regexp";
import { ImageGalleryLink, ImageGalleryProvider } from "../../image-gallery";
import { Box, IconButton } from "@chakra-ui/react";
import { useIsMobile } from "../../../hooks/use-is-mobile";
import { useNavigate } from "react-router-dom";
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import { getSharableNoteId } from "../../../helpers/nip19";
@ -42,7 +41,6 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => {
});
export default function MediaTimeline({ timeline }: { timeline: TimelineLoader }) {
const isMobile = useIsMobile();
const events = useSubject(timeline.timeline);
const images = useMemo(() => {

View File

@ -52,7 +52,7 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
}
// if the stream has not been updated in a day consider it ended
if (stream.created_at < dayjs().subtract(2, "day").unix()) {
if (stream.created_at < dayjs().subtract(1, "week").unix()) {
status = "ended";
}

View File

@ -16,7 +16,7 @@ export default function useAppSettings() {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[settings],
[settings]
);
return {

View File

@ -8,7 +8,7 @@ export default function useEventReactions(eventId: string, additionalRelays: str
const subject = useMemo(
() => eventReactionsService.requestReactions(eventId, relays, alwaysFetch),
[eventId, relays.join("|"), alwaysFetch],
[eventId, relays.join("|"), alwaysFetch]
);
return useSubject(subject);

View File

@ -9,7 +9,7 @@ export default function useEventZaps(eventId: string, additionalRelays: string[]
const subject = useMemo(
() => eventZapsService.requestZaps(eventId, relays, alwaysFetch),
[eventId, relays.join("|"), alwaysFetch],
[eventId, relays.join("|"), alwaysFetch]
);
const events = useSubject(subject) || [];

View File

@ -1,14 +0,0 @@
import { useEffect, useMemo, useState } from "react";
export function useIsMobile() {
const match = useMemo(() => window.matchMedia("(max-width: 1000px)"), []);
const [matches, setMatches] = useState(match.matches);
useEffect(() => {
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
match.addEventListener("change", listener);
return () => match.removeEventListener("change", listener);
}, [match]);
return matches;
}

View File

@ -12,7 +12,7 @@ export function usePaginatedList<T extends unknown>(list: T[], opts?: Options) {
const previous = useCallback(() => setPage((v) => Math.max(v - 1, 0)), [setPage]);
const pageItems = useMemo(
() => list.slice(pageSize * currentPage, pageSize * currentPage + pageSize),
[list, currentPage, pageSize],
[list, currentPage, pageSize]
);
return {

View File

@ -22,6 +22,6 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader)
}
}
},
[timeline],
[timeline]
);
}

View File

@ -5,7 +5,7 @@ import useSubject from "./use-subject";
export function useUserContacts(pubkey: string, relays: string[], alwaysRequest = false) {
const observable = useMemo(
() => userContactsService.requestContacts(pubkey, relays, alwaysRequest),
[pubkey, relays.join("|"), alwaysRequest],
[pubkey, relays.join("|"), alwaysRequest]
);
const contacts = useSubject(observable);

View File

@ -7,7 +7,7 @@ export default function useUserLNURLMetadata(pubkey: string) {
const address = userMetadata?.lud16 || userMetadata?.lud06;
const { value: metadata } = useAsync(
async () => (address ? lnurlMetadataService.requestMetadata(address) : undefined),
[address],
[address]
);
return { metadata, address };

View File

@ -8,7 +8,7 @@ export function useUserMetadata(pubkey: string, additionalRelays: string[] = [],
const subject = useMemo(
() => userMetadataService.requestMetadata(pubkey, relays, alwaysRequest),
[pubkey, relays, alwaysRequest],
[pubkey, relays, alwaysRequest]
);
const metadata = useSubject(subject);

View File

@ -7,7 +7,7 @@ export function useUserRelays(pubkey: string, additionalRelays: string[] = [], a
const relays = useReadRelayUrls(additionalRelays);
const subject = useMemo(
() => userRelaysService.requestRelays(pubkey, relays, alwaysRequest),
[pubkey, relays.join("|"), alwaysRequest],
[pubkey, relays.join("|"), alwaysRequest]
);
const userRelays = useSubject(subject);

View File

@ -23,5 +23,5 @@ const root = createRoot(element);
root.render(
<GlobalProviders>
<App />
</GlobalProviders>,
</GlobalProviders>
);

View File

@ -38,7 +38,7 @@ const mediaMapper = (item: ImageObject[] | VideoObject[]) => ({
const mediaSorter = (
a: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject,
b: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject,
b: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject
) => {
if (!(a.url && b.url)) {
return 0;
@ -114,7 +114,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
ogObject.ogImageProperty,
ogObject.ogImageWidth,
ogObject.ogImageHeight,
ogObject.ogImageType,
ogObject.ogImageType
)
.map(mediaMapper)
.filter((value: ImageObject) => value.url !== undefined && value.url !== "")
@ -134,7 +134,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
ogObject.ogVideoProperty,
ogObject.ogVideoWidth,
ogObject.ogVideoHeight,
ogObject.ogVideoType,
ogObject.ogVideoType
)
.map(mediaMapper)
.filter((value: VideoObject) => value.url !== undefined && value.url !== "")
@ -164,7 +164,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
ogObject.twitterImageProperty,
ogObject.twitterImageWidth,
ogObject.twitterImageHeight,
ogObject.twitterImageAlt,
ogObject.twitterImageAlt
)
.map(mediaMapperTwitterImage)
.filter((value: TwitterImageObject) => value.url !== undefined && value.url !== "")
@ -189,7 +189,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
ogObject.twitterPlayerProperty,
ogObject.twitterPlayerWidth,
ogObject.twitterPlayerHeight,
ogObject.twitterPlayerStream,
ogObject.twitterPlayerStream
)
.map(mediaMapperTwitterPlayer)
.filter((value: TwitterPlayerObject) => value.url !== undefined && value.url !== "")

View File

@ -83,7 +83,7 @@ export class QrCode {
minVersion: int = 1,
maxVersion: int = 40,
mask: int = -1,
boostEcl: boolean = true,
boostEcl: boolean = true
): QrCode {
if (
!(QrCode.MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= QrCode.MAX_VERSION) ||
@ -175,7 +175,7 @@ export class QrCode {
dataCodewords: Readonly<Array<byte>>,
msk: int,
msk: int
) {
// Check scalar arguments
if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
@ -822,7 +822,7 @@ export class QrSegment {
public readonly numChars: int,
// The data bits of this segment. Accessed through getData().
private readonly bitData: Array<bit>,
private readonly bitData: Array<bit>
) {
if (numChars < 0) throw new RangeError("Invalid argument");
this.bitData = bitData.slice(); // Make defensive copy
@ -897,7 +897,7 @@ export class Ecc {
// In the range 0 to 3 (unsigned 2-bit integer).
public readonly ordinal: int,
// (Package-private) In the range 0 to 3 (unsigned 2-bit integer).
public readonly formatBits: int,
public readonly formatBits: int
) {}
}
// }
@ -925,7 +925,7 @@ export class Mode {
// The mode indicator bits, which is a uint4 value (range 0 to 15).
public readonly modeBits: int,
// Number of character count bits for three different version ranges.
private readonly numBitsCharCount: [int, int, int],
private readonly numBitsCharCount: [int, int, int]
) {}
/*-- Method --*/

View File

@ -19,7 +19,7 @@ const IntersectionObserverContext = createContext<{
export type ExtendedIntersectionObserverEntry<T> = { entry: IntersectionObserverEntry; id: T | undefined };
export type ExtendedIntersectionObserverCallback<T> = (
entries: ExtendedIntersectionObserverEntry<T>[],
observer: IntersectionObserver,
observer: IntersectionObserver
) => void;
export function useIntersectionObserver() {
@ -42,7 +42,7 @@ export function useRegisterIntersectionEntity<T>(ref: MutableRefObject<Element |
export function useIntersectionMapCallback<T>(
callback: (map: Map<T, IntersectionObserverEntry>) => void,
watch: DependencyList,
watch: DependencyList
) {
const map = useMemo(() => new Map<T, IntersectionObserverEntry>(), []);
return useCallback<ExtendedIntersectionObserverCallback<T>>(
@ -53,7 +53,7 @@ export function useIntersectionMapCallback<T>(
callback(map);
},
[callback, ...watch],
[callback, ...watch]
);
}
@ -76,11 +76,11 @@ export default function IntersectionObserverProvider<T = undefined>({
entries.map((entry) => {
return { entry, id: elementIds.get(entry.target) };
}),
observer,
observer
);
}, []);
const [observer, setObserver] = useState<IntersectionObserver>(
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold }),
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold })
);
useMount(() => {
@ -97,7 +97,7 @@ export default function IntersectionObserverProvider<T = undefined>({
(element: Element, id: T) => {
elementIds.set(element, id);
},
[elementIds],
[elementIds]
);
const context = useMemo(
@ -105,7 +105,7 @@ export default function IntersectionObserverProvider<T = undefined>({
observer,
setElementId,
}),
[observer, setElementId],
[observer, setElementId]
);
return <IntersectionObserverContext.Provider value={context}>{children}</IntersectionObserverContext.Provider>;

View File

@ -20,7 +20,7 @@ export default function PostModalProvider({ children }: PropsWithChildren) {
setDraft(draft);
onOpen();
},
[setDraft, onOpen],
[setDraft, onOpen]
);
const context = useMemo(() => ({ openModal }), [openModal]);

View File

@ -40,7 +40,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[toast, current],
[toast, current]
);
const requestDecrypt = useCallback(
async (data: string, pubkey: string) => {
@ -51,7 +51,7 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[toast, current],
[toast, current]
);
const requestEncrypt = useCallback(
async (data: string, pubkey: string) => {
@ -62,12 +62,12 @@ export const SigningProvider = ({ children }: { children: React.ReactNode }) =>
if (e instanceof Error) toast({ description: e.message, status: "error" });
}
},
[toast, current],
[toast, current]
);
const context = useMemo(
() => ({ requestSignature, requestDecrypt, requestEncrypt }),
[requestSignature, requestDecrypt, requestEncrypt],
[requestSignature, requestDecrypt, requestEncrypt]
);
return <SigningContext.Provider value={context}>{children}</SigningContext.Provider>;

View File

@ -21,7 +21,7 @@ function handleNewContacts(contacts: UserContacts | undefined) {
const relay = contacts.contactRelay[key];
if (relay) return ["p", key, relay];
else return ["p", key];
}),
})
);
// reset the pending list since we just got a new contacts list
@ -98,7 +98,7 @@ function addContact(pubkey: string, relay?: string) {
return newTag;
}
return t;
}),
})
);
} else {
following.next([...pTags, newTag]);

View File

@ -23,14 +23,14 @@ class DirectMessagesService {
this.incomingSub = new NostrMultiSubscription(
clientRelaysService.getReadUrls(),
undefined,
"incoming-direct-messages",
"incoming-direct-messages"
);
this.incomingSub.onEvent.subscribe(this.receiveEvent, this);
this.outgoingSub = new NostrMultiSubscription(
clientRelaysService.getReadUrls(),
undefined,
"outgoing-direct-messages",
"outgoing-direct-messages"
);
this.outgoingSub.onEvent.subscribe(this.receiveEvent, this);

View File

@ -28,7 +28,7 @@ function getIdentityFromJson(name: string, domain: string, json: IdentityJson):
async function fetchAllIdentities(domain: string) {
const json = await fetchWithCorsFallback(`//${domain}/.well-known/nostr.json`).then(
(res) => res.json() as Promise<IdentityJson>,
(res) => res.json() as Promise<IdentityJson>
);
await addToCache(domain, json);
@ -101,7 +101,7 @@ async function pruneCache() {
const keys = await db.getAllKeysFromIndex(
"dnsIdentifiers",
"updated",
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix()),
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix())
);
for (const pubkey of keys) {

View File

@ -86,7 +86,7 @@ class PubkeyRelayAssignmentService {
if (userRelays.length === 0) userRelays = Array.from(readRelays);
const rankedOptions = Array.from(userRelays).sort(
(a, b) => (relayScores.get(b) ?? 0) - (relayScores.get(a) ?? 0),
(a, b) => (relayScores.get(b) ?? 0) - (relayScores.get(a) ?? 0)
);
assignments[pubkey] = rankedOptions.slice(0, 3);

View File

@ -17,7 +17,7 @@ async function fetchInfo(relay: string) {
url.protocol = url.protocol === "ws:" ? "http" : "https";
const infoDoc = await fetchWithCorsFallback(url, { headers: { Accept: "application/nostr+json" } }).then(
(res) => res.json() as Promise<RelayInformationDocument>,
(res) => res.json() as Promise<RelayInformationDocument>
);
memoryCache.set(relay, infoDoc);

View File

@ -115,7 +115,7 @@ class ReplaceableEventRelayLoader {
`Updating query`,
Array.from(Object.keys(filters))
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
.join(", "),
.join(", ")
);
this.subscription.setQuery(query);
@ -133,7 +133,7 @@ class ReplaceableEventLoaderService {
private events = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
private loaders = new SuperMap<Relay, ReplaceableEventRelayLoader>(
(relay) => new ReplaceableEventRelayLoader(relay, this.log.extend(relay)),
(relay) => new ReplaceableEventRelayLoader(relay, this.log.extend(relay))
);
log = logger.extend("ReplaceableEventLoader");
@ -180,7 +180,7 @@ class ReplaceableEventLoaderService {
const keys = await db.getAllKeysFromIndex(
"replaceableEvents",
"created",
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix()),
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix())
);
this.log(`Pruning ${keys.length} events`);
@ -243,12 +243,9 @@ replaceableEventLoaderService.pruneCache();
setInterval(() => {
replaceableEventLoaderService.update();
}, 1000 * 2);
setInterval(
() => {
replaceableEventLoaderService.pruneCache();
},
1000 * 60 * 60,
);
setInterval(() => {
replaceableEventLoaderService.pruneCache();
}, 1000 * 60 * 60);
if (import.meta.env.DEV) {
//@ts-ignore

View File

@ -11,7 +11,7 @@ const DTAG = "nostrudel-settings";
class UserAppSettings {
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
() => new PersistentSubject<AppSettings>(defaultSettings),
() => new PersistentSubject<AppSettings>(defaultSettings)
);
getSubject(pubkey: string) {
return this.parsedSubjects.get(pubkey);

View File

@ -19,7 +19,7 @@ class SigningService {
private async getKeyMaterial() {
const password = window.prompt(
"Enter local encryption password. This password is used to keep your secret key save.",
"Enter local encryption password. This password is used to keep your secret key save."
);
if (!password) throw new Error("Password required");
const enc = new TextEncoder();
@ -38,7 +38,7 @@ class SigningService {
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
["encrypt", "decrypt"]
);
}

View File

@ -31,15 +31,12 @@ function parseContacts(event: NostrEvent): UserContacts {
const relays = normalizeRelayConfigs(relayJsonToRelayConfig(relayJson));
const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]);
const contactRelay = event.tags.filter(isPTag).reduce(
(dir, tag) => {
if (tag[2]) {
dir[tag[1]] = tag[2];
}
return dir;
},
{} as Record<string, string>,
);
const contactRelay = event.tags.filter(isPTag).reduce((dir, tag) => {
if (tag[2]) {
dir[tag[1]] = tag[2];
}
return dir;
}, {} as Record<string, string>);
return {
pubkey: event.pubkey,
@ -63,7 +60,7 @@ class UserContactsService {
Kind.Contacts,
pubkey,
undefined,
alwaysRequest,
alwaysRequest
);
sub.connectWithHandler(requestSub, (event, next) => next(parseContacts(event)));

View File

@ -32,7 +32,7 @@ class UserMetadataService {
Kind.Metadata,
pubkey,
undefined,
alwaysRequest,
alwaysRequest
);
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
return sub;

View File

@ -63,7 +63,7 @@ class UserTrustedStatsService {
async fetchUserStats(pubkey: string) {
try {
const stats = await fetch(`https://api.nostr.band/v0/stats/profile/${pubkey}`).then(
(res) => res.json() as Promise<{ stats: Record<string, NostrBandProfileStats> }>,
(res) => res.json() as Promise<{ stats: Record<string, NostrBandProfileStats> }>
);
if (stats?.stats[pubkey]) {

View File

@ -57,13 +57,13 @@ function HashTagPage() {
if (!showReplies && isReply(event)) return false;
return timelinePageEventFilter(event);
},
[showReplies],
[showReplies]
);
const timeline = useTimelineLoader(
`${hashtag}-hashtag`,
readRelays,
{ kinds: [1], "#t": [hashtag] },
{ eventFilter },
{ eventFilter }
);
useRelaysChanged(readRelays, () => timeline.reset());

View File

@ -29,7 +29,7 @@ function FollowingTabBody() {
if (!showReplies && isReply(event)) return false;
return timelinePageEventFilter(event);
},
[showReplies, timelinePageEventFilter],
[showReplies, timelinePageEventFilter]
);
const following = contacts?.contacts || [];
@ -37,7 +37,7 @@ function FollowingTabBody() {
`${truncatedId(account.pubkey)}-following`,
readRelays,
{ authors: following, kinds: [Kind.Text, Kind.Repost, 2] },
{ enabled: following.length > 0, eventFilter },
{ enabled: following.length > 0, eventFilter }
);
const header = (

View File

@ -24,7 +24,7 @@ function GlobalPage() {
if (!showReplies && isReply(event)) return false;
return timelineEventFilter(event);
},
[showReplies, timelineEventFilter],
[showReplies, timelineEventFilter]
);
const timeline = useTimelineLoader(`global`, readRelays, { kinds: [1] }, { eventFilter });
useRelaysChanged(readRelays, () => timeline.reset());

View File

@ -51,7 +51,7 @@ export default function LoginNip05View() {
setLoading(false);
},
1000,
[nip05, setPubkey, setRelays, setLoading],
[nip05, setPubkey, setRelays, setLoading]
);
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {

View File

@ -62,7 +62,7 @@ export default function LoginNsecView() {
setError(true);
}
},
[setInputValue, setHexKey, setNpub, setError],
[setInputValue, setHexKey, setNpub, setError]
);
const handleSubmit: React.FormEventHandler<HTMLDivElement> = async (e) => {

View File

@ -104,7 +104,7 @@ export default function MapView() {
const hash = ngeohash.encode(center.lat, center.lng, 5);
setSearchParams({ hash }, { replace: true });
}, 1000),
}, 1000)
);
setMap(map);
@ -122,7 +122,7 @@ export default function MapView() {
"geo-events",
readRelays,
{ "#g": cells, kinds: [Kind.Text] },
{ enabled: cells.length > 0 },
{ enabled: cells.length > 0 }
);
const setCellsFromMap = useCallback(() => {
@ -133,7 +133,7 @@ export default function MapView() {
bbox.getWest(),
bbox.getNorth(),
bbox.getEast(),
getPrecision(map.getZoom()),
getPrecision(map.getZoom())
);
setCells(hashes);

View File

@ -1,14 +1,13 @@
import { useState } from "react";
import { Button, Card, CardBody, Flex, IconButton, Textarea } from "@chakra-ui/react";
import dayjs from "dayjs";
import { Kind } from "nostr-tools";
import { useRef, useState } from "react";
import { Link, Navigate, useParams } from "react-router-dom";
import { nostrPostAction } from "../../classes/nostr-post-action";
import { ArrowLeftSIcon } from "../../components/icons";
import { UserAvatar } from "../../components/user-avatar";
import { UserLink } from "../../components/user-link";
import { normalizeToHex } from "../../helpers/nip19";
import { useIsMobile } from "../../hooks/use-is-mobile";
import useSubject from "../../hooks/use-subject";
import { useSigningContext } from "../../providers/signing-provider";
import clientRelaysService from "../../services/client-relays";
@ -24,7 +23,6 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
const isMobile = useIsMobile();
const account = useCurrentAccount()!;
const { requestEncrypt, requestSignature } = useSigningContext();
const [content, setContent] = useState<string>("");
@ -76,9 +74,9 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
icon={<ArrowLeftSIcon />}
aria-label="Back"
to="/dm"
size={isMobile ? "sm" : "md"}
size={["sm", "md"]}
/>
<UserAvatar pubkey={pubkey} size={isMobile ? "sm" : "md"} />
<UserAvatar pubkey={pubkey} size={["sm", "md"]} />
<UserLink pubkey={pubkey} />
</CardBody>
</Card>

View File

@ -23,7 +23,6 @@ import useSubject from "../../hooks/use-subject";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import directMessagesService from "../../services/direct-messages";
import { ExternalLinkIcon } from "../../components/icons";
import { useIsMobile } from "../../hooks/use-is-mobile";
import RequireCurrentAccount from "../../providers/require-current-account";
function ContactCard({ pubkey }: { pubkey: string }) {
@ -47,7 +46,6 @@ function ContactCard({ pubkey }: { pubkey: string }) {
}
function DirectMessagesPage() {
const isMobile = useIsMobile();
const [from, setFrom] = useState(dayjs().subtract(2, "days"));
const conversations = useSubject(directMessagesService.conversations);
@ -97,7 +95,7 @@ function DirectMessagesPage() {
<Flex direction="column" gap="2" overflowX="hidden" overflowY="auto" height="100%" pt="2" pb="8">
<Alert status="info" flexShrink={0}>
<AlertIcon />
<Flex direction={isMobile ? "column" : "row"}>
<Flex direction={{ base: "column", lg: "row" }}>
<AlertTitle>Give Blowater a try</AlertTitle>
<AlertDescription>
<Text>

View File

@ -3,7 +3,6 @@ import { useState } from "react";
import { ArrowDownSIcon, ArrowUpSIcon } from "../../components/icons";
import { Note } from "../../components/note";
import { countReplies, ThreadItem as ThreadItemData } from "../../helpers/thread";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { TrustProvider } from "../../providers/trust";
export type ThreadItemProps = {
@ -13,7 +12,6 @@ export type ThreadItemProps = {
};
export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps) => {
const isMobile = useIsMobile();
const [showReplies, setShowReplies] = useState(initShowReplies ?? post.replies.length === 1);
const toggle = () => setShowReplies((v) => !v);
@ -31,7 +29,7 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
{showReplies ? <ArrowDownSIcon /> : <ArrowUpSIcon />}
</Button>
{showReplies && (
<Flex direction="column" gap="2" pl={isMobile ? 2 : 4} borderLeftColor="gray.500" borderLeftWidth="1px">
<Flex direction="column" gap="2" pl={[2, 2, 4]} borderLeftColor="gray.500" borderLeftWidth="1px">
{post.replies.map((child) => (
<ThreadPost key={child.event.id} post={child} focusId={focusId} />
))}

View File

@ -206,7 +206,7 @@ export const ProfileEditView = () => {
nip05: metadata?.nip05,
lightningAddress: metadata?.lud16 || metadata?.lud06,
}),
[metadata],
[metadata]
);
const handleSubmit = async (data: FormData) => {

View File

@ -25,7 +25,7 @@ export default function RelaysView() {
.map((r) => r.url)
.filter(safeRelayUrl);
const { value: onlineRelays = [] } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>),
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>)
);
const filteredRelays = useMemo(() => {
@ -51,9 +51,9 @@ export default function RelaysView() {
Add Custom
</Button>
</Flex>
<SimpleGrid minChildWidth="25rem" spacing="2">
<SimpleGrid columns={[1, 1, 1, 2, 3]} spacing="2">
{filteredRelays.map((url) => (
<RelayCard key={url} url={url} variant="outline" maxW="xl" />
<RelayCard key={url} url={url} variant="outline" />
))}
</SimpleGrid>
@ -61,9 +61,9 @@ export default function RelaysView() {
<>
<Divider />
<Heading size="lg">Discovered Relays</Heading>
<SimpleGrid minChildWidth="25rem" spacing="2">
<SimpleGrid columns={[1, 1, 1, 2, 3]} spacing="2">
{discoveredRelays.map((url) => (
<RelayCard key={url} url={url} variant="outline" maxW="xl" />
<RelayCard key={url} url={url} variant="outline" />
))}
</SimpleGrid>
</>

View File

@ -105,7 +105,11 @@ function RelayPage({ relay }: { relay: string }) {
<RelayJoinAction url={relay} />
</Flex>
<RelayMetadata url={relay} />
<Flex gap="2">{info?.supported_nips?.map((nip) => <NipTag key={nip} nip={nip} />)}</Flex>
<Flex gap="2">
{info?.supported_nips?.map((nip) => (
<NipTag key={nip} nip={nip} />
))}
</Flex>
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="brand">
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
<Tab>Reviews</Tab>

View File

@ -87,7 +87,7 @@ function SearchResults({ search }: { search: string }) {
`search`,
searchRelays,
{ search: search || "", kinds: [Kind.Metadata] },
{ enabled: !!search },
{ enabled: !!search }
);
const events = useSubject(timeline?.timeline) ?? [];
@ -96,7 +96,7 @@ function SearchResults({ search }: { search: string }) {
return (
<IntersectionObserverProvider callback={callback}>
<SimpleGrid minChildWidth="30rem" spacing="2">
<SimpleGrid columns={{ base: 1, xl: 2 }} spacing="2">
{events.map((event) => (
<ProfileResult key={event.id} event={event} />
))}
@ -148,17 +148,19 @@ export function SearchPage() {
};
return (
<Flex direction="column" py="2" gap="2">
<Flex direction="column" py="2" px={["2", "2", 0]} gap="2">
<QrScannerModal isOpen={qrScannerModal.isOpen} onClose={qrScannerModal.onClose} onData={handleSearchText} />
<form onSubmit={handleSubmit}>
<Flex gap="2">
<IconButton onClick={qrScannerModal.onOpen} icon={<QrCodeIcon />} aria-label="Qr Scanner" />
{!!navigator.clipboard.readText && (
<IconButton onClick={readClipboard} icon={<ClipboardIcon />} aria-label="Read clipboard" />
)}
<Input type="search" value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
<Button type="submit">Search</Button>
<Flex gap="2" wrap="wrap">
<Flex gap="2" grow={1}>
<IconButton onClick={qrScannerModal.onOpen} icon={<QrCodeIcon />} aria-label="Qr Scanner" />
{!!navigator.clipboard.readText && (
<IconButton onClick={readClipboard} icon={<ClipboardIcon />} aria-label="Read clipboard" />
)}
<Input type="search" value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
<Button type="submit">Search</Button>
</Flex>
<RelaySelectionButton />
</Flex>
</form>

View File

@ -27,7 +27,7 @@ function StreamsPage() {
} catch (e) {}
return false;
},
[filterStatus],
[filterStatus]
);
const { people } = usePeopleListContext();
@ -49,18 +49,18 @@ function StreamsPage() {
return (
<Flex p="2" gap="2" overflow="hidden" direction="column">
<Flex gap="2">
<PeopleListSelection maxW="sm" />
<Select maxW="sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
<Flex gap="2" wrap="wrap">
<PeopleListSelection w={["full", "xs"]} />
<Select w={["full", "xs"]} value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
<option value="live">Live</option>
<option value="ended">Ended</option>
</Select>
<RelaySelectionButton ml="auto" />
</Flex>
<IntersectionObserverProvider callback={callback}>
<SimpleGrid minChildWidth={["full", "20rem"]} spacing="2">
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
{streams.map((stream) => (
<StreamCard key={stream.event.id} stream={stream} maxW="lg" />
<StreamCard key={stream.event.id} stream={stream} />
))}
</SimpleGrid>
<TimelineActionAndStatus timeline={timeline} />

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useScroll } from "react-use";
import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text, useBreakpointValue } from "@chakra-ui/react";
import { useParams, Navigate, useSearchParams, useNavigate } from "react-router-dom";
import { nip19 } from "nostr-tools";
import { Global, css } from "@emotion/react";
@ -12,7 +12,6 @@ import { LiveVideoPlayer } from "../../../components/live-video-player";
import StreamChat, { ChatDisplayMode } from "./stream-chat";
import { UserAvatarLink } from "../../../components/user-avatar-link";
import { UserLink } from "../../../components/user-link";
import { useIsMobile } from "../../../hooks/use-is-mobile";
import StreamSummaryContent from "../components/stream-summary-content";
import { ArrowDownSIcon, ArrowUpSIcon, ExternalLinkIcon } from "../../../components/icons";
import useSetColorMode from "../../../hooks/use-set-color-mode";
@ -24,7 +23,7 @@ import RelaySelectionButton from "../../../components/relay-selection/relay-sele
import RelaySelectionProvider from "../../../providers/relay-selection-provider";
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
const isMobile = useIsMobile();
const vertical = useBreakpointValue({ base: true, lg: false });
const scrollBox = useRef<HTMLDivElement | null>(null);
const scrollState = useScroll(scrollBox);
const navigate = useNavigate();
@ -47,8 +46,8 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
return (
<ButtonGroup>
{isMobile && toggleButton}
{!isMobile && (
{vertical && toggleButton}
{!vertical && (
<CopyIconButton
text={location.href + "?displayMode=log&colorMode=dark"}
aria-label="Copy chat log URL"
@ -78,9 +77,9 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
h="full"
overflowX="hidden"
overflowY="auto"
direction={isMobile ? "column" : "row"}
p={isMobile || !!displayMode ? 0 : "2"}
gap={isMobile ? 0 : "4"}
direction={vertical ? "column" : "row"}
p={vertical || !!displayMode ? 0 : "2"}
gap={vertical ? 0 : "4"}
ref={scrollBox}
>
{displayMode && (
@ -93,14 +92,14 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
/>
)}
{!displayMode && (
<Flex gap={isMobile ? "2" : "4"} direction="column" flexGrow={isMobile ? 0 : 1}>
<Flex gap={vertical ? "2" : "4"} direction="column" flexGrow={vertical ? 0 : 1}>
<LiveVideoPlayer
stream={stream.streaming || stream.recording}
autoPlay={!!stream.streaming}
poster={stream.image}
maxH="100vh"
/>
<Flex gap={isMobile ? "2" : "4"} alignItems="center" p={isMobile ? "2" : 0}>
<Flex gap={vertical ? "2" : "4"} alignItems="center" p={vertical ? "2" : 0}>
<UserAvatarLink pubkey={stream.host} noProxy />
<Box>
<Heading size="md">
@ -113,15 +112,15 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
<RelaySelectionButton />
<Button onClick={() => navigate(-1)}>Back</Button>
</Flex>
<StreamSummaryContent stream={stream} px={isMobile ? "2" : 0} />
<StreamSummaryContent stream={stream} px={vertical ? "2" : 0} />
</Flex>
)}
<StreamChat
stream={stream}
flexGrow={1}
maxW={isMobile || !!displayMode ? undefined : "lg"}
maxW={vertical || !!displayMode ? undefined : "lg"}
maxH="100vh"
minH={isMobile ? "100vh" : undefined}
minH={vertical ? "100vh" : undefined}
flexShrink={0}
actions={renderActions()}
displayMode={displayMode}
@ -152,7 +151,7 @@ export default function StreamView() {
parsed.data.kind,
parsed.data.pubkey,
parsed.data.identifier,
true,
true
);
} catch (e) {
console.log(e);

View File

@ -71,7 +71,7 @@ export default function StreamChat({
const muteList = useUserMuteList(account?.pubkey);
const mutedPubkeys = useMemo(
() => [...(hostMuteList?.tags ?? []), ...(muteList?.tags ?? [])].filter(isPTag).map((t) => t[1] as string),
[hostMuteList, muteList],
[hostMuteList, muteList]
);
const eventFilter = useCallback((event: NostrEvent) => !mutedPubkeys.includes(event.pubkey), [mutedPubkeys]);
@ -82,7 +82,7 @@ export default function StreamChat({
"#a": [getATag(stream)],
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
},
{ eventFilter },
{ eventFilter }
);
const events = useSubject(timeline.timeline).sort((a, b) => b.created_at - a.created_at);
@ -153,7 +153,7 @@ export default function StreamChat({
<ChatMessage key={event.id} event={event} stream={stream} />
) : (
<ZapMessage key={event.id} zap={event} stream={stream} />
),
)
)}
</Flex>
{!isChatLog && (

View File

@ -1,4 +1,4 @@
import { Flex, Heading, IconButton, Spacer } from "@chakra-ui/react";
import { Flex, Heading, IconButton, Spacer, useBreakpointValue } from "@chakra-ui/react";
import { useNavigate, Link as RouterLink } from "react-router-dom";
import { ChatIcon, EditIcon } from "../../../components/icons";
import { UserAvatar } from "../../../components/user-avatar";
@ -8,7 +8,6 @@ import { UserTipButton } from "../../../components/user-tip-button";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import { useIsMobile } from "../../../hooks/use-is-mobile";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { UserProfileMenu } from "./user-profile-menu";
@ -19,7 +18,6 @@ export default function Header({
pubkey: string;
showRelaySelectionModal: () => void;
}) {
const isMobile = useIsMobile();
const navigate = useNavigate();
const metadata = useUserMetadata(pubkey);
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
@ -27,6 +25,8 @@ export default function Header({
const account = useCurrentAccount();
const isSelf = pubkey === account?.pubkey;
const showFullNip05 = useBreakpointValue({ base: false, md: true });
return (
<Flex direction="column" gap="2" px="2" pt="2">
<Flex gap="2" alignItems="center">
@ -34,7 +34,7 @@ export default function Header({
<Heading size="md" isTruncated>
{getUserDisplayName(metadata, pubkey)}
</Heading>
<UserDnsIdentityIcon pubkey={pubkey} onlyIcon={isMobile} />
<UserDnsIdentityIcon pubkey={pubkey} onlyIcon={showFullNip05} />
<Spacer />
{isSelf && (
<IconButton

View File

@ -47,7 +47,7 @@ export default function UserFollowersTab() {
return (
<IntersectionObserverProvider callback={callback}>
<SimpleGrid minChildWidth={["full", "4in"]} spacing="2" py="2">
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2" py="2">
{followers.map((event) => (
<FollowerItem key={event.pubkey} event={event} />
))}

View File

@ -17,7 +17,7 @@ export default function UserFollowingTab() {
if (!contacts) return <Spinner />;
return (
<SimpleGrid minChildWidth={["full", "4in"]} spacing="2" py="2">
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2" py="2">
{people.map((pubkey) => (
<UserCard key={pubkey} pubkey={pubkey} relay={contacts?.contactRelay[pubkey]} />
))}

View File

@ -25,7 +25,7 @@ export default function UserNotesTab() {
if (hideReposts && isRepost(event)) return false;
return timelineEventFilter(event);
},
[showReplies, hideReposts, timelineEventFilter],
[showReplies, hideReposts, timelineEventFilter]
);
const timeline = useTimelineLoader(
truncatedId(pubkey) + "-notes",
@ -34,7 +34,7 @@ export default function UserNotesTab() {
authors: [pubkey],
kinds: [Kind.Text, Kind.Repost, STREAM_KIND, 2],
},
{ eventFilter },
{ eventFilter }
);
const header = (

View File

@ -31,9 +31,9 @@ export default function UserStreamsTab() {
return (
<Flex p="2" gap="2" overflow="hidden" direction="column">
<IntersectionObserverProvider<string> callback={callback}>
<SimpleGrid minChildWidth="20rem" spacing="2">
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
{streams.map((stream) => (
<StreamCard key={stream.event.id} stream={stream} maxW="lg" />
<StreamCard key={stream.event.id} stream={stream} />
))}
</SimpleGrid>
<TimelineActionAndStatus timeline={timeline} />

View File

@ -87,14 +87,14 @@ const UserZapsTab = () => {
}
return true;
},
[filter],
[filter]
);
const timeline = useTimelineLoader(
`${truncatedId(pubkey)}-zaps`,
relays,
{ "#p": [pubkey], kinds: [9735] },
{ eventFilter },
{ eventFilter }
);
const events = useSubject(timeline.timeline);