messy global feed

This commit is contained in:
smolgrrr 2023-09-16 01:56:25 +10:00
parent e968c13939
commit d31771aa11
6 changed files with 234 additions and 212 deletions

View File

@ -3,9 +3,7 @@ import './App.css';
import Home from './components/Home'; import Home from './components/Home';
import Settings from './components/Settings'; import Settings from './components/Settings';
import SwipeableViews from 'react-swipeable-views'; import SwipeableViews from 'react-swipeable-views';
import { NostrProvider } from './utils/relays';
const relayUrls = ['wss://relay.damus.io'];
function App() { function App() {
const [index, setIndex] = React.useState(1); const [index, setIndex] = React.useState(1);
@ -15,7 +13,6 @@ function App() {
}; };
return ( return (
<NostrProvider relayUrls={relayUrls} debug={true}>
<SwipeableViews index={index} onChangeIndex={handleChangeIndex}> <SwipeableViews index={index} onChangeIndex={handleChangeIndex}>
<div> <div>
<Settings /> <Settings />
@ -24,7 +21,6 @@ function App() {
<Home /> <Home />
</div> </div>
</SwipeableViews> </SwipeableViews>
</NostrProvider>
); );
} }

View File

@ -1,48 +1,48 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { relayInit } from 'nostr-tools';
import PostCard from './PostCard/PostCard'; import PostCard from './PostCard/PostCard';
import Header from './Header/Header';
import NewThreadCard from './PostCard/NewThreadCard'; import NewThreadCard from './PostCard/NewThreadCard';
import { getPow } from '../utils/mine'; 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'); const relay = relayInit('wss://nostr.lu.ke');
type EventRelayMap = {
[eventId: string]: string[];
};
const eventRelayMap: EventRelayMap = {}; // eventId: [relay1, relay2]
const Home = () => { const Home = () => {
// Define the type of the state variable const [events, setEvents] = useState<Event[]>([]); // Initialize state
const [events, setEvents] = useState<Event[]>([]);
// Define your callback function for subGlobalFeed
const onEvent = (event: Event, relay: string) => {
setEvents((prevEvents) => [...prevEvents, event]);
console.log(event.id);
};
useEffect(() => { useEffect(() => {
relay.on('connect', async () => { // Subscribe to global feed when the component mounts
console.log(`connected to ${relay.url}`); subGlobalFeed(onEvent);
const eventList = await relay.list([ // Optionally, return a cleanup function to unsubscribe when the component unmounts
{ return () => {
kinds: [1], // Your cleanup code here
limit: 200, };
}, }, []); // Empty dependency array means this useEffect runs once when the component mounts
]);
// Filter events with a difficulty greater than 10 const uniqEvents = events.length > 0 ? uniqBy(events, "id") : [];
const filteredEvents = eventList.filter(event => getPow(event.id) > 2); // 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));
// Assuming eventList is of type Event[]
setEvents(filteredEvents);
});
relay.on('error', () => {
console.log(`failed to connect to ${relay.url}`);
});
relay.connect();
}, []);
return ( return (
<> <>
<main className="bg-black text-white min-h-screen"> <main className="bg-black text-white min-h-screen">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
<NewThreadCard /> <NewThreadCard />
{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) => (
<PostCard key={index} event={event}/> <PostCard key={index} event={event}/>
))} ))}
</div> </div>

View File

@ -1,47 +1,34 @@
import CardContainer from './CardContainer'; import CardContainer from './CardContainer';
import { ArrowUpTrayIcon } from '@heroicons/react/24/outline'; import { ArrowUpTrayIcon, CpuChipIcon } from '@heroicons/react/24/outline';
import { useState } from 'react'; 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'; import { minePow } from '../../utils/mine';
const difficulty = 10 const difficulty = 10
export const relay = relayInit('wss://nostr.lu.ke') const NewThreadCard: React.FC = () => {
const NewThreadCard = () => {
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
let sk = generatePrivateKey() 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()
try { try {
const event = minePow({ const event = minePow({
kind: 1, kind: 1,
tags: [], tags: [],
content: 'Hello, world!', content: comment,
created_at: Math.floor(Date.now() / 1000), //needs to be date To Unix created_at: Math.floor(Date.now() / 1000),
pubkey: pk, pubkey: getPublicKey(sk),
}, difficulty) }, difficulty);
const signedEvent = finishEvent(event, sk) const signedEvent = finishEvent(event, sk);
await relay.publish(signedEvent) // await publish(signedEvent);
console.log(signedEvent.id) console.log(signedEvent.id);
} catch (error) { } catch (error) {
setComment(comment + " " + error); setComment(comment + " " + error);
} }
relay.close()
}; };
// async function attachFile(file_input: File | null) { // async function attachFile(file_input: File | null) {
@ -80,6 +67,8 @@ const NewThreadCard = () => {
name="com" name="com"
wrap="soft" wrap="soft"
className="w-full p-2 rounded bg-gradient-to-r from-blue-900 to-cyan-500 text-white border-none" 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)}
/> />
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -87,6 +76,7 @@ const NewThreadCard = () => {
<ArrowUpTrayIcon className="h-6 w-6 text-white" /> <ArrowUpTrayIcon className="h-6 w-6 text-white" />
<input type="file" className="hidden" /> <input type="file" className="hidden" />
</div> </div>
<span className="flex items-center"><CpuChipIcon className="h-6 w-6 text-white" />: {difficulty}</span>
<button type="submit" className="px-4 py-2 bg-gradient-to-r from-cyan-900 to-blue-500 rounded text-white font-semibold"> <button type="submit" className="px-4 py-2 bg-gradient-to-r from-cyan-900 to-blue-500 rounded text-white font-semibold">
Submit Submit
</button> </button>
@ -98,5 +88,4 @@ const NewThreadCard = () => {
); );
}; };
export default NewThreadCard; export default NewThreadCard;

