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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
import { NDKEvent, NDKRelaySet, NDKSubscription } from '@nostr-dev-kit/ndk' |
|
||||||
import { writable, type Unsubscriber, type Writable } from 'svelte/store' |
import { writable, type Unsubscriber, type Writable } from 'svelte/store' |
||||||
import { base_relays, ndk } from './ndk' |
import type { RepoCollection, RepoEvent } from '$lib/components/repo/type' |
||||||
import type { Repo } from '$lib/components/repo/type' |
import { collection_defaults, event_defaults } from '$lib/components/repo/type' |
||||||
import { defaults } from '$lib/components/repo/type' |
import { ensureRepoCollection } from './repos' |
||||||
import type { User } from '$lib/components/users/type' |
import { selectRepoFromCollection } from '$lib/components/repo/utils' |
||||||
import { ensureUser } from './users' |
|
||||||
import { repo_kind } from '$lib/kinds' |
|
||||||
|
|
||||||
export const selected_repo: Writable<Repo> = writable({ ...defaults }) |
export const selected_repo_collection: Writable<RepoCollection> = writable({ |
||||||
let selected_repo_id: string = '' |
...collection_defaults, |
||||||
|
}) |
||||||
|
|
||||||
let maintainers_unsubscribers: Unsubscriber[] = [] |
export const selected_repo_event: Writable<RepoEvent> = writable({ |
||||||
|
...event_defaults, |
||||||
|
}) |
||||||
|
|
||||||
let sub: NDKSubscription |
selected_repo_collection.subscribe((collection) => { |
||||||
export const ensureSelectedRepo = async (repo_id: string): Promise<Repo> => { |
const selected_from_collection = selectRepoFromCollection(collection) |
||||||
if (selected_repo_id == repo_id) { |
if (selected_from_collection) |
||||||
return new Promise((r) => { |
selected_repo_event.set({ ...selected_from_collection }) |
||||||
const unsubscriber = selected_repo.subscribe((repo) => { |
}) |
||||||
if (repo.repo_id === repo_id && !repo.loading) { |
|
||||||
setTimeout(() => { |
|
||||||
unsubscriber() |
|
||||||
}, 5) |
|
||||||
r({ ...repo }) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
selected_repo_id = repo_id |
|
||||||
|
|
||||||
if (sub) sub.stop() |
let selected_repo_unique_commit_or_identifier: string = '' |
||||||
sub = ndk.subscribe( |
|
||||||
{ |
|
||||||
kinds: [repo_kind], |
|
||||||
'#d': [repo_id], |
|
||||||
limit: 1, |
|
||||||
}, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(base_relays, ndk) |
|
||||||
) |
|
||||||
|
|
||||||
return new Promise((r) => { |
let selected_unsubscriber: Unsubscriber |
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
try { |
export const ensureSelectedRepoCollection = ( |
||||||
if (event.kind == repo_kind && event.tagValue('d') == repo_id) { |
unique_commit_or_identifier: string |
||||||
const maintainers = [ |
): Writable<RepoCollection> => { |
||||||
{ |
if ( |
||||||
hexpubkey: event.pubkey, |
selected_repo_unique_commit_or_identifier !== unique_commit_or_identifier |
||||||
loading: true, |
) { |
||||||
npub: '', |
selected_repo_unique_commit_or_identifier = unique_commit_or_identifier |
||||||
} as User, |
if (selected_unsubscriber) selected_unsubscriber() |
||||||
] |
selected_unsubscriber = ensureRepoCollection( |
||||||
event.getMatchingTags('maintainers').forEach((t: string[]) => { |
unique_commit_or_identifier |
||||||
t.forEach((v, i) => { |
).subscribe((repo_collection) => { |
||||||
if (i > 0 && v !== maintainers[0].hexpubkey) { |
selected_repo_collection.set({ ...repo_collection }) |
||||||
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 } |
|
||||||
}), |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
old_unsubscribers.forEach((unsubscriber) => unsubscriber()) |
|
||||||
} |
|
||||||
} catch {} |
|
||||||
}) |
}) |
||||||
|
} |
||||||
|
return selected_repo_collection |
||||||
|
} |
||||||
|
|
||||||
sub.on('eose', () => { |
export const awaitSelectedRepoCollection = async ( |
||||||
selected_repo.update((repo) => { |
unique_commit_or_identifier: string |
||||||
r({ |
): Promise<RepoCollection> => { |
||||||
...repo, |
return new Promise((r) => { |
||||||
loading: false, |
const unsubscriber = ensureSelectedRepoCollection( |
||||||
}) |
unique_commit_or_identifier |
||||||
return { |
).subscribe((repo_collection) => { |
||||||
...repo, |
if ( |
||||||
loading: false, |
selected_repo_unique_commit_or_identifier == |
||||||
} |
unique_commit_or_identifier && |
||||||
}) |
!repo_collection.loading |
||||||
|
) { |
||||||
|
setTimeout(() => { |
||||||
|
unsubscriber() |
||||||
|
}, 5) |
||||||
|
r({ ...repo_collection }) |
||||||
|
} |
||||||
}) |
}) |
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|||||||
@ -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 @@ |
|||||||
<script lang="ts"> |
<script lang="ts"> |
||||||
import RepoDetails from '$lib/components/repo/RepoDetails.svelte' |
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 = '' |
export let repo_id = '' |
||||||
|
|
||||||
ensureSelectedRepo(repo_id) |
ensureSelectedRepoCollection(repo_id) |
||||||
</script> |
</script> |
||||||
|
|
||||||
<RepoDetails {...$selected_repo} /> |
<RepoDetails {...$selected_repo_event} /> |
||||||
|
|||||||
@ -1,114 +1,85 @@ |
|||||||
<script lang="ts"> |
<script lang="ts"> |
||||||
import ReposSummaryList from '$lib/components/ReposSummaryList.svelte' |
import ReposSummaryList from '$lib/components/ReposSummaryList.svelte' |
||||||
import type { RepoSummary } from '$lib/components/repo/type' |
import type { RepoEvent, RepoSummary } from '$lib/components/repo/type' |
||||||
import type { User } from '$lib/components/users/type' |
|
||||||
import { repo_kind } from '$lib/kinds' |
import { repo_kind } from '$lib/kinds' |
||||||
import { ndk } from '$lib/stores/ndk' |
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 type { NDKEvent } from '@nostr-dev-kit/ndk' |
||||||
import { onDestroy } from 'svelte' |
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 loading: boolean = true |
||||||
let sub = ndk.subscribe({ |
let sub = ndk.subscribe({ |
||||||
kinds: [repo_kind], |
kinds: [repo_kind], |
||||||
limit, |
limit, |
||||||
}) |
}) |
||||||
let maintainers_unsubscribers: Unsubscriber[] = [] |
let events: RepoEvent[] = [] |
||||||
sub.on('event', (event: NDKEvent) => { |
sub.on('event', (event: NDKEvent) => { |
||||||
if (repos.length < limit) { |
let repo_event = eventToRepoEvent(event) |
||||||
try { |
if (repo_event) events.push(repo_event) |
||||||
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) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
// 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) |
|
||||||
) |
|
||||||
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 } |
|
||||||
}), |
|
||||||
] |
|
||||||
} |
|
||||||
// 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 } |
|
||||||
}), |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
) |
|
||||||
}) |
|
||||||
} |
|
||||||
} catch {} |
|
||||||
} else if (loading == true) loading = false |
|
||||||
}) |
}) |
||||||
sub.on('eose', () => { |
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) |
||||||
|
) |
||||||
|
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 } |
||||||
|
}), |
||||||
|
] |
||||||
|
} |
||||||
|
// if not duplicate - add summary |
||||||
|
else if (summary) return [...repos, summary] |
||||||
|
return [...repos] |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
if (loading == true) loading = false |
if (loading == true) loading = false |
||||||
}) |
}) |
||||||
|
|
||||||
onDestroy(() => { |
onDestroy(() => { |
||||||
maintainers_unsubscribers.forEach((unsubscriber) => unsubscriber()) |
|
||||||
sub.stop() |
sub.stop() |
||||||
}) |
}) |
||||||
</script> |
</script> |
||||||
|
|
||||||
<ReposSummaryList title="Latest Repositories" {repos} {loading} /> |
<ReposSummaryList title="Latest Repositories" repos={$repos} {loading} /> |
||||||
|
|||||||
@ -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