Browse Source

feat(RepoPage): add support for listing issues

add support for listing issues
master
DanConwayDev 2 years ago
parent
commit
39e453845b
No known key found for this signature in database
GPG Key ID: 68E15486D73F75E1
  1. 60
      src/lib/components/issues/type.ts
  2. 11
      src/lib/components/proposals/ProposalsList.svelte
  3. 4
      src/lib/components/proposals/ProposalsListItem.svelte
  4. 2
      src/lib/components/proposals/type.ts
  5. 2
      src/lib/kinds.ts
  6. 231
      src/lib/stores/Issues.ts
  7. 2
      src/routes/+page.svelte
  8. 49
      src/routes/repo/[repo_id]/+page.svelte

60
src/lib/components/issues/type.ts

@ -0,0 +1,60 @@
import type { User } from '../users/type'
import { defaults as user_defaults } from '../users/type'
import type { Event } from '../events/type'
import type { NDKEvent } from '@nostr-dev-kit/ndk'
export interface IssueSummary {
type: 'issue'
title: string
descritpion: string
repo_identifier: string
id: string
comments: number
status: undefined | number
status_date: number
author: User
created_at: number | undefined
loading: boolean
}
export const summary_defaults: IssueSummary = {
type: 'issue',
title: '',
descritpion: '',
repo_identifier: '',
id: '',
comments: 0,
status: undefined,
status_date: 0,
author: { ...user_defaults },
created_at: 0,
loading: true,
}
export interface IssueSummaries {
id: string | undefined
summaries: IssueSummary[]
loading: boolean
}
export const summaries_defaults: IssueSummaries = {
id: '',
summaries: [],
loading: true,
}
export interface IssueFull {
summary: IssueSummary
issue_event: NDKEvent | undefined
labels: string[]
events: Event[]
loading: boolean
}
export const full_defaults: IssueFull = {
summary: { ...summary_defaults },
issue_event: undefined,
labels: [],
events: [],
loading: true,
}

11
src/lib/components/proposals/ProposalsList.svelte

