From 7f8691585741d64bbe1a91c2c1548560d9ee1ffd Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 5 Sep 2020 02:31:34 +0300 Subject: Initial commit --- web/index.css | 64 +++++++++++++++++++++ web/index.html | 14 +++++ web/index.js | 175 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/spinner.css | 64 +++++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 web/index.css create mode 100644 web/index.html create mode 100644 web/index.js create mode 100644 web/spinner.css (limited to 'web') 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 @@ + + + + + + Maunium sticker picker + + + + + + + + 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`
<${Spinner} size=${80} green />
` + } else if (this.state.error) { + return html`
+

Failed to load packs

+

${this.state.error}

+
` + } else if (this.state.packs.length === 0) { + return html`

No packs found :(

` + } + return html`
+ ${this.state.packs.map(pack => html`<${Pack} id=${pack.id} ...${pack}/>`)} +
` + } +} + +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` +
+
+
+
+
+
+
+
+ ` + if (!noCenter) { + return html`
${comp}
` + } + return comp +} + +const Pack = ({ title, stickers }) => html`
+

${title}

+
+ ${stickers.map(sticker => html` + <${Sticker} key=${sticker["net.maunium.telegram.sticker"].id} content=${sticker}/> + `)} +
+
` + +const Sticker = ({ content }) => html`
sendSticker(content)}> + ${content.body} +
` + +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); + } +} -- cgit v1.2.3