diff options
| author | Tulir Asokan <tulir@maunium.net> | 2020-09-05 02:31:34 +0300 |
|---|---|---|
| committer | Tulir Asokan <tulir@maunium.net> | 2020-09-05 02:31:34 +0300 |
| commit | 7f8691585741d64bbe1a91c2c1548560d9ee1ffd (patch) | |
| tree | 6b040f7be16620acf3846510801dada07b0687c0 /web/index.js | |
Initial commit
Diffstat (limited to 'web/index.js')
| -rw-r--r-- | web/index.js | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/web/index.js b/web/index.js new file mode 100644 index 0000000..4c0dab5 --- /dev/null +++ b/web/index.js @@ -0,0 +1,175 @@ +// Copyright (c) 2020 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +import { html, render, Component } from "https://unpkg.com/htm/preact/index.mjs?module" + +// The base URL for fetching packs. The app will first fetch ${PACK_BASE_URL}/index.json, +// then ${PACK_BASE_URL}/${packFile} for each packFile in the packs object of the index.json file. +const PACKS_BASE_URL = "packs" +// This is updated from packs/index.json +let HOMESERVER_URL = "https://matrix-client.matrix.org" + +const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${mxc.substr(6)}?height=128&width=128&method=scale` + +class App extends Component { + constructor(props) { + super(props) + this.state = { + packs: [], + loading: true, + error: null, + } + this.observer = null + } + + observeIntersection = intersections => { + for (const entry of intersections) { + const img = entry.target.children.item(0) + if (entry.isIntersecting) { + img.setAttribute("src", img.getAttribute("data-src")) + img.classList.add("visible") + } else { + img.removeAttribute("src") + img.classList.remove("visible") + } + } + } + + componentDidMount() { + fetch(`${PACKS_BASE_URL}/index.json`).then(async indexRes => { + if (indexRes.status >= 400) { + this.setState({ + loading: false, + error: indexRes.status !== 404 ? indexRes.statusText : null, + }) + return + } + const indexData = await indexRes.json() + HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL + // TODO only load pack metadata when scrolled into view? + for (const packFile of indexData.packs) { + const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`) + const packData = await packRes.json() + this.setState({ + packs: [...this.state.packs, packData], + loading: false, + }) + } + }, error => this.setState({ loading: false, error })) + this.observer = new IntersectionObserver(this.observeIntersection, { + rootMargin: "100px", + threshold: 0, + }) + } + + componentDidUpdate() { + for (const elem of document.getElementsByClassName("sticker")) { + this.observer.observe(elem) + } + } + + componentWillUnmount() { + this.observer.disconnect() + } + + render() { + if (this.state.loading) { + return html`<div class="main spinner"><${Spinner} size=${80} green /></div>` + } else if (this.state.error) { + return html`<div class="main error"> + <h1>Failed to load packs</h1> + <p>${this.state.error}</p> + </div>` + } else if (this.state.packs.length === 0) { + return html`<div class="main empty"><h1>No packs found :(</h1></div>` + } + return html`<div class="main pack-list"> + ${this.state.packs.map(pack => html`<${Pack} id=${pack.id} ...${pack}/>`)} + </div>` + } +} + +const Spinner = ({ size = 40, noCenter = false, noMargin = false, green = false }) => { + let margin = 0 + if (!isNaN(+size)) { + size = +size + margin = noMargin ? 0 : `${Math.round(size / 6)}px` + size = `${size}px` + } + const noInnerMargin = !noCenter || !margin + const comp = html` + <div style="width: ${size}; height: ${size}; margin: ${noInnerMargin ? 0 : margin} 0;" + class="sk-chase ${green && "green"}"> + <div class="sk-chase-dot" /> + <div class="sk-chase-dot" /> + <div class="sk-chase-dot" /> + <div class="sk-chase-dot" /> + <div class="sk-chase-dot" /> + <div class="sk-chase-dot" /> + </div> + ` + if (!noCenter) { + return html`<div style="margin: ${margin} 0;" class="sk-center-wrapper">${comp}</div>` + } + return comp +} + +const Pack = ({ title, stickers }) => html`<div class="stickerpack"> + <h1>${title}</h1> + <div class="sticker-list"> + ${stickers.map(sticker => html` + <${Sticker} key=${sticker["net.maunium.telegram.sticker"].id} content=${sticker}/> + `)} + </div> +</div>` + +const Sticker = ({ content }) => html`<div class="sticker" onClick=${() => sendSticker(content)}> + <img data-src=${makeThumbnailURL(content.url)} alt=${content.body} /> +</div>` + +function sendSticker(content) { + window.parent.postMessage({ + api: "fromWidget", + action: "m.sticker", + requestId: `sticker-${Date.now()}`, + widgetId, + data: { + name: content.body, + content, + }, + }, "*") +} + +let widgetId = null + +window.onmessage = event => { + if (!window.parent || !event.data) { + return + } + + const request = event.data + if (!request.requestId || !request.widgetId || !request.action || request.api !== "toWidget") { + return + } + + if (widgetId) { + if (widgetId !== request.widgetId) { + return + } + } else { + widgetId = request.widgetId + } + + window.parent.postMessage({ + ...request, + response: request.action === "capabilities" ? { + capabilities: ["m.sticker"], + } : { + error: { message: "Action not supported" }, + }, + }, event.origin) +} + +render(html`<${App} />`, document.body) |