@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import ProposalsListItem from '$lib/components/proposals/ProposalsListItem.svelte' import ProposalsListItem from '$lib/components/proposals/ProposalsListItem.svelte'
import type { IssueSummary } from '../issues/type'
import type { ProposalSummary } from './type' import type { ProposalSummary } from './type'
export let title: string = '' export let title: string = ''
export let proposals: ProposalSummary[] = [] export let proposals_or_issues: ProposalSummary[] | IssueSummary[] = []
export let loading: boolean = false export let loading: boolean = false
export let show_repo: boolean = false export let show_repo: boolean = false
export let limit: number = 0 export let limit: number = 0
@ -17,22 +18,22 @@
<h4>{title}</h4> <h4>{title}</h4>
</div> </div>
{/if} {/if}
{#if proposals.length == 0 && !loading} {#if proposals_or_issues.length == 0 && !loading}
<p class="prose">None</p> <p class="prose">None</p>
{/if} {/if}
<ul class=" divide-y divide-base-400"> <ul class=" divide-y divide-base-400">
{#each proposals as proposal, index} {#each proposals_or_issues as proposal, index}
{#if current_limit === 0 || index + 1 <= current_limit} {#if current_limit === 0 || index + 1 <= current_limit}
<ProposalsListItem {...proposal} {show_repo} /> <ProposalsListItem {...proposal} {show_repo} />
{/if} {/if}
{/each} {/each}
{#if loading} {#if loading}
<ProposalsListItem loading={true} /> <ProposalsListItem loading={true} />
{#if proposals.length == 0} {#if proposals_or_issues.length == 0}
<ProposalsListItem loading={true} /> <ProposalsListItem loading={true} />
<ProposalsListItem loading={true} /> <ProposalsListItem loading={true} />
{/if} {/if}
{:else if allow_more && limit !== 0 && proposals.length > current_limit} {:else if allow_more && limit !== 0 && proposals_or_issues.length > current_limit}
<button <button
on:click={() => { on:click={() => {
current_limit = current_limit + 5 current_limit = current_limit + 5

4
src/lib/components/proposals/ProposalsListItem.svelte

@ -15,6 +15,8 @@
} from '$lib/kinds' } from '$lib/kinds'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
export let type: 'issue' | 'proposal' = 'proposal'
export let { export let {
title, title,
descritpion, descritpion,
@ -73,7 +75,7 @@
> >
{/if} {/if}
<a <a
href="/repo/{repo_identifier}/proposal/{id}" href="/repo/{repo_identifier}/{type}/{id}"
class="ml-3 grow overflow-hidden text-xs text-neutral-content" class="ml-3 grow overflow-hidden text-xs text-neutral-content"
class:pointer-events-none={loading} class:pointer-events-none={loading}
> >

2
src/lib/components/proposals/type.ts

@ -4,6 +4,7 @@ import type { Event } from '../events/type'
import type { NDKEvent } from '@nostr-dev-kit/ndk' import type { NDKEvent } from '@nostr-dev-kit/ndk'
export interface ProposalSummary { export interface ProposalSummary {
type: 'proposal'
title: string title: string
descritpion: string descritpion: string
repo_identifier: string repo_identifier: string
@ -17,6 +18,7 @@ export interface ProposalSummary {
} }
export const summary_defaults: ProposalSummary = { export const summary_defaults: ProposalSummary = {
type: 'proposal',
title: '', title: '',
descritpion: '', descritpion: '',
repo_identifier: '', repo_identifier: '',

2
src/lib/kinds.ts

@ -21,3 +21,5 @@ export function statusKindtoText(kind: number): string {
export const repo_kind: number = 30617 export const repo_kind: number = 30617
export const patch_kind: number = 1617 export const patch_kind: number = 1617
export const issue_kind: number = 1621

231
src/lib/stores/Issues.ts

@ -0,0 +1,231 @@
import {
NDKRelaySet,
type NDKEvent,
NDKSubscription,
type NDKFilter,
} from '@nostr-dev-kit/ndk'
import { writable, type Unsubscriber, type Writable } from 'svelte/store'
import { base_relays, ndk } from './ndk'
import type { User } from '$lib/components/users/type'
import { ensureUser } from './users'
import { awaitSelectedRepoCollection } from './repo'
import {
issue_kind,
proposal_status_kinds,
proposal_status_open,
repo_kind,
} from '$lib/kinds'
import { extractPatchMessage } from '$lib/components/events/content/utils'
import { selectRepoFromCollection } from '$lib/components/repo/utils'
import {
summary_defaults,
type IssueSummaries,
} from '$lib/components/issues/type'
export const issue_summaries: Writable<IssueSummaries> = writable({
id: '',
summaries: [],
loading: false,
})
let selected_repo_id: string | undefined = ''
let authors_unsubscribers: Unsubscriber[] = []
let sub: NDKSubscription
export const ensureIssueSummaries = async (repo_id: string | undefined) => {
if (selected_repo_id == repo_id) return
issue_summaries.set({
id: repo_id,
summaries: [],
loading: repo_id !== '',
})
if (sub) sub.stop()
if (sub_statuses) sub_statuses.stop()
authors_unsubscribers.forEach((u) => u())
authors_unsubscribers = []
selected_repo_id = repo_id
setTimeout(() => {
issue_summaries.update((summaries) => {
return {
...summaries,
loading: false,
}
})
}, 6000)
let relays_to_use = [...base_relays]
let filter: NDKFilter = {
kinds: [issue_kind],
limit: 50,
}
if (repo_id) {
const repo_collection = await awaitSelectedRepoCollection(repo_id)
const repo = selectRepoFromCollection(repo_collection)
if (!repo) {
// TODO: display error info bar
return
}
relays_to_use =
repo.relays.length > 3
? repo.relays
: [...base_relays].concat(repo.relays)
filter = {
kinds: [issue_kind],
'#a': repo.maintainers.map(
(m) => `${repo_kind}:${m.hexpubkey}:${repo.identifier}`
),
limit: 50,
}
}
sub = ndk.subscribe(
filter,
{
closeOnEose: true,
},
NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
)
sub.on('event', (event: NDKEvent) => {
try {
if (event.kind == issue_kind) {
if (!extractRepoIdentiferFromIssueEvent(event) && !repo_id) {
// link to issue will not work as it requires an identifier
return
}
issue_summaries.update((issues) => {
return {
...issues,
summaries: [
...issues.summaries,
{
...summary_defaults,
id: event.id,
repo_identifier:
extractRepoIdentiferFromIssueEvent(event) || repo_id || '',
title: (
event.tagValue('name') ||
event.tagValue('description') ||
extractPatchMessage(event.content) ||
''
).split('\n')[0],
descritpion: event.tagValue('description') || '',
created_at: event.created_at,
comments: 0,
author: {
hexpubkey: event.pubkey,
loading: true,
npub: '',
},
loading: false,
},
].sort((a, b) => (b.created_at || 0) - (a.created_at || 0)),
}
})
}
authors_unsubscribers.push(
ensureUser(event.pubkey).subscribe((u: User) => {
issue_summaries.update((issues) => {
return {
...issues,
summaries: issues.summaries.map((o) => ({
...o,
author: event.pubkey === o.author.hexpubkey ? u : o.author,
})),
}
})
})
)
} catch {}
})
sub.on('eose', () => {
issue_summaries.update((issues) => {
getAndUpdateIssueStatus(issues, relays_to_use)
return {
...issues,
loading: false,
}
})
})
}
let sub_statuses: NDKSubscription
function getAndUpdateIssueStatus(
issues: IssueSummaries,
relays: string[]
): void {
if (sub_statuses) sub_statuses.stop()
sub_statuses = ndk.subscribe(
{
kinds: proposal_status_kinds,
'#e': issues.summaries.map((issue) => issue.id),
},
{
closeOnEose: true,
},
NDKRelaySet.fromRelayUrls(relays, ndk)
)
sub_statuses.on('event', (event: NDKEvent) => {
const tagged_issue_event = event.tagValue('e')
if (
event.kind &&
proposal_status_kinds.includes(event.kind) &&
tagged_issue_event &&
event.created_at
) {
issue_summaries.update((issues) => {
return {
...issues,
summaries: issues.summaries.map((o) => {
if (
o.id === tagged_issue_event &&
event.created_at &&
o.status_date < event.created_at
) {
return {
...o,
status: event.kind as number,
status_date: event.created_at,
}
}
return o
}),
}
})
}
})
sub_statuses.on('eose', () => {
issue_summaries.update((issues) => {
return {
...issues,
summaries: issues.summaries.map((o) => ({
...o,
status: o.status || proposal_status_open,
})),
}
})
})
}
export const extractRepoIdentiferFromIssueEvent = (
event: NDKEvent
): string | undefined => {
const value = event.tagValue('a')
if (!value) return undefined
const split = value.split(':')
if (split.length < 3) return undefined
return split[2]
}

2
src/routes/+page.svelte

@ -76,7 +76,7 @@
<div class="hero md:basis-1/2"> <div class="hero md:basis-1/2">
<ProposalsList <ProposalsList
title="Recent Proposals" title="Recent Proposals"
proposals={$proposal_summaries.summaries} proposals_or_issues={$proposal_summaries.summaries}
show_repo={true} show_repo={true}
loading={$proposal_summaries.loading} loading={$proposal_summaries.loading}
limit={6} limit={6}

49
src/routes/repo/[repo_id]/+page.svelte

@ -12,12 +12,16 @@
proposal_summaries, proposal_summaries,
} from '$lib/stores/Proposals' } from '$lib/stores/Proposals'
import ProposalsList from '$lib/components/proposals/ProposalsList.svelte' import ProposalsList from '$lib/components/proposals/ProposalsList.svelte'
import { ensureIssueSummaries, issue_summaries } from '$lib/stores/Issues'
export let data: { repo_id: string } export let data: { repo_id: string }
let identifier = data.repo_id let identifier = data.repo_id
ensureSelectedRepoCollection(identifier) ensureSelectedRepoCollection(identifier)
ensureProposalSummaries(identifier) ensureProposalSummaries(identifier)
ensureIssueSummaries(identifier)
let selected_tab: 'issues' | 'proposals' = 'proposals'
let repo_error = false let repo_error = false
@ -58,11 +62,52 @@
<Container> <Container>
<div class="mt-2 md:flex"> <div class="mt-2 md:flex">
<div class="md:mr-2 md:w-2/3"> <div class="md:mr-2 md:w-2/3">
<div class="flex border-b border-base-400">
<div role="tablist" class="tabs tabs-bordered flex-none">
<button
on:click={() => {
selected_tab = 'proposals'
}}
role="tab"
class="tab"
class:tab-active={selected_tab === 'proposals'}
>
Proposals
{#if !$proposal_summaries.loading}
<span class="pl-1 opacity-30">
({$proposal_summaries.summaries.length})
</span>
{/if}
</button>
<button
on:click={() => {
selected_tab = 'issues'
}}
role="tab"
class="tab"
class:tab-active={selected_tab === 'issues'}
>
Issues
{#if !$issue_summaries.loading}
<span class="pl-1 opacity-30">
({$issue_summaries.summaries.length})
</span>
{/if}
</button>
</div>
<div class="flex-grow"></div>
</div>
{#if selected_tab === 'proposals'}
<ProposalsList <ProposalsList
title="Proposals" proposals_or_issues={$proposal_summaries.summaries}
proposals={$proposal_summaries.summaries}
loading={$proposal_summaries.loading} loading={$proposal_summaries.loading}
/> />
{:else if selected_tab === 'issues'}
<ProposalsList
proposals_or_issues={$issue_summaries.summaries}
loading={$issue_summaries.loading}
/>
{/if}
</div> </div>
<div class="prose ml-2 hidden w-1/3 md:flex"> <div class="prose ml-2 hidden w-1/3 md:flex">
<RepoDetails repo_id={identifier} /> <RepoDetails repo_id={identifier} />

Loading…
Cancel
Save