Browse Source
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 eventsmaster
22 changed files with 769 additions and 402 deletions
@ -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) |
||||
}) |
||||
}) |
||||
}) |
||||
@ -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] |
||||
} |
||||
@ -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 }) |
||||
} |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
@ -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), |
||||
} |
||||
} |
||||
) |
||||
} |
||||
} |
||||
) |
||||
}) |
||||
} |
||||
@ -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} /> |
||||
|
||||
@ -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} /> |
||||
|
||||
@ -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…
Reference in new issue