flotilla/src/app/state.ts

369 lines
9.2 KiB
TypeScript
Raw Normal View History

2024-08-16 17:50:38 +00:00
import type {FuseResult} from "fuse.js"
2024-08-30 23:25:41 +00:00
import {get, derived} from "svelte/store"
2024-08-14 00:58:26 +00:00
import type {Maybe} from "@welshman/lib"
2024-09-11 03:19:57 +00:00
import {setContext, max, between, groupBy, pushToMapKey, nthEq, stripProtocol, indexBy} from "@welshman/lib"
2024-08-16 17:50:38 +00:00
import {
getIdFilters,
getIdentifier,
normalizeRelayUrl,
GROUP_META,
GROUPS,
getGroupTags,
GROUP_JOIN,
GROUP_ADD_USER,
2024-08-30 23:25:41 +00:00
REACTION,
ZAP_RESPONSE,
2024-08-16 17:50:38 +00:00
} from "@welshman/util"
2024-08-30 23:25:41 +00:00
import type {TrustedEvent} from "@welshman/util"
2024-08-26 21:43:43 +00:00
import {
2024-08-30 23:25:41 +00:00
pubkey,
2024-08-26 21:43:43 +00:00
repository,
2024-08-30 23:25:41 +00:00
createSearch,
2024-08-14 00:58:26 +00:00
load,
2024-08-30 23:25:41 +00:00
collection,
loadRelay,
relaysByPubkey,
loadProfile,
profilesByPubkey,
loadRelaySelections,
getWriteRelayUrls,
2024-09-11 03:19:57 +00:00
getDefaultAppContext,
getDefaultNetContext,
makeRouter,
2024-08-30 23:25:41 +00:00
} from "@welshman/app"
import type {Relay} from "@welshman/app"
import type {SubscribeRequest} from "@welshman/net"
import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store"
2024-08-13 19:41:25 +00:00
2024-08-30 23:25:41 +00:00
export const DEFAULT_RELAYS = [
"wss://groups.fiatjaf.com/",
"wss://relay29.galaxoidlabs.com/",
"wss://devrelay.highlighter.com/",
"wss://relay.groups.nip29.com/",
]
2024-08-13 19:41:25 +00:00
2024-08-30 23:25:41 +00:00
export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"]
2024-08-13 19:41:25 +00:00
2024-08-30 23:25:41 +00:00
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
2024-08-13 19:41:25 +00:00
2024-08-30 23:25:41 +00:00
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
2024-08-13 19:41:25 +00:00
2024-09-11 03:19:57 +00:00
setContext({
net: getDefaultNetContext(),
app: getDefaultAppContext({
dufflepudUrl: DUFFLEPUD_URL,
indexerRelays: INDEXER_RELAYS,
requestTimeout: 5000,
router: makeRouter(),
}),
})
2024-08-13 19:41:25 +00:00
2024-08-16 17:50:38 +00:00
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
let attempted = false
const filters = getIdFilters([idOrAddress])
const relays = [...hints, ...INDEXER_RELAYS]
return derived(
deriveEvents(repository, {filters, includeDeleted: true}),
2024-08-19 16:06:39 +00:00
(events: TrustedEvent[]) => {
2024-08-16 17:50:38 +00:00
if (!attempted && events.length === 0) {
load({relays, filters})
attempted = true
}
return events[0]
},
)
}
2024-08-13 19:41:25 +00:00
// Groups
export const GROUP_DELIMITER = `'`
export const makeGroupId = (url: string, nom: string) =>
2024-08-16 17:50:38 +00:00
[stripProtocol(url).replace(/\/$/, ""), nom].join(GROUP_DELIMITER)
2024-08-09 23:22:09 +00:00
2024-08-13 19:41:25 +00:00
export const splitGroupId = (groupId: string) => {
const [url, nom] = groupId.split(GROUP_DELIMITER)
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
return [normalizeRelayUrl(url), nom]
}
export const getGroupUrl = (groupId: string) => splitGroupId(groupId)[0]
export const getGroupNom = (groupId: string) => splitGroupId(groupId)[1]
2024-08-19 16:06:39 +00:00
export const getGroupName = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "name"))?.[1]
2024-08-13 19:41:25 +00:00
2024-08-19 16:06:39 +00:00
export const getGroupPicture = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "picture"))?.[1]
2024-08-13 19:41:25 +00:00
2024-08-23 20:33:36 +00:00
export const displayGroup = (group?: Group) => group?.name || group?.nom || "[no name]"
2024-08-16 19:42:00 +00:00
2024-08-13 19:41:25 +00:00
export type Group = {
2024-08-16 17:50:38 +00:00
nom: string
name?: string
about?: string
picture?: string
2024-08-19 16:06:39 +00:00
event?: TrustedEvent
2024-08-13 19:41:25 +00:00
}
export type PublishedGroup = Omit<Group, "event"> & {
2024-08-19 16:06:39 +00:00
event: TrustedEvent
2024-08-13 19:41:25 +00:00
}
2024-08-19 16:06:39 +00:00
export const readGroup = (event: TrustedEvent) => {
2024-08-14 00:58:26 +00:00
const nom = getIdentifier(event)!
2024-08-23 20:33:36 +00:00
const name = event?.tags.find(nthEq(0, "name"))?.[1]
const about = event?.tags.find(nthEq(0, "about"))?.[1]
2024-08-13 19:41:25 +00:00
const picture = event?.tags.find(nthEq(0, "picture"))?.[1]
2024-08-14 21:52:00 +00:00
return {nom, name, about, picture, event}
2024-08-13 19:41:25 +00:00
}
2024-08-15 19:08:07 +00:00
export const groups = deriveEventsMapped<PublishedGroup>(repository, {
2024-08-06 22:46:37 +00:00
filters: [{kinds: [GROUP_META]}],
2024-08-13 19:41:25 +00:00
eventToItem: readGroup,
itemToEvent: item => item.event,
2024-08-06 22:46:37 +00:00
})
2024-08-13 19:41:25 +00:00
export const {
2024-08-14 00:58:26 +00:00
indexStore: groupsByNom,
2024-08-13 19:41:25 +00:00
deriveItem: deriveGroup,
loadItem: loadGroup,
2024-08-30 23:25:41 +00:00
} = collection({
2024-08-16 17:50:38 +00:00
name: "groups",
2024-08-14 19:46:25 +00:00
store: groups,
getKey: (group: PublishedGroup) => group.nom,
2024-08-29 16:23:54 +00:00
load: async (nom: string, hints: string[] = [], request: Partial<SubscribeRequest> = {}) => {
if (hints.length === 0) {
hints = relayUrlsByNom.get().get(nom) || []
}
await Promise.all([
2024-08-22 20:17:21 +00:00
...hints.map(loadRelay),
2024-08-14 19:46:25 +00:00
load({
...request,
2024-08-22 20:17:21 +00:00
relays: hints,
2024-08-16 17:50:38 +00:00
filters: [{kinds: [GROUP_META], "#d": [nom]}],
2024-08-14 19:46:25 +00:00
}),
2024-08-29 16:23:54 +00:00
])
},
2024-08-06 22:46:37 +00:00
})
2024-08-16 17:50:38 +00:00
export const searchGroups = derived(groups, $groups =>
createSearch($groups, {
getValue: (group: PublishedGroup) => group.nom,
sortFn: (result: FuseResult<PublishedGroup>) => {
const scale = result.item.picture ? 0.5 : 1
return result.score! * scale
},
fuseOptions: {
keys: ["name", {name: "about", weight: 0.3}],
},
}),
2024-08-14 21:52:00 +00:00
)
2024-08-14 00:58:26 +00:00
// Qualified groups
export type QualifiedGroup = {
id: string
relay: Relay
group: PublishedGroup
}
export const qualifiedGroups = derived([relaysByPubkey, groups], ([$relaysByPubkey, $groups]) =>
$groups.flatMap((group: PublishedGroup) => {
const relays = $relaysByPubkey.get(group.event.pubkey) || []
return relays.map(relay => ({id: makeGroupId(relay.url, group.nom), relay, group}))
2024-08-16 17:50:38 +00:00
}),
2024-08-14 00:58:26 +00:00
)
2024-08-16 17:50:38 +00:00
export const qualifiedGroupsById = derived(qualifiedGroups, $qualifiedGroups =>
indexBy($qg => $qg.id, $qualifiedGroups),
)
2024-08-14 00:58:26 +00:00
2024-08-16 17:50:38 +00:00
export const qualifiedGroupsByNom = derived(qualifiedGroups, $qualifiedGroups =>
groupBy($qg => $qg.group.nom, $qualifiedGroups),
)
2024-08-15 19:08:07 +00:00
2024-08-29 16:23:54 +00:00
export const relayUrlsByNom = withGetter(
derived(qualifiedGroups, $qualifiedGroups => {
const $relayUrlsByNom = new Map()
2024-08-15 19:08:07 +00:00
2024-08-29 16:23:54 +00:00
for (const {relay, group} of $qualifiedGroups) {
pushToMapKey($relayUrlsByNom, group.nom, relay.url)
}
2024-08-15 19:08:07 +00:00
2024-08-29 16:23:54 +00:00
return $relayUrlsByNom
}),
)
2024-08-15 19:08:07 +00:00
2024-08-13 19:41:25 +00:00
// Group membership
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export type GroupMembership = {
ids: Set<string>
2024-08-14 00:58:26 +00:00
noms: Set<string>
urls: Set<string>
2024-08-19 16:06:39 +00:00
event?: TrustedEvent
2024-08-13 19:41:25 +00:00
}
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export type PublishedGroupMembership = Omit<GroupMembership, "event"> & {
2024-08-19 16:06:39 +00:00
event: TrustedEvent
2024-08-13 19:41:25 +00:00
}
2024-08-06 22:46:37 +00:00
2024-08-19 16:06:39 +00:00
export const readGroupMembership = (event: TrustedEvent) => {
2024-08-14 00:58:26 +00:00
const ids = new Set<string>()
const noms = new Set<string>()
const urls = new Set<string>()
for (const [_, nom, url] of getGroupTags(event.tags)) {
ids.add(makeGroupId(url, nom))
noms.add(nom)
urls.add(url)
}
return {event, ids, noms, urls}
}
2024-08-13 19:41:25 +00:00
2024-08-15 19:08:07 +00:00
export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>(repository, {
2024-08-13 19:41:25 +00:00
filters: [{kinds: [GROUPS]}],
eventToItem: readGroupMembership,
itemToEvent: item => item.event,
})
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export const {
indexStore: groupMembershipByPubkey,
2024-08-13 19:41:25 +00:00
deriveItem: deriveGroupMembership,
loadItem: loadGroupMembership,
2024-08-30 23:25:41 +00:00
} = collection({
2024-08-16 17:50:38 +00:00
name: "groupMemberships",
2024-08-15 19:08:07 +00:00
store: groupMemberships,
getKey: groupMembership => groupMembership.event.pubkey,
2024-08-22 20:17:21 +00:00
load: async (pubkey: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
const relays = getWriteRelayUrls(await loadRelaySelections(pubkey, hints))
return load({
2024-08-15 19:08:07 +00:00
...request,
2024-08-22 20:17:21 +00:00
relays: [...hints, ...relays, ...INDEXER_RELAYS],
2024-08-15 19:08:07 +00:00
filters: [{kinds: [GROUPS], authors: [pubkey]}],
2024-08-22 20:17:21 +00:00
})
},
2024-08-15 19:08:07 +00:00
})
// Group Messages
export type GroupMessage = {
nom: string
2024-08-19 16:06:39 +00:00
event: TrustedEvent
2024-08-15 19:08:07 +00:00
}
2024-08-19 16:06:39 +00:00
export const readGroupMessage = (event: TrustedEvent): Maybe<GroupMessage> => {
2024-08-16 17:50:38 +00:00
const nom = event.tags.find(nthEq(0, "h"))?.[1]
2024-08-15 19:08:07 +00:00
2024-08-26 21:43:43 +00:00
if (
!nom ||
between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind) ||
REACTION_KINDS.includes(event.kind)
) {
2024-08-15 19:08:07 +00:00
return undefined
}
return {nom, event}
}
export const groupMessages = deriveEventsMapped<GroupMessage>(repository, {
filters: [{}],
eventToItem: readGroupMessage,
itemToEvent: item => item.event,
})
2024-08-22 19:21:54 +00:00
// Group Chats
2024-08-15 19:08:07 +00:00
2024-08-22 19:21:54 +00:00
export type GroupChat = {
2024-08-15 19:08:07 +00:00
nom: string
messages: GroupMessage[]
}
2024-08-22 19:21:54 +00:00
export const groupChats = derived(groupMessages, $groupMessages => {
2024-08-15 19:08:07 +00:00
const groupMessagesByNom = groupBy($groupMessage => $groupMessage.nom, $groupMessages)
return Array.from(groupMessagesByNom.entries()).map(([nom, messages]) => ({nom, messages}))
})
export const {
2024-08-22 19:21:54 +00:00
indexStore: groupChatByNom,
deriveItem: deriveGroupChat,
loadItem: loadGroupChat,
2024-08-30 23:25:41 +00:00
} = collection({
2024-08-22 19:21:54 +00:00
name: "groupChats",
store: groupChats,
getKey: groupChat => groupChat.nom,
2024-08-15 19:08:07 +00:00
load: (nom: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
2024-08-16 17:50:38 +00:00
const relays = [...hints, ...(get(relayUrlsByNom).get(nom) || [])]
2024-08-22 19:21:54 +00:00
const chat = get(groupChats).find(c => c.nom === nom)
const timestamps = chat?.messages.map(m => m.event.created_at) || []
2024-08-16 17:50:38 +00:00
const since = Math.max(0, max(timestamps) - 3600)
2024-08-15 19:08:07 +00:00
2024-08-16 17:50:38 +00:00
return load({...request, relays, filters: [{"#h": [nom], since}]})
2024-08-15 19:08:07 +00:00
},
2024-08-06 22:46:37 +00:00
})
// User stuff
2024-08-30 23:25:41 +00:00
export const userProfile = derived([pubkey, profilesByPubkey], ([$pubkey, $profilesByPubkey]) => {
if (!$pubkey) return null
2024-08-30 23:25:41 +00:00
loadProfile($pubkey)
2024-08-30 23:25:41 +00:00
return $profilesByPubkey.get($pubkey)
})
2024-08-16 17:50:38 +00:00
export const userMembership = derived(
2024-08-30 23:25:41 +00:00
[pubkey, groupMembershipByPubkey],
([$pubkey, $groupMembershipByPubkey]) => {
if (!$pubkey) return null
2024-08-30 23:25:41 +00:00
loadGroupMembership($pubkey)
2024-08-30 23:25:41 +00:00
return $groupMembershipByPubkey.get($pubkey)
2024-08-16 17:50:38 +00:00
},
)
2024-08-16 17:50:38 +00:00
export const userGroupsByNom = withGetter(
derived([userMembership, qualifiedGroupsById], ([$userMembership, $qualifiedGroupsById]) => {
const $userGroupsByNom = new Map()
2024-08-16 17:50:38 +00:00
for (const id of $userMembership?.ids || []) {
const [url, nom] = splitGroupId(id)
const group = $qualifiedGroupsById.get(id)
const groups = $userGroupsByNom.get(nom) || []
2024-08-16 17:50:38 +00:00
loadGroup(nom, [url])
2024-08-16 17:50:38 +00:00
if (group) {
groups.push(group)
}
2024-08-16 17:50:38 +00:00
$userGroupsByNom.set(nom, groups)
}
2024-08-16 17:50:38 +00:00
return $userGroupsByNom
}),
)
2024-08-23 20:33:36 +00:00
2024-08-26 21:43:43 +00:00
export const userRelayUrlsByNom = derived(userGroupsByNom, $userGroupsByNom => {
const $userRelayUrlsByNom = new Map()
2024-08-23 20:33:36 +00:00
2024-08-26 21:43:43 +00:00
for (const [nom, groups] of $userGroupsByNom.entries()) {
for (const group of groups) {
pushToMapKey($userRelayUrlsByNom, nom, group.relay.url)
2024-08-23 20:33:36 +00:00
}
}
2024-08-26 21:43:43 +00:00
return $userRelayUrlsByNom
})