diff --git a/package-lock.json b/package-lock.json index 578f7d5..5d62248 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,11 @@ "@noble/hashes": "^1.4.0", "@poppanator/sveltekit-svg": "^4.2.1", "@types/throttle-debounce": "^5.0.2", - "@welshman/lib": "^0.0.13", - "@welshman/net": "^0.0.17", + "@welshman/lib": "^0.0.14", + "@welshman/net": "^0.0.18", "@welshman/signer": "^0.0.2", - "@welshman/store": "^0.0.1", - "@welshman/util": "^0.0.24", + "@welshman/store": "^0.0.2", + "@welshman/util": "^0.0.25", "daisyui": "^4.12.10", "nostr-tools": "^2.7.2", "prettier-plugin-tailwindcss": "^0.6.5", @@ -1372,9 +1372,9 @@ } }, "node_modules/@welshman/lib": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.13.tgz", - "integrity": "sha512-tp5+KAiUwoid04Pap47uqkz3MWDqA1iy+JklX7qu5WppClehMgkYfePtCbKOQ8LS9psl88xnZM1oR8NtDhVqnA==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.14.tgz", + "integrity": "sha512-q5sWp3psLcouajdP97PZs2D44WoMZ0cwwT7EWUdShIPgisRJq9hjuMLSkUFqdgfJ3qBn72SSjcMMfDnkcQja4A==", "dependencies": { "@scure/base": "^1.1.6", "@types/events": "^3.0.3", @@ -1384,12 +1384,12 @@ } }, "node_modules/@welshman/net": { - "version": "0.0.17", - "resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.17.tgz", - "integrity": "sha512-m2hvpb3AdHPmmhtfc16oy03523TG+6ggM9bt7vOLfKZl67qg9ki5VHFRxd7VPRuKosvNabeIErWBsXIxtKWdJg==", + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.18.tgz", + "integrity": "sha512-5a6iKetUSDQisiG7/Cr61x9oSLhLNifqN/qzdOKW8Mu8ZqkOFYMIFPonLKT3UdixByvXn+RH5ka3b/nB77bBIA==", "dependencies": { - "@welshman/lib": "0.0.13", - "@welshman/util": "0.0.24", + "@welshman/lib": "0.0.14", + "@welshman/util": "0.0.25", "isomorphic-ws": "^5.0.0", "ws": "^8.16.0" } @@ -1404,15 +1404,30 @@ "@welshman/util": "^0.0.24" } }, - "node_modules/@welshman/store": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.1.tgz", - "integrity": "sha512-vpnbJOF8zoneTcLOr5iZuRETVwb67mUyLiWmGsfs8yoMI7lpxJkg0QKieVkp2FpVYoDYbb98eQEuYOiHdsf7RQ==", + "node_modules/@welshman/signer/node_modules/@welshman/lib": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.13.tgz", + "integrity": "sha512-tp5+KAiUwoid04Pap47uqkz3MWDqA1iy+JklX7qu5WppClehMgkYfePtCbKOQ8LS9psl88xnZM1oR8NtDhVqnA==", "dependencies": { - "svelte": "^4.2.18" + "@scure/base": "^1.1.6", + "@types/events": "^3.0.3", + "@types/throttle-debounce": "^5.0.2", + "events": "^3.3.0", + "throttle-debounce": "^5.0.0" } }, - "node_modules/@welshman/util": { + "node_modules/@welshman/signer/node_modules/@welshman/net": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.17.tgz", + "integrity": "sha512-m2hvpb3AdHPmmhtfc16oy03523TG+6ggM9bt7vOLfKZl67qg9ki5VHFRxd7VPRuKosvNabeIErWBsXIxtKWdJg==", + "dependencies": { + "@welshman/lib": "0.0.13", + "@welshman/util": "0.0.24", + "isomorphic-ws": "^5.0.0", + "ws": "^8.16.0" + } + }, + "node_modules/@welshman/signer/node_modules/@welshman/util": { "version": "0.0.24", "resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.24.tgz", "integrity": "sha512-Qpe0J5VCYpqhpGB4f966d4FAw65L3JE2xYSqhBGbbfdMXnteqW6nHClP0YVbX9pMSYGRPn3ZXWkWf33yqImbGQ==", @@ -1421,6 +1436,23 @@ "nostr-tools": "^2.3.2" } }, + "node_modules/@welshman/store": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.2.tgz", + "integrity": "sha512-VjyXFP6KILCldtHykTAwdCHhAQVNjpDy2lZrXho1YNDMBxuuskXtmj3AzS6iJuq6zZOKWN+iEDOzjqxbI5raFA==", + "dependencies": { + "svelte": "^4.2.18" + } + }, + "node_modules/@welshman/util": { + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.25.tgz", + "integrity": "sha512-Y0n7YKIyJmhqhUvi9iOTg21PnMPFgEJpoAfaQ1IypKfjUjLMfBe3blKQ/elO/W+nquZLHIFuSY9hNMLpXSnZ+Q==", + "dependencies": { + "@welshman/lib": "0.0.14", + "nostr-tools": "^2.3.2" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", diff --git a/package.json b/package.json index a3cac12..89ce41f 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,12 @@ "@noble/hashes": "^1.4.0", "@poppanator/sveltekit-svg": "^4.2.1", "@types/throttle-debounce": "^5.0.2", - "@welshman/lib": "^0.0.13", - "@welshman/net": "^0.0.17", + "@welshman/lib": "^0.0.14", + "@welshman/net": "^0.0.18", "@welshman/signer": "^0.0.2", - "@welshman/store": "^0.0.1", - "@welshman/util": "^0.0.24", + "@welshman/store": "^0.0.2", + "@welshman/util": "^0.0.25", + "@welshman/domain": "^0.0.1", "daisyui": "^4.12.10", "nostr-tools": "^2.7.2", "prettier-plugin-tailwindcss": "^0.6.5", diff --git a/src/app/base.ts b/src/app/base.ts index 81dab28..5b4c9c8 100644 --- a/src/app/base.ts +++ b/src/app/base.ts @@ -1,6 +1,6 @@ import {derived} from "svelte/store" -import {memoize} from '@welshman/lib' -import type {SignedEvent} from "@welshman/util" +import {memoize, assoc} from '@welshman/lib' +import type {CustomEvent} from '@welshman/util' import {Repository, createEvent, Relay} from "@welshman/util" import {getter} from "@welshman/store" import {NetworkContext, Tracker} from "@welshman/net" @@ -23,17 +23,24 @@ export const pk = synced('pk', null) export const sessions = synced>('sessions', {}) +export const getSessions = getter(sessions) + export const session = derived([pk, sessions], ([$pk, $sessions]) => $pk ? $sessions[$pk] : null) export const getSession = getter(session) +export const addSession = (session: Session) => { + sessions.update(assoc(session.pubkey, session)) + pk.set(session.pubkey) +} + export const makeSigner = memoize((session: Session) => { switch (session?.method) { case "extension": return new Nip07Signer() case "privkey": return new Nip01Signer(session.secret!) - case "connect": + case "nip46": return new Nip46Signer(Nip46Broker.get(session.pubkey, session.secret!, session.handler!)) default: return null @@ -47,8 +54,8 @@ export const getSigner = getter(signer) const seenChallenges = new Set() Object.assign(NetworkContext, { - onEvent: (url: string, event: SignedEvent) => tracker.track(event.id, url), - isDeleted: (url: string, event: SignedEvent) => repository.isDeleted(event), + onEvent: (url: string, event: CustomEvent) => tracker.track(event.id, url), + isDeleted: (url: string, event: CustomEvent) => repository.isDeleted(event), onAuth: async (url: string, challenge: string) => { if (seenChallenges.has(challenge)) { return diff --git a/src/app/commands.ts b/src/app/commands.ts deleted file mode 100644 index c24543c..0000000 --- a/src/app/commands.ts +++ /dev/null @@ -1,175 +0,0 @@ -import {get} from 'svelte/store' -import type {SignedEvent} from '@welshman/util' -import {batcher, uniq, now, postJson, assoc} from "@welshman/lib" -import {normalizeRelayUrl, PROFILE, FOLLOWS, MUTES, GROUP_META} from "@welshman/util" -import {subscribe} from "@welshman/net" -import type {RelayInfo, HandleInfo, Session} from "@app/types" -import {splitGroupId} from "@app/domain" -import {DUFFLEPUD_URL, INDEXER_RELAYS, repository, pk, sessions} from "@app/base" -import {relayInfo, handleInfo, groupsById, profilesByPubkey, mutesByPubkey} from "@app/state" - -// Session - -export const addSession = (session: Session) => { - sessions.update(assoc(session.pubkey, session)) - pk.set(session.pubkey) -} - -// Handle info - -export const loadHandleInfo = batcher(800, async (handles: string[]) => { - const res = await postJson(`${DUFFLEPUD_URL}/handle/info`, {handles: uniq(handles)}) - const data: {handle: string, info: HandleInfo}[] = res?.data || [] - - handleInfo.update($handleInfo => { - for (const {handle, info} of data) { - $handleInfo.set(handle, {...info, fetched_at: now()}) - } - - return $handleInfo - }) - - return data.map(item => item.info) -}) - -export const getHandleInfo = (handle: string) => { - const info = get(handleInfo).get(handle) - - if (info?.fetched_at > now() - 3600) { - return info - } - - return loadHandleInfo(handle) -} - -// Relay info - -export const loadRelayInfo = batcher(800, async (urls: string[]) => { - const res = await postJson(`${DUFFLEPUD_URL}/relay/info`, {urls: uniq(urls)}) - const data: {url: string, info: RelayInfo}[] = res?.data || [] - - relayInfo.update($relayInfo => { - for (const {url, info} of data) { - $relayInfo.set(normalizeRelayUrl(url), {...info, fetched_at: now()}) - } - - return $relayInfo - }) - - return data.map(item => item.info) -}) - -export const getRelayInfo = (url: string) => { - const info = get(relayInfo).get(url) - - if (info?.fetched_at > now() - 3600) { - return info - } - - return loadRelayInfo(url) -} - -// Group meta - -export const getGroup = (groupId: string) => { - const group = get(groupsById).get(groupId) - - if (group?.event.fetched_at > now() - 3600) { - return group - } - - const [url, nom] = splitGroupId(groupId) - - const sub = subscribe({ - relays: [url], - filters: [{kinds: [GROUP_META], '#d': [groupId]}], - closeOnEose: true, - }) - - sub.emitter.on('event', (url: string, e: SignedEvent) => { - e.fetched_at = now() - repository.publish(e) - console.log(e) - }) -} - -// Profile - -export const getProfile = (pubkey: string, relays = []) => { - const profile = get(profilesByPubkey).get(pubkey) - - if (profile?.event.fetched_at > now() - 3600) { - return profile - } - - return new Promise(resolve => { - const sub = subscribe({ - relays: [...relays, ...INDEXER_RELAYS], - filters: [{kinds: [PROFILE], authors: [pubkey]}], - closeOnEose: true, - }) - - sub.emitter.on('event', (url: string, e: SignedEvent) => { - e.fetched_at = now() - repository.publish(e) - console.log(e) - resolve(e) - }) - - sub.emitter.on('close', () => resolve(null)) - }) -} - -// Follows - -export const getFollows = (pubkey: string, relays = []) => { - const follows = get(followsByPubkey).get(pubkey) - - if (follows?.event.fetched_at > now() - 3600) { - return follows - } - - return new Promise(resolve => { - const sub = subscribe({ - relays: [...relays, ...INDEXER_RELAYS], - filters: [{kinds: [FOLLOWS], authors: [pubkey]}], - closeOnEose: true, - }) - - sub.emitter.on('event', (url: string, e: SignedEvent) => { - e.fetched_at = now() - repository.publish(e) - console.log(e) - resolve(e) - }) - - sub.emitter.on('close', () => resolve(null)) - }) -} - -// Mutes - -export const getMutes = (pubkey: string, relays = []) => { - const mutes = get(mutesByPubkey).get(pubkey) - - if (mutes?.event.fetched_at > now() - 3600) { - return mutes - } - - return new Promise(resolve => { - const sub = subscribe({ - relays: [...relays, ...INDEXER_RELAYS], - filters: [{kinds: [MUTES], authors: [pubkey]}], - closeOnEose: true, - }) - - sub.emitter.on('event', (url: string, e: SignedEvent) => { - e.fetched_at = now() - repository.publish(e) - console.log(e) - resolve(e) - }) - - sub.emitter.on('close', () => resolve(null)) - }) -} diff --git a/src/app/components/LogIn.svelte b/src/app/components/LogIn.svelte index b60340e..9419ba5 100644 --- a/src/app/components/LogIn.svelte +++ b/src/app/components/LogIn.svelte @@ -8,13 +8,14 @@ import InfoNostr from '@app/components/LogIn.svelte' import {pushModal, clearModal} from '@app/modal' import {pushToast} from '@app/toast' - import {getProfile, getFollows, getMutes, getHandleInfo, addSession} from '@app/commands' + import {addSession} from '@app/base' + import {loadProfile, loadFollows, loadMutes, loadHandle} from '@app/state' const back = () => history.back() const tryLogin = async () => { const secret = makeSecret() - const handle = await getHandleInfo(`${username}@${handler.domain}`) + const handle = await loadHandle(`${username}@${handler.domain}`) if (!handle?.pubkey) { return pushToast({ @@ -26,9 +27,9 @@ const {pubkey, relays = []} = handle const broker = Nip46Broker.get(pubkey, secret, handler) const [profile, success] = await Promise.all([ - getProfile(pubkey, relays), - getFollows(pubkey, relays), - getMutes(pubkey, relays), + loadProfile(pubkey, relays), + loadFollows(pubkey, relays), + loadMutes(pubkey, relays), broker.connect(), ]) diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte index 287a0cb..9c5e1ac 100644 --- a/src/app/components/PrimaryNav.svelte +++ b/src/app/components/PrimaryNav.svelte @@ -10,16 +10,16 @@ import Icon from "@lib/components/Icon.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import SpaceAdd from '@app/components/SpaceAdd.svelte' - import {makeGroupId} from "@app/domain" import {session} from "@app/base" - import {userGroupRelaysByNom, groupsById, deriveProfile} from "@app/state" + import {deriveGroupMembership, makeGroupId, getGroup, deriveProfile} from "@app/state" import {pushModal} from "@app/modal" export const addSpace = () => pushModal(SpaceAdd) export const browseSpaces = () => goto("/browse") - const profile = deriveProfile($session?.pubkey) + $: profile = deriveProfile($session?.pubkey) + $: membership = deriveGroupMembership($session?.pubkey)
@@ -27,17 +27,24 @@
-
- +
+ {#if $profile?.picture} + + {:else} + + {/if}
- {#each $userGroupRelaysByNom.entries() as [nom, relays] (nom)} - {@const group = $groupsById.get(makeGroupId(relays[0], nom))} - -
- {group.name} -
-
+ {#each Array.from($membership?.ids || []) as groupId (groupId)} + {#await getGroup(groupId)} + + {:then group} + +
+ {group?.name} +
+
+ {/await} {/each}
diff --git a/src/app/components/SpaceJoin.svelte b/src/app/components/SpaceJoin.svelte index d69fb43..f10925d 100644 --- a/src/app/components/SpaceJoin.svelte +++ b/src/app/components/SpaceJoin.svelte @@ -5,11 +5,9 @@ import Field from '@lib/components/Field.svelte' import Icon from '@lib/components/Icon.svelte' import SpaceCreateFinish from '@app/components/SpaceCreateFinish.svelte' - import {splitGroupId, GROUP_DELIMITER} from '@app/domain' - import {getRelayInfo, getGroup} from '@app/commands' import {pushModal} from '@app/modal' import {pushToast} from '@app/toast' - import {relayInfo} from '@app/state' + import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from '@app/state' const back = () => history.back() @@ -18,7 +16,7 @@ const tryJoin = async () => { const [url, nom] = splitGroupId(id) - const info = await getRelayInfo(url) + const info = await loadRelay(url) if (!info) { return pushToast({ @@ -34,8 +32,7 @@ }) } - const group = await getGroup(id) - console.log(info, group) + const group = await loadGroup(id) } const join = async () => { diff --git a/src/app/domain.ts b/src/app/domain.ts deleted file mode 100644 index 63eb959..0000000 --- a/src/app/domain.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {stripProtocol} from "@welshman/lib" -import type {TrustedEvent} from "@welshman/util" -import {getIdentifier, normalizeRelayUrl} from "@welshman/util" - -export const GROUP_DELIMITER = `'` - -export const makeGroupId = (url: string, nom: string) => - [stripProtocol(url), nom].join(GROUP_DELIMITER) - -export const splitGroupId = (groupId: string) => { - const [url, nom] = groupId.split(GROUP_DELIMITER) - - return [normalizeRelayUrl(url), nom] -} - -export const getGroupNom = (e: TrustedEvent) => getIdentifier(e)?.split(GROUP_DELIMITER)[1] - -export const getGroupUrl = (e: TrustedEvent) => { - const id = getIdentifier(e) - const url = id?.split(GROUP_DELIMITER)[0] - - return url ? normalizeRelayUrl(url) : null -} - -export const getGroupName = (e: TrustedEvent | undefined) => e?.tags.find(t => t[0] === "name")?.[1] - -export const getGroupPicture = (e: TrustedEvent | undefined) => - e?.tags.find(t => t[0] === "picture")?.[1] diff --git a/src/app/state.ts b/src/app/state.ts index e10998e..8b2c482 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,83 +1,352 @@ -import {writable, derived} from "svelte/store" -import {pushToMapKey, indexBy} from "@welshman/lib" -import {getIdentifier, getPubkeyTagValues, GROUP_META, PROFILE, FOLLOWS, MUTES, GROUPS, getGroupTagValues} from "@welshman/util" -import {deriveEvents} from "@welshman/store" +import type {Readable} from "svelte/store" +import {writable, readable, derived} from "svelte/store" +import {uniq, pushToMapKey, nthEq, batcher, postJson, stripProtocol, assoc, indexBy, now} from "@welshman/lib" +import {getIdentifier, normalizeRelayUrl, getPubkeyTagValues, GROUP_META, PROFILE, FOLLOWS, MUTES, GROUPS, getGroupTagValues} from "@welshman/util" +import type {Filter, SignedEvent, CustomEvent} from '@welshman/util' +import {subscribe} from '@welshman/net' +import {readProfile, readList, asDecryptedEvent} from '@welshman/domain' +import type {PublishedProfile, PublishedList} from '@welshman/domain' +import {decrypt} from '@welshman/signer' +import {deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store" import {synced, parseJson} from '@lib/util' -import type {Session} from '@app/types' -import {repository, pk} from "@app/base" -import {getGroupNom, getGroupUrl, getGroupName, getGroupPicture, GROUP_DELIMITER} from "@app/domain" +import type {Session, Handle, Relay} from '@app/types' +import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSessions, makeSigner} from "@app/base" -export const relayInfo = writable(new Map()) +// Utils -export const handleInfo = writable(new Map()) +export const createCollection = ({ + store, + getKey, + isStale, + loadItem, +}: { + store: Readable, + getKey: (item: T) => string, + isStale: (item: T) => boolean, + loadItem: (key: string, ...args: any) => Promise +}) => { + const indexStore = derived(store, $items => indexBy(getKey, $items)) + const getIndex = getter(indexStore) -export const profileEvents = deriveEvents(repository, { - filters: [{kinds: [PROFILE]}], -}) + const getItem = async (key: string, ...args: any[]) => { + const item = getIndex().get(key) -export const profiles = derived(profileEvents, $profileEvents => - $profileEvents.map(event => ({...parseJson(event.content), event})) -) + if (item && isStale(item)) { + return item + } -export const profilesByPubkey = derived(profiles, $profiles => indexBy(profile => profile.event.pubkey, $profiles)) + await loadItem(key, ...args) -export const deriveProfile = (pubkey: string) => derived(profilesByPubkey, $m => $m.get(pubkey)) - -export const followEvents = deriveEvents(repository, { - filters: [{kinds: [FOLLOWS]}], -}) - -export const follows = derived(followEvents, $followEvents => - $followEvents.map(event => ({pubkeys: new Set(getPubkeyTagValues(event.tags)), event})) -) - -export const followsByPubkey = derived(follows, $follows => indexBy(follow => follow.event.pubkey, $follows)) - -export const muteEvents = deriveEvents(repository, { - filters: [{kinds: [MUTES]}], -}) - -export const mutes = derived(muteEvents, $muteEvents => - $muteEvents.map(event => ({pubkeys: new Set(getPubkeyTagValues(event.tags)), event})) -) - -export const mutesByPubkey = derived(mutes, $mutes => indexBy(mute => mute.event.pubkey, $mutes)) - -export const groupEvents = deriveEvents(repository, { - filters: [{kinds: [GROUP_META]}], -}) - -export const groups = derived([relayInfo, groupEvents], ([$relayInfo, $groupEvents]) => - $groupEvents - .map(event => ({ - event, - id: getIdentifier(event), - nom: getGroupNom(event), - url: getGroupUrl(event), - name: getGroupName(event), - picture: getGroupPicture(event), - })) - .filter(group => $relayInfo.get(group.url)?.pubkey === group.event.pubkey) -) - -export const groupsById = derived(groups, $groups => indexBy(group => group.id, $groups)) - -export const groupsEvents = deriveEvents(repository, { - filters: [{kinds: [GROUPS]}], -}) - -export const userGroupsEvent = derived([pk, groupsEvents], ([$pk, $groupsEvents]) => - $groupsEvents.find(e => e.pubkey === $pk), -) - -export const userGroupRelaysByNom = derived(userGroupsEvent, $userGroupsEvent => { - const relaysByNom = new Map() - - for (const id of getGroupTagValues($userGroupsEvent?.tags || [])) { - const [relay, nom] = id.split(GROUP_DELIMITER) - - pushToMapKey(relaysByNom, nom, relay) + return getIndex().get(key) } - return relaysByNom + const deriveItem = (key: string | undefined, ...args: any[]) => { + if (!key) { + return readable(undefined) + } + + // If we don't yet have the item, or it's stale, trigger a request for it. The derived + // store will update when it arrives + loadItem(key, ...args) + + return derived(indexStore, $index => $index.get(key)) + } + + return {indexStore, getIndex, deriveItem, loadItem, getItem} +} + +export const load = ({relays, filters}: {relays: string[], filters: Filter[]}) => + new Promise(resolve => { + const sub = subscribe({relays, filters, closeOnEose: true}) + + sub.emitter.on('event', (url: string, event: SignedEvent) => { + const e: CustomEvent = {...event, fetched_at: now()} + + repository.publish(e) + resolve(e) + }) + + sub.emitter.on('close', () => resolve(undefined)) + }) + +// Plaintext + +export const plaintext = withGetter(writable>({})) + +export const getPlaintext = (e: CustomEvent) => plaintext.get()[e.id] + +export const setPlaintext = (e: CustomEvent, content: string) => + plaintext.update(assoc(e.id, content)) + +export const ensurePlaintext = async (e: CustomEvent) => { + if (!getPlaintext(e)) { + const sessions = getSessions() + const session = sessions[e.pubkey] + const signer = makeSigner(session) + + if (signer) { + setPlaintext(e, await decrypt(signer, e.pubkey, e.content)) + } + } + + return getPlaintext(e) +} + +// Relay info + +export const relays = writable([]) + +export const { + indexStore: relaysByUrl, + getIndex: getRelaysByUrl, + deriveItem: deriveRelay, + loadItem: loadRelay, + getItem: getRelay, +} = createCollection({ + store: relays, + getKey: (relay: Relay) => relay.url, + isStale: (relay: Relay) => relay.fetched_at < now() - 3600, + loadItem: batcher(800, async (urls: string[]) => { + const urlSet = new Set(urls) + const res = await postJson(`${DUFFLEPUD_URL}/relay/info`, {urls: Array.from(urlSet)}) + const items: Relay[] = (res?.data || []).map(({url, info}: any) => ({...info, url, fetched_at: now()})) + + relays.update($relays => [ + ...$relays.filter($relay => !urlSet.has($relay.url)), + ...items, + ]) + + return items + }), +}) + +// Handles + +export const handles = writable([]) + +export const { + indexStore: handlesByPubkey, + getIndex: getHandlesByPubkey, + deriveItem: deriveHandle, + loadItem: loadHandle, + getItem: getHandle, +} = createCollection({ + store: handles, + getKey: (handle: Handle) => handle.pubkey, + isStale: (handle: Handle) => handle.fetched_at < now() - 3600, + loadItem: batcher(800, async (nip05s: string[]) => { + const nip05Set = new Set(nip05s) + const res = await postJson(`${DUFFLEPUD_URL}/handle/info`, {handles: Array.from(nip05Set)}) + const items: Handle[] = (res?.data || []).map(({handle: nip05, info}: any) => ({...info, nip05, fetched_at: now()})) + + handles.update($handles => [ + ...$handles.filter($handle => !nip05Set.has($handle.nip05)), + ...items, + ]) + + return items + }), +}) + +// Profiles + +export const profiles = deriveEventsMapped({ + repository, + filters: [{kinds: [PROFILE]}], + eventToItem: readProfile, + itemToEvent: item => item.event, +}) + +export const { + indexStore: profilesByPubkey, + getIndex: getProfilesByPubkey, + deriveItem: deriveProfile, + loadItem: loadProfile, + getItem: getProfile, +} = createCollection({ + store: profiles, + getKey: profile => profile.event.pubkey, + isStale: (profile: PublishedProfile) => profile.event.fetched_at < now() - 3600, + loadItem: (pubkey: string, relays = []) => + load({ + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [PROFILE], authors: [pubkey]}], + }), +}) + +// Follows + +export const follows = deriveEventsMapped({ + repository, + filters: [{kinds: [FOLLOWS]}], + itemToEvent: item => item.event, + eventToItem: async (event: CustomEvent) => + readList( + asDecryptedEvent(event, { + content: await ensurePlaintext(event), + }), + ), +}) + +export const { + indexStore: followsByPubkey, + getIndex: getFollowsByPubkey, + deriveItem: deriveFollows, + loadItem: loadFollows, + getItem: getFollows, +} = createCollection({ + store: follows, + getKey: follows => follows.event.pubkey, + isStale: (follows: PublishedList) => follows.event.fetched_at < now() - 3600, + loadItem: (pubkey: string, relays = []) => + load({ + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [FOLLOWS], authors: [pubkey]}], + }) +}) + +// Mutes + +export const mutes = deriveEventsMapped({ + repository, + filters: [{kinds: [MUTES]}], + itemToEvent: item => item.event, + eventToItem: async (event: CustomEvent) => + readList( + asDecryptedEvent(event, { + content: await ensurePlaintext(event), + }), + ), +}) + +export const { + indexStore: mutesByPubkey, + getIndex: getMutesByPubkey, + deriveItem: deriveMutes, + loadItem: loadMutes, + getItem: getMutes, +} = createCollection({ + store: mutes, + getKey: mute => mute.event.pubkey, + isStale: (mutes: PublishedList) => mutes.event.fetched_at < now() - 3600, + loadItem: (pubkey: string, relays = []) => + load({ + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [MUTES], authors: [pubkey]}], + }) +}) + +// Groups + +export const GROUP_DELIMITER = `'` + +export const makeGroupId = (url: string, nom: string) => + [stripProtocol(url), nom].join(GROUP_DELIMITER) + +export const splitGroupId = (groupId: string) => { + const [url, nom] = groupId.split(GROUP_DELIMITER) + + return [normalizeRelayUrl(url), nom] +} + +export const getGroupUrl = (groupId: string) => splitGroupId(groupId)[0] + +export const getGroupNom = (groupId: string) => splitGroupId(groupId)[1] + +export const getGroupName = (e?: CustomEvent) => e?.tags.find(nthEq(0, "name"))?.[1] + +export const getGroupPicture = (e?: CustomEvent) => e?.tags.find(nthEq(0, "picture"))?.[1] + +export type Group = { + id: string, + nom: string, + url: string, + name?: string, + picture?: string, + event?: CustomEvent +} + +export type PublishedGroup = Omit & { + event: CustomEvent +} + +export const readGroup = (event: CustomEvent) => { + const id = getIdentifier(event)! + const [url, nom] = id.split(GROUP_DELIMITER) + const name = event?.tags.find(nthEq(0, "name"))?.[1] + const picture = event?.tags.find(nthEq(0, "picture"))?.[1] + + return {id, nom, url, name, picture, event} +} + +export const groups = deriveEventsMapped({ + repository, + filters: [{kinds: [GROUP_META]}], + eventToItem: readGroup, + itemToEvent: item => item.event, +}) + +export const validGroups = derived([relaysByUrl, groups], ([$relaysByUrl, $groups]) => + $groups.filter(group => $relaysByUrl.get(group.url)?.pubkey === group.event.pubkey) +) + +export const { + indexStore: groupsById, + getIndex: getGroupsById, + deriveItem: deriveGroup, + loadItem: loadGroup, + getItem: getGroup, +} = createCollection({ + store: validGroups, + getKey: (group: PublishedGroup) => group.id, + isStale: (group: PublishedGroup) => group.event.fetched_at < now() - 3600, + loadItem: (id: string) => { + const url = getGroupUrl(id) + + return Promise.all([ + loadRelay(url), + load({ + relays: [url], + filters: [{kinds: [GROUP_META], '#d': [id]}], + }), + ]) + } +}) + +// Group membership + +export type GroupMembership = { + ids: Set + event?: CustomEvent +} + +export type PublishedGroupMembership = Omit & { + event: CustomEvent +} + +export const readGroupMembership = (event: CustomEvent) => + ({event, ids: new Set(getGroupTagValues(event.tags))}) + +export const groupMemberships = deriveEventsMapped({ + repository, + filters: [{kinds: [GROUPS]}], + eventToItem: readGroupMembership, + itemToEvent: item => item.event, +}) + +export const { + indexStore: groupMembershipsByPubkey, + getIndex: getGroupMembersipsByPubkey, + deriveItem: deriveGroupMembership, + loadItem: loadGroupMembership, + getItem: getGroupMembership, +} = createCollection({ + store: groupMemberships, + getKey: groupMembership => groupMembership.event.pubkey, + isStale: (groupMembership: PublishedGroupMembership) => groupMembership.event.fetched_at < now() - 3600, + loadItem: (pubkey: string, relays = []) => + load({ + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [GROUPS], authors: [pubkey]}], + }) }) diff --git a/src/app/types.ts b/src/app/types.ts index 1958e9b..174bdb3 100644 --- a/src/app/types.ts +++ b/src/app/types.ts @@ -1,4 +1,7 @@ +import {verifiedSymbol} from 'nostr-tools' import type {Nip46Handler} from "@welshman/signer" +import type {SignedEvent, TrustedEvent} from "@welshman/util" +import type {RelayProfile, Handle as HandleInfo} from "@welshman/domain" export type Session = { method: string @@ -7,14 +10,16 @@ export type Session = { handler?: Nip46Handler } -export type RelayInfo = { - fetched_at: number +export type Relay = RelayProfile & { + fetched_at: number, } -export type HandleInfo = { - pubkey: string - nip05: string - nip46: string[] - relays: string[] - fetched_at: number +export type Handle = HandleInfo & { + fetched_at: number, +} + +declare module '@welshman/util' { + interface CustomEvent extends TrustedEvent { + fetched_at: number + } } diff --git a/src/routes/home/+page.svelte b/src/routes/home/+page.svelte index 7201483..9a87ceb 100644 --- a/src/routes/home/+page.svelte +++ b/src/routes/home/+page.svelte @@ -4,14 +4,14 @@
-
+

Welcome to

Flotilla

- + Invite all your friends, do life together. - + Find a community based on your hobbies or interests. diff --git a/vite.config.ts b/vite.config.ts index bf65d4c..185d642 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,9 @@ import svg from "@poppanator/sveltekit-svg" import {defineConfig} from "vite" export default defineConfig({ + server: { + port: 1847, + }, plugins: [ sveltekit(), svg({