No banned users.
diff --git a/src/app/components/SpaceMenu.svelte b/src/app/components/SpaceMenu.svelte
index 7b3b6f52..6829f714 100644
--- a/src/app/components/SpaceMenu.svelte
+++ b/src/app/components/SpaceMenu.svelte
@@ -5,8 +5,8 @@
import {fly} from "@lib/transition"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
- import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
- import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
+ import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
+ import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Exit from "@assets/icons/logout-3.svg?dataurl"
@@ -29,12 +29,10 @@
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
- import SpaceDetail from "@app/components/SpaceDetail.svelte"
import SpaceInvite from "@app/components/SpaceInvite.svelte"
import SpaceExit from "@app/components/SpaceExit.svelte"
import SpaceJoin from "@app/components/SpaceJoin.svelte"
import RelayName from "@app/components/RelayName.svelte"
- import SpaceMembers from "@app/components/SpaceMembers.svelte"
import SpaceActionItems from "@app/components/SpaceActionItems.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte"
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
@@ -42,7 +40,7 @@
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
import {ENABLE_ZAPS} from "@app/env"
import {CONTENT_KINDS} from "@app/content"
- import {deriveSpaceMembers, deriveUserCanCreateRoom, deriveUserIsSpaceAdmin} from "@app/members"
+ import {deriveUserCanCreateRoom, deriveUserIsSpaceAdmin} from "@app/members"
import {
deriveUserRooms,
deriveOtherRooms,
@@ -61,6 +59,7 @@
const {url} = $props()
const relay = deriveRelay(url)
+ const aboutPath = makeSpacePath(url, "about")
const chatPath = makeSpacePath(url, "chat")
const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads")
@@ -70,7 +69,6 @@
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
const otherVoiceRooms = deriveOtherVoiceRooms(url)
- const members = deriveSpaceMembers(url)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const actionItems = deriveSpaceActionItems(url)
@@ -97,10 +95,6 @@
showMenu = !showMenu
}
- const showDetail = () => pushModal(SpaceDetail, {url})
-
- const showMembers = () => pushModal(SpaceMembers, {url})
-
const showActionItems = () => pushModal(SpaceActionItems, {url})
const canCreateRoom = deriveUserCanCreateRoom(url)
@@ -164,22 +158,6 @@
Create Invite
-
-
-
- Space Information
-
-
-
-
-
- {#if $members === undefined}
- View Members
- {:else}
- View Members ({$members.length})
- {/if}
-
-
{#if $userIsAdmin}
@@ -230,6 +208,12 @@
{/if}
+
+ About
+
+
+ Directory
+
{#if hasNip29($relay)}
Recent Activity
@@ -311,8 +295,8 @@
-
+
-
+
diff --git a/src/app/components/SpaceProfileCard.svelte b/src/app/components/SpaceProfileCard.svelte
new file mode 100644
index 00000000..c160ee6e
--- /dev/null
+++ b/src/app/components/SpaceProfileCard.svelte
@@ -0,0 +1,168 @@
+
+
+
diff --git a/src/app/components/SpaceRoles.svelte b/src/app/components/SpaceRoles.svelte
new file mode 100644
index 00000000..5d8f2193
--- /dev/null
+++ b/src/app/components/SpaceRoles.svelte
@@ -0,0 +1,100 @@
+
+
+
+
+
+ Manage Roles
+ on
+
+ {#if $roles.length === 0}
+
+ No roles yet. Create one to start organizing members.
+
+ {:else}
+
+ {#each $roles as role (role.id)}
+
+
+
+ addMembers(role)}>
+
+
+ editRole(role)}>
+
+
+ confirmDelete(role)}>
+
+
+
+
+ {/each}
+
+ {/if}
+
+
+
+
+ Go back
+
+
+
+ Create Role
+
+
+
diff --git a/src/app/content.ts b/src/app/content.ts
index 78349ce2..cfcbbabc 100644
--- a/src/app/content.ts
+++ b/src/app/content.ts
@@ -26,6 +26,12 @@ export const makeCommentFilter = (kinds: number[], extra: Filter = {}) => ({
...extra,
})
+export const makeDeleteFilter = (kinds: number[], extra: Filter = {}) => ({
+ kinds: [DELETE],
+ "#k": kinds.map(String),
+ ...extra,
+})
+
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
diff --git a/src/app/members.ts b/src/app/members.ts
index 9b5704ed..e7af2405 100644
--- a/src/app/members.ts
+++ b/src/app/members.ts
@@ -14,6 +14,7 @@ import {
ROOM_MEMBERS,
ROOM_REMOVE_MEMBER,
getPubkeyTagValues,
+ getTags,
getTagValue,
getTagValues,
sortEventsAsc,
@@ -23,11 +24,136 @@ import {first, memoize, sortBy, spec, uniq} from "@welshman/lib"
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
import {get} from "svelte/store"
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
+
export const deriveSpaceMembers = (url: string) =>
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
uniq(getTagValues("member", event?.tags ?? [])),
)
+export const RELAY_ROLE = 33534
+
+export type SpaceRole = {
+ id: string
+ label: string
+ description: string
+ color: number
+ order: number
+}
+
+// hue is 0-255; map to 0-360deg. Saturation/lightness chosen to read on both themes.
+export const roleColorHue = (color: number) => (((color % 256) + 256) % 256) * (360 / 256)
+
+export const roleColor = (color: number) => `hsl(${roleColorHue(color)}, 70%, 50%)`
+
+export const roleColorSoft = (color: number) => `hsl(${roleColorHue(color)}, 70%, 90%)`
+
+export const deriveSpaceRoles = (url: string) =>
+ derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
+ const roles: SpaceRole[] = []
+
+ for (const event of $events) {
+ const id = getTagValue("d", event.tags)
+
+ if (id) {
+ roles.push({
+ id,
+ label: getTagValue("label", event.tags) ?? "",
+ description: getTagValue("description", event.tags) ?? "",
+ color: parseInt(getTagValue("color", event.tags) ?? "0", 10) || 0,
+ order: parseInt(getTagValue("order", event.tags) ?? "0", 10) || 0,
+ })
+ }
+ }
+
+ return sortBy(r => [r.order, r.label] as [number, string], roles)
+ })
+
+// Map
parsed from EXTRA values on ["member", pubkey, ...roleIds] tags
+export const deriveSpaceMemberRoles = (url: string) =>
+ derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) => {
+ const memberRoles = new Map()
+
+ if (event) {
+ for (const tag of getTags("member", event.tags)) {
+ const pubkey = tag[1]
+ const roleIds = tag.slice(2)
+
+ if (pubkey) {
+ memberRoles.set(pubkey, roleIds)
+ }
+ }
+ }
+
+ return memberRoles
+ })
+
+export const createRole = async (
+ url: string,
+ id: string,
+ label: string,
+ description: string,
+ color: number,
+ order: number,
+): Promise => {
+ const {error} = await manageRelay(url, {
+ method: "createrole" as ManagementMethod,
+ params: [id, label, description, color.toString(), order.toString()],
+ })
+
+ return error
+}
+
+export const editRole = async (
+ url: string,
+ id: string,
+ label: string,
+ description: string,
+ color: number,
+ order: number,
+): Promise => {
+ const {error} = await manageRelay(url, {
+ method: "editrole" as ManagementMethod,
+ params: [id, label, description, color.toString(), order.toString()],
+ })
+
+ return error
+}
+
+export const deleteRole = async (url: string, id: string): Promise => {
+ const {error} = await manageRelay(url, {
+ method: "deleterole" as ManagementMethod,
+ params: [id],
+ })
+
+ return error
+}
+
+export const assignRole = async (
+ url: string,
+ pubkey: string,
+ roleId: string,
+): Promise => {
+ const {error} = await manageRelay(url, {
+ method: "assignrole" as ManagementMethod,
+ params: [pubkey, roleId],
+ })
+
+ return error
+}
+
+export const unassignRole = async (
+ url: string,
+ pubkey: string,
+ roleId: string,
+): Promise => {
+ const {error} = await manageRelay(url, {
+ method: "unassignrole" as ManagementMethod,
+ params: [pubkey, roleId],
+ })
+
+ return error
+}
+
export const deriveRoomMembers = (url: string, h: string) => {
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
@@ -292,6 +418,47 @@ export const addSpaceMembers = async (
}
}
+export const removeSpaceMembers = async (
+ url: string,
+ pubkeys: string[],
+): Promise => {
+ const results = await Promise.all(
+ pubkeys.map(pubkey =>
+ manageRelay(url, {
+ method: ManagementMethod.UnallowPubkey,
+ params: [pubkey],
+ }),
+ ),
+ )
+
+ for (const {error} of results) {
+ if (error) {
+ return error
+ }
+ }
+}
+
+export const banSpaceMembers = async (
+ url: string,
+ pubkeys: string[],
+ reason = "",
+): Promise => {
+ const results = await Promise.all(
+ pubkeys.map(pubkey =>
+ manageRelay(url, {
+ method: ManagementMethod.BanPubkey,
+ params: reason ? [pubkey, reason] : [pubkey],
+ }),
+ ),
+ )
+
+ for (const {error} of results) {
+ if (error) {
+ return error
+ }
+ }
+}
+
export const addRoomMembers = async (
url: string,
room: PublishedRoomMeta,
diff --git a/src/app/sync.ts b/src/app/sync.ts
index 1b4abfbf..be4d3a7d 100644
--- a/src/app/sync.ts
+++ b/src/app/sync.ts
@@ -53,6 +53,7 @@ import {
} from "@app/groups"
import {decodeRelay} from "@app/relays"
import {loadFeedsForPubkey} from "@app/feeds"
+import {RELAY_ROLE} from "@app/members"
import {hasBlossomSupport} from "@app/uploads"
import {LIVEKIT_PARTICIPANTS} from "@app/call/voice"
@@ -268,7 +269,7 @@ const syncUserData = () => {
const syncSpace = (url: string) => {
const since = ago(WEEK)
const controller = new AbortController()
- const relayKinds = [RELAY_MEMBERS]
+ const relayKinds = [RELAY_MEMBERS, RELAY_ROLE]
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
@@ -277,8 +278,8 @@ const syncSpace = (url: string) => {
signal: controller.signal,
filters: [
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]},
- makeCommentFilter(CONTENT_KINDS, {since}),
{kinds: [...REACTION_KINDS, POLL_RESPONSE], since},
+ makeCommentFilter(CONTENT_KINDS, {since}),
],
})
diff --git a/src/routes/spaces/[relay]/about/+page.svelte b/src/routes/spaces/[relay]/about/+page.svelte
new file mode 100644
index 00000000..e92a2128
--- /dev/null
+++ b/src/routes/spaces/[relay]/about/+page.svelte
@@ -0,0 +1,96 @@
+
+
+
+ {#snippet leading()}
+
+ {/snippet}
+ {#snippet title()}
+ About
+ {/snippet}
+ {#snippet action()}
+ {#if $userIsAdmin}
+
+
+ Edit
+
+ {/if}
+ {/snippet}
+
+
+
+
+
+
+
+
+
+
{displayRelayUrl(url)}
+
+
+
+ {#if $relay?.terms_of_service || $relay?.privacy_policy}
+
+ {#if $relay.terms_of_service}
+
+
+ Terms of Service
+
+ {/if}
+ {#if $relay.privacy_policy}
+
+
+ Privacy Policy
+
+ {/if}
+
+ {/if}
+
+ {#if owner}
+
+
+
+ Latest Updates
+
+
+ {#snippet fallback()}
+ No recent posts from the relay admin
+ {/snippet}
+
+
+ {/if}
+
diff --git a/src/routes/spaces/[relay]/directory/+page.svelte b/src/routes/spaces/[relay]/directory/+page.svelte
new file mode 100644
index 00000000..de37c34f
--- /dev/null
+++ b/src/routes/spaces/[relay]/directory/+page.svelte
@@ -0,0 +1,194 @@
+
+
+
+ {#snippet leading()}
+
+ {/snippet}
+ {#snippet title()}
+ Directory
+ {/snippet}
+ {#snippet action()}
+
+
+
+ {#if $userIsAdmin}
+
+
+ Invite members
+
+
+
(menuOpen = !menuOpen)}>
+
+
+ {#if menuOpen}
+
(menuOpen = false)}>
+
+
+
+
+ Manage Roles
+
+
+
+
+
+ Banned Members
+
+
+
+
+ {/if}
+
+ {/if}
+ {#if showSearch}
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+
+ {/if}
+ {/snippet}
+
+
+
+ {#if $members.length === 0}
+
+
+
No members yet!
+ {#if $userIsAdmin}
+
Invite people to get started.
+
+
+ Invite members
+
+ {/if}
+
+ {:else if term.trim() && visibleMembers.length === 0}
+ No matches found.
+ {:else}
+
+ {#each visibleMembers as { pubkey, roleList } (pubkey)}
+
+ {/each}
+
+ {/if}
+