import {nip19} from "nostr-tools" import {get, derived} from "svelte/store" import type {Maybe} from "@welshman/lib" import {setContext, partition, nth, max, pushToMapKey, nthEq} from "@welshman/lib" import { getIdFilters, NOTE, RELAYS, REACTION, ZAP_RESPONSE, EVENT_DATE, EVENT_TIME, getRelayTagValues, isShareableRelayUrl, getAncestorTags, getAncestorTagValues, } from "@welshman/util" import type {TrustedEvent} from "@welshman/util" import { pubkey, repository, load, subscribe, collection, loadRelay, loadProfile, profilesByPubkey, getDefaultAppContext, getDefaultNetContext, makeRouter, trackerStore, } from "@welshman/app" import type {SubscribeRequestWithHandlers} from "@welshman/net" import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store" export const ROOM = "~" export const GENERAL = "general" export const MESSAGE = 209 export const REPLY = 1111 export const MEMBERSHIPS = 10209 export const INDEXER_RELAYS = [ "wss://purplepag.es/", "wss://relay.damus.io/", "wss://relay.nostr.band/", ] export const DUFFLEPUD_URL = "https://dufflepud.onrender.com" export const IMGPROXY_URL = "https://imgproxy.coracle.social" export const REACTION_KINDS = [REACTION, ZAP_RESPONSE] export const dufflepud = (path: string) => DUFFLEPUD_URL + '/' + path export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => { if (!url || url.match("gif$")) { return url } url = url.split("?")[0] try { return url ? `${IMGPROXY_URL}/x/s:${w}:${h}/${btoa(url)}` : url } catch (e) { return url } } export const entityLink = (entity: string) => `https://coracle.social/${entity}` setContext({ net: getDefaultNetContext(), app: getDefaultAppContext({ dufflepudUrl: DUFFLEPUD_URL, indexerRelays: INDEXER_RELAYS, requestTimeout: 5000, router: makeRouter(), }), }) 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}), (events: TrustedEvent[]) => { if (!attempted && events.length === 0) { load({relays, filters}) attempted = true } return events[0] }, ) } // Membership export type Membership = { roomsByUrl: Map event?: TrustedEvent } export type PublishedMembership = Omit & { event: TrustedEvent } export const readMembership = (event: TrustedEvent): PublishedMembership => { const roomsByUrl = new Map() for (const tag of event.tags.filter(nthEq(0, "r"))) { roomsByUrl.set(tag[1], []) } for (const tag of event.tags.filter(nthEq(0, "~"))) { pushToMapKey(roomsByUrl, tag[2], tag[1]) } return {event, roomsByUrl} } export const memberships = deriveEventsMapped(repository, { filters: [{kinds: [MEMBERSHIPS]}], eventToItem: readMembership, itemToEvent: item => item.event, }) export const { indexStore: membershipByPubkey, deriveItem: deriveMembership, loadItem: loadMembership, } = collection({ name: "memberships", store: memberships, getKey: membership => membership.event.pubkey, load: (pubkey: string, request: Partial = {}) => load({ ...request, filters: [{kinds: [MEMBERSHIPS], authors: [pubkey]}], }), }) // Messages export type Message = { room: string event: TrustedEvent } export const readMessage = (event: TrustedEvent): Maybe => { const rooms = event.tags.filter(nthEq(0, ROOM)).map(nth(1)) if (rooms.length > 1) return undefined return {room: rooms[0] || "", event} } export const messages = deriveEventsMapped(repository, { filters: [{kinds: [MESSAGE, REPLY]}], eventToItem: readMessage, itemToEvent: item => item.event, }) // Chats export type Chat = { id: string url: string room: string messages: Message[] } export const makeChatId = (url: string, room: string) => `${url}'${room}` export const splitChatId = (id: string) => id.split("'") export const chats = derived([trackerStore, messages], ([$tracker, $messages]) => { const messagesByChatId = new Map() for (const message of $messages) { for (const url of $tracker.getRelays(message.event.id)) { const chatId = makeChatId(url, message.room) pushToMapKey(messagesByChatId, chatId, message) } } return Array.from(messagesByChatId.entries()).map(([id, messages]) => { const [url, room] = splitChatId(id) return {id, url, room, messages} }) }) export const { indexStore: chatsById, deriveItem: deriveChat, loadItem: loadChat, } = collection({ name: "chats", store: chats, getKey: chat => chat.id, load: (id: string, request: Partial = {}) => { const [url, room] = 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: [url], filters: [{"#~": [room], since}]}) }, }) // Calendar events export const events = deriveEvents(repository, {filters: [{kinds: [EVENT_DATE, EVENT_TIME]}]}) export const eventsByUrl = derived([trackerStore, events], ([$tracker, $events]) => { const eventsByUrl = new Map() for (const event of $events) { for (const url of $tracker.getRelays(event.id)) { pushToMapKey(eventsByUrl, url, event) } } return eventsByUrl }) // Threads export type Thread = { root: TrustedEvent replies: TrustedEvent[] } export const notes = deriveEvents(repository, {filters: [{kinds: [NOTE]}]}) export const threadsByUrl = derived([trackerStore, notes], ([$tracker, $notes]) => { const threadsByUrl = new Map() const [parents, children] = partition(e => getAncestorTags(e.tags).replies.length === 0, $notes) for (const event of parents) { for (const url of $tracker.getRelays(event.id)) { pushToMapKey(threadsByUrl, url, {root: event, replies: []}) } } for (const event of children) { const [id] = getAncestorTagValues(event.tags).replies for (const url of $tracker.getRelays(event.id)) { const threads = threadsByUrl.get(url) || [] const thread = threads.find(thread => thread.root.id === id) if (!thread) { continue } thread.replies.push(event) } } return threadsByUrl }) // Rooms export const roomsByUrl = derived(chats, $chats => { const $roomsByUrl = new Map() for (const chat of $chats) { if (chat.room) { pushToMapKey($roomsByUrl, chat.url, chat.room) } } return $roomsByUrl }) // User stuff export const userProfile = derived([pubkey, profilesByPubkey], ([$pubkey, $profilesByPubkey]) => { if (!$pubkey) return null loadProfile($pubkey) return $profilesByPubkey.get($pubkey) }) export const userMembership = withGetter( derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => { if (!$pubkey) return null loadMembership($pubkey) return $membershipByPubkey.get($pubkey) }), ) // Other utils 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) } } }, })