mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-06-26 16:52:55 +02:00
cleanup
This commit is contained in:
parent
035faf1b5f
commit
1c759c0d20
@ -1,6 +1,6 @@
|
|||||||
# noStrudel
|
# 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.
|
## 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)
|
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
|
## 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
|
git clone git@github.com:hzrd149/nostrudel.git
|
||||||
cd nostrudel
|
cd nostrudel
|
||||||
yarn install
|
yarn install
|
||||||
yarn start
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
@ -2,7 +2,7 @@ describe("Embeds", () => {
|
|||||||
describe("hashtags", () => {
|
describe("hashtags", () => {
|
||||||
it('should handle uppercase hashtags and ","', () => {
|
it('should handle uppercase hashtags and ","', () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsrj5ns6wva3fcghlyx0hp7hhajqtqk3kuckp7xhhscrm4jl7futegpz9mhxue69uhkummnw3e82efwvdhk6qgswaehxw309ahx7um5wgh8w6twv5pkpt8l",
|
"#/n/nevent1qqsrj5ns6wva3fcghlyx0hp7hhajqtqk3kuckp7xhhscrm4jl7futegpz9mhxue69uhkummnw3e82efwvdhk6qgswaehxw309ahx7um5wgh8w6twv5pkpt8l"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.findByRole("link", { name: "#Japan" }).should("be.visible");
|
cy.findByRole("link", { name: "#Japan" }).should("be.visible");
|
||||||
@ -15,18 +15,18 @@ describe("Embeds", () => {
|
|||||||
describe("links", () => {
|
describe("links", () => {
|
||||||
it("embed trustless.computer links", () => {
|
it("embed trustless.computer links", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsfn2mv3pe2v7jak4r5wnyengt36t0rx26w04hgysrmtpml8jnlk5cprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2qgawaehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2aq2wry06",
|
"#/n/nevent1qqsfn2mv3pe2v7jak4r5wnyengt36t0rx26w04hgysrmtpml8jnlk5cprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2qgawaehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2aq2wry06"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('[href="https://trustless.computer/"]').should("be.visible");
|
cy.get('[href="https://trustless.computer/"]').should("be.visible");
|
||||||
cy.get(
|
cy.get(
|
||||||
'[href="https://mempool.space/tx/461c6f56015c94d74837b68c9d08f4b80e7db7ca1e5ac4c53d9aa8c76b667672"]',
|
'[href="https://mempool.space/tx/461c6f56015c94d74837b68c9d08f4b80e7db7ca1e5ac4c53d9aa8c76b667672"]'
|
||||||
).should("be.visible");
|
).should("be.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("embeds links", () => {
|
it("embeds links", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpz9mhxue69uhkummnw3e82efwvdhk6qgjwaehxw309aex2mrp0yhxvdm69e5k7r3xlpe",
|
"#/n/nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpz9mhxue69uhkummnw3e82efwvdhk6qgjwaehxw309aex2mrp0yhxvdm69e5k7r3xlpe"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('[href="https://getalby.com/"]').should("exist");
|
cy.get('[href="https://getalby.com/"]').should("exist");
|
||||||
@ -38,11 +38,11 @@ describe("Embeds", () => {
|
|||||||
|
|
||||||
it("embeds simplex.chat links", () => {
|
it("embeds simplex.chat links", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsymds0vlpp4f5s0dckjf4qz283pdsen0rmx8lu7ct6hpnxag2hpacpremhxue69uhkummnw3ez6un9d3shjtnwda4k7arpwfhjucm0d5q3qamnwvaz7tmwdaehgu3wwa5kueghxyq76",
|
"#/n/nevent1qqsymds0vlpp4f5s0dckjf4qz283pdsen0rmx8lu7ct6hpnxag2hpacpremhxue69uhkummnw3ez6un9d3shjtnwda4k7arpwfhjucm0d5q3qamnwvaz7tmwdaehgu3wwa5kueghxyq76"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get(
|
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");
|
).should("be.visible");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -50,7 +50,7 @@ describe("Embeds", () => {
|
|||||||
describe("Nostr links", () => {
|
describe("Nostr links", () => {
|
||||||
it("should embed noub1...", () => {
|
it("should embed noub1...", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsd5yw7sntqfc4e7u4aempvgctry2plz653t9gpf97ctk5vc0ftskgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmdfxdj3a",
|
"#/n/nevent1qqsd5yw7sntqfc4e7u4aempvgctry2plz653t9gpf97ctk5vc0ftskgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmdfxdj3a"
|
||||||
);
|
);
|
||||||
cy.contains("Alby team");
|
cy.contains("Alby team");
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ describe("Embeds", () => {
|
|||||||
describe("youtube", () => {
|
describe("youtube", () => {
|
||||||
it("should embed playlists", () => {
|
it("should embed playlists", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqs8w6e63smpr5ccmz4l0w5pvnkp6r7z2fxaadjwu2g74y95pl9xv0cpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqqkgf54",
|
"#/n/nevent1qqs8w6e63smpr5ccmz4l0w5pvnkp6r7z2fxaadjwu2g74y95pl9xv0cpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqqkgf54"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.findByTitle(/youtube video player/i).should("be.visible");
|
cy.findByTitle(/youtube video player/i).should("be.visible");
|
||||||
@ -80,31 +80,31 @@ describe("Embeds", () => {
|
|||||||
describe("Music", () => {
|
describe("Music", () => {
|
||||||
it("should handle wavlake links", () => {
|
it("should handle wavlake links", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsve4ud5v8gjds2f2h7exlmjvhqayu4s520pge7frpwe22wezny0pcpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2mxs3z0",
|
"#/n/nevent1qqsve4ud5v8gjds2f2h7exlmjvhqayu4s520pge7frpwe22wezny0pcpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2mxs3z0"
|
||||||
);
|
);
|
||||||
cy.findByTitle("Wavlake Embed").should("be.visible");
|
cy.findByTitle("Wavlake Embed").should("be.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle spotify links", () => {
|
it("should handle spotify links", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsx0lz7m72qzq499exwhnfszvgwea8tv38x9wkv32yhkmwwmhgs7jgprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk25m3sln",
|
"#/n/nevent1qqsx0lz7m72qzq499exwhnfszvgwea8tv38x9wkv32yhkmwwmhgs7jgprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk25m3sln"
|
||||||
);
|
);
|
||||||
cy.findByTitle("Spotify List Embed").should("exist");
|
cy.findByTitle("Spotify List Embed").should("exist");
|
||||||
|
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsqxkmz49hydf8ppa9k6x6zrcq7m4evhhlye0j3lcnz8hrl2q6np4spz3mhxue69uhhyetvv9ujuerpd46hxtnfdult02qz",
|
"#/n/nevent1qqsqxkmz49hydf8ppa9k6x6zrcq7m4evhhlye0j3lcnz8hrl2q6np4spz3mhxue69uhhyetvv9ujuerpd46hxtnfdult02qz"
|
||||||
);
|
);
|
||||||
cy.findByTitle("Spotify Embed").should("exist");
|
cy.findByTitle("Spotify Embed").should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle apple music links", () => {
|
it("should handle apple music links", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqs9kqt9d7r4zjpawcyl82x5qsn4hals4wn294dv95knrahs4mggwasprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2whhzvz",
|
"#/n/nevent1qqs9kqt9d7r4zjpawcyl82x5qsn4hals4wn294dv95knrahs4mggwasprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2whhzvz"
|
||||||
);
|
);
|
||||||
cy.findByTitle("Apple Music Embed").should("exist");
|
cy.findByTitle("Apple Music Embed").should("exist");
|
||||||
|
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqszyrz4uug75j4086kj4f8peg3g0v8g9f04zjxplnpq0uxljtthggqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2aeexmq",
|
"#/n/nevent1qqszyrz4uug75j4086kj4f8peg3g0v8g9f04zjxplnpq0uxljtthggqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2aeexmq"
|
||||||
);
|
);
|
||||||
cy.findByTitle("Apple Music List Embed").should("exist");
|
cy.findByTitle("Apple Music List Embed").should("exist");
|
||||||
});
|
});
|
||||||
@ -118,7 +118,7 @@ describe("Embeds", () => {
|
|||||||
describe("Emoji", () => {
|
describe("Emoji", () => {
|
||||||
it("should embed emojis", () => {
|
it("should embed emojis", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsdj7k47uh4z0ypl2m29lvd4ar9zpf6dcy7ls0q6g6qctnxfj5n3pcpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqdyqlpq",
|
"#/n/nevent1qqsdj7k47uh4z0ypl2m29lvd4ar9zpf6dcy7ls0q6g6qctnxfj5n3pcpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqdyqlpq"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.findByRole("img", { name: /pepeD/i }).should("be.visible");
|
cy.findByRole("img", { name: /pepeD/i }).should("be.visible");
|
||||||
|
@ -50,7 +50,7 @@ describe("Login view", () => {
|
|||||||
|
|
||||||
it("should redirect after login", () => {
|
it("should redirect after login", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqs88gdxv36qsjfwr66k7wxuq9r2tg8rsdcnfkcqdg4sc6vlnsma98qpzpmhxue69uhkummnw3ezuamfdejsz9rhwden5te0wfjkccte9ejxzmt4wvhxjmccew89d",
|
"#/n/nevent1qqs88gdxv36qsjfwr66k7wxuq9r2tg8rsdcnfkcqdg4sc6vlnsma98qpzpmhxue69uhkummnw3ezuamfdejsz9rhwden5te0wfjkccte9ejxzmt4wvhxjmccew89d"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.findByRole("link", { name: /login/i }).click();
|
cy.findByRole("link", { name: /login/i }).click();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
describe("Profile view", () => {
|
describe("Profile view", () => {
|
||||||
it("should load a rss feed profile", () => {
|
it("should load a rss feed profile", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un",
|
"#/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.contains("fjsmu");
|
cy.contains("fjsmu");
|
||||||
|
@ -2,7 +2,7 @@ describe("No account", () => {
|
|||||||
describe("note view", () => {
|
describe("note view", () => {
|
||||||
it("should fetch and render note", () => {
|
it("should fetch and render note", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqs84hwdlls703w4yf66qsszxjqfc0xselfxrzr6n4qp40vzdnczragpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5jcwczn",
|
"#/n/nevent1qqs84hwdlls703w4yf66qsszxjqfc0xselfxrzr6n4qp40vzdnczragpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5jcwczn"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get(".chakra-card")
|
cy.get(".chakra-card")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
describe("Thread", () => {
|
describe("Thread", () => {
|
||||||
it("should handle quote notes with e tags correctly", () => {
|
it("should handle quote notes with e tags correctly", () => {
|
||||||
cy.visit(
|
cy.visit(
|
||||||
"#/n/nevent1qqsx2lnyuke6vmsrz9fdrd6uwjy0g0e9l6menfgdj5truugkh9qmkkgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqgc9md6",
|
"#/n/nevent1qqsx2lnyuke6vmsrz9fdrd6uwjy0g0e9l6menfgdj5truugkh9qmkkgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqgc9md6"
|
||||||
);
|
);
|
||||||
|
|
||||||
// find first note
|
// find first note
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite serve",
|
"start": "vite serve",
|
||||||
|
"dev": "vite serve",
|
||||||
"build": "tsc --project tsconfig.json && vite build",
|
"build": "tsc --project tsconfig.json && vite build",
|
||||||
"format": "prettier --ignore-path .prettierignore -w .",
|
"format": "prettier --ignore-path .prettierignore -w .",
|
||||||
"e2e": "cypress open",
|
"e2e": "cypress open",
|
||||||
|
@ -21,7 +21,6 @@ import {
|
|||||||
import relayPoolService from "../services/relay-pool";
|
import relayPoolService from "../services/relay-pool";
|
||||||
import { useInterval } from "react-use";
|
import { useInterval } from "react-use";
|
||||||
import { RelayStatus } from "./relay-status";
|
import { RelayStatus } from "./relay-status";
|
||||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
|
||||||
import { RelayIcon } from "./icons";
|
import { RelayIcon } from "./icons";
|
||||||
import { Relay } from "../classes/relay";
|
import { Relay } from "../classes/relay";
|
||||||
import { RelayFavicon } from "./relay-favicon";
|
import { RelayFavicon } from "./relay-favicon";
|
||||||
@ -29,7 +28,6 @@ import relayScoreboardService from "../services/relay-scoreboard";
|
|||||||
import { RelayScoreBreakdown } from "./relay-score-breakdown";
|
import { RelayScoreBreakdown } from "./relay-score-breakdown";
|
||||||
|
|
||||||
export const ConnectedRelays = () => {
|
export const ConnectedRelays = () => {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [relays, setRelays] = useState<Relay[]>(relayPoolService.getRelays());
|
const [relays, setRelays] = useState<Relay[]>(relayPoolService.getRelays());
|
||||||
const sortedRelays = useMemo(() => relayScoreboardService.getRankedRelays(relays.map((r) => r.url)), [relays]);
|
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,
|
regexp: /:([a-zA-Z0-9_]+):/i,
|
||||||
render: (match) => {
|
render: (match) => {
|
||||||
const emojiTag = note.tags.find(
|
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) {
|
if (emojiTag) {
|
||||||
return (
|
return (
|
||||||
|
@ -53,7 +53,7 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
|
|||||||
<ExpandProvider>
|
<ExpandProvider>
|
||||||
<Card variant={variant} ref={ref} data-event-id={event.id}>
|
<Card variant={variant} ref={ref} data-event-id={event.id}>
|
||||||
<CardHeader padding="2">
|
<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"]} />
|
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />
|
||||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||||
|
@ -3,16 +3,16 @@ import { getEventRelays } from "../../services/event-relays";
|
|||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { RelayIconStack } from "../relay-icon-stack";
|
import { RelayIconStack } from "../relay-icon-stack";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
import { getEventUID } from "../../helpers/nostr/event";
|
import { getEventUID } from "../../helpers/nostr/event";
|
||||||
|
import { useBreakpointValue } from "@chakra-ui/react";
|
||||||
|
|
||||||
export type NoteRelaysProps = {
|
export type NoteRelaysProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventRelays = memo(({ event }: NoteRelaysProps) => {
|
export const EventRelays = memo(({ event }: NoteRelaysProps) => {
|
||||||
const isMobile = useIsMobile();
|
const maxRelays = useBreakpointValue({ base: 3, md: undefined });
|
||||||
const eventRelays = useSubject(getEventRelays(getEventUID(event)));
|
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 { UserLink } from "../user-link";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
|
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
|
||||||
import { ParsedZap, parseZapEvent } from "../../helpers/zaps";
|
import { ParsedZap } from "../../helpers/zaps";
|
||||||
import { readablizeSats } from "../../helpers/bolt11";
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
import useEventReactions from "../../hooks/use-event-reactions";
|
import useEventReactions from "../../hooks/use-event-reactions";
|
||||||
import useEventZaps from "../../hooks/use-event-zaps";
|
import useEventZaps from "../../hooks/use-event-zaps";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
|
|
||||||
function getReactionIcon(content: string) {
|
function getReactionIcon(content: string) {
|
||||||
switch (content) {
|
switch (content) {
|
||||||
@ -48,12 +47,10 @@ const ReactionEvent = React.memo(({ event }: { event: NostrEvent }) => (
|
|||||||
));
|
));
|
||||||
|
|
||||||
const ZapEvent = React.memo(({ zap }: { zap: ParsedZap }) => {
|
const ZapEvent = React.memo(({ zap }: { zap: ParsedZap }) => {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
if (!zap.payment.amount) return null;
|
if (!zap.payment.amount) return null;
|
||||||
|
|
||||||
return (
|
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">
|
<Flex gap="2" justifyContent="space-between">
|
||||||
<Box>
|
<Box>
|
||||||
<UserAvatarLink pubkey={zap.request.pubkey} size="xs" mr="2" />
|
<UserAvatarLink pubkey={zap.request.pubkey} size="xs" mr="2" />
|
||||||
@ -80,14 +77,13 @@ export default function NoteReactionsModal({
|
|||||||
const zaps = useEventZaps(noteId, [], true) ?? [];
|
const zaps = useEventZaps(noteId, [], true) ?? [];
|
||||||
const reactions = useEventReactions(noteId, [], true) ?? [];
|
const reactions = useEventReactions(noteId, [], true) ?? [];
|
||||||
const [selected, setSelected] = useState("zaps");
|
const [selected, setSelected] = useState("zaps");
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody p={isMobile ? "2" : "4"}>
|
<ModalBody p={["2", "4"]}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button size="sm" variant={selected === "zaps" ? "solid" : "outline"} onClick={() => setSelected("zaps")}>
|
<Button size="sm" variant={selected === "zaps" ? "solid" : "outline"} onClick={() => setSelected("zaps")}>
|
||||||
|
@ -67,7 +67,7 @@ export default function PeopleListProvider({ children }: PropsWithChildren) {
|
|||||||
list,
|
list,
|
||||||
setList,
|
setList,
|
||||||
}),
|
}),
|
||||||
[list, setList],
|
[list, setList]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
|
return <PeopleListContext.Provider value={context}>{children}</PeopleListContext.Provider>;
|
||||||
|
@ -20,7 +20,6 @@ import { normalizeToHex } from "../../helpers/nip19";
|
|||||||
import { getReferences } from "../../helpers/nostr/event";
|
import { getReferences } from "../../helpers/nostr/event";
|
||||||
import { matchHashtag, mentionNpubOrNote } from "../../helpers/regexp";
|
import { matchHashtag, mentionNpubOrNote } from "../../helpers/regexp";
|
||||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
import { useSigningContext } from "../../providers/signing-provider";
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||||
import { ImageIcon } from "../icons";
|
import { ImageIcon } from "../icons";
|
||||||
@ -79,7 +78,6 @@ type PostModalProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) => {
|
export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) => {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const writeRelays = useWriteRelayUrls();
|
const writeRelays = useWriteRelayUrls();
|
||||||
@ -98,7 +96,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
|||||||
const payload = new FormData();
|
const payload = new FormData();
|
||||||
payload.append("fileToUpload", imageFile);
|
payload.append("fileToUpload", imageFile);
|
||||||
const response = await fetch("https://nostr.build/upload.php", { body: payload, method: "POST" }).then((res) =>
|
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];
|
const imageUrl = response.match(/https:\/\/nostr\.build\/i\/[\w.]+/)?.[0];
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
@ -193,7 +191,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
|||||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl" closeOnOverlayClick={false}>
|
<Modal isOpen={isOpen} onClose={onClose} size="4xl" closeOnOverlayClick={false}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalBody padding={isMobile ? "2" : "4"}>{renderContent()}</ModalBody>
|
<ModalBody padding={["2", "2", "4"]}>{renderContent()}</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -28,7 +28,7 @@ function RelayPickerModal({
|
|||||||
}: { onSelect: (relay: string) => void } & Omit<ModalProps, "children">) {
|
}: { onSelect: (relay: string) => void } & Omit<ModalProps, "children">) {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const { value: onlineRelays } = useAsync(async () =>
|
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 ?? []);
|
const relayList = unique(onlineRelays ?? []);
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ export const RelayUrlInput = forwardRef(
|
|||||||
({ onChange, ...props }: Omit<RelayUrlInputProps, "onChange"> & { onChange: (url: string) => void }, ref) => {
|
({ onChange, ...props }: Omit<RelayUrlInputProps, "onChange"> & { onChange: (url: string) => void }, ref) => {
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
const { value: relaysJson } = useAsync(async () =>
|
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 ?? []);
|
const relaySuggestions = unique(relaysJson ?? []);
|
||||||
|
|
||||||
@ -100,5 +100,5 @@ export const RelayUrlInput = forwardRef(
|
|||||||
<RelayPickerModal onClose={onClose} isOpen={isOpen} onSelect={(url) => onChange(url)} size="2xl" />
|
<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 {
|
||||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertIcon,
|
||||||
|
AlertProps,
|
||||||
|
AlertTitle,
|
||||||
|
Button,
|
||||||
|
Spacer,
|
||||||
|
useBreakpointValue,
|
||||||
|
useModal,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { useExpand } from "./note/expanded";
|
import { useExpand } from "./note/expanded";
|
||||||
|
|
||||||
export default function SensitiveContentWarning({ description }: { description: string } & AlertProps) {
|
export default function SensitiveContentWarning({ description }: { description: string } & AlertProps) {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const expand = useExpand();
|
const expand = useExpand();
|
||||||
|
const smallScreen = useBreakpointValue({ base: true, md: false });
|
||||||
|
|
||||||
if (isMobile) {
|
if (smallScreen) {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
status="warning"
|
status="warning"
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback } from "react";
|
||||||
import { Flex, Grid, SimpleGrid } from "@chakra-ui/react";
|
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import GenericNoteTimeline from "./generic-note-timeline";
|
import GenericNoteTimeline from "./generic-note-timeline";
|
||||||
import { ImageGalleryProvider } from "../image-gallery";
|
import { ImageGalleryProvider } from "../image-gallery";
|
||||||
import MediaTimeline from "./media-timeline";
|
import MediaTimeline from "./media-timeline";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
import { TimelineLoader } from "../../classes/timeline-loader";
|
import { TimelineLoader } from "../../classes/timeline-loader";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import TimelineActionAndStatus from "./timeline-action-and-status";
|
import TimelineActionAndStatus from "./timeline-action-and-status";
|
||||||
@ -21,7 +20,7 @@ export function useTimelinePageEventFilter() {
|
|||||||
if (view === "images" && !event.content.match(matchImageUrls)) return false;
|
if (view === "images" && !event.content.match(matchImageUrls)) return false;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[view],
|
[view]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ export default function TimelinePage({ timeline, header }: { timeline: TimelineL
|
|||||||
case "images":
|
case "images":
|
||||||
return (
|
return (
|
||||||
<ImageGalleryProvider>
|
<ImageGalleryProvider>
|
||||||
<SimpleGrid minChildWidth={["full", "15rem"]} gap="4">
|
<SimpleGrid columns={[1, 2, 2, 3, 4, 5]} gap="4">
|
||||||
<MediaTimeline timeline={timeline} />
|
<MediaTimeline timeline={timeline} />
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</ImageGalleryProvider>
|
</ImageGalleryProvider>
|
||||||
|
@ -4,7 +4,6 @@ import useSubject from "../../../hooks/use-subject";
|
|||||||
import { matchImageUrls } from "../../../helpers/regexp";
|
import { matchImageUrls } from "../../../helpers/regexp";
|
||||||
import { ImageGalleryLink, ImageGalleryProvider } from "../../image-gallery";
|
import { ImageGalleryLink, ImageGalleryProvider } from "../../image-gallery";
|
||||||
import { Box, IconButton } from "@chakra-ui/react";
|
import { Box, IconButton } from "@chakra-ui/react";
|
||||||
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import { getSharableNoteId } from "../../../helpers/nip19";
|
import { getSharableNoteId } from "../../../helpers/nip19";
|
||||||
@ -42,7 +41,6 @@ const ImagePreview = React.memo(({ image }: { image: ImagePreview }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function MediaTimeline({ timeline }: { timeline: TimelineLoader }) {
|
export default function MediaTimeline({ timeline }: { timeline: TimelineLoader }) {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline.timeline);
|
||||||
|
|
||||||
const images = useMemo(() => {
|
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 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";
|
status = "ended";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ export default function useAppSettings() {
|
|||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[settings],
|
[settings]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -8,7 +8,7 @@ export default function useEventReactions(eventId: string, additionalRelays: str
|
|||||||
|
|
||||||
const subject = useMemo(
|
const subject = useMemo(
|
||||||
() => eventReactionsService.requestReactions(eventId, relays, alwaysFetch),
|
() => eventReactionsService.requestReactions(eventId, relays, alwaysFetch),
|
||||||
[eventId, relays.join("|"), alwaysFetch],
|
[eventId, relays.join("|"), alwaysFetch]
|
||||||
);
|
);
|
||||||
|
|
||||||
return useSubject(subject);
|
return useSubject(subject);
|
||||||
|
@ -9,7 +9,7 @@ export default function useEventZaps(eventId: string, additionalRelays: string[]
|
|||||||
|
|
||||||
const subject = useMemo(
|
const subject = useMemo(
|
||||||
() => eventZapsService.requestZaps(eventId, relays, alwaysFetch),
|
() => eventZapsService.requestZaps(eventId, relays, alwaysFetch),
|
||||||
[eventId, relays.join("|"), alwaysFetch],
|
[eventId, relays.join("|"), alwaysFetch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const events = useSubject(subject) || [];
|
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 previous = useCallback(() => setPage((v) => Math.max(v - 1, 0)), [setPage]);
|
||||||
const pageItems = useMemo(
|
const pageItems = useMemo(
|
||||||
() => list.slice(pageSize * currentPage, pageSize * currentPage + pageSize),
|
() => list.slice(pageSize * currentPage, pageSize * currentPage + pageSize),
|
||||||
[list, currentPage, pageSize],
|
[list, currentPage, pageSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
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) {
|
export function useUserContacts(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||||
const observable = useMemo(
|
const observable = useMemo(
|
||||||
() => userContactsService.requestContacts(pubkey, relays, alwaysRequest),
|
() => userContactsService.requestContacts(pubkey, relays, alwaysRequest),
|
||||||
[pubkey, relays.join("|"), alwaysRequest],
|
[pubkey, relays.join("|"), alwaysRequest]
|
||||||
);
|
);
|
||||||
const contacts = useSubject(observable);
|
const contacts = useSubject(observable);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ export default function useUserLNURLMetadata(pubkey: string) {
|
|||||||
const address = userMetadata?.lud16 || userMetadata?.lud06;
|
const address = userMetadata?.lud16 || userMetadata?.lud06;
|
||||||
const { value: metadata } = useAsync(
|
const { value: metadata } = useAsync(
|
||||||
async () => (address ? lnurlMetadataService.requestMetadata(address) : undefined),
|
async () => (address ? lnurlMetadataService.requestMetadata(address) : undefined),
|
||||||
[address],
|
[address]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { metadata, address };
|
return { metadata, address };
|
||||||
|
@ -8,7 +8,7 @@ export function useUserMetadata(pubkey: string, additionalRelays: string[] = [],
|
|||||||
|
|
||||||
const subject = useMemo(
|
const subject = useMemo(
|
||||||
() => userMetadataService.requestMetadata(pubkey, relays, alwaysRequest),
|
() => userMetadataService.requestMetadata(pubkey, relays, alwaysRequest),
|
||||||
[pubkey, relays, alwaysRequest],
|
[pubkey, relays, alwaysRequest]
|
||||||
);
|
);
|
||||||
const metadata = useSubject(subject);
|
const metadata = useSubject(subject);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ export function useUserRelays(pubkey: string, additionalRelays: string[] = [], a
|
|||||||
const relays = useReadRelayUrls(additionalRelays);
|
const relays = useReadRelayUrls(additionalRelays);
|
||||||
const subject = useMemo(
|
const subject = useMemo(
|
||||||
() => userRelaysService.requestRelays(pubkey, relays, alwaysRequest),
|
() => userRelaysService.requestRelays(pubkey, relays, alwaysRequest),
|
||||||
[pubkey, relays.join("|"), alwaysRequest],
|
[pubkey, relays.join("|"), alwaysRequest]
|
||||||
);
|
);
|
||||||
const userRelays = useSubject(subject);
|
const userRelays = useSubject(subject);
|
||||||
|
|
||||||
|
@ -23,5 +23,5 @@ const root = createRoot(element);
|
|||||||
root.render(
|
root.render(
|
||||||
<GlobalProviders>
|
<GlobalProviders>
|
||||||
<App />
|
<App />
|
||||||
</GlobalProviders>,
|
</GlobalProviders>
|
||||||
);
|
);
|
||||||
|
@ -38,7 +38,7 @@ const mediaMapper = (item: ImageObject[] | VideoObject[]) => ({
|
|||||||
|
|
||||||
const mediaSorter = (
|
const mediaSorter = (
|
||||||
a: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject,
|
a: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject,
|
||||||
b: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject,
|
b: ImageObject | TwitterImageObject | VideoObject | TwitterPlayerObject
|
||||||
) => {
|
) => {
|
||||||
if (!(a.url && b.url)) {
|
if (!(a.url && b.url)) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -114,7 +114,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
|
|||||||
ogObject.ogImageProperty,
|
ogObject.ogImageProperty,
|
||||||
ogObject.ogImageWidth,
|
ogObject.ogImageWidth,
|
||||||
ogObject.ogImageHeight,
|
ogObject.ogImageHeight,
|
||||||
ogObject.ogImageType,
|
ogObject.ogImageType
|
||||||
)
|
)
|
||||||
.map(mediaMapper)
|
.map(mediaMapper)
|
||||||
.filter((value: ImageObject) => value.url !== undefined && value.url !== "")
|
.filter((value: ImageObject) => value.url !== undefined && value.url !== "")
|
||||||
@ -134,7 +134,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
|
|||||||
ogObject.ogVideoProperty,
|
ogObject.ogVideoProperty,
|
||||||
ogObject.ogVideoWidth,
|
ogObject.ogVideoWidth,
|
||||||
ogObject.ogVideoHeight,
|
ogObject.ogVideoHeight,
|
||||||
ogObject.ogVideoType,
|
ogObject.ogVideoType
|
||||||
)
|
)
|
||||||
.map(mediaMapper)
|
.map(mediaMapper)
|
||||||
.filter((value: VideoObject) => value.url !== undefined && value.url !== "")
|
.filter((value: VideoObject) => value.url !== undefined && value.url !== "")
|
||||||
@ -164,7 +164,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
|
|||||||
ogObject.twitterImageProperty,
|
ogObject.twitterImageProperty,
|
||||||
ogObject.twitterImageWidth,
|
ogObject.twitterImageWidth,
|
||||||
ogObject.twitterImageHeight,
|
ogObject.twitterImageHeight,
|
||||||
ogObject.twitterImageAlt,
|
ogObject.twitterImageAlt
|
||||||
)
|
)
|
||||||
.map(mediaMapperTwitterImage)
|
.map(mediaMapperTwitterImage)
|
||||||
.filter((value: TwitterImageObject) => value.url !== undefined && value.url !== "")
|
.filter((value: TwitterImageObject) => value.url !== undefined && value.url !== "")
|
||||||
@ -189,7 +189,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
|
|||||||
ogObject.twitterPlayerProperty,
|
ogObject.twitterPlayerProperty,
|
||||||
ogObject.twitterPlayerWidth,
|
ogObject.twitterPlayerWidth,
|
||||||
ogObject.twitterPlayerHeight,
|
ogObject.twitterPlayerHeight,
|
||||||
ogObject.twitterPlayerStream,
|
ogObject.twitterPlayerStream
|
||||||
)
|
)
|
||||||
.map(mediaMapperTwitterPlayer)
|
.map(mediaMapperTwitterPlayer)
|
||||||
.filter((value: TwitterPlayerObject) => value.url !== undefined && value.url !== "")
|
.filter((value: TwitterPlayerObject) => value.url !== undefined && value.url !== "")
|
||||||
|
@ -83,7 +83,7 @@ export class QrCode {
|
|||||||
minVersion: int = 1,
|
minVersion: int = 1,
|
||||||
maxVersion: int = 40,
|
maxVersion: int = 40,
|
||||||
mask: int = -1,
|
mask: int = -1,
|
||||||
boostEcl: boolean = true,
|
boostEcl: boolean = true
|
||||||
): QrCode {
|
): QrCode {
|
||||||
if (
|
if (
|
||||||
!(QrCode.MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= QrCode.MAX_VERSION) ||
|
!(QrCode.MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= QrCode.MAX_VERSION) ||
|
||||||
@ -175,7 +175,7 @@ export class QrCode {
|
|||||||
|
|
||||||
dataCodewords: Readonly<Array<byte>>,
|
dataCodewords: Readonly<Array<byte>>,
|
||||||
|
|
||||||
msk: int,
|
msk: int
|
||||||
) {
|
) {
|
||||||
// Check scalar arguments
|
// Check scalar arguments
|
||||||
if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
|
if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
|
||||||
@ -822,7 +822,7 @@ export class QrSegment {
|
|||||||
public readonly numChars: int,
|
public readonly numChars: int,
|
||||||
|
|
||||||
// The data bits of this segment. Accessed through getData().
|
// 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");
|
if (numChars < 0) throw new RangeError("Invalid argument");
|
||||||
this.bitData = bitData.slice(); // Make defensive copy
|
this.bitData = bitData.slice(); // Make defensive copy
|
||||||
@ -897,7 +897,7 @@ export class Ecc {
|
|||||||
// In the range 0 to 3 (unsigned 2-bit integer).
|
// In the range 0 to 3 (unsigned 2-bit integer).
|
||||||
public readonly ordinal: int,
|
public readonly ordinal: int,
|
||||||
// (Package-private) In the range 0 to 3 (unsigned 2-bit integer).
|
// (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).
|
// The mode indicator bits, which is a uint4 value (range 0 to 15).
|
||||||
public readonly modeBits: int,
|
public readonly modeBits: int,
|
||||||
// Number of character count bits for three different version ranges.
|
// Number of character count bits for three different version ranges.
|
||||||
private readonly numBitsCharCount: [int, int, int],
|
private readonly numBitsCharCount: [int, int, int]
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/*-- Method --*/
|
/*-- Method --*/
|
||||||
|
@ -19,7 +19,7 @@ const IntersectionObserverContext = createContext<{
|
|||||||
export type ExtendedIntersectionObserverEntry<T> = { entry: IntersectionObserverEntry; id: T | undefined };
|
export type ExtendedIntersectionObserverEntry<T> = { entry: IntersectionObserverEntry; id: T | undefined };
|
||||||
export type ExtendedIntersectionObserverCallback<T> = (
|
export type ExtendedIntersectionObserverCallback<T> = (
|
||||||
entries: ExtendedIntersectionObserverEntry<T>[],
|
entries: ExtendedIntersectionObserverEntry<T>[],
|
||||||
observer: IntersectionObserver,
|
observer: IntersectionObserver
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export function useIntersectionObserver() {
|
export function useIntersectionObserver() {
|
||||||
@ -42,7 +42,7 @@ export function useRegisterIntersectionEntity<T>(ref: MutableRefObject<Element |
|
|||||||
|
|
||||||
export function useIntersectionMapCallback<T>(
|
export function useIntersectionMapCallback<T>(
|
||||||
callback: (map: Map<T, IntersectionObserverEntry>) => void,
|
callback: (map: Map<T, IntersectionObserverEntry>) => void,
|
||||||
watch: DependencyList,
|
watch: DependencyList
|
||||||
) {
|
) {
|
||||||
const map = useMemo(() => new Map<T, IntersectionObserverEntry>(), []);
|
const map = useMemo(() => new Map<T, IntersectionObserverEntry>(), []);
|
||||||
return useCallback<ExtendedIntersectionObserverCallback<T>>(
|
return useCallback<ExtendedIntersectionObserverCallback<T>>(
|
||||||
@ -53,7 +53,7 @@ export function useIntersectionMapCallback<T>(
|
|||||||
|
|
||||||
callback(map);
|
callback(map);
|
||||||
},
|
},
|
||||||
[callback, ...watch],
|
[callback, ...watch]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,11 +76,11 @@ export default function IntersectionObserverProvider<T = undefined>({
|
|||||||
entries.map((entry) => {
|
entries.map((entry) => {
|
||||||
return { entry, id: elementIds.get(entry.target) };
|
return { entry, id: elementIds.get(entry.target) };
|
||||||
}),
|
}),
|
||||||
observer,
|
observer
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
const [observer, setObserver] = useState<IntersectionObserver>(
|
const [observer, setObserver] = useState<IntersectionObserver>(
|
||||||
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold }),
|
() => new IntersectionObserver(handleIntersection, { rootMargin, threshold })
|
||||||
);
|
);
|
||||||
|
|
||||||
useMount(() => {
|
useMount(() => {
|
||||||
@ -97,7 +97,7 @@ export default function IntersectionObserverProvider<T = undefined>({
|
|||||||
(element: Element, id: T) => {
|
(element: Element, id: T) => {
|
||||||
elementIds.set(element, id);
|
elementIds.set(element, id);
|
||||||
},
|
},
|
||||||
[elementIds],
|
[elementIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
@ -105,7 +105,7 @@ export default function IntersectionObserverProvider<T = undefined>({
|
|||||||
observer,
|
observer,
|
||||||
setElementId,
|
setElementId,
|
||||||
}),
|
}),
|
||||||
[observer, setElementId],
|
[observer, setElementId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <IntersectionObserverContext.Provider value={context}>{children}</IntersectionObserverContext.Provider>;
|
return <IntersectionObserverContext.Provider value={context}>{children}</IntersectionObserverContext.Provider>;
|
||||||
|
@ -20,7 +20,7 @@ export default function PostModalProvider({ children }: PropsWithChildren) {
|
|||||||
setDraft(draft);
|
setDraft(draft);
|
||||||
onOpen();
|
onOpen();
|
||||||
},
|
},
|
||||||
[setDraft, onOpen],
|
[setDraft, onOpen]
|
||||||
);
|
);
|
||||||
const context = useMemo(() => ({ openModal }), [openModal]);
|
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" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toast, current],
|
[toast, current]
|
||||||
);
|
);
|
||||||
const requestDecrypt = useCallback(
|
const requestDecrypt = useCallback(
|
||||||
async (data: string, pubkey: string) => {
|
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" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toast, current],
|
[toast, current]
|
||||||
);
|
);
|
||||||
const requestEncrypt = useCallback(
|
const requestEncrypt = useCallback(
|
||||||
async (data: string, pubkey: string) => {
|
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" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[toast, current],
|
[toast, current]
|
||||||
);
|
);
|
||||||
|
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => ({ requestSignature, requestDecrypt, requestEncrypt }),
|
() => ({ requestSignature, requestDecrypt, requestEncrypt }),
|
||||||
[requestSignature, requestDecrypt, requestEncrypt],
|
[requestSignature, requestDecrypt, requestEncrypt]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <SigningContext.Provider value={context}>{children}</SigningContext.Provider>;
|
return <SigningContext.Provider value={context}>{children}</SigningContext.Provider>;
|
||||||
|
@ -21,7 +21,7 @@ function handleNewContacts(contacts: UserContacts | undefined) {
|
|||||||
const relay = contacts.contactRelay[key];
|
const relay = contacts.contactRelay[key];
|
||||||
if (relay) return ["p", key, relay];
|
if (relay) return ["p", key, relay];
|
||||||
else return ["p", key];
|
else return ["p", key];
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// reset the pending list since we just got a new contacts list
|
// 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 newTag;
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
following.next([...pTags, newTag]);
|
following.next([...pTags, newTag]);
|
||||||
|
@ -23,14 +23,14 @@ class DirectMessagesService {
|
|||||||
this.incomingSub = new NostrMultiSubscription(
|
this.incomingSub = new NostrMultiSubscription(
|
||||||
clientRelaysService.getReadUrls(),
|
clientRelaysService.getReadUrls(),
|
||||||
undefined,
|
undefined,
|
||||||
"incoming-direct-messages",
|
"incoming-direct-messages"
|
||||||
);
|
);
|
||||||
this.incomingSub.onEvent.subscribe(this.receiveEvent, this);
|
this.incomingSub.onEvent.subscribe(this.receiveEvent, this);
|
||||||
|
|
||||||
this.outgoingSub = new NostrMultiSubscription(
|
this.outgoingSub = new NostrMultiSubscription(
|
||||||
clientRelaysService.getReadUrls(),
|
clientRelaysService.getReadUrls(),
|
||||||
undefined,
|
undefined,
|
||||||
"outgoing-direct-messages",
|
"outgoing-direct-messages"
|
||||||
);
|
);
|
||||||
this.outgoingSub.onEvent.subscribe(this.receiveEvent, this);
|
this.outgoingSub.onEvent.subscribe(this.receiveEvent, this);
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ function getIdentityFromJson(name: string, domain: string, json: IdentityJson):
|
|||||||
|
|
||||||
async function fetchAllIdentities(domain: string) {
|
async function fetchAllIdentities(domain: string) {
|
||||||
const json = await fetchWithCorsFallback(`//${domain}/.well-known/nostr.json`).then(
|
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);
|
await addToCache(domain, json);
|
||||||
@ -101,7 +101,7 @@ async function pruneCache() {
|
|||||||
const keys = await db.getAllKeysFromIndex(
|
const keys = await db.getAllKeysFromIndex(
|
||||||
"dnsIdentifiers",
|
"dnsIdentifiers",
|
||||||
"updated",
|
"updated",
|
||||||
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix()),
|
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix())
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const pubkey of keys) {
|
for (const pubkey of keys) {
|
||||||
|
@ -86,7 +86,7 @@ class PubkeyRelayAssignmentService {
|
|||||||
if (userRelays.length === 0) userRelays = Array.from(readRelays);
|
if (userRelays.length === 0) userRelays = Array.from(readRelays);
|
||||||
|
|
||||||
const rankedOptions = Array.from(userRelays).sort(
|
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);
|
assignments[pubkey] = rankedOptions.slice(0, 3);
|
||||||
|
@ -17,7 +17,7 @@ async function fetchInfo(relay: string) {
|
|||||||
url.protocol = url.protocol === "ws:" ? "http" : "https";
|
url.protocol = url.protocol === "ws:" ? "http" : "https";
|
||||||
|
|
||||||
const infoDoc = await fetchWithCorsFallback(url, { headers: { Accept: "application/nostr+json" } }).then(
|
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);
|
memoryCache.set(relay, infoDoc);
|
||||||
|
@ -115,7 +115,7 @@ class ReplaceableEventRelayLoader {
|
|||||||
`Updating query`,
|
`Updating query`,
|
||||||
Array.from(Object.keys(filters))
|
Array.from(Object.keys(filters))
|
||||||
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
|
.map((kind: string) => `kind ${kind}: ${filters[parseInt(kind)].authors?.length}`)
|
||||||
.join(", "),
|
.join(", ")
|
||||||
);
|
);
|
||||||
this.subscription.setQuery(query);
|
this.subscription.setQuery(query);
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ class ReplaceableEventLoaderService {
|
|||||||
private events = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
private events = new SuperMap<Pubkey, Subject<NostrEvent>>(() => new Subject<NostrEvent>());
|
||||||
|
|
||||||
private loaders = new SuperMap<Relay, ReplaceableEventRelayLoader>(
|
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");
|
log = logger.extend("ReplaceableEventLoader");
|
||||||
@ -180,7 +180,7 @@ class ReplaceableEventLoaderService {
|
|||||||
const keys = await db.getAllKeysFromIndex(
|
const keys = await db.getAllKeysFromIndex(
|
||||||
"replaceableEvents",
|
"replaceableEvents",
|
||||||
"created",
|
"created",
|
||||||
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix()),
|
IDBKeyRange.upperBound(dayjs().subtract(1, "day").unix())
|
||||||
);
|
);
|
||||||
|
|
||||||
this.log(`Pruning ${keys.length} events`);
|
this.log(`Pruning ${keys.length} events`);
|
||||||
@ -243,12 +243,9 @@ replaceableEventLoaderService.pruneCache();
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
replaceableEventLoaderService.update();
|
replaceableEventLoaderService.update();
|
||||||
}, 1000 * 2);
|
}, 1000 * 2);
|
||||||
setInterval(
|
setInterval(() => {
|
||||||
() => {
|
|
||||||
replaceableEventLoaderService.pruneCache();
|
replaceableEventLoaderService.pruneCache();
|
||||||
},
|
}, 1000 * 60 * 60);
|
||||||
1000 * 60 * 60,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
|
@ -11,7 +11,7 @@ const DTAG = "nostrudel-settings";
|
|||||||
|
|
||||||
class UserAppSettings {
|
class UserAppSettings {
|
||||||
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
|
private parsedSubjects = new SuperMap<string, PersistentSubject<AppSettings>>(
|
||||||
() => new PersistentSubject<AppSettings>(defaultSettings),
|
() => new PersistentSubject<AppSettings>(defaultSettings)
|
||||||
);
|
);
|
||||||
getSubject(pubkey: string) {
|
getSubject(pubkey: string) {
|
||||||
return this.parsedSubjects.get(pubkey);
|
return this.parsedSubjects.get(pubkey);
|
||||||
|
@ -19,7 +19,7 @@ class SigningService {
|
|||||||
|
|
||||||
private async getKeyMaterial() {
|
private async getKeyMaterial() {
|
||||||
const password = window.prompt(
|
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");
|
if (!password) throw new Error("Password required");
|
||||||
const enc = new TextEncoder();
|
const enc = new TextEncoder();
|
||||||
@ -38,7 +38,7 @@ class SigningService {
|
|||||||
keyMaterial,
|
keyMaterial,
|
||||||
{ name: "AES-GCM", length: 256 },
|
{ name: "AES-GCM", length: 256 },
|
||||||
true,
|
true,
|
||||||
["encrypt", "decrypt"],
|
["encrypt", "decrypt"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,15 +31,12 @@ function parseContacts(event: NostrEvent): UserContacts {
|
|||||||
const relays = normalizeRelayConfigs(relayJsonToRelayConfig(relayJson));
|
const relays = normalizeRelayConfigs(relayJsonToRelayConfig(relayJson));
|
||||||
|
|
||||||
const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]);
|
const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]);
|
||||||
const contactRelay = event.tags.filter(isPTag).reduce(
|
const contactRelay = event.tags.filter(isPTag).reduce((dir, tag) => {
|
||||||
(dir, tag) => {
|
|
||||||
if (tag[2]) {
|
if (tag[2]) {
|
||||||
dir[tag[1]] = tag[2];
|
dir[tag[1]] = tag[2];
|
||||||
}
|
}
|
||||||
return dir;
|
return dir;
|
||||||
},
|
}, {} as Record<string, string>);
|
||||||
{} as Record<string, string>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
@ -63,7 +60,7 @@ class UserContactsService {
|
|||||||
Kind.Contacts,
|
Kind.Contacts,
|
||||||
pubkey,
|
pubkey,
|
||||||
undefined,
|
undefined,
|
||||||
alwaysRequest,
|
alwaysRequest
|
||||||
);
|
);
|
||||||
|
|
||||||
sub.connectWithHandler(requestSub, (event, next) => next(parseContacts(event)));
|
sub.connectWithHandler(requestSub, (event, next) => next(parseContacts(event)));
|
||||||
|
@ -32,7 +32,7 @@ class UserMetadataService {
|
|||||||
Kind.Metadata,
|
Kind.Metadata,
|
||||||
pubkey,
|
pubkey,
|
||||||
undefined,
|
undefined,
|
||||||
alwaysRequest,
|
alwaysRequest
|
||||||
);
|
);
|
||||||
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
|
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
|
||||||
return sub;
|
return sub;
|
||||||
|
@ -63,7 +63,7 @@ class UserTrustedStatsService {
|
|||||||
async fetchUserStats(pubkey: string) {
|
async fetchUserStats(pubkey: string) {
|
||||||
try {
|
try {
|
||||||
const stats = await fetch(`https://api.nostr.band/v0/stats/profile/${pubkey}`).then(
|
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]) {
|
if (stats?.stats[pubkey]) {
|
||||||
|
@ -57,13 +57,13 @@ function HashTagPage() {
|
|||||||
if (!showReplies && isReply(event)) return false;
|
if (!showReplies && isReply(event)) return false;
|
||||||
return timelinePageEventFilter(event);
|
return timelinePageEventFilter(event);
|
||||||
},
|
},
|
||||||
[showReplies],
|
[showReplies]
|
||||||
);
|
);
|
||||||
const timeline = useTimelineLoader(
|
const timeline = useTimelineLoader(
|
||||||
`${hashtag}-hashtag`,
|
`${hashtag}-hashtag`,
|
||||||
readRelays,
|
readRelays,
|
||||||
{ kinds: [1], "#t": [hashtag] },
|
{ kinds: [1], "#t": [hashtag] },
|
||||||
{ eventFilter },
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
useRelaysChanged(readRelays, () => timeline.reset());
|
useRelaysChanged(readRelays, () => timeline.reset());
|
||||||
|
@ -29,7 +29,7 @@ function FollowingTabBody() {
|
|||||||
if (!showReplies && isReply(event)) return false;
|
if (!showReplies && isReply(event)) return false;
|
||||||
return timelinePageEventFilter(event);
|
return timelinePageEventFilter(event);
|
||||||
},
|
},
|
||||||
[showReplies, timelinePageEventFilter],
|
[showReplies, timelinePageEventFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const following = contacts?.contacts || [];
|
const following = contacts?.contacts || [];
|
||||||
@ -37,7 +37,7 @@ function FollowingTabBody() {
|
|||||||
`${truncatedId(account.pubkey)}-following`,
|
`${truncatedId(account.pubkey)}-following`,
|
||||||
readRelays,
|
readRelays,
|
||||||
{ authors: following, kinds: [Kind.Text, Kind.Repost, 2] },
|
{ authors: following, kinds: [Kind.Text, Kind.Repost, 2] },
|
||||||
{ enabled: following.length > 0, eventFilter },
|
{ enabled: following.length > 0, eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
|
@ -24,7 +24,7 @@ function GlobalPage() {
|
|||||||
if (!showReplies && isReply(event)) return false;
|
if (!showReplies && isReply(event)) return false;
|
||||||
return timelineEventFilter(event);
|
return timelineEventFilter(event);
|
||||||
},
|
},
|
||||||
[showReplies, timelineEventFilter],
|
[showReplies, timelineEventFilter]
|
||||||
);
|
);
|
||||||
const timeline = useTimelineLoader(`global`, readRelays, { kinds: [1] }, { eventFilter });
|
const timeline = useTimelineLoader(`global`, readRelays, { kinds: [1] }, { eventFilter });
|
||||||
useRelaysChanged(readRelays, () => timeline.reset());
|
useRelaysChanged(readRelays, () => timeline.reset());
|
||||||
|
@ -51,7 +51,7 @@ export default function LoginNip05View() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
1000,
|
1000,
|
||||||
[nip05, setPubkey, setRelays, setLoading],
|
[nip05, setPubkey, setRelays, setLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {
|
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
@ -62,7 +62,7 @@ export default function LoginNsecView() {
|
|||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setInputValue, setHexKey, setNpub, setError],
|
[setInputValue, setHexKey, setNpub, setError]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit: React.FormEventHandler<HTMLDivElement> = async (e) => {
|
const handleSubmit: React.FormEventHandler<HTMLDivElement> = async (e) => {
|
||||||
|
@ -104,7 +104,7 @@ export default function MapView() {
|
|||||||
const hash = ngeohash.encode(center.lat, center.lng, 5);
|
const hash = ngeohash.encode(center.lat, center.lng, 5);
|
||||||
|
|
||||||
setSearchParams({ hash }, { replace: true });
|
setSearchParams({ hash }, { replace: true });
|
||||||
}, 1000),
|
}, 1000)
|
||||||
);
|
);
|
||||||
|
|
||||||
setMap(map);
|
setMap(map);
|
||||||
@ -122,7 +122,7 @@ export default function MapView() {
|
|||||||
"geo-events",
|
"geo-events",
|
||||||
readRelays,
|
readRelays,
|
||||||
{ "#g": cells, kinds: [Kind.Text] },
|
{ "#g": cells, kinds: [Kind.Text] },
|
||||||
{ enabled: cells.length > 0 },
|
{ enabled: cells.length > 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const setCellsFromMap = useCallback(() => {
|
const setCellsFromMap = useCallback(() => {
|
||||||
@ -133,7 +133,7 @@ export default function MapView() {
|
|||||||
bbox.getWest(),
|
bbox.getWest(),
|
||||||
bbox.getNorth(),
|
bbox.getNorth(),
|
||||||
bbox.getEast(),
|
bbox.getEast(),
|
||||||
getPrecision(map.getZoom()),
|
getPrecision(map.getZoom())
|
||||||
);
|
);
|
||||||
|
|
||||||
setCells(hashes);
|
setCells(hashes);
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Button, Card, CardBody, Flex, IconButton, Textarea } from "@chakra-ui/react";
|
import { Button, Card, CardBody, Flex, IconButton, Textarea } from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import { Link, Navigate, useParams } from "react-router-dom";
|
import { Link, Navigate, useParams } from "react-router-dom";
|
||||||
import { nostrPostAction } from "../../classes/nostr-post-action";
|
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||||
import { ArrowLeftSIcon } from "../../components/icons";
|
import { ArrowLeftSIcon } from "../../components/icons";
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { normalizeToHex } from "../../helpers/nip19";
|
import { normalizeToHex } from "../../helpers/nip19";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useSigningContext } from "../../providers/signing-provider";
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
import clientRelaysService from "../../services/client-relays";
|
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";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
|
|
||||||
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
const { requestEncrypt, requestSignature } = useSigningContext();
|
const { requestEncrypt, requestSignature } = useSigningContext();
|
||||||
const [content, setContent] = useState<string>("");
|
const [content, setContent] = useState<string>("");
|
||||||
@ -76,9 +74,9 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
|||||||
icon={<ArrowLeftSIcon />}
|
icon={<ArrowLeftSIcon />}
|
||||||
aria-label="Back"
|
aria-label="Back"
|
||||||
to="/dm"
|
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} />
|
<UserLink pubkey={pubkey} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -23,7 +23,6 @@ import useSubject from "../../hooks/use-subject";
|
|||||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
import directMessagesService from "../../services/direct-messages";
|
import directMessagesService from "../../services/direct-messages";
|
||||||
import { ExternalLinkIcon } from "../../components/icons";
|
import { ExternalLinkIcon } from "../../components/icons";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
|
||||||
function ContactCard({ pubkey }: { pubkey: string }) {
|
function ContactCard({ pubkey }: { pubkey: string }) {
|
||||||
@ -47,7 +46,6 @@ function ContactCard({ pubkey }: { pubkey: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DirectMessagesPage() {
|
function DirectMessagesPage() {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const [from, setFrom] = useState(dayjs().subtract(2, "days"));
|
const [from, setFrom] = useState(dayjs().subtract(2, "days"));
|
||||||
const conversations = useSubject(directMessagesService.conversations);
|
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">
|
<Flex direction="column" gap="2" overflowX="hidden" overflowY="auto" height="100%" pt="2" pb="8">
|
||||||
<Alert status="info" flexShrink={0}>
|
<Alert status="info" flexShrink={0}>
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Flex direction={isMobile ? "column" : "row"}>
|
<Flex direction={{ base: "column", lg: "row" }}>
|
||||||
<AlertTitle>Give Blowater a try</AlertTitle>
|
<AlertTitle>Give Blowater a try</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Text>
|
<Text>
|
||||||
|
@ -3,7 +3,6 @@ import { useState } from "react";
|
|||||||
import { ArrowDownSIcon, ArrowUpSIcon } from "../../components/icons";
|
import { ArrowDownSIcon, ArrowUpSIcon } from "../../components/icons";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { countReplies, ThreadItem as ThreadItemData } from "../../helpers/thread";
|
import { countReplies, ThreadItem as ThreadItemData } from "../../helpers/thread";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
|
|
||||||
export type ThreadItemProps = {
|
export type ThreadItemProps = {
|
||||||
@ -13,7 +12,6 @@ export type ThreadItemProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps) => {
|
export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps) => {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const [showReplies, setShowReplies] = useState(initShowReplies ?? post.replies.length === 1);
|
const [showReplies, setShowReplies] = useState(initShowReplies ?? post.replies.length === 1);
|
||||||
const toggle = () => setShowReplies((v) => !v);
|
const toggle = () => setShowReplies((v) => !v);
|
||||||
|
|
||||||
@ -31,7 +29,7 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
|
|||||||
{showReplies ? <ArrowDownSIcon /> : <ArrowUpSIcon />}
|
{showReplies ? <ArrowDownSIcon /> : <ArrowUpSIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
{showReplies && (
|
{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) => (
|
{post.replies.map((child) => (
|
||||||
<ThreadPost key={child.event.id} post={child} focusId={focusId} />
|
<ThreadPost key={child.event.id} post={child} focusId={focusId} />
|
||||||
))}
|
))}
|
||||||
|
@ -206,7 +206,7 @@ export const ProfileEditView = () => {
|
|||||||
nip05: metadata?.nip05,
|
nip05: metadata?.nip05,
|
||||||
lightningAddress: metadata?.lud16 || metadata?.lud06,
|
lightningAddress: metadata?.lud16 || metadata?.lud06,
|
||||||
}),
|
}),
|
||||||
[metadata],
|
[metadata]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async (data: FormData) => {
|
const handleSubmit = async (data: FormData) => {
|
||||||
|
@ -25,7 +25,7 @@ export default function RelaysView() {
|
|||||||
.map((r) => r.url)
|
.map((r) => r.url)
|
||||||
.filter(safeRelayUrl);
|
.filter(safeRelayUrl);
|
||||||
const { value: onlineRelays = [] } = useAsync(async () =>
|
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(() => {
|
const filteredRelays = useMemo(() => {
|
||||||
@ -51,9 +51,9 @@ export default function RelaysView() {
|
|||||||
Add Custom
|
Add Custom
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<SimpleGrid minChildWidth="25rem" spacing="2">
|
<SimpleGrid columns={[1, 1, 1, 2, 3]} spacing="2">
|
||||||
{filteredRelays.map((url) => (
|
{filteredRelays.map((url) => (
|
||||||
<RelayCard key={url} url={url} variant="outline" maxW="xl" />
|
<RelayCard key={url} url={url} variant="outline" />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
@ -61,9 +61,9 @@ export default function RelaysView() {
|
|||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Heading size="lg">Discovered Relays</Heading>
|
<Heading size="lg">Discovered Relays</Heading>
|
||||||
<SimpleGrid minChildWidth="25rem" spacing="2">
|
<SimpleGrid columns={[1, 1, 1, 2, 3]} spacing="2">
|
||||||
{discoveredRelays.map((url) => (
|
{discoveredRelays.map((url) => (
|
||||||
<RelayCard key={url} url={url} variant="outline" maxW="xl" />
|
<RelayCard key={url} url={url} variant="outline" />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
|
@ -105,7 +105,11 @@ function RelayPage({ relay }: { relay: string }) {
|
|||||||
<RelayJoinAction url={relay} />
|
<RelayJoinAction url={relay} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<RelayMetadata url={relay} />
|
<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">
|
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="brand">
|
||||||
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
||||||
<Tab>Reviews</Tab>
|
<Tab>Reviews</Tab>
|
||||||
|
@ -87,7 +87,7 @@ function SearchResults({ search }: { search: string }) {
|
|||||||
`search`,
|
`search`,
|
||||||
searchRelays,
|
searchRelays,
|
||||||
{ search: search || "", kinds: [Kind.Metadata] },
|
{ search: search || "", kinds: [Kind.Metadata] },
|
||||||
{ enabled: !!search },
|
{ enabled: !!search }
|
||||||
);
|
);
|
||||||
|
|
||||||
const events = useSubject(timeline?.timeline) ?? [];
|
const events = useSubject(timeline?.timeline) ?? [];
|
||||||
@ -96,7 +96,7 @@ function SearchResults({ search }: { search: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<SimpleGrid minChildWidth="30rem" spacing="2">
|
<SimpleGrid columns={{ base: 1, xl: 2 }} spacing="2">
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
<ProfileResult key={event.id} event={event} />
|
<ProfileResult key={event.id} event={event} />
|
||||||
))}
|
))}
|
||||||
@ -148,17 +148,19 @@ export function SearchPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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} />
|
<QrScannerModal isOpen={qrScannerModal.isOpen} onClose={qrScannerModal.onClose} onData={handleSearchText} />
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Flex gap="2">
|
<Flex gap="2" wrap="wrap">
|
||||||
|
<Flex gap="2" grow={1}>
|
||||||
<IconButton onClick={qrScannerModal.onOpen} icon={<QrCodeIcon />} aria-label="Qr Scanner" />
|
<IconButton onClick={qrScannerModal.onOpen} icon={<QrCodeIcon />} aria-label="Qr Scanner" />
|
||||||
{!!navigator.clipboard.readText && (
|
{!!navigator.clipboard.readText && (
|
||||||
<IconButton onClick={readClipboard} icon={<ClipboardIcon />} aria-label="Read clipboard" />
|
<IconButton onClick={readClipboard} icon={<ClipboardIcon />} aria-label="Read clipboard" />
|
||||||
)}
|
)}
|
||||||
<Input type="search" value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
|
<Input type="search" value={searchInput} onChange={(e) => setSearchInput(e.target.value)} />
|
||||||
<Button type="submit">Search</Button>
|
<Button type="submit">Search</Button>
|
||||||
|
</Flex>
|
||||||
<RelaySelectionButton />
|
<RelaySelectionButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
</form>
|
</form>
|
||||||
|
@ -27,7 +27,7 @@ function StreamsPage() {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[filterStatus],
|
[filterStatus]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { people } = usePeopleListContext();
|
const { people } = usePeopleListContext();
|
||||||
@ -49,18 +49,18 @@ function StreamsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
||||||
<Flex gap="2">
|
<Flex gap="2" wrap="wrap">
|
||||||
<PeopleListSelection maxW="sm" />
|
<PeopleListSelection w={["full", "xs"]} />
|
||||||
<Select maxW="sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
|
<Select w={["full", "xs"]} value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
|
||||||
<option value="live">Live</option>
|
<option value="live">Live</option>
|
||||||
<option value="ended">Ended</option>
|
<option value="ended">Ended</option>
|
||||||
</Select>
|
</Select>
|
||||||
<RelaySelectionButton ml="auto" />
|
<RelaySelectionButton ml="auto" />
|
||||||
</Flex>
|
</Flex>
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<SimpleGrid minChildWidth={["full", "20rem"]} spacing="2">
|
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
|
||||||
{streams.map((stream) => (
|
{streams.map((stream) => (
|
||||||
<StreamCard key={stream.event.id} stream={stream} maxW="lg" />
|
<StreamCard key={stream.event.id} stream={stream} />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<TimelineActionAndStatus timeline={timeline} />
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useScroll } from "react-use";
|
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 { useParams, Navigate, useSearchParams, useNavigate } from "react-router-dom";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { Global, css } from "@emotion/react";
|
import { Global, css } from "@emotion/react";
|
||||||
@ -12,7 +12,6 @@ import { LiveVideoPlayer } from "../../../components/live-video-player";
|
|||||||
import StreamChat, { ChatDisplayMode } from "./stream-chat";
|
import StreamChat, { ChatDisplayMode } from "./stream-chat";
|
||||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
|
||||||
import StreamSummaryContent from "../components/stream-summary-content";
|
import StreamSummaryContent from "../components/stream-summary-content";
|
||||||
import { ArrowDownSIcon, ArrowUpSIcon, ExternalLinkIcon } from "../../../components/icons";
|
import { ArrowDownSIcon, ArrowUpSIcon, ExternalLinkIcon } from "../../../components/icons";
|
||||||
import useSetColorMode from "../../../hooks/use-set-color-mode";
|
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";
|
import RelaySelectionProvider from "../../../providers/relay-selection-provider";
|
||||||
|
|
||||||
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
|
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 scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollState = useScroll(scrollBox);
|
const scrollState = useScroll(scrollBox);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -47,8 +46,8 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{isMobile && toggleButton}
|
{vertical && toggleButton}
|
||||||
{!isMobile && (
|
{!vertical && (
|
||||||
<CopyIconButton
|
<CopyIconButton
|
||||||
text={location.href + "?displayMode=log&colorMode=dark"}
|
text={location.href + "?displayMode=log&colorMode=dark"}
|
||||||
aria-label="Copy chat log URL"
|
aria-label="Copy chat log URL"
|
||||||
@ -78,9 +77,9 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
h="full"
|
h="full"
|
||||||
overflowX="hidden"
|
overflowX="hidden"
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
direction={isMobile ? "column" : "row"}
|
direction={vertical ? "column" : "row"}
|
||||||
p={isMobile || !!displayMode ? 0 : "2"}
|
p={vertical || !!displayMode ? 0 : "2"}
|
||||||
gap={isMobile ? 0 : "4"}
|
gap={vertical ? 0 : "4"}
|
||||||
ref={scrollBox}
|
ref={scrollBox}
|
||||||
>
|
>
|
||||||
{displayMode && (
|
{displayMode && (
|
||||||
@ -93,14 +92,14 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!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
|
<LiveVideoPlayer
|
||||||
stream={stream.streaming || stream.recording}
|
stream={stream.streaming || stream.recording}
|
||||||
autoPlay={!!stream.streaming}
|
autoPlay={!!stream.streaming}
|
||||||
poster={stream.image}
|
poster={stream.image}
|
||||||
maxH="100vh"
|
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 />
|
<UserAvatarLink pubkey={stream.host} noProxy />
|
||||||
<Box>
|
<Box>
|
||||||
<Heading size="md">
|
<Heading size="md">
|
||||||
@ -113,15 +112,15 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
<RelaySelectionButton />
|
<RelaySelectionButton />
|
||||||
<Button onClick={() => navigate(-1)}>Back</Button>
|
<Button onClick={() => navigate(-1)}>Back</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<StreamSummaryContent stream={stream} px={isMobile ? "2" : 0} />
|
<StreamSummaryContent stream={stream} px={vertical ? "2" : 0} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
<StreamChat
|
<StreamChat
|
||||||
stream={stream}
|
stream={stream}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
maxW={isMobile || !!displayMode ? undefined : "lg"}
|
maxW={vertical || !!displayMode ? undefined : "lg"}
|
||||||
maxH="100vh"
|
maxH="100vh"
|
||||||
minH={isMobile ? "100vh" : undefined}
|
minH={vertical ? "100vh" : undefined}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
actions={renderActions()}
|
actions={renderActions()}
|
||||||
displayMode={displayMode}
|
displayMode={displayMode}
|
||||||
@ -152,7 +151,7 @@ export default function StreamView() {
|
|||||||
parsed.data.kind,
|
parsed.data.kind,
|
||||||
parsed.data.pubkey,
|
parsed.data.pubkey,
|
||||||
parsed.data.identifier,
|
parsed.data.identifier,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
@ -71,7 +71,7 @@ export default function StreamChat({
|
|||||||
const muteList = useUserMuteList(account?.pubkey);
|
const muteList = useUserMuteList(account?.pubkey);
|
||||||
const mutedPubkeys = useMemo(
|
const mutedPubkeys = useMemo(
|
||||||
() => [...(hostMuteList?.tags ?? []), ...(muteList?.tags ?? [])].filter(isPTag).map((t) => t[1] as string),
|
() => [...(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]);
|
const eventFilter = useCallback((event: NostrEvent) => !mutedPubkeys.includes(event.pubkey), [mutedPubkeys]);
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ export default function StreamChat({
|
|||||||
"#a": [getATag(stream)],
|
"#a": [getATag(stream)],
|
||||||
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
||||||
},
|
},
|
||||||
{ eventFilter },
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline).sort((a, b) => b.created_at - a.created_at);
|
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} />
|
<ChatMessage key={event.id} event={event} stream={stream} />
|
||||||
) : (
|
) : (
|
||||||
<ZapMessage key={event.id} zap={event} stream={stream} />
|
<ZapMessage key={event.id} zap={event} stream={stream} />
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{!isChatLog && (
|
{!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 { useNavigate, Link as RouterLink } from "react-router-dom";
|
||||||
import { ChatIcon, EditIcon } from "../../../components/icons";
|
import { ChatIcon, EditIcon } from "../../../components/icons";
|
||||||
import { UserAvatar } from "../../../components/user-avatar";
|
import { UserAvatar } from "../../../components/user-avatar";
|
||||||
@ -8,7 +8,6 @@ import { UserTipButton } from "../../../components/user-tip-button";
|
|||||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
|
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
|
||||||
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
|
||||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||||
import { UserProfileMenu } from "./user-profile-menu";
|
import { UserProfileMenu } from "./user-profile-menu";
|
||||||
|
|
||||||
@ -19,7 +18,6 @@ export default function Header({
|
|||||||
pubkey: string;
|
pubkey: string;
|
||||||
showRelaySelectionModal: () => void;
|
showRelaySelectionModal: () => void;
|
||||||
}) {
|
}) {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||||
@ -27,6 +25,8 @@ export default function Header({
|
|||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const isSelf = pubkey === account?.pubkey;
|
const isSelf = pubkey === account?.pubkey;
|
||||||
|
|
||||||
|
const showFullNip05 = useBreakpointValue({ base: false, md: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2" px="2" pt="2">
|
<Flex direction="column" gap="2" px="2" pt="2">
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
@ -34,7 +34,7 @@ export default function Header({
|
|||||||
<Heading size="md" isTruncated>
|
<Heading size="md" isTruncated>
|
||||||
{getUserDisplayName(metadata, pubkey)}
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
</Heading>
|
</Heading>
|
||||||
<UserDnsIdentityIcon pubkey={pubkey} onlyIcon={isMobile} />
|
<UserDnsIdentityIcon pubkey={pubkey} onlyIcon={showFullNip05} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{isSelf && (
|
{isSelf && (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -47,7 +47,7 @@ export default function UserFollowersTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<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) => (
|
{followers.map((event) => (
|
||||||
<FollowerItem key={event.pubkey} event={event} />
|
<FollowerItem key={event.pubkey} event={event} />
|
||||||
))}
|
))}
|
||||||
|
@ -17,7 +17,7 @@ export default function UserFollowingTab() {
|
|||||||
if (!contacts) return <Spinner />;
|
if (!contacts) return <Spinner />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleGrid minChildWidth={["full", "4in"]} spacing="2" py="2">
|
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2" py="2">
|
||||||
{people.map((pubkey) => (
|
{people.map((pubkey) => (
|
||||||
<UserCard key={pubkey} pubkey={pubkey} relay={contacts?.contactRelay[pubkey]} />
|
<UserCard key={pubkey} pubkey={pubkey} relay={contacts?.contactRelay[pubkey]} />
|
||||||
))}
|
))}
|
||||||
|
@ -25,7 +25,7 @@ export default function UserNotesTab() {
|
|||||||
if (hideReposts && isRepost(event)) return false;
|
if (hideReposts && isRepost(event)) return false;
|
||||||
return timelineEventFilter(event);
|
return timelineEventFilter(event);
|
||||||
},
|
},
|
||||||
[showReplies, hideReposts, timelineEventFilter],
|
[showReplies, hideReposts, timelineEventFilter]
|
||||||
);
|
);
|
||||||
const timeline = useTimelineLoader(
|
const timeline = useTimelineLoader(
|
||||||
truncatedId(pubkey) + "-notes",
|
truncatedId(pubkey) + "-notes",
|
||||||
@ -34,7 +34,7 @@ export default function UserNotesTab() {
|
|||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [Kind.Text, Kind.Repost, STREAM_KIND, 2],
|
kinds: [Kind.Text, Kind.Repost, STREAM_KIND, 2],
|
||||||
},
|
},
|
||||||
{ eventFilter },
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
|
@ -31,9 +31,9 @@ export default function UserStreamsTab() {
|
|||||||
return (
|
return (
|
||||||
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
||||||
<IntersectionObserverProvider<string> callback={callback}>
|
<IntersectionObserverProvider<string> callback={callback}>
|
||||||
<SimpleGrid minChildWidth="20rem" spacing="2">
|
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing="2">
|
||||||
{streams.map((stream) => (
|
{streams.map((stream) => (
|
||||||
<StreamCard key={stream.event.id} stream={stream} maxW="lg" />
|
<StreamCard key={stream.event.id} stream={stream} />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<TimelineActionAndStatus timeline={timeline} />
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
|
@ -87,14 +87,14 @@ const UserZapsTab = () => {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[filter],
|
[filter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeline = useTimelineLoader(
|
const timeline = useTimelineLoader(
|
||||||
`${truncatedId(pubkey)}-zaps`,
|
`${truncatedId(pubkey)}-zaps`,
|
||||||
relays,
|
relays,
|
||||||
{ "#p": [pubkey], kinds: [9735] },
|
{ "#p": [pubkey], kinds: [9735] },
|
||||||
{ eventFilter },
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline.timeline);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user