Improve group membership detection

This commit is contained in:
Jon Staab
2025-05-28 16:46:41 -07:00
parent 72d85e5740
commit f7d11cf124
11 changed files with 223 additions and 118 deletions
+1 -6
View File
@@ -17,7 +17,6 @@
MESSAGE,
INBOX_RELAYS,
DIRECT_MESSAGE,
GROUP_META,
MUTES,
FOLLOWS,
PROFILE,
@@ -174,11 +173,7 @@
limit: 10_000,
repository,
rankEvent: (e: TrustedEvent) => {
if (
[PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, INBOX_RELAYS, GROUP_META].includes(
e.kind,
)
) {
if ([PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, INBOX_RELAYS].includes(e.kind)) {
return 1
}
+4 -3
View File
@@ -20,7 +20,6 @@
import {
hasNip29,
decodeRelay,
channelIsLocked,
makeChannelId,
channelsById,
deriveUserRooms,
@@ -155,9 +154,10 @@
</Link>
{#each $userRooms as room (room)}
{@const roomPath = makeRoomPath(url, room)}
{@const channel = $channelsById.get(makeChannelId(url, room))}
<Link href={roomPath} class="btn btn-neutral relative">
<div class="flex min-w-0 items-center gap-2 overflow-hidden text-nowrap">
{#if channelIsLocked($channelsById.get(makeChannelId(url, room)))}
{#if channel?.closed || channel?.private}
<Icon icon="lock" size={4} />
{:else}
<Icon icon="hashtag" />
@@ -173,9 +173,10 @@
<Divider>Other Rooms</Divider>
<div class="grid grid-cols-3 gap-2">
{#each $otherRooms as room (room)}
{@const channel = $channelsById.get(makeChannelId(url, room))}
<Link href={makeRoomPath(url, room)} class="btn btn-neutral">
<div class="relative flex min-w-0 items-center gap-2 overflow-hidden text-nowrap">
{#if channelIsLocked($channelsById.get(makeChannelId(url, room)))}
{#if channel?.closed || channel?.private}
<Icon icon="lock" size={4} />
{:else}
<Icon icon="hashtag" />
+156 -72
View File
@@ -1,12 +1,14 @@
<script lang="ts">
import cx from 'classnames'
import {readable} from "svelte/store"
import {onMount, onDestroy} from "svelte"
import {page} from "$app/stores"
import type {Readable} from "svelte/store"
import {now, formatTimestampAsDate} from "@welshman/lib"
import {request} from "@welshman/net"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
import {pubkey, publishThunk, deriveRelay} from "@welshman/app"
import {createEvent, MESSAGE, DELETE, REACTION, GROUP_ADD_USER, GROUP_REMOVE_USER} from "@welshman/util"
import {pubkey, publishThunk, deriveRelay, getThunkError, waitForThunkCompletion} from "@welshman/app"
import {slide, fade, fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -20,12 +22,15 @@
import ChannelCompose from "@app/components/ChannelCompose.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {
userRoomsByUrl,
userSettingValues,
decodeRelay,
tagRoom,
userRoomsByUrl,
displayChannel,
getEventsForUrl,
deriveUserMembershipStatus,
deriveChannel,
MembershipStatus,
} from "@app/state"
import {setChecked, checked} from "@app/notifications"
import {
@@ -34,7 +39,6 @@
addRoomMembership,
removeRoomMembership,
prependParent,
getThunkError,
} from "@app/commands"
import {PROTECTED} from "@app/state"
import {makeFeed} from "@app/requests"
@@ -45,25 +49,43 @@
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay)
const channel = deriveChannel(url, room)
const filter = {kinds: [MESSAGE], "#h": [room]}
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
const membershipStatus = deriveUserMembershipStatus(url, room)
const addFavorite = () => addRoomMembership(url, room)
const removeFavorite = () => removeRoomMembership(url, room)
const join = async () => {
joiningRoom = true
joining = true
const message = await getThunkError(joinRoom(url, room))
try {
const message = await getThunkError(joinRoom(url, room))
joiningRoom = false
if (message && !message.includes("already")) {
return pushToast({theme: "error", message})
if (message && !message.startsWith("duplicate:")) {
return pushToast({theme: "error", message})
} else {
// Restart the feed now that we're a member
start()
}
} finally {
joining = false
}
addRoomMembership(url, room, displayChannel(url, room))
}
const leave = () => {
leaveRoom(url, room)
removeRoomMembership(url, room)
const leave = async () => {
leaving = true
try {
const message = await getThunkError(leaveRoom(url, room))
if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message})
}
} finally {
leaving = false
}
}
const replyTo = (event: TrustedEvent) => {
@@ -124,7 +146,8 @@
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"})
let joiningRoom = $state(false)
let joining = $state(false)
let leaving = $state(false)
let loadingEvents = $state(true)
let share = $state(popKey<TrustedEvent | undefined>("share"))
let parent: TrustedEvent | undefined = $state()
@@ -196,8 +219,10 @@
return elements
})
onMount(() => {
;({events, cleanup} = makeFeed({
const start = () => {
cleanup?.()
const feed = makeFeed({
element: element!,
relays: [url],
feedFilters: [filter],
@@ -206,7 +231,25 @@
onExhausted: () => {
loadingEvents = false
},
}))
})
events = feed.events
cleanup = feed.cleanup
}
onMount(() => {
const controller = new AbortController()
const req = request({
signal: controller.signal,
relays: [url],
filters: [{
kinds: [GROUP_ADD_USER, GROUP_REMOVE_USER],
'#p': [$pubkey!],
'#h': [room],
limit: 10,
}],
})
const observer = new ResizeObserver(() => {
if (dynamicPadding && chatCompose) {
@@ -216,8 +259,10 @@
observer.observe(chatCompose!)
observer.observe(dynamicPadding!)
start()
return () => {
controller.abort()
observer.unobserve(chatCompose!)
observer.unobserve(dynamicPadding!)
}
@@ -246,21 +291,12 @@
{/snippet}
{#snippet action()}
<div class="row-2">
{#if $userRoomsByUrl.get(url)?.has(room)}
<Button class="btn btn-neutral btn-sm" onclick={leave}>
<Icon icon="arrows-a-logout-2" />
Leave Room
</Button>
{:else}
<Button class="btn btn-neutral btn-sm" disabled={joiningRoom} onclick={join}>
{#if joiningRoom}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon="login-2" />
{/if}
Join Room
</Button>
{/if}
<Button
class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip={isFavorite ? "Remove Favorite" : "Add Favorite"}
onclick={isFavorite ? removeFavorite : addFavorite}>
<Icon size={4} icon="bookmark" class={cx({'text-primary': isFavorite})} />
</Button>
<MenuSpaceButton {url} />
</div>
{/snippet}
@@ -268,48 +304,96 @@
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
<div bind:this={dynamicPadding}></div>
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "new-messages"}
<div
bind:this={newMessages}
class="flex items-center py-2 text-xs transition-colors"
class:opacity-0={showFixedNewMessages}>
<div class="h-px flex-grow bg-primary"></div>
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
<div class="h-px flex-grow bg-primary"></div>
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
<div class="py-20">
<div class="card2 col-8 m-auto max-w-md items-center text-center">
<p class="row-2">
You aren't currently a member of this room.
</p>
{#if $membershipStatus === MembershipStatus.Pending}
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
<Icon icon="clock-circle" />
Access Pending
</Button>
{:else}
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
{#if joining}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon="login-2" />
{/if}
Join Room
</Button>
{/if}
</div>
{:else if type === "date"}
<Divider>{value}</Divider>
{:else}
<div in:slide class:-mt-1={!showPubkey}>
<ChannelMessage
{url}
{room}
{replyTo}
event={$state.snapshot(value as TrustedEvent)}
{showPubkey} />
</div>
{/if}
{/each}
<p class="flex h-10 items-center justify-center py-20">
{#if loadingEvents}
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
{:else}
<Spinner>End of message history</Spinner>
{/if}
</p>
</div>
{:else}
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "new-messages"}
<div
bind:this={newMessages}
class="flex items-center py-2 text-xs transition-colors"
class:opacity-0={showFixedNewMessages}>
<div class="h-px flex-grow bg-primary"></div>
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
<div class="h-px flex-grow bg-primary"></div>
</div>
{:else if type === "date"}
<Divider>{value}</Divider>
{:else}
<div in:slide class:-mt-1={!showPubkey}>
<ChannelMessage
{url}
{room}
{replyTo}
event={$state.snapshot(value as TrustedEvent)}
{showPubkey} />
</div>
{/if}
{/each}
<p class="flex h-10 items-center justify-center py-20">
{#if loadingEvents}
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
{:else}
<Spinner>End of message history</Spinner>
{/if}
</p>
{/if}
</PageContent>
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
<div>
{#if parent}
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if}
{#if share}
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
{/if}
</div>
<ChannelCompose bind:this={compose} {onSubmit} {url} />
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
<!-- pass -->
{:else if $channel?.closed && $membershipStatus !== MembershipStatus.Granted}
<div class="flex flex-row items-center justify-between m-4 px-4 py-3 card bg-alt">
<p>Only members are allowed to post to this room.</p>
{#if $membershipStatus === MembershipStatus.Pending}
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
<Icon icon="clock-circle" />
Access Pending
</Button>
{:else}
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
{#if joining}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon="login-2" />
{/if}
Ask to Join
</Button>
{/if}
</div>
{:else}
<div>
{#if parent}
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if}
{#if share}
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
{/if}
</div>
<ChannelCompose bind:this={compose} {onSubmit} {url} />
{/if}
</div>
{#if showScrollButton}