Convert to simple relay-based groups from nip29
This commit is contained in:
+118
-236
@@ -1,18 +1,29 @@
|
||||
import {nip19} from 'nostr-tools'
|
||||
import type {FuseResult} from "fuse.js"
|
||||
import {get, derived} from "svelte/store"
|
||||
import {get, derived, writable} from "svelte/store"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {setContext, max, between, groupBy, pushToMapKey, nthEq, stripProtocol, indexBy} from "@welshman/lib"
|
||||
import {
|
||||
setContext,
|
||||
max,
|
||||
between,
|
||||
groupBy,
|
||||
pushToMapKey,
|
||||
nthEq,
|
||||
stripProtocol,
|
||||
indexBy,
|
||||
uniq,
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
getIdFilters,
|
||||
getIdentifier,
|
||||
normalizeRelayUrl,
|
||||
GROUP_META,
|
||||
GROUPS,
|
||||
getGroupTags,
|
||||
GROUP_JOIN,
|
||||
GROUP_ADD_USER,
|
||||
RELAYS,
|
||||
APP_DATA,
|
||||
REACTION,
|
||||
ZAP_RESPONSE,
|
||||
getRelayTagValues,
|
||||
getTopicTagValues,
|
||||
isShareableRelayUrl,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
@@ -20,6 +31,7 @@ import {
|
||||
repository,
|
||||
createSearch,
|
||||
load,
|
||||
subscribe,
|
||||
collection,
|
||||
loadRelay,
|
||||
relaysByPubkey,
|
||||
@@ -32,15 +44,14 @@ import {
|
||||
makeRouter,
|
||||
} from "@welshman/app"
|
||||
import type {Relay} from "@welshman/app"
|
||||
import type {SubscribeRequest} from "@welshman/net"
|
||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store"
|
||||
|
||||
export const DEFAULT_RELAYS = [
|
||||
"wss://groups.fiatjaf.com/",
|
||||
"wss://relay29.galaxoidlabs.com/",
|
||||
"wss://devrelay.highlighter.com/",
|
||||
"wss://relay.groups.nip29.com/",
|
||||
]
|
||||
export const MESSAGE = 209
|
||||
|
||||
export const REPLY = 210
|
||||
|
||||
export const MEMBERSHIPS = 30209
|
||||
|
||||
export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"]
|
||||
|
||||
@@ -58,7 +69,6 @@ setContext({
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
let attempted = false
|
||||
|
||||
@@ -78,237 +88,122 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Groups
|
||||
// Persist relay/event mappings
|
||||
|
||||
export const GROUP_DELIMITER = `'`
|
||||
export const relaysByMessage = withGetter(writable(new Map<string, string[]>()))
|
||||
|
||||
export const makeGroupId = (url: string, nom: string) =>
|
||||
[stripProtocol(url).replace(/\/$/, ""), nom].join(GROUP_DELIMITER)
|
||||
// Topics
|
||||
|
||||
export const splitGroupId = (groupId: string) => {
|
||||
const [url, nom] = groupId.split(GROUP_DELIMITER)
|
||||
export const topicsByUrl = withGetter(writable(new Map<string, string[]>()))
|
||||
|
||||
return [normalizeRelayUrl(url), nom]
|
||||
}
|
||||
// Membership
|
||||
|
||||
export const getGroupUrl = (groupId: string) => splitGroupId(groupId)[0]
|
||||
|
||||
export const getGroupNom = (groupId: string) => splitGroupId(groupId)[1]
|
||||
|
||||
export const getGroupName = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "name"))?.[1]
|
||||
|
||||
export const getGroupPicture = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "picture"))?.[1]
|
||||
|
||||
export const displayGroup = (group?: Group) => group?.name || group?.nom || "[no name]"
|
||||
|
||||
export type Group = {
|
||||
nom: string
|
||||
name?: string
|
||||
about?: string
|
||||
picture?: string
|
||||
export type Membership = {
|
||||
topicsByUrl: Map<string, string[]>
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
export type PublishedGroup = Omit<Group, "event"> & {
|
||||
export type PublishedMembership = Omit<Membership, "event"> & {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
export const readGroup = (event: TrustedEvent) => {
|
||||
const nom = getIdentifier(event)!
|
||||
const name = event?.tags.find(nthEq(0, "name"))?.[1]
|
||||
const about = event?.tags.find(nthEq(0, "about"))?.[1]
|
||||
const picture = event?.tags.find(nthEq(0, "picture"))?.[1]
|
||||
export const readMembership = (event: TrustedEvent): PublishedMembership => {
|
||||
const topicsByUrl = new Map<string, string[]>()
|
||||
|
||||
return {nom, name, about, picture, event}
|
||||
}
|
||||
|
||||
export const groups = deriveEventsMapped<PublishedGroup>(repository, {
|
||||
filters: [{kinds: [GROUP_META]}],
|
||||
eventToItem: readGroup,
|
||||
itemToEvent: item => item.event,
|
||||
})
|
||||
|
||||
export const {
|
||||
indexStore: groupsByNom,
|
||||
deriveItem: deriveGroup,
|
||||
loadItem: loadGroup,
|
||||
} = collection({
|
||||
name: "groups",
|
||||
store: groups,
|
||||
getKey: (group: PublishedGroup) => group.nom,
|
||||
load: async (nom: string, hints: string[] = [], request: Partial<SubscribeRequest> = {}) => {
|
||||
if (hints.length === 0) {
|
||||
hints = relayUrlsByNom.get().get(nom) || []
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...hints.map(loadRelay),
|
||||
load({
|
||||
...request,
|
||||
relays: hints,
|
||||
filters: [{kinds: [GROUP_META], "#d": [nom]}],
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
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}],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// 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}))
|
||||
}),
|
||||
)
|
||||
|
||||
export const qualifiedGroupsById = derived(qualifiedGroups, $qualifiedGroups =>
|
||||
indexBy($qg => $qg.id, $qualifiedGroups),
|
||||
)
|
||||
|
||||
export const qualifiedGroupsByNom = derived(qualifiedGroups, $qualifiedGroups =>
|
||||
groupBy($qg => $qg.group.nom, $qualifiedGroups),
|
||||
)
|
||||
|
||||
export const relayUrlsByNom = withGetter(
|
||||
derived(qualifiedGroups, $qualifiedGroups => {
|
||||
const $relayUrlsByNom = new Map()
|
||||
|
||||
for (const {relay, group} of $qualifiedGroups) {
|
||||
pushToMapKey($relayUrlsByNom, group.nom, relay.url)
|
||||
}
|
||||
|
||||
return $relayUrlsByNom
|
||||
}),
|
||||
)
|
||||
|
||||
// Group membership
|
||||
|
||||
export type GroupMembership = {
|
||||
ids: Set<string>
|
||||
noms: Set<string>
|
||||
urls: Set<string>
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
export type PublishedGroupMembership = Omit<GroupMembership, "event"> & {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
export const readGroupMembership = (event: TrustedEvent) => {
|
||||
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)
|
||||
for (const tag of event.tags.filter(nthEq(0, "r"))) {
|
||||
topicsByUrl.set(tag[1], [])
|
||||
}
|
||||
|
||||
return {event, ids, noms, urls}
|
||||
for (const tag of event.tags.filter(nthEq(0, "t"))) {
|
||||
pushToMapKey(topicsByUrl, tag[2], tag[1])
|
||||
}
|
||||
|
||||
return {event, topicsByUrl}
|
||||
}
|
||||
|
||||
export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>(repository, {
|
||||
filters: [{kinds: [GROUPS]}],
|
||||
eventToItem: readGroupMembership,
|
||||
export const memberships = deriveEventsMapped<PublishedMembership>(repository, {
|
||||
filters: [{kinds: [MEMBERSHIPS]}],
|
||||
eventToItem: readMembership,
|
||||
itemToEvent: item => item.event,
|
||||
})
|
||||
|
||||
export const {
|
||||
indexStore: groupMembershipByPubkey,
|
||||
deriveItem: deriveGroupMembership,
|
||||
loadItem: loadGroupMembership,
|
||||
indexStore: membershipByPubkey,
|
||||
deriveItem: deriveMembership,
|
||||
loadItem: loadMembership,
|
||||
} = collection({
|
||||
name: "groupMemberships",
|
||||
store: groupMemberships,
|
||||
getKey: groupMembership => groupMembership.event.pubkey,
|
||||
load: async (pubkey: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
|
||||
const relays = getWriteRelayUrls(await loadRelaySelections(pubkey, hints))
|
||||
|
||||
return load({
|
||||
name: "memberships",
|
||||
store: memberships,
|
||||
getKey: membership => membership.event.pubkey,
|
||||
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
|
||||
load({
|
||||
...request,
|
||||
relays: [...hints, ...relays, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [GROUPS], authors: [pubkey]}],
|
||||
})
|
||||
},
|
||||
filters: [{kinds: [MEMBERSHIPS], authors: [pubkey]}],
|
||||
}),
|
||||
})
|
||||
|
||||
// Group Messages
|
||||
// Messages
|
||||
|
||||
export type GroupMessage = {
|
||||
nom: string
|
||||
export type Message = {
|
||||
url: string
|
||||
chat: string
|
||||
topic: string
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
export const readGroupMessage = (event: TrustedEvent): Maybe<GroupMessage> => {
|
||||
const nom = event.tags.find(nthEq(0, "h"))?.[1]
|
||||
export const readMessage = (event: TrustedEvent): Maybe<Message[]> => {
|
||||
const topics = getTopicTagValues(event.tags)
|
||||
|
||||
if (
|
||||
!nom ||
|
||||
between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind) ||
|
||||
REACTION_KINDS.includes(event.kind)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
if (topics.length !== 1) return undefined
|
||||
|
||||
return {nom, event}
|
||||
const topic = topics[0]
|
||||
const urls = relaysByMessage.get().get(event.id) || []
|
||||
|
||||
return urls.map(url => ({url, topic, chat: getChatId(url, topic), event}))
|
||||
}
|
||||
|
||||
export const groupMessages = deriveEventsMapped<GroupMessage>(repository, {
|
||||
export const messages = deriveEventsMapped<Message>(repository, {
|
||||
filters: [{}],
|
||||
eventToItem: readGroupMessage,
|
||||
eventToItem: readMessage,
|
||||
itemToEvent: item => item.event,
|
||||
})
|
||||
|
||||
// Group Chats
|
||||
// Chats
|
||||
|
||||
export type GroupChat = {
|
||||
nom: string
|
||||
messages: GroupMessage[]
|
||||
export type Chat = {
|
||||
id: string
|
||||
url: string
|
||||
topic: string
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
export const groupChats = derived(groupMessages, $groupMessages => {
|
||||
const groupMessagesByNom = groupBy($groupMessage => $groupMessage.nom, $groupMessages)
|
||||
export const getChatId = (url: string, topic: string) => `${url}'${topic}`
|
||||
|
||||
return Array.from(groupMessagesByNom.entries()).map(([nom, messages]) => ({nom, messages}))
|
||||
})
|
||||
export const splitChatId = (id: string) => id.split("'")
|
||||
|
||||
export const chats = derived(messages, $messages =>
|
||||
Array.from(groupBy($message => $message.chat, $messages).values()).map(messages => {
|
||||
const {chat, url, topic} = messages[0]
|
||||
|
||||
return {id: chat, url, topic, messages}
|
||||
}),
|
||||
)
|
||||
|
||||
export const {
|
||||
indexStore: groupChatByNom,
|
||||
deriveItem: deriveGroupChat,
|
||||
loadItem: loadGroupChat,
|
||||
indexStore: chatsById,
|
||||
deriveItem: deriveChat,
|
||||
loadItem: loadChat,
|
||||
} = collection({
|
||||
name: "groupChats",
|
||||
store: groupChats,
|
||||
getKey: groupChat => groupChat.nom,
|
||||
load: (nom: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
|
||||
const relays = [...hints, ...(get(relayUrlsByNom).get(nom) || [])]
|
||||
const chat = get(groupChats).find(c => c.nom === nom)
|
||||
name: "chats",
|
||||
store: chats,
|
||||
getKey: chat => chat.id,
|
||||
load: (id: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
|
||||
const [url, topic] = splitChatId(id)
|
||||
const chat = get(chatsById).get(id)
|
||||
const timestamps = chat?.messages.map(m => m.event.created_at) || []
|
||||
const since = Math.max(0, max(timestamps) - 3600)
|
||||
|
||||
return load({...request, relays, filters: [{"#h": [nom], since}]})
|
||||
return load({...request, relays: [url], filters: [{"#t": [topic], since}]})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -322,47 +217,34 @@ export const userProfile = derived([pubkey, profilesByPubkey], ([$pubkey, $profi
|
||||
return $profilesByPubkey.get($pubkey)
|
||||
})
|
||||
|
||||
export const userMembership = derived(
|
||||
[pubkey, groupMembershipByPubkey],
|
||||
([$pubkey, $groupMembershipByPubkey]) => {
|
||||
export const userMembership = withGetter(
|
||||
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
|
||||
if (!$pubkey) return null
|
||||
|
||||
loadGroupMembership($pubkey)
|
||||
loadMembership($pubkey)
|
||||
|
||||
return $groupMembershipByPubkey.get($pubkey)
|
||||
},
|
||||
)
|
||||
|
||||
export const userGroupsByNom = withGetter(
|
||||
derived([userMembership, qualifiedGroupsById], ([$userMembership, $qualifiedGroupsById]) => {
|
||||
const $userGroupsByNom = new Map()
|
||||
|
||||
for (const id of $userMembership?.ids || []) {
|
||||
const [url, nom] = splitGroupId(id)
|
||||
const group = $qualifiedGroupsById.get(id)
|
||||
const groups = $userGroupsByNom.get(nom) || []
|
||||
|
||||
loadGroup(nom, [url])
|
||||
|
||||
if (group) {
|
||||
groups.push(group)
|
||||
}
|
||||
|
||||
$userGroupsByNom.set(nom, groups)
|
||||
}
|
||||
|
||||
return $userGroupsByNom
|
||||
return $membershipByPubkey.get($pubkey)
|
||||
}),
|
||||
)
|
||||
|
||||
export const userRelayUrlsByNom = derived(userGroupsByNom, $userGroupsByNom => {
|
||||
const $userRelayUrlsByNom = new Map()
|
||||
// Other utils
|
||||
|
||||
for (const [nom, groups] of $userGroupsByNom.entries()) {
|
||||
for (const group of groups) {
|
||||
pushToMapKey($userRelayUrlsByNom, nom, group.relay.url)
|
||||
}
|
||||
}
|
||||
export const decodeNEvent = (nevent: string) => nip19.decode(nevent).data as string
|
||||
|
||||
return $userRelayUrlsByNom
|
||||
})
|
||||
export const displayReaction = (content: string) => {
|
||||
if (content === "+") return "❤️"
|
||||
if (content === "-") return "👎"
|
||||
return content
|
||||
}
|
||||
|
||||
export const discoverRelays = () =>
|
||||
subscribe({
|
||||
filters: [{kinds: [RELAYS]}],
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
for (const url of getRelayTagValues(event.tags)) {
|
||||
if (isShareableRelayUrl(url)) {
|
||||
loadRelay(url)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user