Skip to content

Commit

Permalink
feat: improve relay publishing
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz committed Aug 18, 2024
1 parent 12ad667 commit 16b8fd2
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 64 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
150 changes: 111 additions & 39 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
Expand All @@ -68,12 +109,17 @@ export default function App() {
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<img src="/jim-index/jim-sm.png" className="w-12 h-12" />
Jim Index
<span className="font-semibold">Jim Index</span>
</div>
<div className="flex items-center justify-end gap-2">
{!store.hasLoaded && (
<span className="loading loading-spinner loading-md"></span>
)}
{!store.isLoggedIn && store.hasLoaded && (
<button onClick={login} className="btn btn-secondary">
Login
</button>
)}
<button onClick={addJim} className="btn btn-primary">
Add Jim
</button>
Expand All @@ -90,7 +136,7 @@ export default function App() {
<img
src={
jim.info?.image ||
`https://api.dicebear.com/9.x/pixel-art/svg?seed=${jim.url}`
`https://api.dicebear.com/9.x/croodles-neutral/svg?seed=${jim.url}`
}
/>
</div>
Expand All @@ -108,28 +154,54 @@ export default function App() {
<p className="text-sm line-clamp-2" title={jim.info?.description}>
{jim.info?.description || "No description"}
</p>
<p>
<a href={jim.url} target="_blank" className="link">
{jim.url}
</a>
</p>
<p className="text-sm">
{jim.recommendedByUsers.length} recommendations (
{jim.recommendedByUsers.filter((r) => r.mutual).length} mutual)
</p>
{!store.isLoggedIn && (
<p className="text-xs">Login to see friend recommendations</p>
)}
{store.isLoggedIn && (
<div className="flex flex-wrap gap-2">
{jim.recommendedByUsers.map((user) => (
<div key={user.user.pubkey} className="avatar">
<div className="w-8 rounded-lg">
<img
title={
user.user.profile?.displayName ||
user.user.profile?.name ||
user.user.npub
}
src={
user.user.profile?.image ||
`https://api.dicebear.com/9.x/pixel-art/svg?seed=${user.user.pubkey}`
}
/>
</div>
</div>
))}
</div>
)}

<div className="card-actions justify-end mt-2">
<a
href={jim.url}
target="_blank"
className="btn btn-primary btn-sm"
title="Visit this Jim in a new tab"
>
Launch
<ExternalLink className="w-4" />
Visit
</a>
<button
onClick={() => recommend(jim.eventId)}
className="btn btn-secondary btn-sm"
className="btn btn-secondary btn-sm flex gap-2 items-center justify-center"
title="Recommend this relay"
>
<ThumbsUp className="w-4" /> {jim.recommendedByUsers.length}
</button>
<button
onClick={() => republish(jim.event)}
className="btn btn-secondary btn-sm flex gap-2 items-center justify-center"
title={`Published on ${jim.event.onRelays.length} relays (${jim.event.onRelays.map((relay) => relay.url).join(", ")})`}
>
Recommend
<Router className="w-4" /> {jim.event.onRelays.length}
</button>
</div>
</div>
Expand Down
75 changes: 50 additions & 25 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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",
Expand All @@ -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;
Expand All @@ -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<boolean>;
};

export const useStore = create<Store>((set) => ({
ndk,
export const useStore = create<Store>((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],
});
Expand All @@ -65,10 +90,23 @@ export const useStore = create<Store>((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,
});
}
}
Expand All @@ -85,31 +123,16 @@ export const useStore = create<Store>((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,
});
}
}
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);
Expand All @@ -118,10 +141,12 @@ export const useStore = create<Store>((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();
}
}
}
Expand All @@ -130,4 +155,4 @@ export const useStore = create<Store>((set) => ({
// TODO: sort jims

useStore.getState().setLoaded(true);
})();
}
Loading

0 comments on commit 16b8fd2

Please sign in to comment.