I just zapped this commit using amethyst:
verbiricha's avatar verbiricha
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 →

Replies (2)