Browse Source

feat: intelligently load repo events

choose repo events based from earliest_unique_commit and indentifier
based on:
 - number of mentions (issues and root patches)
  - most recent created_at

identify repo events for the same repository based on identical identifiers
and use of earliest_unique_commit

use stores for collections of repo events
master
DanConwayDev 2 years ago
parent
commit
2c2ee926eb
No known key found for this signature in database
GPG Key ID: 68E15486D73F75E1
  1. 4
      src/lib/components/RepoSummaryCard.svelte
  2. 4
      src/lib/components/ReposSummaryList.svelte
  3. 3
      src/lib/components/proposals/ProposalsListItem.svelte
  4. 14
      src/lib/components/proposals/StatusSelector.svelte
  5. 11
      src/lib/components/repo/RepoDetails.svelte
  6. 12
      src/lib/components/repo/RepoHeader.svelte
  7. 38
      src/lib/components/repo/type.ts
  8. 161
      src/lib/components/repo/utils.spec.ts
  9. 17
      src/lib/components/repo/utils.ts
  10. 34
      src/lib/components/repo/vectors.ts
  11. 29
      src/lib/stores/Proposal.ts
  12. 41
      src/lib/stores/Proposals.ts
  13. 2
      src/lib/stores/ndk.ts
  14. 152
      src/lib/stores/repo.ts
  15. 313
      src/lib/stores/repos.ts
  16. 17
      src/lib/wrappers/Compose.svelte
  17. 0
      src/lib/wrappers/RecentProposals.svelte
  18. 9
      src/lib/wrappers/RepoDetails.svelte
  19. 139
      src/lib/wrappers/ReposRecent.svelte
  20. 20
      src/routes/repo/[repo_id]/+page.svelte
  21. 14
      src/routes/repo/[repo_id]/proposal/[proposal_id]/+page.svelte
  22. 121
      src/routes/repo/[repo_id]/proposal/[proposal_id]/page.svelte

