Refactor: use relay-provided member lists as source of truth #191
@@ -243,7 +243,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $members.length > 0}
|
||||
{#if $members !== undefined && $members.length > 0}
|
||||
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span>Members:</span>
|
||||
@@ -251,6 +251,10 @@
|
||||
</div>
|
||||
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
|
||||
</div>
|
||||
{:else if $members === undefined}
|
||||
<div class="card2 card2-sm bg-base-200 flex items-center gap-4">
|
||||
<span class="text-error">Member list not available from this relay</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card2 card2-sm bg-alt col-4">
|
||||
<strong class="text-lg">Room Settings</strong>
|
||||
|
||||
@@ -73,34 +73,44 @@
|
||||
</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each $members as pubkey (pubkey)}
|
||||
<div class="card2 bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
<div class="relative">
|
||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => removeMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Remove Member
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if $members === undefined}
|
||||
<div class="card2 bg-base-200 p-4">
|
||||
<span class="text-error">Member list not available from this relay</span>
|
||||
</div>
|
||||
{:else if $members.length === 0}
|
||||
<div class="card2 bg-base-200 p-4">
|
||||
<span class="text-base-content/70">No members yet</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#each $members as pubkey (pubkey)}
|
||||
<div class="card2 bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
<div class="relative">
|
||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => removeMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Remove Member
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!$spaceMembers) {
|
||||
addMembers()
|
||||
return
|
||||
}
|
||||
|
||||
const pubkeysSnapshot = $state.snapshot(pubkeys)
|
||||
const nonSpaceMembers = pubkeysSnapshot.filter(pubkey => !$spaceMembers.includes(pubkey))
|
||||
|
||||
|
hodlbod marked this conversation as resolved
Outdated
|
||||
|
||||
@@ -112,46 +112,58 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each $members as pubkey (pubkey)}
|
||||
<div class="card2 card2-sm bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canBan || canUnallow}
|
||||
<div class="relative">
|
||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
{#if canUnallow}
|
||||
<li>
|
||||
<Button onclick={() => unallowMember(pubkey)}>
|
||||
<Icon icon={UserMinus} />
|
||||
Remove User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canBan}
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => banMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Ban User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $members === undefined}
|
||||
<div class="card2 bg-base-200 p-4">
|
||||
<span class="text-error">Member list not available from this space</span>
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Use "space" instead of "relay" Use "space" instead of "relay"
|
||||
</div>
|
||||
{/each}
|
||||
{:else if $members.length === 0}
|
||||
<div class="card2 bg-base-200 p-4">
|
||||
<span class="text-base-content/70">No members yet</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#each $members as pubkey (pubkey)}
|
||||
<div class="card2 card2-sm bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canBan || canUnallow}
|
||||
<div class="relative">
|
||||
<Button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
{#if canUnallow}
|
||||
<li>
|
||||
<Button onclick={() => unallowMember(pubkey)}>
|
||||
<Icon icon={UserMinus} />
|
||||
Remove User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canBan}
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => banMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Ban User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -181,7 +181,11 @@
|
||||
<li>
|
||||
<Button onclick={showMembers}>
|
||||
<Icon icon={UserRounded} />
|
||||
View Members ({$members.length})
|
||||
{#if $members === undefined}
|
||||
View Members
|
||||
|
hodlbod
commented
Remove Remove `(unavailable)`
|
||||
{:else}
|
||||
View Members ({$members.length})
|
||||
{/if}
|
||||
</Button>
|
||||
</li>
|
||||
{#if $userIsAdmin}
|
||||
|
||||
@@ -766,9 +766,10 @@ export const addSpaceMembers = async (
|
||||
pubkeys: string[],
|
||||
): Promise<string | undefined> => {
|
||||
const spaceMembers = get(deriveSpaceMembers(url))
|
||||
|
||||
const results = await Promise.all(
|
||||
pubkeys
|
||||
.filter(pubkey => !spaceMembers.includes(pubkey))
|
||||
.filter(pubkey => !spaceMembers || !spaceMembers.includes(pubkey))
|
||||
.map(pubkey =>
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Don't fail hard, just send all pubkeys (change the filter line to Don't fail hard, just send all pubkeys (change the filter line to `.filter(pubkey => spaceMembers && !spaceMembers.includes(pubkey))`)
|
||||
manageRelay(url, {
|
||||
method: ManagementMethod.AllowPubkey,
|
||||
|
||||
+106
-83
@@ -808,36 +808,49 @@ export const deriveOtherRooms = (url: string) =>
|
||||
|
||||
// Space/room memberships
|
||||
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Let's just keep calling this Same thing in deriveRoomMemberList (which should be deriveRoomMembers). Let's just keep calling this `getSpaceMembers`. I was thinking of using the list directly, but it's more convenient to return the uniqe list of pubkeys. However, you might change the body to:
```typescript
([event]) => uniq(getTagValues("member", event?.tags ?? []))
```
Same thing in deriveRoomMemberList (which should be deriveRoomMembers).
|
||||
const getSpaceMembers = (_url: string, events: TrustedEvent[]) => {
|
||||
const members = new Set<string>()
|
||||
export const deriveSpaceMembers = (url: string) =>
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
This should be renamed to deriveSpaceMembers since it returns a svelte store, not the value itself. This should be renamed to deriveSpaceMembers since it returns a svelte store, not the value itself.
|
||||
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
|
||||
uniq(getTagValues("member", event?.tags ?? [])),
|
||||
)
|
||||
|
||||
for (const event of sortEventsAsc(events)) {
|
||||
if (event.kind === RELAY_MEMBERS) {
|
||||
members.clear()
|
||||
export const deriveRoomMembers = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
|
||||
|
||||
for (const pubkey of uniq(getTagValues("member", event.tags))) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
return derived(deriveEventsForUrl(url, filters), ([event]) =>
|
||||
uniq(getPubkeyTagValues(event?.tags ?? [])),
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
export type BannedPubkeyItem = {
|
||||
pubkey: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
|
||||
|
||||
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||
const store = writable(spaceBannedPubkeyItems.get(url) || [])
|
||||
|
||||
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
|
||||
spaceBannedPubkeyItems.set(url, res.result)
|
||||
store.set(res.result)
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const deriveRoomAdmins = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
|
||||
|
||||
return derived(deriveEventsForUrl(url, filters), $events => {
|
||||
const adminsEvent = first($events)
|
||||
|
||||
if (adminsEvent) {
|
||||
return getPubkeyTagValues(adminsEvent.tags)
|
||||
}
|
||||
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
|
||||
if (event.kind === RELAY_ADD_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === RELAY_REMOVE_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.delete(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(members)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
||||
@@ -876,53 +889,6 @@ const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
||||
return Array.from(members)
|
||||
}
|
||||
|
||||
export const deriveSpaceMembers = (url: string) =>
|
||||
derived(
|
||||
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
|
||||
$events => getSpaceMembers(url, $events),
|
||||
)
|
||||
|
||||
export type BannedPubkeyItem = {
|
||||
pubkey: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
|
||||
|
||||
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||
const store = writable(spaceBannedPubkeyItems.get(url) || [])
|
||||
|
||||
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
|
||||
spaceBannedPubkeyItems.set(url, res.result)
|
||||
store.set(res.result)
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const deriveRoomMembers = (url: string, h: string) => {
|
||||
const filters: Filter[] = [
|
||||
{kinds: [ROOM_MEMBERS], "#d": [h]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
||||
]
|
||||
|
||||
return derived(deriveEventsForUrl(url, filters), $events => getRoomMembers(url, h, $events))
|
||||
}
|
||||
|
||||
export const deriveRoomAdmins = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
|
||||
|
||||
return derived(deriveEventsForUrl(url, filters), $events => {
|
||||
const adminsEvent = first($events)
|
||||
|
||||
if (adminsEvent) {
|
||||
return getPubkeyTagValues(adminsEvent.tags)
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
// Action items (admin review queue)
|
||||
// const pendingJoins: TrustedEvent[] = []
|
||||
|
||||
@@ -1019,19 +985,49 @@ export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
|
||||
})
|
||||
|
||||
export const deriveUserSpaceMembershipStatus = (url: string) => {
|
||||
const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]
|
||||
// Fetch member list and user add/remove events directly in this derivation.
|
||||
const memberListFilters: Filter[] = [{kinds: [RELAY_MEMBERS]}]
|
||||
const userEventFilters: Filter[] = [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}]
|
||||
|
||||
return derived(
|
||||
[
|
||||
pubkey,
|
||||
deriveSpaceMembers(url),
|
||||
deriveEventsForUrl(url, filters),
|
||||
deriveRelaySignedEvents(url, memberListFilters),
|
||||
deriveRelaySignedEvents(url, userEventFilters),
|
||||
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
|
||||
deriveUserIsSpaceAdmin(url),
|
||||
],
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
([$pubkey, $memberListEvents, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
||||
// If admin, always granted.
|
||||
if ($isAdmin) {
|
||||
return MembershipStatus.Granted
|
||||
}
|
||||
|
||||
for (const event of $events) {
|
||||
const membersEvent = $memberListEvents.find(spec({kind: RELAY_MEMBERS}))
|
||||
const memberList = membersEvent ? uniq(getTagValues("member", membersEvent.tags)) : undefined
|
||||
|
||||
let isMember = false
|
||||
|
||||
if (memberList) {
|
||||
// Member list exists - check if user is in it.
|
||||
isMember = memberList.includes($pubkey!)
|
||||
} else {
|
||||
// No member list available - replay the user's add/remove history.
|
||||
for (const event of sortBy(e => e.created_at, $userAddRemoveEvents)) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.kind === RELAY_ADD_MEMBER) {
|
||||
isMember = true
|
||||
} else if (event.kind === RELAY_REMOVE_MEMBER) {
|
||||
isMember = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of $joinLeaveEvents) {
|
||||
// Join events indicate pending or granted status, leave resets to initial.
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
@@ -1057,19 +1053,46 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
||||
)
|
||||
|
||||
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
|
||||
// Fetch the room member list and the current user's add/remove events.
|
||||
const userEventFilters: Filter[] = [{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]}]
|
||||
const joinLeaveFilters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
|
||||
|
||||
return derived(
|
||||
[
|
||||
pubkey,
|
||||
deriveRoomMembers(url, h),
|
||||
deriveEventsForUrl(url, filters),
|
||||
deriveEventsForUrl(url, userEventFilters),
|
||||
deriveEventsForUrl(url, joinLeaveFilters),
|
||||
deriveUserIsRoomAdmin(url, h),
|
||||
],
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
([$pubkey, $memberList, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
||||
// If admin of this room's space, always granted.
|
||||
if ($isAdmin) {
|
||||
return MembershipStatus.Granted
|
||||
}
|
||||
|
||||
for (const event of $events) {
|
||||
let isMember = false
|
||||
|
||||
if ($memberList) {
|
||||
// Member list exists - check if user is in it.
|
||||
isMember = $memberList.includes($pubkey!)
|
||||
} else {
|
||||
// No member list available - replay the user's add/remove history.
|
||||
for (const event of sortEventsAsc($userAddRemoveEvents)) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.kind === ROOM_ADD_MEMBER) {
|
||||
isMember = true
|
||||
} else if (event.kind === ROOM_REMOVE_MEMBER) {
|
||||
isMember = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of $joinLeaveEvents) {
|
||||
// Join events indicate pending or granted status, leave resets to initial.
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ const syncSpace = (url: string) => {
|
||||
{kinds: [MESSAGE, ...CONTENT_KINDS], since, "#h": [room]},
|
||||
makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}),
|
||||
{
|
||||
kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
|
||||
kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE],
|
||||
"#h": [room],
|
||||
},
|
||||
{kinds: [PollResponse], since},
|
||||
@@ -293,15 +293,15 @@ const syncSpace = (url: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const relayKinds = [RELAY_MEMBERS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]
|
||||
const relayKinds = [RELAY_MEMBERS]
|
||||
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
|
||||
const roomMemberKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
|
||||
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
|
||||
|
||||
pullAndListen({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
|
hodlbod marked this conversation as resolved
Outdated
hodlbod
commented
Remove the comment Remove the comment
|
||||
filters: [
|
||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, ...CONTENT_KINDS, MESSAGE]},
|
||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]},
|
||||
makeCommentFilter(CONTENT_KINDS, {since}),
|
||||
{kinds: [PollResponse], since},
|
||||
],
|
||||
|
||||
@@ -79,7 +79,7 @@ export const makeEditor = async ({
|
||||
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
||||
sortFn: ({score = 1, item}) => {
|
||||
const wotScore = getWotGraph().get(item.event.pubkey) || 0
|
||||
const membershipScale = $spaceMembers.includes(item.event.pubkey) ? 2 : 1
|
||||
const membershipScale = $spaceMembers?.includes(item.event.pubkey) ? 2 : 1
|
||||
|
||||
return dec(score) * inc(wotScore / getMaxWot()) * membershipScale
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user
Don't fail here, just skip the space membership step if there's no member list.