View File

@ -21,11 +21,6 @@ const colorCombos = [
'from-sky-400 to-cyan-500' '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 => { const getColorFromHash = (id: string, colors: string[]): string => {
// Create a simple hash from the event.id // Create a simple hash from the event.id
let hash = 0; let hash = 0;

View File

@ -1,152 +1,108 @@
import { import {Event, Filter, relayInit, Relay, Sub} from 'nostr-tools';
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react"
import { Relay, Filter, Event, relayInit, Sub } from "nostr-tools"
import { uniqBy } from "./utils"
type OnConnectFunc = (relay: Relay) => void type SubCallback = (
type OnDisconnectFunc = (relay: Relay) => void event: Readonly<Event>,
type OnEventFunc = (event: Event) => void relay: Readonly<string>,
type OnDoneFunc = () => void ) => void;
type OnSubscribeFunc = (sub: Sub, relay: Relay) => void
interface NostrContextType { type Subscribe = {
isLoading: boolean cb: SubCallback;
debug?: boolean filter: Filter;
connectedRelays: Relay[] unsub?: boolean;
onConnect: (_onConnectCallback?: OnConnectFunc) => void };
onDisconnect: (_onDisconnectCallback?: OnDisconnectFunc) => void
publish: (event: Event) => void const subList: Array<Sub> = [];
const currentSubList: Array<Subscribe> = [];
const relayMap = new Map<string, Relay>();
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}`);
} }
};
const NostrContext = createContext<NostrContextType>({ export const unsubscribe = (sub: Sub) => {
isLoading: true, sub.unsub();
connectedRelays: [], subList.splice(subList.indexOf(sub), 1);
onConnect: () => null, };
onDisconnect: () => null,
publish: () => null,
})
const log = ( const subscribe = (
isOn: boolean | undefined, cb: SubCallback,
type: "info" | "error" | "warn", filter: Filter,
...args: unknown[] relay: Relay,
unsub?: boolean
) => { ) => {
if (!isOn) return const sub = relay.sub([filter]);
console[type](...args) 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 function NostrProvider({ export const sub = (obj: Subscribe) => {
children, currentSubList.push(obj);
relayUrls, relayMap.forEach((relay) => subscribe(obj.cb, obj.filter, relay, obj.unsub));
debug, };
}: {
children: ReactNode
relayUrls: string[]
debug?: boolean
}) {
const [isLoading, setIsLoading] = useState(true)
const [connectedRelays, setConnectedRelays] = useState<Relay[]>([])
const [relays, setRelays] = useState<Relay[]>([])
const relayUrlsRef = useRef<string[]>([])
let onConnectCallback: null | OnConnectFunc = null export const subOnce = (
let onDisconnectCallback: null | OnDisconnectFunc = null obj: Subscribe & {relay: string}
) => {
const disconnectToRelays = useCallback( const relay = relayMap.get(obj.relay);
(relayUrls: string[]) => { if (relay) {
relayUrls.forEach(async (relayUrl) => { const sub = subscribe(obj.cb, obj.filter, relay);
await relays.find((relay) => relay.url === relayUrl)?.close() sub.on('eose', () => {
setRelays((prev) => prev.filter((r) => r.url !== relayUrl)) // console.log('eose', obj.relay);
}) unsubscribe(sub);
}, });
[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")) export const unsubAll = () => {
relay.connect() subList.forEach(unsubscribe);
currentSubList.length = 0;
};
relay.on("connect", () => { type PublishCallback = (
log(debug, "info", `✅ nostr (${relayUrl}): Connected!`) relay: string,
setIsLoading(false) errorMessage?: string,
onConnectCallback?.(relay) ) => void;
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", () => { export const publish = (event: Event, cb: PublishCallback) => {
log(debug, "error", `❌ nostr (${relayUrl}): Connection error!`) relayMap.forEach(async (relay, url) => {
}) try {
}) await relay.publish(event);
}, console.info(`${relay.url} has accepted our event`);
[connectedRelays, debug, onConnectCallback, onDisconnectCallback], cb(relay.url);
) } catch (reason) {
console.error(`failed to publish to ${relay.url}: ${reason}`);
useEffect(() => { cb(relay.url, reason as string);
if (relayUrlsRef.current === relayUrls) {
// relayUrls isn't updated, skip
return
} }
});
};
const relayUrlsToDisconnect = relayUrlsRef.current.filter(
(relayUrl) => !relayUrls.includes(relayUrl),
)
disconnectToRelays(relayUrlsToDisconnect) addRelay('wss://relay.snort.social');
connectToRelays(relayUrls) addRelay('wss://nostr.bitcoiner.social');
addRelay('wss://nostr.mom');
relayUrlsRef.current = relayUrls addRelay('wss://relay.nostr.bg');
}, [relayUrls, connectToRelays, disconnectToRelays]) addRelay('wss://nos.lol');
// addRelay('wss://relay.nostr.ch');
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 <NostrContext.Provider value={value}>{children}</NostrContext.Provider>
}
export function useNostr() {
return useContext(NostrContext)
}

View File

@ -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<string>();
const notes = new Set<string>();
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,
}
});
};