I've been thinking about this, and I think we need to get away from storing tables (unordered lists) in events, that can get really long and are frequently updated. Could we use labels, instead of bookmarks?
From ef62256d050161fdfccafd2111005ab46c4f49dd Mon Sep 17 00:00:00 2001
From: Alejandro Gómez
Date: Mon, 22 Apr 2024 12:54:38 +0200
Subject: [PATCH] repo bookmarking
---
src/lib/components/AsyncButton.svelte | 21 +++++++++++++++++++++
src/lib/components/repo/RepoDetails.svelte | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
src/lib/components/stars/icons.ts | 11 +++++++++++
src/lib/components/stars/type.ts | 13 +++++++++++++
src/lib/kinds.ts | 2 ++
src/lib/promise.ts | 3 +++
src/lib/stores/Issues.ts | 2 +-
src/lib/stores/Proposal.ts | 2 +-
src/lib/stores/Stargazers.ts | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib/wrappers/RepoMenu.svelte | 33 ++++++++++++++++++++++++++++++++-
src/lib/wrappers/RepoPageWrapper.svelte | 2 ++
src/routes/repo/[repo_id]/stargazers/+page.svelte | 34 ++++++++++++++++++++++++++++++++++
src/routes/repo/[repo_id]/stargazers/+page.ts | 5 +++++
13 files changed, 317 insertions(+), 5 deletions(-)
create mode 100644 src/lib/components/AsyncButton.svelte
create mode 100644 src/lib/components/stars/icons.ts
create mode 100644 src/lib/components/stars/type.ts
create mode 100644 src/lib/promise.ts
create mode 100644 src/lib/stores/Stargazers.ts
create mode 100644 src/routes/repo/[repo_id]/stargazers/+page.svelte
create mode 100644 src/routes/repo/[repo_id]/stargazers/+page.ts
diff --git a/src/lib/components/AsyncButton.svelte b/src/lib/components/AsyncButton.svelte
new file mode 100644
index 0000000..a750834
--- /dev/null
+++ b/src/lib/components/AsyncButton.svelte
@@ -0,0 +1,21 @@
+
+ let isLoading = false
+
+ export let disabled: boolean | undefined = false
+ export let onClick = async () => {}
+
+ async function onClickHandler(){
+ isLoading = true;
+ try {
+ await onClick();
+ } catch (error) {
+ console.error(error);
+ } finally {
+ isLoading = false;
+ }
+ }
+
+
+
+
+
diff --git a/src/lib/components/repo/RepoDetails.svelte b/src/lib/components/repo/RepoDetails.svelte
index 9be1f41..287ed68 100644
--- a/src/lib/components/repo/RepoDetails.svelte
+++ b/src/lib/components/repo/RepoDetails.svelte
@@ -1,6 +1,14 @@
import UserHeader from '$lib/components/users/UserHeader.svelte'
- import { NDKUser } from '@nostr-dev-kit/ndk'
+ import AsyncButton from '$lib/components/AsyncButton.svelte'
+ import { timeout } from '$lib/promise'
+ import { star_icon_path } from '$lib/components/stars/icons'
+ import { ndk } from '$lib/stores/ndk'
+ import { stargazers } from '$lib/stores/Stargazers'
+ import { getUserRelays, logged_in_user } from '$lib/stores/users'
+ import { bookmarks_kind } from '$lib/kinds'
+ import { NDKUser, NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk'
+ import { nip19 } from 'nostr-tools'
import { icons_misc } from '../icons'
import { event_defaults } from './type'
@@ -27,9 +35,91 @@
let naddr_copied = false
let git_url_copied: false | string = false
let maintainer_copied: false | string = false
+
+ let ref: string | undefined = undefined;
+ $: if (naddr) {
+ const decoded = nip19.decode(naddr)
+ if (decoded.type === "naddr") {
+ const { kind, pubkey, identifier } = decoded.data;
+ ref = `${kind}:${pubkey}:${identifier}`
+ }
+ }
+
+ let isStarred = false
+ $: isStarred = $stargazers.events.some((e) => e.pubkey === $logged_in_user?.hexpubkey)
+
+ async function toggleStarred(){
+ if (!$logged_in_user) {
+ return
+ }
+ const user_relays = await getUserRelays($logged_in_user.hexpubkey)
+ const relayUrls = [
+ ...relays,
+ ...(user_relays.ndk_relays
+ ? user_relays.ndk_relays.writeRelayUrls
+ : []),
+ ]
+ const relaySet = NDKRelaySet.fromRelayUrls(relayUrls, ndk)
+
+ if (isStarred) {
+ let event = new NDKEvent(ndk);
+ const oldEvent = $stargazers.events.find(e => e.pubkey === $logged_in_user.hexpubkey);
+ event.kind = bookmarks_kind;
+ event.content = oldEvent.content;
+ event.tags = oldEvent.tags.filter(t => t[0] === 'a' && t[1] !== ref);
+ try {
+ await event.sign()
+ } catch {
+ alert('failed to sign event')
+ }
+ try {
+ await event.publish(relaySet)
+ stargazers.update((stars) => {
+ return {
+ ...stars,
+ events: stars.events.filter(e => e.pubkey !== $logged_in_user.hexpubkey),
+ }
+ })
+ } catch {
+ alert('failed to publish event')
+ }
+ } else {
+ const oldEvent = await Promise.race([
+ ndk.fetchEvent({
+ kinds: [bookmarks_kind],
+ author: [$logged_in_user.hexpubkey],
+ }, { groupable: false }, relaySet),
+ timeout(2000),
+ ])
+ let event = new NDKEvent(ndk)
+ event.kind = bookmarks_kind
+ if (oldEvent) {
+ event.tags = oldEvent.tags;
+ }
+ event.tags.push(['a', ref, relays.length ? relays[0] : ''])
+ try {
+ await event.sign()
+ } catch {
+ alert('failed to sign event')
+ }
+ try {
+ await event.publish(relaySet)
+ stargazers.update((stars) => {
+ return {
+ ...stars,
+ events: stars.events.filter(e => e.pubkey !== $logged_in_user.hexpubkey).concat([event]),
+ }
+ })
+ } catch {
+ alert('failed to publish event')
+ }
+ }
+ }
-
+
+
+
{#if name == identifier}
{#if loading}
@@ -66,6 +156,32 @@
{identifier}
{/if}
{/if}
+
+ {#if ref}
+
+
+ {#if isStarred}
+ Unstar
+ {:else}
+ Star
+ {/if}
+
+ {/if}
+
{#if loading}
diff --git a/src/lib/components/stars/icons.ts b/src/lib/components/stars/icons.ts
new file mode 100644
index 0000000..73c465a
--- /dev/null
+++ b/src/lib/components/stars/icons.ts
@@ -0,0 +1,11 @@
+// icon are MIT licenced
+export const star_icon_path = {
+ // https://icon-sets.iconify.design/gravity-ui/star/
+ outline: [
+ "m9.194 5l.351.873l.94.064l3.197.217l-2.46 2.055l-.722.603l.23.914l.782 3.108l-2.714-1.704L8 10.629l-.798.5l-2.714 1.705l.782-3.108l.23-.914l-.723-.603l-2.46-2.055l3.198-.217l.94-.064l.35-.874L8 2.025zm-7.723-.292l3.943-.268L6.886.773C7.29-.231 8.71-.231 9.114.773l1.472 3.667l3.943.268c1.08.073 1.518 1.424.688 2.118L12.185 9.36l.964 3.832c.264 1.05-.886 1.884-1.802 1.31L8 12.4l-3.347 2.101c-.916.575-2.066-.26-1.802-1.309l.964-3.832L.783 6.826c-.83-.694-.391-2.045.688-2.118",
+ ],
+ // https://icon-sets.iconify.design/gravity-ui/star-fill/
+ filled: [
+ "M6.886.773C7.29-.231 8.71-.231 9.114.773l1.472 3.667l3.943.268c1.08.073 1.518 1.424.688 2.118L12.185 9.36l.964 3.832c.264 1.05-.886 1.884-1.802 1.31L8 12.4l-3.347 2.101c-.916.575-2.066-.26-1.802-1.309l.964-3.832L.783 6.826c-.83-.694-.391-2.045.688-2.118l3.943-.268z",
+ ],
+}
diff --git a/src/lib/components/stars/type.ts b/src/lib/components/stars/type.ts
new file mode 100644
index 0000000..f076086
--- /dev/null
+++ b/src/lib/components/stars/type.ts
@@ -0,0 +1,13 @@
+import type { NDKEvent } from '@nostr-dev-kit/ndk'
+
+export interface Stargazers {
+ id: string | undefined
+ events: NDKEvent[]
+ loading: boolean;
+}
+
+export const stars_defaults: Stargazers = {
+ id: '',
+ events: [],
+ loading: true,
+}
diff --git a/src/lib/kinds.ts b/src/lib/kinds.ts
index fd65cf2..06cd21b 100644
--- a/src/lib/kinds.ts
+++ b/src/lib/kinds.ts
@@ -27,3 +27,5 @@ export const repo_kind: number = 30617
export const patch_kind: number = 1617
export const issue_kind: number = 1621
+
+export const bookmarks_kind: number = 10617
diff --git a/src/lib/promise.ts b/src/lib/promise.ts
new file mode 100644
index 0000000..1577059
--- /dev/null
+++ b/src/lib/promise.ts
@@ -0,0 +1,3 @@
+export function timeout(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
diff --git a/src/lib/stores/Issues.ts b/src/lib/stores/Issues.ts
index 23b6023..ba229fb 100644
--- a/src/lib/stores/Issues.ts
+++ b/src/lib/stores/Issues.ts
@@ -34,7 +34,7 @@ let selected_repo_id: string | undefined = ''
let sub: NDKSubscription
export const ensureIssueSummaries = async (repo_id: string | undefined) => {
- if (selected_repo_id == repo_id) return
+ if (selected_repo_id === repo_id) return
issue_summaries.set({
id: repo_id,
summaries: [],
diff --git a/src/lib/stores/Proposal.ts b/src/lib/stores/Proposal.ts
index c92d5f7..7aad603 100644
--- a/src/lib/stores/Proposal.ts
+++ b/src/lib/stores/Proposal.ts
@@ -109,7 +109,7 @@ export const ensureProposalFull = (
created_at: event.created_at,
comments: 0,
author: {
- hexpubkey: event.pubkey,
+ pubkey: event.pubkey,
loading: true,
npub: '',
},
diff --git a/src/lib/stores/Stargazers.ts b/src/lib/stores/Stargazers.ts
new file mode 100644
index 0000000..fbf5a11
--- /dev/null
+++ b/src/lib/stores/Stargazers.ts
@@ -0,0 +1,74 @@
+import {
+ NDKRelaySet,
+ type NDKEvent,
+ NDKSubscription,
+ type NDKFilter,
+} from '@nostr-dev-kit/ndk'
+import { writable, type Writable } from 'svelte/store'
+import { awaitSelectedRepoCollection } from './repo'
+import { selectRepoFromCollection } from '$lib/components/repo/utils'
+import { base_relays, ndk } from './ndk'
+import { repo_kind, bookmarks_kind } from '$lib/kinds'
+import { stars_defaults, type Stargazers } from '$lib/components/stars/type'
+
+export const stargazers: Writable
+
+let selected_repo_id: string | undefined = ''
+
+let sub: NDKSubscription
+
+export async function fetchStargazers(repo_id: string | undefined) {
+ if (selected_repo_id === repo_id) return
+ selected_repo_id = repo_id;
+ stargazers.set({
+ id: repo_id,
+ events: [],
+ loading: true,
+ })
+ if (sub) sub.stop()
+ if (repo_id) {
+ const repo_collection = await awaitSelectedRepoCollection(repo_id)
+ const repo = selectRepoFromCollection(repo_collection)
+ if (!repo) {
+ // TODO: display error info bar
+ return
+ }
+ const relays_to_use =
+ repo.relays.length > 3
+ ? repo.relays
+ : [...base_relays].concat(repo.relays)
+
+ // todo: relays usually return max 500 results, if a repo is very popular, we may need to paginate
+ const filter = {
+ kinds: [bookmarks_kind],
+ '#a': repo.maintainers.map((m) => `${repo_kind}:${m}:${repo.identifier}`),
+ }
+
+ sub = ndk.subscribe(
+ filter,
+ {
+ closeOnEose: false,
+ },
+ NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
+ )
+
+ sub.on('event', (event: NDKEvent) => {
+ stargazers.update((stars) => {
+ return {
+ ...stars,
+ events: stars.events.concat([event]),
+ loading: false,
+ }
+ })
+ })
+
+ sub.on('eose', () => {
+ stargazers.update((stars) => {
+ return {
+ ...stars,
+ loading: false,
+ }
+ })
+ })
+ }
+}
diff --git a/src/lib/wrappers/RepoMenu.svelte b/src/lib/wrappers/RepoMenu.svelte
index a5958be..df9e477 100644
--- a/src/lib/wrappers/RepoMenu.svelte
+++ b/src/lib/wrappers/RepoMenu.svelte
@@ -1,17 +1,23 @@
import { issue_icon_path } from '$lib/components/issues/icons'
+ import { star_icon_path } from '$lib/components/stars/icons'
import { proposal_icon_path } from '$lib/components/proposals/icons'
import type { RepoPage } from '$lib/components/repo/type'
import { proposal_status_open } from '$lib/kinds'
import { issue_summaries } from '$lib/stores/Issues'
+ import { stargazers } from '$lib/stores/Stargazers'
+ import { logged_in_user } from '$lib/stores/users'
import { proposal_summaries } from '$lib/stores/Proposals'
import { selected_repo_readme } from '$lib/stores/repo'
export let selected_tab: RepoPage = 'about'
export let identifier = ''
+
+ let isStarred = false
+ $: isStarred = $stargazers.events.some((e) => e.pubkey === $logged_in_user?.hexpubkey)
-
+
diff --git a/src/lib/wrappers/RepoPageWrapper.svelte b/src/lib/wrappers/RepoPageWrapper.svelte
index 76107b8..d9618cd 100644
--- a/src/lib/wrappers/RepoPageWrapper.svelte
+++ b/src/lib/wrappers/RepoPageWrapper.svelte
@@ -9,6 +9,7 @@
import Container from '$lib/components/Container.svelte'
import { ensureProposalSummaries } from '$lib/stores/Proposals'
import { ensureIssueSummaries } from '$lib/stores/Issues'
+ import { fetchStargazers } from '$lib/stores/Stargazers'
import type { RepoPage } from '$lib/components/repo/type'
export let identifier = ''
@@ -18,6 +19,7 @@
ensureSelectedRepoCollection(identifier)
ensureProposalSummaries(identifier)
ensureIssueSummaries(identifier)
+ fetchStargazers(identifier)
let repo_error = false
diff --git a/src/routes/repo/[repo_id]/stargazers/+page.svelte b/src/routes/repo/[repo_id]/stargazers/+page.svelte
new file mode 100644
index 0000000..e77188d
--- /dev/null
+++ b/src/routes/repo/[repo_id]/stargazers/+page.svelte
@@ -0,0 +1,34 @@
+
+ import UserHeader from '$lib/components/users/UserHeader.svelte'
+ import type { IssueSummary } from '$lib/components/issues/type'
+ import {
+ proposal_status_applied,
+ proposal_status_closed,
+ proposal_status_open,
+ statusKindtoText,
+ } from '$lib/kinds'
+ import { stargazers } from '$lib/stores/Stargazers'
+ import RepoPageWrapper from '$lib/wrappers/RepoPageWrapper.svelte'
+
+ export let data: { repo_id: string }
+ let identifier = data.repo_id
+ let status: number = proposal_status_open
+
+
+
+ {#if !$stargazers.loading }
+
+ {#if $stargazers.events.length === 0}
+
+ there aren't any stargazers yet
+
+ {:else}
+
+ {#each $stargazers.events as event}
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
diff --git a/src/routes/repo/[repo_id]/stargazers/+page.ts b/src/routes/repo/[repo_id]/stargazers/+page.ts
new file mode 100644
index 0000000..c70bf13
--- /dev/null
+++ b/src/routes/repo/[repo_id]/stargazers/+page.ts
@@ -0,0 +1,5 @@
+export const load = ({ params }: { params: { repo_id: string } }) => {
+ return {
+ repo_id: decodeURIComponent(params.repo_id),
+ }
+}
--
libgit2 1.7.2
Discussion
No replies yet.