diff --git a/client/src/App.tsx b/client/src/App.tsx index a4b1847..9944934 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,9 +3,7 @@ import './App.css'; import Home from './components/Home'; import Settings from './components/Settings'; import SwipeableViews from 'react-swipeable-views'; -import { NostrProvider } from './utils/relays'; -const relayUrls = ['wss://relay.damus.io']; function App() { const [index, setIndex] = React.useState(1); @@ -15,7 +13,6 @@ function App() { }; return ( -
@@ -24,7 +21,6 @@ function App() {
-
); } diff --git a/client/src/components/Home.tsx b/client/src/components/Home.tsx index 63a3efd..5520523 100644 --- a/client/src/components/Home.tsx +++ b/client/src/components/Home.tsx @@ -1,48 +1,48 @@ import React, { useEffect, useState } from 'react'; -import { relayInit } from 'nostr-tools'; import PostCard from './PostCard/PostCard'; -import Header from './Header/Header'; import NewThreadCard from './PostCard/NewThreadCard'; import { getPow } from '../utils/mine'; -import { Event } from 'nostr-tools'; +import { relayInit, Event } from 'nostr-tools'; +import { subGlobalFeed, simpleSub24hFeed } from '../utils/subscriptions'; +import { uniqBy } from '../utils/utils'; const relay = relayInit('wss://nostr.lu.ke'); +type EventRelayMap = { + [eventId: string]: string[]; +}; +const eventRelayMap: EventRelayMap = {}; // eventId: [relay1, relay2] + + const Home = () => { - // Define the type of the state variable - const [events, setEvents] = useState([]); + const [events, setEvents] = useState([]); // Initialize state + + // Define your callback function for subGlobalFeed + const onEvent = (event: Event, relay: string) => { + setEvents((prevEvents) => [...prevEvents, event]); + console.log(event.id); + }; useEffect(() => { - relay.on('connect', async () => { - console.log(`connected to ${relay.url}`); + // Subscribe to global feed when the component mounts + subGlobalFeed(onEvent); - const eventList = await relay.list([ - { - kinds: [1], - limit: 200, - }, - ]); + // Optionally, return a cleanup function to unsubscribe when the component unmounts + return () => { + // Your cleanup code here + }; + }, []); // Empty dependency array means this useEffect runs once when the component mounts - // Filter events with a difficulty greater than 10 - const filteredEvents = eventList.filter(event => getPow(event.id) > 2); - - // Assuming eventList is of type Event[] - setEvents(filteredEvents); - }); - - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`); - }); - - relay.connect(); - }, []); + const uniqEvents = events.length > 0 ? uniqBy(events, "id") : []; + // const filteredEvents = uniqEvents.filter(event => getPow(event.id) > 5); + const sortedEvents = uniqEvents.sort((a, b) => (b.created_at as any) - (a.created_at as any)); return ( <>
- {events.sort((a, b) => b.created_at - a.created_at).map((event, index) => ( + {sortedEvents.sort((a, b) => b.created_at - a.created_at).map((event, index) => ( ))}
diff --git a/client/src/components/PostCard/NewThreadCard.tsx b/client/src/components/PostCard/NewThreadCard.tsx index f0e1ffb..646b0e5 100644 --- a/client/src/components/PostCard/NewThreadCard.tsx +++ b/client/src/components/PostCard/NewThreadCard.tsx @@ -1,47 +1,34 @@ import CardContainer from './CardContainer'; -import { ArrowUpTrayIcon } from '@heroicons/react/24/outline'; +import { ArrowUpTrayIcon, CpuChipIcon } from '@heroicons/react/24/outline'; import { useState } from 'react'; -import { generatePrivateKey, getPublicKey, finishEvent, relayInit} from 'nostr-tools'; +import { Event, generatePrivateKey, getPublicKey, finishEvent, relayInit} from 'nostr-tools'; import { minePow } from '../../utils/mine'; const difficulty = 10 -export const relay = relayInit('wss://nostr.lu.ke') - -const NewThreadCard = () => { +const NewThreadCard: React.FC = () => { const [comment, setComment] = useState(""); - + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); - let sk = generatePrivateKey() - let pk = getPublicKey(sk) - - relay.on('connect', () => { - console.log(`connected to ${relay.url}`) - }) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`) - }) - - await relay.connect() + let sk = generatePrivateKey(); try { const event = minePow({ kind: 1, tags: [], - content: 'Hello, world!', - created_at: Math.floor(Date.now() / 1000), //needs to be date To Unix - pubkey: pk, - }, difficulty) + content: comment, + created_at: Math.floor(Date.now() / 1000), + pubkey: getPublicKey(sk), + }, difficulty); - const signedEvent = finishEvent(event, sk) - await relay.publish(signedEvent) - console.log(signedEvent.id) + const signedEvent = finishEvent(event, sk); + // await publish(signedEvent); + console.log(signedEvent.id); } catch (error) { setComment(comment + " " + error); } - relay.close() }; // async function attachFile(file_input: File | null) { @@ -80,6 +67,8 @@ const NewThreadCard = () => { name="com" wrap="soft" className="w-full p-2 rounded bg-gradient-to-r from-blue-900 to-cyan-500 text-white border-none" + value={comment} + onChange={(e) => setComment(e.target.value)} />
@@ -87,9 +76,10 @@ const NewThreadCard = () => {
- + : {difficulty} +
@@ -98,5 +88,4 @@ const NewThreadCard = () => { ); }; - export default NewThreadCard; \ No newline at end of file diff --git a/client/src/components/PostCard/PostCard.tsx b/client/src/components/PostCard/PostCard.tsx index cd1e93b..aac8fb1 100644 --- a/client/src/components/PostCard/PostCard.tsx +++ b/client/src/components/PostCard/PostCard.tsx @@ -21,11 +21,6 @@ const colorCombos = [ 'from-sky-400 to-cyan-500' ]; -function getRandomElement(array: string[]): string { - const index = Math.floor(Math.random() * array.length); - return array[index]; -} - const getColorFromHash = (id: string, colors: string[]): string => { // Create a simple hash from the event.id let hash = 0; diff --git a/client/src/utils/relays.tsx b/client/src/utils/relays.tsx index 16bf007..35ae5ff 100644 --- a/client/src/utils/relays.tsx +++ b/client/src/utils/relays.tsx @@ -1,152 +1,108 @@ -import { -createContext, -ReactNode, -useCallback, -useContext, -useEffect, -useRef, -useState, -} from "react" -import { Relay, Filter, Event, relayInit, Sub } from "nostr-tools" -import { uniqBy } from "./utils" +import {Event, Filter, relayInit, Relay, Sub} from 'nostr-tools'; -type OnConnectFunc = (relay: Relay) => void -type OnDisconnectFunc = (relay: Relay) => void -type OnEventFunc = (event: Event) => void -type OnDoneFunc = () => void -type OnSubscribeFunc = (sub: Sub, relay: Relay) => void +type SubCallback = ( + event: Readonly, + relay: Readonly, +) => void; -interface NostrContextType { - isLoading: boolean - debug?: boolean - connectedRelays: Relay[] - onConnect: (_onConnectCallback?: OnConnectFunc) => void - onDisconnect: (_onDisconnectCallback?: OnDisconnectFunc) => void - publish: (event: Event) => void -} - -const NostrContext = createContext({ - isLoading: true, - connectedRelays: [], - onConnect: () => null, - onDisconnect: () => null, - publish: () => null, -}) +type Subscribe = { + cb: SubCallback; + filter: Filter; + unsub?: boolean; +}; -const log = ( - isOn: boolean | undefined, - type: "info" | "error" | "warn", - ...args: unknown[] -) => { - if (!isOn) return - console[type](...args) -} +const subList: Array = []; +const currentSubList: Array = []; +const relayMap = new Map(); -export function NostrProvider({ - children, - relayUrls, - debug, - }: { - children: ReactNode - relayUrls: string[] - debug?: boolean - }) { - const [isLoading, setIsLoading] = useState(true) - const [connectedRelays, setConnectedRelays] = useState([]) - const [relays, setRelays] = useState([]) - const relayUrlsRef = useRef([]) - - let onConnectCallback: null | OnConnectFunc = null - let onDisconnectCallback: null | OnDisconnectFunc = null - - const disconnectToRelays = useCallback( - (relayUrls: string[]) => { - relayUrls.forEach(async (relayUrl) => { - await relays.find((relay) => relay.url === relayUrl)?.close() - setRelays((prev) => prev.filter((r) => r.url !== relayUrl)) - }) - }, - [relays], - ) - - const connectToRelays = useCallback( - (relayUrls: string[]) => { - relayUrls.forEach(async (relayUrl) => { - const relay = relayInit(relayUrl) - - if (connectedRelays.findIndex((r) => r.url === relayUrl) >= 0) { - // already connected, skip - return - } - - setRelays((prev) => uniqBy([...prev, relay], "url")) - relay.connect() - - relay.on("connect", () => { - log(debug, "info", `✅ nostr (${relayUrl}): Connected!`) - setIsLoading(false) - onConnectCallback?.(relay) - setConnectedRelays((prev) => uniqBy([...prev, relay], "url")) - }) - - relay.on("disconnect", () => { - log(debug, "warn", `🚪 nostr (${relayUrl}): Connection closed.`) - onDisconnectCallback?.(relay) - setConnectedRelays((prev) => prev.filter((r) => r.url !== relayUrl)) - }) - - relay.on("error", () => { - log(debug, "error", `❌ nostr (${relayUrl}): Connection error!`) - }) - }) - }, - [connectedRelays, debug, onConnectCallback, onDisconnectCallback], - ) - - useEffect(() => { - if (relayUrlsRef.current === relayUrls) { - // relayUrls isn't updated, skip - return - } - - const relayUrlsToDisconnect = relayUrlsRef.current.filter( - (relayUrl) => !relayUrls.includes(relayUrl), - ) - - disconnectToRelays(relayUrlsToDisconnect) - connectToRelays(relayUrls) - - relayUrlsRef.current = relayUrls - }, [relayUrls, connectToRelays, disconnectToRelays]) - - const publish = (event: Event) => { - return connectedRelays.map((relay) => { - log(debug, "info", `⬆️ nostr (${relay.url}): Sending event:`, event) - - return relay.publish(event) - }) - } - - const value: NostrContextType = { - debug, - isLoading, - connectedRelays, - publish, - onConnect: (_onConnectCallback?: OnConnectFunc) => { - if (_onConnectCallback) { - onConnectCallback = _onConnectCallback - } - }, - onDisconnect: (_onDisconnectCallback?: OnDisconnectFunc) => { - if (_onDisconnectCallback) { - onDisconnectCallback = _onDisconnectCallback - } - }, - } - - return {children} +export const addRelay = async (url: string) => { + const relay = relayInit(url); + relay.on('connect', () => { + console.info(`connected to ${relay.url}`); + }); + relay.on('error', () => { + console.warn(`failed to connect to ${relay.url}`); + }); + try { + await relay.connect(); + currentSubList.forEach(({cb, filter}) => subscribe(cb, filter, relay)); + relayMap.set(url, relay); + } catch { + console.warn(`could not connect to ${url}`); } - - export function useNostr() { - return useContext(NostrContext) - } \ No newline at end of file +}; + +export const unsubscribe = (sub: Sub) => { + sub.unsub(); + subList.splice(subList.indexOf(sub), 1); +}; + +const subscribe = ( + cb: SubCallback, + filter: Filter, + relay: Relay, + unsub?: boolean +) => { + const sub = relay.sub([filter]); + subList.push(sub); + sub.on('event', (event: Event) => { + cb(event, relay.url); + }); + if (unsub) { + sub.on('eose', () => { + // console.log('eose', relay.url); + unsubscribe(sub); + }); + } + return sub; +}; + +export const sub = (obj: Subscribe) => { + currentSubList.push(obj); + relayMap.forEach((relay) => subscribe(obj.cb, obj.filter, relay, obj.unsub)); +}; + +export const subOnce = ( + obj: Subscribe & {relay: string} +) => { + const relay = relayMap.get(obj.relay); + if (relay) { + const sub = subscribe(obj.cb, obj.filter, relay); + sub.on('eose', () => { + // console.log('eose', obj.relay); + unsubscribe(sub); + }); + } +}; + +export const unsubAll = () => { + subList.forEach(unsubscribe); + currentSubList.length = 0; +}; + +type PublishCallback = ( + relay: string, + errorMessage?: string, +) => void; + + +export const publish = (event: Event, cb: PublishCallback) => { + relayMap.forEach(async (relay, url) => { + try { + await relay.publish(event); + console.info(`${relay.url} has accepted our event`); + cb(relay.url); + } catch (reason) { + console.error(`failed to publish to ${relay.url}: ${reason}`); + cb(relay.url, reason as string); + } + }); + }; + + +addRelay('wss://relay.snort.social'); +addRelay('wss://nostr.bitcoiner.social'); +addRelay('wss://nostr.mom'); +addRelay('wss://relay.nostr.bg'); +addRelay('wss://nos.lol'); +// addRelay('wss://relay.nostr.ch'); \ No newline at end of file diff --git a/client/src/utils/subscriptions.ts b/client/src/utils/subscriptions.ts new file mode 100644 index 0000000..1ab2f55 --- /dev/null +++ b/client/src/utils/subscriptions.ts @@ -0,0 +1,86 @@ +import {sub, subOnce, unsubAll} from './relays'; +import { Event } from 'nostr-tools'; + +type SubCallback = ( + event: Event, + relay: string, +) => void; + +/** subscribe to global feed */ +export const subGlobalFeed = (onEvent: SubCallback) => { + console.info('subscribe to global feed'); + unsubAll(); + const now = Math.floor(Date.now() * 0.001); + const pubkeys = new Set(); + const notes = new Set(); + const prefix = Math.floor(10 / 4); // 4 bits in each '0' character + + sub({ // get past events + cb: (evt, relay) => { + pubkeys.add(evt.pubkey); + notes.add(evt.id); + onEvent(evt, relay); + }, + filter: { + kinds: [1], + since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)), + limit: 10, + }, + unsub: true + }); + + setTimeout(() => { + // get profile info + sub({ + cb: onEvent, + filter: { + authors: Array.from(pubkeys), + kinds: [0], + limit: pubkeys.size, + }, + unsub: true, + }); + pubkeys.clear(); + notes.clear(); + }, 2000); + + // subscribe to future notes, reactions and profile updates + sub({ + cb: (evt, relay) => { + onEvent(evt, relay); + if ( + evt.kind !== 1 + || pubkeys.has(evt.pubkey) + ) { + return; + } + subOnce({ // get profil data + relay, + cb: onEvent, + filter: { + authors: [evt.pubkey], + kinds: [0], + limit: 1, + } + }); + }, + filter: { + kinds: [0, 1], + since: now, + }, + }); + }; + + /** subscribe to global feed */ +export const simpleSub24hFeed = (onEvent: SubCallback) => { + unsubAll(); + sub({ + cb: onEvent, + filter: { + kinds: [1], + //until: Math.floor(Date.now() * 0.001), + since: Math.floor((Date.now() * 0.001) - (24 * 60 * 60)), + limit: 1, + } + }); +}; \ No newline at end of file