Add relay discovery map

Fix relay notes showing notes from other relays from cache
This commit is contained in:
hzrd149
2024-09-10 12:58:56 -05:00
parent 359dbcb508
commit c5e70354b8
29 changed files with 759 additions and 142 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add relay discovery map

View File

@@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix relay notes showing notes from other relays from cache

View File

@@ -56,6 +56,7 @@
"emojilib": "^3", "emojilib": "^3",
"framer-motion": "^10.16.0", "framer-motion": "^10.16.0",
"hls.js": "^1.4.14", "hls.js": "^1.4.14",
"i18n-iso-countries": "^7.12.0",
"idb": "^8.0.0", "idb": "^8.0.0",
"identicon.js": "^2.3.3", "identicon.js": "^2.3.3",
"iso-language-codes": "^2.0.0", "iso-language-codes": "^2.0.0",
@@ -87,7 +88,8 @@
"react-simplemde-editor": "^5.2.0", "react-simplemde-editor": "^5.2.0",
"react-singleton-hook": "^4.0.1", "react-singleton-hook": "^4.0.1",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"react-virtualized-auto-sizer": "^1.0.20", "react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-wiki-link": "^2.0.1", "remark-wiki-link": "^2.0.1",
"three": "^0.160.0", "three": "^0.160.0",
@@ -113,6 +115,7 @@
"@types/ngeohash": "^0.6.8", "@types/ngeohash": "^0.6.8",
"@types/react": "^18.2.45", "@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@types/react-window": "^1.8.8",
"@types/three": "^0.160.0", "@types/three": "^0.160.0",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5", "@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
"@types/zen-observable": "^0.8.7", "@types/zen-observable": "^0.8.7",

View File

