Refactor role view models and member grouping
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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})`
|
||||||
|
|||||||
Reference in New Issue
Block a user