OctoSpaccHub/public/MatrixStickerHelper/index.html

567 lines
19 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"/>
<title>🃏️ [Matrix] Sticker Helper</title>
<!--
<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>
<link rel="stylesheet" href="./paper.min.css"/>
<!--
<script module="Meta">({
Name: "🃏️ [Matrix] Sticker Helper",
})</script>
-->
<!--
TODO:
* more error handling
* add a way to delete a sticker or a pack
* allow reordering stickers and packs
-->
<div id="Main" hidden="true">
<div id="LayoutAccountSelect"></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 class="margin-large"/>
<p>
🃏️ [Matrix] Sticker Helper <a name="version" href="javascript:;">Early Version</a>,
created with ☕️ by <a href="https://hub.octt.eu.org">OctoSpacc</a>.
<br/>
Made possible by <a href="https://github.com/maunium/stickerpicker" target="_blank">Maunium sticker picker</a>,
brought to paper thanks to <a href="https://www.getpapercss.com" target="_blank">PaperCSS</a>.
</p>
<details class="col border margin">
<summary>
<p>Help</p>
</summary>
<p>
There is no one around to help... yet.
Maybe join my Matrix space if you need some: <a href="https://matrix.to/#/#Spacc:matrix.org">https://matrix.to/#/#Spacc:matrix.org</a>.
</p>
</details>
<script module="Main" type="module">
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.
<br/>
You can try to continue anyway if you think it should work, otherwise you should reinitialize sticker data.
`,
mCreatePackHint: `
<!-- Optionally include --> Include the URL of a sticker pack in JSON format to import it.
<!-- Otherwise, leave the field empty --> The option to create a brand-new pack will soon be available.
`,
mLoginHint: `
Please login with your [Matrix] account.
<br/>
(Your login details are processed locally and only sent to the homeserver you specified.)
`,
}
};
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));
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, props) => (mxcId ? `${props?.homeserver || `https://${mxcId.split('mxc://')[1].split('/')[0]}`}/_matrix/media/r0/${props?.type || 'download'}/${mxcId.split('mxc://')[1]}` : undefined);
function ResetLayouts () {
$`#Main`.hidden = false;
for (const id of ['LayoutInfo', 'LayoutAccountSelect', 'LayoutPacksList', 'LayoutPackGrid']) {
$`#${id}`.innerHTML = '';
}
for (const id of ['LayoutCollectionActions', 'LayoutCollectionOptions']) {
$`#${id}`.hidden = true;
}
for (const id of ['LayoutCollectionOptions']) {
$`#${id}`.open = false;
}
}
function InitializeState () {
$`#LayoutCollectionActions button[name="back"]`.onclick = () => DisplayAccountSelect();
$`#LayoutCollectionOptions input[name="pickerUrl"]`.value = Defaults.stickerSelectorUrl;
}
async function RequestAccountWidgetsData (postData) {
const request = await fetch(`${State.account.homeserver}/_matrix/client/v3/user/${GetMatrixUserTag(State.account)}/account_data/m.widgets`, {
method: (postData ? "PUT" : "GET"),
headers: { Authorization: `Bearer ${State.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 (fileData, fileMime) {
const request = await fetch(`${State.account.homeserver}/_matrix/media/v3/upload`, {
method: "POST",
headers: { Authorization: `Bearer ${State.account.token}`, ...(fileMime && { "Content-Type": fileMime }) },
body: fileData,
});
const result = await request.json();
if (request.status === 200) {
return await result;
} else {
Spacc.ShowModal(`Error: ${JSON.stringify(result)}`);
}
}
async function PreparePacksEditor (account) {
if (account) {
State.account = account;
}
ResetLayouts();
State.packsData = { homeserver_url: State.account.homeserver, packs: [] };
State.stickersData = [];
$`#LayoutCollectionActions`.hidden = false;
$`#LayoutCollectionOptions`.hidden = false;
$`#LayoutCollectionOptions button[name="reinit"]`.onclick = () => Spacc.ShowModal({
label: Defaults.Strings.mConfirmCommit,
action: () => ReinitStickersAccountData(),
});
$`#LayoutInfo`.innerHTML = `<p>
Fetching account data...
</p>`;
State.widgetsData = await RequestAccountWidgetsData();
if (State.widgetsData) {
const isManaged = State.widgetsData?.stickerpicker?.content?.managedBy?.includes(`${Defaults.appIdentity}/${Defaults.appInterface}`);
const packsUrl = (new URLSearchParams(State.widgetsData?.stickerpicker?.content?.url?.split('?')[1])).get('config');
if (!isManaged || !State.widgetsData?.stickerpicker) {
$`#LayoutCollectionOptions`.open = true;
if (!State.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(packsUrl);
};
}
} else {
DisplayPacksEditor(packsUrl);
}
}
}
async function DisplayPacksEditor (packsUrl) {
ResetLayouts();
$`#LayoutCollectionActions`.hidden = false;
$`#LayoutCollectionOptions`.hidden = false;
$`#LayoutCollectionActions button[name="commit"]`.onclick = () => Spacc.ShowModal({
label: Defaults.Strings.mConfirmCommit,
action: () => CommitNewAccountStickersAndData(),
});
if (packsUrl) {
try {
const request = await fetch(packsUrl);
if (request.status === 200) {
State.packsData = await request.json();
}
} catch(err) {
Spacc.ShowModal(`${err} ${packsUrl}`);
}
}
const addButton = $().createElement('button');
addButton.name = 'add';
addButton.innerHTML = ' Create/Import New Pack';
addButton.onclick = (event) => Spacc.ShowModal({ label: Defaults.Strings.mCreatePackHint, extraHTML: `
<input name="packUrl" type="text" style="width: 100%;"/>
`, action: (event, modalButton) => CreateNewPack(event, modalButton) });
$`#LayoutPacksList`.appendChild(addButton);
LoadStickerPacksList();
}
async function LoadStickerPacksList () {
for (const pack of State.packsData?.packs) {
try {
const request = await fetch(pack);
const packData = await request.json();
State.stickersData.push({ edited: false, data: packData });
AddNewPackButton(packData);
} catch(err) {
Spacc.ShowModal(`${err} ${pack}`);
}
}
}
async function CreateNewPack (event, modalButton) {
let packData = { stickers: [] };
// if the user specified an URL, try downloading data from there
const packUrl = modalButton.parentElement.querySelector('input[name="packUrl"]').value;
// TODO: warn if an existing pack is imported from that URL and let the user choose if to continue or cancel
//if (packUrl && IsAnyPackImportedFrom(packUrl) && await Spacc.ShowModal({ label: Defaults.Strings.mAlreadyImported, action: () => 'continue', actionCancel: () => 'cancel' }) === 'continue') {
// return;
//};
if (packUrl) {
try {
const request = await fetch(packUrl);
packData = await request.json();
$`#LayoutCollectionActions button[name="commit"]`.disabled = false;
} catch(err) {
Spacc.ShowModal(`${err} ${packUrl}`);
return;
}
}
packData[Defaults.appIdentity] = {
...(packUrl && { importedFrom: packUrl }),
};
State.packsData.packs.push(null);
State.stickersData.push({ edited: true, data: packData });
AddNewPackButton(packData, event, modalButton);
}
function IsAnyPackImportedFrom (packUrl) {
for (const pack of State.stickersData) {
if (pack.data[Defaults.appIdentity].importedFrom === packUrl) {
return true;
}
}
return false;
}
function AddNewPackButton (packData, event, modalButton) {
for (const elem of $`#LayoutPacksList`.querySelectorAll('button')) {
elem.disabled = false;
}
const packButton = MakeStickerPackButton(packData);
$`#LayoutPacksList`.insertBefore(packButton, $`#LayoutPacksList > button[name="add"]`.nextElementSibling);
packButton.click();
}
function MakeStickerPackButton (packData) {
const packButton = $().createElement('button');
packButton.innerHTML = `<img src="${GetMatrixMediaUrl(packData.stickers[0]?.info?.thumbnail_url, { homeserver: State.packsData?.homeserver_url, type: 'thumbnail' }) || ''}?&height=64&width=64&method=scale"/>`;
packButton.onclick = (event) => ShowStickerPack(event, packData);
return packButton;
}
function ShowStickerPack (event, packData) {
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, { homeserver: State.packsData?.homeserver_url, type: 'thumbnail' }) || ''}?&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"/>
`;
/* TODO
addButton.querySelector('input[type="file"]').onchange = async (event) => {
let newPackStickers = [];
for (const file of event.target.files) {
const result = await RequestUploadFile(State.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 = await 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];
if (newPackStickers.length > 0) {
$`#LayoutCollectionActions button[name="commit"]`.disabled = false;
}
};
*/
addButton.onclick = (event) => event.srcElement.querySelector('input[type="file"]')?.click();
// TODO: $`#LayoutPackGrid`.appendChild(addButton);
}
async function ReinitStickersAccountData () {
const userTag = GetMatrixUserTag(State.account);
State.packsData = { homeserver_url: State.account.homeserver, packs: [] };
State.stickersData = [];
State.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(State.widgetsData);
PreparePacksEditor();
}
async function CommitNewAccountStickersAndData () {
$`#LayoutCollectionActions button[name="commit"]`.disabled = true;
// upload new metadata for sticker packs which have been edited
for (const packIndex in State.stickersData) {
const pack = State.stickersData[packIndex];
if (pack.edited && pack.data.stickers.length > 0) {
const uploadResult = await RequestUploadFile(JSON.stringify(pack.data), 'application/json');
State.packsData.packs[packIndex] = GetMatrixMediaUrl(uploadResult.content_uri);
}
}
// remove empty sticker packs before committing, they break the picker
for (const packIndex in State.stickersData) {
if (State.stickersData[packIndex].data.stickers.length === 0) {
State.stickersData[packIndex].data.stickers.splice(packIndex, 1);
State.packsData.packs.splice(packIndex, 1);
}
}
// finally upload new index file and update profile data
const uploadResult = await RequestUploadFile(JSON.stringify(State.packsData), 'application/json');
const packsUrlNew = GetMatrixMediaUrl(uploadResult.content_uri);
State.widgetsData.stickerpicker.content.url = `${$`#LayoutCollectionOptions input[name="pickerUrl"]`.value}?&config=${packsUrlNew}&theme=$theme`;
if (await RequestAccountWidgetsData(State.widgetsData)) {
PreparePacksEditor();
} else {
$`#LayoutCollectionActions button[name="commit"]`.disabled = false;
}
}
function DisplayAccountSelect () {
ResetLayouts();
for (const accountIndex in Config.accounts) {
const account = Config.accounts[accountIndex];
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.splice(accountIndex, 1);
Config.Save();
DisplayAccountSelect();
} });
$`#LayoutAccountSelect`.appendChild(deleteButton);
}
const addButton = $().createElement('button');
addButton.innerHTML += '🆕️ Add new account';
addButton.onclick = async () => {
const modal = await Spacc.ShowModal({ label: Defaults.Strings.mLoginHint, extraHTML: `
<label>
Homeserver
<input name="homeserver" type="url" placeholder="https://matrix.example.com" value="https://matrix.org"/>
</label>
<label>
Username
<input name="username" type="text" placeholder="AzureDiamond"/>
</label>
<label>
Password
<input name="password" type="password" placeholder="*******"/>
</label>
<label>
Import account via session token instead
<input name="useToken" type="checkbox"/>
</label>
`, action: async (event, modalButton) => {
const modal = modalButton.parentElement;
const homeserver = modal.querySelector('input[name="homeserver"]').value;
const username = modal.querySelector('input[name="username"]').value;
const password = modal.querySelector('input[name="password"]').value;
const loginData = {
homeserver: homeserver,
username: username,
};
if (modal.querySelector('input[name="useToken"]').checked) {
loginData.token = password;
} else {
try {
const response = await fetch(`${homeserver}/_matrix/client/v3/login`, {
method: "POST",
body: JSON.stringify({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: username,
},
password: password,
}),
});
const result = await response.json();
if (response.status === 200) {
loginData.token = result.access_token;
} else {
throw JSON.stringify(result);
}
} catch(err) {
Spacc.ShowModal(`Error trying to get a session token: ${err}`);
return;
}
}
Config.accounts.push(loginData);
Config.Save();
DisplayAccountSelect();
} });
const buttonConfirm = modal.querySelector('button[name="confirm"]');
buttonConfirm.disabled = true;
for (const requiredElem of modal.querySelectorAll('input[type="url"], input[type="text"], input[type="password"]')) {
for (const event of ['change', 'input', 'paste']) {
requiredElem[`on${event}`] = () => {
let allFilled = true;
for (const requiredElem of modal.querySelectorAll('input[type="url"], input[type="text"], input[type="password"]')) {
if (!requiredElem.value) {
allFilled = false;
}
}
buttonConfirm.disabled = !allFilled;
};
}
}
};
$`#LayoutAccountSelect`.appendChild(addButton);
}
function Main () {
InitializeState();
DisplayAccountSelect();
}
Main();
</script>
<style>
:root {
--margin: 8px;
}
* {
box-sizing: border-box;
}
body, .ActionForm, .TableForm {
margin: var(--margin);
overflow-x: hidden;
}
details > summary > * {
display: inline-block;
}
#LayoutPacksList img {
max-width: 64px;
}
#LayoutPackGrid img {
max-width: 128px;
}
</style>