17 changed files with 408 additions and 41 deletions
@ -0,0 +1,200 @@
@@ -0,0 +1,200 @@
|
||||
import { NDKRelaySet, type NDKEvent, NDKSubscription } 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 { type IssueFull, full_defaults } from '$lib/components/issues/type' |
||||
import { proposal_status_kinds, proposal_status_open } from '$lib/kinds' |
||||
import { awaitSelectedRepoCollection } from './repo' |
||||
import { |
||||
extractIssueDescription, |
||||
extractIssueTitle, |
||||
} from '$lib/components/events/content/utils' |
||||
import { goto } from '$app/navigation' |
||||
import { selectRepoFromCollection } from '$lib/components/repo/utils' |
||||
|
||||
export const selected_issue_full: Writable<IssueFull> = writable({ |
||||
...full_defaults, |
||||
}) |
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let selected_issue_repo_id: string = '' |
||||
let selected_issue_id: string = '' |
||||
let issue_summary_author_unsubsriber: Unsubscriber | undefined |
||||
|
||||
export const selected_issue_replies: Writable<NDKEvent[]> = writable([]) |
||||
|
||||
let selected_issue_status_date = 0 |
||||
|
||||
let sub: NDKSubscription |
||||
|
||||
let sub_replies: NDKSubscription |
||||
|
||||
export const ensureIssueFull = (repo_identifier: string, issue_id: string) => { |
||||
if (selected_issue_id == issue_id) return |
||||
if (issue_id == '') { |
||||
selected_issue_full.set({ ...full_defaults }) |
||||
selected_issue_replies.set([]) |
||||
return |
||||
} |
||||
|
||||
if (sub) sub.stop() |
||||
if (sub_replies) sub_replies.stop() |
||||
|
||||
selected_issue_repo_id = repo_identifier |
||||
selected_issue_id = issue_id |
||||
selected_issue_status_date = 0 |
||||
selected_issue_replies.set([]) |
||||
|
||||
selected_issue_full.set({ |
||||
...full_defaults, |
||||
summary: { |
||||
...full_defaults.summary, |
||||
id: issue_id, |
||||
repo_identifier: repo_identifier, |
||||
loading: true, |
||||
}, |
||||
loading: true, |
||||
}) |
||||
if (issue_summary_author_unsubsriber) issue_summary_author_unsubsriber() |
||||
issue_summary_author_unsubsriber = undefined |
||||
|
||||
new Promise(async (r) => { |
||||
const repo_collection = await awaitSelectedRepoCollection(repo_identifier) |
||||
const repo = selectRepoFromCollection(repo_collection) |
||||
const relays_to_use = |
||||
repo && repo.relays.length > 3 |
||||
? repo.relays |
||||
: [...base_relays].concat(repo ? repo.relays : []) |
||||
|
||||
sub = ndk.subscribe( |
||||
{ |
||||
ids: [issue_id], |
||||
limit: 50, |
||||
}, |
||||
{ |
||||
closeOnEose: true, |
||||
}, |
||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
||||
) |
||||
|
||||
sub.on('event', (event: NDKEvent) => { |
||||
try { |
||||
if (event.id == issue_id) { |
||||
const event_repo_id = event.tagValue('a')?.split(':')[2] |
||||
if (event_repo_id && event_repo_id !== repo_identifier) { |
||||
goto(`/repo/${encodeURIComponent(event_repo_id)}/issue/${issue_id}`) |
||||
} |
||||
selected_issue_full.update((full) => { |
||||
return { |
||||
...full, |
||||
issue_event: event, |
||||
summary: { |
||||
...full.summary, |
||||
title: extractIssueTitle(event.content), |
||||
descritpion: extractIssueDescription(event.content), |
||||
created_at: event.created_at, |
||||
comments: 0, |
||||
author: { |
||||
hexpubkey: event.pubkey, |
||||
loading: true, |
||||
npub: '', |
||||
}, |
||||
loading: false, |
||||
}, |
||||
} |
||||
}) |
||||
|
||||
issue_summary_author_unsubsriber = ensureUser(event.pubkey).subscribe( |
||||
(u: User) => { |
||||
selected_issue_full.update((full) => { |
||||
return { |
||||
...full, |
||||
summary: { |
||||
...full.summary, |
||||
author: |
||||
event.pubkey == u.hexpubkey ? u : full.summary.author, |
||||
}, |
||||
} |
||||
}) |
||||
} |
||||
) |
||||
} |
||||
} catch {} |
||||
}) |
||||
|
||||
sub.on('eose', () => { |
||||
selected_issue_full.update((full) => { |
||||
const updated = { |
||||
...full, |
||||
summary: { |
||||
...full.summary, |
||||
loading: false, |
||||
}, |
||||
} |
||||
if (full.loading === false) { |
||||
r({ ...updated }) |
||||
} |
||||
return updated |
||||
}) |
||||
}) |
||||
|
||||
sub_replies = ndk.subscribe( |
||||
{ |
||||
'#e': [issue_id], |
||||
}, |
||||
{ |
||||
closeOnEose: false, |
||||
}, |
||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
||||
) |
||||
|
||||
const process_replies = (event: NDKEvent) => { |
||||
if ( |
||||
event.kind && |
||||
proposal_status_kinds.includes(event.kind) && |
||||
event.created_at && |
||||
selected_issue_status_date < event.created_at |
||||
) { |
||||
selected_issue_status_date = event.created_at |
||||
selected_issue_full.update((full) => { |
||||
return { |
||||
...full, |
||||
summary: { |
||||
...full.summary, |
||||
status: event.kind, |
||||
// this wont be 0 as we are ensuring it is not undefined above
|
||||
status_date: event.created_at || 0, |
||||
}, |
||||
} |
||||
}) |
||||
} |
||||
selected_issue_replies.update((replies) => { |
||||
return [...replies, event].sort( |
||||
(a, b) => (a.created_at || 0) - (b.created_at || 0) |
||||
) |
||||
}) |
||||
} |
||||
|
||||
sub_replies.on('event', (event: NDKEvent) => { |
||||
process_replies(event) |
||||
}) |
||||
|
||||
sub_replies.on('eose', () => { |
||||
selected_issue_full.update((full) => { |
||||
const updated = { |
||||
...full, |
||||
summary: { |
||||
...full.summary, |
||||
status: full.summary.status || proposal_status_open, |
||||
}, |
||||
loading: false, |
||||
} |
||||
if (full.summary.loading === false) { |
||||
r({ ...updated }) |
||||
} |
||||
return updated |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
<script lang="ts"> |
||||
import { |
||||
ensureSelectedRepoCollection, |
||||
selected_repo_collection, |
||||
selected_repo_event, |
||||
} from '$lib/stores/repo' |
||||
import { |
||||
ensureIssueFull, |
||||
selected_issue_full, |
||||
selected_issue_replies, |
||||
} from '$lib/stores/Issue' |
||||
import RepoHeader from '$lib/components/repo/RepoHeader.svelte' |
||||
import Thread from '$lib/wrappers/Thread.svelte' |
||||
import Container from '$lib/components/Container.svelte' |
||||
import ParsedContent from '$lib/components/events/content/ParsedContent.svelte' |
||||
import ComposeReply from '$lib/wrappers/ComposeReply.svelte' |
||||
import { patch_kind } from '$lib/kinds' |
||||
import Patch from '$lib/components/events/content/Patch.svelte' |
||||
import ProposalHeader from '$lib/components/proposals/ProposalHeader.svelte' |
||||
import ProposalDetails from '$lib/components/proposals/ProposalDetails.svelte' |
||||
|
||||
export let data: { |
||||
repo_id: string |
||||
issue_id: string |
||||
} |
||||
|
||||
let repo_id = data.repo_id |
||||
let issue_id = data.issue_id |
||||
|
||||
ensureSelectedRepoCollection(repo_id) |
||||
ensureIssueFull(repo_id, issue_id) |
||||
|
||||
let repo_error = false |
||||
let issue_error = false |
||||
$: { |
||||
repo_error = |
||||
!$selected_repo_collection.loading && |
||||
$selected_repo_event.name.length === 0 |
||||
issue_error = |
||||
!$selected_issue_full.summary.loading && |
||||
$selected_issue_full.summary.created_at === 0 |
||||
} |
||||
</script> |
||||
|
||||
{#if !repo_error} |
||||
<RepoHeader {...$selected_repo_event} /> |
||||
{/if} |
||||
|
||||
{#if issue_error} |
||||
<Container> |
||||
<div role="alert" class="alert alert-error m-auto mt-6 w-full max-w-xs"> |
||||
<svg |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
class="h-6 w-6 shrink-0 stroke-current" |
||||
fill="none" |
||||
viewBox="0 0 24 24" |
||||
><path |
||||
stroke-linecap="round" |
||||
stroke-linejoin="round" |
||||
stroke-width="2" |
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" |
||||
/></svg |
||||
> |
||||
<span>Error! cannot find Issue {repo_error ? 'or repo ' : ''}event</span> |
||||
</div> |
||||
</Container> |
||||
{:else} |
||||
<ProposalHeader {...$selected_issue_full.summary} /> |
||||
<Container> |
||||
<div class="mx-auto max-w-6xl md:flex"> |
||||
<div class="md:mr-2 md:w-2/3"> |
||||
<div class="max-w-4xl"> |
||||
<div class="my-3"> |
||||
<ParsedContent content={$selected_issue_full.summary.descritpion} /> |
||||
</div> |
||||
{#each $selected_issue_replies as event} |
||||
<Thread type="issue" {event} replies={[]} /> |
||||
{/each} |
||||
<div class="my-3"> |
||||
<ComposeReply type="issue" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="prose ml-2 hidden w-1/3 md:flex"> |
||||
<ProposalDetails |
||||
type="issue" |
||||
summary={$selected_issue_full.summary} |
||||
labels={$selected_issue_full.labels} |
||||
loading={$selected_issue_full.loading} |
||||
/> |
||||
</div> |
||||
</div> |
||||
</Container> |
||||
{/if} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
export const load = ({ |
||||
params, |
||||
}: { |
||||
params: { issue_id: string; repo_id: string } |
||||
}) => { |
||||
return { |
||||
repo_id: decodeURIComponent(params.repo_id), |
||||
issue_id: params.issue_id, |
||||
} |
||||
} |
||||
Loading…
Reference in new issue