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
+5 -5
View File
@@ -4243,8 +4243,8 @@ packages:
svelte: svelte:
optional: true optional: true
svelte@4.2.19: svelte@4.2.20:
resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==} resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==}
engines: {node: '>=16'} engines: {node: '>=16'}
svelte@5.25.10: svelte@5.25.10:
@@ -6442,7 +6442,7 @@ snapshots:
'@welshman/util': 0.3.0(typescript@5.8.3) '@welshman/util': 0.3.0(typescript@5.8.3)
fuse.js: 7.1.0 fuse.js: 7.1.0
idb: 8.0.2 idb: 8.0.2
svelte: 4.2.19 svelte: 4.2.20
throttle-debounce: 5.0.2 throttle-debounce: 5.0.2
transitivePeerDependencies: transitivePeerDependencies:
- nostr-signer-capacitor-plugin - nostr-signer-capacitor-plugin
@@ -6563,7 +6563,7 @@ snapshots:
'@welshman/lib': 0.3.0 '@welshman/lib': 0.3.0
'@welshman/relay': 0.3.0(typescript@5.8.3) '@welshman/relay': 0.3.0(typescript@5.8.3)
'@welshman/util': 0.3.0(typescript@5.8.3) '@welshman/util': 0.3.0(typescript@5.8.3)
svelte: 4.2.19 svelte: 4.2.20
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
@@ -9327,7 +9327,7 @@ snapshots:
optionalDependencies: optionalDependencies:
svelte: 5.25.10 svelte: 5.25.10
svelte@4.2.19: svelte@4.2.20:
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0
+3 -20
View File
@@ -53,6 +53,7 @@ import {
tagEventForComment, tagEventForComment,
tagEventForQuote, tagEventForQuote,
thunkIsComplete, thunkIsComplete,
getThunkError,
} from "@welshman/app" } from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import { import {
@@ -83,21 +84,6 @@ export const getPubkeyPetname = (pubkey: string) => {
return display return display
} }
export const getThunkError = (thunk: Thunk) =>
new Promise<string>(resolve => {
thunk.subscribe($thunk => {
for (const [relay, status] of Object.entries($thunk.status)) {
if (status === PublishStatus.Failure) {
resolve($thunk.details[relay])
}
}
if (thunkIsComplete($thunk)) {
resolve("")
}
})
})
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => { export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
if (parent) { if (parent) {
const nevent = nip19.neventEncode({ const nevent = nip19.neventEncode({
@@ -189,12 +175,9 @@ export const removeSpaceMembership = async (url: string) => {
return publishThunk({event, relays}) return publishThunk({event, relays})
} }
export const addRoomMembership = async (url: string, room: string, name: string) => { export const addRoomMembership = async (url: string, room: string) => {
const list = get(userMembership) || makeList({kind: GROUPS}) const list = get(userMembership) || makeList({kind: GROUPS})
const newTags = [ const newTags = [["r", url], ["group", room, url]]
["r", url],
["group", room, url, name],
]
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)]) const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
+2 -2
View File
@@ -3,7 +3,7 @@
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte" import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import ChannelName from "@app/components/ChannelName.svelte"
import {makeRoomPath} from "@app/routes" import {makeRoomPath} from "@app/routes"
import {deriveChannel, channelIsLocked} from "@app/state" import {deriveChannel} from "@app/state"
import {notifications} from "@app/notifications" import {notifications} from "@app/notifications"
interface Props { interface Props {
@@ -23,7 +23,7 @@
href={path} href={path}
{replaceState} {replaceState}
notification={notify ? $notifications.has(path) : false}> notification={notify ? $notifications.has(path) : false}>
{#if channelIsLocked($channel)} {#if $channel?.closed || $channel?.private}
<Icon icon="lock" size={4} /> <Icon icon="lock" size={4} />
{:else} {:else}
<Icon icon="hashtag" /> <Icon icon="hashtag" />
+2 -3
View File
@@ -2,7 +2,7 @@
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {randomId} from "@welshman/lib" import {randomId} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app" import {deriveRelay, getThunkError} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -11,7 +11,7 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {hasNip29, loadChannel} from "@app/state" import {hasNip29, loadChannel} from "@app/state"
import {addRoomMembership, createRoom, editRoom, joinRoom, getThunkError} from "@app/commands" import {createRoom, editRoom, joinRoom} from "@app/commands"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@@ -43,7 +43,6 @@
await loadChannel(url, room) await loadChannel(url, room)
addRoomMembership(url, room, name)
goto(makeSpacePath(url, room)) goto(makeSpacePath(url, room))
} }
+39 -7
View File
@@ -34,6 +34,9 @@ import {
GROUPS, GROUPS,
THREAD, THREAD,
COMMENT, COMMENT,
GROUP_JOIN,
GROUP_ADD_USER,
GROUP_REMOVE_USER,
getGroupTags, getGroupTags,
getRelayTagValues, getRelayTagValues,
getPubkeyTagValues, getPubkeyTagValues,
@@ -43,6 +46,8 @@ import {
getListTags, getListTags,
asDecryptedEvent, asDecryptedEvent,
normalizeRelayUrl, normalizeRelayUrl,
getTag,
getTagValues,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util" import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
import {Nip59, decrypt} from "@welshman/signer" import {Nip59, decrypt} from "@welshman/signer"
@@ -486,8 +491,8 @@ export type Channel = {
room: string room: string
name: string name: string
event: TrustedEvent event: TrustedEvent
access: "public" | "private" closed: boolean
membership: "open" | "closed" private: boolean
picture?: string picture?: string
about?: string about?: string
} }
@@ -520,8 +525,8 @@ export const channels = derived(
room, room,
event, event,
name: meta.name || room, name: meta.name || room,
access: meta.private ? "private" : "public", closed: Boolean(getTag("closed", event.tags)),
membership: meta.closed ? "closed" : "open", private: Boolean(getTag("private", event.tags)),
picture: meta.picture, picture: meta.picture,
about: meta.about, about: meta.about,
}) })
@@ -563,9 +568,6 @@ export const displayChannel = (url: string, room: string) =>
export const roomComparator = (url: string) => (room: string) => export const roomComparator = (url: string) => (room: string) =>
displayChannel(url, room).toLowerCase() displayChannel(url, room).toLowerCase()
export const channelIsLocked = (channel?: Channel) =>
channel?.access === "private" && channel?.membership === "closed"
// User stuff // User stuff
export const userSettings = withGetter( export const userSettings = withGetter(
@@ -626,6 +628,36 @@ export const deriveOtherRooms = (url: string) =>
), ),
) )
export enum MembershipStatus {
Initial,
Pending,
Granted,
}
export const deriveUserMembershipStatus = (url: string, room: string) =>
derived(
[pubkey, deriveEventsForUrl(url, [{kinds: [GROUP_JOIN, GROUP_ADD_USER, GROUP_REMOVE_USER], '#h': [room]}])],
([$pubkey, $events]) => {
let status = MembershipStatus.Initial
for (const event of $events) {
if (event.kind === GROUP_JOIN && event.pubkey === $pubkey) {
status = MembershipStatus.Pending
}
if (event.kind === GROUP_REMOVE_USER && getTagValues("p", event.tags).includes($pubkey!)) {
break
}
if (event.kind === GROUP_ADD_USER && getTagValues("p", event.tags).includes($pubkey!)) {
return MembershipStatus.Granted
}
}
return status
}
)
// Other utils // Other utils
export const encodeRelay = (url: string) => export const encodeRelay = (url: string) =>
+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 16.0909V11.0975C21 6.80891 21 4.6646 19.682 3.3323C18.364 2 16.2426 2 12 2C7.75736 2 5.63604 2 4.31802 3.3323C3 4.6646 3 6.80891 3 11.0975V16.0909C3 19.1875 3 20.7358 3.73411 21.4123C4.08421 21.735 4.52615 21.9377 4.99692 21.9915C5.98402 22.1045 7.13673 21.0849 9.44216 19.0458C10.4612 18.1445 10.9708 17.6938 11.5603 17.5751C11.8506 17.5166 12.1494 17.5166 12.4397 17.5751C13.0292 17.6938 13.5388 18.1445 14.5578 19.0458C16.8633 21.0849 18.016 22.1045 19.0031 21.9915C19.4739 21.9377 19.9158 21.735 20.2659 21.4123C21 20.7358 21 19.1875 21 16.0909Z" stroke="#1C274D" stroke-width="1.5"/>
<path d="M15 6H9" stroke="#1C274D" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 784 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.15316 5.40838C10.4198 3.13613 11.0531 2 12 2C12.9469 2 13.5802 3.13612 14.8468 5.40837L15.1745 5.99623C15.5345 6.64193 15.7144 6.96479 15.9951 7.17781C16.2757 7.39083 16.6251 7.4699 17.3241 7.62805L17.9605 7.77203C20.4201 8.32856 21.65 8.60682 21.9426 9.54773C22.2352 10.4886 21.3968 11.4691 19.7199 13.4299L19.2861 13.9372C18.8096 14.4944 18.5713 14.773 18.4641 15.1177C18.357 15.4624 18.393 15.8341 18.465 16.5776L18.5306 17.2544C18.7841 19.8706 18.9109 21.1787 18.1449 21.7602C17.3788 22.3417 16.2273 21.8115 13.9243 20.7512L13.3285 20.4768C12.6741 20.1755 12.3469 20.0248 12 20.0248C11.6531 20.0248 11.3259 20.1755 10.6715 20.4768L10.0757 20.7512C7.77268 21.8115 6.62118 22.3417 5.85515 21.7602C5.08912 21.1787 5.21588 19.8706 5.4694 17.2544L5.53498 16.5776C5.60703 15.8341 5.64305 15.4624 5.53586 15.1177C5.42868 14.773 5.19043 14.4944 4.71392 13.9372L4.2801 13.4299C2.60325 11.4691 1.76482 10.4886 2.05742 9.54773C2.35002 8.60682 3.57986 8.32856 6.03954 7.77203L6.67589 7.62805C7.37485 7.4699 7.72433 7.39083 8.00494 7.17781C8.28555 6.96479 8.46553 6.64194 8.82547 5.99623L9.15316 5.40838Z" stroke="#1C274C" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -9,6 +9,7 @@
import {switcher} from "@welshman/lib" import {switcher} from "@welshman/lib"
import AddSquare from "@assets/icons/Add Square.svg?dataurl" import AddSquare from "@assets/icons/Add Square.svg?dataurl"
import ArrowsALogout2 from "@assets/icons/Arrows ALogout 2.svg?dataurl" import ArrowsALogout2 from "@assets/icons/Arrows ALogout 2.svg?dataurl"
import Bookmark from "@assets/icons/Bookmark.svg?dataurl"
import Code2 from "@assets/icons/Code 2.svg?dataurl" import Code2 from "@assets/icons/Code 2.svg?dataurl"
import Document from "@assets/icons/Document.svg?dataurl" import Document from "@assets/icons/Document.svg?dataurl"
import Earth from "@assets/icons/Earth.svg?dataurl" import Earth from "@assets/icons/Earth.svg?dataurl"
@@ -80,6 +81,7 @@
import SmileCircle from "@assets/icons/Smile Circle.svg?dataurl" import SmileCircle from "@assets/icons/Smile Circle.svg?dataurl"
import SquareShareLine from "@assets/icons/Square Share Line.svg?dataurl" import SquareShareLine from "@assets/icons/Square Share Line.svg?dataurl"
import SortVertical from "@assets/icons/Sort Vertical.svg?dataurl" import SortVertical from "@assets/icons/Sort Vertical.svg?dataurl"
import Star from "@assets/icons/Star.svg?dataurl"
import TrashBin2 from "@assets/icons/Trash Bin 2.svg?dataurl" import TrashBin2 from "@assets/icons/Trash Bin 2.svg?dataurl"
import UFO3 from "@assets/icons/UFO 3.svg?dataurl" import UFO3 from "@assets/icons/UFO 3.svg?dataurl"
import UserHeart from "@assets/icons/User Heart.svg?dataurl" import UserHeart from "@assets/icons/User Heart.svg?dataurl"
@@ -104,6 +106,7 @@
const data = switcher(icon, { const data = switcher(icon, {
"add-square": AddSquare, "add-square": AddSquare,
"arrows-a-logout-2": ArrowsALogout2, "arrows-a-logout-2": ArrowsALogout2,
bookmark: Bookmark,
"code-2": Code2, "code-2": Code2,
document: Document, document: Document,
earth: Earth, earth: Earth,
@@ -177,6 +180,7 @@
"ufo-3": UFO3, "ufo-3": UFO3,
"square-share-line": SquareShareLine, "square-share-line": SquareShareLine,
"sort-vertical": SortVertical, "sort-vertical": SortVertical,
star: Star,
"user-heart": UserHeart, "user-heart": UserHeart,
"user-circle": UserCircle, "user-circle": UserCircle,
"user-rounded": UserRounded, "user-rounded": UserRounded,
+1 -6
View File
@@ -17,7 +17,6 @@
MESSAGE, MESSAGE,
INBOX_RELAYS, INBOX_RELAYS,
DIRECT_MESSAGE, DIRECT_MESSAGE,
GROUP_META,
MUTES, MUTES,
FOLLOWS, FOLLOWS,
PROFILE, PROFILE,
@@ -174,11 +173,7 @@
limit: 10_000, limit: 10_000,
repository, repository,
rankEvent: (e: TrustedEvent) => { rankEvent: (e: TrustedEvent) => {
if ( if ([PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, INBOX_RELAYS].includes(e.kind)) {
[PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, INBOX_RELAYS, GROUP_META].includes(
e.kind,
)
) {
return 1 return 1
} }
+4 -3
View File
@@ -20,7 +20,6 @@
import { import {
hasNip29, hasNip29,
decodeRelay, decodeRelay,
channelIsLocked,
makeChannelId, makeChannelId,
channelsById, channelsById,
deriveUserRooms, deriveUserRooms,
@@ -155,9 +154,10 @@
</Link> </Link>
{#each $userRooms as room (room)} {#each $userRooms as room (room)}
{@const roomPath = makeRoomPath(url, room)} {@const roomPath = makeRoomPath(url, room)}
{@const channel = $channelsById.get(makeChannelId(url, room))}
<Link href={roomPath} class="btn btn-neutral relative"> <Link href={roomPath} class="btn btn-neutral relative">
<div class="flex min-w-0 items-center gap-2 overflow-hidden text-nowrap"> <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} /> <Icon icon="lock" size={4} />
{:else} {:else}
<Icon icon="hashtag" /> <Icon icon="hashtag" />
@@ -173,9 +173,10 @@
<Divider>Other Rooms</Divider> <Divider>Other Rooms</Divider>
<div class="grid grid-cols-3 gap-2"> <div class="grid grid-cols-3 gap-2">
{#each $otherRooms as room (room)} {#each $otherRooms as room (room)}
{@const channel = $channelsById.get(makeChannelId(url, room))}
<Link href={makeRoomPath(url, room)} class="btn btn-neutral"> <Link href={makeRoomPath(url, room)} class="btn btn-neutral">
<div class="relative flex min-w-0 items-center gap-2 overflow-hidden text-nowrap"> <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} /> <Icon icon="lock" size={4} />
{:else} {:else}
<Icon icon="hashtag" /> <Icon icon="hashtag" />
+156 -72
View File
@@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames'
import {readable} from "svelte/store" import {readable} from "svelte/store"
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import {now, formatTimestampAsDate} from "@welshman/lib" import {now, formatTimestampAsDate} from "@welshman/lib"
import {request} from "@welshman/net"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util" import {createEvent, MESSAGE, DELETE, REACTION, GROUP_ADD_USER, GROUP_REMOVE_USER} from "@welshman/util"
import {pubkey, publishThunk, deriveRelay} from "@welshman/app" import {pubkey, publishThunk, deriveRelay, getThunkError, waitForThunkCompletion} from "@welshman/app"
import {slide, fade, fly} from "@lib/transition" import {slide, fade, fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -20,12 +22,15 @@
import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelCompose from "@app/components/ChannelCompose.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import { import {
userRoomsByUrl,
userSettingValues, userSettingValues,
decodeRelay, decodeRelay,
tagRoom, tagRoom,
userRoomsByUrl,
displayChannel, displayChannel,
getEventsForUrl, getEventsForUrl,
deriveUserMembershipStatus,
deriveChannel,
MembershipStatus,
} from "@app/state" } from "@app/state"
import {setChecked, checked} from "@app/notifications" import {setChecked, checked} from "@app/notifications"
import { import {
@@ -34,7 +39,6 @@
addRoomMembership, addRoomMembership,
removeRoomMembership, removeRoomMembership,
prependParent, prependParent,
getThunkError,
} from "@app/commands" } from "@app/commands"
import {PROTECTED} from "@app/state" import {PROTECTED} from "@app/state"
import {makeFeed} from "@app/requests" import {makeFeed} from "@app/requests"
@@ -45,25 +49,43 @@
const mounted = now() const mounted = now()
const lastChecked = $checked[$page.url.pathname] const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const channel = deriveChannel(url, room)
const filter = {kinds: [MESSAGE], "#h": [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 () => { 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.startsWith("duplicate:")) {
return pushToast({theme: "error", message})
if (message && !message.includes("already")) { } else {
return pushToast({theme: "error", message}) // Restart the feed now that we're a member
start()
}
} finally {
joining = false
} }
addRoomMembership(url, room, displayChannel(url, room))
} }
const leave = () => { const leave = async () => {
leaveRoom(url, room) leaving = true
removeRoomMembership(url, room) try {
const message = await getThunkError(leaveRoom(url, room))
if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message})
}
} finally {
leaving = false
}
} }
const replyTo = (event: TrustedEvent) => { const replyTo = (event: TrustedEvent) => {
@@ -124,7 +146,8 @@
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"}) 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 loadingEvents = $state(true)
let share = $state(popKey<TrustedEvent | undefined>("share")) let share = $state(popKey<TrustedEvent | undefined>("share"))
let parent: TrustedEvent | undefined = $state() let parent: TrustedEvent | undefined = $state()
@@ -196,8 +219,10 @@
return elements return elements
}) })
onMount(() => { const start = () => {
;({events, cleanup} = makeFeed({ cleanup?.()
const feed = makeFeed({
element: element!, element: element!,
relays: [url], relays: [url],
feedFilters: [filter], feedFilters: [filter],
@@ -206,7 +231,25 @@
onExhausted: () => { onExhausted: () => {
loadingEvents = false 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(() => { const observer = new ResizeObserver(() => {
if (dynamicPadding && chatCompose) { if (dynamicPadding && chatCompose) {
@@ -216,8 +259,10 @@
observer.observe(chatCompose!) observer.observe(chatCompose!)
observer.observe(dynamicPadding!) observer.observe(dynamicPadding!)
start()
return () => { return () => {
controller.abort()
observer.unobserve(chatCompose!) observer.unobserve(chatCompose!)
observer.unobserve(dynamicPadding!) observer.unobserve(dynamicPadding!)
} }
@@ -246,21 +291,12 @@
{/snippet} {/snippet}
{#snippet action()} {#snippet action()}
<div class="row-2"> <div class="row-2">
{#if $userRoomsByUrl.get(url)?.has(room)} <Button
<Button class="btn btn-neutral btn-sm" onclick={leave}> class="btn btn-neutral btn-sm tooltip tooltip-left"
<Icon icon="arrows-a-logout-2" /> data-tip={isFavorite ? "Remove Favorite" : "Add Favorite"}
Leave Room onclick={isFavorite ? removeFavorite : addFavorite}>
</Button> <Icon size={4} icon="bookmark" class={cx({'text-primary': isFavorite})} />
{:else} </Button>
<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}
<MenuSpaceButton {url} /> <MenuSpaceButton {url} />
</div> </div>
{/snippet} {/snippet}
@@ -268,48 +304,96 @@
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4"> <PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
<div bind:this={dynamicPadding}></div> <div bind:this={dynamicPadding}></div>
{#each elements as { type, id, value, showPubkey } (id)} {#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
{#if type === "new-messages"} <div class="py-20">
<div <div class="card2 col-8 m-auto max-w-md items-center text-center">
bind:this={newMessages} <p class="row-2">
class="flex items-center py-2 text-xs transition-colors" You aren't currently a member of this room.
class:opacity-0={showFixedNewMessages}> </p>
<div class="h-px flex-grow bg-primary"></div> {#if $membershipStatus === MembershipStatus.Pending}
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p> <Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
<div class="h-px flex-grow bg-primary"></div> <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> </div>
{:else if type === "date"} </div>
<Divider>{value}</Divider> {:else}
{:else} {#each elements as { type, id, value, showPubkey } (id)}
<div in:slide class:-mt-1={!showPubkey}> {#if type === "new-messages"}
<ChannelMessage <div
{url} bind:this={newMessages}
{room} class="flex items-center py-2 text-xs transition-colors"
{replyTo} class:opacity-0={showFixedNewMessages}>
event={$state.snapshot(value as TrustedEvent)} <div class="h-px flex-grow bg-primary"></div>
{showPubkey} /> <p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
</div> <div class="h-px flex-grow bg-primary"></div>
{/if} </div>
{/each} {:else if type === "date"}
<p class="flex h-10 items-center justify-center py-20"> <Divider>{value}</Divider>
{#if loadingEvents} {:else}
<Spinner loading={loadingEvents}>Looking for messages...</Spinner> <div in:slide class:-mt-1={!showPubkey}>
{:else} <ChannelMessage
<Spinner>End of message history</Spinner> {url}
{/if} {room}
</p> {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> </PageContent>
<div class="chat__compose bg-base-200" bind:this={chatCompose}> <div class="chat__compose bg-base-200" bind:this={chatCompose}>
<div> {#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
{#if parent} <!-- pass -->
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" /> {:else if $channel?.closed && $membershipStatus !== MembershipStatus.Granted}
{/if} <div class="flex flex-row items-center justify-between m-4 px-4 py-3 card bg-alt">
{#if share} <p>Only members are allowed to post to this room.</p>
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" /> {#if $membershipStatus === MembershipStatus.Pending}
{/if} <Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
</div> <Icon icon="clock-circle" />
<ChannelCompose bind:this={compose} {onSubmit} {url} /> 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> </div>
{#if showScrollButton} {#if showScrollButton}