flotilla/src/app/state.ts

265 lines
6.2 KiB
TypeScript
Raw Normal View History

2024-09-11 18:09:25 +00:00
import {nip19} from "nostr-tools"
2024-09-11 22:21:25 +00:00
import {get, derived} from "svelte/store"
2024-08-14 00:58:26 +00:00
import type {Maybe} from "@welshman/lib"
2024-09-23 17:28:33 +00:00
import {setContext, nth, max, pushToMapKey, nthEq} from "@welshman/lib"
2024-08-16 17:50:38 +00:00
import {
getIdFilters,
RELAYS,
2024-08-30 23:25:41 +00:00
REACTION,
ZAP_RESPONSE,
2024-09-11 22:21:25 +00:00
EVENT_DATE,
EVENT_TIME,
getRelayTagValues,
isShareableRelayUrl,
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-14 00:58:26 +00:00
load,
subscribe,
2024-08-30 23:25:41 +00:00
collection,
loadRelay,
loadProfile,
profilesByPubkey,
2024-09-11 03:19:57 +00:00
getDefaultAppContext,
getDefaultNetContext,
makeRouter,
2024-09-11 21:08:04 +00:00
trackerStore,
2024-08-30 23:25:41 +00:00
} from "@welshman/app"
import type {SubscribeRequestWithHandlers} from "@welshman/net"
2024-08-30 23:25:41 +00:00
import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store"
2024-08-13 19:41:25 +00:00
2024-09-23 17:28:33 +00:00
export const ROOM = "~"
export const GENERAL = "general"
export const MESSAGE = 209
2024-09-23 17:28:33 +00:00
export const REPLY = 1111
2024-09-11 21:08:04 +00:00
export const MEMBERSHIPS = 10209
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-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]
},
)
}
// Membership
2024-08-16 19:42:00 +00:00
export type Membership = {
2024-09-23 17:28:33 +00:00
roomsByUrl: Map<string, string[]>
2024-08-19 16:06:39 +00:00
event?: TrustedEvent
2024-08-13 19:41:25 +00:00
}
export type PublishedMembership = Omit<Membership, "event"> & {
2024-08-19 16:06:39 +00:00
event: TrustedEvent
2024-08-13 19:41:25 +00:00
}
export const readMembership = (event: TrustedEvent): PublishedMembership => {
2024-09-23 17:28:33 +00:00
const roomsByUrl = new Map<string, string[]>()
2024-08-06 22:46:37 +00:00
for (const tag of event.tags.filter(nthEq(0, "r"))) {
2024-09-23 17:28:33 +00:00
roomsByUrl.set(tag[1], [])
}
2024-08-14 00:58:26 +00:00
for (const tag of event.tags.filter(nthEq(0, "t"))) {
2024-09-23 17:28:33 +00:00
pushToMapKey(roomsByUrl, tag[2], tag[1])
2024-08-14 00:58:26 +00:00
}
2024-09-23 17:28:33 +00:00
return {event, roomsByUrl}
2024-08-14 00:58:26 +00:00
}
2024-08-13 19:41:25 +00:00
export const memberships = deriveEventsMapped<PublishedMembership>(repository, {
filters: [{kinds: [MEMBERSHIPS]}],
eventToItem: readMembership,
2024-08-13 19:41:25 +00:00
itemToEvent: item => item.event,
})
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export const {
indexStore: membershipByPubkey,
deriveItem: deriveMembership,
loadItem: loadMembership,
2024-08-30 23:25:41 +00:00
} = collection({
name: "memberships",
store: memberships,
getKey: membership => membership.event.pubkey,
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
load({
2024-08-15 19:08:07 +00:00
...request,
filters: [{kinds: [MEMBERSHIPS], authors: [pubkey]}],
}),
2024-08-15 19:08:07 +00:00
})
// Messages
2024-08-15 19:08:07 +00:00
export type Message = {
2024-09-23 17:28:33 +00:00
room: string
2024-08-19 16:06:39 +00:00
event: TrustedEvent
2024-08-15 19:08:07 +00:00
}
2024-09-11 21:08:04 +00:00
export const readMessage = (event: TrustedEvent): Maybe<Message> => {
2024-09-23 17:28:33 +00:00
const rooms = event.tags.filter(nthEq(0, ROOM)).map(nth(1))
2024-08-15 19:08:07 +00:00
2024-09-23 17:28:33 +00:00
if (rooms.length > 1) return undefined
2024-09-23 17:28:33 +00:00
return {room: rooms[0] || "", event}
2024-08-15 19:08:07 +00:00
}
export const messages = deriveEventsMapped<Message>(repository, {
2024-09-11 18:21:16 +00:00
filters: [{kinds: [MESSAGE, REPLY]}],
eventToItem: readMessage,
2024-08-15 19:08:07 +00:00
itemToEvent: item => item.event,
})
// Chats
2024-08-15 19:08:07 +00:00
export type Chat = {
id: string
url: string
2024-09-23 17:28:33 +00:00
room: string
messages: Message[]
2024-08-15 19:08:07 +00:00
}
2024-09-23 17:28:33 +00:00
export const makeChatId = (url: string, room: string) => `${url}'${room}`
2024-08-15 19:08:07 +00:00
export const splitChatId = (id: string) => id.split("'")
2024-09-11 22:21:25 +00:00
export const chats = derived([trackerStore, messages], ([$tracker, $messages]) => {
const messagesByChatId = new Map<string, Message[]>()
2024-09-11 22:21:25 +00:00
for (const message of $messages) {
for (const url of $tracker.getRelays(message.event.id)) {
2024-09-23 17:28:33 +00:00
const chatId = makeChatId(url, message.room)
2024-09-11 21:08:04 +00:00
2024-09-11 22:21:25 +00:00
pushToMapKey(messagesByChatId, chatId, message)
2024-09-11 21:08:04 +00:00
}
2024-09-11 22:21:25 +00:00
}
2024-09-11 21:08:04 +00:00
2024-09-11 22:21:25 +00:00
return Array.from(messagesByChatId.entries()).map(([id, messages]) => {
2024-09-23 17:28:33 +00:00
const [url, room] = splitChatId(id)
2024-09-11 21:08:04 +00:00
2024-09-23 17:28:33 +00:00
return {id, url, room, messages}
2024-09-11 22:21:25 +00:00
})
})
2024-08-15 19:08:07 +00:00
export const {
indexStore: chatsById,
deriveItem: deriveChat,
loadItem: loadChat,
2024-08-30 23:25:41 +00:00
} = collection({
name: "chats",
store: chats,
getKey: chat => chat.id,
load: (id: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
2024-09-23 17:28:33 +00:00
const [url, room] = splitChatId(id)
const chat = get(chatsById).get(id)
2024-08-22 19:21:54 +00:00
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-09-23 17:28:33 +00:00
return load({...request, relays: [url], filters: [{'#~': [room], since}]})
2024-08-15 19:08:07 +00:00
},
2024-08-06 22:46:37 +00:00
})
2024-09-11 22:21:25 +00:00
// Calendar vents
export const events = deriveEvents(repository, {filters: [{kinds: [EVENT_DATE, EVENT_TIME]}]})
export const eventsByUrl = derived([trackerStore, events], ([$tracker, $events]) => {
const eventsByUrl = new Map<string, TrustedEvent[]>()
for (const event of $events) {
for (const url of $tracker.getRelays(event.id)) {
pushToMapKey(eventsByUrl, url, event)
}
}
return eventsByUrl
})
2024-09-23 17:28:33 +00:00
// Rooms
2024-09-11 19:19:45 +00:00
2024-09-23 17:28:33 +00:00
export const roomsByUrl = derived(chats, $chats => {
const $roomsByUrl = new Map<string, string[]>()
2024-09-11 19:19:45 +00:00
for (const chat of $chats) {
2024-09-23 17:28:33 +00:00
if (chat.room) {
pushToMapKey($roomsByUrl, chat.url, chat.room)
2024-09-11 19:19:45 +00:00
}
}
2024-09-23 17:28:33 +00:00
return $roomsByUrl
2024-09-11 19:19:45 +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)
})
export const userMembership = withGetter(
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
2024-08-30 23:25:41 +00:00
if (!$pubkey) return null
loadMembership($pubkey)
return $membershipByPubkey.get($pubkey)
}),
2024-08-16 17:50:38 +00:00
)
// Other utils
2024-09-11 18:21:16 +00:00
export const decodeNRelay = (nevent: string) => nip19.decode(nevent).data as string
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)
}
2024-08-16 17:50:38 +00:00
}
},
})