mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 10:57:04 +00:00
Use new collection pattern
This commit is contained in:
68
package-lock.json
generated
68
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string | null>('pk', null)
|
||||
|
||||
export const sessions = synced<Record<string, Session>>('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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
@@ -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(),
|
||||
])
|
||||
|
||||
|
||||
@@ -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)
|
||||
</script>
|
||||
|
||||
<div class="relative w-14 bg-base-100">
|
||||
@@ -27,17 +27,24 @@
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
<div>
|
||||
<PrimaryNavItem title={$profile?.name}>
|
||||
<div class="w-10 rounded-full border border-solid border-base-300">
|
||||
<img alt="" src={$profile?.picture} />
|
||||
<div class="!flex w-10 items-center justify-center rounded-full border border-solid border-base-300">
|
||||
{#if $profile?.picture}
|
||||
<img alt="" src={$profile.picture} />
|
||||
{:else}
|
||||
<Icon icon="user-rounded" size={7} />
|
||||
{/if}
|
||||
</div>
|
||||
</PrimaryNavItem>
|
||||
{#each $userGroupRelaysByNom.entries() as [nom, relays] (nom)}
|
||||
{@const group = $groupsById.get(makeGroupId(relays[0], nom))}
|
||||
<PrimaryNavItem title={group.name}>
|
||||
<div class="w-10 rounded-full border border-solid border-base-300">
|
||||
<img alt={group.name} src={group.picture} />
|
||||
</div>
|
||||
</PrimaryNavItem>
|
||||
{#each Array.from($membership?.ids || []) as groupId (groupId)}
|
||||
{#await getGroup(groupId)}
|
||||
<!-- pass -->
|
||||
{:then group}
|
||||
<PrimaryNavItem title={group?.name}>
|
||||
<div class="w-10 rounded-full border border-solid border-base-300">
|
||||
<img alt={group?.name} src={group?.picture} />
|
||||
</div>
|
||||
</PrimaryNavItem>
|
||||
{/await}
|
||||
{/each}
|
||||
<PrimaryNavItem title="Add Space" on:click={addSpace}>
|
||||
<div class="!flex w-10 items-center justify-center">
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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]
|
||||
415
src/app/state.ts
415
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 = <T>({
|
||||
store,
|
||||
getKey,
|
||||
isStale,
|
||||
loadItem,
|
||||
}: {
|
||||
store: Readable<T[]>,
|
||||
getKey: (item: T) => string,
|
||||
isStale: (item: T) => boolean,
|
||||
loadItem: (key: string, ...args: any) => Promise<any>
|
||||
}) => {
|
||||
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<CustomEvent | undefined>(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<Record<string, string>>({}))
|
||||
|
||||
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<Relay[]>([])
|
||||
|
||||
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<Handle[]>([])
|
||||
|
||||
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<PublishedProfile>({
|
||||
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<PublishedList>({
|
||||
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<PublishedList>({
|
||||
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<Group, "event"> & {
|
||||
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<PublishedGroup>({
|
||||
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<string>
|
||||
event?: CustomEvent
|
||||
}
|
||||
|
||||
export type PublishedGroupMembership = Omit<GroupMembership, "event"> & {
|
||||
event: CustomEvent
|
||||
}
|
||||
|
||||
export const readGroupMembership = (event: CustomEvent) =>
|
||||
({event, ids: new Set(getGroupTagValues(event.tags))})
|
||||
|
||||
export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>({
|
||||
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]}],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content">
|
||||
<div class="flex max-w-xl flex-col gap-4">
|
||||
<div class="flex max-w-2xl flex-col gap-4">
|
||||
<h1 class="text-stark-content text-center text-5xl">Welcome to</h1>
|
||||
<h1 class="text-stark-content mb-4 text-center text-5xl font-bold uppercase">Flotilla</h1>
|
||||
<div class="grid lg:grid-cols-2 gap-3">
|
||||
<CardButton icon="add-circle" title="Create a group" class="h-24">
|
||||
<CardButton icon="add-circle" title="Create a space" class="h-24">
|
||||
Invite all your friends, do life together.
|
||||
</CardButton>
|
||||
<CardButton icon="compass" title="Discover groups" class="h-24">
|
||||
<CardButton icon="compass" title="Discover spaces" class="h-24">
|
||||
Find a community based on your hobbies or interests.
|
||||
</CardButton>
|
||||
<CardButton icon="plain" title="Leave feedback" class="h-24">
|
||||
|
||||
@@ -3,6 +3,9 @@ import svg from "@poppanator/sveltekit-svg"
|
||||
import {defineConfig} from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 1847,
|
||||
},
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
svg({
|
||||
|
||||
Reference in New Issue
Block a user