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"