Use new kinds, re work channels

This commit is contained in:
Jon Staab
2024-12-05 13:37:15 -08:00
parent 64916f5d29
commit 14cd49caf3
6 changed files with 131 additions and 140 deletions

View File

@@ -39,7 +39,7 @@
if ($page.url.pathname === path) return false if ($page.url.pathname === path) return false
const lastChecked = max([$checked["*"], $checked[path]]) 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) return getNotification($pubkey, lastChecked, roomEvents)
}), }),

View File

@@ -2,6 +2,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte" import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {displayRoom} from "@app/state"
import {deriveNotification, getRoomFilters} from "@app/notifications" import {deriveNotification, getRoomFilters} from "@app/notifications"
export let url export let url
@@ -13,5 +14,5 @@
<SecondaryNavItem href={path} notification={$notification}> <SecondaryNavItem href={path} notification={$notification}>
<Icon icon="hashtag" /> <Icon icon="hashtag" />
{room} {displayRoom(room)}
</SecondaryNavItem> </SecondaryNavItem>

View File

@@ -40,7 +40,7 @@ export const THREAD_FILTERS: Filter[] = [
export const getNotificationFilters = (since: number): Filter[] => export const getNotificationFilters = (since: number): Filter[] =>
[...CHAT_FILTERS, ...SPACE_FILTERS, ...THREAD_FILTERS].map(assoc("since", since)) [...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 // Notification derivation

View File

@@ -1,7 +1,6 @@
import twColors from "tailwindcss/colors" import twColors from "tailwindcss/colors"
import {get, derived} from "svelte/store" import {get, derived} from "svelte/store"
import {nip19} from "nostr-tools" import {nip19} from "nostr-tools"
import type {Maybe} from "@welshman/lib"
import { import {
ctx, ctx,
setContext, setContext,
@@ -32,8 +31,6 @@ import {
readList, readList,
getListTags, getListTags,
asDecryptedEvent, asDecryptedEvent,
isSignedEvent,
hasValidSignature,
normalizeRelayUrl, normalizeRelayUrl,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util" import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
@@ -48,6 +45,7 @@ import {
getDefaultNetContext, getDefaultNetContext,
makeRouter, makeRouter,
tracker, tracker,
trackerStore,
relay, relay,
getSession, getSession,
getSigner, getSigner,
@@ -56,22 +54,23 @@ import {
createSearch, createSearch,
userFollows, userFollows,
ensurePlaintext, ensurePlaintext,
thunkWorker,
} from "@welshman/app" } from "@welshman/app"
import type {AppSyncOpts} from "@welshman/app" import type {AppSyncOpts, Thunk} from "@welshman/app"
import type {SubscribeRequestWithHandlers} from "@welshman/net" import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store" 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 COMMENT = 1111
export const MEMBERSHIPS = 10209 export const MEMBERSHIPS = 10009
export const INDEXER_RELAYS = [ export const INDEXER_RELAYS = [
"wss://purplepag.es/", "wss://purplepag.es/",
@@ -154,7 +153,7 @@ export const pubkeyLink = (
relays = ctx.app.router.FromPubkeys([pubkey]).getUrls(), relays = ctx.app.router.FromPubkeys([pubkey]).getUrls(),
) => entityLink(nip19.nprofileEncode({pubkey, relays})) ) => 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 = () => { export const getDefaultPubkeys = () => {
const appPubkeys = DEFAULT_PUBKEYS.split(",") const appPubkeys = DEFAULT_PUBKEYS.split(",")
@@ -222,30 +221,6 @@ export const pullConservatively = ({relays, filters}: AppSyncOpts) => {
return Promise.all(promises) 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[] = []) => { export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
let attempted = false 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[]) => export const getEventsForUrl = (url: string, filters: Filter[]) =>
sortBy( sortBy(
e => -e.created_at, 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[]) => export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
derived(deriveEvents(repository, {filters}), $events => derived([deriveEvents(repository, {filters}), trackerStore], ([$events, $tracker]) =>
sortBy( sortBy(
e => -e.created_at, 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 // Settings
export const canDecrypt = synced("canDecrypt", false) export const canDecrypt = synced("canDecrypt", false)
@@ -335,7 +329,7 @@ export const {
export const hasMembershipUrl = (list: List | undefined, url: string) => export const hasMembershipUrl = (list: List | undefined, url: string) =>
getListTags(list).some(t => { getListTags(list).some(t => {
if (t[0] === "r") return t[1] === url 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 return false
}) })
@@ -344,13 +338,13 @@ export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getList
export const getMembershipRooms = (list?: List) => export const getMembershipRooms = (list?: List) =>
getListTags(list) getListTags(list)
.filter(t => t[0] === "~") .filter(t => t[0] === "group")
.map(t => ({url: t[2], room: t[1]})) .map(t => ({url: t[2], room: t[1]}))
export const getMembershipRoomsByUrl = (url: string, list?: List) => export const getMembershipRoomsByUrl = (url: string, list?: List) =>
sort( sort(
getListTags(list) getListTags(list)
.filter(t => t[0] === "~" && t[2] === url) .filter(t => t[0] === "group" && t[2] === url)
.map(nth(1)), .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<ChannelMessage> => {
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<ChannelMessage>(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<string, ChannelMessage[]>()
// 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<SubscribeRequestWithHandlers> = {}) => {
const [url, room] = splitChannelId(id)
return load({...request, relays: [url], filters: [{"#~": [room]}]})
},
})
// Chats // Chats
export const chatMessages = deriveEvents(repository, {filters: [{kinds: [DIRECT_MESSAGE]}]}) 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<string, TrustedEvent[]>()
// 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<string, Channel>()
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 // Rooms
export const roomsByUrl = derived(channels, $channels => { export const roomsByUrl = derived(channelsById, $channelsById => {
const $roomsByUrl = new Map<string, string[]>() const $roomsByUrl = new Map<string, string[]>()
for (const channel of $channels) { for (const {url, room} of $channelsById.values()) {
if (channel.room) { pushToMapKey($roomsByUrl, url, room)
pushToMapKey($roomsByUrl, channel.url, channel.room)
}
} }
return $roomsByUrl return $roomsByUrl
}) })
export const displayRoom = (room: string) => {
if (room === GENERAL) {
return "general"
}
return room
}
// User stuff // User stuff
export const userSettings = withGetter( export const userSettings = withGetter(

View File

@@ -208,7 +208,7 @@
filters: [ filters: [
{kinds: [THREAD], limit: 1}, {kinds: [THREAD], limit: 1},
{kinds: [COMMENT], "#K": [String(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: [ filters: [
{kinds: [THREAD], since}, {kinds: [THREAD], since},
{kinds: [COMMENT], "#K": [String(THREAD)], since}, {kinds: [COMMENT], "#K": [String(THREAD)], since},
{kinds: [MESSAGE], "#~": rooms, since}, {kinds: [MESSAGE], "#h": rooms, since},
], ],
}) })
}) })

View File

@@ -13,7 +13,7 @@
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import type {Editor} from "svelte-tiptap" import type {Editor} from "svelte-tiptap"
import {page} from "$app/stores" 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 type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, DELETE} from "@welshman/util" import {createEvent, DELETE} from "@welshman/util"
import {formatTimestampAsDate, publishThunk} from "@welshman/app" import {formatTimestampAsDate, publishThunk} from "@welshman/app"
@@ -32,12 +32,12 @@
userSettingValues, userSettingValues,
userMembership, userMembership,
decodeRelay, decodeRelay,
makeChannelId, deriveChannelMessages,
deriveChannel,
GENERAL, GENERAL,
tagRoom, tagRoom,
MESSAGE, MESSAGE,
getMembershipRoomsByUrl, getMembershipRoomsByUrl,
displayRoom,
} from "@app/state" } from "@app/state"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
import {addRoomMembership, removeRoomMembership, subscribePersistent} from "@app/commands" import {addRoomMembership, removeRoomMembership, subscribePersistent} from "@app/commands"
@@ -46,7 +46,7 @@
const {room = GENERAL} = $page.params const {room = GENERAL} = $page.params
const content = popKey<string>("content") || "" const content = popKey<string>("content") || ""
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const channel = deriveChannel(makeChannelId(url, room)) const events = deriveChannelMessages(url, room)
const assertEvent = (e: any) => e as TrustedEvent const assertEvent = (e: any) => e as TrustedEvent
@@ -80,7 +80,7 @@
let previousDate let previousDate
let previousPubkey 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 {id, pubkey, created_at} = event
const date = formatTimestampAsDate(created_at) const date = formatTimestampAsDate(created_at)
@@ -108,7 +108,7 @@
pullConservatively({ pullConservatively({
relays: [url], relays: [url],
filters: [{kinds: [MESSAGE, DELETE], "#~": [room]}], filters: [{kinds: [MESSAGE, DELETE], "#h": [room]}],
}) })
scroller = createScroller({ scroller = createScroller({
@@ -122,7 +122,7 @@
unsub = subscribePersistent({ unsub = subscribePersistent({
relays: [url], relays: [url],
filters: [{kinds: [MESSAGE], "#~": [room], since: now()}], filters: [{kinds: [MESSAGE], "#h": [room], since: now()}],
}) })
}) })
@@ -142,7 +142,7 @@
<div slot="icon" class="center"> <div slot="icon" class="center">
<Icon icon="hashtag" /> <Icon icon="hashtag" />
</div> </div>
<strong slot="title">{room}</strong> <strong slot="title">{displayRoom(room)}</strong>
<div slot="action" class="row-2"> <div slot="action" class="row-2">
{#if room !== GENERAL} {#if room !== GENERAL}
{#if getMembershipRoomsByUrl(url, $userMembership).includes(room)} {#if getMembershipRoomsByUrl(url, $userMembership).includes(room)}