Browse Source

feat: use user and repository relays

- send status events to user and repository relays
- get repoistory events from base relays
- get pr events and replies from repository relays
master
DanConwayDev 2 years ago
parent
commit
7507ea2ea7
No known key found for this signature in database
GPG Key ID: 68E15486D73F75E1
  1. 157
      src/lib/components/prs/StatusSelector.svelte
  2. 206
      src/lib/stores/PR.ts
  3. 27
      src/lib/stores/PRs.ts
  4. 19
      src/lib/stores/ndk.ts
  5. 138
      src/lib/stores/repo.ts
  6. 89
      src/lib/stores/users.ts

157
src/lib/components/prs/StatusSelector.svelte

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ndk } from "$lib/stores/ndk"; import { ndk } from "$lib/stores/ndk";
import { NDKEvent, type NDKTag } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKRelaySet, type NDKTag } from "@nostr-dev-kit/ndk";
import type { PRStatus } from "./type"; import type { PRStatus } from "./type";
import { selected_pr_full } from "$lib/stores/PR"; import { selected_pr_full } from "$lib/stores/PR";
import { load } from "../../../routes/repo/[repo_id]/+page"; import { load } from "../../../routes/repo/[repo_id]/+page";
import { patch_kind } from "$lib/kinds"; import { patch_kind } from "$lib/kinds";
import { getLoggedInUserRelays, logged_in_user } from "$lib/stores/users";
import { selected_repo } from "$lib/stores/repo";
export let status: PRStatus = "Draft"; export let status: PRStatus = "Draft";
export let repo_id: string = ""; export let repo_id: string = "";
@ -12,19 +14,41 @@
let loading = false; let loading = false;
let edit_mode = false;
$: {
edit_mode =
$logged_in_user !== undefined && repo_id === $selected_repo.repo_id;
}
async function changeStatus(new_status: PRStatus) { async function changeStatus(new_status: PRStatus) {
let event = new NDKEvent(ndk); let event = new NDKEvent(ndk);
// TODO: use random custom kind for status instead of NIP32?
event.kind = patch_kind; event.kind = patch_kind;
event.tags.push(["t", new_status]); event.tags.push(["t", new_status]);
event.tags.push(["e", pr_id]); event.tags.push(["e", pr_id]);
event.tags.push(["r", `r-${repo_id}`]); event.tags.push(["r", `r-${repo_id}`]);
event.sign();
loading = true; loading = true;
// TODO send to repo relays, current user relay and pr event pubkey relays let relays = [...$selected_repo.relays];
try {
event.sign();
} catch {
alert("failed to sign event");
}
try { try {
// TODO: check if we are signed in a signer is in ndk let user_relays = await getLoggedInUserRelays();
// let res = await event.publish(); relays = [
...relays,
...(user_relays.ndk_relays
? user_relays.ndk_relays.writeRelayUrls
: []),
// TODO: pr event pubkey relays
];
} catch {
alert("failed to get user relays");
}
try {
let res = await event.publish(
NDKRelaySet.fromRelayUrls(relays, ndk),
);
selected_pr_full.update((pr_full) => { selected_pr_full.update((pr_full) => {
if (pr_full.summary.id !== pr_id) return pr_full; if (pr_full.summary.id !== pr_id) return pr_full;
return { return {
@ -47,6 +71,7 @@
class:btn-success={status === "Open"} class:btn-success={status === "Open"}
class:btn-primary={status === "Merged"} class:btn-primary={status === "Merged"}
class:btn-neutral={status === "Draft" || status === "Closed"} class:btn-neutral={status === "Draft" || status === "Closed"}
class:cursor-default={edit_mode}
class="btn btn-success btn-sm mr-6 align-middle" class="btn btn-success btn-sm mr-6 align-middle"
> >
{#if status === "Open"} {#if status === "Open"}
@ -94,64 +119,68 @@
> >
Draft Draft
{/if} {/if}
<svg {#if edit_mode}
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5s flex-none fill-success-content" viewBox="0 0 24 24"
><path class="h-5 w-5s flex-none fill-success-content"
fill="currentColor" ><path
d="M11.646 15.146L5.854 9.354a.5.5 0 0 1 .353-.854h11.586a.5.5 0 0 1 .353.854l-5.793 5.792a.5.5 0 0 1-.707 0" fill="currentColor"
/></svg d="M11.646 15.146L5.854 9.354a.5.5 0 0 1 .353-.854h11.586a.5.5 0 0 1 .353.854l-5.793 5.792a.5.5 0 0 1-.707 0"
> /></svg
</div> >
<ul
tabIndex={0}
class="dropdown-content z-[1] menu p-2 ml-0 shadow bg-base-300 rounded-box w-52"
>
{#if status !== "Draft"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Draft");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Draft</button
>
</li>
{/if}
{#if status !== "Open"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Open");
}}
class="btn btn-success btn-sm mx-2 align-middle"
>Open</button
>
</li>
{/if}
{#if status !== "Merged"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Merged");
}}
class="btn btn-primary btn-sm mx-2 align-middle"
>Merged</button
>
</li>
{/if}
{#if status !== "Closed"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Closed");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Closed</button
>
</li>
{/if} {/if}
</ul> </div>
{#if edit_mode}
<ul
tabIndex={0}
class="dropdown-content z-[1] menu p-2 ml-0 shadow bg-base-300 rounded-box w-52"
>
{#if status !== "Draft"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Draft");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Draft</button
>
</li>
{/if}
{#if status !== "Open"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Open");
}}
class="btn btn-success btn-sm mx-2 align-middle"
>Open</button
>
</li>
{/if}
{#if status !== "Merged"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Merged");
}}
class="btn btn-primary btn-sm mx-2 align-middle"
>Merged</button
>
</li>
{/if}
{#if status !== "Closed"}
<li class="pl-0">
<button
on:click={() => {
changeStatus("Closed");
}}
class="btn btn-neutral btn-sm mx-2 align-middle"
>Closed</button
>
</li>
{/if}
</ul>
{/if}
</div> </div>
{/if} {/if}

206
src/lib/stores/PR.ts

@ -1,15 +1,15 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { NDKRelaySet, type NDKEvent } from "@nostr-dev-kit/ndk";
import { writable, type Unsubscriber, type Writable } from "svelte/store" import { writable, type Unsubscriber, type Writable } from "svelte/store"
import { ndk } from "./ndk"; import { ndk } from "./ndk";
import type { User } from "$lib/components/users/type"; import type { User } from "$lib/components/users/type";
import { ensureUser } from "./users"; import { ensureUser } from "./users";
import { type PRFull, full_defaults, isPRStatus, type PRStatus } from "$lib/components/prs/type"; import { type PRFull, full_defaults, isPRStatus, type PRStatus } from "$lib/components/prs/type";
import { pr_kind, pr_status_kind } from "$lib/kinds"; import { pr_kind, pr_status_kind } from "$lib/kinds";
import { ensureSelectedRepo } from "./repo";
export let selected_pr_full: Writable<PRFull> = writable({ ...full_defaults }); export let selected_pr_full: Writable<PRFull> = writable({ ...full_defaults });
let selected_repo_id: string = ""; let selected_pr_repo_id: string = "";
let selected_pr_id: string = ""; let selected_pr_id: string = "";
let pr_summary_author_unsubsriber: Unsubscriber | undefined; let pr_summary_author_unsubsriber: Unsubscriber | undefined;
@ -21,124 +21,144 @@ export let ensurePRFull = (repo_id: string, pr_id: string) => {
if (selected_pr_id == pr_id) return; if (selected_pr_id == pr_id) return;
if (pr_id == "") { if (pr_id == "") {
selected_pr_full.set({ ...full_defaults }); selected_pr_full.set({ ...full_defaults });
selected_pr_replies.set([]);
return; return;
} }
selected_repo_id = repo_id; selected_pr_repo_id = repo_id;
selected_pr_id = pr_id; selected_pr_id = pr_id;
selected_pr_status_date = 0; selected_pr_status_date = 0;
selected_pr_replies.set([]); selected_pr_replies.set([]);
selected_pr_full.update(full => { selected_pr_full.set({
return { ...full_defaults,
...full, summary: {
summary: { ...full_defaults.summary,
...full.summary, id: pr_id,
id: pr_id, repo_id: repo_id,
repo_id: repo_id,
loading: true,
},
loading: true, loading: true,
}; },
loading: true,
}); });
if (pr_summary_author_unsubsriber) pr_summary_author_unsubsriber(); if (pr_summary_author_unsubsriber) pr_summary_author_unsubsriber();
pr_summary_author_unsubsriber = undefined; pr_summary_author_unsubsriber = undefined;
let sub = ndk.subscribe({ new Promise(async (r) => {
ids: [pr_id], let repo = await ensureSelectedRepo(repo_id);
limit: 1,
});
sub.on("event", (event: NDKEvent) => {
try {
if (event.kind == pr_kind
&& event.getMatchingTags("r").find(t => t[1] === `r-${repo_id}`)
&& event.id == pr_id
) {
selected_pr_full.update(full => { let sub = ndk.subscribe(
return { {
...full, ids: [pr_id],
pr_event: event, kinds: [pr_kind],
summary: { '#r': [`r-${repo_id}`],
...full.summary, limit: 50,
title: event.tagValue("name") || "", },
descritpion: event.tagValue("description") || "", {},
created_at: event.created_at, NDKRelaySet.fromRelayUrls(repo.relays, ndk),
comments: 0, );
author: {
hexpubkey: event.pubkey, sub.on("event", (event: NDKEvent) => {
loading: true, try {
npub: "", if (event.kind == pr_kind
}, && event.getMatchingTags("r").find(t => t[1] === `r-${repo_id}`)
loading: false, && event.id == pr_id
} ) {
};
});
pr_summary_author_unsubsriber = ensureUser(event.pubkey).subscribe((u: User) => {
selected_pr_full.update(full => { selected_pr_full.update(full => {
return { return {
...full, ...full,
pr_event: event,
summary: { summary: {
...full.summary, ...full.summary,
author: event.pubkey == u.hexpubkey ? u : full.summary.author, title: event.tagValue("name") || "",
descritpion: event.tagValue("description") || "",
created_at: event.created_at,
comments: 0,
author: {
hexpubkey: event.pubkey,
loading: true,
npub: "",
},
loading: false,
} }
}; };
}); });
});
}
} catch { }
});
sub.on("eose", () => { pr_summary_author_unsubsriber = ensureUser(event.pubkey).subscribe((u: User) => {
selected_pr_full.update(full => { selected_pr_full.update(full => {
return { return {
...full, ...full,
summary: { summary: {
...full.summary, ...full.summary,
loading: false, author: event.pubkey == u.hexpubkey ? u : full.summary.author,
}, }
}; };
});
});
}
} catch { }
}); });
});
let sub_replies = ndk.subscribe({ sub.on("eose", () => {
"#e": [pr_id], selected_pr_full.update(full => {
}); let updated = {
...full,
summary: {
...full.summary,
loading: false,
},
};
if (full.loading === false) {
r({ ...updated });
}
return updated;
});
});
let sub_replies = ndk.subscribe(
{
"#e": [pr_id],
},
{},
NDKRelaySet.fromRelayUrls(repo.relays, ndk),
);
sub_replies.on("event", (event: NDKEvent) => {
if (event.kind == pr_status_kind
&& event.created_at && selected_pr_status_date < event.created_at
&& event.getMatchingTags("t").length === 1
&& event.getMatchingTags("t")[0].length > 1
) {
let potential_status = event.getMatchingTags("t")[0][1];
sub_replies.on("event", (event: NDKEvent) => { if (isPRStatus(potential_status)) {
if (event.kind == pr_status_kind selected_pr_status_date = event.created_at;
&& event.created_at && selected_pr_status_date < event.created_at selected_pr_full.update(full => {
&& event.getMatchingTags("t").length === 1 return {
&& event.getMatchingTags("t")[0].length > 1 ...full,
) { status: potential_status as PRStatus,
let potential_status = event.getMatchingTags("t")[0][1]; };
});
if (isPRStatus(potential_status)) { }
selected_pr_status_date = event.created_at;
selected_pr_full.update(full => {
return {
...full,
status: potential_status as PRStatus,
};
});
} }
} selected_pr_replies.update(replies => {
selected_pr_replies.update(replies => { return [
return [ ...replies,
...replies, event,
event, ];
]; });
}); });
});
sub.on("eose", () => { sub.on("eose", () => {
selected_pr_full.update(full => { selected_pr_full.update(full => {
return { let updated = {
...full, ...full,
loading: false, loading: false,
}; };
if (full.summary.loading === false) {
r({ ...updated });
}
return updated;
});
}); });
}); });
} }

27
src/lib/stores/PRs.ts

@ -1,11 +1,11 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { NDKRelaySet, type NDKEvent } from "@nostr-dev-kit/ndk";
import { writable, type Unsubscriber, type Writable } from "svelte/store" import { writable, type Unsubscriber, type Writable } from "svelte/store"
import { ndk } from "./ndk"; import { ndk } from "./ndk";
import type { Repo } from "$lib/components/repo/type";
import { summary_defaults } from "$lib/components/prs/type"; import { summary_defaults } from "$lib/components/prs/type";
import type { User } from "$lib/components/users/type"; import type { User } from "$lib/components/users/type";
import { ensureUser, users } from "./users"; import { ensureUser } from "./users";
import type { PRSummaries, PRSummary } from "$lib/components/prs/type"; import type { PRSummaries } from "$lib/components/prs/type";
import { ensureSelectedRepo } from "./repo";
export let pr_summaries: Writable<PRSummaries> = writable({ export let pr_summaries: Writable<PRSummaries> = writable({
id: "", id: "",
@ -19,7 +19,7 @@ let selected_repo_id: string = "";
let authors_unsubscribers: Unsubscriber[] = []; let authors_unsubscribers: Unsubscriber[] = [];
export let ensurePRSummaries = (repo_id: string) => { export let ensurePRSummaries = async (repo_id: string) => {
if (selected_repo_id == repo_id) return; if (selected_repo_id == repo_id) return;
if (repo_id == "") return pr_summaries.set({ if (repo_id == "") return pr_summaries.set({
id: "", id: "",
@ -28,6 +28,9 @@ export let ensurePRSummaries = (repo_id: string) => {
}); });
selected_repo_id = repo_id; selected_repo_id = repo_id;
let repo = await ensureSelectedRepo(repo_id);
pr_summaries.update(prs => { pr_summaries.update(prs => {
return { return {
...prs, ...prs,
@ -38,11 +41,15 @@ export let ensurePRSummaries = (repo_id: string) => {
authors_unsubscribers.forEach(u => u()); authors_unsubscribers.forEach(u => u());
authors_unsubscribers = []; authors_unsubscribers = [];
let sub = ndk.subscribe({ let sub = ndk.subscribe(
kinds: [pr_kind], {
'#r': [`r-${repo_id}`], kinds: [pr_kind],
limit: 50, '#r': [`r-${repo_id}`],
}); limit: 50,
},
{},
NDKRelaySet.fromRelayUrls(repo.relays, ndk),
);
sub.on("event", (event: NDKEvent) => { sub.on("event", (event: NDKEvent) => {
try { try {

19
src/lib/stores/ndk.ts

@ -1,7 +1,24 @@
import NDKSvelte from '@nostr-dev-kit/ndk-svelte'; import NDKSvelte from '@nostr-dev-kit/ndk-svelte';
export let base_relays = import.meta.env.DEV
? [
"ws://localhost:8055",
]
: [
"wss://relayable.org",
"wss://relay.f7z.io",
"wss://relay.damus.io",
"wss://relay.snort.social",
// "wss://nostr.wine/",
// "wss://eden.nostr.land/",
// "wss://relay.nostr.band/",
];
// TODO: fallback_relays for if profile cannot be found
export const ndk = new NDKSvelte({ export const ndk = new NDKSvelte({
explicitRelayUrls: ['ws://localhost:8055'], explicitRelayUrls: [...base_relays],
}); });
ndk.connect(); ndk.connect();

138
src/lib/stores/repo.ts

@ -1,6 +1,6 @@
import { NDKUser } from "@nostr-dev-kit/ndk"; import { NDKRelaySet } from "@nostr-dev-kit/ndk";
import { writable, type Unsubscriber, type Writable } from "svelte/store" import { writable, type Unsubscriber, type Writable, get } from "svelte/store"
import { ndk } from "./ndk"; import { base_relays, ndk } from "./ndk";
import type { Repo } from "$lib/components/repo/type"; import type { Repo } from "$lib/components/repo/type";
import { defaults } from "$lib/components/repo/type"; import { defaults } from "$lib/components/repo/type";
import type { User } from "$lib/components/users/type"; import type { User } from "$lib/components/users/type";
@ -12,63 +12,87 @@ let selected_repo_id: string = "";
let maintainers_unsubscribers: Unsubscriber[] = []; let maintainers_unsubscribers: Unsubscriber[] = [];
export let ensureSelectedRepo = (repo_id: string) => { export let ensureSelectedRepo = async (repo_id: string): Promise<Repo> => {
if (selected_repo_id == repo_id) return selected_repo; if (selected_repo_id == repo_id) {
return new Promise(r => {
let unsubscriber = selected_repo.subscribe(repo => {
if (repo.repo_id === repo_id && !repo.loading) {
setTimeout(() => {
unsubscriber();
}, 5);
r({ ...repo });
}
});
})
}
selected_repo_id = repo_id; selected_repo_id = repo_id;
let sub = ndk.subscribe({ let sub = ndk.subscribe(
kinds: [repo_kind], {
'#d': [repo_id], kinds: [repo_kind],
limit: 1, '#d': [repo_id],
}); limit: 1,
},
{},
NDKRelaySet.fromRelayUrls(base_relays, ndk),
);
return new Promise((r) => {
sub.on("event", (event) => {
try {
if (event.kind == repo_kind && event.tagValue("d") == repo_id) {
selected_repo.set({
loading: false,
repo_id: event.replaceableDTag(),
name: event.tagValue("name") || "",
description: event.tagValue("description") || "",
git_server: event.tagValue("git_server") || "",
tags: event.getMatchingTags("t") || [],
maintainers: event.getMatchingTags("p").map(
(t: string[]) =>
({
hexpubkey: t[1],
loading: true,
npub: "",
}) as User,
),
relays: event
.getMatchingTags("relay")
.map((t: string[]) => t[1]),
});
let old_unsubscribers = maintainers_unsubscribers;
maintainers_unsubscribers = event
.getMatchingTags("p")
.map((t: string[]) => {
return ensureUser(t[1]).subscribe((u: User) => {
selected_repo.update((repo) => {
return {
...repo,
maintainers: repo.maintainers.map((m) => {
if (m.hexpubkey == u.hexpubkey) return { ...u };
else return { ...m };
}),
};
});
})
});
old_unsubscribers.forEach((unsubscriber) => unsubscriber());
}
} catch { }
});
sub.on("event", (event) => { sub.on("eose", () => {
try { selected_repo.update((repo) => {
if (event.kind == repo_kind && event.tagValue("d") == repo_id) { r({
selected_repo.set({ ...repo,
loading: false, loading: false,
repo_id: event.replaceableDTag(),
name: event.tagValue("name") || "",
description: event.tagValue("description") || "",
git_server: event.tagValue("git_server") || "",
tags: event.getMatchingTags("t") || [],
maintainers: event.getMatchingTags("p").map(
(t: string[]) =>
({
hexpubkey: t[1],
loading: true,
npub: "",
}) as User,
),
relays: event
.getMatchingTags("relay")
.map((t: string[]) => t[1]),
}); });
let old_unsubscribers = maintainers_unsubscribers; return {
maintainers_unsubscribers = event ...repo,
.getMatchingTags("p") loading: false,
.map((t: string[]) => { }
return ensureUser(t[1]).subscribe((u: User) => { })
selected_repo.update((repo) => { });
return {
...repo,
maintainers: repo.maintainers.map((m) => {
if (m.hexpubkey == u.hexpubkey) return { ...u };
else return { ...m };
}),
};
});
})
});
old_unsubscribers.forEach((unsubscriber) => unsubscriber());
}
} catch { }
});
sub.on("eose", () => {
selected_repo.update((repo) => {
return {
...repo,
loading: false,
}
})
}); });
} }

89
src/lib/stores/users.ts

@ -16,6 +16,7 @@ export let ensureUser = (hexpubkey: string): Writable<User> => {
}; };
users[hexpubkey] = writable(base); users[hexpubkey] = writable(base);
getUserRelays(hexpubkey);
u.fetchProfile().then( u.fetchProfile().then(
(p) => { (p) => {
users[hexpubkey].update((u) => ({ users[hexpubkey].update((u) => ({
@ -35,6 +36,17 @@ export let ensureUser = (hexpubkey: string): Writable<User> => {
return users[hexpubkey]; return users[hexpubkey];
} }
export let returnUser = async (hexpubkey: string): Promise<User> => {
return new Promise((r) => {
let unsubscriber = ensureUser(hexpubkey).subscribe((u) => {
if (!u.loading) {
unsubscriber();
r(u);
}
});
});
}
// nip07_plugin is set in Navbar component // nip07_plugin is set in Navbar component
export let nip07_plugin: Writable<undefined | boolean> = writable(undefined); export let nip07_plugin: Writable<undefined | boolean> = writable(undefined);
@ -89,3 +101,80 @@ export let login = async (): Promise<void> => {
} }
}); });
}; };
interface UserRelays {
loading: boolean;
ndk_relays: NDKRelayList | undefined;
}
export let user_relays: { [hexpubkey: string]: Writable<UserRelays>; } = {};
export let getUserRelays = async (hexpubkey: string): Promise<UserRelays> => {
return new Promise(async (res, rej) => {
if (user_relays[hexpubkey]) {
let unsubscriber: Unsubscriber;
unsubscriber = user_relays[hexpubkey].subscribe(querying_user_relays => {
if (querying_user_relays && !querying_user_relays.loading) {
res(querying_user_relays);
unsubscriber();
}
});
}
else {
user_relays[hexpubkey] = writable({
loading: true,
ndk_relays: undefined,
});
logged_in_user_relays.set({
loading: true,
ndk_relays: undefined,
});
let relay_list = await ndk.getUser({ hexpubkey }).relayList();
let querying_user_relays = {
loading: false,
ndk_relays: relay_list,
};
user_relays[hexpubkey].set({ ...querying_user_relays });
res(querying_user_relays);
}
});
};
export let logged_in_user_relays: Writable<undefined | UserRelays> = writable(undefined);
export let getLoggedInUserRelays = async (): Promise<UserRelays> => {
return new Promise(async (res, rej) => {
let user_relays = get(logged_in_user_relays);
if (user_relays) {
if (!user_relays.loading) return res(user_relays);
let unsubscriber = logged_in_user_relays.subscribe(user_relays => {
if (user_relays && !user_relays.loading) {
res(user_relays);
unsubscriber();
}
});
}
else {
let unsubscriber: Unsubscriber;
unsubscriber = logged_in_user.subscribe(async user => {
if (user) {
if (unsubscriber) unsubscriber();
logged_in_user_relays.set({
loading: true,
ndk_relays: undefined,
});
let relay_list = await ndk.getUser({ hexpubkey: user.hexpubkey }).relayList();
let user_relays = {
loading: false,
ndk_relays: relay_list,
};
logged_in_user_relays.set({ ...user_relays });
res(user_relays);
}
});
}
});
};
Loading…
Cancel
Save