diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/index.css | 64 | ||||
| -rw-r--r-- | web/index.html | 14 | ||||
| -rw-r--r-- | web/index.js | 175 | ||||
| -rw-r--r-- | web/spinner.css | 64 |
4 files changed, 317 insertions, 0 deletions
diff --git a/web/index.css b/web/index.css new file mode 100644 index 0000000..1ff2fc4 --- /dev/null +++ b/web/index.css @@ -0,0 +1,64 @@ +/* +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/. +*/ + +* { + font-family: sans-serif; +} + +html { + scrollbar-width: none; +} + +body { + margin: 0; +} + +.main:not(.pack-list) { + margin: 2rem; +} + +.main.empty { + text-align: center; +} + +.stickerpack > .sticker-list { + display: flex; + flex-wrap: wrap; +} + +.stickerpack > h1 { + margin: .75rem; +} + +.sticker { + display: flex; + padding: 4px; + cursor: pointer; + position: relative; + width: 25vw; + height: 25vw; + box-sizing: border-box; +} + +.sticker:hover { + background-color: #eee; +} + +.sticker > img { + display: none; + width: 100%; + object-fit: contain; +} + +.sticker > img.visible { + display: initial; +} + +h1 { + font-size: 1rem; +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..66ebff4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <title>Maunium sticker picker</title> + <link rel="stylesheet" href="index.css"/> + <link rel="stylesheet" href="spinner.css"/> +</head> +<body> + <noscript>This sticker picker requires JavaScript</noscript> + <script src="index.js" type="module"></script> +</body> +</html> 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) diff --git a/web/spinner.css b/web/spinner.css new file mode 100644 index 0000000..dcf6832 --- /dev/null +++ b/web/spinner.css @@ -0,0 +1,64 @@ +/* Chase spinner from https://tobiasahlin.com/spinkit/. MIT license */ +.sk-center-wrapper { + width: 100%; + display: flex; + justify-content: space-around; +} + +.sk-chase { + position: relative; + animation: sk-chase 2.5s infinite linear both; +} + +.sk-chase-dot { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + animation: sk-chase-dot 2.0s infinite ease-in-out both; +} + +.sk-chase-dot:before { + content: ''; + display: block; + width: 25%; + height: 25%; + border-radius: 100%; + animation: sk-chase-dot-before 2.0s infinite ease-in-out both; + background-color: #FFF; +} + +.sk-chase.green > .sk-chase-dot:before { + background-color: #00C853; +} + +.sk-chase-dot:nth-child(1) { animation-delay: -1.1s; } +.sk-chase-dot:nth-child(2) { animation-delay: -1.0s; } +.sk-chase-dot:nth-child(3) { animation-delay: -0.9s; } +.sk-chase-dot:nth-child(4) { animation-delay: -0.8s; } +.sk-chase-dot:nth-child(5) { animation-delay: -0.7s; } +.sk-chase-dot:nth-child(6) { animation-delay: -0.6s; } +.sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; } +.sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; } +.sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; } +.sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; } +.sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; } +.sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; } + +@keyframes sk-chase { + 100% { transform: rotate(360deg); } +} + +@keyframes sk-chase-dot { + 80%, 100% { transform: rotate(360deg); } +} + +@keyframes sk-chase-dot-before { + 50% { + transform: scale(0.4); + } + 100%, 0% { + transform: scale(1.0); + } +} |
