import { html, Component, createRef } from "../lib/index.js"; function encodeContentDisposition(filename) { // Encode filename according to RFC 5987 if necessary. Note, // encodeURIComponent will percent-encode a superset of attr-char. let encodedFilename = encodeURIComponent(filename); if (encodedFilename === filename) { return "attachment; filename=\"" + filename + "\""; } else { return "attachment; filename*=UTF-8''" + encodedFilename; } } export default class Composer extends Component { state = { text: "", }; textInput = createRef(); lastAutocomplete = null; constructor(props) { super(props); this.handleInput = this.handleInput.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleInputKeyDown = this.handleInputKeyDown.bind(this); this.handleInputPaste = this.handleInputPaste.bind(this); this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this); this.handleWindowPaste = this.handleWindowPaste.bind(this); } handleInput(event) { this.setState({ [event.target.name]: event.target.value }); if (this.props.readOnly && event.target.name === "text" && !event.target.value) { event.target.blur(); } } handleSubmit(event) { event.preventDefault(); this.props.onSubmit(this.state.text); this.setState({ text: "" }); } handleInputKeyDown(event) { let input = event.target; if (!this.props.autocomplete || event.key !== "Tab") { return; } if (input.selectionStart !== input.selectionEnd) { return; } event.preventDefault(); let carretPos = input.selectionStart; let text = this.state.text; let autocomplete; if (this.lastAutocomplete && this.lastAutocomplete.text === text && this.lastAutocomplete.carretPos === carretPos) { autocomplete = this.lastAutocomplete; } else { this.lastAutocomplete = null; let wordStart; for (wordStart = carretPos - 1; wordStart >= 0; wordStart--) { if (text[wordStart] === " ") { break; } } wordStart++; let wordEnd; for (wordEnd = carretPos; wordEnd < text.length; wordEnd++) { if (text[wordEnd] === " ") { break; } } let word = text.slice(wordStart, wordEnd); if (!word) { return; } let replacements = this.props.autocomplete(word); if (replacements.length === 0) { return; } autocomplete = { text, carretPos: input.selectionStart, prefix: text.slice(0, wordStart), suffix: text.slice(wordEnd), replacements, replIndex: -1, }; } let n = autocomplete.replacements.length; if (event.shiftKey) { autocomplete.replIndex--; } else { autocomplete.replIndex++; } autocomplete.replIndex = (autocomplete.replIndex + n) % n; let repl = autocomplete.replacements[autocomplete.replIndex]; if (!autocomplete.prefix && !autocomplete.suffix) { if (repl.startsWith("/")) { repl += " "; } else { repl += ": "; } } autocomplete.text = autocomplete.prefix + repl + autocomplete.suffix; autocomplete.carretPos = autocomplete.prefix.length + repl.length; input.value = autocomplete.text; input.selectionStart = autocomplete.carretPos; input.selectionEnd = input.selectionStart; this.lastAutocomplete = autocomplete; this.setState({ text: autocomplete.text }); } async handleInputPaste(event) { let client = this.props.client; if (!event.clipboardData.files.length || !client || this.props.readOnly) { return; } let endpoint = client.isupport.filehost(); if (!endpoint) { return; } event.preventDefault(); event.stopImmediatePropagation(); // TODO: support more than one file let file = event.clipboardData.files.item(0); let auth; if (client.params.saslPlain) { let params = client.params.saslPlain; auth = "Basic " + btoa(params.username + ":" + params.password); } else if (client.params.saslOauthBearer) { auth = "Bearer " + client.params.saslOauthBearer.token; } let headers = { "Content-Length": file.size, "Content-Disposition": encodeContentDisposition(file.name), }; if (file.type) { headers["Content-Type"] = file.type; } if (auth) { headers["Authorization"] = auth; } // TODO: show a loading UI while uploading // TODO: show a cancel button let resp = await fetch(endpoint, { method: "POST", body: file, headers, credentials: "include", }); if (!resp.ok) { throw new Error(`HTTP request failed (${resp.status})`); } let loc = resp.headers.get("Location"); if (!loc) { throw new Error("filehost response missing Location header field"); } let uploadURL = new URL(loc, endpoint); this.setState((state) => { if (state.text) { return { text: state.text + " " + uploadURL.toString() }; } else { return { text: uploadURL.toString() }; } }); } handleWindowKeyDown(event) { // If an or