mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
parent
f3dae06346
commit
fd8e5150a0
7 changed files with 464 additions and 7 deletions
|
@ -332,6 +332,12 @@ export interface RespOpenIDToken {
|
||||||
export type RoomVisibility = "public" | "private"
|
export type RoomVisibility = "public" | "private"
|
||||||
export type RoomPreset = "private_chat" | "public_chat" | "trusted_private_chat"
|
export type RoomPreset = "private_chat" | "public_chat" | "trusted_private_chat"
|
||||||
|
|
||||||
|
export interface CreateRoomInitialState {
|
||||||
|
type: EventType
|
||||||
|
state_key?: string
|
||||||
|
content: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReqCreateRoom {
|
export interface ReqCreateRoom {
|
||||||
visibility?: RoomVisibility
|
visibility?: RoomVisibility
|
||||||
room_alias_name?: string
|
room_alias_name?: string
|
||||||
|
@ -340,11 +346,7 @@ export interface ReqCreateRoom {
|
||||||
invite?: UserID[]
|
invite?: UserID[]
|
||||||
preset?: RoomPreset
|
preset?: RoomPreset
|
||||||
is_direct?: boolean
|
is_direct?: boolean
|
||||||
initial_state?: {
|
initial_state?: CreateRoomInitialState[]
|
||||||
type: EventType
|
|
||||||
state_key?: string
|
|
||||||
content: Record<string, unknown>
|
|
||||||
}[]
|
|
||||||
room_version?: string
|
room_version?: string
|
||||||
creation_content?: Record<string, unknown>
|
creation_content?: Record<string, unknown>
|
||||||
power_level_content_override?: Record<string, unknown>
|
power_level_content_override?: Record<string, unknown>
|
||||||
|
|
1
web/src/icons/add-circle.svg
Normal file
1
web/src/icons/add-circle.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M440-280h80v-160h160v-80H520v-160h-80v160H280v80h160v160Zm40 200q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
After Width: | Height: | Size: 468 B |
1
web/src/icons/add.svg
Normal file
1
web/src/icons/add.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"/></svg>
|
After Width: | Height: | Size: 183 B |
|
@ -88,9 +88,10 @@ div.room-search-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
height: 3rem;
|
height: 2.5rem;
|
||||||
width: 3rem;
|
width: 2.5rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
color: var(--text-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,12 @@ import toSearchableString from "@/util/searchablestring.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import MainScreenContext from "../MainScreenContext.ts"
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
import { keyToString } from "../keybindings.ts"
|
import { keyToString } from "../keybindings.ts"
|
||||||
|
import { ModalContext } from "../modal"
|
||||||
|
import CreateRoomView from "../roomview/CreateRoomView.tsx"
|
||||||
import Entry from "./Entry.tsx"
|
import Entry from "./Entry.tsx"
|
||||||
import FakeSpace from "./FakeSpace.tsx"
|
import FakeSpace from "./FakeSpace.tsx"
|
||||||
import Space from "./Space.tsx"
|
import Space from "./Space.tsx"
|
||||||
|
import AddCircleIcon from "@/icons/add-circle.svg?react"
|
||||||
import CloseIcon from "@/icons/close.svg?react"
|
import CloseIcon from "@/icons/close.svg?react"
|
||||||
import SearchIcon from "@/icons/search.svg?react"
|
import SearchIcon from "@/icons/search.svg?react"
|
||||||
import "./RoomList.css"
|
import "./RoomList.css"
|
||||||
|
@ -36,6 +39,7 @@ interface RoomListProps {
|
||||||
|
|
||||||
const RoomList = ({ activeRoomID, space }: RoomListProps) => {
|
const RoomList = ({ activeRoomID, space }: RoomListProps) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
|
const openModal = use(ModalContext)
|
||||||
const mainScreen = use(MainScreenContext)
|
const mainScreen = use(MainScreenContext)
|
||||||
const roomList = useEventAsState(client.store.roomList)
|
const roomList = useEventAsState(client.store.roomList)
|
||||||
const spaces = useEventAsState(client.store.topLevelSpaces)
|
const spaces = useEventAsState(client.store.topLevelSpaces)
|
||||||
|
@ -46,6 +50,14 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => {
|
||||||
client.store.currentRoomListQuery = toSearchableString(evt.target.value)
|
client.store.currentRoomListQuery = toSearchableString(evt.target.value)
|
||||||
directSetQuery(evt.target.value)
|
directSetQuery(evt.target.value)
|
||||||
}
|
}
|
||||||
|
const openCreateRoom = () => {
|
||||||
|
openModal({
|
||||||
|
dimmed: true,
|
||||||
|
boxed: true,
|
||||||
|
boxClass: "create-room-view-modal",
|
||||||
|
content: <CreateRoomView />,
|
||||||
|
})
|
||||||
|
}
|
||||||
const onClickSpace = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
|
const onClickSpace = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
|
||||||
const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!)
|
const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!)
|
||||||
mainScreen.setSpace(store)
|
mainScreen.setSpace(store)
|
||||||
|
@ -117,6 +129,9 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => {
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
id="room-search"
|
id="room-search"
|
||||||
/>
|
/>
|
||||||
|
{query === "" && <button onClick={openCreateRoom} title="Create room">
|
||||||
|
<AddCircleIcon/>
|
||||||
|
</button>}
|
||||||
<button onClick={clearQuery} disabled={query === ""}>
|
<button onClick={clearQuery} disabled={query === ""}>
|
||||||
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
||||||
</button>
|
</button>
|
||||||
|
|
132
web/src/ui/roomview/CreateRoomView.css
Normal file
132
web/src/ui/roomview/CreateRoomView.css
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
form.create-room-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input, select, textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: .5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: .25rem;
|
||||||
|
font-family: var(--monospace-font-stack);
|
||||||
|
font-size: 1em;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.form-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: .5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input, > select, > textarea {
|
||||||
|
grid-column: 2;
|
||||||
|
|
||||||
|
&#room-create-id {
|
||||||
|
font-family: var(--monospace-font-stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> input[type="checkbox"] {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--primary-color-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.form-fields.item-list {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: .25rem;
|
||||||
|
|
||||||
|
> div.item-list-header {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> button.item-list-add {
|
||||||
|
padding: 0 .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .item-list-item {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button.item-list-remove {
|
||||||
|
grid-column: 2;
|
||||||
|
padding: .25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.state-event-form {
|
||||||
|
display: grid;
|
||||||
|
gap: .25rem;
|
||||||
|
grid-template:
|
||||||
|
"type stateKey" auto
|
||||||
|
"content content" 1fr;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
> input.state-event-type {
|
||||||
|
grid-area: type;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input.state-event-key {
|
||||||
|
grid-area: stateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
> textarea.state-event-content {
|
||||||
|
grid-area: content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div.invite-user-ids {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div.error {
|
||||||
|
border: 2px solid var(--error-color);
|
||||||
|
border-radius: .25rem;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.create-room-view-modal {
|
||||||
|
width: min(35rem, 80vw);
|
||||||
|
|
||||||
|
> div.modal-box-inner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
305
web/src/ui/roomview/CreateRoomView.tsx
Normal file
305
web/src/ui/roomview/CreateRoomView.tsx
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2025 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import { Fragment, use, useState } from "react"
|
||||||
|
import { CreateRoomInitialState, RoomPreset, RoomVersion, UserID } from "@/api/types"
|
||||||
|
import { getServerName } from "@/util/validation"
|
||||||
|
import ClientContext from "../ClientContext"
|
||||||
|
import MainScreenContext from "../MainScreenContext"
|
||||||
|
import { ModalCloseContext } from "../modal"
|
||||||
|
import AddIcon from "@/icons/add.svg?react"
|
||||||
|
import CloseIcon from "@/icons/close.svg?react"
|
||||||
|
import InviteIcon from "@/icons/person-add.svg?react"
|
||||||
|
import "./CreateRoomView.css"
|
||||||
|
|
||||||
|
interface initialStateEntry {
|
||||||
|
type: string
|
||||||
|
stateKey: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateRoomView = () => {
|
||||||
|
const client = use(ClientContext)!
|
||||||
|
const closeModal = use(ModalCloseContext)
|
||||||
|
const mainScreen = use(MainScreenContext)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [preset, setPreset] = useState<RoomPreset>("private_chat")
|
||||||
|
const [name, setName] = useState("")
|
||||||
|
const [topic, setTopic] = useState("")
|
||||||
|
const [invite, setInvite] = useState<UserID[]>([])
|
||||||
|
const [isDirect, setIsDirect] = useState(false)
|
||||||
|
const [isEncrypted, setIsEncrypted] = useState(true)
|
||||||
|
const [initialState, setInitialState] = useState<initialStateEntry[]>([])
|
||||||
|
const [roomVersion, setRoomVersion] = useState<RoomVersion | "">("")
|
||||||
|
const [roomID, setRoomID] = useState("")
|
||||||
|
const [creationContent, setCreationContent] = useState<string>("{\n\n}")
|
||||||
|
const [powerLevelContentOverride, setPowerLevelContentOverride] = useState<string>(() => `{
|
||||||
|
"users": {
|
||||||
|
${JSON.stringify(client.store.userID)}: 9001
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
const onSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
let creation_content, power_level_content_override
|
||||||
|
try {
|
||||||
|
creation_content = JSON.parse(creationContent)
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Failed to parse creation content: ${err}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
power_level_content_override = JSON.parse(powerLevelContentOverride)
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Failed to parse power level content override: ${err}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let reqInitialState: CreateRoomInitialState[]
|
||||||
|
try {
|
||||||
|
reqInitialState = initialState.filter(state => state.type && state.content).map(state => ({
|
||||||
|
type: state.type,
|
||||||
|
state_key: state.stateKey,
|
||||||
|
content: JSON.parse(state.content),
|
||||||
|
}))
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Failed to parse initial state: ${err}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
evt.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError("")
|
||||||
|
if (isEncrypted) {
|
||||||
|
reqInitialState.push({
|
||||||
|
type: "m.room.encryption",
|
||||||
|
content: {
|
||||||
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
client.rpc.createRoom({
|
||||||
|
name: name || undefined,
|
||||||
|
topic: topic || undefined,
|
||||||
|
preset,
|
||||||
|
is_direct: isDirect,
|
||||||
|
invite: invite.filter(id => !!id),
|
||||||
|
initial_state: reqInitialState,
|
||||||
|
room_version: roomVersion || undefined,
|
||||||
|
creation_content,
|
||||||
|
power_level_content_override,
|
||||||
|
"fi.mau.room_id": roomID || undefined,
|
||||||
|
}).then(resp => {
|
||||||
|
closeModal()
|
||||||
|
console.log("Created room:", resp.room_id)
|
||||||
|
|
||||||
|
// FIXME this is a hacky way to work around the room taking time to come down /sync
|
||||||
|
setTimeout(() => {
|
||||||
|
mainScreen.setActiveRoom(resp.room_id)
|
||||||
|
}, 1000)
|
||||||
|
}, err => {
|
||||||
|
setError(`${err}`.replace(/^Error: /, ""))
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverName = getServerName(client.store.userID)
|
||||||
|
|
||||||
|
return <form className="create-room-view" onSubmit={onSubmit}>
|
||||||
|
<h2>Create a new room</h2>
|
||||||
|
|
||||||
|
<div className="form-fields">
|
||||||
|
<label htmlFor="room-create-name" title="The name of the room">Name</label>
|
||||||
|
<input
|
||||||
|
id="room-create-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Meow room"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="room-create-topic" title="A short description of the room">Topic</label>
|
||||||
|
<input
|
||||||
|
id="room-create-topic"
|
||||||
|
type="text"
|
||||||
|
placeholder="A room for meowing"
|
||||||
|
value={topic}
|
||||||
|
onChange={e => setTopic(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="room-create-encrypted" title="Whether the room is encrypted">
|
||||||
|
Encrypted
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="room-create-encrypted"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isEncrypted}
|
||||||
|
onChange={e => setIsEncrypted(e.target.checked)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label htmlFor="room-create-preset" title="Preset for join rules and history visibility">Preset</label>
|
||||||
|
<select id="room-create-preset" value={preset} onChange={e => setPreset(e.target.value as RoomPreset)}>
|
||||||
|
<option value="public_chat">Public chat</option>
|
||||||
|
<option value="private_chat">Private chat</option>
|
||||||
|
<option value="trusted_private_chat">Trusted private chat</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-fields item-list" id="room-create-invite">
|
||||||
|
<div className="item-list-header">
|
||||||
|
Users to invite
|
||||||
|
<button
|
||||||
|
className="item-list-add"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setInvite([...invite, ""])}
|
||||||
|
><InviteIcon /></button>
|
||||||
|
</div>
|
||||||
|
{invite.map((id, index) => {
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setInvite([...invite.slice(0, index), e.target.value, ...invite.slice(index + 1)])
|
||||||
|
const onRemove = () => setInvite([...invite.slice(0, index), ...invite.slice(index + 1)])
|
||||||
|
return <Fragment key={index}>
|
||||||
|
<input
|
||||||
|
className="item-list-item"
|
||||||
|
type="text"
|
||||||
|
placeholder={`@user:${serverName}`}
|
||||||
|
value={id}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="item-list-remove"
|
||||||
|
type="button"
|
||||||
|
onClick={onRemove}
|
||||||
|
><CloseIcon /></button>
|
||||||
|
</Fragment>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<details>
|
||||||
|
<summary>Advanced options</summary>
|
||||||
|
<div className="form-fields">
|
||||||
|
<label htmlFor="room-create-is-direct" title="Whether the room is a direct chat">
|
||||||
|
Direct chat
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="room-create-is-direct"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isDirect}
|
||||||
|
onChange={e => setIsDirect(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="room-create-version"
|
||||||
|
title="The version of the room to create. If unset, the server will decide"
|
||||||
|
>
|
||||||
|
Room version
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="room-create-version"
|
||||||
|
type="text"
|
||||||
|
placeholder="11"
|
||||||
|
value={roomVersion}
|
||||||
|
onChange={e => setRoomVersion(e.target.value as RoomVersion)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="room-create-id" title="Custom room ID. Only works if supported by the server.">
|
||||||
|
Room ID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="room-create-id"
|
||||||
|
type="text"
|
||||||
|
placeholder={`!meow:${serverName}`}
|
||||||
|
value={roomID}
|
||||||
|
onChange={e => setRoomID(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="room-create-power-level-override" title="Override power levels in the room">
|
||||||
|
Power level override
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="room-create-power-level-override"
|
||||||
|
value={powerLevelContentOverride}
|
||||||
|
onChange={e => setPowerLevelContentOverride(e.target.value)}
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
<label htmlFor="room-create-creation-content" title="Override the creation content of the room">
|
||||||
|
Creation content
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="room-create-creation-content"
|
||||||
|
value={creationContent}
|
||||||
|
onChange={e => setCreationContent(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-fields item-list state-event-list" id="room-create-initial-state">
|
||||||
|
<div className="item-list-header">
|
||||||
|
Initial state
|
||||||
|
<button
|
||||||
|
className="item-list-state-add"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setInitialState([
|
||||||
|
...initialState,
|
||||||
|
{ type: "", stateKey: "", content: "{\n\n}" },
|
||||||
|
])}
|
||||||
|
><AddIcon /></button>
|
||||||
|
</div>
|
||||||
|
{initialState.map((state, index) => {
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setInitialState([
|
||||||
|
...initialState.slice(0, index),
|
||||||
|
{
|
||||||
|
...initialState[index],
|
||||||
|
[e.target.dataset.field!]: e.target.value,
|
||||||
|
},
|
||||||
|
...initialState.slice(index + 1),
|
||||||
|
])
|
||||||
|
const onRemove = () => setInitialState([
|
||||||
|
...initialState.slice(0, index),
|
||||||
|
...initialState.slice(index + 1),
|
||||||
|
])
|
||||||
|
return <Fragment key={index}>
|
||||||
|
<div className="item-list-item state-event-form">
|
||||||
|
<input
|
||||||
|
className="state-event-type"
|
||||||
|
type="text"
|
||||||
|
data-field="type"
|
||||||
|
placeholder="Event type"
|
||||||
|
value={state.type}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="state-event-key"
|
||||||
|
type="text"
|
||||||
|
data-field="stateKey"
|
||||||
|
placeholder="State key"
|
||||||
|
value={state.stateKey}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="state-event-content"
|
||||||
|
data-field="content"
|
||||||
|
placeholder="Event content"
|
||||||
|
value={state.content}
|
||||||
|
onChange={onChange}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="item-list-remove"
|
||||||
|
type="button"
|
||||||
|
onClick={onRemove}
|
||||||
|
><CloseIcon /></button>
|
||||||
|
</Fragment>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<button type="submit" disabled={loading}>Create</button>
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateRoomView
|
Loading…
Add table
Reference in a new issue