mirror of
https://github.com/wah-su/mstickers.git
synced 2025-05-04 03:49:35 +05:00
Initial Commit
This commit is contained in:
commit
e50589906c
38 changed files with 3645 additions and 0 deletions
34
stickerpicker/src/frequently-used.js
Normal file
34
stickerpicker/src/frequently-used.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const FREQUENTLY_USED = JSON.parse(window.localStorage.mauFrequentlyUsedStickerIDs || "{}")
|
||||
let FREQUENTLY_USED_SORTED = null
|
||||
|
||||
export const add = id => {
|
||||
const [count] = FREQUENTLY_USED[id] || [0]
|
||||
FREQUENTLY_USED[id] = [count + 1, Date.now()]
|
||||
window.localStorage.mauFrequentlyUsedStickerIDs = JSON.stringify(FREQUENTLY_USED)
|
||||
FREQUENTLY_USED_SORTED = null
|
||||
}
|
||||
|
||||
export const get = (limit = 16) => {
|
||||
if (FREQUENTLY_USED_SORTED === null) {
|
||||
FREQUENTLY_USED_SORTED = Object.entries(FREQUENTLY_USED)
|
||||
.sort(([, [count1, date1]], [, [count2, date2]]) =>
|
||||
count2 === count1 ? date2 - date1 : count2 - count1)
|
||||
.map(([emoji]) => emoji)
|
||||
}
|
||||
return FREQUENTLY_USED_SORTED.slice(0, limit)
|
||||
}
|
498
stickerpicker/src/index.js
Normal file
498
stickerpicker/src/index.js
Normal file
|
@ -0,0 +1,498 @@
|
|||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { html, render, Component } from "../lib/htm/preact.js";
|
||||
import { Spinner } from "./spinner.js";
|
||||
import { SearchBox } from "./search-box.js";
|
||||
import * as widgetAPI from "./widget-api.js";
|
||||
import * as frequent from "./frequently-used.js";
|
||||
|
||||
// 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 = "../stickerpacks";
|
||||
|
||||
let INDEX = `${PACKS_BASE_URL}/index.json`;
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
if (params.has("config")) {
|
||||
INDEX = params.get("config");
|
||||
}
|
||||
// This is updated from packs/index.json
|
||||
let HOMESERVER_URL = "https://matrix-client.matrix.org";
|
||||
|
||||
// const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/v3/thumbnail/${mxc.slice(6)}?height=128&width=128&method=scale`
|
||||
const makeThumbnailURL = (mxc) =>
|
||||
`${HOMESERVER_URL}/__thumbnail/${mxc.slice(0, 2)}/${mxc.slice(
|
||||
2,
|
||||
4
|
||||
)}/${mxc.slice(4)}`;
|
||||
|
||||
// We need to detect iOS webkit because it has a bug related to scrolling non-fixed divs
|
||||
// This is also used to fix scrolling to sections on Element iOS
|
||||
const isMobileSafari =
|
||||
navigator.userAgent.match(/(iPod|iPhone|iPad)/) &&
|
||||
navigator.userAgent.match(/AppleWebKit/);
|
||||
|
||||
const supportedThemes = ["light", "dark", "black"];
|
||||
|
||||
const defaultState = {
|
||||
packs: [],
|
||||
filtering: {
|
||||
searchTerm: "",
|
||||
packs: [],
|
||||
},
|
||||
};
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.defaultTheme = params.get("theme");
|
||||
this.state = {
|
||||
viewingGifs: false,
|
||||
packs: defaultState.packs,
|
||||
loading: true,
|
||||
error: null,
|
||||
stickersPerRow: parseInt(localStorage.mauStickersPerRow || "4"),
|
||||
theme: localStorage.mauStickerThemeOverride || this.defaultTheme,
|
||||
frequentlyUsed: {
|
||||
id: "frequently-used",
|
||||
title: "Frequently used",
|
||||
stickerIDs: frequent.get(),
|
||||
stickers: [],
|
||||
},
|
||||
filtering: defaultState.filtering,
|
||||
};
|
||||
if (!supportedThemes.includes(this.state.theme)) {
|
||||
this.state.theme = "light";
|
||||
}
|
||||
if (!supportedThemes.includes(this.defaultTheme)) {
|
||||
this.defaultTheme = "light";
|
||||
}
|
||||
this.stickersByID = new Map(
|
||||
JSON.parse(localStorage.mauFrequentlyUsedStickerCache || "[]")
|
||||
);
|
||||
this.state.frequentlyUsed.stickers = this._getStickersByID(
|
||||
this.state.frequentlyUsed.stickerIDs
|
||||
);
|
||||
this.imageObserver = null;
|
||||
this.packListRef = null;
|
||||
this.navRef = null;
|
||||
this.searchStickers = this.searchStickers.bind(this);
|
||||
this.sendSticker = this.sendSticker.bind(this);
|
||||
this.navScroll = this.navScroll.bind(this);
|
||||
this.reloadPacks = this.reloadPacks.bind(this);
|
||||
this.observeSectionIntersections =
|
||||
this.observeSectionIntersections.bind(this);
|
||||
this.observeImageIntersections = this.observeImageIntersections.bind(this);
|
||||
}
|
||||
|
||||
_getStickersByID(ids) {
|
||||
return ids
|
||||
.map((id) => this.stickersByID.get(id))
|
||||
.filter((sticker) => !!sticker);
|
||||
}
|
||||
|
||||
updateFrequentlyUsed() {
|
||||
const stickerIDs = frequent.get();
|
||||
const stickers = this._getStickersByID(stickerIDs);
|
||||
this.setState({
|
||||
frequentlyUsed: {
|
||||
...this.state.frequentlyUsed,
|
||||
stickerIDs,
|
||||
stickers,
|
||||
},
|
||||
});
|
||||
localStorage.mauFrequentlyUsedStickerCache = JSON.stringify(
|
||||
stickers.map((sticker) => [sticker.id, sticker])
|
||||
);
|
||||
}
|
||||
|
||||
searchStickers(e) {
|
||||
const sanitizeString = (s) => s.toLowerCase().trim();
|
||||
const searchTerm = sanitizeString(e.target.value);
|
||||
|
||||
const allPacks = [this.state.frequentlyUsed, ...this.state.packs];
|
||||
const packsWithFilteredStickers = allPacks.map((pack) => ({
|
||||
...pack,
|
||||
stickers: pack.stickers.filter(
|
||||
(sticker) =>
|
||||
sanitizeString(sticker.body).includes(searchTerm) ||
|
||||
sanitizeString(sticker.id).includes(searchTerm)
|
||||
),
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
filtering: {
|
||||
...this.state.filtering,
|
||||
searchTerm,
|
||||
packs: packsWithFilteredStickers.filter(
|
||||
({ stickers }) => !!stickers.length
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setStickersPerRow(val) {
|
||||
localStorage.mauStickersPerRow = val;
|
||||
document.documentElement.style.setProperty(
|
||||
"--stickers-per-row",
|
||||
localStorage.mauStickersPerRow
|
||||
);
|
||||
this.setState({
|
||||
stickersPerRow: val,
|
||||
});
|
||||
this.packListRef.scrollTop = this.packListRef.scrollHeight;
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
if (theme === "default") {
|
||||
delete localStorage.mauStickerThemeOverride;
|
||||
this.setState({ theme: this.defaultTheme });
|
||||
} else {
|
||||
localStorage.mauStickerThemeOverride = theme;
|
||||
this.setState({ theme: theme });
|
||||
}
|
||||
}
|
||||
|
||||
reloadPacks() {
|
||||
this.imageObserver.disconnect();
|
||||
this.sectionObserver.disconnect();
|
||||
this.setState({
|
||||
packs: defaultState.packs,
|
||||
filtering: defaultState.filtering,
|
||||
});
|
||||
this._loadPacks(true);
|
||||
}
|
||||
|
||||
_loadPacks(disableCache = false) {
|
||||
const cache = disableCache ? "no-cache" : undefined;
|
||||
fetch(INDEX, { cache }).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) {
|
||||
let packRes;
|
||||
if (
|
||||
packFile.startsWith("https://") ||
|
||||
packFile.startsWith("http://")
|
||||
) {
|
||||
packRes = await fetch(packFile, { cache });
|
||||
} else {
|
||||
packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`, { cache });
|
||||
}
|
||||
const packData = await packRes.json();
|
||||
for (const sticker of packData.stickers) {
|
||||
this.stickersByID.set(sticker.id, sticker);
|
||||
}
|
||||
this.setState({
|
||||
packs: [...this.state.packs, packData],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
this.updateFrequentlyUsed();
|
||||
},
|
||||
(error) => this.setState({ loading: false, error })
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.documentElement.style.setProperty(
|
||||
"--stickers-per-row",
|
||||
this.state.stickersPerRow.toString()
|
||||
);
|
||||
this._loadPacks();
|
||||
this.imageObserver = new IntersectionObserver(
|
||||
this.observeImageIntersections,
|
||||
{
|
||||
rootMargin: "100px",
|
||||
}
|
||||
);
|
||||
this.sectionObserver = new IntersectionObserver(
|
||||
this.observeSectionIntersections
|
||||
);
|
||||
}
|
||||
|
||||
observeImageIntersections(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeSectionIntersections(intersections) {
|
||||
const navWidth = this.navRef.getBoundingClientRect().width;
|
||||
let minX = 0,
|
||||
maxX = navWidth;
|
||||
let minXElem = null;
|
||||
let maxXElem = null;
|
||||
for (const entry of intersections) {
|
||||
const packID = entry.target.getAttribute("data-pack-id");
|
||||
if (!packID) {
|
||||
continue;
|
||||
}
|
||||
const navElement = document.getElementById(`nav-${packID}`);
|
||||
if (entry.isIntersecting) {
|
||||
navElement.classList.add("visible");
|
||||
const bb = navElement.getBoundingClientRect();
|
||||
if (bb.x < minX) {
|
||||
minX = bb.x;
|
||||
minXElem = navElement;
|
||||
} else if (bb.right > maxX) {
|
||||
maxX = bb.right;
|
||||
maxXElem = navElement;
|
||||
}
|
||||
} else {
|
||||
navElement.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
if (minXElem !== null) {
|
||||
minXElem.scrollIntoView({ inline: "start" });
|
||||
} else if (maxXElem !== null) {
|
||||
maxXElem.scrollIntoView({ inline: "end" });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.packListRef === null) {
|
||||
return;
|
||||
}
|
||||
for (const elem of this.packListRef.getElementsByClassName("sticker")) {
|
||||
this.imageObserver.observe(elem);
|
||||
}
|
||||
for (const elem of this.packListRef.children) {
|
||||
this.sectionObserver.observe(elem);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.imageObserver.disconnect();
|
||||
this.sectionObserver.disconnect();
|
||||
}
|
||||
|
||||
sendSticker(evt) {
|
||||
const id = evt.currentTarget.getAttribute("data-sticker-id");
|
||||
const sticker = this.stickersByID.get(id);
|
||||
frequent.add(id);
|
||||
this.updateFrequentlyUsed();
|
||||
widgetAPI.sendSticker(sticker);
|
||||
}
|
||||
|
||||
navScroll(evt) {
|
||||
this.navRef.scrollLeft += evt.deltaY;
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = `theme-${this.state.theme}`;
|
||||
const filterActive = !!this.state.filtering.searchTerm;
|
||||
const packs = filterActive
|
||||
? this.state.filtering.packs
|
||||
: [this.state.frequentlyUsed, ...this.state.packs];
|
||||
|
||||
if (this.state.loading) {
|
||||
return html`
|
||||
<main class="spinner ${theme}">
|
||||
<${Spinner} size=${80} green />
|
||||
</main>
|
||||
`;
|
||||
} else if (this.state.error) {
|
||||
return html`
|
||||
<main class="error ${theme}">
|
||||
<h1>Failed to load packs</h1>
|
||||
<p>${this.state.error}</p>
|
||||
</main>
|
||||
`;
|
||||
} else if (this.state.packs.length === 0) {
|
||||
return html`
|
||||
<main class="empty ${theme}"><h1>No packs found 😿</h1></main>
|
||||
`;
|
||||
}
|
||||
|
||||
const onClickOverride = this.state.viewingGifs
|
||||
? (evt, packID) => {
|
||||
evt.preventDefault();
|
||||
this.setState({ viewingGifs: false }, () => {
|
||||
scrollToSection(null, packID);
|
||||
});
|
||||
}
|
||||
: null;
|
||||
|
||||
return html` <main class="has-content ${theme}">
|
||||
<nav onWheel=${this.navScroll} ref=${(elem) => (this.navRef = elem)}>
|
||||
<${NavBarItem}
|
||||
pack=${this.state.frequentlyUsed}
|
||||
iconOverride="recent"
|
||||
onClickOverride=${onClickOverride}
|
||||
/>
|
||||
${this.state.packs.map(
|
||||
(pack) =>
|
||||
html`<${NavBarItem}
|
||||
id=${pack.id}
|
||||
pack=${pack}
|
||||
onClickOverride=${onClickOverride}
|
||||
/>`
|
||||
)}
|
||||
<${NavBarItem}
|
||||
pack=${{ id: "settings", title: "Settings" }}
|
||||
iconOverride="settings"
|
||||
onClickOverride=${onClickOverride}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<${SearchBox}
|
||||
onInput=${this.searchStickers}
|
||||
value=${this.state.filtering.searchTerm ?? ""}
|
||||
/>
|
||||
<div
|
||||
class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}"
|
||||
ref=${(elem) => (this.packListRef = elem)}
|
||||
>
|
||||
${filterActive && packs.length === 0
|
||||
? html`<div class="search-empty">
|
||||
<h1>No stickers match your search</h1>
|
||||
</div>`
|
||||
: null}
|
||||
${packs.map(
|
||||
(pack) =>
|
||||
html`<${Pack}
|
||||
id=${pack.id}
|
||||
pack=${pack}
|
||||
send=${this.sendSticker}
|
||||
/>`
|
||||
)}
|
||||
<${Settings} app=${this} />
|
||||
</div>
|
||||
</main>`;
|
||||
}
|
||||
}
|
||||
|
||||
const Settings = ({ app }) => html`
|
||||
<section
|
||||
class="stickerpack settings"
|
||||
id="pack-settings"
|
||||
data-pack-id="settings"
|
||||
>
|
||||
<h1>Settings</h1>
|
||||
<div class="settings-list">
|
||||
<button onClick=${app.reloadPacks}>Reload</button>
|
||||
<div>
|
||||
<label for="stickers-per-row"
|
||||
>Stickers per row: ${app.state.stickersPerRow}</label
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="2"
|
||||
max="10"
|
||||
id="stickers-per-row"
|
||||
id="stickers-per-row"
|
||||
value=${app.state.stickersPerRow}
|
||||
onInput=${(evt) => app.setStickersPerRow(evt.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="theme">Theme: </label>
|
||||
<select
|
||||
name="theme"
|
||||
id="theme"
|
||||
onChange=${(evt) => app.setTheme(evt.target.value)}
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="black">Black</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
// By default we just let the browser handle scrolling to sections, but webviews on Element iOS
|
||||
// open the link in the browser instead of just scrolling there, so we need to scroll manually:
|
||||
const scrollToSection = (evt, id) => {
|
||||
const pack = document.getElementById(`pack-${id}`);
|
||||
if (pack) {
|
||||
pack.scrollIntoView({ block: "start", behavior: "instant" });
|
||||
}
|
||||
evt?.preventDefault();
|
||||
};
|
||||
|
||||
const NavBarItem = ({
|
||||
pack,
|
||||
iconOverride = null,
|
||||
onClickOverride = null,
|
||||
extraClass = null,
|
||||
}) => html`
|
||||
<a
|
||||
href="#pack-${pack.id}"
|
||||
id="nav-${pack.id}"
|
||||
data-pack-id=${pack.id}
|
||||
title=${pack.title}
|
||||
class="${extraClass}"
|
||||
onClick=${onClickOverride
|
||||
? (evt) => onClickOverride(evt, pack.id)
|
||||
: isMobileSafari
|
||||
? (evt) => scrollToSection(evt, pack.id)
|
||||
: undefined}
|
||||
>
|
||||
<div class="sticker">
|
||||
${iconOverride
|
||||
? html` <span class="icon icon-${iconOverride}" /> `
|
||||
: html`
|
||||
<img
|
||||
src=${makeThumbnailURL(pack.stickers[0].id)}
|
||||
alt=${pack.stickers[0].body}
|
||||
class="visible"
|
||||
/>
|
||||
`}
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
const Pack = ({ pack, send }) => html`
|
||||
<section class="stickerpack" id="pack-${pack.id}" data-pack-id=${pack.id}>
|
||||
<h1>${pack.title}</h1>
|
||||
<div class="sticker-list">
|
||||
${pack.stickers.map(
|
||||
(sticker) => html`
|
||||
<${Sticker} key=${sticker.id} content=${sticker} send=${send} />
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
const Sticker = ({ content, send }) => html`
|
||||
<div class="sticker" onClick=${send} data-sticker-id=${content.id}>
|
||||
<img
|
||||
data-src=${makeThumbnailURL(content.id)}
|
||||
alt=${content.body}
|
||||
title=${content.body}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
render(html`<${App} />`, document.body);
|
26
stickerpicker/src/search-box.js
Normal file
26
stickerpicker/src/search-box.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import {html} from "../lib/htm/preact.js"
|
||||
|
||||
export const SearchBox = ({onInput, onKeyUp, value, placeholder = 'Find stickers'}) => {
|
||||
const component = html`
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder=${placeholder} value=${value} onInput=${onInput} onKeyUp=${onKeyUp}/>
|
||||
<span class="icon icon-search"/>
|
||||
</div>
|
||||
`
|
||||
return component
|
||||
}
|
41
stickerpicker/src/spinner.js
Normal file
41
stickerpicker/src/spinner.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { html } from "../lib/htm/preact.js"
|
||||
|
||||
export 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
|
||||
}
|
77
stickerpicker/src/widget-api.js
Normal file
77
stickerpicker/src/widget-api.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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
|
||||
}
|
||||
|
||||
let response
|
||||
|
||||
if (request.action === "visibility") {
|
||||
response = {}
|
||||
} else if (request.action === "capabilities") {
|
||||
response = { capabilities: ["m.sticker"] }
|
||||
} else {
|
||||
response = { error: { message: "Action not supported" } }
|
||||
}
|
||||
|
||||
window.parent.postMessage({ ...request, response }, event.origin)
|
||||
}
|
||||
|
||||
export function sendSticker(content) {
|
||||
const data = {
|
||||
content: { ...content },
|
||||
// `name` is for Element Web (and also the spec)
|
||||
// Element Android uses content -> body as the name
|
||||
name: content.body,
|
||||
}
|
||||
// Custom field that stores the ID even for non-telegram stickers
|
||||
delete data.content.id
|
||||
|
||||
// This is for Element iOS
|
||||
const widgetData = {
|
||||
...data,
|
||||
description: content.body,
|
||||
file: content.filename ?? `${content.id}.png`,
|
||||
}
|
||||
delete widgetData.content.filename
|
||||
// Element iOS explodes if there are extra fields present
|
||||
delete widgetData.content["net.maunium.telegram.sticker"]
|
||||
|
||||
window.parent.postMessage({
|
||||
api: "fromWidget",
|
||||
action: "m.sticker",
|
||||
requestId: `sticker-${Date.now()}`,
|
||||
widgetId,
|
||||
data,
|
||||
widgetData,
|
||||
}, "*")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue