feat(rbac): implement NIP-29 room roles and permission gating (#47) #220

Open
Khushvendra wants to merge 5 commits from Khushvendra/flotilla:feat/nip29-rbac-47 into dev
16 changed files with 865 additions and 177 deletions
+6 -4
View File
@@ -3,7 +3,7 @@
import type {Snippet} from "svelte"
import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT, ManagementMethod} from "@welshman/util"
import {COMMENT, ManagementMethod, getTagValue} from "@welshman/util"
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
@@ -17,7 +17,8 @@
import Report from "@app/components/Report.svelte"
import EventShare from "@app/components/EventShare.svelte"
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {hasNip29, deriveUserIsSpaceAdmin} from "@app/core/state"
import {deriveHasPermission, ROOM_PERMISSION_DELETE_EVENT} from "@app/core/roles"
import {hasNip29} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {makeSpaceChatPath} from "@app/util/routes"
@@ -33,7 +34,8 @@
const {url, noun, event, onClick, customActions}: Props = $props()
const isRoot = event.kind !== COMMENT
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const h = getTagValue("h", event.tags)
const canDelete = deriveHasPermission(url, h, ROOM_PERMISSION_DELETE_EVENT)
const report = () => pushModal(Report, {url, event})
1
@@ -107,7 +109,7 @@
Report Content
</Button>
</li>
{#if $userIsAdmin}
{#if $canDelete}
<li>
<Button class="text-error" onclick={showAdminDelete}>
<Icon size={4} icon={TrashBin2} />
+36 -12
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {readable} from "svelte/store"
import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util"
import {
@@ -28,7 +29,14 @@
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte"
import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state"
import RoleBadge from "@app/components/RoleBadge.svelte"
import {pubkeyLink, deriveSpaceBannedPubkeyItems} from "@app/core/state"
import {
deriveUserHasSpacePermission,
deriveSpaceMemberRoles,
ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_BAN_USER,
} from "@app/core/roles"
import {addSpaceMembers} from "@app/core/commands"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
@@ -43,10 +51,16 @@
const profile = deriveProfile(pubkey, removeUndefined([url]))
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const canBan = url ? deriveUserHasSpacePermission(url, ROOM_PERMISSION_BAN_USER) : readable(false)
const canRestore = url
? deriveUserHasSpacePermission(url, ROOM_PERMISSION_ADD_MEMBER)
: readable(false)
const bannedPubkeys = url ? deriveSpaceBannedPubkeyItems(url) : undefined
const assignedRoles = url ? deriveSpaceMemberRoles(url, pubkey) : readable([])
const isBanned = $derived($bannedPubkeys?.some(item => item.pubkey === pubkey) ?? false)
const back = () => history.back()
@@ -105,7 +119,7 @@
<div class="flex flex-col gap-4">
<div class="flex justify-between">
<Profile showPubkey avatarSize={14} {pubkey} {url} />
{#if $profile || $userIsAdmin}
{#if $profile || $canBan || $canRestore}
<div class="relative">
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
<Icon icon={MenuDots} />
@@ -123,22 +137,22 @@
</Button>
</li>
{/if}
{#if $userIsAdmin}
{#if isBanned}
{#if isBanned}
{#if $canRestore}
<li>
<Button onclick={restoreMember}>
<Icon icon={Restart} />
Restore User
</Button>
</li>
{:else}
<li>
<Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} />
Ban User
</Button>
</li>
{/if}
{:else if $canBan}
<li>
<Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} />
Ban User
</Button>
</li>
{/if}
</ul>
</Popover>
@@ -147,6 +161,16 @@
{/if}
</div>
<ProfileInfo {pubkey} {url} />
{#if $assignedRoles.length > 0}
<div class="card2 card2-sm bg-alt col-3">
<h3 class="text-lg font-semibold">Roles</h3>
<div class="flex flex-wrap gap-2">
{#each $assignedRoles as role (role.name)}
<RoleBadge {role} class="badge-md" />
{/each}
</div>
</div>
{/if}
<ProfileBadges {pubkey} {url} />
</div>
</ModalBody>
+2 -1
View File
@@ -23,7 +23,8 @@
import Icon from "@lib/components/Icon.svelte"
import Reaction from "@app/components/Reaction.svelte"
import ReportDetails from "@app/components/ReportDetails.svelte"
import {REACTION_KINDS, deriveUserIsSpaceAdmin} from "@app/core/state"
import {deriveUserIsSpaceAdmin} from "@app/core/roles"
import {REACTION_KINDS} from "@app/core/state"
import {pushModal} from "@app/util/modal"
interface Props {
+1 -1
View File
@@ -12,7 +12,7 @@
import Popover from "@lib/components/Popover.svelte"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import {deriveUserIsSpaceAdmin} from "@app/core/state"
import {deriveUserIsSpaceAdmin} from "@app/core/roles"
import {publishDelete, canEnforceNip70} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
import cx from "classnames"
import type {RoleDefinition} from "@app/core/roles"
import {roleColorToCSS} from "@app/core/roles"
type Props = {
role: RoleDefinition
class?: string
}
const {role, ...props}: Props = $props()
const style = $derived(
role.color === undefined
? ""
: `color: ${roleColorToCSS(role.color)}; border-color: ${roleColorToCSS(role.color)};`,
)
const className = $derived(cx("badge badge-outline badge-sm", props.class))
</script>
<span class={className} {style}>
{role.label || role.name}
</span>
+36 -7
View File
@@ -27,14 +27,22 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import RoleBadge from "@app/components/RoleBadge.svelte"
import RoomMembers from "@app/components/RoomMembers.svelte"
import RoomEdit from "@app/components/RoomEdit.svelte"
import RoomName from "@app/components/RoomName.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
import {
deriveRoom,
deriveRoomMembers,
deriveRoomRoleDefinitions,
deriveUserIsRoomAdmin,
deriveHasPermission,
getRolePermissionsLabel,
getRoleAccessLabel,
ROOM_PERMISSION_EDIT_META,
} from "@app/core/roles"
import {
deriveRoom,
deriveUserRoomMembershipStatus,
deriveUserRooms,
deriveShouldNotify,
@@ -58,7 +66,9 @@
const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h)
const roleDefinitions = deriveRoomRoleDefinitions(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const canEditMetadata = deriveHasPermission(url, h, ROOM_PERMISSION_EDIT_META)
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
const userRooms = deriveUserRooms(url)
1
@@ -152,7 +162,7 @@
<ul
transition:fly
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
{#if $userIsAdmin}
{#if $canEditMetadata}
<li>
<Button onclick={startEdit}>
<Icon icon={Pen} />
@@ -243,17 +253,36 @@
{/if}
</div>
</div>
{#if $members !== undefined && $members.length > 0}
{#if $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>
<ProfileCircles pubkeys={$members} />
<ProfileCircles pubkeys={$members.map(member => member.pubkey)} />
</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>
{/if}
{#if $userIsAdmin && $roleDefinitions.length > 0}
<div class="card2 card2-sm bg-alt col-4">
<strong class="text-lg">Role Definitions</strong>
<div class="flex flex-col gap-2">
{#each $roleDefinitions as role (role.name)}
<div class="rounded-box bg-base-300 p-3 flex flex-col gap-2">
<div class="flex items-center gap-2">
<RoleBadge {role} class="badge-md" />
{#if role.order !== undefined}
<span class="text-xs opacity-70">Order {role.order}</span>
{/if}
</div>
{#if role.permissions.length > 0}
<p class="text-xs opacity-75">Permissions: {getRolePermissionsLabel(role)}</p>
{/if}
{#if role.access.size > 0}
<p class="text-xs opacity-75">Access: {getRoleAccessLabel(role)}</p>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<div class="card2 card2-sm bg-alt col-4">
1
+1 -1
View File
@@ -13,7 +13,7 @@
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {deriveUserIsSpaceAdmin} from "@app/core/state"
import {deriveUserIsSpaceAdmin} from "@app/core/roles"
type Props = {
url: string
+58 -32
View File
@@ -16,9 +16,17 @@
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import Profile from "@app/components/Profile.svelte"
import RoleBadge from "@app/components/RoleBadge.svelte"
import RoomName from "@app/components/RoomName.svelte"
import RoomMembersAdd from "@app/components/RoomMembersAdd.svelte"
import {deriveRoom, deriveRoomMembers, deriveUserIsRoomAdmin} from "@app/core/state"
import {
deriveRoomMembers,
deriveGroupedRoomMembers,
deriveHasPermission,
ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_REMOVE_MEMBER,
} from "@app/core/roles"
import {deriveRoom} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
@@ -31,7 +39,9 @@
const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const memberGroups = deriveGroupedRoomMembers(url, h)
const canAddMembers = deriveHasPermission(url, h, ROOM_PERMISSION_ADD_MEMBER)
const canRemoveMembers = deriveHasPermission(url, h, ROOM_PERMISSION_REMOVE_MEMBER)
const back = () => history.back()
1
@@ -73,42 +83,58 @@
</ModalSubtitle>
</ModalHeader>
<div class="flex flex-col gap-2">
{#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}
{#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>
{#each $memberGroups as group (group.key)}
<div class="pt-2 pb-1">
{#if group.role}
<RoleBadge role={group.role} class="badge-md" />
{:else}
<span class="text-sm font-semibold opacity-75">Members</span>
{/if}
</div>
{#each group.members as member (member.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={member.pubkey} {url} />
{#if member.roles.length > 0}
<div class="mt-1 flex flex-wrap gap-1">
{#each member.roles as role (role.name)}
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

This exists already in @welshman/lib

This exists already in @welshman/lib
<RoleBadge {role} />
{/each}
</div>
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

This whole thing could probably go in the roles.ts file. See if you can define types that make sense in this context. The fewer named (and especially ad-hoc) types, the easier the code is to read.

This whole thing could probably go in the `roles.ts` file. See if you can define types that make sense in this context. The fewer named (and especially ad-hoc) types, the easier the code is to read.
{/if}
</div>
{#if $canRemoveMembers}
<div class="relative">
<Button
class="btn btn-circle btn-ghost btn-sm"
onclick={() => toggleMenu(member.pubkey)}>
<Icon icon={MenuDots} />
</Button>
{#if menuPubkey === member.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(member.pubkey)}>
<Icon icon={MinusCircle} />
Remove Member
</Button>
</li>
</ul>
</Popover>
{/if}
</div>
{/if}
</div>
</div>
</div>
{/each}
{/each}
{/if}
</div>
@@ -118,7 +144,7 @@
<Icon icon={AltArrowLeft} />
Go back
</Button>
{#if $userIsAdmin}
{#if $canAddMembers}
<Button class="btn btn-primary" onclick={addMember}>
<Icon icon={AddCircle} />
Add members
+3 -3
View File
@@ -18,7 +18,7 @@
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import ProfileLatest from "@app/components/ProfileLatest.svelte"
import {deriveUserIsSpaceAdmin} from "@app/core/state"
import {deriveUserHasSpacePermission, ROOM_PERMISSION_EDIT_META} from "@app/core/roles"
import {pushModal} from "@app/util/modal"
type Props = {
@@ -28,7 +28,7 @@
const {url}: Props = $props()
const relay = deriveRelay(url)
const owner = $derived($relay?.pubkey)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const canEdit = deriveUserHasSpacePermission(url, ROOM_PERMISSION_EDIT_META)
const back = () => history.back()
@@ -54,7 +54,7 @@
<p class="ellipsize text-sm opacity-75">{displayRelayUrl(url)}</p>
</div>
</div>
{#if $userIsAdmin}
{#if $canEdit}
<Button class="btn btn-primary" onclick={startEdit}>
<Icon icon={Pen} />
Edit
+75 -52
View File
@@ -17,16 +17,20 @@
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RoleBadge from "@app/components/RoleBadge.svelte"
import RelayName from "@app/components/RelayName.svelte"
import Profile from "@app/components/Profile.svelte"
import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte"
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
import {deriveSpaceMembers, deriveSpaceBannedPubkeyItems} from "@app/core/state"
import {
deriveSpaceMembers,
deriveSpaceBannedPubkeyItems,
deriveUserIsSpaceAdmin,
deriveGroupedSpaceMembers,
deriveSupportedMethods,
} from "@app/core/state"
deriveUserHasSpacePermission,
ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_REMOVE_MEMBER,
ROOM_PERMISSION_BAN_USER,
} from "@app/core/roles"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
@@ -38,10 +42,17 @@
const members = deriveSpaceMembers(url)
const bans = deriveSpaceBannedPubkeyItems(url)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const memberGroups = deriveGroupedSpaceMembers(url, members)
const canAddMember = deriveUserHasSpacePermission(url, ROOM_PERMISSION_ADD_MEMBER)
const canBanByPermission = deriveUserHasSpacePermission(url, ROOM_PERMISSION_BAN_USER)
const canUnallowByPermission = deriveUserHasSpacePermission(url, ROOM_PERMISSION_REMOVE_MEMBER)
Review

I think this is mixing different concerns, the ability for a relay admin to ban/add relay members has nothing to do with a group admin's ability to ban/add group members. These actions should be based exclusively on nip 86 management methods (or equivalent rbac that we're planning to add to nip 43). Looking more closely at this whole PR, I am afraid we're creating a confusing mess — room roles should only apply to rooms, not spaces.

I think this is mixing different concerns, the ability for a relay admin to ban/add relay members has nothing to do with a group admin's ability to ban/add group members. These actions should be based exclusively on nip 86 management methods (or equivalent rbac that we're planning to add to nip 43). Looking more closely at this whole PR, I am afraid we're creating a confusing mess — room roles should only apply to rooms, not spaces.
const supportedMethods = deriveSupportedMethods(url)
const canBan = $derived($supportedMethods.includes(ManagementMethod.BanPubkey))
const canUnallow = $derived($supportedMethods.includes(ManagementMethod.UnallowPubkey))
const canBan = $derived(
$canBanByPermission && $supportedMethods.includes(ManagementMethod.BanPubkey),
)
const canUnallow = $derived(
$canUnallowByPermission && $supportedMethods.includes(ManagementMethod.UnallowPubkey),
)
const back = () => history.back()
2
@@ -104,7 +115,7 @@
<ModalTitle>Members</ModalTitle>
<ModalSubtitle>of <RelayName {url} class="text-primary" /></ModalSubtitle>
</ModalHeader>
{#if $userIsAdmin}
{#if canBan || canUnallow}
{#if $bans.length > 0}
<Button class="btn btn-neutral" onclick={showBannedPubkeyItems}>
Banned users ({$bans.length})
@@ -112,56 +123,68 @@
{/if}
{/if}
<div class="flex flex-col gap-2">
{#if $members === undefined}
<div class="card2 bg-base-200 p-4">
<span class="text-error">Member list not available from this space</span>
</div>
{:else if $members.length === 0}
{#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>
{#each $memberGroups as group (group.key)}
<div class="pt-2 pb-1">
{#if group.role}
<RoleBadge role={group.role} class="badge-md" />
{:else}
<span class="text-sm font-semibold opacity-75">Members</span>
{/if}
</div>
{#each group.members as member (member.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={member.pubkey} {url} />
{#if member.roles.length > 0}
<div class="mt-1 flex flex-wrap gap-1">
{#each member.roles as role (role.name)}
<RoleBadge {role} />
{/each}
</div>
{/if}
</div>
{/if}
{#if canBan || canUnallow}
<div class="relative">
<Button
class="btn btn-circle btn-ghost btn-sm"
onclick={() => toggleMenu(member.pubkey)}>
<Icon icon={MenuDots} />
</Button>
{#if menuPubkey === member.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(member.pubkey)}>
<Icon icon={UserMinus} />
Remove User
</Button>
</li>
{/if}
{#if canBan}
<li>
<Button class="text-error" onclick={() => banMember(member.pubkey)}>
<Icon icon={MinusCircle} />
Ban User
</Button>
</li>
{/if}
</ul>
</Popover>
{/if}
</div>
{/if}
</div>
</div>
</div>
{/each}
{/each}
{/if}
</div>
@@ -171,7 +194,7 @@
<Icon icon={AltArrowLeft} />
Go back
</Button>
{#if $userIsAdmin}
{#if $canAddMember}
<Button class="btn btn-primary" onclick={addMember}>
<Icon icon={AddCircle} />
Add members
+2 -1
View File
@@ -16,7 +16,8 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Profile from "@app/components/Profile.svelte"
import {deriveSpaceBannedPubkeyItems, deriveSupportedMethods} from "@app/core/state"
import {deriveSupportedMethods} from "@app/core/roles"
import {deriveSpaceBannedPubkeyItems} from "@app/core/state"
import {addSpaceMembers} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
+1 -1
View File
@@ -40,6 +40,7 @@
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
import VoiceWidget from "@app/components/VoiceWidget.svelte"
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
import {deriveUserIsSpaceAdmin} from "@app/core/roles"
import {
ENABLE_ZAPS,
CONTENT_KINDS,
@@ -50,7 +51,6 @@
userSpaceUrls,
hasNip29,
deriveUserCanCreateRoom,
deriveUserIsSpaceAdmin,
deriveEventsForUrl,
deriveSpaceActionItems,
notificationSettings,
+576
View File
@@ -0,0 +1,576 @@
import {derived, readable, type Readable} from "svelte/store"
import {first, memoize, removeUndefined, simpleCache, sortBy, uniq} from "@welshman/lib"
import {pubkey, manageRelay} from "@welshman/app"
import {deriveEventsForUrl} from "@app/core/state"
import {
ManagementMethod,
ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER,
ROOM_EDIT_META,
ROOM_DELETE_EVENT,
ROOM_ADMINS,
ROOM_MEMBERS,
getTagValue,
isRelayUrl,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
export const ROOM_ROLES = 39003
export const ROOM_PERMISSION_ADD_MEMBER = ROOM_ADD_MEMBER
export const ROOM_PERMISSION_REMOVE_MEMBER = ROOM_REMOVE_MEMBER
export const ROOM_PERMISSION_EDIT_META = ROOM_EDIT_META
export const ROOM_PERMISSION_DELETE_EVENT = ROOM_DELETE_EVENT
export const ROOM_PERMISSION_BAN_USER = 9009
const ALL_ROOM_PERMISSIONS = [
ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_REMOVE_MEMBER,
ROOM_PERMISSION_EDIT_META,
ROOM_PERMISSION_DELETE_EVENT,
ROOM_PERMISSION_BAN_USER,
]
export const deriveSupportedMethods = simpleCache(([url]: [string]) =>
readable<ManagementMethod[]>([], set => {
manageRelay(url, {
method: ManagementMethod.SupportedMethods,
params: [],
}).then(({result = []}) => set(result))
}),
)
export type RoleAccess = "read" | "write" | "join"
export type RoleDefinition = {
name: string
label?: string
color?: number
order?: number
permissions: number[]
access: Set<RoleAccess>
}
export type RoomRoles = {
url: string
h: string
roles: Map<string, RoleDefinition>
}
export type RoomMember = {
pubkey: string
roles: string[]
}
type MemberRoleInfo = {
pubkey: string
roles: RoleDefinition[]
primaryRole?: RoleDefinition
}
type MemberRoleGroup = {
key: string
role?: RoleDefinition
members: MemberRoleInfo[]
}
type ParsedRoleState = {
roles: Map<string, RoleDefinition>
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

These types are the place where the most time should be spent on the next revision. These types aren't bad, but I think we could cut probably ~100 LOC by making them cleaner. I don't have time right now to give it the necessary thought though.

These types are the place where the most time should be spent on the next revision. These types aren't bad, but I think we could cut probably ~100 LOC by making them cleaner. I don't have time right now to give it the necessary thought though.
Outdated
Review

ill have a look into this (less code = always better)

ill have a look into this (less code = always better)
hasPermissionTags: boolean
}
type SpaceRoleState = {
hasPermissionTags: boolean
userPermissions: Set<number>
memberRoles: Map<string, RoleDefinition[]>
}
const makeRoleDefinition = (name: string): RoleDefinition => ({
name,
permissions: [],
access: new Set<RoleAccess>(),
})
const ensureRole = (roles: Map<string, RoleDefinition>, roleName: string) => {
if (!roles.has(roleName)) {
roles.set(roleName, makeRoleDefinition(roleName))
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

This function exists already in core/state

This function exists already in core/state
}
return roles.get(roleName)!
}
const asNumber = (value: string | undefined) => {
const n = parseInt(value || "")
return isNaN(n) ? undefined : n
}
const asAccess = (value: string | undefined): RoleAccess | undefined => {
if (value === "read" || value === "write" || value === "join") {
return value
}
}
const parseRoleState = (event?: TrustedEvent): ParsedRoleState => {
const roles = new Map<string, RoleDefinition>()
let hasPermissionTags = false
let activeRoleName: string | undefined
for (const tag of event?.tags || []) {
const [name, firstValue, secondValue] = tag
if (name === "role") {
if (firstValue) {
activeRoleName = firstValue
ensureRole(roles, firstValue)
}
continue
}
if (
!["role-label", "role-color", "role-order", "role-permission", "role-access"].includes(name)
) {
continue
}
const hasExplicitRole = Boolean(firstValue && secondValue !== undefined)
const roleName = hasExplicitRole ? firstValue : activeRoleName
const value = hasExplicitRole ? secondValue : firstValue
if (!roleName || !value) {
continue
}
const role = ensureRole(roles, roleName)
if (name === "role-label") {
role.label = value
continue
}
if (name === "role-color") {
const color = asNumber(value)
if (color !== undefined && color >= 0 && color <= 255) {
role.color = color
}
continue
}
if (name === "role-order") {
const order = asNumber(value)
if (order !== undefined) {
role.order = order
}
continue
}
if (name === "role-permission") {
const permission = asNumber(value)
hasPermissionTags = true
if (permission !== undefined && !role.permissions.includes(permission)) {
role.permissions.push(permission)
}
continue
}
if (name === "role-access") {
const access = asAccess(value)
if (access) {
role.access.add(access)
}
}
}
return {roles, hasPermissionTags}
}
const getRoleTokens = (tag: string[]) => {
const roles: string[] = []
for (const value of tag.slice(2)) {
if (!value || isRelayUrl(value)) {
continue
}
roles.push(value)
}
return uniq(roles)
}
export const parseRoomMembers = (tags: string[][]) => {
const byPubkey = new Map<string, Set<string>>()
for (const tag of tags) {
if (tag[0] !== "p" || !tag[1]) {
continue
}
if (!byPubkey.has(tag[1])) {
byPubkey.set(tag[1], new Set<string>())
}
const roles = byPubkey.get(tag[1])!
for (const role of getRoleTokens(tag)) {
roles.add(role)
}
}
return Array.from(byPubkey.entries()).map(([pubkey, roles]) => ({
pubkey,
roles: Array.from(roles),
}))
}
export const deriveRoomMembers = (url: string, h: string) =>
derived(deriveEventsForUrl(url, [{kinds: [ROOM_MEMBERS], "#d": [h]}]), ([event]) =>
parseRoomMembers(event?.tags || []),
)
export const deriveRoomAdmins = (url: string, h: string) =>
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [h]}]), ([event]) =>
parseRoomMembers(event?.tags || []),
)
const deriveRoomRoleState = simpleCache(([url, h]: [string, string]) =>
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ROLES], "#d": [h]}]), ([event]) =>
parseRoleState(event),
),
)
export const deriveRoomRoles = (url: string, h: string) =>
derived(deriveRoomRoleState(url, h), $state => ({
url,
h,
roles: $state.roles,
}))
const getMember = (members: RoomMember[], targetPubkey: string) =>
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

This could be simplified to

derived(
  deriveEventsForUrl(url, [{kinds: [ROOM_MEMBERS], '#d': [h]}]),
  ([event]) => {

The reason being that Repository handles replaceable event deduplication.

This could be simplified to ``` derived( deriveEventsForUrl(url, [{kinds: [ROOM_MEMBERS], '#d': [h]}]), ([event]) => { ``` The reason being that Repository handles replaceable event deduplication.
members.find(member => member.pubkey === targetPubkey)
const getResolvedRoles = (rolesByName: Map<string, RoleDefinition>, roleNames: string[]) =>
removeUndefined(roleNames.map(name => rolesByName.get(name)))
export const sortRolesDesc = <T extends {order?: number}>(items: T[]) =>
sortBy(item => -(item.order ?? -Infinity), items)
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

Same thing here, which means you can remove deriveRoomListStore

Same thing here, which means you can remove `deriveRoomListStore`
export const getRolePermissionsLabel = (role: RoleDefinition) => role.permissions.join(", ")
export const getRoleAccessLabel = (role: RoleDefinition) => Array.from(role.access).join(", ")
const toMemberRoleInfo = (pubkey: string, roles: RoleDefinition[]): MemberRoleInfo => {
const sortedRoles = sortRolesDesc(roles)
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

Same here, no need for getLatest, just use the zero or one event provided. That means you can remove getLatestEventByKind too

Same here, no need for getLatest, just use the zero or one event provided. That means you can remove `getLatestEventByKind` too
return {
pubkey,
roles: sortedRoles,
primaryRole: first(sortedRoles),
}
}
const sortMemberRoleInfos = (members: MemberRoleInfo[]) =>
sortBy(member => -(member.primaryRole?.order ?? -Infinity), members)
const groupMemberRoleInfos = (members: MemberRoleInfo[]) => {
const byRole = new Map<string, MemberRoleGroup>()
const ungrouped: MemberRoleGroup = {
key: "members",
members: [],
}
for (const member of sortMemberRoleInfos(members)) {
if (!member.primaryRole) {
ungrouped.members.push(member)
continue
}
const key = member.primaryRole.name
if (!byRole.has(key)) {
byRole.set(key, {
key,
role: member.primaryRole,
members: [],
})
}
byRole.get(key)!.members.push(member)
}
const groups = sortBy(group => -(group.role?.order ?? -Infinity), Array.from(byRole.values()))
if (ungrouped.members.length > 0) {
groups.push(ungrouped)
}
return groups
}
const deriveRoomRoleAssignments = simpleCache(([url, h]: [string, string]) =>
derived(
[deriveRoomRoleState(url, h), deriveRoomMembers(url, h), deriveRoomAdmins(url, h)],
([$rolesState, $members, $admins]) => ({
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

This is redundant with deriveSupportedMethods in core/state.

This is redundant with `deriveSupportedMethods` in core/state.
rolesState: $rolesState,
members: $members,
admins: $admins,
}),
),
)
export const deriveUserRoles = (url: string, h: string, targetPubkey: string) =>
derived(deriveRoomRoleAssignments(url, h), ({members, admins}) => {
const member = getMember(members, targetPubkey)
const admin = getMember(admins, targetPubkey)
return uniq([...(member?.roles || []), ...(admin?.roles || [])])
})
export const deriveUserPermissions = (url: string, h: string) =>
derived(
[pubkey, deriveRoomRoleAssignments(url, h)],
([$pubkey, {rolesState, members, admins}]) => {
const permissions = new Set<number>()
if (!$pubkey) {
return permissions
}
const member = getMember(members, $pubkey)
const admin = getMember(admins, $pubkey)
const assignedRoleNames = uniq([...(member?.roles || []), ...(admin?.roles || [])])
if (!rolesState.hasPermissionTags) {
if (admin) {
for (const permission of ALL_ROOM_PERMISSIONS) {
permissions.add(permission)
}
}
return permissions
}
for (const role of getResolvedRoles(rolesState.roles, assignedRoleNames)) {
for (const permission of role.permissions) {
permissions.add(permission)
}
}
return permissions
},
)
const mergeRoleDefinitions = (left: RoleDefinition[], right: RoleDefinition[]) => {
const merged = new Map<string, RoleDefinition>()
for (const role of [...left, ...right]) {
if (!merged.has(role.name)) {
merged.set(role.name, {
name: role.name,
label: role.label,
color: role.color,
order: role.order,
permissions: [...role.permissions],
access: new Set(role.access),
})
continue
}
const existing = merged.get(role.name)!
if (existing.label === undefined) {
existing.label = role.label
}
if (existing.color === undefined) {
existing.color = role.color
}
if (existing.order === undefined) {
existing.order = role.order
}
existing.permissions = uniq([...existing.permissions, ...role.permissions])
for (const access of role.access) {
existing.access.add(access)
}
}
return sortBy(role => role.name, Array.from(merged.values()))
}
const deriveSpaceRoleState = simpleCache(([url]: [string]) =>
derived(
[pubkey, deriveEventsForUrl(url, [{kinds: [ROOM_ROLES, ROOM_MEMBERS, ROOM_ADMINS]}])],
([$pubkey, $events]): SpaceRoleState => {
const userPermissions = new Set<number>()
const memberRoles = new Map<string, RoleDefinition[]>()
const rolesByH = new Map<string, ReturnType<typeof parseRoleState>>()
let hasPermissionTags = false
for (const event of $events) {
if (event.kind === ROOM_ROLES) {
const h = getTagValue("d", event.tags)
if (h) {
const parsed = parseRoleState(event)
rolesByH.set(h, parsed)
if (parsed.hasPermissionTags) {
hasPermissionTags = true
}
}
}
}
for (const event of $events) {
if (event.kind === ROOM_MEMBERS || event.kind === ROOM_ADMINS) {
const h = getTagValue("d", event.tags)
if (!h) continue
const rolesState = rolesByH.get(h)
if (!rolesState) continue
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

I don't understand the purpose of snapshots, couldn't you just use the latest event of each kind and parse members on demand? Or do you expect that to be computationally intensive? It seems odd to couple roles/members/admins like this instead of access each individually. Since this function is used in only one place it might just make sense to fold the logic in there to avoid the entire idea of snapshots.

I don't understand the purpose of snapshots, couldn't you just use the latest event of each kind and parse members on demand? Or do you expect that to be computationally intensive? It seems odd to couple roles/members/admins like this instead of access each individually. Since this function is used in only one place it might just make sense to fold the logic in there to avoid the entire idea of snapshots.
const members = parseRoomMembers(event.tags)
for (const member of members) {
const resolvedRoles = getResolvedRoles(rolesState.roles, member.roles)
if (resolvedRoles.length === 0) {
continue
}
memberRoles.set(
member.pubkey,
mergeRoleDefinitions(memberRoles.get(member.pubkey) || [], resolvedRoles),
)
if ($pubkey === member.pubkey && rolesState.hasPermissionTags) {
for (const role of resolvedRoles) {
for (const permission of role.permissions) {
userPermissions.add(permission)
}
}
}
}
}
}
return {
hasPermissionTags,
userPermissions,
memberRoles,
}
},
),
)
export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
if (!url) {
return readable(false)
}
return derived(
[deriveSpaceRoleState(url), deriveSupportedMethods(url)],
([$spaceRoleState, $supportedMethods]) => {
if ($spaceRoleState.hasPermissionTags) {
return $spaceRoleState.userPermissions.size > 0
}
return $supportedMethods.length > 0
},
)
})
export const deriveUserSpacePermissions = (url: string) =>
derived(
[deriveSpaceRoleState(url), deriveUserIsSpaceAdmin(url)],
([$spaceRoleState, $isAdmin]) => {
const permissions = new Set<number>()
if ($spaceRoleState.hasPermissionTags) {
for (const permission of $spaceRoleState.userPermissions) {
permissions.add(permission)
}
return permissions
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

should be deriveHasPermission

should be deriveHasPermission
}
if ($isAdmin) {
for (const permission of ALL_ROOM_PERMISSIONS) {
permissions.add(permission)
}
}
return permissions
},
)
export const deriveUserHasSpacePermission = (url: string, kind: number) =>
derived(deriveUserSpacePermissions(url), $permissions => $permissions.has(kind))
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
derived(
[deriveUserPermissions(url, h), deriveUserIsSpaceAdmin(url)],
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.size > 0,
)
export const deriveHasPermission = (url: string, h: string | undefined, kind: number) => {
if (!h) return deriveUserIsSpaceAdmin(url)
return derived(
[deriveUserPermissions(url, h), deriveUserIsSpaceAdmin(url)],
([$permissions, $isSpaceAdmin]) => $isSpaceAdmin || $permissions.has(kind),
)
}
export const deriveRoomRoleDefinitions = (url: string, h: string) =>
derived(deriveRoomRoles(url, h), $roomRoles =>
sortRolesDesc(Array.from($roomRoles.roles.values())),
)
const deriveRoomMemberRoleInfo = (url: string, h: string) =>
derived([deriveRoomMembers(url, h), deriveRoomRoles(url, h)], ([$members, $roomRoles]) =>
sortMemberRoleInfos(
$members.map(member =>
toMemberRoleInfo(member.pubkey, getResolvedRoles($roomRoles.roles, member.roles)),
),
),
)
export const deriveGroupedRoomMembers = (url: string, h: string) =>
derived(deriveRoomMemberRoleInfo(url, h), $members => groupMemberRoleInfos($members))
const deriveSpaceMemberRoleInfo = (url: string) =>
derived(deriveSpaceRoleState(url), $spaceRoleState => {
const roleInfoByPubkey = new Map<string, MemberRoleInfo>()
for (const [pubkey, roles] of $spaceRoleState.memberRoles.entries()) {
roleInfoByPubkey.set(pubkey, toMemberRoleInfo(pubkey, roles))
}
return roleInfoByPubkey
})
export const deriveSpaceMemberRoles = (url: string, targetPubkey: string) =>
derived(
deriveSpaceMemberRoleInfo(url),
$spaceMemberRoles => $spaceMemberRoles.get(targetPubkey)?.roles || [],
)
export const deriveGroupedSpaceMembers = (url: string, members: Readable<string[]>) =>
derived([members, deriveSpaceMemberRoleInfo(url)], ([$members, $spaceMemberRoles]) =>
groupMemberRoleInfos(
$members.map(pubkey => $spaceMemberRoles.get(pubkey) || toMemberRoleInfo(pubkey, [])),
),
)
export const roleColorToCSS = (hue: number) => `oklch(0.75 0.15 ${(hue * 360) / 255})`
+40 -61
View File
@@ -20,7 +20,6 @@ import {
partition,
shuffle,
parseJson,
memoize,
addToMapKey,
identity,
always,
@@ -84,7 +83,6 @@ import {
ROOM_JOIN,
ROOM_LEAVE,
ROOM_MEMBERS,
ROOM_ADMINS,
ROOM_META,
ROOM_DELETE,
ROOM_REMOVE_MEMBER,
@@ -152,6 +150,18 @@ import {
} from "@welshman/app"
import {checkRelayHasLivekit} from "$lib/livekit"
import {readFeed} from "@lib/feeds"
import {
parseRoomMembers,
deriveRoomMembers,
deriveUserIsSpaceAdmin,
deriveUserIsRoomAdmin,
deriveUserSpacePermissions,
ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_REMOVE_MEMBER,
ROOM_PERMISSION_DELETE_EVENT,
ROOM_PERMISSION_BAN_USER,
} from "@app/core/roles"
import type {RoomMember} from "@app/core/roles"
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
@@ -813,14 +823,6 @@ export const deriveSpaceMembers = (url: string) =>
uniq(getTagValues("member", event?.tags ?? [])),
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

Instead of re-exporting, update imports

Instead of re-exporting, update imports
)
export const deriveRoomMembers = (url: string, h: string) => {
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
return derived(deriveEventsForUrl(url, filters), ([event]) =>
uniq(getPubkeyTagValues(event?.tags ?? [])),
)
}
export type BannedPubkeyItem = {
pubkey: string
reason: string
@@ -839,20 +841,6 @@ export const deriveSpaceBannedPubkeyItems = (url: string) => {
return store
}
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

Instead of re-exporting, update imports

Instead of re-exporting, update imports
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 []
})
}
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
const members = new Set<string>()
@@ -860,8 +848,8 @@ const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
if (event.kind === ROOM_MEMBERS && getTagValue("d", event.tags) === h) {
members.clear()
for (const pubkey of uniq(getPubkeyTagValues(event.tags))) {
members.add(pubkey)
for (const member of parseRoomMembers(event.tags)) {
members.add(member.pubkey)
}
continue
@@ -894,16 +882,31 @@ const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
export const deriveSpaceActionItems = (url: string) =>
derived(
deriveEventsForUrl(url, [
{
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
},
]),
$events => {
[
deriveEventsForUrl(url, [
{
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
},
]),
deriveUserIsSpaceAdmin(url),
deriveUserSpacePermissions(url),
],
([$events, $isAdmin, $permissions]) => {
if (!$isAdmin) {
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

This seems unnecessary to me, isn't this taken into account by deriveUserSpacePermissions?

This seems unnecessary to me, isn't this taken into account by `deriveUserSpacePermissions`?
return []
}
const getRoomId = (e: TrustedEvent) =>
getTagValue(e.kind === ROOM_MEMBERS ? "d" : "h", e.tags)
const reports = $events.filter(e => e.kind === REPORT)
const pendingJoins: TrustedEvent[] = []
const canReviewReports =
$permissions.has(ROOM_PERMISSION_DELETE_EVENT) || $permissions.size === 0
const canReviewJoins =
$permissions.has(ROOM_PERMISSION_ADD_MEMBER) ||
$permissions.has(ROOM_PERMISSION_REMOVE_MEMBER) ||
$permissions.has(ROOM_PERMISSION_BAN_USER) ||
$permissions.size === 0
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

The permissions.size is here because this assumes permissions aren't filled for the space admin, but I think that is actually done in the roles file.

The permissions.size is here because this assumes permissions aren't filled for the space admin, but I think that is actually done in the roles file.
// Room-level join requests — most recent per pubkey+h
for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
@@ -960,7 +963,10 @@ export const deriveSpaceActionItems = (url: string) =>
)
}
return sortEventsDesc([...reports, ...pendingJoins])
return sortEventsDesc([
...(canReviewReports ? reports : []),
...(canReviewJoins ? pendingJoins : []),
])
},
)
@@ -972,18 +978,6 @@ export enum MembershipStatus {
Granted,
}
export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
const store = writable(false)
if (url) {
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
store.set(Boolean(res.result?.length)),
)
}
return store
})
export const deriveUserSpaceMembershipStatus = (url: string) => {
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

Instead of re-exporting, update imports

Instead of re-exporting, update imports
// Fetch member list and user add/remove events directly in this derivation.
const memberListFilters: Filter[] = [{kinds: [RELAY_MEMBERS]}]
@@ -1046,12 +1040,6 @@ export const deriveUserSpaceMembershipStatus = (url: string) => {
)
}
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
derived(
[pubkey, deriveRoomAdmins(url, h), deriveUserIsSpaceAdmin(url)],
([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
)
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
// 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]}]
Khushvendra marked this conversation as resolved Outdated
Outdated
Review

Instead of re-exporting, update imports

Instead of re-exporting, update imports
@@ -1075,7 +1063,7 @@ export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
if ($memberList) {
// Member list exists - check if user is in it.
isMember = $memberList.includes($pubkey!)
isMember = $memberList.some((member: RoomMember) => member.pubkey === $pubkey)
} else {
// No member list available - replay the user's add/remove history.
for (const event of sortEventsAsc($userAddRemoveEvents)) {
@@ -1312,15 +1300,6 @@ export const deriveSocketStatus = (url: string) =>
}),
)
export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
return readable<ManagementMethod[]>([], set => {
manageRelay(url, {
method: ManagementMethod.SupportedMethods,
params: [],
}).then(({result = []}) => set(result))
})
})
export const deriveHasLivekit = simpleCache(([url]: [string]) =>
readable<boolean | undefined>(undefined, set => {
checkRelayHasLivekit(url).then(has => set(has))
+2 -1
View File
@@ -55,6 +55,7 @@ import {
makeCommentFilter,
loadFeedsForPubkey,
} from "@app/core/state"
import {ROOM_ROLES} from "@app/core/roles"
import {hasBlossomSupport} from "@app/core/commands"
import {LIVEKIT_PARTICIPANTS} from "@app/call/voice"
@@ -271,7 +272,7 @@ const syncSpace = (url: string) => {
const since = ago(WEEK)
const controller = new AbortController()
const relayKinds = [RELAY_MEMBERS]
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
const roomMetaKinds = [ROOM_META, ROOM_ROLES, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
pullAndListen({
+2
View File
@@ -47,6 +47,7 @@ import {
} from "@welshman/app"
import type {Unsubscriber} from "svelte/store"
import {db} from "@app/core/storage"
import {ROOM_ROLES} from "@app/core/roles"
// Shared interval for all non-critical store flushes, so they batch on the same cadence
const FLUSH_INTERVAL = 3000
@@ -65,6 +66,7 @@ const kinds = {
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
space: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE],
room: [
ROOM_ROLES,
ROOM_META,
ROOM_DELETE,
ROOM_ADMINS,