Add room editing

This commit is contained in:
Jon Staab
2025-10-30 14:59:39 -07:00
parent a669a23dbc
commit 478721d349
9 changed files with 351 additions and 221 deletions

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ChannelName from "@app/components/ChannelName.svelte"
import {deriveChannel} from "@app/core/state"
interface Props {
url: any
room: any
}
const {url, room}: Props = $props()
const channel = deriveChannel(url, room)
</script>
{#if $channel?.picture}
{@const src = $channel.picture}
{#if src.match("\.(png|svg)$") || src.match("image/(png|svg)")}
<Icon icon={src} />
{:else}
<img alt="Room icon" {src} class="h-6 w-6 rounded-lg" />
{/if}
{:else if $channel?.closed || $channel?.private}
<Icon icon={Lock} />
{:else}
<Icon icon={Hashtag} />
{/if}
<div class="min-w-0 overflow-hidden text-ellipsis">
<ChannelName {url} {room} />
</div>

View File

@@ -1,11 +1,7 @@
<script lang="ts"> <script lang="ts">
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
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 ChannelNameWithImage from "@app/components/ChannelNameWithImage.svelte"
import {makeRoomPath} from "@app/util/routes" import {makeRoomPath} from "@app/util/routes"
import {deriveChannel} from "@app/core/state"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
interface Props { interface Props {
@@ -18,26 +14,11 @@
const {url, room, notify = false, replaceState = false}: Props = $props() const {url, room, notify = false, replaceState = false}: Props = $props()
const path = makeRoomPath(url, room) const path = makeRoomPath(url, room)
const channel = deriveChannel(url, room)
</script> </script>
<SecondaryNavItem <SecondaryNavItem
href={path} href={path}
{replaceState} {replaceState}
notification={notify ? $notifications.has(path) : false}> notification={notify ? $notifications.has(path) : false}>
{#if $channel?.picture} <ChannelNameWithImage {url} {room} />
{@const src = $channel.picture}
{#if src.match("\.(png|svg)$") || src.match("image/(png|svg)")}
<Icon icon={src} />
{:else}
<img alt="Room icon" {src} class="h-6 w-6 rounded-lg" />
{/if}
{:else if $channel?.closed || $channel?.private}
<Icon icon={Lock} />
{:else}
<Icon icon={Hashtag} />
{/if}
<div class="min-w-0 overflow-hidden text-ellipsis">
<ChannelName {url} {room} />
</div>
</SecondaryNavItem> </SecondaryNavItem>

View File

@@ -1,181 +1,47 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {uniqBy, nth} from "@welshman/lib" import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {deriveRelay, waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
import {preventDefault, compressFile} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.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 IconPickerButton from "@lib/components/IconPickerButton.svelte" import RoomForm from "@app/components/RoomForm.svelte"
import {hasNip29, loadChannel} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes" import {makeSpacePath} from "@app/util/routes"
import {pushToast} from "@app/util/toast"
import {uploadFile} from "@app/core/commands"
const {url} = $props() const {url} = $props()
const room = makeRoomMeta()
const relay = deriveRelay(url)
const back = () => history.back() const back = () => history.back()
const tryCreate = async () => { const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
room.tags = uniqBy(nth(0), [...room.tags, ["name", name]])
if (imageFile) {
const {error, result} = await uploadFile(imageFile)
if (error) {
return pushToast({theme: "error", message: error})
}
room.tags.push(["picture", result.url, ...result.tags])
} else if (selectedIcon) {
room.tags.push(["picture", selectedIcon])
}
const createMessage = await waitForThunkError(createRoom(url, room))
if (createMessage && !createMessage.match(/^duplicate:|already a member/)) {
return pushToast({theme: "error", message: createMessage})
}
const editMessage = await waitForThunkError(editRoom(url, room))
if (editMessage) {
return pushToast({theme: "error", message: editMessage})
}
const joinMessage = await waitForThunkError(joinRoom(url, room))
if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage})
}
await loadChannel(url, room.id)
goto(makeSpacePath(url, room.id))
}
const create = async () => {
loading = true
try {
await tryCreate()
} finally {
loading = false
}
}
let name = $state("")
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state<string | undefined>()
let selectedIcon = $state<string | undefined>()
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
selectedIcon = undefined
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
const reader = new FileReader()
reader.onload = e => {
imagePreview = e.target?.result as string
}
reader.readAsDataURL(imageFile)
}
}
const handleIconSelect = (iconUrl: string) => {
imageFile = undefined
imagePreview = undefined
selectedIcon = iconUrl
}
</script> </script>
<form class="column gap-4" onsubmit={preventDefault(create)}> <RoomForm {url} {onsubmit}>
<ModalHeader> {#snippet header()}
{#snippet title()} <ModalHeader>
<div>Create a Room</div> {#snippet title()}
{/snippet} <div>Create a Room</div>
{#snippet info()}
<div>
On <span class="text-primary">{displayRelayUrl(url)}</span>
</div>
{/snippet}
</ModalHeader>
{#if hasNip29($relay)}
<FieldInline>
{#snippet label()}
<p>Room Name</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet info()}
<label class="input input-bordered flex w-full items-center gap-2"> <div>
<Icon icon={Hashtag} /> On <span class="text-primary">{displayRelayUrl(url)}</span>
<input bind:value={name} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<div class="flex items-center justify-between">
<p class="font-bold">Room Icon</p>
<div class="flex items-center gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<img
src={imagePreview}
alt="Room icon preview"
class="h-8 w-8 rounded-lg object-cover" />
</div>
{:else if selectedIcon}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<Icon icon={selectedIcon} class="h-8 w-8" />
</div>
{:else}
<span class="text-sm opacity-75">No icon selected</span>
{/if}
<div class="flex gap-2">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
<Icon icon={StickerSmileSquare} size={4} />
Select
</IconPickerButton>
<label class="btn btn-neutral btn-sm cursor-pointer">
<Icon icon={UploadMinimalistic} size={4} />
Upload
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
</label>
</div> </div>
</div> {/snippet}
</div> </ModalHeader>
{:else} {/snippet}
<p class="bg-alt card2 row-2"> {#snippet footer({loading})}
<Icon icon={Danger} /> <ModalFooter>
This relay does not support creating rooms. <Button class="btn btn-link" onclick={back}>
</p> <Icon icon={AltArrowLeft} />
{/if} Go back
<ModalFooter> </Button>
<Button class="btn btn-link" onclick={back}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Icon icon={AltArrowLeft} /> <Spinner {loading}>Create Room</Spinner>
Go back <Icon icon={AltArrowRight} />
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={!name || loading || !hasNip29($relay)}> </ModalFooter>
<Spinner {loading}>Create Room</Spinner> {/snippet}
<Icon icon={AltArrowRight} /> </RoomForm>
</Button>
</ModalFooter>
</form>

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta, readRoomMeta} from "@welshman/util"
import {deleteRoom, waitForThunkError, repository} from "@welshman/app"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RoomForm from "@app/components/RoomForm.svelte"
import {deriveChannel} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
type Props = {
url: string
room: string
}
const {url, room}: Props = $props()
const channel = deriveChannel(url, room)
const initialValues = $channel ? readRoomMeta($channel.event) : makeRoomMeta({h: room})
const back = () => history.back()
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
const startDelete = () =>
pushModal(Confirm, {
title: "Are you sure you want to delete this room?",
message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => {
const thunk = deleteRoom(url, makeRoomMeta({h: room}))
const message = await waitForThunkError(thunk)
if (message) {
repository.removeEvent(thunk.event.id)
pushToast({theme: "error", message})
} else {
goto(makeSpacePath(url))
}
},
})
</script>
<RoomForm {url} {onsubmit} {initialValues}>
{#snippet header()}
<ModalHeader>
{#snippet title()}
<div>Edit a Room</div>
{/snippet}
{#snippet info()}
<div>
On <span class="text-primary">{displayRelayUrl(url)}</span>
</div>
{/snippet}
</ModalHeader>
{/snippet}
{#snippet footer({loading})}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<div class="flex gap-2">
<Button class="btn btn-outline btn-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
<span class="hidden md:inline">Delete Room</span>
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</div>
</ModalFooter>
{/snippet}
</RoomForm>

View File

@@ -0,0 +1,193 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {RoomMeta} from "@welshman/util"
import {makeRoomMeta} from "@welshman/util"
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
import {preventDefault, compressFile} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
import Icon from "@lib/components/Icon.svelte"
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
import {pushToast} from "@app/util/toast"
import {uploadFile} from "@app/core/commands"
type Props = {
url: string
header: Snippet
footer: Snippet<[{loading: boolean}]>
onsubmit: (room: RoomMeta) => void
initialValues?: RoomMeta
}
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
const values = $state(initialValues)
const submit = async () => {
const room = $state.snapshot(values)
if (imageFile) {
const {error, result} = await uploadFile(imageFile)
if (error) {
return pushToast({theme: "error", message: error})
}
room.picture = result.url
room.pictureMeta = result.tags
} else if (selectedIcon) {
room.picture = selectedIcon
}
const createMessage = await waitForThunkError(createRoom(url, room))
if (createMessage && !createMessage.includes("already")) {
return pushToast({theme: "error", message: createMessage})
}
const editMessage = await waitForThunkError(editRoom(url, room))
if (editMessage) {
return pushToast({theme: "error", message: editMessage})
}
const joinMessage = await waitForThunkError(joinRoom(url, room))
if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage})
}
onsubmit(room)
}
const trySubmit = async () => {
loading = true
try {
await submit()
} finally {
loading = false
}
}
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state(initialValues.picture)
let selectedIcon = $state<string | undefined>()
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
selectedIcon = undefined
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
const reader = new FileReader()
reader.onload = e => {
imagePreview = e.target?.result as string
}
reader.readAsDataURL(imageFile)
}
}
const handleIconSelect = (iconUrl: string) => {
imageFile = undefined
imagePreview = undefined
selectedIcon = iconUrl
}
</script>
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
{@render header()}
<FieldInline>
{#snippet label()}
<p>Icon</p>
{/snippet}
{#snippet input()}
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<img src={imagePreview} alt="Room icon preview" class="h-5 w-5 rounded-lg object-cover" />
</div>
{:else if selectedIcon}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<Icon icon={selectedIcon} class="h-8 w-8" />
</div>
{:else}
<span class="text-sm opacity-75">No icon selected</span>
{/if}
<div class="flex gap-2">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
<Icon icon={StickerSmileSquare} size={4} />
Select
</IconPickerButton>
<label class="btn btn-neutral btn-sm cursor-pointer">
<Icon icon={UploadMinimalistic} size={4} />
Upload
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
</label>
</div>
</div>
</div>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Name</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
{#if imagePreview}
<img src={imagePreview} alt="Room icon preview" class="h-5 w-5 rounded-lg object-cover" />
{:else if selectedIcon}
<Icon icon={selectedIcon} class="h-8 w-8" />
{:else}
<Icon icon={Hashtag} />
{/if}
<input bind:value={values.name} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Description</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<input bind:value={values.description} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Access Control</p>
{/snippet}
{#snippet input()}
<div class="flex items-center justify-end gap-4">
<span class="flex gap-3">
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
Closed
</span>
<span class="flex gap-3">
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
Private
</span>
<span class="flex gap-3">
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
Hidden
</span>
</div>
{/snippet}
{#snippet info()}
<p>Only members can send messages to closed groups and read messages from private groups.</p>
{/snippet}
</FieldInline>
{@render footer({loading})}
</form>

View File

@@ -264,7 +264,7 @@ const syncSpace = (url: string) => {
signal: controller.signal, signal: controller.signal,
filters: [ filters: [
{kinds: [RELAY_MEMBERS]}, {kinds: [RELAY_MEMBERS]},
{kinds: [ROOM_META]}, {kinds: [ROOM_META, ROOM_DELETE]},
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}, {kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]},
...MESSAGE_KINDS.map(kind => ({kinds: [kind]})), ...MESSAGE_KINDS.map(kind => ({kinds: [kind]})),
makeCommentFilter(CONTENT_KINDS), makeCommentFilter(CONTENT_KINDS),
@@ -337,7 +337,6 @@ const syncRoomChat = (url: string, room: string) => {
filters: [ filters: [
{kinds: [ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]}, {kinds: [ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]}, {kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]},
{kinds: [ROOM_DELETE], "#h": [room]},
{kinds: [MESSAGE], "#h": [room]}, {kinds: [MESSAGE], "#h": [room]},
], ],
}) })

View File

@@ -26,6 +26,7 @@ import {
ROOM_CREATE_PERMISSION, ROOM_CREATE_PERMISSION,
ROOM_MEMBERS, ROOM_MEMBERS,
ROOM_META, ROOM_META,
ROOM_DELETE,
ROOM_REMOVE_MEMBER, ROOM_REMOVE_MEMBER,
ROOMS, ROOMS,
THREAD, THREAD,
@@ -75,6 +76,7 @@ const syncEvents = async () => {
const spaceKinds = [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE] const spaceKinds = [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE]
const roomKinds = [ const roomKinds = [
ROOM_META, ROOM_META,
ROOM_DELETE,
ROOM_MEMBERS, ROOM_MEMBERS,
ROOM_ADD_MEMBER, ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER, ROOM_REMOVE_MEMBER,

View File

@@ -16,7 +16,7 @@
<div class="col-span-2 flex items-center gap-2"> <div class="col-span-2 flex items-center gap-2">
{@render props.input?.()} {@render props.input?.()}
</div> </div>
<p class="flex-end text-sm md:col-span-3"> <p class="flex-end text-sm opacity-70 md:col-span-3">
{#if props.info} {#if props.info}
{@render props.info?.()} {@render props.info?.()}
{/if} {/if}

View File

@@ -3,7 +3,6 @@
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 {goto} from "$app/navigation"
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import type {MakeNonOptional} from "@welshman/lib" import type {MakeNonOptional} from "@welshman/lib"
import {now, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib" import {now, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
@@ -15,20 +14,12 @@
ROOM_ADD_MEMBER, ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER, ROOM_REMOVE_MEMBER,
} from "@welshman/util" } from "@welshman/util"
import { import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
pubkey,
publishThunk,
waitForThunkError,
deleteRoom,
joinRoom,
leaveRoom,
repository,
} from "@welshman/app"
import {slide, fade, fly} from "@lib/transition" import {slide, fade, fly} from "@lib/transition"
import Hashtag from "@assets/icons/hashtag.svg?dataurl" import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
import Login2 from "@assets/icons/login-3.svg?dataurl" import Login2 from "@assets/icons/login-3.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
import Logout2 from "@assets/icons/logout-3.svg?dataurl" import Logout2 from "@assets/icons/logout-3.svg?dataurl"
import Bookmark from "@assets/icons/bookmark.svg?dataurl" import Bookmark from "@assets/icons/bookmark.svg?dataurl"
@@ -38,9 +29,9 @@
import PageBar from "@lib/components/PageBar.svelte" import PageBar from "@lib/components/PageBar.svelte"
import PageContent from "@lib/components/PageContent.svelte" import PageContent from "@lib/components/PageContent.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte" import ThunkToast from "@app/components/ThunkToast.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import RoomEdit from "@app/components/RoomEdit.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import ChannelName from "@app/components/ChannelName.svelte"
import ChannelItem from "@app/components/ChannelItem.svelte" import ChannelItem from "@app/components/ChannelItem.svelte"
import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte" import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte"
@@ -71,7 +62,6 @@
import {popKey} from "@lib/implicit" import {popKey} from "@lib/implicit"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {makeSpacePath} from "@app/util/routes"
const {room, relay} = $page.params as MakeNonOptional<typeof $page.params> const {room, relay} = $page.params as MakeNonOptional<typeof $page.params>
const mounted = now() const mounted = now()
@@ -92,7 +82,7 @@
joining = true joining = true
try { try {
const message = await waitForThunkError(joinRoom(url, makeRoomMeta({id: room}))) const message = await waitForThunkError(joinRoom(url, makeRoomMeta({h: room})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
return pushToast({theme: "error", message}) return pushToast({theme: "error", message})
@@ -108,7 +98,7 @@
const leave = async () => { const leave = async () => {
leaving = true leaving = true
try { try {
const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({id: room}))) const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({h: room})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message}) pushToast({theme: "error", message})
@@ -307,23 +297,7 @@
} }
} }
const startDelete = () => const startEdit = () => pushModal(RoomEdit, {url, room})
pushModal(Confirm, {
title: "Are you sure you want to delete this room?",
message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => {
const thunk = deleteRoom(url, makeRoomMeta({id: room}))
const message = await waitForThunkError(thunk)
if (message) {
repository.removeEvent(thunk.event.id)
pushToast({theme: "error", message})
} else {
goto(makeSpacePath(url))
}
},
})
onMount(() => { onMount(() => {
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
@@ -368,9 +342,9 @@
{#if $userIsAdmin || true} {#if $userIsAdmin || true}
<Button <Button
class="btn btn-neutral btn-sm tooltip tooltip-left" class="btn btn-neutral btn-sm tooltip tooltip-left"
data-tip="Delete this room" data-tip="Edit room information"
onclick={startDelete}> onclick={startEdit}>
<Icon size={4} icon={TrashBin2} /> <Icon size={4} icon={Pen} />
</Button> </Button>
{:else if $membershipStatus === MembershipStatus.Initial} {:else if $membershipStatus === MembershipStatus.Initial}
<Button <Button