I just zapped this commit using amethyst:
From ef62256d050161fdfccafd2111005ab46c4f49dd Mon Sep 17 00:00:00 2001
From: Alejandro Gómez <alejandrogomez@bitrefill.com>
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 @@
+<script lang="ts">
+ 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;
+ }
+ }
+</script>
+
+<button class="bg-transparent hover:bg-gray-100 hover:border-transparent hover:text-gray-700 border border-gray-500 text-xs font-semibold py-1 px-1 rounded inline-flex items-center" class:cursor-not-allowed={disabled || isLoading} class:cursor-progress={isLoading} disabled={disabled || isLoading ? 'true' : ''} on:click={onClickHandler}>
+ <slot />
+</button>
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 @@
<script lang="ts">
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')
+ }
+ }
+ }
</script>
-<div class="prose w-full max-w-md">
+<div class="w-full max-w-md">
+ <div class="flex justify-between items-start">
+ <div class="prose">
{#if name == identifier}
{#if loading}
<div class="skeleton my-3 h-5 w-20"></div>
@@ -66,6 +156,32 @@
<p class="my-2 break-words text-sm">{identifier}</p>
{/if}
{/if}
+ </div>
+ {#if ref}
+ <AsyncButton disabled={$logged_in_user || !ref ? '' : 'true'} onClick={toggleStarred}>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 16 16"
+ class={`w-4 h-4 mr-2 ${isStarred ? "fill-yellow-500" : "fill-gray-400"}`}
+ >
+ {#if isStarred}
+ {#each star_icon_path.filled as p}
+ <path fill-rule="evenodd" d={p} />
+ {/each}
+ {:else}
+ {#each star_icon_path.outline as p}
+ <path fill-rule="evenodd" d={p} />
+ {/each}
+ {/if}
+ </svg>
+ {#if isStarred}
+ <span>Unstar</span>
+ {:else}
+ <span>Star</span>
+ {/if}
+ </AsyncButton>
+ {/if}
+ </div>
{#if loading}
<div class="skeleton my-3 h-5 w-20"></div>
<div class="skeleton my-2 h-4"></div>
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<Stargazers> = writable(stars_defaults)
+
+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 @@
<script lang="ts">
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)
</script>
-<div class="flex border-b border-base-400">
+<div class="flex border-b border-base-400 overflow-x-auto">
<div role="tablist" class="tabs tabs-bordered flex-none">
{#if !$selected_repo_readme.failed}
<a
@@ -66,6 +72,31 @@
</span>
{/if}
</a>
+ <a
+ href={`/repo/${identifier}/stargazers`}
+ class="tab"
+ class:tab-active={selected_tab === 'stargazers'}
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 16 16"
+ class={`mb-1 mr-1 h-4 w-4 flex-none fill-base-content pt-1 ${isStarred ? "fill-yellow-500" : "opacity-50"}`}
+ >
+ {#if isStarred}
+ {#each star_icon_path.filled as p}
+ <path fill-rule="evenodd" d={p} />
+ {/each}
+ {:else}
+ {#each star_icon_path.outline as p}
+ <path fill-rule="evenodd" d={p} />
+ {/each}
+ {/if}
+ </svg>
+ Stars
+ <span class="badge badge-neutral badge-sm ml-2">
+ {$stargazers.events.length}
+ </span>
+ </a>
</div>
<div class="flex-grow"></div>
</div>
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 @@
+<script lang="ts">
+ 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
+</script>
+
+<RepoPageWrapper {identifier} selected_tab="stargazers">
+ {#if !$stargazers.loading }
+ <div class="mt-2 border border-base-400 p-2">
+ {#if $stargazers.events.length === 0}
+ <div class="py-10 text-center lowercase">
+ there aren't any stargazers yet
+ </div>
+ {:else}
+ <div class="flex flex-col gap-2">
+ {#each $stargazers.events as event}
+ <UserHeader user={event.pubkey} inline={true} size="md" />
+ {/each}
+ </div>
+ {/if}
+ </div>
+ {/if}
+</RepoPageWrapper>
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
View quoted note →