mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 19:07:06 +00:00
Use lists for room memberships
This commit is contained in:
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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!"})
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user