mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-06-01 18:50:01 +02:00
cleanup
This commit is contained in:
parent
035faf1b5f
commit
1c759c0d20
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
|
@ -1,7 +1,7 @@
|
||||
describe("Profile view", () => {
|
||||
it("should load a rss feed profile", () => {
|
||||
cy.visit(
|
||||
"#/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un",
|
||||
"#/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un"
|
||||
);
|
||||
|
||||
cy.contains("fjsmu");
|
||||
|
@ -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")
|
||||
|
@ -1,7 +1,7 @@
|
||||
describe("Thread", () => {
|
||||
it("should handle quote notes with e tags correctly", () => {
|
||||
cy.visit(
|
||||
"#/n/nevent1qqsx2lnyuke6vmsrz9fdrd6uwjy0g0e9l6menfgdj5truugkh9qmkkgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqgc9md6",
|
||||
"#/n/nevent1qqsx2lnyuke6vmsrz9fdrd6uwjy0g0e9l6menfgdj5truugkh9qmkkgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqgc9md6"
|
||||
);
|
||||
|
||||
// find first note
|
||||
|
@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
@ -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",
|
||||
|
@ -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]);
|
||||
|
@ -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 (
|
||||
|
@ -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 />
|
||||
|
@ -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} />;
|
||||
});
|
||||
|
@ -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")}>
|
||||
|
@ -67,7 +67,7 @@ export default function PeopleListProvider({ children }: PropsWithChildren) {
|
||||
list,
|
||||
setList,
|
||||
}),
|
||||
[list, setList],
|
||||
[list, setList]
|
||||
);
|
||||
|
||||
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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(() => {
|
||||
|
@ -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";
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ export default function useAppSettings() {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
},
|
||||
[settings],
|
||||
[settings]
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -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);
|
||||
|
@ -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) || [];
|
||||
|
@ -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;
|
||||
}
|
@ -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 {
|
||||
|
@ -22,6 +22,6 @@ export function useTimelineCurserIntersectionCallback(timeline: TimelineLoader)
|
||||
}
|
||||
}
|
||||
},
|
||||
[timeline],
|
||||
[timeline]
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 };
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -23,5 +23,5 @@ const root = createRoot(element);
|
||||
root.render(
|
||||
<GlobalProviders>
|
||||
<App />
|
||||
</GlobalProviders>,
|
||||
</GlobalProviders>
|
||||
);
|
||||
|
@ -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 !== "")
|
||||
|
@ -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 --*/
|
||||
|
@ -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>;
|
||||
|
@ -20,7 +20,7 @@ export default function PostModalProvider({ children }: PropsWithChildren) {
|
||||
setDraft(draft);
|
||||
onOpen();
|
||||
},
|
||||
[setDraft, onOpen],
|
||||
[setDraft, onOpen]
|
||||
);
|
||||
const context = useMemo(() => ({ openModal }), [openModal]);
|
||||
|
||||
|
@ -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>;
|
||||
|
@ -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]);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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"]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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)));
|
||||
|
@ -32,7 +32,7 @@ class UserMetadataService {
|
||||
Kind.Metadata,
|
||||
pubkey,
|
||||
undefined,
|
||||
alwaysRequest,
|
||||
alwaysRequest
|
||||
);
|
||||
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
|
||||
return sub;
|
||||
|
@ -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]) {
|
||||
|
@ -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());
|
||||
|
@ -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 = (
|
||||
|
@ -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());
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
@ -206,7 +206,7 @@ export const ProfileEditView = () => {
|
||||
nip05: metadata?.nip05,
|
||||
lightningAddress: metadata?.lud16 || metadata?.lud06,
|
||||
}),
|
||||
[metadata],
|
||||
[metadata]
|
||||
);
|
||||
|
||||
const handleSubmit = async (data: FormData) => {
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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);
|
||||
|
@ -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 && (
|
||||
|
@ -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
|
||||
|
@ -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} />
|
||||
))}
|
||||
|
@ -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]} />
|
||||
))}
|
||||
|
@ -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 = (
|
||||
|
@ -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} />
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user