diff --git a/src/app/commands.ts b/src/app/commands.ts index a735ceb9..1a5cd702 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -12,6 +12,12 @@ import { createEvent, displayProfile, normalizeRelayUrl, + makeList, + addToListPublicly, + removeFromList, + removeFromListByPredicate, + getListTags, + getRelayTags, } from "@welshman/util" import type {TrustedEvent, EventTemplate} from "@welshman/util" import type {SubscribeRequestWithHandlers} from "@welshman/net" @@ -34,9 +40,11 @@ import { tagPubkey, tagReactionTo, getRelayUrls, + userRelaySelections, userInboxRelaySelections, + nip44EncryptToSelf, } from "@welshman/app" -import {tagRoom, MEMBERSHIPS, INDEXER_RELAYS} from "@app/state" +import {tagRoom, userMembership, MEMBERSHIPS, INDEXER_RELAYS} from "@app/state" // Utils @@ -113,77 +121,71 @@ export const broadcastUserData = async (relays: string[]) => { // List updates -export type ModifyTags = (tags: string[][]) => string[][] +export const addSpaceMembership = async (url: string) => { + const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) + const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf) -export const updateList = async (kind: number, modifyTags: ModifyTags) => { - const $pubkey = pubkey.get()! - const [prev] = repository.query([{kinds: [kind], authors: [$pubkey]}]) - const relays = getWriteRelayUrls(relaySelectionsByPubkey.get().get($pubkey)) - - // Preserve content if we have it - const event = prev - ? {...prev, tags: modifyTags(prev.tags)} - : createEvent(kind, {tags: modifyTags([])}) - - return publishThunk(makeThunk({event, relays})) + return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}) } -export const addSpaceMembership = (url: string) => - updateList(MEMBERSHIPS, (tags: string[][]) => uniqBy(t => t.join(""), [...tags, ["r", url]])) +export const removeSpaceMembership = async (url: string) => { + const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) + const pred = (t: string[]) => equals(["r", url], t) || t[2] !== url + const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf) -export const addRoomMembership = (url: string, room: string) => - updateList(MEMBERSHIPS, (tags: string[][]) => - uniqBy(t => t.join(""), [...tags, tagRoom(room, url)]), - ) + return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}) +} -export const removeSpaceMembership = (url: string) => - updateList(MEMBERSHIPS, (tags: string[][]) => - tags.filter(t => !equals(["r", url], t) && t[2] !== url), - ) +export const addRoomMembership = async (url: string, room: string) => { + const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) + const event = await addToListPublicly(list, tagRoom(room, url)).reconcile(nip44EncryptToSelf) -export const removeRoomMembership = (url: string, room: string) => - updateList(MEMBERSHIPS, (tags: string[][]) => tags.filter(t => !equals(tagRoom(room, url), t))) + return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}) +} -export const unfollowPerson = (pubkey: string) => - updateList(FOLLOWS, tags => tags.filter(t => t[1] !== pubkey)) +export const removeRoomMembership = async (url: string, room: string) => { + const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) + const pred = (t: string[]) => equals(tagRoom(room, url), t) + const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf) -export const followPerson = (pubkey: string) => - updateList(FOLLOWS, tags => append(tagPubkey(pubkey), tags)) + return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}) +} -export const unmutePerson = (pubkey: string) => - updateList(MUTES, tags => tags.filter(t => t[1] !== pubkey)) +export const setRelayPolicy = (url: string, read: boolean, write: boolean) => { + const list = get(userRelaySelections) || makeList({kind: RELAYS}) -export const mutePerson = (pubkey: string) => - updateList(MUTES, tags => append(tagPubkey(pubkey), tags)) + let tags = getRelayTags(getListTags(list)) + .filter(t => normalizeRelayUrl(t[1]) !== url) -export const setRelayPolicy = (url: string, read: boolean, write: boolean) => - updateList(RELAYS, tags => { - tags = tags.filter(t => normalizeRelayUrl(t[1]) !== url) + if (read && write) { + tags.push(["r", url]) + } else if (read) { + tags.push(["r", url, "read"]) + } else if (write) { + tags.push(["r", url, "write"]) + } - if (read && write) { - tags.push(["r", url]) - } else if (read) { - tags.push(["r", url, "read"]) - } else if (write) { - tags.push(["r", url, "write"]) - } - - return tags + return publishThunk({ + event: createEvent(list.kind, {tags}), + relays: ctx.app.router.WriteRelays().getUrls(), }) +} export const setInboxRelayPolicy = (url: string, enabled: boolean) => { - const urls = getRelayUrls(get(userInboxRelaySelections)) + const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS}) // Only update inbox policies if they already exist or we're adding them - if (enabled || urls.includes(url)) { - updateList(INBOX_RELAYS, tags => { - tags = tags.filter(t => normalizeRelayUrl(t[1]) !== url) + if (enabled || getRelayUrls(list).includes(url)) { + let tags = getRelayTags(getListTags(list)) + .filter(t => normalizeRelayUrl(t[1]) !== url) - if (enabled) { - tags.push(["relay", url]) - } + if (enabled) { + tags.push(["relay", url]) + } - return tags + return publishThunk({ + event: createEvent(list.kind, {tags}), + relays: ctx.app.router.WriteRelays().getUrls(), }) } } diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte index 87e66b90..bae79bcd 100644 --- a/src/app/components/PrimaryNav.svelte +++ b/src/app/components/PrimaryNav.svelte @@ -15,7 +15,7 @@ import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import SpaceAdd from "@app/components/SpaceAdd.svelte" import SpaceAvatar from "@app/components/SpaceAvatar.svelte" - import {userMembership} from "@app/state" + import {userMembership, getMembershipUrls} from "@app/state" import {pushModal} from "@app/modal" import {makeSpacePath, getPrimaryNavItemIndex} from "@app/routes" @@ -50,7 +50,7 @@ src={$userProfile?.picture} class="!h-10 !w-10 border border-solid border-base-300" /> - {#each $userMembership?.roomsByUrl.keys() || [] as url (url)} + {#each getMembershipUrls($userMembership) as url (url)} diff --git a/src/app/routes.ts b/src/app/routes.ts index 0067830d..a140f5d0 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -1,6 +1,6 @@ import {nip19} from "nostr-tools" import type {Page} from "@sveltejs/kit" -import {userMembership, makeChatId, decodeNRelay} from "@app/state" +import {userMembership, makeChatId, decodeNRelay, getMembershipUrls} from "@app/state" export const makeSpacePath = (url: string, extra = "") => { let path = `/spaces/${nip19.nrelayEncode(url)}` @@ -17,7 +17,7 @@ export const makeChatPath = (pubkeys: string[]) => `/home/${makeChatId(pubkeys)} export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1] export const getPrimaryNavItemIndex = ($page: Page) => { - const urls = Array.from(userMembership.get()?.roomsByUrl.keys() || []) + const urls = getMembershipUrls(userMembership.get()) switch (getPrimaryNavItem($page)) { case "discover": diff --git a/src/app/state.ts b/src/app/state.ts index 74031e17..1e8bb9f8 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -32,8 +32,11 @@ import { getPubkeyTagValues, isHashedEvent, displayProfile, + readList, + getListTags, + asDecryptedEvent, } from "@welshman/util" -import type {TrustedEvent, SignedEvent} from "@welshman/util" +import type {TrustedEvent, SignedEvent, PublishedList, List} from "@welshman/util" import {Nip59} from "@welshman/signer" import { pubkey, @@ -211,33 +214,17 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => { // Membership -export type Membership = { - roomsByUrl: Map - event?: TrustedEvent -} +export const getMembershipUrls = (list?: List) => + sort(getRelayTagValues(getListTags(list))) -export type PublishedMembership = Omit & { - event: TrustedEvent -} +export const getMembershipRoomsByUrl = (url: string, list?: List) => + sort(getListTags(list).filter(t => t[0] === '~' && t[2] === url).map(nth(1))) -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, { +export const memberships = deriveEventsMapped(repository, { filters: [{kinds: [MEMBERSHIPS]}], - eventToItem: readMembership, itemToEvent: item => item.event, + eventToItem: (event: TrustedEvent) => + readList(asDecryptedEvent(event)), }) export const { @@ -247,7 +234,7 @@ export const { } = collection({ name: "memberships", store: memberships, - getKey: membership => membership.event.pubkey, + getKey: list => list.event.pubkey, load: (pubkey: string, request: Partial = {}) => load({ ...request, @@ -460,7 +447,7 @@ export const roomsByUrl = derived(channels, $channels => { export const userMembership = withGetter( derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => { - if (!$pubkey) return null + if (!$pubkey) return undefined loadMembership($pubkey) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c5335299..5eaf3b86 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -72,7 +72,7 @@ Object.assign(window, {get, ...app, ...state}) if (!db) { - await initStorage("flotilla", 4, { + ready = initStorage("flotilla", 4, { events: storageAdapters.fromRepository(repository, {throttle: 300}), relays: {keyPath: "url", store: throttled(1000, relays)}, handles: {keyPath: "nip05", store: throttled(1000, handles)}, @@ -80,7 +80,7 @@ freshness: storageAdapters.fromObjectStore(freshness, {throttle: 1000}), plaintext: storageAdapters.fromObjectStore(plaintext, {throttle: 1000}), tracker: storageAdapters.fromTracker(tracker, {throttle: 1000}), - }) + }).then(() => sleep(300)) repository.on("update", ({added}) => { for (const event of added) { diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte index 8c22a3a9..7bd6082c 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -6,7 +6,7 @@ import {createScroller} from "@lib/html" import Icon from "@lib/components/Icon.svelte" import {makeSpacePath} from "@app/routes" - import {userMembership, discoverRelays} from "@app/state" + import {userMembership, discoverRelays, getMembershipRoomsByUrl} from "@app/state" let term = "" let limit = 20 @@ -58,7 +58,7 @@ {/if} - {#if $userMembership?.roomsByUrl.has(relay.url)} + {#if getMembershipRoomsByUrl(relay.url, $userMembership)}
- import {always} from "@welshman/lib" - import {getListTags, getPubkeyTagValues, MUTES} from "@welshman/util" - import {userMutes, tagPubkey} from "@welshman/app" + import {always, ctx} from "@welshman/lib" + import {getListTags, createEvent, getPubkeyTagValues, MUTES} from "@welshman/util" + import {userMutes, tagPubkey, publishThunk} from "@welshman/app" import Field from "@lib/components/Field.svelte" import Button from "@lib/components/Button.svelte" import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte" - import {updateList} from "@app/commands" import {pushToast} from "@app/toast" let mutedPubkeys = getPubkeyTagValues(getListTags($userMutes)) @@ -15,7 +14,10 @@ } const onSubmit = async () => { - await updateList(MUTES, always(mutedPubkeys.map(tagPubkey))) + publishThunk({ + event: createEvent(MUTES, {tags: mutedPubkeys.map(tagPubkey)}), + relays: ctx.app.router.WriteRelays().getUrls(), + }) pushToast({message: "Your settings have been saved!"}) } diff --git a/src/routes/spaces/[nrelay]/+layout.svelte b/src/routes/spaces/[nrelay]/+layout.svelte index 4c4e659f..9c16444b 100644 --- a/src/routes/spaces/[nrelay]/+layout.svelte +++ b/src/routes/spaces/[nrelay]/+layout.svelte @@ -16,7 +16,7 @@ import SpaceExit from "@app/components/SpaceExit.svelte" import SpaceJoin from "@app/components/SpaceJoin.svelte" import RoomCreate from "@app/components/RoomCreate.svelte" - import {userMembership, pullConservatively, roomsByUrl, decodeNRelay, GENERAL, MESSAGE} from "@app/state" + import {getMembershipRoomsByUrl, userMembership, pullConservatively, roomsByUrl, decodeNRelay, GENERAL, MESSAGE} from "@app/state" import {pushModal} from "@app/modal" import {makeSpacePath} from "@app/routes" @@ -48,7 +48,7 @@ let showMenu = false $: url = decodeNRelay($page.params.nrelay) - $: rooms = sort($userMembership?.roomsByUrl?.get(url) || []) + $: rooms = getMembershipRoomsByUrl(url, $userMembership) $: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room)) onMount(() => { @@ -74,7 +74,7 @@
{#if room !== GENERAL} - {#if membership.includes(room)} + {#if getMembershipRoomsByUrl(url, $userMembership).includes(room)}