OctoSpaccHub/public/MatrixStickerHelper/index.html
2024-01-03 00:31:15 +01:00

744 lines
24 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!--
<script src="../../../SpaccDotWeb/SpaccDotWeb.js" module="SpaccDotWeb"></script>
<script src="//SpaccInc.gitlab.io/SpaccDotWeb/SpaccDotWeb.js" module="SpaccDotWeb"></script>
-->
<script src="../../../SpaccDotWeb/SpaccDotWeb.Alt.js" module="SpaccDotWeb"></script>
<script src="//SpaccInc.gitlab.io/SpaccDotWeb/SpaccDotWeb.Alt.js" module="SpaccDotWeb"></script>
<script src="https://googlechrome.github.io/dialog-polyfill/dist/dialog-polyfill.js"></script>
<link rel="stylesheet" href="./paper.min.css"/>
<div id="Main" hidden="true">
<div id="LayoutAccountSelect"></div>
<div id="LayoutAccountLogin">
<label>
Homeserver
<input type="text"/>
</label>
<label>
Username
<input type="text"/>
</label>
<label>
Password
<input type="password"/>
</label>
<label>
Add account via session token instead
<input type="checkbox"/>
</label>
<label>
<input type="button" value="Add"/>
</label>
</div>
<div id="LayoutCollectionActions">
<button name="back">🔙️ Go Back</button>
<button name="commit" disabled="true">📝️ Commit Changes</button>
</div>
<div id="LayoutPacksList"></div>
<div id="LayoutPackGrid"></div>
<div id="LayoutInfo"></div>
<details class="col border margin" id="LayoutCollectionOptions">
<summary>
<p>Options</p>
</summary>
<p>
Reinitializing sticker data for your account will simply override
<!-- the <code>stickerpicker</code> subfield in --> its <code>m.widgets</code> field.
</p>
<button name="reinit">💡️ Reinitialize sticker data as new</button>
<details class="col border margin">
<summary>
<p>Advanced</p>
</summary>
<label>
Sticker selector app URL
<input name="pickerUrl" type="text"/>
</label>
</details>
</details>
</div>
<hr/>
<p>
🃏️ [Matrix] Sticker Helper <a name="version" href="javascript:;">WIP</a>,
created with ☕️ by <a href="https://hub.octt.eu.org">OctoSpacc</a>.
<br/>
Made possible by <a href="https://github.com/maunium/stickerpicker">Maunium sticker picker</a>,
brought to paper thanks to <a href="https://www.getpapercss.com">PaperCSS</a>.
</p>
<details class="col border margin">
<summary>
<p>Help</p>
</summary>
<p>
There is no one around to help.
</p>
</details>
<script module="Meta">({
Name: "🃏️ [Matrix] Sticker Helper",
})</script>
<!-- <script module="Main"> -->
<script module="Main" type="module">
//import { html, render } from 'https://esm.sh/htm/preact/standalone';
//import axios from 'https://cdn.skypack.dev/axios';
const Spacc = SpaccDotWeb.AppInit();
const State = {};
const Defaults = {
stickerSelectorUrl: "https://maunium.net/stickers-demo/",
appIdentity: "org.eu.octt.MatrixStickerHelper",
appInterface: "v1",
Strings: {
mConfirmCommit: "Confirm committing account data?",
mMustInit: "Your account must first be initialized to handle stickers.",
mNotManaged: `
Your account is set-up with a sticker picker, but it's not marked as being managed by this app.
This could mean that you are currently using an incompatible sticker picker.
You can try to continue anyway if you think it should work, otherwise you should reinitialize sticker data.
`,
}
};
let Config = localStorage.getItem('SpaccInc-Matrix-Config');
if (Config) Config = JSON.parse(Config);
else Config = {
accounts: [],
};
Config.Save = () => localStorage.setItem('SpaccInc-Matrix-Config', JSON.stringify(Config));
/*
function ActionForm (props) {
let formElems = [];
for (const action of props.actionEntries) {
action.options ||= [];
formElems.push(html`
<button
style=${{ textAlign: 'left', width: `calc(100% - ${action.options.length * 3}em)`, display: 'inline-block' }}
dataIndex=${action.dataIndex}
onclick=${action.onclick}
disabled=${action.disabled}
name=${action.name}
>
${action.label}
</button>
`)
for (const option of action.options) {
formElems.push(html`
<button
style=${{ width: '3em', display: 'inline-block' }}
dataIndex=${option.dataIndex}
onclick=${option.onclick}
>
${option.label}
</button>
`)
}
}
return html`<div class="ActionForm">
${formElems}
</div>`
}
function TableForm (props) {
let tableRows = [];
for (const entry of props.formEntries) {
tableRows.push(html`<tr>
<td>
${entry.type !== 'button' ? entry.label : ''}
</td>
<td style=${{ width: '100%' }}>
<input
style=${{ width: '100%' }}
name=${entry.name || entry.label.toLowerCase()}
type=${entry.type}
placeholder=${entry.placeholder || entry.value}
value=${entry.type !== 'button' ? entry.value : (entry.value || entry.label)}
onclick=${entry.onclick || entry.onInteract}
onselect=${entry.onInteract}
onchange=${entry.onInteract}
oninput=${entry.onInteract}
onpaste=${entry.onInteract}
/>
</td>
</tr>`)
}
}
function ClickImg (props) {
return html`<div class="ClickImg">
<img
src=${props.src}
onclick=${() => {
for (const openImg of document.querySelectorAll('.ClickImg[data-opened="1"]')) {
if (openImg !== this.base ) {
openImg.dataset.opened = '0';
}
}
const isOpened = this.base.dataset.opened;
if (isOpened === '1') {
this.base.dataset.opened = '0';
} else {
this.base.dataset.opened = '1';
}
}}
/>
<${SecureButton}
name="delete"
modalText='❌️ Confirm delete?'
onclick=${() => {
alert(1)
}}
>
❌️
</${SecureButton}>
</div>`
}
async function TryMatrixLoginAndSaveAndUse (loginData) {
try {
const response = await axios.post(`${loginData.homeserver}/_matrix/client/v3/login`, {
type: "m.login.password",
identifier: {
type: "m.id.user",
user: loginData.username,
},
password: loginData.password,
});
State.account = {
homeserver: loginData.homeserver,
username: loginData.username,
token: response.data.access_token,
};
Config.accounts.push(State.account);
Config.Save();
Render(CollectionScreen);
} catch(err) {
alert(`Error trying to get a session token: ${err.response.request.response}`);
document.querySelector('#AccountLoginForm input[name="loginSave"]').disabled = false;
}
}
async function DataWidgetSaveRequest (configUrl) {
document.querySelector('input[name="reinit"]').disabled = true;
document.querySelector('button[name="commit"]').disabled = true;
State.dataWidgets.stickerpicker = {
content: {
type: "m.stickerpicker",
url: `${document.querySelector('input[name="sticker selector url"]').value}?&config=${configUrl || ''}&theme=$theme`,
name: "Stickerpicker",
creatorUserId: `${GetMatrixUserTag(State.account)}`,
managedBy: [
`${Defaults.appIdentity}/${Defaults.appInterface}`,
],
},
sender: `${GetMatrixUserTag(State.account)}`,
state_key: "stickerpicker",
type: "m.widget",
id: "stickerpicker",
};
try {
const response = await axios.put(`${State.account.homeserver}/_matrix/client/v3/user/${GetMatrixUserTag(State.account)}/account_data/m.widgets`, State.dataWidgets, {
headers: { "Authorization": `Bearer ${State.account.token}` },
});
console.log(response.data);
Render(CollectionScreen);
} catch(err) {
alert(`Error updating account widget data: ${err.response.request.response}`);
document.querySelector('button[name="commit"]').disabled = false;
}
document.querySelector('input[name="reinit"]').disabled = false;
}
function AccountLogin (props) {
return html`<Fragment>
<p>
Please login with your [Matrix] account.
(Your login details are processed locally and only sent to the homeserver you specified.)
</p>
<${TableForm} id="AccountLoginForm" formEntries=${[
{ label: 'Homeserver', type: 'text', value: 'https://matrix.org' },
{ label: 'Username', type: 'text', placeholder: 'AzureDiamond' },
{ label: 'Password', type: 'password', placeholder: '*******' },
{ label: 'Import via session token', type: 'checkbox' },
{ label: '🆕️ Login and Save', type: 'button', onclick: () => {
this.base.querySelector('input[name="loginSave"]').disabled = true;
const password = this.base.querySelector('input[name="password"]').value;
let loginData = {
homeserver: this.base.querySelector('input[name="homeserver"]').value,
username: this.base.querySelector('input[name="username"]').value,
};
if (this.base.querySelector('input[name="import via session token"]').checked) {
loginData.token = password;
Config.accounts.push(loginData);
Config.Save();
Render(CollectionScreen);
} else {
loginData.password = password;
TryMatrixLoginAndSaveAndUse(loginData);
}
}, name: 'loginSave' },
...(Config.accounts.length > 0
? [{ label: '✖️ Cancel', type: 'button', onclick: () => Render() }]
: [])
]}/>
</Fragment>`
}
function CollectionList (props) {
return html`<${CollectionViewMenu} optionsOpen=${false}>
<div id="CollectionList"><div>
<button style=${{ verticalAlign: 'top', height: '64px' }}>
Create new pack
</button>
${packElems}
</div></div>
<${CollectionGrid}/>
</${CollectionViewMenu}>`
}
function CollectionGrid (props) {
return html`<div id="CollectionGrid">
<${ClickImg} src="https://http.cat/100"/>
<${ClickImg} src="https://http.cat/101"/>
<${ClickImg} src="https://http.cat/102"/>
<${ClickImg} src="https://http.cat/103"/>
<${ClickImg} src="https://http.cat/200"/>
<${ClickImg} src="https://http.cat/201"/>
<${ClickImg} src="https://http.cat/202"/>
<${ClickImg} src="https://http.cat/203"/>
<${ClickImg} src="https://http.cat/204"/>
<button
style=${{ verticalAlign: 'top', width: '128px', height: '128px' }}
onclick=${() => this.base.querySelector('button > input[type="file"]').click()}
>
Upload new
<br/><br/>
<small>(Only image files supported for now)</small>
<input
style=${{ display: 'none' }}
type="file"
accept="image/jpeg,image/gif,image/png,image/webp"
multiple
onchange=${(data) => {
console.log(data.target.files);
document.querySelector('button[name="commit"]').disabled = false;
}}
/>
</button>
</div>`
}
function CollectionViewMenu (props) {
return html`<Fragment class="CollectionViewMenu">
<${ActionForm} actionEntries=${[
{ label: '🔙️ Go Back', onclick: () => Render() },
{ name: 'commit', label: '📝️ Commit Changes', disabled: true, onclick: () => DataWidgetSaveRequest(packsUrl) },
]}/>
${props.children}
<details open=${props.optionsOpen}>
<summary>
<h3 style=${{ display: 'inline' }}>
Options
</h3>
</summary>
<p>
Reinitializing sticker data for your account will simply override
the <code>stickerpicker</code> subfield in its <code>m.widgets</code> field.
</p>
<${TableForm} formEntries=${[
{ name: 'reinit', label: '💡️ Reinitialize sticker data as new', type: 'button', onclick: () => DataWidgetSaveRequest(this.base.querySelector('input[name="import json from url"]').value) },
{ label: 'or', type: 'hidden' },
{ label: 'Import JSON from URL', placeholder: 'https://example.com', type: 'text', onInteract: () => {
const thisElem = this.base.querySelector('input[name="import json from url"]');
const button = this.base.querySelector('input[name="reinit"]');
if (thisElem.value === '') {
button.value = '💡️ Reinitialize sticker data as new';
} else {
button.value = '💡️ Reinitialize sticker packs from URL';
}
} },
]}/>
<details open=${false}>
<summary>
<h4 style=${{ display: 'inline' }}>
Advanced
</h4>
</summary>
<${TableForm} formEntries=${[
{ label: 'Sticker selector URL', value: Defaults.stickerSelectorUrl, type: 'text' },
]}/>
</details>
</details>
</Fragment>`
}
*/
const $ = (query, ...params) => (query
? document.querySelector(Array.isArray(query)
? (params.length > 0
? params.map((a, i) => `${query[i]}${a}`).join('')
: query.join(''))
: query)
: document);
const GetMatrixUserTag = (account) => `@${account.username}:${account.homeserver.split('://')[1]}`;
const GetMatrixMediaUrl = (mxcId, homeserverUrl) => (mxcId ? `${homeserverUrl || `https://${mxcId.split('mxc://')[1].split('/')[0]}`}/_matrix/media/r0/thumbnail/${mxcId.split('mxc://')[1]}` : undefined);
function ResetLayouts () {
$`#Main`.hidden = false;
for (const id of ['LayoutInfo', 'LayoutAccountSelect', 'LayoutPacksList', 'LayoutPackGrid']) {
$`#${id}`.innerHTML = '';
}
for (const id of ['LayoutAccountLogin', 'LayoutCollectionActions', 'LayoutCollectionOptions']) {
$`#${id}`.hidden = true;
}
for (const id of ['LayoutCollectionOptions']) {
$`#${id}`.open = false;
}
}
function RegisterLayouts () {
$`#LayoutCollectionActions button[name="back"]`.onclick = () => DisplayAccountSelectLogin();
$`#LayoutCollectionOptions input[name="pickerUrl"]`.value = Defaults.stickerSelectorUrl;
}
async function RequestAccountWidgetsData (account, postData) {
const request = await fetch(`${account.homeserver}/_matrix/client/v3/user/${GetMatrixUserTag(account)}/account_data/m.widgets`, {
method: (postData ? "PUT" : "GET"),
headers: { Authorization: `Bearer ${account.token}` },
body: JSON.stringify(postData),
});
const result = await request.json();
if (request.status === 200 || result.errcode === 'M_NOT_FOUND') {
return result;
} else {
Spacc.ShowModal(`Error: ${JSON.stringify(result)}`);
}
}
async function RequestUploadFile (account, fileData, fileMime) {
const request = await fetch(`${account.homeserver}/_matrix/media/v3/upload`, {
method: "POST",
headers: { Authorization: `Bearer ${account.token}`, ...(fileMime ? { "Content-Type": fileMime } : {}) },
body: fileData,
});
const result = await request.json();
if (request.status === 200) {
return result;
} else {
Spacc.ShowModal(`Error: ${JSON.stringify(result)}`);
}
}
async function PreparePacksEditor (account) {
const userTag = GetMatrixUserTag(account);
let widgetsData = {};
ResetLayouts();
State.packsCount = 0;
$`#LayoutCollectionActions`.hidden = false;
$`#LayoutCollectionOptions`.hidden = false;
$`#LayoutCollectionOptions button[name="reinit"]`.onclick = () => Spacc.ShowModal({ label: Defaults.Strings.mConfirmCommit, action: async () => {
widgetsData = {
stickerpicker: {
content: {
type: "m.stickerpicker",
url: `${$`#LayoutCollectionOptions input[name="pickerUrl"]`.value}?&config=&theme=$theme`,
name: "Stickerpicker",
creatorUserId: userTag,
managedBy: [
`${Defaults.appIdentity}`,
`${Defaults.appIdentity}/${Defaults.appInterface}`,
],
},
sender: userTag,
state_key: "stickerpicker",
type: "m.widget",
id: "stickerpicker",
},
};
await RequestAccountWidgetsData(account, widgetsData);
DisplayPacksEditor(account, widgetsData);
} });
$`#LayoutInfo`.innerHTML = `<p>
Fetching account data...
</p>`;
widgetsData = await RequestAccountWidgetsData(account);
if (widgetsData) {
const isManaged = widgetsData.stickerpicker?.content?.managedBy?.includes(`${Defaults.appIdentity}/${Defaults.appInterface}`);
const packsUrl = (new URLSearchParams(widgetsData.stickerpicker?.content?.url?.split('?')[1])).get('config');
if (!isManaged || !widgetsData.stickerpicker) {
$`#LayoutCollectionOptions`.open = true;
if (!widgetsData.stickerpicker) {
$`#LayoutInfo`.innerHTML = `<p>${Defaults.Strings.mMustInit}</p>`;
} else
if (!isManaged) {
$`#LayoutInfo`.innerHTML = `
<p>${Defaults.Strings.mNotManaged}</p>
<button name="continue">⏭️ Continue</button>
`;
$`#LayoutInfo > button[name="continue"]`.onclick = () => {
$`#LayoutCollectionOptions`.open = false;
DisplayPacksEditor(account, widgetsData, packsUrl);
};
}
} else {
DisplayPacksEditor(account, widgetsData, packsUrl);
}
}
}
async function DisplayPacksEditor (account, widgetsData, packsUrl) {
let packsData = { homeserver_url: account.homeserver, packs: [] };
let editedPacks = {};
ResetLayouts();
$`#LayoutCollectionActions`.hidden = false;
$`#LayoutCollectionOptions`.hidden = false;
$`#LayoutCollectionActions button[name="commit"]`.onclick = () => Spacc.ShowModal({
label: Defaults.Strings.mConfirmCommit,
action: () => CommitNewAccountData(account, widgetsData, packsData, editedPacks),
});
if (packsUrl) {
try {
const request = await fetch(packsUrl);
if (request.status === 200) {
packsData = await request.json();
}
} catch(err) {
SpaccDotWeb.ShowModal(`${err} ${packsUrl}`);
}
}
const addButton = $().createElement('button');
addButton.name = 'add';
addButton.innerHTML = ' Create New Pack';
addButton.onclick = (event) => Spacc.ShowModal({ label: 'Optionally include the URL of a sticker pack in JSON format to import it. Otherwise, leave the field empty to create a brand-new pack.', extraHTML: `
<input name="packUrl" type="text"/>
`, action: async (event, modalButton) => {
// TODO error handling?
let packData = { stickers: [] };
const packUrl = modalButton.parentElement.querySelector('input[name="packUrl"]').value;
if (packUrl) {
const request = await fetch(packUrl);
packData = await request.json();
}
for (const elem of event.srcElement.parentElement.querySelectorAll('button')) {
elem.disabled = false;
}
const packButton = MakeStickerPackButton(State.packsCount, account, packsData, packData);
$`#LayoutPacksList`.insertBefore(packButton, $`#LayoutPacksList > button[name="add"]`.nextElementSibling);
$`#LayoutCollectionActions button[name="commit"]`.disabled = false;
packButton.click();
State.packsCount++;
} });
$`#LayoutPacksList`.appendChild(addButton);
for (const pack of packsData?.packs) {
const request = await fetch(pack);
const packData = await request.json();
$`#LayoutPacksList`.appendChild(MakeStickerPackButton(State.packsCount, account, packsData, packData));
State.packsCount++;
}
}
function MakeStickerPackButton (index, account, packsData, packData) {
const packButton = $().createElement('button');
packButton.dataset.index = index;
packButton.innerHTML = `<img src="${GetMatrixMediaUrl(packData.stickers[0]?.info?.thumbnail_url, packsData?.homeserver_url) || ''}?&height=64&width=64&method=scale"/>`;
packButton.onclick = (event) => {
const thisElem = (event.srcElement.tagName.toLowerCase() === 'button' ? event.srcElement : event.srcElement.parentElement);
for (const elem of thisElem.parentElement.querySelectorAll('button')) {
elem.disabled = false;
}
thisElem.disabled = true;
$`#LayoutPackGrid`.innerHTML = '';
for (const sticker of (packData.stickers || [])) {
const stickerElem = $().createElement('button');
stickerElem.innerHTML = `<img src="${GetMatrixMediaUrl(sticker.info.thumbnail_url, packsData?.homeserver_url) || ''}?&height=128&width=128&method=scale"/>`;
$`#LayoutPackGrid`.appendChild(stickerElem);
}
const addButton = $().createElement('button');
addButton.innerHTML = `
Upload New Sticker(s)
<input type=file hidden="true" multiple="true" accept="image/jpeg,image/gif,image/png,image/webp"/>
`;
addButton.querySelector('input[type="file"]').onchange = async (event) => {
let newPackStickers = [];
for (const file of event.target.files) {
const result = await RequestUploadFile(account, file);
if (result) {
newPackStickers.push({
id: result.content_uri,
url: result.content_uri,
msgtype: "m.sticker",
body: file.name,
info: {
// ... TODO read real image dimensions
w: 256,
h: 256,
size: file.size,
mimetype: file.type,
thumbnail_url: result.content_uri,
thumbnail_info: {
// ... TODO read real image dimensions
w: 256,
h: 256,
size: file.size,
mimetype: file.type,
},
},
});
// ... we must use the result.content_uri to add the new sticker to the screen, add it somewhere keeping the current pack state, and the packs state, ready for committing
} else {
const answer = Spacc.ShowModal({
label: 'File upload failed. What to do?',
action: () => 'continue',
actionCancel: () => 'cancel',
labelSecondary: '🔄️ Retry', actionSecondary: () => 'retry',
});
if (answer === 'cancel') {
newPackStickers = [];
break;
} else if (answer === 'retry') {
// ... find out how to handle this
} else if (answer === 'continue') {
continue;
}
}
}
// ... we must handle new stickers to add them to the initial object
packData.stickers = [...packData.stickers, newPackStickers];
};
addButton.onclick = (event) => event.srcElement.querySelector('input[type="file"]')?.click();
$`#LayoutPackGrid`.appendChild(addButton);
};
return packButton;
}
async function CommitNewAccountData (account, widgetsData, packsData, editedPacks) {
for (const packIndex in editedPacks) {
const pack = editedPacks[pack];
const packUrlNew = GetMatrixMediaUrl(await RequestUploadFile(account, JSON.stringify(pack.data), 'application/json').content_uri);
packsData.packs.remove(pack.url);
packsData.packs.push(packUrlNew);
}
const packsUrlNew = GetMatrixMediaUrl(await RequestUploadFile(account, JSON.stringify(packsData), 'application/json').content_uri);
widgetsData.stickerpicker.content.url = `${$`#LayoutCollectionOptions input[name="pickerUrl"]`.value}?&config=${packsUrlNew}&theme=$theme`;
if (await RequestAccountWidgetsData(account, widgetsData)) {
$`#LayoutCollectionActions button[name="commit"]`.disabled = true;
}
}
function DisplayAccountSelect () {
ResetLayouts();
for (const account of Config.accounts) {
const accountButton = $().createElement('button');
accountButton.style.width = 'calc(100% - 3em)';
accountButton.innerHTML += `🐱️ ${GetMatrixUserTag(account)}`;
accountButton.onclick = () => PreparePacksEditor(account);
$`#LayoutAccountSelect`.appendChild(accountButton);
const deleteButton = $().createElement('button');
deleteButton.innerHTML += `❌️`;
deleteButton.onclick = () => Spacc.ShowModal({ label: '❌️ Confirm remove account?', action: () => {
Config.accounts.pop(account);
Config.Save();
DisplayAccountSelectLogin();
} });
$`#LayoutAccountSelect`.appendChild(deleteButton);
}
const addButton = $().createElement('button');
addButton.innerHTML += '🆕️ Add new account';
addButton.onclick = () => DisplayAccountLogin();
$`#LayoutAccountSelect`.appendChild(addButton);
}
function DisplayAccountLogin() {
ResetLayouts();
$`#LayoutAccountLogin`.hidden = false;
}
function DisplayAccountSelectLogin () {
(Config.accounts.length > 0 ? DisplayAccountSelect : DisplayAccountLogin)();
}
RegisterLayouts();
DisplayAccountSelectLogin();
</script>
<style>
:root {
--margin: 8px;
}
* {
box-sizing: border-box;
}
body, .ActionForm, .TableForm {
margin: var(--margin);
overflow-x: hidden;
}
details > summary > * {
display: inline-block;
}
/*
#CollectionList {
max-width: 100%;
overflow: auto;
}
#CollectionList > div {
width: max-content;
}
#CollectionList > div > * {
width: 64px;
margin: var(--margin);
}
.ActionButton,
.ActionForm > *,
.TableForm td > * {
display: block;
height: 2em;
width: 100%;
}
.ActionForm > * > * {
height: 2em;
}
#CollectionGrid > *,
.ClickImg {
display: inline-block;
margin: 8px;
}
.ClickImg,
.ClickImg img {
width: 128px;
}
.ClickImg[data-opened="1"], .ClickImg[data-opened="1"] > img {
width: calc(100% - var(--margin));
}
*/
</style>