@@ -15,6 +15,7 @@ const DiscoveryHomeView = lazy(() => import("./views/discovery/index"));
const DVMFeedView = lazy(() => import("./views/discovery/dvm-feed/feed")); const DVMFeedView = lazy(() => import("./views/discovery/dvm-feed/feed"));
const BlindspotHomeView = lazy(() => import("./views/discovery/blindspot")); const BlindspotHomeView = lazy(() => import("./views/discovery/blindspot"));
const BlindspotFeedView = lazy(() => import("./views/discovery/blindspot/feed")); const BlindspotFeedView = lazy(() => import("./views/discovery/blindspot/feed"));
const RelayDiscoveryView = lazy(() => import("./views/discovery/relays/index"));
import SettingsView from "./views/settings"; import SettingsView from "./views/settings";
import NostrLinkView from "./views/link"; import NostrLinkView from "./views/link";
import ProfileView from "./views/profile"; import ProfileView from "./views/profile";
@@ -228,6 +229,14 @@ const router = createHashRouter([
</RouteProviders> </RouteProviders>
), ),
}, },
{
path: "/discovery/relays",
element: (
<RouteProviders>
<RelayDiscoveryView />
</RouteProviders>
),
},
{ {
path: "/", path: "/",
element: <RootPage />, element: <RootPage />,

View File

@@ -35,6 +35,7 @@ export default class TimelineLoader {
loadNextBlockBuffer = 2; loadNextBlockBuffer = 2;
eventFilter?: EventFilter; eventFilter?: EventFilter;
useCache = true;
name: string; name: string;
process: Process; process: Process;
@@ -81,7 +82,7 @@ export default class TimelineLoader {
if (isReplaceable(event.kind)) replaceableEventsService.handleEvent(event); if (isReplaceable(event.kind)) replaceableEventsService.handleEvent(event);
this.events.addEvent(event); this.events.addEvent(event);
if (!fromCache && localRelay && !this.seenInCache.has(event.id)) localRelay.publish(event); if (!fromCache && this.useCache && localRelay && !this.seenInCache.has(event.id)) localRelay.publish(event);
if (fromCache) this.seenInCache.add(event.id); if (fromCache) this.seenInCache.add(event.id);
} }
@@ -133,7 +134,7 @@ export default class TimelineLoader {
// recreate cache chunk loader // recreate cache chunk loader
if (this.cacheLoader) this.disconnectFromChunkLoader(this.cacheLoader); if (this.cacheLoader) this.disconnectFromChunkLoader(this.cacheLoader);
if (localRelay) { if (localRelay && this.useCache) {
this.cacheLoader = new ChunkedRequest(localRelay, this.filters, this.log.extend("cache-relay")); this.cacheLoader = new ChunkedRequest(localRelay, this.filters, this.log.extend("cache-relay"));
this.connectToChunkLoader(this.cacheLoader); this.connectToChunkLoader(this.cacheLoader);
} }

View File

@@ -0,0 +1,20 @@
import { useMemo } from "react";
import { Select, SelectProps } from "@chakra-ui/react";
import { getAlpha3Codes, getName, registerLocale } from "i18n-iso-countries";
// TODO: support others
import en from "i18n-iso-countries/langs/en.json";
registerLocale(en);
export default function CountyPicker({ ...props }: Omit<SelectProps, "children">) {
const codes = useMemo(() => Object.keys(getAlpha3Codes()).map((code) => ({ name: getName(code, "en"), code })), []);
return (
<Select {...props}>
<option value="">Any</option>
{codes.map(({ code, name }) => (
<option value={code}>{name}</option>
))}
</Select>
);
}

View File

@@ -6,7 +6,6 @@ import { kinds } from "nostr-tools";
import EmbeddedNote from "./event-types/embedded-note"; import EmbeddedNote from "./event-types/embedded-note";
import useSingleEvent from "../../hooks/use-single-event"; import useSingleEvent from "../../hooks/use-single-event";
import { NostrEvent } from "../../types/nostr-event"; import { NostrEvent } from "../../types/nostr-event";
import { STREAM_CHAT_MESSAGE_KIND, STREAM_KIND } from "../../helpers/nostr/stream";
import { GOAL_KIND } from "../../helpers/nostr/goal"; import { GOAL_KIND } from "../../helpers/nostr/goal";
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs"; import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
import { import {
@@ -18,32 +17,34 @@ import {
} from "../../helpers/nostr/lists"; } from "../../helpers/nostr/lists";
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
import { STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr"; import { STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr";
import { TORRENT_COMMENT_KIND, TORRENT_KIND } from "../../helpers/nostr/torrents";
import { FLARE_VIDEO_KIND } from "../../helpers/nostr/flare";
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
import useReplaceableEvent from "../../hooks/use-replaceable-event"; import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { safeDecode } from "../../helpers/nip19"; import { safeDecode } from "../../helpers/nip19";
import type { EmbeddedGoalOptions } from "./event-types/embedded-goal";
import LoadingNostrLink from "../loading-nostr-link";
import RelayCard from "../../views/relays/components/relay-card"; import RelayCard from "../../views/relays/components/relay-card";
import EmbeddedStream from "./event-types/embedded-stream"; import EmbeddedRepost from "./event-types/embedded-repost";
import EmbeddedEmojiPack from "./event-types/embedded-emoji-pack";
import EmbeddedGoal, { EmbeddedGoalOptions } from "./event-types/embedded-goal";
import EmbeddedUnknown from "./event-types/embedded-unknown";
import EmbeddedList from "./event-types/embedded-list"; import EmbeddedList from "./event-types/embedded-list";
import EmbeddedArticle from "./event-types/embedded-article";
import EmbeddedBadge from "./event-types/embedded-badge";
import EmbeddedStreamMessage from "./event-types/embedded-stream-message";
import EmbeddedCommunity from "./event-types/embedded-community";
import EmbeddedReaction from "./event-types/embedded-reaction"; import EmbeddedReaction from "./event-types/embedded-reaction";
import EmbeddedDM from "./event-types/embedded-dm"; import EmbeddedDM from "./event-types/embedded-dm";
import { TORRENT_COMMENT_KIND, TORRENT_KIND } from "../../helpers/nostr/torrents"; import EmbeddedUnknown from "./event-types/embedded-unknown";
import EmbeddedTorrent from "./event-types/embedded-torrent";
import EmbeddedTorrentComment from "./event-types/embedded-torrent-comment"; const EmbeddedGoal = lazy(() => import("./event-types/embedded-goal"));
import EmbeddedChannel from "./event-types/embedded-channel"; const EmbeddedArticle = lazy(() => import("./event-types/embedded-article"));
import { FLARE_VIDEO_KIND } from "../../helpers/nostr/flare"; const EmbeddedCommunity = lazy(() => import("./event-types/embedded-community"));
import EmbeddedFlareVideo from "./event-types/embedded-flare-video"; const EmbeddedBadge = lazy(() => import("./event-types/embedded-badge"));
import LoadingNostrLink from "../loading-nostr-link"; const EmbeddedTorrent = lazy(() => import("./event-types/embedded-torrent"));
import EmbeddedRepost from "./event-types/embedded-repost"; const EmbeddedTorrentComment = lazy(() => import("./event-types/embedded-torrent-comment"));
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki"; const EmbeddedChannel = lazy(() => import("./event-types/embedded-channel"));
import EmbeddedWikiPage from "./event-types/embedded-wiki-page"; const EmbeddedFlareVideo = lazy(() => import("./event-types/embedded-flare-video"));
import EmbeddedZapRecept from "./event-types/embedded-zap-receipt"; const EmbeddedEmojiPack = lazy(() => import("./event-types/embedded-emoji-pack"));
const EmbeddedZapRecept = lazy(() => import("./event-types/embedded-zap-receipt"));
const EmbeddedWikiPage = lazy(() => import("./event-types/embedded-wiki-page"));
const EmbeddedStream = lazy(() => import("./event-types/embedded-stream"));
const EmbeddedStreamMessage = lazy(() => import("./event-types/embedded-stream-message"));
const EmbeddedStemstrTrack = lazy(() => import("./event-types/embedded-stemstr-track")); const EmbeddedStemstrTrack = lazy(() => import("./event-types/embedded-stemstr-track"));
export type EmbedProps = { export type EmbedProps = {
@@ -63,7 +64,7 @@ export function EmbedEvent({
return <EmbeddedReaction event={event} {...cardProps} />; return <EmbeddedReaction event={event} {...cardProps} />;
case kinds.EncryptedDirectMessage: case kinds.EncryptedDirectMessage:
return <EmbeddedDM dm={event} {...cardProps} />; return <EmbeddedDM dm={event} {...cardProps} />;
case STREAM_KIND: case kinds.LiveEvent:
return <EmbeddedStream event={event} {...cardProps} />; return <EmbeddedStream event={event} {...cardProps} />;
case GOAL_KIND: case GOAL_KIND:
return <EmbeddedGoal goal={event} {...cardProps} {...goalProps} />; return <EmbeddedGoal goal={event} {...cardProps} {...goalProps} />;
@@ -79,7 +80,7 @@ export function EmbedEvent({
return <EmbeddedArticle article={event} {...cardProps} />; return <EmbeddedArticle article={event} {...cardProps} />;
case kinds.BadgeDefinition: case kinds.BadgeDefinition:
return <EmbeddedBadge badge={event} {...cardProps} />; return <EmbeddedBadge badge={event} {...cardProps} />;
case STREAM_CHAT_MESSAGE_KIND: case kinds.LiveChatMessage:
return <EmbeddedStreamMessage message={event} {...cardProps} />; return <EmbeddedStreamMessage message={event} {...cardProps} />;
case COMMUNITY_DEFINITION_KIND: case COMMUNITY_DEFINITION_KIND:
return <EmbeddedCommunity community={event} {...cardProps} />; return <EmbeddedCommunity community={event} {...cardProps} />;

View File

@@ -1,10 +1,11 @@
import { lazy } from "react";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { isStreamURL, isVideoURL } from "../../../helpers/url"; import { isStreamURL, isVideoURL } from "../../../helpers/url";
import useAppSettings from "../../../hooks/use-app-settings"; import useAppSettings from "../../../hooks/use-app-settings";
import useElementTrustBlur from "../../../hooks/use-element-trust-blur"; import useElementTrustBlur from "../../../hooks/use-element-trust-blur";
import ExpandableEmbed from "../expandable-embed"; import ExpandableEmbed from "../expandable-embed";
import { LiveVideoPlayer } from "../../live-video-player"; const LiveVideoPlayer = lazy(() => import("../../live-video-player"));
const StyledVideo = styled.video` const StyledVideo = styled.video`
max-width: 30rem; max-width: 30rem;

View File

@@ -8,7 +8,7 @@ export enum VideoStatus {
} }
// copied from zap.stream // copied from zap.stream
export function LiveVideoPlayer({ export default function LiveVideoPlayer({
stream, stream,
autoPlay, autoPlay,
poster, poster,

View File

@@ -265,4 +265,8 @@ export function getSortedKinds(events: NostrEvent[]) {
.reduce((dir, k) => ({ ...dir, [k.kind]: k.count }), {} as Record<string, number>); .reduce((dir, k) => ({ ...dir, [k.kind]: k.count }), {} as Record<string, number>);
} }
export function getTagValue(event: NostrEvent, tag: string){
return event.tags.find(t => t[0]===tag && t.length>=2)?.[1]
}
export { getEventUID }; export { getEventUID };

View File

@@ -9,6 +9,7 @@ type Options = {
/** @deprecated */ /** @deprecated */
enabled?: boolean; enabled?: boolean;
eventFilter?: EventFilter; eventFilter?: EventFilter;
useCache?: boolean;
cursor?: number; cursor?: number;
customSort?: (a: NostrEvent, b: NostrEvent) => number; customSort?: (a: NostrEvent, b: NostrEvent) => number;
}; };
@@ -21,6 +22,9 @@ export default function useTimelineLoader(
) { ) {
const timeline = useMemo(() => timelineCacheService.createTimeline(key), [key]); const timeline = useMemo(() => timelineCacheService.createTimeline(key), [key]);
// set use cache
if (opts?.useCache !== undefined) timeline.useCache = opts?.useCache;
// update relays // update relays
useEffect(() => { useEffect(() => {
timeline.setRelays(relays); timeline.setRelays(relays);

View File

@@ -13,6 +13,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import IntersectionObserverProvider from "../../providers/local/intersection-observer"; import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import Telescope from "../../components/icons/telescope"; import Telescope from "../../components/icons/telescope";
import HoverLinkOverlay from "../../components/hover-link-overlay"; import HoverLinkOverlay from "../../components/hover-link-overlay";
import { RelayIcon } from "../../components/icons";
function DVMFeeds() { function DVMFeeds() {
const readRelays = useReadRelays(); const readRelays = useReadRelays();
@@ -50,17 +51,30 @@ function DVMFeeds() {
function DiscoveryHomePage() { function DiscoveryHomePage() {
return ( return (
<VerticalPageLayout> <VerticalPageLayout>
<Card as={LinkBox} display="block" p="4" maxW="lg"> <SimpleGrid columns={{ base: 1, md: 1, lg: 2, xl: 3 }} spacing="2">
<Telescope boxSize={16} float="left" ml="2" my="2" mr="6" /> <Card as={LinkBox} display="block" p="4" maxW="lg">
<Flex direction="column"> <Telescope boxSize={16} float="left" ml="2" my="2" mr="6" />
<Heading size="md"> <Flex direction="column">
<HoverLinkOverlay as={RouterLink} to="/discovery/blindspot"> <Heading size="md">
Blind spots <HoverLinkOverlay as={RouterLink} to="/discovery/blindspot">
</HoverLinkOverlay> Blind spots
</Heading> </HoverLinkOverlay>
<Text>What are other users seeing that you are not?</Text> </Heading>
</Flex> <Text>What are other users seeing that you are not?</Text>
</Card> </Flex>
</Card>
<Card as={LinkBox} display="block" p="4" maxW="lg">
<RelayIcon boxSize={16} float="left" ml="2" my="2" mr="6" />
<Flex direction="column">
<Heading size="md">
<HoverLinkOverlay as={RouterLink} to="/discovery/relays">
Relays
</HoverLinkOverlay>
</Heading>
<Text>What are other users seeing that you are not?</Text>
</Flex>
</Card>
</SimpleGrid>
<DVMFeeds /> <DVMFeeds />
</VerticalPageLayout> </VerticalPageLayout>
); );

View File

@@ -0,0 +1,147 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
ButtonGroup,
CloseButton,
Code,
Flex,
FlexProps,
Heading,
IconButton,
Spacer,
Text,
} from "@chakra-ui/react";
import { useContext } from "react";
import { NostrEvent } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import { SelectedContext } from "../selected-context";
import { getTagValue } from "../../../../helpers/nostr/event";
import DebugEventButton from "../../../../components/debug-modal/debug-event-button";
import SupportedNIPs from "../../../relays/components/supported-nips";
import RelayNotes from "../../../relays/relay/relay-notes";
import { safeRelayUrl } from "../../../../helpers/relay";
import { ExternalLinkIcon } from "../../../../components/icons";
import PeopleListProvider from "../../../../providers/local/people-list-provider";
import { getPubkeysFromList } from "../../../../helpers/nostr/lists";
import UserAvatarLink from "../../../../components/user/user-avatar-link";
import UserName from "../../../../components/user/user-name";
import UserDnsIdentity from "../../../../components/user/user-dns-identity";
import { RelayFavicon } from "../../../../components/relay-favicon";
export default function RelayStatusDetails({ event, ...props }: Omit<FlexProps, "children"> & { event: NostrEvent }) {
const selected = useContext(SelectedContext);
const identity = getTagValue(event, "d");
const network = getTagValue(event, "n");
const software = getTagValue(event, "s");
const version = event.tags.find((t) => t[0] === "l" && t[2] === "nip11.version")?.[1];
const url = identity ? safeRelayUrl(identity) : undefined;
// gather labels
const misc: Record<string, string[]> = {};
for (const tag of event.tags) {
if (tag[0] == "l" && tag.length >= 3) {
if (misc[tag[2]]) misc[tag[2]].push(tag[1]);
else misc[tag[2]] = [tag[1]];
}
}
const nips = event.tags
.filter((t) => t[0] === "N" && t[1])
.map((t) => parseInt(t[1]))
.filter((n) => Number.isFinite(n));
const pubkeys = getPubkeysFromList(event);
return (
<Flex direction="column" gap="2" overflow="hidden" {...props}>
<Flex gap="2" alignItems="center">
<CloseButton onClick={() => selected.clearValue()} />
{identity && <RelayFavicon relay={identity} size="sm" />}
<Heading size="md" isTruncated>
{identity}
</Heading>
<ButtonGroup ml="auto" variant="ghost" size="sm">
{identity && (
<IconButton
icon={<ExternalLinkIcon />}
as={RouterLink}
to={`/r/${encodeURIComponent(identity)}`}
aria-label="Open"
/>
)}
<DebugEventButton event={event} />
</ButtonGroup>
</Flex>
<Accordion allowToggle>
<AccordionItem>
<AccordionButton px="2">
<Box as="span" flex="1" textAlign="left">
Software info
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel px="2" pb="2" pt="0" display="flex" flexDirection="column" gap="2">
<Box>
<Text>NIPs:</Text>
<SupportedNIPs nips={nips} names />
</Box>
{software && (
<Box>
<Text>Software:</Text>
<Code isTruncated>{software}</Code>
<Text>
Version: <Code>{version}</Code>
</Text>
</Box>
)}
</AccordionPanel>
</AccordionItem>
{pubkeys.length > 0 && (
<AccordionItem>
<AccordionButton px="2">
<Box as="span" flex="1" textAlign="left">
Pubkeys
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel px="2" pb="2" pt="0">
{pubkeys.map(({ pubkey }) => (
<Flex gap="2" key={pubkey} alignItems="center">
<UserAvatarLink pubkey={pubkey} size="sm" />
<UserName isTruncated pubkey={pubkey} />
</Flex>
))}
</AccordionPanel>
</AccordionItem>
)}
<AccordionItem>
<AccordionButton px="2">
<Box as="span" flex="1" textAlign="left">
Miscellaneous info
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel px="2" pb="2" pt="0">
{Object.entries(misc).map(([label, values]) => (
<Text key={label}>
{label}: {values.join(", ")}
</Text>
))}
</AccordionPanel>
</AccordionItem>
</Accordion>
<PeopleListProvider initList="global">
<Flex overflow="auto" h="full" w="full" direction="column" gap="2">
{url && <RelayNotes relay={url} />}
</Flex>
</PeopleListProvider>
</Flex>
);
}

View File

@@ -0,0 +1,71 @@
import { useMemo, useState } from "react";
import { useThrottle } from "react-use";
import { NostrEvent } from "nostr-tools";
import { Box, Flex, FlexProps, Input, Text } from "@chakra-ui/react";
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import RelayStatusCard from "./relay-status-card";
/** returns a map of nip -> percent supported */
function computeCommonNips(events: NostrEvent[]) {
const common = new Map<number, number>();
for (const event of events) {
for (const tag of event.tags) {
if (tag[0] === "N" && tag[1]) {
const nip = parseInt(tag[1]);
if (Number.isFinite(nip)) common.set(nip, (common.get(nip) ?? 0) + 1);
}
}
}
for (const [nip, count] of common) {
common.set(nip, count / events.length);
}
return common;
}
function Row({ index, style, data }: ListChildComponentProps<NostrEvent[]>) {
return (
<Box style={style} pb="2">
<RelayStatusCard event={data[index]} h="full" />
</Box>
);
}
export default function RelayList({ events, ...props }: Omit<FlexProps, "children"> & { events: NostrEvent[] }) {
const [filter, setFilter] = useState("");
const filterThrottle = useThrottle(filter.toLocaleLowerCase(), 100);
const filtered = useMemo(() => {
if (filterThrottle.length >= 1)
return events.filter((event) =>
event.tags.some((t) => {
if (t[1]) return t[1].toLocaleLowerCase().includes(filterThrottle);
}),
);
else return events;
}, [filterThrottle, events]);
// const commonNips = useMemo(() => computeCommonNips(filtered), [filtered]);
return (
<Flex direction="column" gap="2" overflow="hidden" {...props}>
<Flex gap="2">
<Input type="search" placeholder="filter relays" value={filter} onChange={(e) => setFilter(e.target.value)} />
</Flex>
<Text fontSize="sm">{filtered.length} Relays</Text>
<Flex direction="column" flex={1}>
<AutoSizer>
{({ height, width }) => (
<List itemCount={filtered.length} itemSize={200} itemData={filtered} width={width} height={height}>
{Row}
</List>
)}
</AutoSizer>
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1,46 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { BoxProps } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import L from "leaflet";
import LeafletMap from "../../../map/components/leaflet-map";
import useEventMarkers, { getEventLatLng } from "../../../map/hooks/use-event-markers";
import { getEventUID } from "../../../../helpers/nostr/event";
import { SelectedContext } from "../selected-context";
export default function RelayMap({ events, ...props }: Omit<BoxProps, "children"> & { events: NostrEvent[] }) {
const [map, setMap] = useState<L.Map>();
const selected = useContext(SelectedContext);
const prev = useRef(selected.value);
const selectRelay = useCallback(
(event: NostrEvent) => {
const id = getEventUID(event);
if (prev.current !== id) {
prev.current = id;
selected.setValue(id);
}
},
[selected.setValue],
);
useEventMarkers(events, map, selectRelay);
const eventsRef = useRef(events);
eventsRef.current = events;
// center map when selected changes
useEffect(() => {
if (!map || selected.value === prev.current) return;
const selectedEvent = eventsRef.current.find((e) => getEventUID(e) === selected.value);
if (selectedEvent) {
const latLng = getEventLatLng(selectedEvent);
if (latLng) map.setView(latLng, 10);
}
}, [map, selected.value]);
return <LeafletMap onCreate={setMap} {...props} />;
}

View File

@@ -0,0 +1,71 @@
import { useContext } from "react";
import {
Badge,
Box,
Card,
CardBody,
CardFooter,
CardHeader,
CardProps,
Heading,
Link,
Spacer,
Text,
} from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { getEventUID, getTagValue } from "../../../../helpers/nostr/event";
import SupportedNIPs from "../../../relays/components/supported-nips";
import { SelectedContext } from "../selected-context";
import { RelayFavicon } from "../../../../components/relay-favicon";
import Timestamp from "../../../../components/timestamp";
const IgnoreNips = [1, 2, 4, 11, 12, 15, 16];
export default function RelayStatusCard({
event,
commonNips,
...props
}: Omit<CardProps, "children"> & {
event: NostrEvent;
commonNips?: Map<number, number>;
}) {
const selected = useContext(SelectedContext);
const identity = getTagValue(event, "d");
const network = getTagValue(event, "n");
const software = getTagValue(event, "s");
const countryName = event.tags.find((t) => t[0] === "l" && t[2] === "countryName")?.[1];
const host = event.tags.find((t) => t[0] === "l" && t[2] === "host.isp")?.[1];
const nips = event.tags
.filter((t) => t[0] === "N" && t[1])
.map((t) => parseInt(t[1]))
.filter((n) => Number.isFinite(n))
.filter((n) => !IgnoreNips.includes(n))
.filter((n) => (commonNips ? (commonNips.get(n) ?? 1) < 0.8 : true));
return (
<Card {...props}>
<CardHeader display="flex" alignItems="center" py="2" px="2" gap="2">
{identity && <RelayFavicon relay={identity} size="sm" />}
<Heading size="sm" isTruncated>
<Link onClick={() => selected.setValue(getEventUID(event))}>{identity}</Link>
</Heading>
<Timestamp timestamp={event.created_at} />
<Spacer />
<Badge>{network}</Badge>
</CardHeader>
<CardBody px="2" pt="0" pb="0" display="flex" gap="2" flexDirection="column">
<Box>
{countryName && <Text>Country: {countryName}</Text>}
{host && <Text>Host: {host}</Text>}
</Box>
</CardBody>
<CardFooter p="2">
<SupportedNIPs nips={nips} />
</CardFooter>
</Card>
);
}

View File

@@ -0,0 +1,109 @@
import { useEffect, useState } from "react";
import { Flex, Select } from "@chakra-ui/react";
import { Filter, matchFilters, NostrEvent } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import { useThrottle } from "react-use";
import PersistentSubscription from "../../../classes/persistent-subscription";
import BackButton from "../../../components/router/back-button";
import relayPoolService from "../../../services/relay-pool";
import RelayList from "./components/relay-list";
import useRouteStateValue from "../../../hooks/use-route-state-value";
import RelayMap from "./components/relay-map";
import RelayStatusDetails from "./components/relay-details";
import { getTagValue, sortByDate } from "../../../helpers/nostr/event";
import { SelectedContext } from "./selected-context";
import CountyPicker from "../../../components/county-picker";
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
export default function RelayDiscoveryView() {
const showMap = useBreakpointValue({ base: false, lg: true });
const [discoveryRelay, setDiscoveryRelay] = useState("wss://history.nostr.watch/");
const [monitor, setMonitor] = useState("9bbbb845e5b6c831c29789900769843ab43bb5047abe697870cb50b6fc9bf923");
const [network, setNetwork] = useState("");
const [county, setCounty] = useState("");
const selected = useRouteStateValue<string>("selected");
const [events, setEvents] = useState<Record<string, NostrEvent>>({});
const [subscription, setSubscription] = useState<PersistentSubscription>();
// recreate the subscription when the relay changes
useEffect(() => {
if (subscription && !subscription.closed) subscription.close();
const relay = relayPoolService.requestRelay(discoveryRelay);
const sub = new PersistentSubscription(relay, {
onevent: (event) => {
if (getTagValue(event, "d")) {
setEvents((arr) => ({ ...arr, [getEventUID(event)]: event }));
}
},
});
setSubscription(sub);
}, [discoveryRelay, setEvents]);
useEffect(() => {
if (!subscription) return;
const filter: Filter = {
authors: [monitor],
kinds: [30166],
// set from https://github.com/nostr-protocol/nips/pull/230#pullrequestreview-2290873405
since: Math.round(Date.now() / 1000) - 60 * 60 * 2,
};
if (network) filter["#n"] = [network];
if (county) {
// if (filter["#L"]) filter["#L"].push("countryCode");
// else filter["#L"] = ["countryCode"];
if (filter["#l"]) filter["#l"].push(county);
else filter["#l"] = [county];
}
subscription.filters = [filter];
// remove non matching events
setEvents((dir) => {
const newDir: typeof dir = {};
for (const [uid, event] of Object.entries(dir)) {
if (matchFilters(subscription.filters, event)) newDir[uid] = event;
}
return newDir;
});
// update subscription
subscription.update();
}, [subscription, monitor, network, county, setEvents]);
// throttle updates to map
const eventsThrottle = useThrottle(Object.values(events), 250);
return (
<SelectedContext.Provider value={selected}>
<Flex direction="column" overflow="hidden" h="100vh" gap="2" p="2">
<Flex gap="2">
<BackButton />
<Select value={network} onChange={(e) => setNetwork(e.target.value)} w="auto">
<option value="">All</option>
<option value="clearnet">clearnet</option>
<option value="tor">Tor</option>
<option value="i2p">I2P</option>
</Select>
<CountyPicker value={county} onChange={(e) => setCounty(e.target.value)} w="auto" />
</Flex>
<Flex gap="2" overflow="hidden" h="full">
{selected.value && events[selected.value] ? (
<RelayStatusDetails w={{ base: "full", lg: "lg" }} event={events[selected.value]} flexShrink={0} />
) : (
<RelayList w={{ base: "full", lg: "lg" }} flexShrink={0} events={eventsThrottle} />
)}
{showMap && <RelayMap events={eventsThrottle} />}
</Flex>
</Flex>
</SelectedContext.Provider>
);
}

View File

@@ -0,0 +1,8 @@
import { createContext } from "react";
import useRouteStateValue from "../../../hooks/use-route-state-value";
export const SelectedContext = createContext<ReturnType<typeof useRouteStateValue<string>>>({
value: "",
setValue() {},
clearValue() {},
});

View File

@@ -51,7 +51,7 @@ function RenderRedirect({ event, link }: { event?: NostrEvent; link: string }) {
let k = decoded.data.kind || event?.kind; let k = decoded.data.kind || event?.kind;
if (k === kinds.ShortTextNote) return <Navigate to={`/n/${link}`} replace />; if (k === kinds.ShortTextNote) return <Navigate to={`/n/${link}`} replace />;
if (k === TORRENT_KIND) return <Navigate to={`/torrents/${link}`} replace />; if (k === TORRENT_KIND) return <Navigate to={`/torrents/${link}`} replace />;
if (k === STREAM_KIND) return <Navigate to={`/streams/${link}`} replace />; if (k === kinds.LiveEvent) return <Navigate to={`/streams/${link}`} replace />;
if (k === EMOJI_PACK_KIND) return <Navigate to={`/emojis/${link}`} replace />; if (k === EMOJI_PACK_KIND) return <Navigate to={`/emojis/${link}`} replace />;
if (k === NOTE_LIST_KIND) return <Navigate to={`/lists/${link}`} replace />; if (k === NOTE_LIST_KIND) return <Navigate to={`/lists/${link}`} replace />;
if (k === PEOPLE_LIST_KIND) return <Navigate to={`/lists/${link}`} replace />; if (k === PEOPLE_LIST_KIND) return <Navigate to={`/lists/${link}`} replace />;

View File

@@ -0,0 +1,48 @@
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { Box, BoxProps } from "@chakra-ui/react";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import "leaflet.locatecontrol/dist/L.Control.Locate.min.css";
import "leaflet.locatecontrol";
export type EventMapProps = Omit<BoxProps, "children"> & {
onMove?: (map: L.Map) => void;
onCreate?: (map: L.Map) => void;
mapRef?: MutableRefObject<L.Map>;
};
export default function LeafletMap({ onMove, mapRef, onCreate, ...props }: EventMapProps) {
const ref = useRef<HTMLDivElement | null>(null);
const [map, setMap] = useState<L.Map>();
const onMoveRef = useRef(onMove);
onMoveRef.current = onMove;
useEffect(() => {
if (!ref.current) return;
const map = L.map(ref.current).setView([39, -97], 4);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "© OpenStreetMap",
}).addTo(map);
L.control.locate().addTo(map);
map.addEventListener("move", () => {
if (onMoveRef.current) onMoveRef.current(map);
});
setMap(map);
if (mapRef) mapRef.current = map;
if (onCreate) onCreate(map);
return () => {
setMap(undefined);
map.remove();
};
}, []);
return <Box w="full" ref={ref} {...props} />;
}

View File

@@ -0,0 +1,63 @@
import { useEffect, useRef } from "react";
import { NostrEvent } from "nostr-tools";
import L from "leaflet";
import ngeohash from "ngeohash";
import iconUrl from "../marker-icon.svg";
const pinIcon = L.icon({ iconUrl, iconSize: [32, 32], iconAnchor: [16, 32] });
export function getEventGeohash(event: NostrEvent) {
let hash = "";
for (const tag of event.tags) {
if (tag[0] === "g" && tag[1] && tag[1].length > hash.length) {
hash = tag[1];
}
}
return hash || null;
}
export function getEventLatLng(event: NostrEvent): [number, number] | undefined {
const geohash = getEventGeohash(event);
if (!geohash) return;
const latLng = ngeohash.decode(geohash);
return [latLng.latitude, latLng.longitude];
}
export default function useEventMarkers(events: NostrEvent[], map?: L.Map, onClick?: (event: NostrEvent) => void) {
const markers = useRef<Record<string, L.Marker>>({});
// create markers
useEffect(() => {
for (const event of events) {
const latLng = getEventLatLng(event);
if (!latLng) continue;
const marker = markers.current[event.id] || L.marker([0, 0], { icon: pinIcon });
marker.setLatLng(latLng);
if (onClick) marker.addEventListener("click", () => onClick(event));
markers.current[event.id] = marker;
}
}, [events]);
// add makers to map
useEffect(() => {
if (!map) return;
const ids = events.map((e) => e.id);
for (const [id, marker] of Object.entries(markers.current)) {
if (ids.includes(id)) marker?.addTo(map);
else marker?.remove();
}
return () => {
for (const [id, marker] of Object.entries(markers.current)) {
marker?.removeFrom(map);
}
};
}, [map, events]);
}

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { Box, Button, Flex } from "@chakra-ui/react"; import { Button, Flex } from "@chakra-ui/react";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
import ngeohash from "ngeohash"; import ngeohash from "ngeohash";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
@@ -17,9 +17,8 @@ import TimelineActionAndStatus from "../../components/timeline/timeline-action-a
import { NostrEvent } from "../../types/nostr-event"; import { NostrEvent } from "../../types/nostr-event";
import MapTimeline from "./timeline"; import MapTimeline from "./timeline";
import iconUrl from "./marker-icon.svg"; import useEventMarkers from "./hooks/use-event-markers";
import useRouteSearchValue from "../../hooks/use-route-search-value"; import LeafletMap from "./components/leaflet-map";
const pinIcon = L.icon({ iconUrl, iconSize: [32, 32], iconAnchor: [16, 32] });
function getPrecision(zoom: number) { function getPrecision(zoom: number) {
if (zoom <= 4) return 1; if (zoom <= 4) return 1;
@@ -31,91 +30,28 @@ function getPrecision(zoom: number) {
if (zoom <= 18) return 7; if (zoom <= 18) return 7;
return 7; return 7;
} }
function getEventGeohash(event: NostrEvent) {
let hash = "";
for (const tag of event.tags) {
if (tag[0] === "g" && tag[1] && tag[1].length > hash.length) {
hash = tag[1];
}
}
return hash || null;
}
function useEventMarkers(events: NostrEvent[], map?: L.Map, onClick?: (event: NostrEvent) => void) {
const markers = useRef<Record<string, L.Marker>>({});
// create markers
useEffect(() => {
for (const event of events) {
const geohash = getEventGeohash(event);
if (!geohash) continue;
const marker = markers.current[event.id] || L.marker([0, 0], { icon: pinIcon });
const latLng = ngeohash.decode(geohash);
marker.setLatLng([latLng.latitude, latLng.longitude]);
if (onClick) {
marker.addEventListener("click", () => onClick(event));
}
markers.current[event.id] = marker;
}
}, [events]);
// add makers to map
useEffect(() => {
if (!map) return;
const ids = events.map((e) => e.id);
for (const [id, marker] of Object.entries(markers.current)) {
if (ids.includes(id)) marker?.addTo(map);
else marker?.remove();
}
return () => {
for (const [id, marker] of Object.entries(markers.current)) {
marker?.removeFrom(map);
}
};
}, [map, events]);
}
export default function MapView() { export default function MapView() {
const navigate = useNavigate(); const navigate = useNavigate();
const ref = useRef<HTMLDivElement | null>(null);
const [map, setMap] = useState<L.Map>(); const [map, setMap] = useState<L.Map>();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
// listen for map move event
useEffect(() => { useEffect(() => {
if (!ref.current) return; if (!map) return;
const map = L.map(ref.current).setView([39, -97], 4);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { const listener = debounce(() => {
maxZoom: 19, const center = map.getCenter();
attribution: "© OpenStreetMap", const hash = ngeohash.encode(center.lat, center.lng, 5);
}).addTo(map);
L.control.locate().addTo(map); setSearchParams({ hash }, { replace: true });
}, 1000);
map.addEventListener(
"move",
debounce(() => {
const center = map.getCenter();
const hash = ngeohash.encode(center.lat, center.lng, 5);
setSearchParams({ hash }, { replace: true });
}, 1000),
);
setMap(map);
map.addEventListener("move", listener);
return () => { return () => {
setMap(undefined); map.removeEventListener("move", listener);
map.remove();
}; };
}, []); });
const [cells, setCells] = useState<string[]>([]); const [cells, setCells] = useState<string[]>([]);
@@ -167,7 +103,7 @@ export default function MapView() {
</Flex> </Flex>
</Flex> </Flex>
<Box w="full" ref={ref} h={{ base: "50vh", lg: "100vh" }} /> <LeafletMap onCreate={setMap} h={{ base: "50vh", lg: "100vh" }} />
</Flex> </Flex>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Flex, Tag, Tooltip } from "@chakra-ui/react"; import { Flex, FlexProps, Tag, Tooltip } from "@chakra-ui/react";
// copied from github // copied from github
export const NIP_NAMES: Record<string, string> = { export const NIP_NAMES: Record<string, string> = {
@@ -13,9 +13,12 @@ export const NIP_NAMES: Record<string, string> = {
"09": "Event Deletion", "09": "Event Deletion",
"10": "Conventions for clients' use of e and p tags in text events", "10": "Conventions for clients' use of e and p tags in text events",
"11": "Relay Information Document", "11": "Relay Information Document",
"12": "Generic Tag Queries",
"13": "Proof of Work", "13": "Proof of Work",
"14": "Subject tag in text events", "14": "Subject tag in Text events",
"15": "Nostr Marketplace", "15": "Nostr Marketplace",
"16": "Event Treatment",
"17": "Private Direct Messages",
"18": "Reposts", "18": "Reposts",
"19": "bech32-encoded entities", "19": "bech32-encoded entities",
"20": "Command Results", "20": "Command Results",
@@ -30,28 +33,36 @@ export const NIP_NAMES: Record<string, string> = {
"30": "Custom Emoji", "30": "Custom Emoji",
"31": "Dealing with Unknown Events", "31": "Dealing with Unknown Events",
"32": "Labeling", "32": "Labeling",
"33": "Parameterized Replaceable Events",
"34": "git stuff", "34": "git stuff",
"36": "Sensitive Content", "35": "Torrents",
"36": "Sensitive Content / Content Warning",
"38": "User Statuses", "38": "User Statuses",
"39": "External Identities in Profiles", "39": "External Identities in Profiles",
"40": "Expiration Timestamp", "40": "Expiration Timestamp",
"42": "Authentication of clients to relays", "42": "Authentication of clients to relays",
"44": "Versioned Encryption", "44": "Encrypted Payloads (Versioned)",
"45": "Counting results", "45": "Counting results",
"46": "Nostr Connect", "46": "Nostr Remote Signing",
"47": "Wallet Connect", "47": "Nostr Wallet Connect",
"48": "Proxy Tags", "48": "Proxy Tags",
"49": "Private Key Encryption", "49": "Private Key Encryption",
"50": "Search Capability", "50": "Search Capability",
"51": "Lists", "51": "Lists",
"52": "Calendar Events", "52": "Calendar Events",
"53": "Live Activities", "53": "Live Activities",
"54": "Wiki",
"55": "Android Signer Application",
"56": "Reporting", "56": "Reporting",
"57": "Lightning Zaps", "57": "Lightning Zaps",
"58": "Badges", "58": "Badges",
"59": "Gift Wrap", "59": "Gift Wrap",
"64": "Chess",
"65": "Relay List Metadata", "65": "Relay List Metadata",
"70": "Protected Events",
"71": "Video Events",
"72": "Moderated Communities", "72": "Moderated Communities",
"73": "External Content IDs",
"75": "Zap Goals", "75": "Zap Goals",
"78": "Application-specific data", "78": "Application-specific data",
"84": "Highlights", "84": "Highlights",
@@ -64,23 +75,28 @@ export const NIP_NAMES: Record<string, string> = {
"99": "Classified Listings", "99": "Classified Listings",
}; };
function NipTag({ nip }: { nip: number }) { function NipTag({ nip, name }: { nip: number; name?: boolean }) {
const nipStr = String(nip).padStart(2, "0"); const nipStr = String(nip).padStart(2, "0");
const nipNumber = `NIP-${nip}`;
return ( return (
<Tooltip label={NIP_NAMES[nipStr]}> <Tooltip label={NIP_NAMES[nipStr]}>
<Tag as="a" target="_blank" href={`https://github.com/nostr-protocol/nips/blob/master/${nipStr}.md`}> <Tag as="a" target="_blank" href={`https://github.com/nostr-protocol/nips/blob/master/${nipStr}.md`}>
NIP-{nip} {name ? NIP_NAMES[nipStr] ?? nipNumber : nipNumber}
</Tag> </Tag>
</Tooltip> </Tooltip>
); );
} }
export default function SupportedNIPs({ nips }: { nips: number[] }) { export default function SupportedNIPs({
nips,
names,
...props
}: Omit<FlexProps, "children"> & { nips: number[]; names?: boolean }) {
return ( return (
<Flex gap="2" wrap="wrap"> <Flex gap="2" wrap="wrap" {...props}>
{nips.map((nip) => ( {nips.map((nip) => (
<NipTag key={nip} nip={nip} /> <NipTag key={nip} nip={nip} name={names} />
))} ))}
</Flex> </Flex>
); );

View File

@@ -1,4 +1,4 @@
import { Button, Card, Flex, Heading, Text, useColorModeValue, useTheme } from "@chakra-ui/react"; import { Button, Card, Flex, Heading, Text, useColorModeValue, useTheme, useToast } from "@chakra-ui/react";
import { import {
Chart as ChartJS, Chart as ChartJS,
@@ -97,6 +97,7 @@ function buildLineChartData(events: NostrEvent[], timeBlock = 60 * 60): ChartDat
export default function RelayDetailsTab({ relay }: { relay: string }) { export default function RelayDetailsTab({ relay }: { relay: string }) {
useAppTitle(`${relay} - Details`); useAppTitle(`${relay} - Details`);
const toast = useToast();
const theme = useTheme(); const theme = useTheme();
const token = theme.semanticTokens.colors["chakra-body-text"]; const token = theme.semanticTokens.colors["chakra-body-text"];
const color = useColorModeValue(token._light, token._dark) as string; const color = useColorModeValue(token._light, token._dark) as string;
@@ -118,7 +119,8 @@ export default function RelayDetailsTab({ relay }: { relay: string }) {
throttleUpdate(); throttleUpdate();
}, },
oneose: () => sub.close(), oneose: () => sub.close(),
onclose: () => { onclose: (reason) => {
if (reason !== "closed by caller") toast({ status: "error", description: reason });
setLoading(false); setLoading(false);
}, },
}); });

View File

@@ -34,6 +34,7 @@ export default function RelayNotes({ relay }: { relay: string }) {
); );
const timeline = useTimelineLoader(`${relay}-notes`, [relay], filter ? { ...filter, kinds: k } : undefined, { const timeline = useTimelineLoader(`${relay}-notes`, [relay], filter ? { ...filter, kinds: k } : undefined, {
eventFilter, eventFilter,
useCache: false,
}); });
const header = ( const header = (

View File

@@ -1,6 +1,6 @@
import { memo } from "react"; import { memo } from "react";
import { LiveVideoPlayer } from "../../../components/live-video-player"; import LiveVideoPlayer from "../../../components/live-video-player";
import { ParsedStream } from "../../../helpers/nostr/stream"; import { ParsedStream } from "../../../helpers/nostr/stream";
function LiveVideoCard({ stream }: { stream: ParsedStream }) { function LiveVideoCard({ stream }: { stream: ParsedStream }) {

View File

@@ -17,13 +17,13 @@ import {
useDisclosure, useDisclosure,
} from "@chakra-ui/react"; } 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 { kinds, nip19 } from "nostr-tools";
import { Global, css } from "@emotion/react"; import { Global, css } from "@emotion/react";
import { ParsedStream, STREAM_KIND, parseStreamEvent } from "../../../helpers/nostr/stream"; import { ParsedStream, parseStreamEvent } from "../../../helpers/nostr/stream";
import { useReadRelays } from "../../../hooks/use-client-relays"; import { useReadRelays } from "../../../hooks/use-client-relays";
import { unique } from "../../../helpers/array"; import { unique } from "../../../helpers/array";
import { LiveVideoPlayer } from "../../../components/live-video-player"; import LiveVideoPlayer from "../../../components/live-video-player";
import StreamChat, { ChatDisplayMode } from "./stream-chat"; import StreamChat, { ChatDisplayMode } from "./stream-chat";
import UserAvatarLink from "../../../components/user/user-avatar-link"; import UserAvatarLink from "../../../components/user/user-avatar-link";
import UserLink from "../../../components/user/user-link"; import UserLink from "../../../components/user/user-link";
@@ -248,7 +248,7 @@ export default function StreamView() {
try { try {
const parsed = nip19.decode(naddr); const parsed = nip19.decode(naddr);
if (parsed.type !== "naddr") throw new Error("Invalid stream address"); if (parsed.type !== "naddr") throw new Error("Invalid stream address");
if (parsed.data.kind !== STREAM_KIND) throw new Error("Invalid stream kind"); if (parsed.data.kind !== kinds.LiveEvent) throw new Error("Invalid stream kind");
const addrRelays = parsed.data.relays ?? []; const addrRelays = parsed.data.relays ?? [];
return replaceableEventsService.requestEvent( return replaceableEventsService.requestEvent(

View File

@@ -22,8 +22,8 @@ export default defineConfig({
// enabled: true, // enabled: true,
// }, // },
workbox: { workbox: {
// This increase the cache limit to 3mB // This increase the cache limit to 4mB
maximumFileSizeToCacheInBytes: 2097152 * 1.5, maximumFileSizeToCacheInBytes: 2097152 * 2,
}, },
manifest: { manifest: {
name: "noStrudel", name: "noStrudel",

View File

@@ -3075,6 +3075,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-window@^1.8.8":
version "1.8.8"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3"
integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.35", "@types/react@^18.2.22", "@types/react@^18.2.45": "@types/react@*", "@types/react@^16.9.35", "@types/react@^18.2.22", "@types/react@^18.2.45":
version "18.2.55" version "18.2.55"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.55.tgz#38141821b7084404b5013742bc4ae08e44da7a67" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.55.tgz#38141821b7084404b5013742bc4ae08e44da7a67"
@@ -4196,6 +4203,11 @@ devlop@^1.0.0, devlop@^1.1.0:
dependencies: dependencies:
dequal "^2.0.0" dequal "^2.0.0"
diacritics@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==
diff@^5.1.0: diff@^5.1.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
@@ -4934,6 +4946,13 @@ hyphenate-style-name@^1.0.3:
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
i18n-iso-countries@^7.12.0:
version "7.12.0"
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.12.0.tgz#e189d85a505ee025f0f48b5ccc35fa66c436e99c"
integrity sha512-NDFf5j/raA5JrcPT/NcHP3RUMH7TkdkxQKAKdvDlgb+MS296WJzzqvV0Y5uwavSm7A6oYvBeSV0AxoHdDiHIiw==
dependencies:
diacritics "1.3.0"
iconv-lite@^0.4.24: iconv-lite@^0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -5768,6 +5787,11 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
"memoize-one@>=3.1.1 <6":
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memoize-one@^6.0.0: memoize-one@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
@@ -6866,16 +6890,24 @@ react-use@^17.4.0:
ts-easing "^0.2.0" ts-easing "^0.2.0"
tslib "^2.1.0" tslib "^2.1.0"
react-virtualized-auto-sizer@^1.0.20: react-virtualized-auto-sizer@^1.0.24:
version "1.0.22" version "1.0.24"
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.22.tgz#5aca94a45c91c9fe6ec57d711a65e6a55d0da71b" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz#3ebdc92f4b05ad65693b3cc8e7d8dd54924c0227"
integrity sha512-2CGT/4rZ6jvVkKqzJGnZlyQxj4rWPKAwZR80vMlmpYToN18xaB0yIODOoBltWZLbSgpHBpIk0Ae1nrVO9hVClA== integrity sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==
react-webcam@^5.0.1: react-webcam@^5.0.1:
version "5.2.4" version "5.2.4"
resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-5.2.4.tgz#714b4460ea43ac7ed081824299cd2a580f764478" resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-5.2.4.tgz#714b4460ea43ac7ed081824299cd2a580f764478"
integrity sha512-Qqj14t68Ke1eoEYjFde+N48HtuIJg0ePIQRpFww9eZt5oBcDpe/l60h+m3VRFJAR5/E3dOhSU5R8EJEcdCq/Eg== integrity sha512-Qqj14t68Ke1eoEYjFde+N48HtuIJg0ePIQRpFww9eZt5oBcDpe/l60h+m3VRFJAR5/E3dOhSU5R8EJEcdCq/Eg==
react-window@^1.8.10:
version "1.8.10"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03"
integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@^18.2.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"