diff --git a/package.json b/package.json index 60cd3c6..01ae1c8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@nostr-dev-kit/ndk": "^2.10.0", + "lucide-react": "^0.428.0", "react": "^18.3.1", "react-dom": "^18.3.1", "zustand": "^4.5.5" diff --git a/src/App.tsx b/src/App.tsx index 198e7ff..710594e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,55 @@ -import { NDKEvent } from "@nostr-dev-kit/ndk"; -import { useStore } from "./store"; +import { getRelayListForUsers, NDKEvent } from "@nostr-dev-kit/ndk"; +import { ndk, useStore } from "./store"; import { JIM_INSTANCE_KIND } from "./types"; +import { ExternalLink, Router, ThumbsUp } from "lucide-react"; export default function App() { const store = useStore(); + async function login() { + return store.login(); + } + + async function _publishToRelays(event: NDKEvent) { + const user = await ndk.signer?.user(); + if (!user) { + throw new Error("Could not fetch user"); + } + const relayLists = await getRelayListForUsers([user.pubkey], ndk); + const relayList = relayLists.get(user.pubkey); + + if (!relayList?.relays?.length) { + throw new Error("User has no relays"); + } + + if ( + !confirm( + "Confirm publish event " + + JSON.stringify(event.rawEvent()) + + " to relays " + + relayList.relays.join(", "), + ) + ) { + throw new Error("user cancelled"); + } + const publishedRelays = await event.publish( + relayList.relaySet, + undefined, + 1, + ); + alert( + "Published to " + + Array.from(publishedRelays) + .map((relay) => relay.url) + .join(", "), + ); + } + async function addJim() { + if (!(await login())) { + alert("Failed to login"); + return; + } const promptResponse = prompt("Enter your Jim URL"); if (!promptResponse) { return; @@ -17,47 +61,44 @@ export default function App() { if (jimUrl.endsWith("/")) { jimUrl = jimUrl.substring(0, jimUrl.length - 1); } - if (!confirm("Confirm publish new Jim: " + url)) { - return; - } } catch (error) { alert("Invalid URL: " + error); return; } - const event = new NDKEvent(store.ndk); + const event = new NDKEvent(ndk); event.kind = JIM_INSTANCE_KIND; event.dTag = jimUrl.toString(); try { - const publishedRelays = await event.publish(undefined, undefined, 1); - - alert( - "Published to " + - Array.from(publishedRelays) - .map((relay) => relay.url) - .join(", "), - ); + await _publishToRelays(event); } catch (error) { alert("Publish failed: " + error); } } async function recommend(eventId: string) { - const event = new NDKEvent(store.ndk); + if (!(await login())) { + alert("Failed to login"); + return; + } + const event = new NDKEvent(ndk); event.kind = 38000; event.tags.push(["k", JIM_INSTANCE_KIND.toString()]); event.dTag = eventId; try { - if (!confirm("Confirm publish recommendation for event " + eventId)) { - return; - } - const publishedRelays = await event.publish(undefined, undefined, 1); - alert( - "Published to " + - Array.from(publishedRelays) - .map((relay) => relay.url) - .join(", "), - ); + await _publishToRelays(event); + } catch (error) { + alert("Publish failed: " + error); + } + } + + async function republish(event: NDKEvent) { + if (!(await login())) { + alert("Failed to login"); + return; + } + try { + await _publishToRelays(event); } catch (error) { alert("Publish failed: " + error); } @@ -68,12 +109,17 @@ export default function App() {
- Jim Index + Jim Index
{!store.hasLoaded && ( )} + {!store.isLoggedIn && store.hasLoaded && ( + + )} @@ -90,7 +136,7 @@ export default function App() {
@@ -108,28 +154,54 @@ export default function App() {

{jim.info?.description || "No description"}

-

- - {jim.url} - -

-

- {jim.recommendedByUsers.length} recommendations ( - {jim.recommendedByUsers.filter((r) => r.mutual).length} mutual) -

+ {!store.isLoggedIn && ( +

Login to see friend recommendations

+ )} + {store.isLoggedIn && ( +
+ {jim.recommendedByUsers.map((user) => ( +
+
+ +
+
+ ))} +
+ )} +
- Launch + + Visit +
diff --git a/src/store.ts b/src/store.ts index 4d45003..109b08c 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,5 +1,11 @@ // Import the package -import NDK, { NDKKind, NDKNip07Signer, NDKUser } from "@nostr-dev-kit/ndk"; +import NDK, { + NDKEvent, + NDKKind, + NDKNip07Signer, + NDKSigner, + NDKUser, +} from "@nostr-dev-kit/ndk"; import { create } from "zustand"; import { JIM_INSTANCE_KIND } from "./types"; @@ -10,8 +16,7 @@ if (window.nostr) { // TODO: do not create signer on startup as it launches dialog // Create a new NDK instance with explicit relays -const signer = new NDKNip07Signer(); -const ndk = new NDK({ +export const ndk = new NDK({ // TODO: review relays explicitRelayUrls: [ "wss://relay.damus.io", @@ -23,12 +28,12 @@ const ndk = new NDK({ "wss://nostr.stakey.net", "wss://relay.n057r.club", ], - signer, }); type Jim = { url: string; eventId: string; + event: NDKEvent; recommendedByUsers: { user: NDKUser; mutual: boolean }[]; info?: { name?: string; @@ -38,24 +43,44 @@ type Jim = { }; type Store = { - readonly ndk: NDK; readonly jims: Jim[]; + readonly isLoggedIn: boolean; readonly hasLoaded: boolean; setJims(jims: Jim[]): void; setLoaded(hasLoaded: boolean): void; + login(): Promise; }; -export const useStore = create((set) => ({ - ndk, +export const useStore = create((set, get) => ({ + isLoggedIn: false, jims: [], hasLoaded: false, setJims: (jims) => set({ jims }), setLoaded: (hasLoaded) => set({ hasLoaded }), + login: async () => { + if (get().isLoggedIn || !get().hasLoaded) { + return get().isLoggedIn; + } + set({ hasLoaded: false }); + const signer = new NDKNip07Signer(); + ndk.signer = signer; + await loadJims(); + await loadMutualRecommendations(signer); + set({ + isLoggedIn: true, + hasLoaded: true, + }); + return true; + }, })); (async () => { await ndk.connect(); + await loadJims(); + useStore.getState().setLoaded(true); +})(); +async function loadJims() { const jimInstanceEvents = await ndk.fetchEvents({ kinds: [JIM_INSTANCE_KIND as NDKKind], }); @@ -65,10 +90,23 @@ export const useStore = create((set) => ({ for (const event of jimInstanceEvents) { const url = event.dTag; if (url && !url.endsWith("/")) { + let info: Jim["info"]; + try { + const response = await fetch(new URL("/api/info", url)); + if (response.ok) { + info = await response.json(); + } + } catch (error) { + console.error("failed to fetch jim info", url, error); + continue; + } + jims.push({ eventId: event.id, url, recommendedByUsers: [], + event, + info, }); } } @@ -85,7 +123,6 @@ export const useStore = create((set) => ({ for (const recommendationEvent of jimRecommendationEvents) { const jim = jims.find((j) => j.eventId === recommendationEvent.dTag); if (jim) { - // TODO: save pubkeys jim.recommendedByUsers.push({ user: recommendationEvent.author, mutual: false, @@ -93,23 +130,9 @@ export const useStore = create((set) => ({ } } useStore.getState().setJims(jims); +} - // fetch jim info - for (const jim of jims) { - try { - const response = await fetch(new URL("/api/info", jim.url)); - if (response.ok) { - jim.info = await response.json(); - } - } catch (error) { - console.error("failed to fetch jim info", jim.url, error); - } - } - - useStore.getState().setJims(jims); - - // mutual recommendations - +async function loadMutualRecommendations(signer: NDKSigner) { console.log("fetching user..."); const user = await signer.user(); console.log("user", user); @@ -118,10 +141,12 @@ export const useStore = create((set) => ({ const followsPubkeys = [...Array.from(follows), user].map( (follow) => follow.pubkey, ); + const jims = useStore.getState().jims; for (const jim of jims) { for (const recommendedByUser of jim.recommendedByUsers) { if (followsPubkeys.includes(recommendedByUser.user.pubkey)) { recommendedByUser.mutual = true; + await recommendedByUser.user.fetchProfile(); } } } @@ -130,4 +155,4 @@ export const useStore = create((set) => ({ // TODO: sort jims useStore.getState().setLoaded(true); -})(); +} diff --git a/yarn.lock b/yarn.lock index 8900699..8731e24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1664,6 +1664,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lucide-react@^0.428.0: + version "0.428.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.428.0.tgz#dfd96d682c74f78c2961040036d8c1265ccce039" + integrity sha512-rGrzslfEcgqwh+TLBC5qJ8wvVIXhLvAIXVFKNHndYyb1utSxxn9rXOC+1CNJLi6yNOooyPqIs6+3YCp6uSiEvg== + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"