4
src/lib/components/RepoSummaryCard.svelte

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
import { summary_defaults } from './repo/type'
import UserHeader from './users/UserHeader.svelte'
export let { name, description, repo_id, maintainers, loading } =
export let { name, description, identifier, maintainers, loading } =
summary_defaults
let short_name: string
$: {
@ -24,7 +24,7 @@ @@ -24,7 +24,7 @@
{:else}
<a
class="link-primary break-words"
href="/repo/{encodeURIComponent(repo_id)}">{short_name}</a
href="/repo/{encodeURIComponent(identifier)}">{short_name}</a
>
{#if short_descrption.length > 0}
<p class="text-muted break-words pb-1 text-sm">

4
src/lib/components/ReposSummaryList.svelte

@ -17,8 +17,8 @@ @@ -17,8 +17,8 @@
<p class="prose">None</p>
{:else}
<div class="">
{#each repos as { name, description, repo_id, maintainers }}
<RepoSummaryCard {name} {description} {repo_id} {maintainers} />
{#each repos as { name, description, identifier, maintainers }}
<RepoSummaryCard {name} {description} {identifier} {maintainers} />
{/each}
{#if loading}
<RepoSummaryCard loading={true} />

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

@ -17,10 +17,12 @@ @@ -17,10 +17,12 @@
dayjs.extend(relativeTime)
export let {
title,
descritpion,
id,
repo_id,
comments,
status,
status_date,
author,
created_at,
loading,
@ -74,6 +76,7 @@ @@ -74,6 +76,7 @@
<a
href="/repo/{repo_id}/proposal/{id}"
class="ml-3 grow overflow-hidden text-xs text-neutral-content"
class:pointer-events-none={loading}
>
{#if loading}
<div class="skeleton h-5 w-60 flex-none pt-1"></div>

14
src/lib/components/proposals/StatusSelector.svelte

@ -13,7 +13,10 @@ @@ -13,7 +13,10 @@
statusKindtoText,
} from '$lib/kinds'
import { getUserRelays, logged_in_user } from '$lib/stores/users'
import { selected_repo } from '$lib/stores/repo'
import {
selected_repo_collection,
selected_repo_event,
} from '$lib/stores/repo'
import Status from '$lib/components/proposals/Status.svelte'
export let status: number | undefined = undefined
@ -25,7 +28,8 @@ @@ -25,7 +28,8 @@
let edit_mode = false
$: {
edit_mode =
$logged_in_user !== undefined && repo_id === $selected_repo.repo_id
$logged_in_user !== undefined &&
repo_id === $selected_repo_collection.identifier
}
async function changeStatus(new_status_kind: number) {
@ -42,10 +46,10 @@ @@ -42,10 +46,10 @@
.forEach((revision) => {
event.tags.push(['e', revision.id, 'mention'])
})
if ($selected_repo.unique_commit)
event.tags.push(['r', $selected_repo.unique_commit])
if ($selected_repo_event.unique_commit)
event.tags.push(['r', $selected_repo_event.unique_commit])
loading = true
let relays = [...$selected_repo.relays]
let relays = [...$selected_repo_event.relays]
try {
event.sign()
} catch {

11
src/lib/components/repo/RepoDetails.svelte

@ -1,17 +1,22 @@ @@ -1,17 +1,22 @@
<script lang="ts">
import UserHeader from '$lib/components/users/UserHeader.svelte'
import { defaults } from './type'
import { event_defaults } from './type'
export let {
repo_id,
event_id,
identifier,
unique_commit,
name,
description,
clone,
web,
tags,
maintainers,
relays,
referenced_by,
created_at,
loading,
} = defaults
} = event_defaults
$: short_descrption =
description.length > 500 ? description.slice(0, 450) + '...' : description
</script>

12
src/lib/components/repo/RepoHeader.svelte

@ -1,18 +1,22 @@ @@ -1,18 +1,22 @@
<script lang="ts">
import Container from '../Container.svelte'
import { defaults } from './type'
import { event_defaults } from './type'
export let {
repo_id,
event_id,
identifier,
unique_commit,
name,
description,
clone,
web,
tags,
maintainers,
relays,
referenced_by,
created_at,
loading,
} = defaults
} = event_defaults
let short_name: string
$: {
if (name.length > 45) short_name = name.slice(0, 45) + '...'
@ -29,7 +33,7 @@ @@ -29,7 +33,7 @@
</div>
{:else}
<a
href={`/repo/${repo_id}`}
href={`/repo/${identifier}`}
class="strong btn btn-ghost mb-0 mt-0 break-words px-3 text-sm"
>{short_name}</a
>

38
src/lib/components/repo/type.ts

@ -1,39 +1,65 @@ @@ -1,39 +1,65 @@
import { defaults as user_defaults, type User } from '../users/type'
export interface Repo {
repo_id: string
export interface RepoEvent {
event_id: string
identifier: string
unique_commit: string | undefined
name: string
description: string
clone: string
web: string[]
tags: string[]
maintainers: User[]
relays: string[]
referenced_by: string[]
created_at: number
loading: boolean
}
export const defaults: Repo = {
repo_id: '',
export const event_defaults: RepoEvent = {
event_id: '',
identifier: '',
unique_commit: '',
name: '',
description: '',
clone: '',
web: [],
tags: [],
maintainers: [],
relays: [],
referenced_by: [],
created_at: 0,
loading: true,
}
export interface RepoCollection {
selected_event_id: string
unique_commit: string
identifier: string
events: RepoEvent[]
loading: boolean
}
export const collection_defaults: RepoCollection = {
identifier: '',
unique_commit: '',
selected_event_id: '',
events: [],
loading: true,
}
export interface RepoSummary {
name: string
description: string
repo_id: string
identifier: string
unique_commit: string | undefined
maintainers: User[]
loading?: boolean
created_at: number
}
export const summary_defaults: RepoSummary = {
name: '',
repo_id: '',
identifier: '',
unique_commit: undefined,
description: '',
maintainers: [{ ...user_defaults }],
loading: false,

161
src/lib/components/repo/utils.spec.ts

@ -0,0 +1,161 @@ @@ -0,0 +1,161 @@
import { describe, expect, test } from 'vitest'
import { selectRepoFromCollection } from './utils'
import {
collection_defaults,
event_defaults,
type RepoCollection,
type RepoEvent,
} from './type'
const repo_event: RepoEvent = {
...event_defaults,
event_id: '123',
unique_commit: 'abc123',
identifier: 'abc',
created_at: 10,
}
describe('getSelectedRepo', () => {
describe('selected_event_id is default (empty string)', () => {
test('if no events returns undefined', () => {
expect(
selectRepoFromCollection({
...collection_defaults,
selected_event_id: 'b',
} as RepoCollection)
).toBeUndefined()
})
test('if no event with id returns undefined', () => {
expect(
selectRepoFromCollection({
...collection_defaults,
selected_event_id: 'b',
events: [
{
...repo_event,
event_id: 'a',
created_at: 1,
referenced_by: ['d', 'e'],
},
],
} as RepoCollection)
).toBeUndefined()
})
test('returns event with selected id', () => {
const preferable_event = {
...repo_event,
event_id: 'a',
created_at: 1,
referenced_by: ['d', 'e'],
}
expect(
selectRepoFromCollection({
...collection_defaults,
selected_event_id: preferable_event.event_id,
events: [
preferable_event,
{
...repo_event,
event_id: 'b',
created_at: 2,
referenced_by: ['x', 'y', 'z'],
},
{
...repo_event,
event_id: 'c',
created_at: 3,
},
],
} as RepoCollection)
).toEqual(preferable_event)
})
})
describe('selected_event_id is default (empty string)', () => {
test('if no events returns undefined', () => {
expect(
selectRepoFromCollection({
...collection_defaults,
} as RepoCollection)
).toBeUndefined()
})
test('if referenced_by is undefined (still loading), treat its as no references', () => {
const preferable_event = {
...repo_event,
event_id: 'c',
referenced_by: ['d', 'e'],
created_at: 2,
}
expect(
selectRepoFromCollection({
...collection_defaults,
events: [
{
...repo_event,
event_id: 'a',
created_at: 1,
},
{
...repo_event,
event_id: 'b',
created_at: 3,
},
preferable_event,
],
} as RepoCollection)
).toEqual(preferable_event)
})
test('if no references to either event return youngest', () => {
const preferable_event = {
...repo_event,
event_id: 'b',
created_at: 3,
}
expect(
selectRepoFromCollection({
...collection_defaults,
events: [
{
...repo_event,
event_id: 'a',
created_at: 1,
},
preferable_event,
{
...repo_event,
event_id: 'c',
created_at: 2,
},
],
} as RepoCollection)
).toEqual(preferable_event)
})
test('returns most referenced event', () => {
const preferable_event = {
...repo_event,
event_id: 'b',
created_at: 2,
referenced_by: ['x', 'y', 'z'],
}
expect(
selectRepoFromCollection({
...collection_defaults,
events: [
{
...repo_event,
event_id: 'a',
created_at: 1,
referenced_by: ['d', 'e'],
},
preferable_event,
{
...repo_event,
event_id: 'c',
created_at: 3,
},
],
} as RepoCollection)
).toEqual(preferable_event)
})
})
})

17
src/lib/components/repo/utils.ts

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
import type { RepoCollection, RepoEvent } from './type'
export const selectRepoFromCollection = (
collection: RepoCollection
): RepoEvent | undefined => {
if (collection.selected_event_id && collection.selected_event_id.length > 0)
return collection.events.find(
(e) => e.event_id === collection.selected_event_id
)
return [...collection.events].sort((a, b) => {
const a_ref = a.referenced_by ? a.referenced_by.length : 0
const b_ref = b.referenced_by ? b.referenced_by.length : 0
if (a_ref === b_ref) return b.created_at - a.created_at
return b_ref - a_ref
})[0]
}

34
src/lib/components/repo/vectors.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { UserVectors, withName } from '../users/vectors'
import type { Repo, RepoSummary } from './type'
import type { RepoEvent, RepoSummary } from './type'
export const RepoSummaryCardArgsVectors = {
Short: {
@ -33,8 +33,8 @@ export const RepoSummaryCardArgsVectors = { @@ -33,8 +33,8 @@ export const RepoSummaryCardArgsVectors = {
],
} as RepoSummary,
}
const base: Repo = {
repo_id: '9ee507fc4357d7ee16a5d8901bedcd103f23c17d',
const base: RepoEvent = {
identifier: '9ee507fc4357d7ee16a5d8901bedcd103f23c17d',
unique_commit: '9ee507fc4357d7ee16a5d8901bedcd103f23c17d',
name: 'Short Name',
description: 'short description',
@ -50,23 +50,23 @@ const base: Repo = { @@ -50,23 +50,23 @@ const base: Repo = {
}
export const RepoDetailsArgsVectors = {
Short: { ...base } as Repo,
Short: { ...base } as RepoEvent,
Long: {
...base,
name: 'Long Name that goes on and on and on and on and on and on and on and on and on',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie. Nulla vitae purus nec augue accumsan facilisis sed sed ligula. Vestibulum sed risus lacinia risus lacinia molestie. Ut lorem quam, consequat eget tempus in, rhoncus vel nunc. Duis efficitur a leo vel sodales. Nam id fermentum lacus. Etiam nec placerat velit. Praesent ac consectetur est. Aenean iaculis commodo enim.\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie.',
} as Repo,
} as RepoEvent,
LongNoSpaces: {
...base,
name: 'LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName',
description:
'LoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsum',
} as Repo,
NoNameOrDescription: { ...base, name: '', description: '' } as Repo,
NoDescription: { ...base, description: '' } as Repo,
NoTags: { ...base, tags: [] } as Repo,
NoGitServer: { ...base, clone: '' } as Repo,
} as RepoEvent,
NoNameOrDescription: { ...base, name: '', description: '' } as RepoEvent,
NoDescription: { ...base, description: '' } as RepoEvent,
NoTags: { ...base, tags: [] } as RepoEvent,
NoGitServer: { ...base, clone: '' } as RepoEvent,
MaintainersOneProfileNotLoaded: {
...base,
maintainers: [
@ -74,7 +74,7 @@ export const RepoDetailsArgsVectors = { @@ -74,7 +74,7 @@ export const RepoDetailsArgsVectors = {
{ ...UserVectors.loading },
{ ...base.maintainers[2] },
],
} as Repo,
} as RepoEvent,
MaintainersOneProfileDisplayNameWithoutName: {
...base,
maintainers: [
@ -82,7 +82,7 @@ export const RepoDetailsArgsVectors = { @@ -82,7 +82,7 @@ export const RepoDetailsArgsVectors = {
{ ...UserVectors.display_name_only },
{ ...base.maintainers[2] },
],
} as Repo,
} as RepoEvent,
MaintainersOneProfileNameAndDisplayNamePresent: {
...base,
maintainers: [
@ -90,7 +90,7 @@ export const RepoDetailsArgsVectors = { @@ -90,7 +90,7 @@ export const RepoDetailsArgsVectors = {
{ ...UserVectors.display_name_and_name },
{ ...base.maintainers[2] },
],
} as Repo,
} as RepoEvent,
MaintainersOneProfileNoNameOrDisplayNameBeingPresent: {
...base,
maintainers: [
@ -98,8 +98,8 @@ export const RepoDetailsArgsVectors = { @@ -98,8 +98,8 @@ export const RepoDetailsArgsVectors = {
{ ...UserVectors.no_profile },
{ ...base.maintainers[2] },
],
} as Repo,
NoMaintainers: { ...base, maintainers: [] } as Repo,
NoRelays: { ...base, relays: [] } as Repo,
NoMaintainersOrRelays: { ...base, maintainers: [], relays: [] } as Repo,
} as RepoEvent,
NoMaintainers: { ...base, maintainers: [] } as RepoEvent,
NoRelays: { ...base, relays: [] } as RepoEvent,
NoMaintainersOrRelays: { ...base, maintainers: [], relays: [] } as RepoEvent,
}

29
src/lib/stores/Proposal.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { NDKRelaySet, type NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
import { writable, type Unsubscriber, type Writable } from 'svelte/store'
import { ndk } from './ndk'
import { base_relays, ndk } from './ndk'
import type { User } from '$lib/components/users/type'
import { ensureUser } from './users'
import {
@ -8,9 +8,10 @@ import { @@ -8,9 +8,10 @@ import {
full_defaults,
} from '$lib/components/proposals/type'
import { proposal_status_kinds, proposal_status_open } from '$lib/kinds'
import { ensureSelectedRepo } from './repo'
import { awaitSelectedRepoCollection } from './repo'
import { extractPatchMessage } from '$lib/components/events/content/utils'
import { goto } from '$app/navigation'
import { selectRepoFromCollection } from '$lib/components/repo/utils'
export const selected_proposal_full: Writable<ProposalFull> = writable({
...full_defaults,
@ -58,8 +59,16 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => { @@ -58,8 +59,16 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => {
if (proposal_summary_author_unsubsriber) proposal_summary_author_unsubsriber()
proposal_summary_author_unsubsriber = undefined
new Promise(async (r) => {
const repo = await ensureSelectedRepo(repo_id)
new Promise(async (r, reject) => {
const repo_collection = await awaitSelectedRepoCollection(repo_id)
const repo = selectRepoFromCollection(repo_collection)
if (!repo) {
return reject()
}
const relays_to_use =
repo.relays.length > 3
? repo.relays
: [...base_relays].concat(repo.relays)
sub = ndk.subscribe(
{
@ -69,9 +78,7 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => { @@ -69,9 +78,7 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => {
{
closeOnEose: true,
},
repo.relays.length > 0
? NDKRelaySet.fromRelayUrls(repo.relays, ndk)
: undefined
NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
)
sub.on('event', (event: NDKEvent) => {
@ -148,9 +155,7 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => { @@ -148,9 +155,7 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => {
{
closeOnEose: false,
},
repo.relays.length > 0
? NDKRelaySet.fromRelayUrls(repo.relays, ndk)
: undefined
NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
)
const process_replies = (event: NDKEvent) => {
@ -187,9 +192,7 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => { @@ -187,9 +192,7 @@ export const ensureProposalFull = (repo_id: string, proposal_id: string) => {
{
closeOnEose: true,
},
repo.relays.length > 0
? NDKRelaySet.fromRelayUrls(repo.relays, ndk)
: undefined
NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
)
sub_revision_replies.on('event', (event: NDKEvent) => {
process_replies(event)

41
src/lib/stores/Proposals.ts

@ -5,20 +5,21 @@ import { @@ -5,20 +5,21 @@ import {
type NDKFilter,
} from '@nostr-dev-kit/ndk'
import { writable, type Unsubscriber, type Writable } from 'svelte/store'
import { ndk } from './ndk'
import { base_relays, ndk } from './ndk'
import { summary_defaults } from '$lib/components/proposals/type'
import type { User } from '$lib/components/users/type'
import { ensureUser } from './users'
import type { ProposalSummaries } from '$lib/components/proposals/type'
import { ensureSelectedRepo } from './repo'
import { awaitSelectedRepoCollection } from './repo'
import {
patch_kind,
proposal_status_kinds,
proposal_status_open,
repo_kind,
} from '$lib/kinds'
import type { Repo } from '$lib/components/repo/type'
import type { RepoEvent } from '$lib/components/repo/type'
import { extractPatchMessage } from '$lib/components/events/content/utils'
import { selectRepoFromCollection } from '$lib/components/repo/utils'
export const proposal_summaries: Writable<ProposalSummaries> = writable({
id: '',
@ -47,14 +48,31 @@ export const ensureProposalSummaries = async (repo_id: string) => { @@ -47,14 +48,31 @@ export const ensureProposalSummaries = async (repo_id: string) => {
selected_repo_id = repo_id
const repo = await ensureSelectedRepo(repo_id)
setTimeout(() => {
proposal_summaries.update((summaries) => {
return {
...summaries,
loading: false,
}
})
}, 6000)
const repo_collection = await awaitSelectedRepoCollection(repo_id)
const repo = selectRepoFromCollection(repo_collection)
if (!repo) {
return
}
const relays_to_use =
repo.relays.length > 3 ? repo.relays : [...base_relays].concat(repo.relays)
const without_root_tag = !repo.unique_commit
const filter_with_root: NDKFilter = {
kinds: [patch_kind],
'#a': repo.maintainers.map(
(m) => `${repo_kind}:${m.hexpubkey}:${repo.repo_id}`
(m) => `${repo_kind}:${m.hexpubkey}:${repo.identifier}`
),
'#t': ['root'],
limit: 50,
@ -63,7 +81,7 @@ export const ensureProposalSummaries = async (repo_id: string) => { @@ -63,7 +81,7 @@ export const ensureProposalSummaries = async (repo_id: string) => {
const filter_without_root: NDKFilter = {
kinds: [patch_kind],
'#a': repo.maintainers.map(
(m) => `${repo_kind}:${m.hexpubkey}:${repo.repo_id}`
(m) => `${repo_kind}:${m.hexpubkey}:${repo.identifier}`
),
limit: 50,
}
@ -73,9 +91,7 @@ export const ensureProposalSummaries = async (repo_id: string) => { @@ -73,9 +91,7 @@ export const ensureProposalSummaries = async (repo_id: string) => {
{
closeOnEose: true,
},
repo.relays.length > 0
? NDKRelaySet.fromRelayUrls(repo.relays, ndk)
: undefined
NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
)
sub.on('event', (event: NDKEvent) => {
@ -145,8 +161,11 @@ let sub_statuses: NDKSubscription @@ -145,8 +161,11 @@ let sub_statuses: NDKSubscription
function getAndUpdateProposalStatus(
proposals: ProposalSummaries,
repo: Repo
repo: RepoEvent
): void {
const relays_to_use =
repo.relays.length > 3 ? repo.relays : [...base_relays].concat(repo.relays)
if (sub_statuses) sub_statuses.stop()
sub_statuses = ndk.subscribe(
{
@ -156,7 +175,7 @@ function getAndUpdateProposalStatus( @@ -156,7 +175,7 @@ function getAndUpdateProposalStatus(
{
closeOnEose: true,
},
NDKRelaySet.fromRelayUrls(repo.relays, ndk)
NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
)
sub_statuses.on('event', (event: NDKEvent) => {
const tagged_proposal_event = event.tagValue('e')

2
src/lib/stores/ndk.ts

@ -19,4 +19,4 @@ export const ndk = new NDKSvelte({ @@ -19,4 +19,4 @@ export const ndk = new NDKSvelte({
explicitRelayUrls: [...base_relays],
})
ndk.connect()
ndk.connect(5000)

152
src/lib/stores/repo.ts

@ -1,117 +1,61 @@ @@ -1,117 +1,61 @@
import { NDKEvent, NDKRelaySet, NDKSubscription } from '@nostr-dev-kit/ndk'
import { writable, type Unsubscriber, type Writable } from 'svelte/store'
import { base_relays, ndk } from './ndk'
import type { Repo } from '$lib/components/repo/type'
import { defaults } from '$lib/components/repo/type'
import type { User } from '$lib/components/users/type'
import { ensureUser } from './users'
import { repo_kind } from '$lib/kinds'
import type { RepoCollection, RepoEvent } from '$lib/components/repo/type'
import { collection_defaults, event_defaults } from '$lib/components/repo/type'
import { ensureRepoCollection } from './repos'
import { selectRepoFromCollection } from '$lib/components/repo/utils'
export const selected_repo: Writable<Repo> = writable({ ...defaults })
let selected_repo_id: string = ''
export const selected_repo_collection: Writable<RepoCollection> = writable({
...collection_defaults,
})
let maintainers_unsubscribers: Unsubscriber[] = []
export const selected_repo_event: Writable<RepoEvent> = writable({
...event_defaults,
})
let sub: NDKSubscription
export const ensureSelectedRepo = async (repo_id: string): Promise<Repo> => {
if (selected_repo_id == repo_id) {
return new Promise((r) => {
const unsubscriber = selected_repo.subscribe((repo) => {
if (repo.repo_id === repo_id && !repo.loading) {
setTimeout(() => {
unsubscriber()
}, 5)
r({ ...repo })
}
})
})
}
selected_repo_id = repo_id
selected_repo_collection.subscribe((collection) => {
const selected_from_collection = selectRepoFromCollection(collection)
if (selected_from_collection)
selected_repo_event.set({ ...selected_from_collection })
})
if (sub) sub.stop()
sub = ndk.subscribe(
{
kinds: [repo_kind],
'#d': [repo_id],
limit: 1,
},
{
closeOnEose: false,
},
NDKRelaySet.fromRelayUrls(base_relays, ndk)
)
let selected_repo_unique_commit_or_identifier: string = ''
return new Promise((r) => {
sub.on('event', (event: NDKEvent) => {
try {
if (event.kind == repo_kind && event.tagValue('d') == repo_id) {
const maintainers = [
{
hexpubkey: event.pubkey,
loading: true,
npub: '',
} as User,
]
event.getMatchingTags('maintainers').forEach((t: string[]) => {
t.forEach((v, i) => {
if (i > 0 && v !== maintainers[0].hexpubkey) {
maintainers.push({
hexpubkey: v,
loading: true,
npub: '',
} as User)
}
})
})
const relays: string[] = []
event.getMatchingTags('relays').forEach((t: string[]) => {
t.forEach((v, i) => {
if (i > 0) {
relays.push(v)
}
})
})
selected_repo.set({
loading: false,
repo_id: event.replaceableDTag(),
unique_commit: event.tagValue('r') || undefined,
name: event.tagValue('name') || '',
description: event.tagValue('description') || '',
clone: event.tagValue('clone') || '',
tags: event.getMatchingTags('t').map((t) => t[1]) || [],
maintainers,
relays,
})
const old_unsubscribers = maintainers_unsubscribers
maintainers_unsubscribers = maintainers.map((m: User) => {
return ensureUser(m.hexpubkey).subscribe((u: User) => {
selected_repo.update((repo) => {
return {
...repo,
maintainers: repo.maintainers.map((m) => {
if (m.hexpubkey == u.hexpubkey) return { ...u }
else return { ...m }
}),
}
})
})
let selected_unsubscriber: Unsubscriber
export const ensureSelectedRepoCollection = (
unique_commit_or_identifier: string
): Writable<RepoCollection> => {
if (
selected_repo_unique_commit_or_identifier !== unique_commit_or_identifier
) {
selected_repo_unique_commit_or_identifier = unique_commit_or_identifier
if (selected_unsubscriber) selected_unsubscriber()
selected_unsubscriber = ensureRepoCollection(
unique_commit_or_identifier
).subscribe((repo_collection) => {
selected_repo_collection.set({ ...repo_collection })
})
old_unsubscribers.forEach((unsubscriber) => unsubscriber())
}
} catch {}
})
return selected_repo_collection
}
sub.on('eose', () => {
selected_repo.update((repo) => {
r({
...repo,
loading: false,
})
return {
...repo,
loading: false,
export const awaitSelectedRepoCollection = async (
unique_commit_or_identifier: string
): Promise<RepoCollection> => {
return new Promise((r) => {
const unsubscriber = ensureSelectedRepoCollection(
unique_commit_or_identifier
).subscribe((repo_collection) => {
if (
selected_repo_unique_commit_or_identifier ==
unique_commit_or_identifier &&
!repo_collection.loading
) {
setTimeout(() => {
unsubscriber()
}, 5)
r({ ...repo_collection })
}
})
})
})
}

313
src/lib/stores/repos.ts

@ -0,0 +1,313 @@ @@ -0,0 +1,313 @@
import {
collection_defaults,
type RepoCollection,
type RepoEvent,
type RepoSummary,
} from '$lib/components/repo/type'
import { NDKRelaySet, type NDKFilter, NDKEvent } from '@nostr-dev-kit/ndk'
import { writable, type Writable } from 'svelte/store'
import { base_relays, ndk } from './ndk'
import { repo_kind } from '$lib/kinds'
import type { User } from '$lib/components/users/type'
import { ensureUser } from './users'
import { selectRepoFromCollection } from '$lib/components/repo/utils'
export const repos: {
[unique_commit_or_identifier: string]: Writable<RepoCollection>
} = {}
export const ensureRepoCollection = (
unique_commit_or_identifier: string
): Writable<RepoCollection> => {
if (!repos[unique_commit_or_identifier]) {
let base: RepoCollection = {
...collection_defaults,
}
if (unique_commit_or_identifier.length === 40) {
base = { ...base, unique_commit: unique_commit_or_identifier }
} else {
base = { ...base, identifier: unique_commit_or_identifier }
}
repos[unique_commit_or_identifier] = writable(base)
const filter: NDKFilter = base.unique_commit
? {
kinds: [repo_kind],
'#r': [base.unique_commit],
limit: 100,
}
: {
kinds: [repo_kind],
'#d': [base.identifier],
limit: 100,
}
const sub = ndk.subscribe(
filter,
{
groupable: true,
// default 100
groupableDelay: 200,
closeOnEose: true,
},
NDKRelaySet.fromRelayUrls(base_relays, ndk)
)
sub.on('event', (event: NDKEvent) => {
const repo_event = eventToRepoEvent(event)
if (repo_event) {
const collection_for_unique_commit =
unique_commit_or_identifier.length === 40
// get repo events with same identifer but no unique_commit as
// the assumption is that they will be the same repo
if (collection_for_unique_commit) {
ensureRepoCollection(repo_event.identifier)
// we will process them just before we turn loading to true
}
repos[unique_commit_or_identifier].update((repo_collection) => {
return {
...repo_collection,
events: [...repo_collection.events, repo_event as RepoEvent],
}
})
const relays_to_use =
repo_event.relays.length < 3
? repo_event.relays
: [...base_relays].concat(repo_event.relays)
// get references
const ref_sub = ndk.subscribe(
{
'#a': [
`${repo_kind}:${repo_event.maintainers[0].hexpubkey}:${repo_event.identifier}`,
],
limit: 10,
},
{
groupable: true,
// default 100
groupableDelay: 200,
closeOnEose: true,
},
NDKRelaySet.fromRelayUrls(relays_to_use, ndk)
)
ref_sub.on('event', (ref_event: NDKEvent) => {
repos[unique_commit_or_identifier].update((repo_collection) => {
return {
...repo_collection,
events: [
...repo_collection.events.map((latest_ref_event) => {
if (latest_ref_event.event_id === repo_event.event_id) {
return {
...latest_ref_event,
referenced_by: latest_ref_event.referenced_by
? [...latest_ref_event.referenced_by, ref_event.id]
: [ref_event.id],
}
}
return latest_ref_event
}),
],
}
})
})
ref_sub.on('eose', () => {
repos[unique_commit_or_identifier].update((repo_collection) => {
const events = [
...repo_collection.events.map((latest_ref_event) => {
if (latest_ref_event.event_id === repo_event.event_id) {
return {
...latest_ref_event,
// finished loading repo_event as we have all referenced_by events
loading: false,
}
}
return latest_ref_event
}),
]
const still_loading_events_in_collection = events.some(
(e) => e.loading
)
if (
collection_for_unique_commit &&
!still_loading_events_in_collection
)
addEventsWithMatchingIdentifiers(events)
return {
...repo_collection,
events,
loading:
still_loading_events_in_collection ||
// for uninque_commit loading will complete after extra identifer events are added
collection_for_unique_commit,
}
})
})
// load maintainers - we will subscribe later to prevent too many updates
repo_event.maintainers.forEach((m) => ensureUser(m.hexpubkey))
}
})
sub.on('eose', () => {
// still awaiting reference_by at this point
repos[unique_commit_or_identifier].update((repo_collection) => {
// subscribe to maintainers
const hexpubkeys = repo_collection.events.flatMap((repo_event) =>
repo_event.maintainers.map((m) => m.hexpubkey)
)
hexpubkeys.forEach((hexpubkey) => {
ensureUser(hexpubkey).subscribe((u) => {
repos[unique_commit_or_identifier].update((repo_collection) => ({
...repo_collection,
events: [
...repo_collection.events.map((repo_event) => ({
...repo_event,
maintainers: [
...repo_event.maintainers.map((m) => ({
...(m.hexpubkey === u.hexpubkey ? u : m),
})),
],
})),
],
}))
})
})
return {
...repo_collection,
loading: false,
}
})
})
}
setTimeout(() => {
repos[unique_commit_or_identifier].update((collection) => {
return {
...collection,
events: collection.events.map((e) => ({ ...e, loading: false })),
loading: false,
}
})
}, 5000)
return repos[unique_commit_or_identifier]
}
export const eventToRepoEvent = (event: NDKEvent): RepoEvent | undefined => {
if (event.kind !== repo_kind) return undefined
const maintainers = [
{
hexpubkey: event.pubkey,
loading: true,
npub: '',
} as User,
]
event.getMatchingTags('maintainers').forEach((t: string[]) => {
t.forEach((v, i) => {
if (i > 0 && v !== maintainers[0].hexpubkey) {
maintainers.push({
hexpubkey: v,
loading: true,
npub: '',
} as User)
}
})
})
const relays: string[] = []
event.getMatchingTags('relays').forEach((t: string[]) => {
t.forEach((v, i) => {
if (i > 0) {
relays.push(v)
}
})
})
const web: string[] = []
event.getMatchingTags('web').forEach((t: string[]) => {
t.forEach((v, i) => {
if (i > 0) {
relays.push(v)
}
})
})
return {
event_id: event.id,
identifier: event.replaceableDTag(),
unique_commit: event.tagValue('r') || undefined,
name: event.tagValue('name') || '',
description: event.tagValue('description') || '',
clone: event.tagValue('clone') || '',
web,
tags: event.getMatchingTags('t').map((t) => t[1]) || [],
maintainers,
relays,
referenced_by: [],
created_at: event.created_at || 0,
loading: true, // loading until references fetched
}
}
export const repoCollectionToSummary = (
collection: RepoCollection
): RepoSummary | undefined => {
const selected = selectRepoFromCollection(collection)
if (!selected) return undefined
return {
name: selected.name,
identifier: selected.identifier,
unique_commit: selected.unique_commit,
description: selected.description,
maintainers: selected.maintainers,
loading: collection.loading,
created_at: selected.created_at,
} as RepoSummary
}
/** to be called once all existing events have been found. this
* function is useful if we assume events with the same
* identifier reference the same repository */
const addEventsWithMatchingIdentifiers = (exisiting_events: RepoEvent[]) => {
// add events with same identifier but no unique_commit
exisiting_events
// filter out duplicate identifiers
.filter(
(e, i) =>
exisiting_events.findIndex((v) => v.identifier == e.identifier) === i
)
// subscribe to each identifier
.forEach((repo_event) => {
ensureRepoCollection(repo_event.identifier).subscribe(
(identiifer_collection) => {
// if extra event(s)
if (
identiifer_collection.events.some(
(identifier_repo) =>
!exisiting_events.some(
(e) => e.event_id === identifier_repo.event_id
)
)
) {
// add identifier events
repos[repo_event.unique_commit as string].update(
(repo_collection) => {
const events = [
...repo_collection.events,
...identiifer_collection.events
.filter(
(identifier_repo) =>
!repo_collection.events.some(
(e) => e.event_id === identifier_repo.event_id
)
)
.map((e) => ({ ...e })),
]
return {
...repo_collection,
events,
// if all RepoEvents are loaded, the collection is too
loading: events.some((e) => e.loading),
}
}
)
}
}
)
})
}

17
src/lib/wrappers/Compose.svelte

@ -3,7 +3,10 @@ @@ -3,7 +3,10 @@
import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk'
import { reply_kind, repo_kind } from '$lib/kinds'
import { getUserRelays, logged_in_user } from '$lib/stores/users'
import { selected_repo } from '$lib/stores/repo'
import {
selected_repo_collection,
selected_repo_event,
} from '$lib/stores/repo'
import Compose from '$lib/components/events/Compose.svelte'
import { selected_proposal_full } from '$lib/stores/Proposal'
@ -16,7 +19,7 @@ @@ -16,7 +19,7 @@
let submitted = false
let edit_mode = false
$: {
repo_id = $selected_repo.repo_id
repo_id = $selected_repo_collection.identifier
proposal_id = $selected_proposal_full.summary.id
edit_mode = repo_id.length > 0 && proposal_id.length > 0 && !submitted
@ -30,20 +33,20 @@ @@ -30,20 +33,20 @@
if (reply_to_event_id.length > 0) {
event.tags.push(['e', reply_to_event_id, 'reply'])
}
if ($selected_repo.unique_commit) {
event.tags.push(['r', $selected_repo.unique_commit])
if ($selected_repo_event.unique_commit) {
event.tags.push(['r', $selected_repo_event.unique_commit])
}
event.tags.push([
'a',
`${repo_kind}:${$selected_repo.maintainers[0].hexpubkey}:${repo_id}`,
`${repo_kind}:${$selected_repo_event.maintainers[0].hexpubkey}:${repo_id}`,
])
$selected_repo.maintainers.forEach((m) =>
$selected_repo_event.maintainers.forEach((m) =>
event.tags.push(['p', m.hexpubkey])
)
// TODO nip-10 reply chain p tags
event.content = content
submitting = true
let relays = [...$selected_repo.relays]
let relays = [...$selected_repo_event.relays]
try {
event.sign()
} catch {

0
src/lib/wrappers/RecentProposals.svelte

9
src/lib/wrappers/RepoDetails.svelte

@ -1,10 +1,13 @@ @@ -1,10 +1,13 @@
<script lang="ts">
import RepoDetails from '$lib/components/repo/RepoDetails.svelte'
import { ensureSelectedRepo, selected_repo } from '$lib/stores/repo'
import {
ensureSelectedRepoCollection,
selected_repo_event,
} from '$lib/stores/repo'
export let repo_id = ''
ensureSelectedRepo(repo_id)
ensureSelectedRepoCollection(repo_id)
</script>
<RepoDetails {...$selected_repo} />
<RepoDetails {...$selected_repo_event} />

139
src/lib/wrappers/ReposRecent.svelte

@ -1,114 +1,85 @@ @@ -1,114 +1,85 @@
<script lang="ts">
import ReposSummaryList from '$lib/components/ReposSummaryList.svelte'
import type { RepoSummary } from '$lib/components/repo/type'
import type { User } from '$lib/components/users/type'
import type { RepoEvent, RepoSummary } from '$lib/components/repo/type'
import { repo_kind } from '$lib/kinds'
import { ndk } from '$lib/stores/ndk'
import { ensureUser } from '$lib/stores/users'
import {
ensureRepoCollection,
eventToRepoEvent,
repoCollectionToSummary,
} from '$lib/stores/repos'
import type { NDKEvent } from '@nostr-dev-kit/ndk'
import { onDestroy } from 'svelte'
import type { Unsubscriber } from 'svelte/store'
import { writable, type Writable } from 'svelte/store'
export let limit: number = 50
export let limit: number = 100
let repos: Writable<RepoSummary[]> = writable([])
let repos: RepoSummary[] = []
let loading: boolean = true
let sub = ndk.subscribe({
kinds: [repo_kind],
limit,
})
let maintainers_unsubscribers: Unsubscriber[] = []
let events: RepoEvent[] = []
sub.on('event', (event: NDKEvent) => {
if (repos.length < limit) {
try {
if (event.kind == repo_kind) {
const maintainers = [
{
hexpubkey: event.pubkey,
loading: true,
npub: '',
} as User,
]
event.getMatchingTags('maintainers').forEach((t: string[]) => {
t.forEach((v, i) => {
if (i > 0 && v !== maintainers[0].hexpubkey) {
maintainers.push({
hexpubkey: v,
loading: true,
npub: '',
} as User)
}
})
let repo_event = eventToRepoEvent(event)
if (repo_event) events.push(repo_event)
})
// not duplicate name
if (!repos.some((r) => r.repo_id == event.replaceableDTag())) {
repos = [
...repos,
{
name: event.tagValue('name') || '',
description: event.tagValue('description') || '',
repo_id: event.replaceableDTag(),
maintainers,
created_at: event.created_at || 0,
},
]
} else {
// duplicate name
repos = [
...repos.map((r) => {
if (event.created_at && r.repo_id == event.replaceableDTag()) {
let new_maintainers = maintainers.filter(
(m) =>
!r.maintainers.some((o) => o.hexpubkey == m.hexpubkey)
sub.on('eose', () => {
let unique_commits = [
...new Set(events.map((e) => e.unique_commit).filter((s) => !!s)),
] as string[]
let identifers_not_linked_to_unique_commit = [
...new Set(events.map((e) => e.identifier)),
].filter(
(identifier) =>
!events.some((e) => e.identifier == identifier && e.unique_commit)
)
return {
name:
r.created_at < event.created_at
? event.tagValue('name') || r.name
: r.name,
description:
r.created_at < event.created_at
? event.tagValue('description') || r.description
: r.description,
repo_id: r.repo_id,
maintainers: [...r.maintainers, ...new_maintainers],
created_at:
r.created_at < event.created_at
? event.created_at
: r.created_at,
}
} else return { ...r }
unique_commits
.concat(identifers_not_linked_to_unique_commit)
.forEach((c) => {
ensureRepoCollection(c).subscribe((repo_collection) => {
let summary = repoCollectionToSummary(repo_collection)
if (!summary) return
repos.update((repos) => {
// if duplicate
if (
repos.some(
(repo) =>
(repo.unique_commit &&
repo.unique_commit === repo_collection.unique_commit) ||
(!repo.unique_commit &&
repo.identifier === repo_collection.identifier)
)
) {
return [
// update summary
...repos.map((repo) => {
if (
summary &&
((repo.unique_commit &&
repo.unique_commit === repo_collection.unique_commit) ||
(!repo.unique_commit &&
repo.identifier === repo_collection.identifier))
)
return summary
return { ...repo }
}),
]
}
// get maintainers profile
maintainers.forEach((m) => {
maintainers_unsubscribers.push(
ensureUser(m.hexpubkey).subscribe((u: User) => {
repos = repos.map((r) => {
return {
...r,
maintainers: r.maintainers.map((m) => {
if (m.hexpubkey == u.hexpubkey) return { ...u }
else return { ...m }
}),
}
// if not duplicate - add summary
else if (summary) return [...repos, summary]
return [...repos]
})
})
)
})
}
} catch {}
} else if (loading == true) loading = false
})
sub.on('eose', () => {
if (loading == true) loading = false
})
onDestroy(() => {
maintainers_unsubscribers.forEach((unsubscriber) => unsubscriber())
sub.stop()
})
</script>
<ReposSummaryList title="Latest Repositories" {repos} {loading} />
<ReposSummaryList title="Latest Repositories" repos={$repos} {loading} />

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

@ -1,18 +1,24 @@ @@ -1,18 +1,24 @@
<script lang="ts">
import RepoDetails from '$lib/wrappers/RepoDetails.svelte'
import OpenProposals from '$lib/wrappers/OpenProposals.svelte'
import { ensureSelectedRepo, selected_repo } from '$lib/stores/repo'
import {
ensureSelectedRepoCollection,
selected_repo_collection,
selected_repo_event,
} from '$lib/stores/repo'
import RepoHeader from '$lib/components/repo/RepoHeader.svelte'
import Container from '$lib/components/Container.svelte'
export let data: { repo_id: string }
let repo_id = data.repo_id
let identifier = data.repo_id
ensureSelectedRepo(repo_id)
ensureSelectedRepoCollection(identifier)
let repo_error = false
$: {
repo_error = !$selected_repo.loading && $selected_repo.name.length === 0
repo_error =
!$selected_repo_collection.loading &&
$selected_repo_event.name.length === 0
}
</script>
@ -35,15 +41,15 @@ @@ -35,15 +41,15 @@
</div>
</Container>
{:else}
<RepoHeader {...$selected_repo} />
<RepoHeader {...$selected_repo_event} />
<Container>
<div class="mt-2 md:flex">
<div class="md:mr-2 md:w-2/3">
<OpenProposals {repo_id} />
<OpenProposals repo_id={identifier} />
</div>
<div class="prose ml-2 hidden w-1/3 md:flex">
<RepoDetails {repo_id} />
<RepoDetails repo_id={identifier} />
</div>
</div>
</Container>

14
src/routes/repo/[repo_id]/proposal/[proposal_id]/+page.svelte

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
<script lang="ts">
import { ensureSelectedRepo, selected_repo } from '$lib/stores/repo'
import {
ensureSelectedRepoCollection,
selected_repo_collection,
selected_repo_event,
} from '$lib/stores/repo'
import {
ensureProposalFull,
selected_proposal_full,
@ -23,13 +27,15 @@ @@ -23,13 +27,15 @@
let repo_id = data.repo_id
let proposal_id = data.proposal_id
ensureSelectedRepo(repo_id)
ensureSelectedRepoCollection(repo_id)
ensureProposalFull(repo_id, proposal_id)
let repo_error = false
let proposal_error = false
$: {
repo_error = !$selected_repo.loading && $selected_repo.name.length === 0
repo_error =
!$selected_repo_collection.loading &&
$selected_repo_event.name.length === 0
proposal_error =
!$selected_proposal_full.summary.loading &&
$selected_proposal_full.summary.created_at === 0
@ -37,7 +43,7 @@ @@ -37,7 +43,7 @@
</script>
{#if !repo_error}
<RepoHeader {...$selected_repo} />
<RepoHeader {...$selected_repo_event} />
{/if}
{#if proposal_error}

121
src/routes/repo/[repo_id]/proposal/[proposal_id]/page.svelte

@ -1,121 +0,0 @@ @@ -1,121 +0,0 @@
<script lang="ts">
import { ensureSelectedRepo, selected_repo } from '$lib/stores/repo'
import {
ensureProposalFull,
selected_proposal_full,
selected_proposal_replies,
} from '$lib/stores/Proposal'
import ProposalHeader from '$lib/components/proposals/ProposalHeader.svelte'
import RepoHeader from '$lib/components/repo/RepoHeader.svelte'
import Thread from '$lib/wrappers/Thread.svelte'
import ProposalDetails from '$lib/components/proposals/ProposalDetails.svelte'
import Container from '$lib/components/Container.svelte'
import ParsedContent from '$lib/components/events/content/ParsedContent.svelte'
import Compose from '$lib/wrappers/Compose.svelte'
import { patch_kind } from '$lib/kinds'
import Patch from '$lib/components/events/content/Patch.svelte'
export let data: {
repo_id: string
proposal_id: string
}
let repo_id = data.repo_id
let proposal_id = data.proposal_id
ensureSelectedRepo(repo_id)
ensureProposalFull(repo_id, proposal_id)
let repo_error = false
let proposal_error = false
$: {
repo_error = !$selected_repo.loading && $selected_repo.name.length === 0
proposal_error =
!$selected_proposal_full.summary.loading &&
$selected_proposal_full.summary.created_at === 0
}
</script>
{#if !repo_error}
<RepoHeader {...$selected_repo} />
{/if}
{#if proposal_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 Proposal {repo_error ? 'or repo ' : ''}event</span
>
</div>
</Container>
{:else}
<ProposalHeader {...$selected_proposal_full.summary} />
<Container>
<div class="md:flex">
<div class="md:mr-2 md:w-2/3">
<div role="alert" class="alert mt-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-info"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path></svg
>
<div>
<h3 class="prose mb-2 text-sm font-bold">
view proposal in local git repository
</h3>
<p class="prose text-xs">
<a href="/ngit">install ngit</a>, run
<span class="rounded bg-neutral p-1 font-mono"
><span class="py-3">ngit list</span></span
> from the local repository and select the proposal title
</p>
</div>
</div>
<div class="prose my-3">
{#if $selected_proposal_full.proposal_event && $selected_proposal_full.proposal_event.kind === patch_kind}
<Patch
content={$selected_proposal_full.proposal_event.content}
tags={$selected_proposal_full.proposal_event.tags}
/>
{:else}
<ParsedContent
content={$selected_proposal_full.summary.descritpion}
/>
{/if}
</div>
{#each $selected_proposal_replies as event}
<Thread {event} replies={[]} />
{/each}
<div class="my-3">
<Compose />
</div>
</div>
<div class="prose ml-2 hidden w-1/3 md:flex">
<ProposalDetails
summary={$selected_proposal_full.summary}
labels={$selected_proposal_full.labels}
loading={$selected_proposal_full.loading}
/>
</div>
</div>
</Container>
{/if}
Loading…
Cancel
Save