mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 12:07:43 +02:00
add relay selection to global feed
This commit is contained in:
@@ -29,13 +29,7 @@ export class NostrSubscription {
|
|||||||
|
|
||||||
this.relays = relayUrls.map((url) => relayPool.requestRelay(url));
|
this.relays = relayUrls.map((url) => relayPool.requestRelay(url));
|
||||||
}
|
}
|
||||||
handleOpen(relay: Relay) {
|
private handleEvent(event: IncomingEvent) {
|
||||||
if (this.query) {
|
|
||||||
// when the relay connects send the req event
|
|
||||||
relay.send(["REQ", this.id, this.query]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleEvent(event: IncomingEvent) {
|
|
||||||
if (this.state === NostrSubscription.OPEN && event.subId === this.id && !this.seenEvents.has(event.body.id)) {
|
if (this.state === NostrSubscription.OPEN && event.subId === this.id && !this.seenEvents.has(event.body.id)) {
|
||||||
this.onEvent.next(event.body);
|
this.onEvent.next(event.body);
|
||||||
this.seenEvents.add(event.body.id);
|
this.seenEvents.add(event.body.id);
|
||||||
@@ -47,12 +41,13 @@ export class NostrSubscription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup: SubscriptionLike[] = [];
|
private cleanup = new Map<Relay, SubscriptionLike>();
|
||||||
/** listen for event and open events from relays */
|
/** listen for event and open events from relays */
|
||||||
private subscribeToRelays() {
|
private subscribeToRelays() {
|
||||||
for (const relay of this.relays) {
|
for (const relay of this.relays) {
|
||||||
this.cleanup.push(relay.onEvent.subscribe(this.handleEvent.bind(this)));
|
if (!this.cleanup.has(relay)) {
|
||||||
this.cleanup.push(relay.onOpen.subscribe(this.handleOpen.bind(this)));
|
this.cleanup.set(relay, relay.onEvent.subscribe(this.handleEvent.bind(this)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const url of this.relayUrls) {
|
for (const url of this.relayUrls) {
|
||||||
@@ -60,8 +55,9 @@ export class NostrSubscription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** listen for event and open events from relays */
|
/** listen for event and open events from relays */
|
||||||
private unsubscribeToRelays() {
|
private unsubscribeFromRelays() {
|
||||||
this.cleanup.forEach((sub) => sub.unsubscribe());
|
this.cleanup.forEach((sub) => sub.unsubscribe());
|
||||||
|
this.cleanup.clear();
|
||||||
|
|
||||||
for (const url of this.relayUrls) {
|
for (const url of this.relayUrls) {
|
||||||
relayPool.removeClaim(url, this);
|
relayPool.removeClaim(url, this);
|
||||||
@@ -83,7 +79,7 @@ export class NostrSubscription {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
update(query: NostrQuery) {
|
setQuery(query: NostrQuery) {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
if (this.state === NostrSubscription.OPEN) {
|
if (this.state === NostrSubscription.OPEN) {
|
||||||
this.send(["REQ", this.id, this.query]);
|
this.send(["REQ", this.id, this.query]);
|
||||||
@@ -91,13 +87,35 @@ export class NostrSubscription {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
setRelays(relays: string[]) {
|
setRelays(relays: string[]) {
|
||||||
this.unsubscribeToRelays();
|
this.unsubscribeFromRelays();
|
||||||
|
const newRelays = relays.map((url) => relayPool.requestRelay(url));
|
||||||
|
|
||||||
// get new relays
|
for (const relay of this.relays) {
|
||||||
|
if (!newRelays.includes(relay)) {
|
||||||
|
// if the subscription is open and the relay is connected
|
||||||
|
if (this.state === NostrSubscription.OPEN && relay.connected) {
|
||||||
|
// close the connection to this relay
|
||||||
|
relay.send(["CLOSE", this.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const relay of newRelays) {
|
||||||
|
if (!this.relays.includes(relay)) {
|
||||||
|
// if the subscription is open and it has a query
|
||||||
|
if (this.state === NostrSubscription.OPEN && this.query) {
|
||||||
|
// open a connection to this relay
|
||||||
|
relay.send(["REQ", this.id, this.query]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set new relays
|
||||||
this.relayUrls = relays;
|
this.relayUrls = relays;
|
||||||
this.relays = relays.map((url) => relayPool.requestRelay(url));
|
this.relays = newRelays;
|
||||||
|
|
||||||
this.subscribeToRelays();
|
if (this.state === NostrSubscription.OPEN) {
|
||||||
|
this.subscribeToRelays();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
close() {
|
close() {
|
||||||
if (this.state !== NostrSubscription.OPEN) return this;
|
if (this.state !== NostrSubscription.OPEN) return this;
|
||||||
@@ -109,7 +127,7 @@ export class NostrSubscription {
|
|||||||
// forget all seen events
|
// forget all seen events
|
||||||
this.seenEvents.clear();
|
this.seenEvents.clear();
|
||||||
// unsubscribe from relay messages
|
// unsubscribe from relay messages
|
||||||
this.unsubscribeToRelays();
|
this.unsubscribeFromRelays();
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
console.info(`Subscription: "${this.name || this.id}" closed`);
|
console.info(`Subscription: "${this.name || this.id}" closed`);
|
||||||
@@ -117,4 +135,8 @@ export class NostrSubscription {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
forgetEvents() {
|
||||||
|
// forget all seen events
|
||||||
|
this.seenEvents.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -68,7 +68,7 @@ export class ThreadLoader {
|
|||||||
|
|
||||||
private updateSubscription() {
|
private updateSubscription() {
|
||||||
if (this.rootId.value) {
|
if (this.rootId.value) {
|
||||||
this.subscription.update({ "#e": [this.rootId.value], kinds: [1] });
|
this.subscription.setQuery({ "#e": [this.rootId.value], kinds: [1] });
|
||||||
if (this.subscription.state !== NostrSubscription.OPEN) {
|
if (this.subscription.state !== NostrSubscription.OPEN) {
|
||||||
this.subscription.open();
|
this.subscription.open();
|
||||||
}
|
}
|
||||||
|
@@ -39,7 +39,12 @@ export class TimelineLoader {
|
|||||||
if (!query.since) throw new Error('Timeline requires "since" to be set in query');
|
if (!query.since) throw new Error('Timeline requires "since" to be set in query');
|
||||||
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.subscription.update(query);
|
this.subscription.setQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRelays(relays: string[]) {
|
||||||
|
this.relays = relays;
|
||||||
|
this.subscription.setRelays(relays);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleEvent(event: NostrEvent) {
|
private handleEvent(event: NostrEvent) {
|
||||||
@@ -78,9 +83,10 @@ export class TimelineLoader {
|
|||||||
this.page.next(this.page.value + 1);
|
this.page.next(this.page.value + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
forgetEvents() {
|
||||||
this.events.next([]);
|
this.events.next([]);
|
||||||
this.seenEvents.clear();
|
this.seenEvents.clear();
|
||||||
|
this.subscription.forgetEvents();
|
||||||
}
|
}
|
||||||
open() {
|
open() {
|
||||||
this.subscription.open();
|
this.subscription.open();
|
||||||
|
24
src/components/relay-url-input.tsx
Normal file
24
src/components/relay-url-input.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Input, InputProps } from "@chakra-ui/react";
|
||||||
|
import { useAsync } from "react-use";
|
||||||
|
|
||||||
|
export type RelayUrlInputProps = Omit<InputProps, "type">;
|
||||||
|
|
||||||
|
export const RelayUrlInput = ({ ...props }: RelayUrlInputProps) => {
|
||||||
|
const { value: relaysJson, loading: loadingRelaysJson } = useAsync(async () =>
|
||||||
|
fetch("/relays.json").then((res) => res.json() as Promise<{ relays: string[] }>)
|
||||||
|
);
|
||||||
|
const relaySuggestions = relaysJson?.relays ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input list="relay-suggestions" type="url" isDisabled={props.isDisabled ?? loadingRelaysJson} {...props} />
|
||||||
|
<datalist id="relay-suggestions">
|
||||||
|
{relaySuggestions.map((url) => (
|
||||||
|
<option key={url} value={url}>
|
||||||
|
{url}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -17,7 +17,7 @@ export function useSubscription(query: NostrQuery, opts?: Options) {
|
|||||||
|
|
||||||
useDeepCompareEffect(() => {
|
useDeepCompareEffect(() => {
|
||||||
if (sub.current) {
|
if (sub.current) {
|
||||||
sub.current.update(query);
|
sub.current.setQuery(query);
|
||||||
if (opts?.enabled ?? true) sub.current.open();
|
if (opts?.enabled ?? true) sub.current.open();
|
||||||
else sub.current.close();
|
else sub.current.close();
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +1,27 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { useUnmount } from "react-use";
|
import { useDeepCompareEffect, useUnmount } from "react-use";
|
||||||
import { NostrQueryWithStart, TimelineLoader, TimelineLoaderOptions } from "../classes/timeline-loader";
|
import { NostrQueryWithStart, TimelineLoader, TimelineLoaderOptions } from "../classes/timeline-loader";
|
||||||
import settings from "../services/settings";
|
|
||||||
import useSubject from "./use-subject";
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
type Options = TimelineLoaderOptions & {
|
type Options = TimelineLoaderOptions & {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTimelineLoader(key: string, query: NostrQueryWithStart, opts?: Options) {
|
export function useTimelineLoader(key: string, relays: string[], query: NostrQueryWithStart, opts?: Options) {
|
||||||
const relays = useSubject(settings.relays);
|
|
||||||
if (opts && !opts.name) opts.name = key;
|
if (opts && !opts.name) opts.name = key;
|
||||||
|
|
||||||
const ref = useRef<TimelineLoader | null>(null);
|
const ref = useRef<TimelineLoader | null>(null);
|
||||||
const loader = (ref.current = ref.current || new TimelineLoader(relays, query, opts));
|
const loader = (ref.current = ref.current || new TimelineLoader(relays, query, opts));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loader.reset();
|
loader.forgetEvents();
|
||||||
loader.setQuery(query);
|
loader.setQuery(query);
|
||||||
}, [key]);
|
}, [key]);
|
||||||
|
|
||||||
|
useDeepCompareEffect(() => {
|
||||||
|
loader.setRelays(relays);
|
||||||
|
}, [relays]);
|
||||||
|
|
||||||
const enabled = opts?.enabled ?? true;
|
const enabled = opts?.enabled ?? true;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
@@ -71,7 +71,7 @@ function flushRequests() {
|
|||||||
const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [3] };
|
const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [3] };
|
||||||
|
|
||||||
subscription.setRelays(Array.from(relays));
|
subscription.setRelays(Array.from(relays));
|
||||||
subscription.update(query);
|
subscription.setQuery(query);
|
||||||
if (subscription.state !== NostrSubscription.OPEN) {
|
if (subscription.state !== NostrSubscription.OPEN) {
|
||||||
subscription.open();
|
subscription.open();
|
||||||
}
|
}
|
||||||
|
@@ -55,7 +55,7 @@ function flushRequests() {
|
|||||||
const query: NostrQuery = { kinds: [3], "#p": Array.from(pubkeys) };
|
const query: NostrQuery = { kinds: [3], "#p": Array.from(pubkeys) };
|
||||||
|
|
||||||
subscription.setRelays(Array.from(relays));
|
subscription.setRelays(Array.from(relays));
|
||||||
subscription.update(query);
|
subscription.setQuery(query);
|
||||||
if (subscription.state !== NostrSubscription.OPEN) {
|
if (subscription.state !== NostrSubscription.OPEN) {
|
||||||
subscription.open();
|
subscription.open();
|
||||||
}
|
}
|
||||||
|
@@ -56,7 +56,7 @@ function flushRequests() {
|
|||||||
const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [0] };
|
const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [0] };
|
||||||
|
|
||||||
subscription.setRelays(Array.from(relays));
|
subscription.setRelays(Array.from(relays));
|
||||||
subscription.update(query);
|
subscription.setQuery(query);
|
||||||
if (subscription.state !== NostrSubscription.OPEN) {
|
if (subscription.state !== NostrSubscription.OPEN) {
|
||||||
subscription.open();
|
subscription.open();
|
||||||
}
|
}
|
||||||
|
145
src/views/home/components/feed-filters.tsx
Normal file
145
src/views/home/components/feed-filters.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalProps,
|
||||||
|
Switch,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useList } from "react-use";
|
||||||
|
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||||
|
import { unique } from "../../../helpers/array";
|
||||||
|
import useSubject from "../../../hooks/use-subject";
|
||||||
|
import settings from "../../../services/settings";
|
||||||
|
|
||||||
|
const CustomRelayForm = ({ onSubmit }: { onSubmit: (url: string) => void }) => {
|
||||||
|
const [customRelay, setCustomRelay] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(customRelay);
|
||||||
|
setCustomRelay("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex gap="2">
|
||||||
|
<RelayUrlInput
|
||||||
|
size="sm"
|
||||||
|
placeholder="wss://relay.example.com"
|
||||||
|
value={customRelay}
|
||||||
|
onChange={(e) => setCustomRelay(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button size="sm" type="submit">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterValues = {
|
||||||
|
relays: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FeedFiltersProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: ModalProps["onClose"];
|
||||||
|
values: FilterValues;
|
||||||
|
onSave: (values: FilterValues) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeedFilters = ({ isOpen, onClose, values }: FeedFiltersProps) => {
|
||||||
|
const defaultRelays = useSubject(settings.relays);
|
||||||
|
|
||||||
|
const [selectedRelays, relayActions] = useList(values.relays);
|
||||||
|
const availableRelays = unique([...defaultRelays, ...selectedRelays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Filters</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Accordion allowToggle>
|
||||||
|
<AccordionItem>
|
||||||
|
<h2>
|
||||||
|
<AccordionButton>
|
||||||
|
<Box as="span" flex="1" textAlign="left">
|
||||||
|
Relays
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
</h2>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
{availableRelays.map((url) => (
|
||||||
|
<Box key={url}>
|
||||||
|
<FormLabel>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
mr="2"
|
||||||
|
isChecked={selectedRelays.includes(url)}
|
||||||
|
onChange={() =>
|
||||||
|
selectedRelays.includes(url)
|
||||||
|
? relayActions.removeAt(selectedRelays.indexOf(url))
|
||||||
|
: relayActions.push(url)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{url}
|
||||||
|
</FormLabel>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
<Flex gap="2">
|
||||||
|
<Button size="sm" onClick={() => relayActions.set(defaultRelays)}>
|
||||||
|
Select All
|
||||||
|
</Button>
|
||||||
|
<CustomRelayForm onSubmit={(url) => relayActions.push(url)} />
|
||||||
|
</Flex>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
|
||||||
|
<AccordionItem>
|
||||||
|
<h2>
|
||||||
|
<AccordionButton>
|
||||||
|
<Box as="span" flex="1" textAlign="left">
|
||||||
|
Add Custom
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
</h2>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
|
||||||
|
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
|
||||||
|
ex ea commodo consequat.
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button mr={3} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="blue" variant="solid">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@@ -9,6 +9,7 @@ import identity from "../../services/identity";
|
|||||||
import userContactsService from "../../services/user-contacts";
|
import userContactsService from "../../services/user-contacts";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { isNote } from "../../helpers/nostr-event";
|
import { isNote } from "../../helpers/nostr-event";
|
||||||
|
import settings from "../../services/settings";
|
||||||
|
|
||||||
function useExtendedContacts(pubkey: string) {
|
function useExtendedContacts(pubkey: string) {
|
||||||
const [extendedContacts, setExtendedContacts] = useState<string[]>([]);
|
const [extendedContacts, setExtendedContacts] = useState<string[]>([]);
|
||||||
@@ -39,10 +40,12 @@ function useExtendedContacts(pubkey: string) {
|
|||||||
|
|
||||||
export const DiscoverTab = () => {
|
export const DiscoverTab = () => {
|
||||||
const pubkey = useSubject(identity.pubkey);
|
const pubkey = useSubject(identity.pubkey);
|
||||||
|
const relays = useSubject(settings.relays);
|
||||||
|
|
||||||
const contactsOfContacts = useExtendedContacts(pubkey);
|
const contactsOfContacts = useExtendedContacts(pubkey);
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const { events, loading, loadMore } = useTimelineLoader(
|
||||||
`discover`,
|
`discover`,
|
||||||
|
relays,
|
||||||
{ authors: contactsOfContacts, kinds: [1], since: moment().subtract(1, "hour").unix() },
|
{ authors: contactsOfContacts, kinds: [1], since: moment().subtract(1, "hour").unix() },
|
||||||
{ pageSize: moment.duration(1, "hour").asSeconds(), enabled: contactsOfContacts.length > 0 }
|
{ pageSize: moment.duration(1, "hour").asSeconds(), enabled: contactsOfContacts.length > 0 }
|
||||||
);
|
);
|
||||||
|
@@ -7,9 +7,11 @@ import useSubject from "../../hooks/use-subject";
|
|||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||||
import identity from "../../services/identity";
|
import identity from "../../services/identity";
|
||||||
|
import settings from "../../services/settings";
|
||||||
|
|
||||||
export const FollowingTab = () => {
|
export const FollowingTab = () => {
|
||||||
const pubkey = useSubject(identity.pubkey);
|
const pubkey = useSubject(identity.pubkey);
|
||||||
|
const relays = useSubject(settings.relays);
|
||||||
const contacts = useUserContacts(pubkey);
|
const contacts = useUserContacts(pubkey);
|
||||||
const [search, setSearch] = useSearchParams();
|
const [search, setSearch] = useSearchParams();
|
||||||
const showReplies = search.has("replies");
|
const showReplies = search.has("replies");
|
||||||
@@ -20,6 +22,7 @@ export const FollowingTab = () => {
|
|||||||
const following = contacts?.contacts || [];
|
const following = contacts?.contacts || [];
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const { events, loading, loadMore } = useTimelineLoader(
|
||||||
`following-posts`,
|
`following-posts`,
|
||||||
|
relays,
|
||||||
{ authors: following, kinds: [1], since: moment().subtract(2, "hour").unix() },
|
{ authors: following, kinds: [1], since: moment().subtract(2, "hour").unix() },
|
||||||
{ pageSize: moment.duration(2, "hour").asSeconds(), enabled: following.length > 0 }
|
{ pageSize: moment.duration(2, "hour").asSeconds(), enabled: following.length > 0 }
|
||||||
);
|
);
|
||||||
|
@@ -1,24 +1,60 @@
|
|||||||
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
import { Button, Flex, FormControl, FormLabel, Select, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { useState } from "react";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { isNote } from "../../helpers/nostr-event";
|
import { isNote } from "../../helpers/nostr-event";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
|
import settings from "../../services/settings";
|
||||||
|
|
||||||
export const GlobalTab = () => {
|
export const GlobalTab = () => {
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const availableRelays = useSubject(settings.relays);
|
||||||
|
const [selectedRelay, setSelectedRelay] = useState("");
|
||||||
|
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||||
|
const { events, loading, loadMore, loader } = useTimelineLoader(
|
||||||
`global`,
|
`global`,
|
||||||
|
selectedRelay ? [selectedRelay] : availableRelays,
|
||||||
{ kinds: [1], since: moment().subtract(5, "minutes").unix() },
|
{ kinds: [1], since: moment().subtract(5, "minutes").unix() },
|
||||||
{ pageSize: moment.duration(5, "minutes").asSeconds() }
|
{ pageSize: moment.duration(5, "minutes").asSeconds() }
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeline = events.filter(isNote);
|
const timeline = showReplies ? events : events.filter(isNote);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" overflow="auto" gap="2">
|
<>
|
||||||
{timeline.map((event) => (
|
<Flex direction="column" overflow="auto" gap="2">
|
||||||
<Note key={event.id} event={event} />
|
<Flex gap="2">
|
||||||
))}
|
<Select
|
||||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
placeholder="All Relays"
|
||||||
</Flex>
|
maxWidth="250"
|
||||||
|
value={selectedRelay}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedRelay(e.target.value);
|
||||||
|
loader.forgetEvents();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{availableRelays.map((url) => (
|
||||||
|
<option key={url} value={url}>
|
||||||
|
{url}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<FormControl display="flex" alignItems="center">
|
||||||
|
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} mr="2" />
|
||||||
|
<FormLabel htmlFor="show-replies" mb="0">
|
||||||
|
Show Replies
|
||||||
|
</FormLabel>
|
||||||
|
</FormControl>
|
||||||
|
</Flex>
|
||||||
|
{timeline.map((event) => (
|
||||||
|
<Note key={event.id} event={event} />
|
||||||
|
))}
|
||||||
|
{loading ? (
|
||||||
|
<Spinner ml="auto" mr="auto" mt="8" mb="8" />
|
||||||
|
) : (
|
||||||
|
<Button onClick={() => loadMore()}>Load More</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -28,16 +28,12 @@ import { RelayStatus } from "./relay-status";
|
|||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import settings from "../../services/settings";
|
import settings from "../../services/settings";
|
||||||
import { clearData } from "../../services/db";
|
import { clearData } from "../../services/db";
|
||||||
|
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||||
|
|
||||||
export const SettingsView = () => {
|
export const SettingsView = () => {
|
||||||
const relays = useSubject(settings.relays);
|
const relays = useSubject(settings.relays);
|
||||||
const [relayInputValue, setRelayInputValue] = useState("");
|
const [relayInputValue, setRelayInputValue] = useState("");
|
||||||
|
|
||||||
const { value: relaysJson, loading: loadingRelaysJson } = useAsync(async () =>
|
|
||||||
fetch("/relays.json").then((res) => res.json() as Promise<{ relays: string[] }>)
|
|
||||||
);
|
|
||||||
const relaySuggestions = relaysJson?.relays.filter((url) => !relays.includes(url)) ?? [];
|
|
||||||
|
|
||||||
const { colorMode, setColorMode } = useColorMode();
|
const { colorMode, setColorMode } = useColorMode();
|
||||||
|
|
||||||
const handleRemoveRelay = (url: string) => {
|
const handleRemoveRelay = (url: string) => {
|
||||||
@@ -46,8 +42,10 @@ export const SettingsView = () => {
|
|||||||
const handleAddRelay = (event: SyntheticEvent<HTMLFormElement>) => {
|
const handleAddRelay = (event: SyntheticEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
settings.relays.next([...relays, relayInputValue]);
|
if (!relays.includes(relayInputValue)) {
|
||||||
setRelayInputValue("");
|
settings.relays.next([...relays, relayInputValue]);
|
||||||
|
setRelayInputValue("");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [clearing, setClearing] = useState(false);
|
const [clearing, setClearing] = useState(false);
|
||||||
@@ -104,22 +102,12 @@ export const SettingsView = () => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel htmlFor="relay-url-input">Add Relay</FormLabel>
|
<FormLabel htmlFor="relay-url-input">Add Relay</FormLabel>
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Input
|
<RelayUrlInput
|
||||||
id="relay-url-input"
|
id="relay-url-input"
|
||||||
value={relayInputValue}
|
value={relayInputValue}
|
||||||
onChange={(e) => setRelayInputValue(e.target.value)}
|
onChange={(e) => setRelayInputValue(e.target.value)}
|
||||||
required
|
isRequired
|
||||||
list="relay-suggestions"
|
|
||||||
type="url"
|
|
||||||
isDisabled={loadingRelaysJson}
|
|
||||||
/>
|
/>
|
||||||
<datalist id="relay-suggestions">
|
|
||||||
{relaySuggestions.map((url) => (
|
|
||||||
<option key={url} value={url}>
|
|
||||||
{url}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
<Button type="submit">Add</Button>
|
<Button type="submit">Add</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@@ -3,13 +3,17 @@ import moment from "moment";
|
|||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { isNote } from "../../helpers/nostr-event";
|
import { isNote } from "../../helpers/nostr-event";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
|
import settings from "../../services/settings";
|
||||||
|
|
||||||
const UserNotesTab = () => {
|
const UserNotesTab = () => {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
|
const relays = useSubject(settings.relays);
|
||||||
|
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const { events, loading, loadMore } = useTimelineLoader(
|
||||||
`${pubkey} notes`,
|
`${pubkey} notes`,
|
||||||
|
relays,
|
||||||
{ authors: [pubkey], kinds: [1], since: moment().subtract(1, "day").unix() },
|
{ authors: [pubkey], kinds: [1], since: moment().subtract(1, "day").unix() },
|
||||||
{ pageSize: moment.duration(1, "day").asSeconds() }
|
{ pageSize: moment.duration(1, "day").asSeconds() }
|
||||||
);
|
);
|
||||||
|
@@ -3,12 +3,17 @@ import moment from "moment";
|
|||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { isReply } from "../../helpers/nostr-event";
|
import { isReply } from "../../helpers/nostr-event";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
|
import settings from "../../services/settings";
|
||||||
|
|
||||||
const UserRepliesTab = () => {
|
const UserRepliesTab = () => {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
|
const relays = useSubject(settings.relays);
|
||||||
|
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const { events, loading, loadMore } = useTimelineLoader(
|
||||||
`${pubkey} replies`,
|
`${pubkey} replies`,
|
||||||
|
relays,
|
||||||
{ authors: [pubkey], kinds: [1], since: moment().subtract(4, "hours").unix() },
|
{ authors: [pubkey], kinds: [1], since: moment().subtract(4, "hours").unix() },
|
||||||
{ pageSize: moment.duration(1, "day").asSeconds() }
|
{ pageSize: moment.duration(1, "day").asSeconds() }
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user