From 87e88ccccaf3658687a9a8575ef18d422df6f95e Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Tue, 16 Apr 2024 13:22:24 +0200 Subject: [PATCH] Add support for soju.im/filehost For now, only handle paste events containing files. Co-authored-by: Alex McGrath --- components/app.js | 6 +++ components/composer.js | 83 ++++++++++++++++++++++++++++++++++++++++++ lib/irc.js | 4 ++ 3 files changed, 93 insertions(+) diff --git a/components/app.js b/components/app.js index a27c915..51b6e0b 100644 --- a/components/app.js +++ b/components/app.js @@ -1967,6 +1967,11 @@ export default class App extends Component { } } + let activeClient = null; + if (activeBuffer) { + activeClient = this.clients.get(activeBuffer.server); + } + if (this.state.connectForm) { let status = activeServer ? activeServer.status : ServerStatus.DISCONNECTED; let connecting = status === ServerStatus.CONNECTING || status === ServerStatus.REGISTERING; @@ -2208,6 +2213,7 @@ export default class App extends Component { ${memberList} <${Composer} ref=${this.composer} + client=${activeClient} readOnly=${composerReadOnly} onSubmit=${this.handleComposerSubmit} autocomplete=${this.autocomplete} diff --git a/components/composer.js b/components/composer.js index 2e9a533..eaa5659 100644 --- a/components/composer.js +++ b/components/composer.js @@ -1,5 +1,16 @@ 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: "", @@ -13,6 +24,7 @@ export default class Composer extends Component { 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); } @@ -116,6 +128,71 @@ export default class Composer extends Component { 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