Use lists for room memberships

This commit is contained in:
Jon Staab
2024-10-08 15:30:18 -07:00
parent a351d9d792
commit 86c7e6f831
9 changed files with 88 additions and 98 deletions

View File

@@ -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(),
})
}
}

View File

@@ -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" />
</PrimaryNavItem>
{#each $userMembership?.roomsByUrl.keys() || [] as url (url)}
{#each getMembershipUrls($userMembership) as url (url)}
<PrimaryNavItem title={displayRelayUrl(url)} href={makeSpacePath(url)}>
<SpaceAvatar {url} />
</PrimaryNavItem>

View File

@@ -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":

View File

@@ -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<string, string[]>
event?: TrustedEvent
}
export const getMembershipUrls = (list?: List) =>
sort(getRelayTagValues(getListTags(list)))
export type PublishedMembership = Omit<Membership, "event"> & {
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<string, string[]>()
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<PublishedMembership>(repository, {
export const memberships = deriveEventsMapped<PublishedList>(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<SubscribeRequestWithHandlers> = {}) =>
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)

View File

@@ -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) {

View File

@@ -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}
</div>
</div>
{#if $userMembership?.roomsByUrl.has(relay.url)}
{#if getMembershipRoomsByUrl(relay.url, $userMembership)}
<div class="center absolute flex w-full">
<div
class="tooltip relative left-8 top-[38px] h-5 w-5 rounded-full bg-primary"

View File

@@ -1,11 +1,10 @@
<script lang="ts">
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!"})
}

View File

@@ -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 @@
<ul
transition:fly
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
{#if $userMembership?.roomsByUrl.has(url)}
{#if getMembershipRoomsByUrl(url, $userMembership)}
<li class="text-error">
<Button on:click={leaveSpace}>
<Icon icon="exit" />

View File

@@ -28,6 +28,7 @@
GENERAL,
tagRoom,
MESSAGE,
getMembershipRoomsByUrl,
} from "@app/state"
import {addRoomMembership, removeRoomMembership} from "@app/commands"
@@ -46,8 +47,6 @@
let loading = true
let elements: Element[] = []
$: membership = $userMembership?.roomsByUrl.get(url) || []
$: {
elements = []
@@ -90,7 +89,7 @@
<strong>{room}</strong>
</div>
{#if room !== GENERAL}
{#if membership.includes(room)}
{#if getMembershipRoomsByUrl(url, $userMembership).includes(room)}
<Button class="btn btn-neutral btn-sm" on:click={() => removeRoomMembership(url, room)}>
<Icon icon="arrows-a-logout-2" />
Leave Room