diff --git a/client/src/App.tsx b/client/src/App.tsx index 2ac97ca..a4b1847 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,7 +3,9 @@ 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); @@ -13,7 +15,7 @@ function App() { }; return ( -
+
@@ -22,7 +24,7 @@ function App() {
-
+ ); } diff --git a/client/src/components/PostCard/PostCard.tsx b/client/src/components/PostCard/PostCard.tsx index 6f5b000..cd1e93b 100644 --- a/client/src/components/PostCard/PostCard.tsx +++ b/client/src/components/PostCard/PostCard.tsx @@ -25,6 +25,19 @@ 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; + for (let i = 0; i < id.length; i++) { + hash = (hash << 5) - hash + id.charCodeAt(i); + } + + // Use the hash to pick a color from the colors array + const index = Math.abs(hash) % colors.length; + return colors[index]; +}; + const timeAgo = (unixTime: number) => { const seconds = Math.floor((new Date().getTime() / 1000) - unixTime); @@ -52,6 +65,7 @@ const PostCard = ({ event}: { event: Event }) => { // limit: 200, // }, // ]); + const colorCombo = getColorFromHash(event.id, colorCombos); return ( <> @@ -59,7 +73,7 @@ const PostCard = ({ event}: { event: Event }) => {
-
+
Anonymous
diff --git a/client/src/utils/relays.tsx b/client/src/utils/relays.tsx new file mode 100644 index 0000000..16bf007 --- /dev/null +++ b/client/src/utils/relays.tsx @@ -0,0 +1,152 @@ +import { +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 OnDisconnectFunc = (relay: Relay) => void +type OnEventFunc = (event: Event) => void +type OnDoneFunc = () => void +type OnSubscribeFunc = (sub: Sub, relay: Relay) => 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, +}) + +const log = ( + isOn: boolean | undefined, + type: "info" | "error" | "warn", + ...args: unknown[] +) => { + if (!isOn) return + console[type](...args) +} + +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 function useNostr() { + return useContext(NostrContext) + } \ No newline at end of file diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts new file mode 100644 index 0000000..d2a7fc5 --- /dev/null +++ b/client/src/utils/utils.ts @@ -0,0 +1,21 @@ +export const uniqBy = (arr: T[], key: keyof T): T[] => { + return Object.values( + arr.reduce( + (map, item) => ({ + ...map, + [`${item[key]}`]: item, + }), + {}, + ), + ) + } + + export const uniqValues = (value: string, index: number, self: string[]) => { + return self.indexOf(value) === index + } + + export const dateToUnix = (_date?: Date) => { + const date = _date || new Date() + + return Math.floor(date.getTime() / 1000) + } \ No newline at end of file