mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-11 03:17:02 +00:00
Display rooms using nip29 meta
This commit is contained in:
12
src/app/components/ChannelName.svelte
Normal file
12
src/app/components/ChannelName.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {GENERAL, channelsById, makeChannelId} from "@app/state"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let room
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if room === GENERAL}
|
||||||
|
general
|
||||||
|
{:else}
|
||||||
|
{$channelsById.get(makeChannelId(url, room))?.name || room}
|
||||||
|
{/if}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {sortBy} from "@welshman/lib"
|
||||||
|
import {displayRelayUrl, GROUP_META} from "@welshman/util"
|
||||||
|
import {load} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {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"
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ProfileList from "@app/components/ProfileList.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
||||||
import {
|
import {
|
||||||
getMembershipRoomsByUrl,
|
getMembershipRoomsByUrl,
|
||||||
@@ -20,8 +23,9 @@
|
|||||||
hasMembershipUrl,
|
hasMembershipUrl,
|
||||||
userMembership,
|
userMembership,
|
||||||
memberships,
|
memberships,
|
||||||
roomsByUrl,
|
channelsByUrl,
|
||||||
GENERAL,
|
GENERAL,
|
||||||
|
displayChannel,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {deriveNotification, THREAD_FILTERS} from "@app/notifications"
|
import {deriveNotification, THREAD_FILTERS} from "@app/notifications"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
@@ -58,13 +62,28 @@
|
|||||||
let showMenu = false
|
let showMenu = false
|
||||||
let replaceState = false
|
let replaceState = false
|
||||||
let element: Element
|
let element: Element
|
||||||
|
let userRooms: string[] = []
|
||||||
|
let otherRooms: string[] = []
|
||||||
|
|
||||||
$: rooms = getMembershipRoomsByUrl(url, $userMembership)
|
|
||||||
$: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room))
|
|
||||||
$: members = $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey)
|
$: members = $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey)
|
||||||
|
|
||||||
|
$: {
|
||||||
|
userRooms = [GENERAL, ...getMembershipRoomsByUrl(url, $userMembership)]
|
||||||
|
otherRooms = []
|
||||||
|
|
||||||
|
for (const channel of $channelsByUrl.get(url) || []) {
|
||||||
|
if (!userRooms.includes(channel.room)) {
|
||||||
|
otherRooms.push(channel.room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userRooms = sortBy(room => displayChannel(url, room), userRooms)
|
||||||
|
otherRooms = sortBy(room => displayChannel(url, room), otherRooms)
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
replaceState = Boolean(element.closest(".drawer"))
|
replaceState = Boolean(element.closest(".drawer"))
|
||||||
|
load({relays: [url], filters: [{kinds: [GROUP_META]}]})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -118,14 +137,13 @@
|
|||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
<div class="h-2" />
|
<div class="h-2" />
|
||||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||||
<MenuSpaceRoomItem {url} room={GENERAL} />
|
{#each userRooms as room, i (room)}
|
||||||
{#each rooms as room, i (room)}
|
|
||||||
<MenuSpaceRoomItem {url} {room} />
|
<MenuSpaceRoomItem {url} {room} />
|
||||||
{/each}
|
{/each}
|
||||||
{#if otherRooms.length > 0}
|
{#if otherRooms.length > 0}
|
||||||
<div class="h-2" />
|
<div class="h-2" />
|
||||||
<SecondaryNavHeader>
|
<SecondaryNavHeader>
|
||||||
{#if rooms.length > 0}
|
{#if userRooms.length > 0}
|
||||||
Other Rooms
|
Other Rooms
|
||||||
{:else}
|
{:else}
|
||||||
Rooms
|
Rooms
|
||||||
@@ -135,7 +153,7 @@
|
|||||||
{#each otherRooms as room, i (room)}
|
{#each otherRooms as room, i (room)}
|
||||||
<SecondaryNavItem href={makeSpacePath(url, room)}>
|
<SecondaryNavItem href={makeSpacePath(url, room)}>
|
||||||
<Icon icon="hashtag" />
|
<Icon icon="hashtag" />
|
||||||
{room}
|
<ChannelName {url} {room} />
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/each}
|
{/each}
|
||||||
<SecondaryNavItem on:click={addRoom}>
|
<SecondaryNavItem on:click={addRoom}>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {displayRoom} from "@app/state"
|
|
||||||
import {deriveNotification, getRoomFilters} from "@app/notifications"
|
import {deriveNotification, getRoomFilters} from "@app/notifications"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
@@ -14,5 +14,5 @@
|
|||||||
|
|
||||||
<SecondaryNavItem href={path} notification={$notification}>
|
<SecondaryNavItem href={path} notification={$notification}>
|
||||||
<Icon icon="hashtag" />
|
<Icon icon="hashtag" />
|
||||||
{displayRoom(room)}
|
<ChannelName {url} {room} />
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
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 {roomsByUrl} from "@app/state"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
|
import {channelsByUrl} from "@app/state"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
import {setKey} from "@app/implicit"
|
import {setKey} from "@app/implicit"
|
||||||
|
|
||||||
@@ -37,14 +38,14 @@
|
|||||||
<div slot="info">Which room would you like to share this thread to?</div>
|
<div slot="info">Which room would you like to share this thread to?</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
{#each $roomsByUrl.get(url) || [] as room (room)}
|
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="btn"
|
||||||
class:btn-neutral={selection !== room}
|
class:btn-neutral={selection !== channel.room}
|
||||||
class:btn-primary={selection === room}
|
class:btn-primary={selection === channel.room}
|
||||||
on:click={() => toggleRoom(room)}>
|
on:click={() => toggleRoom(channel.room)}>
|
||||||
#{room}
|
#<ChannelName {...channel} />
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
141
src/app/state.ts
141
src/app/state.ts
@@ -15,6 +15,7 @@ import {
|
|||||||
nthEq,
|
nthEq,
|
||||||
shuffle,
|
shuffle,
|
||||||
parseJson,
|
parseJson,
|
||||||
|
fromPairs,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
getIdFilters,
|
getIdFilters,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
REACTION,
|
REACTION,
|
||||||
ZAP_RESPONSE,
|
ZAP_RESPONSE,
|
||||||
DIRECT_MESSAGE,
|
DIRECT_MESSAGE,
|
||||||
|
GROUP_META,
|
||||||
getGroupTags,
|
getGroupTags,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
@@ -444,63 +446,108 @@ export const chatSearch = derived(chats, $chats =>
|
|||||||
|
|
||||||
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
|
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
|
||||||
|
|
||||||
|
// Group Meta
|
||||||
|
|
||||||
|
export const groupMeta = deriveEvents(repository, {filters: [{kinds: [GROUP_META]}]})
|
||||||
|
|
||||||
// Channels
|
// Channels
|
||||||
|
|
||||||
|
export type ChannelMeta = {
|
||||||
|
access: "public" | "private"
|
||||||
|
membership: "open" | "closed"
|
||||||
|
picture?: string
|
||||||
|
about?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type Channel = {
|
export type Channel = {
|
||||||
url: string
|
url: string
|
||||||
room: string
|
room: string
|
||||||
name: string
|
name: string
|
||||||
events: TrustedEvent[]
|
events: TrustedEvent[]
|
||||||
|
meta?: ChannelMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeChannelId = (url: string, room: string) => `${url}|${room}`
|
export const makeChannelId = (url: string, room: string) => `${url}|${room}`
|
||||||
|
|
||||||
export const splitChannelId = (id: string) => id.split("|")
|
export const splitChannelId = (id: string) => id.split("|")
|
||||||
|
|
||||||
export const channelsById = derived(
|
export const channelsById = withGetter(
|
||||||
[memberships, messages, trackerStore],
|
derived(
|
||||||
([$memberships, $messages, $tracker]) => {
|
[groupMeta, memberships, messages, trackerStore],
|
||||||
const eventsByChannelId = new Map<string, TrustedEvent[]>()
|
([$groupMeta, $memberships, $messages, $tracker]) => {
|
||||||
|
const eventsByChannelId = new Map<string, TrustedEvent[]>()
|
||||||
|
|
||||||
// Add known rooms by membership so we have a full listing even if there are no messages there
|
// Add known rooms by membership so we have a full listing even if there are no messages there
|
||||||
for (const membership of $memberships) {
|
for (const membership of $memberships) {
|
||||||
for (const {url, room} of getMembershipRooms(membership)) {
|
for (const {url, room} of getMembershipRooms(membership)) {
|
||||||
eventsByChannelId.set(makeChannelId(url, room), [])
|
eventsByChannelId.set(makeChannelId(url, room), [])
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add known messages to rooms
|
|
||||||
for (const event of $messages) {
|
|
||||||
const [_, room] = event.tags.find(nthEq(0, ROOM)) || []
|
|
||||||
|
|
||||||
if (room) {
|
|
||||||
for (const url of $tracker.getRelays(event.id)) {
|
|
||||||
pushToMapKey(eventsByChannelId, makeChannelId(url, room), event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelsById = new Map<string, Channel>()
|
|
||||||
|
|
||||||
for (const [id, unsorted] of eventsByChannelId.entries()) {
|
|
||||||
const [url, room] = splitChannelId(id)
|
|
||||||
const events = sortBy(e => -e.created_at, unsorted)
|
|
||||||
|
|
||||||
let name = room
|
|
||||||
for (const event of events) {
|
|
||||||
const tag = event.tags.find(t => t[0] === ROOM && t[2])
|
|
||||||
|
|
||||||
if (tag) {
|
|
||||||
name = tag[2]
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
channelsById.set(id, {url, room, name, events})
|
// Add known messages to rooms
|
||||||
}
|
for (const event of $messages) {
|
||||||
|
const [_, room] = event.tags.find(nthEq(0, ROOM)) || []
|
||||||
|
|
||||||
return channelsById
|
if (room) {
|
||||||
},
|
for (const url of $tracker.getRelays(event.id)) {
|
||||||
|
pushToMapKey(eventsByChannelId, makeChannelId(url, room), event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelsById = new Map<string, Channel>()
|
||||||
|
|
||||||
|
for (const [id, unsorted] of eventsByChannelId.entries()) {
|
||||||
|
const [url, room] = splitChannelId(id)
|
||||||
|
const events = sortBy(e => -e.created_at, unsorted)
|
||||||
|
|
||||||
|
let name = room
|
||||||
|
for (const event of events) {
|
||||||
|
const tag = event.tags.find(t => t[0] === ROOM && t[2])
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
name = tag[2]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelsById.set(id, {url, room, name, events})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add meta using group meta events
|
||||||
|
for (const event of $groupMeta) {
|
||||||
|
const meta = fromPairs(event.tags)
|
||||||
|
const room = meta.d
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
for (const url of $tracker.getRelays(event.id)) {
|
||||||
|
const id = makeChannelId(url, room)
|
||||||
|
const channel: Channel = channelsById.get(id) || {
|
||||||
|
url,
|
||||||
|
room,
|
||||||
|
name: room,
|
||||||
|
events: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.name) {
|
||||||
|
channel.name = meta.name
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.meta = {
|
||||||
|
access: meta.public ? "public" : "private",
|
||||||
|
membership: meta.open ? "open" : "closed",
|
||||||
|
picture: meta.picture,
|
||||||
|
about: meta.about,
|
||||||
|
}
|
||||||
|
|
||||||
|
channelsById.set(id, channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelsById
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveChannel = (url: string, room: string) =>
|
export const deriveChannel = (url: string, room: string) =>
|
||||||
@@ -509,24 +556,22 @@ export const deriveChannel = (url: string, room: string) =>
|
|||||||
export const deriveChannelMessages = (url: string, room: string) =>
|
export const deriveChannelMessages = (url: string, room: string) =>
|
||||||
derived(channelsById, $channelsById => $channelsById.get(makeChannelId(url, room))?.events || [])
|
derived(channelsById, $channelsById => $channelsById.get(makeChannelId(url, room))?.events || [])
|
||||||
|
|
||||||
// Rooms
|
export const channelsByUrl = derived(channelsById, $channelsById => {
|
||||||
|
const $channelsByUrl = new Map<string, Channel[]>()
|
||||||
|
|
||||||
export const roomsByUrl = derived(channelsById, $channelsById => {
|
for (const channel of $channelsById.values()) {
|
||||||
const $roomsByUrl = new Map<string, string[]>()
|
pushToMapKey($channelsByUrl, channel.url, channel)
|
||||||
|
|
||||||
for (const {url, room} of $channelsById.values()) {
|
|
||||||
pushToMapKey($roomsByUrl, url, room)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $roomsByUrl
|
return $channelsByUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
export const displayRoom = (room: string) => {
|
export const displayChannel = (url: string, room: string) => {
|
||||||
if (room === GENERAL) {
|
if (room === GENERAL) {
|
||||||
return "general"
|
return "general"
|
||||||
}
|
}
|
||||||
|
|
||||||
return room
|
return channelsById.get().get(makeChannelId(url, room))?.name || room
|
||||||
}
|
}
|
||||||
|
|
||||||
// User stuff
|
// User stuff
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
import PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||||
import ProfileFeed from "@app/components/ProfileFeed.svelte"
|
import ProfileFeed from "@app/components/ProfileFeed.svelte"
|
||||||
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import {decodeRelay, roomsByUrl} from "@app/state"
|
import {decodeRelay, channelsByUrl} from "@app/state"
|
||||||
import {makeChatPath, makeRoomPath, makeSpacePath} from "@app/routes"
|
import {makeChatPath, makeRoomPath, makeSpacePath} from "@app/routes"
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay)
|
const url = decodeRelay($page.params.relay)
|
||||||
@@ -87,14 +88,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-2 md:hidden">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<Link href={makeSpacePath(url, "threads")} class="bg-alt btn btn-neutral border-none">
|
<Link
|
||||||
|
href={makeSpacePath(url, "threads")}
|
||||||
|
class="bg-alt btn btn-neutral justify-start border-none">
|
||||||
<Icon icon="notes-minimalistic" /> Threads
|
<Icon icon="notes-minimalistic" /> Threads
|
||||||
</Link>
|
</Link>
|
||||||
{#each $roomsByUrl.get(url) || [] as room (room)}
|
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
||||||
<Link href={makeRoomPath(url, room)} class="bg-alt btn btn-neutral border-none">
|
<Link
|
||||||
|
href={makeRoomPath(url, channel.room)}
|
||||||
|
class="bg-alt btn btn-neutral flex-nowrap justify-start whitespace-nowrap border-none">
|
||||||
<Icon icon="hashtag" />
|
<Icon icon="hashtag" />
|
||||||
{room}
|
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||||
|
<ChannelName {...channel} />
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||||
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import ChannelMessage from "@app/components/ChannelMessage.svelte"
|
import ChannelMessage from "@app/components/ChannelMessage.svelte"
|
||||||
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +31,6 @@
|
|||||||
tagRoom,
|
tagRoom,
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
getMembershipRoomsByUrl,
|
getMembershipRoomsByUrl,
|
||||||
displayRoom,
|
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {addRoomMembership, removeRoomMembership, subscribePersistent} from "@app/commands"
|
import {addRoomMembership, removeRoomMembership, subscribePersistent} from "@app/commands"
|
||||||
@@ -135,7 +135,9 @@
|
|||||||
<div slot="icon" class="center">
|
<div slot="icon" class="center">
|
||||||
<Icon icon="hashtag" />
|
<Icon icon="hashtag" />
|
||||||
</div>
|
</div>
|
||||||
<strong slot="title">{displayRoom(room)}</strong>
|
<strong slot="title">
|
||||||
|
<ChannelName {url} {room} />
|
||||||
|
</strong>
|
||||||
<div slot="action" class="row-2">
|
<div slot="action" class="row-2">
|
||||||
{#if room !== GENERAL}
|
{#if room !== GENERAL}
|
||||||
{#if getMembershipRoomsByUrl(url, $userMembership).includes(room)}
|
{#if getMembershipRoomsByUrl(url, $userMembership).includes(room)}
|
||||||
|
|||||||
Reference in New Issue
Block a user