aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/index.css64
-rw-r--r--web/index.html14
-rw-r--r--web/index.js175
-rw-r--r--web/spinner.css64
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);
+ }
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage