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",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@welshman/lib": "^0.0.13",
|
"@welshman/lib": "^0.0.14",
|
||||||
"@welshman/net": "^0.0.17",
|
"@welshman/net": "^0.0.18",
|
||||||
"@welshman/signer": "^0.0.2",
|
"@welshman/signer": "^0.0.2",
|
||||||
"@welshman/store": "^0.0.1",
|
"@welshman/store": "^0.0.2",
|
||||||
"@welshman/util": "^0.0.24",
|
"@welshman/util": "^0.0.25",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
@@ -1372,9 +1372,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/lib": {
|
"node_modules/@welshman/lib": {
|
||||||
"version": "0.0.13",
|
"version": "0.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.14.tgz",
|
||||||
"integrity": "sha512-tp5+KAiUwoid04Pap47uqkz3MWDqA1iy+JklX7qu5WppClehMgkYfePtCbKOQ8LS9psl88xnZM1oR8NtDhVqnA==",
|
"integrity": "sha512-q5sWp3psLcouajdP97PZs2D44WoMZ0cwwT7EWUdShIPgisRJq9hjuMLSkUFqdgfJ3qBn72SSjcMMfDnkcQja4A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/base": "^1.1.6",
|
"@scure/base": "^1.1.6",
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
@@ -1384,12 +1384,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/net": {
|
"node_modules/@welshman/net": {
|
||||||
"version": "0.0.17",
|
"version": "0.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.18.tgz",
|
||||||
"integrity": "sha512-m2hvpb3AdHPmmhtfc16oy03523TG+6ggM9bt7vOLfKZl67qg9ki5VHFRxd7VPRuKosvNabeIErWBsXIxtKWdJg==",
|
"integrity": "sha512-5a6iKetUSDQisiG7/Cr61x9oSLhLNifqN/qzdOKW8Mu8ZqkOFYMIFPonLKT3UdixByvXn+RH5ka3b/nB77bBIA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "0.0.13",
|
"@welshman/lib": "0.0.14",
|
||||||
"@welshman/util": "0.0.24",
|
"@welshman/util": "0.0.25",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
}
|
}
|
||||||
@@ -1404,15 +1404,30 @@
|
|||||||
"@welshman/util": "^0.0.24"
|
"@welshman/util": "^0.0.24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/store": {
|
"node_modules/@welshman/signer/node_modules/@welshman/lib": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.13.tgz",
|
||||||
"integrity": "sha512-vpnbJOF8zoneTcLOr5iZuRETVwb67mUyLiWmGsfs8yoMI7lpxJkg0QKieVkp2FpVYoDYbb98eQEuYOiHdsf7RQ==",
|
"integrity": "sha512-tp5+KAiUwoid04Pap47uqkz3MWDqA1iy+JklX7qu5WppClehMgkYfePtCbKOQ8LS9psl88xnZM1oR8NtDhVqnA==",
|
||||||
"dependencies": {
|
"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",
|
"version": "0.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.24.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.24.tgz",
|
||||||
"integrity": "sha512-Qpe0J5VCYpqhpGB4f966d4FAw65L3JE2xYSqhBGbbfdMXnteqW6nHClP0YVbX9pMSYGRPn3ZXWkWf33yqImbGQ==",
|
"integrity": "sha512-Qpe0J5VCYpqhpGB4f966d4FAw65L3JE2xYSqhBGbbfdMXnteqW6nHClP0YVbX9pMSYGRPn3ZXWkWf33yqImbGQ==",
|
||||||
@@ -1421,6 +1436,23 @@
|
|||||||
"nostr-tools": "^2.3.2"
|
"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": {
|
"node_modules/acorn": {
|
||||||
"version": "8.12.1",
|
"version": "8.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
|
|||||||
@@ -38,11 +38,12 @@
|
|||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@welshman/lib": "^0.0.13",
|
"@welshman/lib": "^0.0.14",
|
||||||
"@welshman/net": "^0.0.17",
|
"@welshman/net": "^0.0.18",
|
||||||
"@welshman/signer": "^0.0.2",
|
"@welshman/signer": "^0.0.2",
|
||||||
"@welshman/store": "^0.0.1",
|
"@welshman/store": "^0.0.2",
|
||||||
"@welshman/util": "^0.0.24",
|
"@welshman/util": "^0.0.25",
|
||||||
|
"@welshman/domain": "^0.0.1",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {memoize} from '@welshman/lib'
|
import {memoize, assoc} from '@welshman/lib'
|
||||||
import type {SignedEvent} from "@welshman/util"
|
import type {CustomEvent} from '@welshman/util'
|
||||||
import {Repository, createEvent, Relay} from "@welshman/util"
|
import {Repository, createEvent, Relay} from "@welshman/util"
|
||||||
import {getter} from "@welshman/store"
|
import {getter} from "@welshman/store"
|
||||||
import {NetworkContext, Tracker} from "@welshman/net"
|
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 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 session = derived([pk, sessions], ([$pk, $sessions]) => $pk ? $sessions[$pk] : null)
|
||||||
|
|
||||||
export const getSession = getter(session)
|
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) => {
|
export const makeSigner = memoize((session: Session) => {
|
||||||
switch (session?.method) {
|
switch (session?.method) {
|
||||||
case "extension":
|
case "extension":
|
||||||
return new Nip07Signer()
|
return new Nip07Signer()
|
||||||
case "privkey":
|
case "privkey":
|
||||||
return new Nip01Signer(session.secret!)
|
return new Nip01Signer(session.secret!)
|
||||||
case "connect":
|
case "nip46":
|
||||||
return new Nip46Signer(Nip46Broker.get(session.pubkey, session.secret!, session.handler!))
|
return new Nip46Signer(Nip46Broker.get(session.pubkey, session.secret!, session.handler!))
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
@@ -47,8 +54,8 @@ export const getSigner = getter(signer)
|
|||||||
const seenChallenges = new Set()
|
const seenChallenges = new Set()
|
||||||
|
|
||||||
Object.assign(NetworkContext, {
|
Object.assign(NetworkContext, {
|
||||||
onEvent: (url: string, event: SignedEvent) => tracker.track(event.id, url),
|
onEvent: (url: string, event: CustomEvent) => tracker.track(event.id, url),
|
||||||
isDeleted: (url: string, event: SignedEvent) => repository.isDeleted(event),
|
isDeleted: (url: string, event: CustomEvent) => repository.isDeleted(event),
|
||||||
onAuth: async (url: string, challenge: string) => {
|
onAuth: async (url: string, challenge: string) => {
|
||||||
if (seenChallenges.has(challenge)) {
|
if (seenChallenges.has(challenge)) {
|
||||||
return
|
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 InfoNostr from '@app/components/LogIn.svelte'
|
||||||
import {pushModal, clearModal} from '@app/modal'
|
import {pushModal, clearModal} from '@app/modal'
|
||||||
import {pushToast} from '@app/toast'
|
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 back = () => history.back()
|
||||||
|
|
||||||
const tryLogin = async () => {
|
const tryLogin = async () => {
|
||||||
const secret = makeSecret()
|
const secret = makeSecret()
|
||||||
const handle = await getHandleInfo(`${username}@${handler.domain}`)
|
const handle = await loadHandle(`${username}@${handler.domain}`)
|
||||||
|
|
||||||
if (!handle?.pubkey) {
|
if (!handle?.pubkey) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -26,9 +27,9 @@
|
|||||||
const {pubkey, relays = []} = handle
|
const {pubkey, relays = []} = handle
|
||||||
const broker = Nip46Broker.get(pubkey, secret, handler)
|
const broker = Nip46Broker.get(pubkey, secret, handler)
|
||||||
const [profile, success] = await Promise.all([
|
const [profile, success] = await Promise.all([
|
||||||
getProfile(pubkey, relays),
|
loadProfile(pubkey, relays),
|
||||||
getFollows(pubkey, relays),
|
loadFollows(pubkey, relays),
|
||||||
getMutes(pubkey, relays),
|
loadMutes(pubkey, relays),
|
||||||
broker.connect(),
|
broker.connect(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import SpaceAdd from '@app/components/SpaceAdd.svelte'
|
import SpaceAdd from '@app/components/SpaceAdd.svelte'
|
||||||
import {makeGroupId} from "@app/domain"
|
|
||||||
import {session} from "@app/base"
|
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"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export const addSpace = () => pushModal(SpaceAdd)
|
export const addSpace = () => pushModal(SpaceAdd)
|
||||||
|
|
||||||
export const browseSpaces = () => goto("/browse")
|
export const browseSpaces = () => goto("/browse")
|
||||||
|
|
||||||
const profile = deriveProfile($session?.pubkey)
|
$: profile = deriveProfile($session?.pubkey)
|
||||||
|
$: membership = deriveGroupMembership($session?.pubkey)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative w-14 bg-base-100">
|
<div class="relative w-14 bg-base-100">
|
||||||
@@ -27,17 +27,24 @@
|
|||||||
<div class="flex h-full flex-col justify-between">
|
<div class="flex h-full flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
<PrimaryNavItem title={$profile?.name}>
|
<PrimaryNavItem title={$profile?.name}>
|
||||||
<div class="w-10 rounded-full border border-solid border-base-300">
|
<div class="!flex w-10 items-center justify-center rounded-full border border-solid border-base-300">
|
||||||
<img alt="" src={$profile?.picture} />
|
{#if $profile?.picture}
|
||||||
|
<img alt="" src={$profile.picture} />
|
||||||
|
{:else}
|
||||||
|
<Icon icon="user-rounded" size={7} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{#each $userGroupRelaysByNom.entries() as [nom, relays] (nom)}
|
{#each Array.from($membership?.ids || []) as groupId (groupId)}
|
||||||
{@const group = $groupsById.get(makeGroupId(relays[0], nom))}
|
{#await getGroup(groupId)}
|
||||||
<PrimaryNavItem title={group.name}>
|
<!-- pass -->
|
||||||
|
{:then group}
|
||||||
|
<PrimaryNavItem title={group?.name}>
|
||||||
<div class="w-10 rounded-full border border-solid border-base-300">
|
<div class="w-10 rounded-full border border-solid border-base-300">
|
||||||
<img alt={group.name} src={group.picture} />
|
<img alt={group?.name} src={group?.picture} />
|
||||||
</div>
|
</div>
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
|
{/await}
|
||||||
{/each}
|
{/each}
|
||||||
<PrimaryNavItem title="Add Space" on:click={addSpace}>
|
<PrimaryNavItem title="Add Space" on:click={addSpace}>
|
||||||
<div class="!flex w-10 items-center justify-center">
|
<div class="!flex w-10 items-center justify-center">
|
||||||
|
|||||||
@@ -5,11 +5,9 @@
|
|||||||
import Field from '@lib/components/Field.svelte'
|
import Field from '@lib/components/Field.svelte'
|
||||||
import Icon from '@lib/components/Icon.svelte'
|
import Icon from '@lib/components/Icon.svelte'
|
||||||
import SpaceCreateFinish from '@app/components/SpaceCreateFinish.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 {pushModal} from '@app/modal'
|
||||||
import {pushToast} from '@app/toast'
|
import {pushToast} from '@app/toast'
|
||||||
import {relayInfo} from '@app/state'
|
import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from '@app/state'
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
@@ -18,7 +16,7 @@
|
|||||||
const tryJoin = async () => {
|
const tryJoin = async () => {
|
||||||
const [url, nom] = splitGroupId(id)
|
const [url, nom] = splitGroupId(id)
|
||||||
|
|
||||||
const info = await getRelayInfo(url)
|
const info = await loadRelay(url)
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -34,8 +32,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = await getGroup(id)
|
const group = await loadGroup(id)
|
||||||
console.log(info, group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const join = async () => {
|
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]
|
|
||||||
419
src/app/state.ts
419
src/app/state.ts
@@ -1,83 +1,352 @@
|
|||||||
import {writable, derived} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {pushToMapKey, indexBy} from "@welshman/lib"
|
import {writable, readable, derived} from "svelte/store"
|
||||||
import {getIdentifier, getPubkeyTagValues, GROUP_META, PROFILE, FOLLOWS, MUTES, GROUPS, getGroupTagValues} from "@welshman/util"
|
import {uniq, pushToMapKey, nthEq, batcher, postJson, stripProtocol, assoc, indexBy, now} from "@welshman/lib"
|
||||||
import {deriveEvents} from "@welshman/store"
|
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 {synced, parseJson} from '@lib/util'
|
||||||
import type {Session} from '@app/types'
|
import type {Session, Handle, Relay} from '@app/types'
|
||||||
import {repository, pk} from "@app/base"
|
import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSessions, makeSigner} from "@app/base"
|
||||||
import {getGroupNom, getGroupUrl, getGroupName, getGroupPicture, GROUP_DELIMITER} from "@app/domain"
|
|
||||||
|
|
||||||
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, {
|
const getItem = async (key: string, ...args: any[]) => {
|
||||||
filters: [{kinds: [PROFILE]}],
|
const item = getIndex().get(key)
|
||||||
})
|
|
||||||
|
|
||||||
export const profiles = derived(profileEvents, $profileEvents =>
|
if (item && isStale(item)) {
|
||||||
$profileEvents.map(event => ({...parseJson(event.content), event}))
|
return item
|
||||||
)
|
|
||||||
|
|
||||||
export const profilesByPubkey = derived(profiles, $profiles => indexBy(profile => profile.event.pubkey, $profiles))
|
|
||||||
|
|
||||||
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 relaysByNom
|
await loadItem(key, ...args)
|
||||||
|
|
||||||
|
return getIndex().get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {Nip46Handler} from "@welshman/signer"
|
||||||
|
import type {SignedEvent, TrustedEvent} from "@welshman/util"
|
||||||
|
import type {RelayProfile, Handle as HandleInfo} from "@welshman/domain"
|
||||||
|
|
||||||
export type Session = {
|
export type Session = {
|
||||||
method: string
|
method: string
|
||||||
@@ -7,14 +10,16 @@ export type Session = {
|
|||||||
handler?: Nip46Handler
|
handler?: Nip46Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelayInfo = {
|
export type Relay = RelayProfile & {
|
||||||
fetched_at: number
|
fetched_at: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HandleInfo = {
|
export type Handle = HandleInfo & {
|
||||||
pubkey: string
|
fetched_at: number,
|
||||||
nip05: string
|
}
|
||||||
nip46: string[]
|
|
||||||
relays: string[]
|
declare module '@welshman/util' {
|
||||||
fetched_at: number
|
interface CustomEvent extends TrustedEvent {
|
||||||
|
fetched_at: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
|
|
||||||
<div class="hero min-h-screen bg-base-200">
|
<div class="hero min-h-screen bg-base-200">
|
||||||
<div class="hero-content">
|
<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 text-center text-5xl">Welcome to</h1>
|
||||||
<h1 class="text-stark-content mb-4 text-center text-5xl font-bold uppercase">Flotilla</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">
|
<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.
|
Invite all your friends, do life together.
|
||||||
</CardButton>
|
</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.
|
Find a community based on your hobbies or interests.
|
||||||
</CardButton>
|
</CardButton>
|
||||||
<CardButton icon="plain" title="Leave feedback" class="h-24">
|
<CardButton icon="plain" title="Leave feedback" class="h-24">
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import svg from "@poppanator/sveltekit-svg"
|
|||||||
import {defineConfig} from "vite"
|
import {defineConfig} from "vite"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 1847,
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
sveltekit(),
|
sveltekit(),
|
||||||
svg({
|
svg({
|
||||||
|
|||||||
Reference in New Issue
Block a user