Initial commit
This commit is contained in:
		
							
								
								
									
										64
									
								
								web/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								web/index.css
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| } | ||||
							
								
								
									
										14
									
								
								web/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								web/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
							
								
								
									
										175
									
								
								web/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								web/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										64
									
								
								web/spinner.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								web/spinner.css
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Tulir Asokan
					Tulir Asokan