flotilla/src/app/components/RoomForm.svelte

165 lines
5.3 KiB
Svelte
Raw Normal View History

2025-10-30 21:59:39 +00:00
<script lang="ts">
import type {Snippet} from "svelte"
2026-07-28 16:16:26 +00:00
import {randomId} from "@welshman/lib"
import type {RoomMeta} from "@welshman/app"
2025-10-30 21:59:39 +00:00
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Volume from "@assets/icons/volume.svg?dataurl"
2025-11-12 01:34:15 +00:00
import {preventDefault} from "@lib/html"
2025-10-30 21:59:39 +00:00
import FieldInline from "@lib/components/FieldInline.svelte"
import Icon from "@lib/components/Icon.svelte"
2025-10-31 23:18:58 +00:00
import ImageIcon from "@lib/components/ImageIcon.svelte"
2026-07-15 16:30:17 +00:00
import IconInput from "@lib/components/IconInput.svelte"
2026-01-30 22:14:12 +00:00
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
2026-07-28 16:16:26 +00:00
import {rooms} from "@app/core"
2026-06-08 21:14:40 +00:00
import {pushToast} from "@app/toast"
import {compressFileForUpload, uploadFileOrFallback} from "@app/uploads"
import {deriveHasLivekit} from "@app/relays"
2026-07-28 16:16:26 +00:00
import {RoomType} from "@app/rooms"
2025-10-30 21:59:39 +00:00
type Props = {
url: string
header: Snippet
footer: Snippet<[{loading: boolean}]>
onsubmit: (room: RoomMeta) => void
initialValues?: RoomMeta
}
2026-07-28 16:16:26 +00:00
const {url, header, footer, onsubmit, initialValues = {h: randomId()}}: Props = $props()
2025-10-30 21:59:39 +00:00
const values = $state(initialValues)
const relayHasLivekit = deriveHasLivekit(url)
2025-10-30 21:59:39 +00:00
const submit = async () => {
const room = $state.snapshot(values)
if (roomType === RoomType.Voice && !$relayHasLivekit) {
return pushToast({
theme: "error",
message: "This relay does not support voice rooms.",
})
}
room.livekit = roomType === RoomType.Voice
2025-10-30 21:59:39 +00:00
if (imageFile) {
const compressedFile = await compressFileForUpload(imageFile, {
maxWidth: 128,
maxHeight: 128,
})
2025-10-30 21:59:39 +00:00
const result = await uploadFileOrFallback(compressedFile)
2025-10-30 21:59:39 +00:00
room.picture = result.url
}
2026-07-28 16:16:26 +00:00
const createCommand = await $rooms.createRoom(url, room)
const createMessage = await createCommand.publish().waitForError()
2025-10-30 21:59:39 +00:00
if (createMessage && !createMessage.includes("already")) {
return pushToast({theme: "error", message: createMessage})
}
2026-07-28 16:16:26 +00:00
const editCommand = await $rooms.editRoom(url, room)
const editMessage = await editCommand.publish().waitForError()
2025-10-30 21:59:39 +00:00
if (editMessage) {
return pushToast({theme: "error", message: editMessage})
}
2026-07-28 16:16:26 +00:00
const joinCommand = await $rooms.joinRoom(url, room)
const joinMessage = await joinCommand.publish().waitForError()
2025-10-30 21:59:39 +00:00
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)
2026-07-28 16:16:26 +00:00
let roomType = $state(initialValues.livekit ? RoomType.Voice : RoomType.Text)
2025-10-30 21:59:39 +00:00
</script>
2026-01-30 22:14:12 +00:00
<Modal tag="form" onsubmit={preventDefault(trySubmit)}>
<ModalBody>
{@render header()}
<FieldInline>
{#snippet label()}
<p>Icon</p>
{/snippet}
{#snippet input()}
2026-07-15 16:30:17 +00:00
<IconInput bind:file={imageFile} bind:preview={imagePreview} previewClass="rounded-xl" />
2026-01-30 22:14:12 +00:00
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Name</p>
{/snippet}
{#snippet input()}
<label class="input input-group flex w-full items-center gap-2">
2026-01-30 22:14:12 +00:00
{#if imagePreview}
2026-07-15 16:30:17 +00:00
<ImageIcon src={imagePreview} alt="" class="rounded-xl" />
2026-01-30 22:14:12 +00:00
{:else}
<Icon icon={roomType === RoomType.Voice ? Volume : Hashtag} />
2026-01-30 22:14:12 +00:00
{/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-group flex w-full items-center gap-2">
2026-01-30 22:14:12 +00:00
<input bind:value={values.about} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
{#if $relayHasLivekit}
<FieldInline>
{#snippet label()}
<p>Room type</p>
{/snippet}
{#snippet input()}
<select class="select input w-full" bind:value={roomType} aria-label="Room type">
<option value={RoomType.Text}>Text</option>
<option value={RoomType.Voice}>Voice</option>
</select>
{/snippet}
</FieldInline>
{/if}
2026-01-30 22:14:12 +00:00
<strong class="md:hidden">Permissions</strong>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
<span class="text-sm opacity-75">Only allow members to send messages</span>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
<span class="text-sm opacity-75">Only allow members to read messages</span>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
<span class="text-sm opacity-75">Hide this group from non-members</span>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
<span class="text-sm opacity-75">Membership requires approval</span>
2026-01-30 22:14:12 +00:00
</div>
</ModalBody>
2025-10-30 21:59:39 +00:00
{@render footer({loading})}
2026-01-30 22:14:12 +00:00
</Modal>