Refactor role view models and member grouping

This commit is contained in:
2026-04-21 03:19:21 +05:30
parent 5a926ab6cf
commit 7259e4d2cf
6 changed files with 144 additions and 201 deletions
+5 -7
View File
@@ -33,7 +33,7 @@
import {pubkeyLink, deriveSpaceBannedPubkeyItems} from "@app/core/state" import {pubkeyLink, deriveSpaceBannedPubkeyItems} from "@app/core/state"
import { import {
deriveUserHasSpacePermission, deriveUserHasSpacePermission,
deriveSpaceMemberRoleInfo, deriveSpaceMemberRoles,
ROOM_PERMISSION_ADD_MEMBER, ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_BAN_USER, ROOM_PERMISSION_BAN_USER,
} from "@app/core/roles" } from "@app/core/roles"
@@ -59,9 +59,7 @@
const bannedPubkeys = url ? deriveSpaceBannedPubkeyItems(url) : undefined const bannedPubkeys = url ? deriveSpaceBannedPubkeyItems(url) : undefined
const spaceMemberRoles = url ? deriveSpaceMemberRoleInfo(url) : readable(new Map()) const assignedRoles = url ? deriveSpaceMemberRoles(url, pubkey) : readable([])
const assignedRoles = $derived($spaceMemberRoles.get(pubkey)?.roles || [])
const isBanned = $derived($bannedPubkeys?.some(item => item.pubkey === pubkey) ?? false) const isBanned = $derived($bannedPubkeys?.some(item => item.pubkey === pubkey) ?? false)
@@ -163,12 +161,12 @@
{/if} {/if}
</div> </div>
<ProfileInfo {pubkey} {url} /> <ProfileInfo {pubkey} {url} />
{#if assignedRoles.length > 0} {#if $assignedRoles.length > 0}
<div class="card2 card2-sm bg-alt col-3"> <div class="card2 card2-sm bg-alt col-3">
<h3 class="text-lg font-semibold">Roles</h3> <h3 class="text-lg font-semibold">Roles</h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each assignedRoles as role (role.name)} {#each $assignedRoles as role (role.name)}
<RoleBadge role={role.name} label={role.label} color={role.color} class="badge-md" /> <RoleBadge {role} class="badge-md" />
{/each} {/each}
</div> </div>
</div> </div>
+11 -4
View File
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import type {RoleDefinition} from "@app/core/roles"
import {roleColorToCSS} from "@app/core/roles" import {roleColorToCSS} from "@app/core/roles"
type Props = { type Props = {
role: string role: RoleDefinition | string
label?: string label?: string
color?: number color?: number
class?: string class?: string
@@ -11,15 +12,21 @@
const {role, label, color, ...props}: Props = $props() const {role, label, color, ...props}: Props = $props()
const roleName = $derived(typeof role === "string" ? role : role.name)
const roleLabel = $derived(
label || (typeof role === "string" ? undefined : role.label) || roleName,
)
const roleColor = $derived(color ?? (typeof role === "string" ? undefined : role.color))
const style = $derived( const style = $derived(
color === undefined roleColor === undefined
? "" ? ""
: `color: ${roleColorToCSS(color)}; border-color: ${roleColorToCSS(color)};`, : `color: ${roleColorToCSS(roleColor)}; border-color: ${roleColorToCSS(roleColor)};`,
) )
const className = $derived(cx("badge badge-outline badge-sm", props.class)) const className = $derived(cx("badge badge-outline badge-sm", props.class))
</script> </script>
<span class={className} {style}> <span class={className} {style}>
{label || role} {roleLabel}
</span> </span>
+11 -27
View File
@@ -34,10 +34,11 @@
import RoomImage from "@app/components/RoomImage.svelte" import RoomImage from "@app/components/RoomImage.svelte"
import { import {
deriveRoomMembers, deriveRoomMembers,
deriveRoomRoles, deriveRoomRoleDefinitions,
deriveUserIsRoomAdmin, deriveUserIsRoomAdmin,
deriveHasPermission, deriveHasPermission,
sortRolesDesc, getRolePermissionsLabel,
getRoleAccessLabel,
ROOM_PERMISSION_EDIT_META, ROOM_PERMISSION_EDIT_META,
} from "@app/core/roles" } from "@app/core/roles"
import { import {
@@ -65,7 +66,7 @@
const room = deriveRoom(url, h) const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h) const members = deriveRoomMembers(url, h)
const roomRoles = deriveRoomRoles(url, h) const roleDefinitions = deriveRoomRoleDefinitions(url, h)
const userIsAdmin = deriveUserIsRoomAdmin(url, h) const userIsAdmin = deriveUserIsRoomAdmin(url, h)
const canEditMetadata = deriveHasPermission(url, h, ROOM_PERMISSION_EDIT_META) const canEditMetadata = deriveHasPermission(url, h, ROOM_PERMISSION_EDIT_META)
const membershipStatus = deriveUserRoomMembershipStatus(url, h) const membershipStatus = deriveUserRoomMembershipStatus(url, h)
@@ -74,19 +75,6 @@
const isFavorite = $derived($userRooms.includes(h)) const isFavorite = $derived($userRooms.includes(h))
const shouldNotify = deriveShouldNotify(url, h) const shouldNotify = deriveShouldNotify(url, h)
const roleRows = $derived.by(() =>
sortRolesDesc(
Array.from($roomRoles.roles.values()).map(role => ({
name: role.name,
label: role.label,
color: role.color,
order: role.order,
permissionsLabel: role.permissions.join(", "),
accessLabel: Array.from(role.access).join(", "),
})),
),
)
const back = () => history.back() const back = () => history.back()
const toggleMenu = () => { const toggleMenu = () => {
@@ -278,27 +266,23 @@
<span class="text-error">Member list not available from this relay</span> <span class="text-error">Member list not available from this relay</span>
</div> </div>
{/if} {/if}
{#if $userIsAdmin && roleRows.length > 0} {#if $userIsAdmin && $roleDefinitions.length > 0}
<div class="card2 card2-sm bg-alt col-4"> <div class="card2 card2-sm bg-alt col-4">
<strong class="text-lg">Role Definitions</strong> <strong class="text-lg">Role Definitions</strong>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#each roleRows as role (role.name)} {#each $roleDefinitions as role (role.name)}
<div class="rounded-box bg-base-300 p-3 flex flex-col gap-2"> <div class="rounded-box bg-base-300 p-3 flex flex-col gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<RoleBadge <RoleBadge {role} class="badge-md" />
role={role.name}
label={role.label}
color={role.color}
class="badge-md" />
{#if role.order !== undefined} {#if role.order !== undefined}
<span class="text-xs opacity-70">Order {role.order}</span> <span class="text-xs opacity-70">Order {role.order}</span>
{/if} {/if}
</div> </div>
{#if role.permissionsLabel} {#if role.permissions.length > 0}
<p class="text-xs opacity-75">Permissions: {role.permissionsLabel}</p> <p class="text-xs opacity-75">Permissions: {getRolePermissionsLabel(role)}</p>
{/if} {/if}
{#if role.accessLabel} {#if role.access.size > 0}
<p class="text-xs opacity-75">Access: {role.accessLabel}</p> <p class="text-xs opacity-75">Access: {getRoleAccessLabel(role)}</p>
{/if} {/if}
</div> </div>
{/each} {/each}
+9 -68
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import {first, removeUndefined, sortBy} from "@welshman/lib"
import {waitForThunkError, removeRoomMember} from "@welshman/app" import {waitForThunkError, removeRoomMember} from "@welshman/app"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -20,12 +19,10 @@
import RoleBadge from "@app/components/RoleBadge.svelte" import RoleBadge from "@app/components/RoleBadge.svelte"
import RoomName from "@app/components/RoomName.svelte" import RoomName from "@app/components/RoomName.svelte"
import RoomMembersAdd from "@app/components/RoomMembersAdd.svelte" import RoomMembersAdd from "@app/components/RoomMembersAdd.svelte"
import type {RoomMember} from "@app/core/roles"
import { import {
deriveRoomMembers, deriveRoomMembers,
deriveRoomRoles, deriveGroupedRoomMembers,
deriveHasPermission, deriveHasPermission,
sortRolesDesc,
ROOM_PERMISSION_ADD_MEMBER, ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_REMOVE_MEMBER, ROOM_PERMISSION_REMOVE_MEMBER,
} from "@app/core/roles" } from "@app/core/roles"
@@ -42,7 +39,7 @@
const room = deriveRoom(url, h) const room = deriveRoom(url, h)
const members = deriveRoomMembers(url, h) const members = deriveRoomMembers(url, h)
const roomRoles = deriveRoomRoles(url, h) const memberGroups = deriveGroupedRoomMembers(url, h)
const canAddMembers = deriveHasPermission(url, h, ROOM_PERMISSION_ADD_MEMBER) const canAddMembers = deriveHasPermission(url, h, ROOM_PERMISSION_ADD_MEMBER)
const canRemoveMembers = deriveHasPermission(url, h, ROOM_PERMISSION_REMOVE_MEMBER) const canRemoveMembers = deriveHasPermission(url, h, ROOM_PERMISSION_REMOVE_MEMBER)
@@ -56,58 +53,6 @@
menuPubkey = undefined menuPubkey = undefined
} }
const getResolvedRoles = (member: RoomMember) =>
removeUndefined(member.roles.map(roleName => $roomRoles.roles.get(roleName)))
const getPrimaryRole = (member: RoomMember) => first(sortRolesDesc(getResolvedRoles(member)))
const memberGroups = $derived.by(() => {
const byRole = new Map<
string,
{
key: string
label: string
color?: number
order?: number
members: RoomMember[]
}
>()
const defaultGroup = {
key: "members",
label: "Members",
members: [] as RoomMember[],
}
for (const member of $members) {
const primaryRole = getPrimaryRole(member)
if (!primaryRole) {
defaultGroup.members.push(member)
continue
}
if (!byRole.has(primaryRole.name)) {
byRole.set(primaryRole.name, {
key: primaryRole.name,
label: primaryRole.label || primaryRole.name,
color: primaryRole.color,
order: primaryRole.order,
members: [],
})
}
byRole.get(primaryRole.name)!.members.push(member)
}
const groups = sortBy(group => -(group.order ?? -Infinity), Array.from(byRole.values()))
if (defaultGroup.members.length > 0) {
groups.push(defaultGroup)
}
return groups
})
const addMember = () => pushModal(RoomMembersAdd, {url, h}) const addMember = () => pushModal(RoomMembersAdd, {url, h})
const removeMember = (pubkey: string) => const removeMember = (pubkey: string) =>
@@ -147,16 +92,12 @@
<span class="text-base-content/70">No members yet</span> <span class="text-base-content/70">No members yet</span>
</div> </div>
{:else} {:else}
{#each memberGroups as group (group.key)} {#each $memberGroups as group (group.key)}
<div class="pt-2 pb-1"> <div class="pt-2 pb-1">
{#if group.color !== undefined} {#if group.role}
<RoleBadge <RoleBadge role={group.role} class="badge-md" />
role={group.key}
label={group.label}
color={group.color}
class="badge-md" />
{:else} {:else}
<span class="text-sm font-semibold opacity-75">{group.label}</span> <span class="text-sm font-semibold opacity-75">Members</span>
{/if} {/if}
</div> </div>
{#each group.members as member (member.pubkey)} {#each group.members as member (member.pubkey)}
@@ -164,10 +105,10 @@
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<Profile pubkey={member.pubkey} {url} /> <Profile pubkey={member.pubkey} {url} />
{#if getResolvedRoles(member).length > 0} {#if member.roles.length > 0}
<div class="mt-1 flex flex-wrap gap-1"> <div class="mt-1 flex flex-wrap gap-1">
{#each getResolvedRoles(member) as role (role.name)} {#each member.roles as role (role.name)}
<RoleBadge role={role.name} label={role.label} color={role.color} /> <RoleBadge {role} />
{/each} {/each}
</div> </div>
{/if} {/if}
+9 -81
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import {sortBy} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {manageRelay, displayProfileByPubkey} from "@welshman/app" import {manageRelay, displayProfileByPubkey} from "@welshman/app"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
@@ -23,14 +22,13 @@
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte" import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte"
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte" import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
import type {RoomMember} from "@app/core/roles"
import { import {
deriveSpaceMembers, deriveSpaceMembers,
deriveSpaceBannedPubkeyItems, deriveSpaceBannedPubkeyItems,
deriveSupportedMethods, deriveSupportedMethods,
} from "@app/core/state" } from "@app/core/state"
import { import {
deriveSpaceMemberRoleInfo, deriveGroupedSpaceMembers,
deriveUserHasSpacePermission, deriveUserHasSpacePermission,
ROOM_PERMISSION_ADD_MEMBER, ROOM_PERMISSION_ADD_MEMBER,
ROOM_PERMISSION_REMOVE_MEMBER, ROOM_PERMISSION_REMOVE_MEMBER,
@@ -47,7 +45,7 @@
const members = deriveSpaceMembers(url) const members = deriveSpaceMembers(url)
const bans = deriveSpaceBannedPubkeyItems(url) const bans = deriveSpaceBannedPubkeyItems(url)
const spaceMemberRoles = deriveSpaceMemberRoleInfo(url) const memberGroups = deriveGroupedSpaceMembers(url, members)
const canAddMember = deriveUserHasSpacePermission(url, ROOM_PERMISSION_ADD_MEMBER) const canAddMember = deriveUserHasSpacePermission(url, ROOM_PERMISSION_ADD_MEMBER)
const canBanByPermission = deriveUserHasSpacePermission(url, ROOM_PERMISSION_BAN_USER) const canBanByPermission = deriveUserHasSpacePermission(url, ROOM_PERMISSION_BAN_USER)
const canUnallowByPermission = deriveUserHasSpacePermission(url, ROOM_PERMISSION_REMOVE_MEMBER) const canUnallowByPermission = deriveUserHasSpacePermission(url, ROOM_PERMISSION_REMOVE_MEMBER)
@@ -59,72 +57,6 @@
$canUnallowByPermission && $supportedMethods.includes(ManagementMethod.UnallowPubkey), $canUnallowByPermission && $supportedMethods.includes(ManagementMethod.UnallowPubkey),
) )
type SpaceMemberWithRoles = RoomMember & {
roleDefinitions: Array<{name: string; label?: string; color?: number; order?: number}>
primaryRole?: {name: string; label?: string; color?: number}
sortKey: number
}
const memberGroups = $derived.by(() => {
const byRole = new Map<
string,
{
key: string
label: string
color?: number
order?: number
members: SpaceMemberWithRoles[]
}
>()
const defaultGroup = {
key: "members",
label: "Members",
members: [] as SpaceMemberWithRoles[],
}
for (const pubkey of $members) {
const roleInfo = $spaceMemberRoles.get(pubkey)
const member = {
pubkey,
roles: roleInfo?.roles.map(role => role.name) || [],
roleDefinitions: roleInfo?.roles || [],
primaryRole: roleInfo?.primaryRole,
sortKey: roleInfo?.sortKey ?? -Infinity,
}
if (!member.primaryRole) {
defaultGroup.members.push(member)
continue
}
const roleName = member.primaryRole.name
if (!byRole.has(roleName)) {
byRole.set(roleName, {
key: roleName,
label: member.primaryRole.label || roleName,
color: member.primaryRole.color,
order: member.sortKey,
members: [],
})
}
byRole.get(roleName)!.members.push(member)
}
const groups = sortBy(group => -(group.order ?? -Infinity), Array.from(byRole.values()))
for (const group of groups) {
group.members = sortBy(member => -member.sortKey, group.members)
}
if (defaultGroup.members.length > 0) {
groups.push(defaultGroup)
}
return groups
})
const back = () => history.back() const back = () => history.back()
const toggleMenu = (pubkey: string) => { const toggleMenu = (pubkey: string) => {
@@ -203,16 +135,12 @@
<span class="text-base-content/70">No members yet</span> <span class="text-base-content/70">No members yet</span>
</div> </div>
{:else} {:else}
{#each memberGroups as group (group.key)} {#each $memberGroups as group (group.key)}
<div class="pt-2 pb-1"> <div class="pt-2 pb-1">
{#if group.color !== undefined} {#if group.role}
<RoleBadge <RoleBadge role={group.role} class="badge-md" />
role={group.key}
label={group.label}
color={group.color}
class="badge-md" />
{:else} {:else}
<span class="text-sm font-semibold opacity-75">{group.label}</span> <span class="text-sm font-semibold opacity-75">Members</span>
{/if} {/if}
</div> </div>
{#each group.members as member (member.pubkey)} {#each group.members as member (member.pubkey)}
@@ -220,10 +148,10 @@
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<Profile pubkey={member.pubkey} {url} /> <Profile pubkey={member.pubkey} {url} />
{#if member.roleDefinitions.length > 0} {#if member.roles.length > 0}
<div class="mt-1 flex flex-wrap gap-1"> <div class="mt-1 flex flex-wrap gap-1">
{#each member.roleDefinitions as role (role.name)} {#each member.roles as role (role.name)}
<RoleBadge role={role.name} label={role.label} color={role.color} /> <RoleBadge {role} />
{/each} {/each}
</div> </div>
{/if} {/if}
+99 -14
View File
@@ -1,4 +1,4 @@
import {derived, readable} from "svelte/store" import {derived, readable, type Readable} from "svelte/store"
import {first, memoize, removeUndefined, simpleCache, sortBy, uniq} from "@welshman/lib" import {first, memoize, removeUndefined, simpleCache, sortBy, uniq} from "@welshman/lib"
import {deriveArray, deriveEventsByIdForUrl} from "@welshman/store" import {deriveArray, deriveEventsByIdForUrl} from "@welshman/store"
import {pubkey, repository, tracker} from "@welshman/app" import {pubkey, repository, tracker} from "@welshman/app"
@@ -53,6 +53,18 @@ export type RoomMember = {
roles: string[] roles: string[]
} }
export type MemberRoleInfo = {
pubkey: string
roles: RoleDefinition[]
primaryRole?: RoleDefinition
}
export type MemberRoleGroup = {
key: string
role?: RoleDefinition
members: MemberRoleInfo[]
}
type ParsedRoleState = { type ParsedRoleState = {
roles: Map<string, RoleDefinition> roles: Map<string, RoleDefinition>
hasPermissionTags: boolean hasPermissionTags: boolean
@@ -65,11 +77,7 @@ type RoomSnapshot = {
admins: RoomMember[] admins: RoomMember[]
} }
export type SpaceMemberRoleInfo = { export type SpaceMemberRoleInfo = MemberRoleInfo
roles: RoleDefinition[]
primaryRole?: RoleDefinition
sortKey: number
}
type SpaceRoleState = { type SpaceRoleState = {
hasPermissionTags: boolean hasPermissionTags: boolean
@@ -275,8 +283,62 @@ const getResolvedRoles = (rolesByName: Map<string, RoleDefinition>, roleNames: s
export const sortRolesDesc = <T extends {order?: number}>(items: T[]) => export const sortRolesDesc = <T extends {order?: number}>(items: T[]) =>
sortBy(item => -(item.order ?? -Infinity), items) sortBy(item => -(item.order ?? -Infinity), items)
export const getRoleLabel = (role: RoleDefinition) => role.label || role.name
export const getRolePermissionsLabel = (role: RoleDefinition) => role.permissions.join(", ")
export const getRoleAccessLabel = (role: RoleDefinition) => Array.from(role.access).join(", ")
const getPrimaryRole = (roles: RoleDefinition[]) => first(sortRolesDesc(roles)) const getPrimaryRole = (roles: RoleDefinition[]) => first(sortRolesDesc(roles))
const toMemberRoleInfo = (pubkey: string, roles: RoleDefinition[]): MemberRoleInfo => {
const sortedRoles = sortRolesDesc(roles)
return {
pubkey,
roles: sortedRoles,
primaryRole: first(sortedRoles),
}
}
const sortMemberRoleInfos = (members: MemberRoleInfo[]) =>
sortBy(member => -(member.primaryRole?.order ?? -Infinity), members)
export 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]) => const deriveRoomRoleAssignments = simpleCache(([url, h]: [string, string]) =>
derived( derived(
[deriveRoomRoleState(url, h), deriveRoomMembers(url, h), deriveRoomAdmins(url, h)], [deriveRoomRoleState(url, h), deriveRoomMembers(url, h), deriveRoomAdmins(url, h)],
@@ -523,6 +585,23 @@ export const deriveUserRoleColor = (url: string, h: string, targetPubkey: string
getPrimaryRole(getResolvedRoles($roomRoles.roles, $roleNames))?.color, getPrimaryRole(getResolvedRoles($roomRoles.roles, $roleNames))?.color,
) )
export const deriveRoomRoleDefinitions = (url: string, h: string) =>
derived(deriveRoomRoles(url, h), $roomRoles =>
sortRolesDesc(Array.from($roomRoles.roles.values())),
)
export 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))
export const getRoleSortKey = (url: string, h: string, targetPubkey: string) => export const getRoleSortKey = (url: string, h: string, targetPubkey: string) =>
derived( derived(
[deriveUserRoles(url, h, targetPubkey), deriveRoomRoles(url, h)], [deriveUserRoles(url, h, targetPubkey), deriveRoomRoles(url, h)],
@@ -535,17 +614,23 @@ export const deriveSpaceMemberRoleInfo = (url: string) =>
const roleInfoByPubkey = new Map<string, SpaceMemberRoleInfo>() const roleInfoByPubkey = new Map<string, SpaceMemberRoleInfo>()
for (const [pubkey, roles] of $spaceRoleState.memberRoles.entries()) { for (const [pubkey, roles] of $spaceRoleState.memberRoles.entries()) {
const sortedRoles = sortRolesDesc(roles) roleInfoByPubkey.set(pubkey, toMemberRoleInfo(pubkey, roles))
const primaryRole = first(sortedRoles)
roleInfoByPubkey.set(pubkey, {
roles: sortedRoles,
primaryRole,
sortKey: primaryRole?.order ?? -Infinity,
})
} }
return roleInfoByPubkey 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})` export const roleColorToCSS = (hue: number) => `oklch(0.75 0.15 ${(hue * 360) / 255})`