From c44c3793fa131922ea080eb8202a0d71b16a36b9 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 22 Jun 2026 16:06:00 -0700 Subject: [PATCH] Split up space information and directory --- src/app.css | 7 +- src/app/components/CalendarEventItem.svelte | 2 +- src/app/components/ClassifiedItem.svelte | 2 +- src/app/components/ContentLinkBlock.svelte | 4 +- src/app/components/ContentQuote.svelte | 2 +- src/app/components/EditFeaturedContent.svelte | 76 ++++++ src/app/components/GoalItem.svelte | 2 +- src/app/components/NoteItem.svelte | 2 +- src/app/components/RecentConversation.svelte | 2 +- src/app/components/RecentItem.svelte | 34 +++ src/app/components/SpaceDetails.svelte | 120 --------- .../components/SpaceFeaturedContent.svelte | 51 ++++ src/app/components/SpaceMember.svelte | 92 +++---- src/app/components/SpaceMemberRoles.svelte | 4 +- src/app/components/SpaceMembersSummary.svelte | 76 ++++++ src/app/components/SpaceMenu.svelte | 5 +- src/app/components/SpaceRecentSummary.svelte | 38 +++ src/app/components/ThreadItem.svelte | 2 +- src/app/featured.ts | 49 ++++ src/app/recent.ts | 67 +++++ src/app/routes.ts | 2 +- src/app/sync.ts | 3 + src/lib/components/Dialog.svelte | 2 +- src/routes/spaces/[relay]/about/+page.svelte | 229 +++++++----------- .../spaces/[relay]/directory/+page.svelte | 150 ++++++++++++ src/routes/spaces/[relay]/recent/+page.svelte | 116 +-------- src/routes/spaces/create/+page.svelte | 4 +- 27 files changed, 710 insertions(+), 433 deletions(-) create mode 100644 src/app/components/EditFeaturedContent.svelte create mode 100644 src/app/components/RecentItem.svelte delete mode 100644 src/app/components/SpaceDetails.svelte create mode 100644 src/app/components/SpaceFeaturedContent.svelte create mode 100644 src/app/components/SpaceMembersSummary.svelte create mode 100644 src/app/components/SpaceRecentSummary.svelte create mode 100644 src/app/featured.ts create mode 100644 src/app/recent.ts create mode 100644 src/routes/spaces/[relay]/directory/+page.svelte diff --git a/src/app.css b/src/app.css index 5d35be3c..d9ed14b7 100644 --- a/src/app.css +++ b/src/app.css @@ -85,7 +85,7 @@ } @utility card2 { - @apply rounded-box text-base-content p-4 sm:p-6; + @apply rounded-box text-base-content border-base-content/20 border border-solid p-4 sm:p-6 shadow-xl/5 bg-base-100; } @utility column { @@ -276,6 +276,11 @@ @apply text-base-content p-2 sm:p-4; } +.card2 .card2, +.dialog .card2 { + @apply shadow-none; +} + [data-tip]::before { @apply overflow-hidden text-ellipsis; } diff --git a/src/app/components/CalendarEventItem.svelte b/src/app/components/CalendarEventItem.svelte index 01e89143..1447ca56 100644 --- a/src/app/components/CalendarEventItem.svelte +++ b/src/app/components/CalendarEventItem.svelte @@ -19,7 +19,7 @@
diff --git a/src/app/components/ClassifiedItem.svelte b/src/app/components/ClassifiedItem.svelte index 84ba53c1..6c7136c5 100644 --- a/src/app/components/ClassifiedItem.svelte +++ b/src/app/components/ClassifiedItem.svelte @@ -25,7 +25,7 @@ {#if title}
diff --git a/src/app/components/ContentLinkBlock.svelte b/src/app/components/ContentLinkBlock.svelte index e6dfc90b..07f68df3 100644 --- a/src/app/components/ContentLinkBlock.svelte +++ b/src/app/components/ContentLinkBlock.svelte @@ -77,7 +77,7 @@
{:then preview} -
+
{#if preview.image && !hideImage}
{:catch} -

+

Unable to load a preview for {url}

{/await} diff --git a/src/app/components/ContentQuote.svelte b/src/app/components/ContentQuote.svelte index 1f847a7a..52cd6786 100644 --- a/src/app/components/ContentQuote.svelte +++ b/src/app/components/ContentQuote.svelte @@ -53,7 +53,7 @@
{:else} - + {/if} diff --git a/src/app/components/EditFeaturedContent.svelte b/src/app/components/EditFeaturedContent.svelte new file mode 100644 index 00000000..b427617c --- /dev/null +++ b/src/app/components/EditFeaturedContent.svelte @@ -0,0 +1,76 @@ + + + + + + Featured Content + on + + + {#snippet info()} +

Each entry is shown on the space's About page. Links will be fetched and displayed automatically.

+ {/snippet} + {#snippet input()} + + {#snippet addLabel()} + Add content + {/snippet} + + {/snippet} +
+
+ + + + +
diff --git a/src/app/components/GoalItem.svelte b/src/app/components/GoalItem.svelte index 0c3c62bb..6f279de8 100644 --- a/src/app/components/GoalItem.svelte +++ b/src/app/components/GoalItem.svelte @@ -21,7 +21,7 @@

{event.content}

- +
diff --git a/src/app/components/RecentConversation.svelte b/src/app/components/RecentConversation.svelte index 4ae6a510..4513d959 100644 --- a/src/app/components/RecentConversation.svelte +++ b/src/app/components/RecentConversation.svelte @@ -25,7 +25,7 @@ const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url)) - - {/if} -
- - {#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 $relay} - {@const {pubkey, software, version, supported_nips, limitation} = $relay} -
- {#if pubkey} -
- Administrator: -
- {/if} - {#if $relay?.contact} -
- Contact: {$relay.contact} -
- {/if} - {#if software} -
- Software: {software} -
- {/if} - {#if version} -
- Version: {version} -
- {/if} - {#if Array.isArray(supported_nips)} -

- Supported NIPs: {supported_nips.join(", ")} -

- {/if} - {#if limitation?.auth_required} -

- Auth Required -

- {/if} - {#if limitation?.payment_required} -

- Payment Required -

- {/if} - {#if limitation?.min_pow_difficulty} -

- Min PoW: {limitation?.min_pow_difficulty} -

- {/if} -
- {/if} -
diff --git a/src/app/components/SpaceFeaturedContent.svelte b/src/app/components/SpaceFeaturedContent.svelte new file mode 100644 index 00000000..727eead3 --- /dev/null +++ b/src/app/components/SpaceFeaturedContent.svelte @@ -0,0 +1,51 @@ + + +{#if $content.length > 0 || canEdit} +
+
+

+ + Featured +

+ {#if canEdit} + + {/if} +
+ {#if $content.length === 0} +

No featured content yet.

+ {:else} +
+ {#each $content as value (value)} + + {/each} +
+ {/if} +
+{:else} + +{/if} diff --git a/src/app/components/SpaceMember.svelte b/src/app/components/SpaceMember.svelte index dbd02528..1a0e2834 100644 --- a/src/app/components/SpaceMember.svelte +++ b/src/app/components/SpaceMember.svelte @@ -3,8 +3,6 @@ import {displayProfileByPubkey} from "@welshman/app" import {fly} from "@lib/transition" import MenuDots from "@assets/icons/menu-dots.svg?dataurl" - import UserRounded from "@assets/icons/user-rounded.svg?dataurl" - import Letter from "@assets/icons/letter-opened.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl" import UserMinus from "@assets/icons/user-minus.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" @@ -21,7 +19,6 @@ import {deriveSupportedMethods} from "@app/relays" import {pushModal} from "@app/modal" import {pushToast} from "@app/toast" - import {goToChat} from "@app/routes" interface Props { url: string @@ -47,11 +44,6 @@ pushModal(ProfileDetail, {pubkey, url}) } - const sendMessage = () => { - menuOpen = false - goToChat([pubkey]) - } - const editRoles = () => { menuOpen = false pushModal(SpaceMemberRoles, {url, pubkey}) @@ -94,7 +86,7 @@ } -
+
-
- - {#if menuOpen} - - - - {/if} -
+ {#if canAssign || canUnassign || canUnallow || canBan} +
+ + {#if menuOpen} + + + + {/if} +
+ {/if} diff --git a/src/app/components/SpaceMemberRoles.svelte b/src/app/components/SpaceMemberRoles.svelte index d600bf08..d1a44931 100644 --- a/src/app/components/SpaceMemberRoles.svelte +++ b/src/app/components/SpaceMemberRoles.svelte @@ -90,13 +90,13 @@ {:else}
{#each $roles as role (role.id)} -
diff --git a/src/app/components/SpaceMembersSummary.svelte b/src/app/components/SpaceMembersSummary.svelte new file mode 100644 index 00000000..e91d27a5 --- /dev/null +++ b/src/app/components/SpaceMembersSummary.svelte @@ -0,0 +1,76 @@ + + +
+

+ + Members +

+ {#if admins.length > 0} +
+

Admins

+ {#each admins as pubkey (pubkey)} + + {/each} +
+ {/if} + {#if $newMembers.length > 0} +
+

New members

+ {#each $newMembers as pubkey (pubkey)} + + {/each} +
+ {/if} + + View all members + + +
diff --git a/src/app/components/SpaceMenu.svelte b/src/app/components/SpaceMenu.svelte index bb4b53d2..278b427d 100644 --- a/src/app/components/SpaceMenu.svelte +++ b/src/app/components/SpaceMenu.svelte @@ -5,7 +5,7 @@ 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 UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl" import Home from "@assets/icons/home.svg?dataurl" import Danger from "@assets/icons/danger.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl" @@ -219,6 +219,9 @@ Chat {/if} + + Directory + {#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)} Goals diff --git a/src/app/components/SpaceRecentSummary.svelte b/src/app/components/SpaceRecentSummary.svelte new file mode 100644 index 00000000..2317e2ef --- /dev/null +++ b/src/app/components/SpaceRecentSummary.svelte @@ -0,0 +1,38 @@ + + +
+

+ + Recent Activity +

+ {#if $recentActivity.length === 0} +

No recent activity yet.

+ {:else} +
+ {#each $recentActivity.slice(0, 3) as item (item.event.id)} + + {/each} +
+ {/if} + + View all recent activity + + +
diff --git a/src/app/components/ThreadItem.svelte b/src/app/components/ThreadItem.svelte index 405fdfbe..e0c8ea73 100644 --- a/src/app/components/ThreadItem.svelte +++ b/src/app/components/ThreadItem.svelte @@ -21,7 +21,7 @@ {#if title}
diff --git a/src/app/featured.ts b/src/app/featured.ts new file mode 100644 index 00000000..f7df5d18 --- /dev/null +++ b/src/app/featured.ts @@ -0,0 +1,49 @@ +import {derived} from "svelte/store" +import {first, now} from "@welshman/lib" +import {APP_DATA, getTagValues} from "@welshman/util" +import type {ManagementMethod} from "@welshman/util" +import {publish} from "@welshman/net" +import {deriveRelay, manageRelay} from "@welshman/app" +import {deriveEventsForUrl} from "@app/repository" + +// NIP-78 app data published by the relay's self key. Each featured entry is a +// ["content", ] tag (freeform text, intended to be a url or nevent). +export const FEATURED_CONTENT_D = "flotilla/featured-content" + +export const deriveFeaturedContent = (url: string) => + derived( + [deriveRelay(url), deriveEventsForUrl(url, [{kinds: [APP_DATA], "#d": [FEATURED_CONTENT_D]}])], + ([$relay, $events]) => { + const self = $relay?.self || $relay?.pubkey + const event = (self && $events.find(e => e.pubkey === self)) || first($events) + + return getTagValues("content", event?.tags ?? []) + }, + ) + +// Publish the featured content list by asking the relay to sign it with its self +// key (the unofficial NIP-86 "signevent" method). +export const setFeaturedContent = async ( + url: string, + content: string[], +): Promise => { + const template = { + kind: APP_DATA, + created_at: now(), + content: "", + tags: [ + ["d", FEATURED_CONTENT_D], + ...content + .map(value => value.trim()) + .filter(Boolean) + .map(value => ["content", value]), + ], + } + + const {error} = await manageRelay(url, { + method: "signevent" as ManagementMethod, + params: [template], + }) + + return error +} diff --git a/src/app/recent.ts b/src/app/recent.ts new file mode 100644 index 00000000..a912a536 --- /dev/null +++ b/src/app/recent.ts @@ -0,0 +1,67 @@ +import {derived} from "svelte/store" +import {groupBy, first, sortBy, uniqBy, ago, MONTH} from "@welshman/lib" +import {MESSAGE, COMMENT, getTagValue, getTagValues, getIdAndAddress} from "@welshman/util" +import type {TrustedEvent} from "@welshman/util" +import {repository} from "@welshman/app" +import {deriveEventsForUrl} from "@app/repository" +import {CONTENT_KINDS} from "@app/content" + +export type RecentActivityItem = { + type: "message" | "content" + event: TrustedEvent + count: number + timestamp: number +} + +// Recent activity for a space: latest message per room plus content with the +// most recent activity (post or comment), sorted newest first. +export const deriveRecentActivity = (url: string) => { + const since = ago(3, MONTH) + const messages = deriveEventsForUrl(url, [{kinds: [MESSAGE], since}]) + const content = deriveEventsForUrl(url, [{kinds: CONTENT_KINDS, since}]) + const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], since}]) + + return derived([messages, content, comments], ([$messages, $content, $comments]) => { + const activity: RecentActivityItem[] = [] + + const byRoom = groupBy(e => getTagValue("h", e.tags), $messages) + for (const roomMessages of byRoom.values()) { + const latest = first(roomMessages) + if (latest) { + activity.push({ + type: "message", + event: latest, + count: roomMessages.length, + timestamp: latest.created_at, + }) + } + } + + const latestActivityByKey = new Map() + + for (const event of $content) { + for (const k of getIdAndAddress(event)) { + latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at)) + } + } + + for (const event of $comments) { + for (const k of getTagValues(["E", "A"], event.tags)) { + latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at)) + } + } + + for (const [address, timestamp] of latestActivityByKey.entries()) { + const event = repository.getEvent(address) + + if (event) { + activity.push({type: "content", event, timestamp, count: 1}) + } + } + + return sortBy( + a => -a.timestamp, + uniqBy(a => a.event.id, activity), + ) + }) +} diff --git a/src/app/routes.ts b/src/app/routes.ts index 95ab4a42..2f523445 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -87,7 +87,7 @@ export const goToSpace = async (url: string) => { } else if (!hasNip29(getRelay(url))) { goto(makeSpaceChatPath(url), {replaceState: true}) } else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) { - goto(makeSpacePath(url, "recent"), {replaceState: true}) + goto(makeSpacePath(url, "about"), {replaceState: true}) } else { goto(makeSpacePath(url), {replaceState: true}) } diff --git a/src/app/sync.ts b/src/app/sync.ts index be4d3a7d..72af9717 100644 --- a/src/app/sync.ts +++ b/src/app/sync.ts @@ -20,6 +20,7 @@ import { RELAY_REMOVE_MEMBER, MESSAGE, POLL_RESPONSE, + APP_DATA, isSignedEvent, unionFilters, } from "@welshman/util" @@ -54,6 +55,7 @@ import { import {decodeRelay} from "@app/relays" import {loadFeedsForPubkey} from "@app/feeds" import {RELAY_ROLE} from "@app/members" +import {FEATURED_CONTENT_D} from "@app/featured" import {hasBlossomSupport} from "@app/uploads" import {LIVEKIT_PARTICIPANTS} from "@app/call/voice" @@ -278,6 +280,7 @@ const syncSpace = (url: string) => { signal: controller.signal, filters: [ {kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]}, + {kinds: [APP_DATA], "#d": [FEATURED_CONTENT_D]}, {kinds: [...REACTION_KINDS, POLL_RESPONSE], since}, makeCommentFilter(CONTENT_KINDS, {since}), ], diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte index 150bdc0b..136ead54 100644 --- a/src/lib/components/Dialog.svelte +++ b/src/lib/components/Dialog.svelte @@ -44,7 +44,7 @@ ) -
+
- {#if $userIsAdmin} -
- - {#if menuOpen} - (menuOpen = false)}> - - - {/if} -
+
+
+ +
+
+

+ +

+

{displayRelayUrl(url)}

+
+
+ {#if $userIsAdmin} + + {/if} +
+ + {#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 visibleMembers.length === 0} -

No members found.

- {:else} -
- {#each visibleMembers as { pubkey, roleList } (pubkey)} - - {/each} + {/if} + {#if $relay} + {@const {pubkey, software, version, supported_nips, limitation} = $relay} +
+ {#if pubkey} +
+ Administrator: +
+ {/if} + {#if $relay?.contact} +
+ Contact: {$relay.contact} +
+ {/if} + {#if software} +
+ Software: {software} +
+ {/if} + {#if version} +
+ Version: {version} +
+ {/if} + {#if Array.isArray(supported_nips)} +

+ Supported NIPs: {supported_nips.join(", ")} +

+ {/if} + {#if limitation?.auth_required} +

+ Auth Required +

+ {/if} + {#if limitation?.payment_required} +

+ Payment Required +

+ {/if} + {#if limitation?.min_pow_difficulty} +

+ Min PoW: {limitation?.min_pow_difficulty} +

+ {/if}
{/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..0caf4040 --- /dev/null +++ b/src/routes/spaces/[relay]/directory/+page.svelte @@ -0,0 +1,150 @@ + + + + {#snippet leading()} + + {/snippet} + {#snippet title()} + Members + {/snippet} + {#snippet action()} + + {#if $userIsAdmin} +
+ + {#if menuOpen} + (menuOpen = false)}> + + + {/if} +
+ {/if} + {/snippet} +
+ + +
+ + {#if visibleMembers.length === 0} +

No members found.

+ {:else} +
+ {#each visibleMembers as { pubkey, roleList } (pubkey)} + + {/each} +
+ {/if} +
+
diff --git a/src/routes/spaces/[relay]/recent/+page.svelte b/src/routes/spaces/[relay]/recent/+page.svelte index 3fdd2d1a..ebe62f42 100644 --- a/src/routes/spaces/[relay]/recent/+page.svelte +++ b/src/routes/spaces/[relay]/recent/+page.svelte @@ -1,38 +1,11 @@