Support using nostr-relay-tray as cache relay

This commit is contained in:
hzrd149 2024-02-01 11:08:41 +00:00
parent 96fc7cde44
commit f965281520
10 changed files with 354 additions and 113 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add support for using nostr-relay-tray as cache relay

View File

@ -86,6 +86,9 @@ import VideoDetailsView from "./views/videos/video";
import BookmarksView from "./views/bookmarks";
import MailboxesView from "./views/mailboxes";
import RequireReadRelays from "./providers/route/require-read-relays";
import CacheRelayView from "./views/relays/cache";
import RelaySetView from "./views/relays/relay-set";
import AppRelays from "./views/relays/app";
const TracksView = lazy(() => import("./views/tracks"));
const UserTracksTab = lazy(() => import("./views/user/tracks"));
const UserVideosTab = lazy(() => import("./views/user/videos"));
@ -255,11 +258,13 @@ const router = createHashRouter([
{ path: "settings", element: <SettingsView /> },
{
path: "relays",
element: <RelaysView />,
children: [
{ path: "", element: <RelaysView /> },
{ path: "popular", element: <PopularRelaysView /> },
{ path: "reviews", element: <RelayReviewsView /> },
{ path: "", element: <AppRelays /> },
{ path: "app", element: <AppRelays /> },
{ path: "cache", element: <CacheRelayView /> },
{ path: "sets", element: <BrowseRelaySetsView /> },
{ path: ":id", element: <RelaySetView /> },
],
},
{ path: "r/:relay", element: <RelayView /> },

View File

@ -1,12 +1,12 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { IconButton, IconButtonProps } from "@chakra-ui/react";
import { ChevronLeftIcon } from "./icons";
import { useNavigate } from "react-router-dom";
export default function BackButton({ ...props }: Omit<ButtonProps, "onClick" | "children">) {
export default function BackButton({ ...props }: Omit<IconButtonProps, "onClick" | "children" | "aria-label">) {
const navigate = useNavigate();
return (
<Button leftIcon={<ChevronLeftIcon />} {...props} onClick={() => navigate(-1)}>
<IconButton icon={<ChevronLeftIcon />} aria-label="Back" {...props} onClick={() => navigate(-1)}>
Back
</Button>
</IconButton>
);
}

View File

@ -1,10 +1,10 @@
import { useCallback } from "react";
import { kinds } from "nostr-tools";
import { useReadRelays } from "./use-client-relays";
import useSubject from "./use-subject";
import useTimelineLoader from "./use-timeline-loader";
import { NostrEvent, isRTag } from "../types/nostr-event";
import { kinds } from "nostr-tools";
export default function useUserRelaySets(pubkey?: string, additionalRelays?: Iterable<string>) {
const readRelays = useReadRelays(additionalRelays);

View File

@ -2,6 +2,22 @@ import { CacheRelay, openDB, pruneLastUsed } from "nostr-idb";
import { Relay } from "nostr-tools";
import { logger } from "../helpers/debug";
import _throttle from "lodash.throttle";
import { safeRelayUrl } from "../helpers/relay";
export const NOSTR_RELAY_TRAY_URL = "ws://localhost:4869/";
export async function checkNostrRelayTray() {
return new Promise((res) => {
const test = new Relay(NOSTR_RELAY_TRAY_URL);
test
.connect()
.then(() => {
test.close();
res(true);
})
.catch(() => res(false));
});
}
const log = logger.extend(`LocalRelay`);
@ -15,39 +31,58 @@ if (paramRelay) {
}
const storedCacheRelayURL = localStorage.getItem("localRelay");
const url = (storedCacheRelayURL && new URL(storedCacheRelayURL)) || new URL("/local-relay", location.href);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
/** @deprecated */
const localRelayURL = (storedCacheRelayURL && new URL(storedCacheRelayURL)) || new URL("/local-relay", location.href);
localRelayURL.protocol = localRelayURL.protocol === "https:" ? "wss:" : "ws:";
/** @deprecated */
export const LOCAL_CACHE_RELAY_ENABLED = !!window.CACHE_RELAY_ENABLED || !!localStorage.getItem("localRelay");
/** @deprecated */
export const LOCAL_CACHE_RELAY = url.toString();
export const LOCAL_CACHE_RELAY = localRelayURL.toString();
export const localDatabase = await openDB();
function createRelay() {
if (LOCAL_CACHE_RELAY_ENABLED) {
log(`Using ${LOCAL_CACHE_RELAY}`);
return new Relay(LOCAL_CACHE_RELAY);
} else {
log(`Using IndexedDB`);
const stored = localStorage.getItem("localRelay");
if (!stored || stored.startsWith("nostr-idb://")) {
return new CacheRelay(localDatabase, { maxEvents: 10000 });
} else if (safeRelayUrl(stored)) {
return new Relay(safeRelayUrl(stored)!);
} else if (window.CACHE_RELAY_ENABLED) {
return new Relay(new URL("/local-relay", location.href).toString());
}
return new CacheRelay(localDatabase, { maxEvents: 10000 });
// if (LOCAL_CACHE_RELAY_ENABLED) {
// log(`Using ${LOCAL_CACHE_RELAY}`);
// return new Relay(LOCAL_CACHE_RELAY);
// } else {
// log(`Using IndexedDB`);
// return new CacheRelay(localDatabase, { maxEvents: 10000 });
// }
}
async function connectRelay() {
const relay = createRelay();
try {
await relay.connect();
log("Connected");
return relay;
} catch (e) {
log("Failed to connect to local relay, falling back to internal");
return new CacheRelay(localDatabase, { maxEvents: 10000 });
}
}
export const localRelay = createRelay();
export const localRelay = await connectRelay();
function pruneLocalDatabase() {
if (localRelay instanceof CacheRelay) {
pruneLastUsed(localRelay.db, 20_000);
}
}
// connect without waiting
localRelay.connect().then(() => {
log("Connected");
pruneLocalDatabase();
});
// keep the relay connection alive
setInterval(() => {

View File

@ -0,0 +1,27 @@
import { Button, Flex, Heading } from "@chakra-ui/react";
import useSubject from "../../../hooks/use-subject";
import { offlineMode } from "../../../services/offline-mode";
import WifiOff from "../../../components/icons/wifi-off";
import Wifi from "../../../components/icons/wifi";
import BackButton from "../../../components/back-button";
export default function AppRelays() {
const offline = useSubject(offlineMode);
return (
<Flex gap="2" direction="column" overflow="auto hidden" flex={1}>
<Flex gap="2" alignItems="center">
<BackButton hideFrom="lg" size="sm" />
<Heading size="md">App Relays</Heading>
<Button
onClick={() => offlineMode.next(!offline)}
leftIcon={offline ? <WifiOff /> : <Wifi />}
ml="auto"
size={{ base: "sm", lg: "md" }}
>
{offline ? "Offline" : "Online"}
</Button>
</Flex>
</Flex>
);
}

85
src/views/relays/cache/index.tsx vendored Normal file
View File

@ -0,0 +1,85 @@
import { Button, Card, CardBody, CardHeader, Flex, Heading, Link, Text } from "@chakra-ui/react";
import BackButton from "../../../components/back-button";
import { useAsync } from "react-use";
import { NOSTR_RELAY_TRAY_URL, checkNostrRelayTray, localRelay } from "../../../services/local-relay";
import { CacheRelay } from "nostr-idb";
function InternalRelay() {
const enabled = localRelay instanceof CacheRelay;
const enable = () => {
localStorage.setItem("localRelay", "nostr-idb://internal");
location.reload();
};
return (
<Card borderColor={enabled ? "primary.500" : undefined} variant="outline">
<CardHeader p="4" display="flex" gap="2" alignItems="center">
<Heading size="md">Browser Cache</Heading>
<Button size="sm" colorScheme="primary" ml="auto" onClick={enable} isDisabled={enabled}>
{enabled ? "Enabled" : "Enable"}
</Button>
</CardHeader>
<CardBody p="4" pt="0">
<Text mb="2">Use the browsers built-in database to cache events.</Text>
<Text>Maximum capacity: 10k events</Text>
<Text>Performance: Usable, but limited by the browser</Text>
</CardBody>
</Card>
);
}
function NostrRelayTray() {
const { value: available, loading: checking } = useAsync(checkNostrRelayTray);
const enabled = localRelay.url.startsWith(NOSTR_RELAY_TRAY_URL);
const enable = () => {
localStorage.setItem("localRelay", NOSTR_RELAY_TRAY_URL);
location.reload();
};
return (
<Card borderColor={enabled ? "primary.500" : undefined} variant="outline">
<CardHeader p="4" display="flex" gap="2" alignItems="center">
<Heading size="md">Nostr Relay Tray</Heading>
<Link color="blue.500" href="https://github.com/CodyTseng/nostr-relay-tray" isExternal>
GitHub
</Link>
{available ? (
<Button size="sm" colorScheme="primary" ml="auto" isLoading={checking} onClick={enable} isDisabled={enabled}>
{enabled ? "Enabled" : "Enable"}
</Button>
) : (
<Button
as={Link}
isExternal
href="https://github.com/CodyTseng/nostr-relay-tray"
colorScheme="blue"
size="sm"
ml="auto"
>
Get the app
</Button>
)}
</CardHeader>
<CardBody p="4" pt="0">
<Text mb="2">A cool little app that runs a local relay in your systems tray</Text>
<Text>Maximum capacity: Unlimited</Text>
<Text>Performance: As fast as your computer</Text>
</CardBody>
</Card>
);
}
export default function CacheRelayView() {
return (
<Flex gap="2" direction="column" flex={1}>
<Flex gap="2" alignItems="center">
<BackButton hideFrom="lg" size="sm" />
<Heading size="lg" my="1">
Cache Relay
</Heading>
</Flex>
<InternalRelay />
<NostrRelayTray />
</Flex>
);
}

View File

@ -0,0 +1,101 @@
import { useDeferredValue, useMemo, useState } from "react";
import { useAsync } from "react-use";
import { Link as RouterLink } from "react-router-dom";
import { Button, Flex, Heading, Input, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react";
import relayPoolService from "../../services/relay-pool";
import AddCustomRelayModal from "./components/add-custom-modal";
import RelayCard from "./components/relay-card";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import { ErrorBoundary } from "../../components/error-boundary";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { isValidRelayURL } from "../../helpers/relay";
import { useReadRelays, useWriteRelays } from "../../hooks/use-client-relays";
import { offlineMode } from "../../services/offline-mode";
import useSubject from "../../hooks/use-subject";
import Wifi from "../../components/icons/wifi";
import WifiOff from "../../components/icons/wifi-off";
export default function RelaysView() {
const [search, setSearch] = useState("");
const deboundedSearch = useDeferredValue(search);
const isSearching = deboundedSearch.length > 2;
const addRelayModal = useDisclosure();
const offline = useSubject(offlineMode);
const readRelays = useReadRelays();
const writeRelays = useWriteRelays();
const discoveredRelays = relayPoolService
.getRelays()
.filter((r) => !readRelays.has(r.url) && !writeRelays.has(r.url))
.map((r) => r.url)
.filter(isValidRelayURL);
const { value: onlineRelays = [] } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>),
);
const filteredRelays = useMemo(() => {
if (isSearching) {
return onlineRelays.filter((url) => url.toLowerCase().includes(deboundedSearch.toLowerCase()));
}
return [...readRelays];
}, [isSearching, deboundedSearch, onlineRelays, readRelays]);
return (
<VerticalPageLayout>
<Flex alignItems="center" gap="2" wrap="wrap">
<Input type="search" placeholder="search" value={search} onChange={(e) => setSearch(e.target.value)} w="auto" />
<Button onClick={() => offlineMode.next(!offline)} leftIcon={offline ? <WifiOff /> : <Wifi />}>
{offline ? "Offline" : "Online"}
</Button>
<Spacer />
<Button as={RouterLink} to="/relays/popular">
Popular Relays
</Button>
<Button as={RouterLink} to="/relays/reviews">
Browse Reviews
</Button>
<Button colorScheme="primary" onClick={addRelayModal.onOpen}>
Add Custom
</Button>
</Flex>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{filteredRelays.map((url) => (
<ErrorBoundary>
<RelayCard key={url} url={url} variant="outline" />
</ErrorBoundary>
))}
</SimpleGrid>
{discoveredRelays.length > 0 && !isSearching && (
<>
<Heading size="lg" my="2">
Discovered Relays
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{discoveredRelays.map((url) => (
<ErrorBoundary>
<RelayCard key={url} url={url} variant="outline" />
</ErrorBoundary>
))}
</SimpleGrid>
</>
)}
{addRelayModal.isOpen && (
<AddCustomRelayModal
isOpen
onClose={addRelayModal.onClose}
size="2xl"
onSubmit={(url) => {
clientRelaysService.addRelay(url, RelayMode.ALL);
addRelayModal.onClose();
}}
/>
)}
</VerticalPageLayout>
);
}

View File

@ -1,101 +1,75 @@
import { useDeferredValue, useMemo, useState } from "react";
import { useAsync } from "react-use";
import { Link as RouterLink } from "react-router-dom";
import { Button, Flex, Heading, Input, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react";
import { useState } from "react";
import { Outlet, Link as RouterLink, useLocation, useMatch } from "react-router-dom";
import { Button, Divider, Flex, Heading, VStack } from "@chakra-ui/react";
import relayPoolService from "../../services/relay-pool";
import AddCustomRelayModal from "./components/add-custom-modal";
import RelayCard from "./components/relay-card";
import clientRelaysService from "../../services/client-relays";
import { RelayMode } from "../../classes/relay";
import { ErrorBoundary } from "../../components/error-boundary";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { isValidRelayURL } from "../../helpers/relay";
import { useReadRelays, useWriteRelays } from "../../hooks/use-client-relays";
import { offlineMode } from "../../services/offline-mode";
import useSubject from "../../hooks/use-subject";
import Wifi from "../../components/icons/wifi";
import WifiOff from "../../components/icons/wifi-off";
import useCurrentAccount from "../../hooks/use-current-account";
import useUserRelaySets from "../../hooks/use-user-relay-sets";
import { getListName } from "../../helpers/nostr/lists";
import { getEventCoordinate } from "../../helpers/nostr/events";
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
import BackButton from "../../components/back-button";
export default function RelaysView() {
const [search, setSearch] = useState("");
const deboundedSearch = useDeferredValue(search);
const isSearching = deboundedSearch.length > 2;
const addRelayModal = useDisclosure();
const offline = useSubject(offlineMode);
const account = useCurrentAccount();
const relaySets = useUserRelaySets(account?.pubkey, undefined);
const vertical = useBreakpointValue({ base: true, lg: false });
const readRelays = useReadRelays();
const writeRelays = useWriteRelays();
const discoveredRelays = relayPoolService
.getRelays()
.filter((r) => !readRelays.has(r.url) && !writeRelays.has(r.url))
.map((r) => r.url)
.filter(isValidRelayURL);
const location = useLocation();
const { value: onlineRelays = [] } = useAsync(async () =>
fetch("https://api.nostr.watch/v1/online").then((res) => res.json() as Promise<string[]>),
);
const filteredRelays = useMemo(() => {
if (isSearching) {
return onlineRelays.filter((url) => url.toLowerCase().includes(deboundedSearch.toLowerCase()));
}
return [...readRelays];
}, [isSearching, deboundedSearch, onlineRelays, readRelays]);
return (
<VerticalPageLayout>
<Flex alignItems="center" gap="2" wrap="wrap">
<Input type="search" placeholder="search" value={search} onChange={(e) => setSearch(e.target.value)} w="auto" />
<Button onClick={() => offlineMode.next(!offline)} leftIcon={offline ? <WifiOff /> : <Wifi />}>
{offline ? "Offline" : "Online"}
const renderContent = () => {
const nav = (
<Flex gap="2" direction="column" minW="xs" overflowY="auto" overflowX="hidden" w={vertical ? "full" : undefined}>
<Button
as={RouterLink}
variant="outline"
colorScheme={
(location.pathname === "/relays" && !vertical) || location.pathname === "/relays/app"
? "primary"
: undefined
}
to="/relays/app"
>
App Relays
</Button>
<Spacer />
<Button as={RouterLink} to="/relays/popular">
Popular Relays
<Button
as={RouterLink}
variant="outline"
colorScheme={location.pathname === "/relays/cache" ? "primary" : undefined}
to="/relays/cache"
>
Cache Relay
</Button>
<Button as={RouterLink} to="/relays/reviews">
Browse Reviews
</Button>
<Button colorScheme="primary" onClick={addRelayModal.onOpen}>
Add Custom
</Button>
</Flex>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{filteredRelays.map((url) => (
<ErrorBoundary>
<RelayCard key={url} url={url} variant="outline" />
</ErrorBoundary>
))}
</SimpleGrid>
{discoveredRelays.length > 0 && !isSearching && (
<>
<Heading size="lg" my="2">
Discovered Relays
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
{discoveredRelays.map((url) => (
<ErrorBoundary>
<RelayCard key={url} url={url} variant="outline" />
</ErrorBoundary>
{account && (
<>
<Heading size="sm" mt="2">
Relay Sets
</Heading>
{relaySets.map((set) => (
<Button
as={RouterLink}
variant="outline"
colorScheme={location.pathname.endsWith(getEventCoordinate(set)) ? "primary" : undefined}
to={`/relays/${getEventCoordinate(set)}`}
>
{getListName(set)}
</Button>
))}
</SimpleGrid>
</>
)}
</>
)}
</Flex>
);
if (vertical) {
if (location.pathname !== "/relays") return <Outlet />;
else return nav;
} else
return (
<Flex gap="2" maxH="100vh" overflow="hidden">
{nav}
<Outlet />
</Flex>
);
};
{addRelayModal.isOpen && (
<AddCustomRelayModal
isOpen
onClose={addRelayModal.onClose}
size="2xl"
onSubmit={(url) => {
clientRelaysService.addRelay(url, RelayMode.ALL);
addRelayModal.onClose();
}}
/>
)}
</VerticalPageLayout>
);
return <VerticalPageLayout>{renderContent()}</VerticalPageLayout>;
}

View File

@ -0,0 +1,9 @@
import { Flex } from "@chakra-ui/react";
export default function RelaySetView() {
return (
<Flex gap="2" direction="column">
Relay Set
</Flex>
);
}