forked from coracle/flotilla
Compare commits
5 Commits
9f386f6968
...
964ef441ec
| Author | SHA1 | Date | |
|---|---|---|---|
| 964ef441ec | |||
| 796f37d320 | |||
| b46fd94578 | |||
| bdc8e75640 | |||
| ef08821796 |
@@ -279,7 +279,7 @@
|
||||
</div>
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
||||
<PageContent class="flex flex-col-reverse gap-2 py-4">
|
||||
{#if missingRelayLists.length > 0}
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $members.length > 0}
|
||||
{#if $members !== undefined && $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>
|
||||
@@ -251,6 +251,10 @@
|
||||
</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>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card2 card2-sm bg-alt col-4">
|
||||
<strong class="text-lg">Room Settings</strong>
|
||||
|
||||
@@ -73,34 +73,44 @@
|
||||
</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#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>
|
||||
{/if}
|
||||
{#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}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!$spaceMembers) {
|
||||
addMembers()
|
||||
return
|
||||
}
|
||||
|
||||
const pubkeysSnapshot = $state.snapshot(pubkeys)
|
||||
const nonSpaceMembers = pubkeysSnapshot.filter(pubkey => !$spaceMembers.includes(pubkey))
|
||||
|
||||
|
||||
@@ -112,46 +112,58 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#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>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $members === undefined}
|
||||
<div class="card2 bg-base-200 p-4">
|
||||
<span class="text-error">Member list not available from this space</span>
|
||||
</div>
|
||||
{/each}
|
||||
{:else 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>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -181,7 +181,11 @@
|
||||
<li>
|
||||
<Button onclick={showMembers}>
|
||||
<Icon icon={UserRounded} />
|
||||
View Members ({$members.length})
|
||||
{#if $members === undefined}
|
||||
View Members
|
||||
{:else}
|
||||
View Members ({$members.length})
|
||||
{/if}
|
||||
</Button>
|
||||
</li>
|
||||
{#if $userIsAdmin}
|
||||
|
||||
@@ -766,9 +766,10 @@ export const addSpaceMembers = async (
|
||||
pubkeys: string[],
|
||||
): Promise<string | undefined> => {
|
||||
const spaceMembers = get(deriveSpaceMembers(url))
|
||||
|
||||
const results = await Promise.all(
|
||||
pubkeys
|
||||
.filter(pubkey => !spaceMembers.includes(pubkey))
|
||||
.filter(pubkey => !spaceMembers || !spaceMembers.includes(pubkey))
|
||||
.map(pubkey =>
|
||||
manageRelay(url, {
|
||||
method: ManagementMethod.AllowPubkey,
|
||||
|
||||
+106
-83
@@ -808,36 +808,49 @@ export const deriveOtherRooms = (url: string) =>
|
||||
|
||||
// Space/room memberships
|
||||
|
||||
const getSpaceMembers = (_url: string, events: TrustedEvent[]) => {
|
||||
const members = new Set<string>()
|
||||
export const deriveSpaceMembers = (url: string) =>
|
||||
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
|
||||
uniq(getTagValues("member", event?.tags ?? [])),
|
||||
)
|
||||
|
||||
for (const event of sortEventsAsc(events)) {
|
||||
if (event.kind === RELAY_MEMBERS) {
|
||||
members.clear()
|
||||
export const deriveRoomMembers = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
|
||||
|
||||
for (const pubkey of uniq(getTagValues("member", event.tags))) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
return derived(deriveEventsForUrl(url, filters), ([event]) =>
|
||||
uniq(getPubkeyTagValues(event?.tags ?? [])),
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
export type BannedPubkeyItem = {
|
||||
pubkey: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
|
||||
|
||||
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||
const store = writable(spaceBannedPubkeyItems.get(url) || [])
|
||||
|
||||
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
|
||||
spaceBannedPubkeyItems.set(url, res.result)
|
||||
store.set(res.result)
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
|
||||
if (event.kind === RELAY_ADD_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === RELAY_REMOVE_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.delete(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(members)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
||||
@@ -876,53 +889,6 @@ const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
||||
return Array.from(members)
|
||||
}
|
||||
|
||||
export const deriveSpaceMembers = (url: string) =>
|
||||
derived(
|
||||
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
|
||||
$events => getSpaceMembers(url, $events),
|
||||
)
|
||||
|
||||
export type BannedPubkeyItem = {
|
||||
pubkey: string
|
||||
reason: string
|
||||
}
|
||||
|
||||
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
|
||||
|
||||
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||
const store = writable(spaceBannedPubkeyItems.get(url) || [])
|
||||
|
||||
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
|
||||
spaceBannedPubkeyItems.set(url, res.result)
|
||||
store.set(res.result)
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const deriveRoomMembers = (url: string, h: string) => {
|
||||
const filters: Filter[] = [
|
||||
{kinds: [ROOM_MEMBERS], "#d": [h]},
|
||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
||||
]
|
||||
|
||||
return derived(deriveEventsForUrl(url, filters), $events => getRoomMembers(url, h, $events))
|
||||
}
|
||||
|
||||
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 []
|
||||
})
|
||||
}
|
||||
|
||||
// Action items (admin review queue)
|
||||
// const pendingJoins: TrustedEvent[] = []
|
||||
|
||||
@@ -1019,19 +985,49 @@ export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
|
||||
})
|
||||
|
||||
export const deriveUserSpaceMembershipStatus = (url: string) => {
|
||||
const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]
|
||||
// Fetch member list and user add/remove events directly in this derivation.
|
||||
const memberListFilters: Filter[] = [{kinds: [RELAY_MEMBERS]}]
|
||||
const userEventFilters: Filter[] = [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}]
|
||||
|
||||
return derived(
|
||||
[
|
||||
pubkey,
|
||||
deriveSpaceMembers(url),
|
||||
deriveEventsForUrl(url, filters),
|
||||
deriveRelaySignedEvents(url, memberListFilters),
|
||||
deriveRelaySignedEvents(url, userEventFilters),
|
||||
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
|
||||
deriveUserIsSpaceAdmin(url),
|
||||
],
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
([$pubkey, $memberListEvents, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
||||
// If admin, always granted.
|
||||
if ($isAdmin) {
|
||||
return MembershipStatus.Granted
|
||||
}
|
||||
|
||||
for (const event of $events) {
|
||||
const membersEvent = $memberListEvents.find(spec({kind: RELAY_MEMBERS}))
|
||||
const memberList = membersEvent ? uniq(getTagValues("member", membersEvent.tags)) : undefined
|
||||
|
||||
let isMember = false
|
||||
|
||||
if (memberList) {
|
||||
// Member list exists - check if user is in it.
|
||||
isMember = memberList.includes($pubkey!)
|
||||
} else {
|
||||
// No member list available - replay the user's add/remove history.
|
||||
for (const event of sortBy(e => e.created_at, $userAddRemoveEvents)) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.kind === RELAY_ADD_MEMBER) {
|
||||
isMember = true
|
||||
} else if (event.kind === RELAY_REMOVE_MEMBER) {
|
||||
isMember = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of $joinLeaveEvents) {
|
||||
// Join events indicate pending or granted status, leave resets to initial.
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
@@ -1057,19 +1053,46 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
||||
)
|
||||
|
||||
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
|
||||
// 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]}]
|
||||
const joinLeaveFilters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
|
||||
|
||||
return derived(
|
||||
[
|
||||
pubkey,
|
||||
deriveRoomMembers(url, h),
|
||||
deriveEventsForUrl(url, filters),
|
||||
deriveEventsForUrl(url, userEventFilters),
|
||||
deriveEventsForUrl(url, joinLeaveFilters),
|
||||
deriveUserIsRoomAdmin(url, h),
|
||||
],
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
([$pubkey, $memberList, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
||||
// If admin of this room's space, always granted.
|
||||
if ($isAdmin) {
|
||||
return MembershipStatus.Granted
|
||||
}
|
||||
|
||||
for (const event of $events) {
|
||||
let isMember = false
|
||||
|
||||
if ($memberList) {
|
||||
// Member list exists - check if user is in it.
|
||||
isMember = $memberList.includes($pubkey!)
|
||||
} else {
|
||||
// No member list available - replay the user's add/remove history.
|
||||
for (const event of sortEventsAsc($userAddRemoveEvents)) {
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.kind === ROOM_ADD_MEMBER) {
|
||||
isMember = true
|
||||
} else if (event.kind === ROOM_REMOVE_MEMBER) {
|
||||
isMember = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of $joinLeaveEvents) {
|
||||
// Join events indicate pending or granted status, leave resets to initial.
|
||||
if (event.pubkey !== $pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ const syncSpace = (url: string) => {
|
||||
{kinds: [MESSAGE, ...CONTENT_KINDS], since, "#h": [room]},
|
||||
makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}),
|
||||
{
|
||||
kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
|
||||
kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE],
|
||||
"#h": [room],
|
||||
},
|
||||
{kinds: [PollResponse], since},
|
||||
@@ -293,15 +293,15 @@ const syncSpace = (url: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const relayKinds = [RELAY_MEMBERS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]
|
||||
const relayKinds = [RELAY_MEMBERS]
|
||||
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
|
||||
const roomMemberKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
|
||||
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
|
||||
|
||||
pullAndListen({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, ...CONTENT_KINDS, MESSAGE]},
|
||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]},
|
||||
makeCommentFilter(CONTENT_KINDS, {since}),
|
||||
{kinds: [PollResponse], since},
|
||||
],
|
||||
|
||||
@@ -79,7 +79,7 @@ export const makeEditor = async ({
|
||||
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
||||
sortFn: ({score = 1, item}) => {
|
||||
const wotScore = getWotGraph().get(item.event.pubkey) || 0
|
||||
const membershipScale = $spaceMembers.includes(item.event.pubkey) ? 2 : 1
|
||||
const membershipScale = $spaceMembers?.includes(item.event.pubkey) ? 2 : 1
|
||||
|
||||
return dec(score) * inc(wotScore / getMaxWot()) * membershipScale
|
||||
},
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="9" cy="6" r="1.5" fill="#000000"/>
|
||||
<circle cx="15" cy="6" r="1.5" fill="#000000"/>
|
||||
<circle cx="9" cy="12" r="1.5" fill="#000000"/>
|
||||
<circle cx="15" cy="12" r="1.5" fill="#000000"/>
|
||||
<circle cx="9" cy="18" r="1.5" fill="#000000"/>
|
||||
<circle cx="15" cy="18" r="1.5" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -1,53 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
type Props = {
|
||||
children: Snippet
|
||||
root?: HTMLElement
|
||||
initiallyVisible?: boolean
|
||||
estimatedHeight?: number
|
||||
}
|
||||
|
||||
const {children, root, initiallyVisible = false, estimatedHeight = 48}: Props = $props()
|
||||
|
||||
let visible = $state(initiallyVisible)
|
||||
let height = $state(estimatedHeight)
|
||||
let el: HTMLElement | undefined = $state()
|
||||
let hasMeasured = false
|
||||
|
||||
$effect(() => {
|
||||
if (!el) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
visible = true
|
||||
} else {
|
||||
// Measure actual height before hiding content
|
||||
if (el) {
|
||||
const h = el.offsetHeight
|
||||
if (h > 0) {
|
||||
height = h
|
||||
hasMeasured = true
|
||||
}
|
||||
}
|
||||
if (hasMeasured) {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
},
|
||||
{root: root || null, rootMargin: "1000px 0px"},
|
||||
)
|
||||
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={el}>
|
||||
{#if visible}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<div style:height="{height}px"></div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<Page>
|
||||
<ContentSearch>
|
||||
{#snippet input()}
|
||||
<label class="row-2 input input-bordered">
|
||||
<label class="row-2 input input-bordered w-full">
|
||||
<Icon icon={Magnifier} />
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
|
||||
@@ -58,14 +58,14 @@
|
||||
<RelaySettingsItem
|
||||
icon={Inbox}
|
||||
title="Inbox Relays"
|
||||
subtitle="Where you send your public notes. Be sure to select relays that will accept your notes, and which will let people who follow you read them."
|
||||
subtitle="Where other people should send notes intended for you. Be sure to select relays that will accept notes that tag you."
|
||||
relays={readRelayUrls}
|
||||
addRelay={addReadRelay}
|
||||
removeRelay={removeReadRelay} />
|
||||
<RelaySettingsItem
|
||||
icon={Plane}
|
||||
title="Outbox Relays"
|
||||
subtitle="Where other people should send notes intended for you. Be sure to select relays that will accept notes that tag you."
|
||||
subtitle="Where you send your public notes. Be sure to select relays that will accept your notes, and which will let people who follow you read them."
|
||||
relays={writeRelayUrls}
|
||||
addRelay={addWriteRelay}
|
||||
removeRelay={removeWriteRelay} />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {onMount, tick} from "svelte"
|
||||
import {flip} from "svelte/animate"
|
||||
import {cubicOut} from "svelte/easing"
|
||||
import {derived as _derived} from "svelte/store"
|
||||
import {dec, insertAt, removeAt, sleep} from "@welshman/lib"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
@@ -7,6 +9,7 @@
|
||||
import {relays, createSearch} from "@welshman/app"
|
||||
import {createScroller} from "@lib/html"
|
||||
import {fly} from "@lib/transition"
|
||||
import DragHandle from "@assets/icons/drag-handle.svg?dataurl"
|
||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
@@ -98,6 +101,8 @@
|
||||
const onDragStart = (e: DragEvent, url: string) => {
|
||||
draggedUrl = url
|
||||
dragStartOrder = [...orderedSpaceUrls]
|
||||
lastDragTarget = undefined
|
||||
didDrop = false
|
||||
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = "move"
|
||||
@@ -105,15 +110,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
const onDragOver = (e: DragEvent, targetUrl: string) => {
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const onDragEnter = (e: DragEvent, targetUrl: string) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (lastDragTarget === targetUrl) return
|
||||
|
||||
lastDragTarget = targetUrl
|
||||
reorderSpaceUrls(targetUrl)
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent, targetUrl: string) => {
|
||||
e.preventDefault()
|
||||
reorderSpaceUrls(targetUrl)
|
||||
didDrop = true
|
||||
draggedUrl = undefined
|
||||
lastDragTarget = undefined
|
||||
|
||||
if (dragStartOrder && !isSameOrder(dragStartOrder, orderedSpaceUrls)) {
|
||||
void setSpaceMembershipOrder(orderedSpaceUrls).catch(console.error)
|
||||
@@ -123,8 +138,14 @@
|
||||
}
|
||||
|
||||
const onDragEnd = () => {
|
||||
if (!didDrop && dragStartOrder && !isSameOrder(dragStartOrder, orderedSpaceUrls)) {
|
||||
orderedSpaceUrls = dragStartOrder
|
||||
}
|
||||
|
||||
draggedUrl = undefined
|
||||
dragStartOrder = undefined
|
||||
lastDragTarget = undefined
|
||||
didDrop = false
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -143,6 +164,8 @@
|
||||
let orderedSpaceUrls = $state<string[]>([])
|
||||
let draggedUrl = $state<string | undefined>()
|
||||
let dragStartOrder = $state<string[] | undefined>()
|
||||
let lastDragTarget = $state<string | undefined>()
|
||||
let didDrop = $state(false)
|
||||
|
||||
const openSearch = () => {
|
||||
showSearch = true
|
||||
@@ -247,17 +270,25 @@
|
||||
<Divider>Your spaces</Divider>
|
||||
{#each filteredUserUrls as url (url)}
|
||||
<div
|
||||
class:opacity-60={draggedUrl === url}
|
||||
animate:flip={{duration: 300, easing: cubicOut}}
|
||||
class="transition-opacity duration-200 {draggedUrl === url ? 'opacity-50' : ''}"
|
||||
draggable="true"
|
||||
role="listitem"
|
||||
ondragstart={e => onDragStart(e, url)}
|
||||
ondragover={e => onDragOver(e, url)}
|
||||
ondragover={onDragOver}
|
||||
ondragenter={e => onDragEnter(e, url)}
|
||||
ondrop={e => onDrop(e, url)}
|
||||
ondragend={onDragEnd}>
|
||||
<Button
|
||||
class="card2 bg-alt shadow-md transition-all hover:shadow-lg hover:dark:brightness-[1.1] w-full relative"
|
||||
class="group card2 bg-alt shadow-md transition-all hover:shadow-lg hover:dark:brightness-[1.1] w-full relative min-w-0"
|
||||
onclick={() => openSpace(url)}>
|
||||
<RelaySummary hideFavorites {url} />
|
||||
<div class="flex w-full items-start gap-2">
|
||||
<div
|
||||
class="mt-4 flex cursor-grab p-1 text-base-content/30 transition-colors group-hover:text-base-content/60">
|
||||
<Icon icon={DragHandle} />
|
||||
</div>
|
||||
<RelaySummary hideFavorites {url} />
|
||||
</div>
|
||||
{#if $notifications.has(makeSpacePath(url))}
|
||||
<div class="absolute right-3 top-3 h-2 w-2 rounded-full bg-primary"></div>
|
||||
{/if}
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||
import VirtualItem from "@lib/components/VirtualItem.svelte"
|
||||
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
|
||||
import {canEnforceNip70, prependParent, publishDelete} from "@app/core/commands"
|
||||
import {
|
||||
@@ -486,23 +485,21 @@
|
||||
{:else if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else if shouldVirtualize}
|
||||
<VirtualItem root={element} initiallyVisible={i < 25}>
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === ROOM_ADD_MEMBER}
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else}
|
||||
<div class="cv">
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
{showPubkey}
|
||||
{addSpaceBelow}
|
||||
canEdit={canEditEvent}
|
||||
onEdit={onEditEvent} />
|
||||
</div>
|
||||
{/if}
|
||||
</VirtualItem>
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === ROOM_ADD_MEMBER}
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else}
|
||||
<div class="cv">
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
{showPubkey}
|
||||
{addSpaceBelow}
|
||||
canEdit={canEditEvent}
|
||||
onEdit={onEditEvent} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === ROOM_ADD_MEMBER}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||
import RoomItem from "@app/components/RoomItem.svelte"
|
||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||
import VirtualItem from "@lib/components/VirtualItem.svelte"
|
||||
|
||||
import RoomCompose from "@app/components/RoomCompose.svelte"
|
||||
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
|
||||
@@ -324,23 +323,21 @@
|
||||
{:else if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else if shouldVirtualize}
|
||||
<VirtualItem root={element} initiallyVisible={i < 25}>
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === RELAY_ADD_MEMBER}
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else}
|
||||
<div>
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
{showPubkey}
|
||||
canEdit={canEditEvent}
|
||||
onEdit={onEditEvent}
|
||||
{addSpaceBelow} />
|
||||
</div>
|
||||
{/if}
|
||||
</VirtualItem>
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === RELAY_ADD_MEMBER}
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else}
|
||||
<div>
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
{showPubkey}
|
||||
canEdit={canEditEvent}
|
||||
onEdit={onEditEvent}
|
||||
{addSpaceBelow} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === RELAY_ADD_MEMBER}
|
||||
|
||||
Reference in New Issue
Block a user