diff --git a/src/app/components/MenuSpaceButton.svelte b/src/app/components/MenuSpaceButton.svelte index f44fc32..dfc7f39 100644 --- a/src/app/components/MenuSpaceButton.svelte +++ b/src/app/components/MenuSpaceButton.svelte @@ -39,7 +39,7 @@ if ($page.url.pathname === path) return false const lastChecked = max([$checked["*"], $checked[path]]) - const roomEvents = $events.filter(e => matchFilter({"#~": [room]}, e)) + const roomEvents = $events.filter(e => matchFilter({"#h": [room]}, e)) return getNotification($pubkey, lastChecked, roomEvents) }), diff --git a/src/app/components/MenuSpaceRoomItem.svelte b/src/app/components/MenuSpaceRoomItem.svelte index fb790bb..c2808c4 100644 --- a/src/app/components/MenuSpaceRoomItem.svelte +++ b/src/app/components/MenuSpaceRoomItem.svelte @@ -2,6 +2,7 @@ import Icon from "@lib/components/Icon.svelte" import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte" import {makeSpacePath} from "@app/routes" + import {displayRoom} from "@app/state" import {deriveNotification, getRoomFilters} from "@app/notifications" export let url @@ -13,5 +14,5 @@ - {room} + {displayRoom(room)} diff --git a/src/app/notifications.ts b/src/app/notifications.ts index 8185085..40b4c35 100644 --- a/src/app/notifications.ts +++ b/src/app/notifications.ts @@ -40,7 +40,7 @@ export const THREAD_FILTERS: Filter[] = [ export const getNotificationFilters = (since: number): Filter[] => [...CHAT_FILTERS, ...SPACE_FILTERS, ...THREAD_FILTERS].map(assoc("since", since)) -export const getRoomFilters = (room: string): Filter[] => ROOM_FILTERS.map(assoc("#~", [room])) +export const getRoomFilters = (room: string): Filter[] => ROOM_FILTERS.map(assoc("#h", [room])) // Notification derivation diff --git a/src/app/state.ts b/src/app/state.ts index bb75e19..ffd7a10 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,7 +1,6 @@ import twColors from "tailwindcss/colors" import {get, derived} from "svelte/store" import {nip19} from "nostr-tools" -import type {Maybe} from "@welshman/lib" import { ctx, setContext, @@ -32,8 +31,6 @@ import { readList, getListTags, asDecryptedEvent, - isSignedEvent, - hasValidSignature, normalizeRelayUrl, } from "@welshman/util" import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util" @@ -48,6 +45,7 @@ import { getDefaultNetContext, makeRouter, tracker, + trackerStore, relay, getSession, getSigner, @@ -56,22 +54,23 @@ import { createSearch, userFollows, ensurePlaintext, + thunkWorker, } from "@welshman/app" -import type {AppSyncOpts} from "@welshman/app" +import type {AppSyncOpts, Thunk} from "@welshman/app" import type {SubscribeRequestWithHandlers} from "@welshman/net" import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store" -export const ROOM = "~" +export const ROOM = "h" -export const GENERAL = "general" +export const GENERAL = "_" -export const MESSAGE = 209 +export const MESSAGE = 9 -export const THREAD = 309 +export const THREAD = 11 export const COMMENT = 1111 -export const MEMBERSHIPS = 10209 +export const MEMBERSHIPS = 10009 export const INDEXER_RELAYS = [ "wss://purplepag.es/", @@ -154,7 +153,7 @@ export const pubkeyLink = ( relays = ctx.app.router.FromPubkeys([pubkey]).getUrls(), ) => entityLink(nip19.nprofileEncode({pubkey, relays})) -export const tagRoom = (room: string, url: string) => [ROOM, room, url] +export const tagRoom = (room: string, url: string) => [ROOM, room] export const getDefaultPubkeys = () => { const appPubkeys = DEFAULT_PUBKEYS.split(",") @@ -222,30 +221,6 @@ export const pullConservatively = ({relays, filters}: AppSyncOpts) => { return Promise.all(promises) } -setContext({ - net: getDefaultNetContext({ - isValid: (url: string, event: TrustedEvent) => { - if (!isSignedEvent(event) || !hasValidSignature(event)) { - return false - } - - const roomTags = event.tags.filter(nthEq(0, "~")) - - if (roomTags.length > 0 && !roomTags.some(nthEq(2, url))) { - return false - } - - return true - }, - }), - app: getDefaultAppContext({ - dufflepudUrl: DUFFLEPUD_URL, - indexerRelays: INDEXER_RELAYS, - requestTimeout: 5000, - router: makeRouter(), - }), -}) - export const deriveEvent = (idOrAddress: string, hints: string[] = []) => { let attempted = false @@ -265,23 +240,42 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => { ) } -export const eventIsForUrl = (url: string, event: TrustedEvent) => - event.tags.find(nthEq(0, "~"))?.[2] === url - export const getEventsForUrl = (url: string, filters: Filter[]) => sortBy( e => -e.created_at, - repository.query(filters).filter(e => eventIsForUrl(url, e)), + repository.query(filters).filter(e => tracker.hasRelay(e.id, url)), ) export const deriveEventsForUrl = (url: string, filters: Filter[]) => - derived(deriveEvents(repository, {filters}), $events => + derived([deriveEvents(repository, {filters}), trackerStore], ([$events, $tracker]) => sortBy( e => -e.created_at, - $events.filter(e => eventIsForUrl(url, e)), + $events.filter(e => $tracker.hasRelay(e.id, url)), ), ) +// Context + +setContext({ + net: getDefaultNetContext(), + app: getDefaultAppContext({ + dufflepudUrl: DUFFLEPUD_URL, + indexerRelays: INDEXER_RELAYS, + requestTimeout: 5000, + router: makeRouter(), + }), +}) + +// Track what urls we're attempting to send messages to so we can associate them with spaces immediately + +thunkWorker.addGlobalHandler((thunk: Thunk) => { + if (thunk.event.tags.find(t => t[0] === ROOM)) { + for (const url of thunk.request.relays) { + tracker.track(thunk.event.id, url) + } + } +}) + // Settings export const canDecrypt = synced("canDecrypt", false) @@ -335,7 +329,7 @@ export const { export const hasMembershipUrl = (list: List | undefined, url: string) => getListTags(list).some(t => { if (t[0] === "r") return t[1] === url - if (t[0] === "~") return t[2] === url + if (t[0] === "group") return t[2] === url return false }) @@ -344,13 +338,13 @@ export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getList export const getMembershipRooms = (list?: List) => getListTags(list) - .filter(t => t[0] === "~") + .filter(t => t[0] === "group") .map(t => ({url: t[2], room: t[1]})) export const getMembershipRoomsByUrl = (url: string, list?: List) => sort( getListTags(list) - .filter(t => t[0] === "~" && t[2] === url) + .filter(t => t[0] === "group" && t[2] === url) .map(nth(1)), ) @@ -375,85 +369,6 @@ export const { }), }) -// Messages - -export type ChannelMessage = { - url: string - room: string - event: TrustedEvent -} - -export const readMessage = (event: TrustedEvent): Maybe => { - const roomTags = event.tags.filter(nthEq(0, ROOM)) - - if (roomTags.length !== 1) return undefined - - const [_, room, url] = roomTags[0] - - if (!url || !room) return undefined - - return {url: normalizeRelayUrl(url), room, event} -} - -export const channelMessages = deriveEventsMapped(repository, { - filters: [{kinds: [MESSAGE]}], - eventToItem: readMessage, - itemToEvent: item => item.event, -}) - -// Channels - -export type Channel = { - id: string - url: string - room: string - messages: ChannelMessage[] -} - -export const makeChannelId = (url: string, room: string) => `${url}|${room}` - -export const splitChannelId = (id: string) => id.split("|") - -export const channels = derived( - [memberships, channelMessages], - ([$memberships, $channelMessages]) => { - const messagesByChannelId = new Map() - - // Add known rooms by membership so we don't have to scan messages to load all rooms - for (const membership of $memberships) { - for (const {url, room} of getMembershipRooms(membership)) { - messagesByChannelId.set(makeChannelId(url, room), []) - } - } - - // Add messages/rooms without memberships - for (const message of $channelMessages) { - pushToMapKey(messagesByChannelId, makeChannelId(message.url, message.room), message) - } - - return Array.from(messagesByChannelId.entries()).map(([id, messages]) => { - const [url, room] = splitChannelId(id) - - return {id, url, room, messages} - }) - }, -) - -export const { - indexStore: channelsById, - deriveItem: deriveChannel, - loadItem: loadChannel, -} = collection({ - name: "channels", - store: channels, - getKey: channel => channel.id, - load: (id: string, request: Partial = {}) => { - const [url, room] = splitChannelId(id) - - return load({...request, relays: [url], filters: [{"#~": [room]}]}) - }, -}) - // Chats export const chatMessages = deriveEvents(repository, {filters: [{kinds: [DIRECT_MESSAGE]}]}) @@ -518,20 +433,95 @@ export const chatSearch = derived(chats, $chats => }), ) +// Messages + +export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]}) + +// Channels + +export type Channel = { + url: string + room: string + name: string + events: TrustedEvent[] +} + +export const makeChannelId = (url: string, room: string) => `${url}|${room}` + +export const splitChannelId = (id: string) => id.split("|") + +export const channelsById = derived( + [memberships, messages, trackerStore], + ([$memberships, $messages, $tracker]) => { + const eventsByChannelId = new Map() + + // Add known rooms by membership so we have a full listing even if there are no messages there + for (const membership of $memberships) { + for (const {url, room} of getMembershipRooms(membership)) { + eventsByChannelId.set(makeChannelId(url, room), []) + } + } + + // Add known messages to rooms + for (const event of $messages) { + const [_, room] = event.tags.find(nthEq(0, ROOM)) || [] + + if (room) { + for (const url of $tracker.getRelays(event.id)) { + pushToMapKey(eventsByChannelId, makeChannelId(url, room), event) + } + } + } + + const channelsById = new Map() + + for (const [id, unsorted] of eventsByChannelId.entries()) { + const [url, room] = splitChannelId(id) + const events = sortBy(e => -e.created_at, unsorted) + + let name = room + for (const event of events) { + const tag = event.tags.find(t => t[0] === ROOM && t[2]) + + if (tag) { + name = tag[2] + break + } + } + + channelsById.set(id, {url, room, name, events}) + } + + return channelsById + }, +) + +export const deriveChannel = (url: string, room: string) => + derived(channelsById, $channelsById => $channelsById.get(makeChannelId(url, room))) + +export const deriveChannelMessages = (url: string, room: string) => + derived(channelsById, $channelsById => $channelsById.get(makeChannelId(url, room))?.events || []) + // Rooms -export const roomsByUrl = derived(channels, $channels => { +export const roomsByUrl = derived(channelsById, $channelsById => { const $roomsByUrl = new Map() - for (const channel of $channels) { - if (channel.room) { - pushToMapKey($roomsByUrl, channel.url, channel.room) - } + for (const {url, room} of $channelsById.values()) { + pushToMapKey($roomsByUrl, url, room) } return $roomsByUrl }) +export const displayRoom = (room: string) => { + if (room === GENERAL) { + return "general" + } + + return room +} + // User stuff export const userSettings = withGetter( diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3119a14..2ba7322 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -208,7 +208,7 @@ filters: [ {kinds: [THREAD], limit: 1}, {kinds: [COMMENT], "#K": [String(THREAD)], limit: 1}, - ...rooms.map(room => ({kinds: [MESSAGE], "#~": [room], limit: 1})), + ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})), ], }) @@ -218,7 +218,7 @@ filters: [ {kinds: [THREAD], since}, {kinds: [COMMENT], "#K": [String(THREAD)], since}, - {kinds: [MESSAGE], "#~": rooms, since}, + {kinds: [MESSAGE], "#h": rooms, since}, ], }) }) diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte index 2a70d2b..3141d90 100644 --- a/src/routes/spaces/[relay]/[room]/+page.svelte +++ b/src/routes/spaces/[relay]/[room]/+page.svelte @@ -13,7 +13,7 @@ import type {Readable} from "svelte/store" import type {Editor} from "svelte-tiptap" import {page} from "$app/stores" - import {sortBy, sleep, append, now, ctx} from "@welshman/lib" + import {sleep, append, now, ctx} from "@welshman/lib" import type {TrustedEvent, EventContent} from "@welshman/util" import {createEvent, DELETE} from "@welshman/util" import {formatTimestampAsDate, publishThunk} from "@welshman/app" @@ -32,12 +32,12 @@ userSettingValues, userMembership, decodeRelay, - makeChannelId, - deriveChannel, + deriveChannelMessages, GENERAL, tagRoom, MESSAGE, getMembershipRoomsByUrl, + displayRoom, } from "@app/state" import {setChecked} from "@app/notifications" import {addRoomMembership, removeRoomMembership, subscribePersistent} from "@app/commands" @@ -46,7 +46,7 @@ const {room = GENERAL} = $page.params const content = popKey("content") || "" const url = decodeRelay($page.params.relay) - const channel = deriveChannel(makeChannelId(url, room)) + const events = deriveChannelMessages(url, room) const assertEvent = (e: any) => e as TrustedEvent @@ -80,7 +80,7 @@ let previousDate let previousPubkey - for (const {event} of sortBy(m => m.event.created_at, $channel?.messages || [])) { + for (const event of $events.toReversed()) { const {id, pubkey, created_at} = event const date = formatTimestampAsDate(created_at) @@ -108,7 +108,7 @@ pullConservatively({ relays: [url], - filters: [{kinds: [MESSAGE, DELETE], "#~": [room]}], + filters: [{kinds: [MESSAGE, DELETE], "#h": [room]}], }) scroller = createScroller({ @@ -122,7 +122,7 @@ unsub = subscribePersistent({ relays: [url], - filters: [{kinds: [MESSAGE], "#~": [room], since: now()}], + filters: [{kinds: [MESSAGE], "#h": [room], since: now()}], }) }) @@ -142,7 +142,7 @@
- {room} + {displayRoom(room)}
{#if room !== GENERAL} {#if getMembershipRoomsByUrl(url, $userMembership).includes(room)}