pages: work in progress [might break]

This commit is contained in:
Leon van Kammen 2024-07-11 11:15:19 +02:00
parent 1c2016c37e
commit c83ef1cc54
8 changed files with 520 additions and 0 deletions

2
example/webxdc/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.xdc

24
example/webxdc/LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

74
example/webxdc/README.md Normal file
View File

@ -0,0 +1,74 @@
# Hello
Sample project with a simple implementation of the webxdc read and write APIs.
## Demo (no server or installation required)
1. Open `index.html` in your web browser
2. Click 'Add Peer' to open as many peers as you like
3. Type a message and press 'Send' to see the update in each peer. (For Safari you might need to check the setting under Develop > Disable Local File Restrictions.)
## Developing webxdc apps
Simply copy `webxdc.js` from this repo beside your `index.html` and you are ready to go
to **develop and test your app in most browsers.**
Bundle your app using `./create-xdc.sh your-app-name`
and **send it to your friends** 🙂
<img width="311" alt="Screenshot 2023-02-10 at 20 40 22" src="https://user-images.githubusercontent.com/9800740/218183018-59d0aa06-da92-445b-9cad-51e416594d31.png">
## Further Hints and Troubleshooting
### Limitations
Due to the nature of most browsers and how they scope `localStorage`,
each emulated peer will get the same `localStorage`.
To really test the storage usage of your Webxdc,
bundle the app and test it in Delta Chat directly
where all peers get their own `localStorage`.
Alternatively, use the more advanced [webxdc-dev](https://github.com/webxdc/webxdc-dev) tool.
### Type-checking and completion
If you are using VSCode you can have autocompletion and type-checking even without using TypeScript by adding these two lines to your JavaScript source files:
```js
//@ts-check
/** @typedef {import('./webxdc').Webxdc} Webxdc */
```
Without VSCode you need to install TypeScript and then run the check manually.
```sh
npm -g typescript
tsc --noEmit --allowJs --lib es2016,dom webxdc.js # to check if types and simulator are in sync
tsc --noEmit --allowJs --lib es2016,dom your_js_file.js
```
### Developing in Safari
To use the devtool in safari you need to disable the local file restrictions
under `Develop` -> `Disable Local File Restrictions`.
After doing this you can use the dev tool simulator.
Make sure to reload (`Cmd + R`) all simulator tabs/windows to apply this setting.
Without this option `Add Peer` seems to work (it opens a new instance), but **the instances will not be able to communicate**.
### Developing on Android
- install Termux
- install Python and Git in Termux
- `git clone` the devtool repo or your fork of it
- use `python -m http.server` to serve it for development using nano/vim
- when you are done, use `./create-xdc.sh` for bundling
- copy the created `.xdc` file to a location from where you can access and send it via Delta Chat
- pro tip: you can create symbolic link to a folder in the external storage

27
example/webxdc/create-xdc.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/sh
case "$1" in
"-h" | "--help")
echo "usage: ${0##*/} [PACKAGE_NAME]"
exit
;;
"")
PACKAGE_NAME=${PWD##*/} # '##*/' removes everything before the last slash and the last slash
;;
*)
PACKAGE_NAME=${1%.xdc} # '%.xdc' removes the extension and allows PACKAGE_NAME to be given with or without extension
;;
esac
rm "$PACKAGE_NAME.xdc" 2> /dev/null
zip -9 --recurse-paths "$PACKAGE_NAME.xdc" --exclude LICENSE README.md webxdc.js "./*.sh" "./*.xdc" -- *
echo "success, archive contents:"
unzip -l "$PACKAGE_NAME.xdc"
# check package size
MAXSIZE=655360
size=$(wc -c < "$PACKAGE_NAME.xdc")
if [ "$size" -ge $MAXSIZE ]; then
echo "WARNING: package size exceeded the limit ($size > $MAXSIZE)"
fi

BIN
example/webxdc/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

67
example/webxdc/index.html Normal file
View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width"/>
<script src="webxdc.js"></script>
<style type="text/css">
body {
font-family: Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Hello</h1>
<form>
<input id="input" type="text" placeholder="Message" autofocus required />
<input type="submit" onclick="sendMsg(); return false;" value="Send" />
</form>
<p id="output"></p>
<p><em><small id="deviceName"></small></em></p>
<script>
var El = function (tag, text) {
var el = document.createElement(tag);
el.innerText = text || '';
return el;
};
// handle past and future state updates
window.webxdc.setUpdateListener(function (update) {
var output = document.getElementById('output');
// when appending content to an element with output.innerHTML +=
// that content is implicitly parsed, making it possible for messages
// to be interpreted as scripts. Creating elements directly,
// injecting content as plain text, and appending them to the DOM
// is a much safer practice.
[
El('strong', update.payload.name + ':'),
El('span', update.payload.msg),
El('br'),
].forEach(function (item) {
output.appendChild(item);
});
});
function sendMsg() {
msg = document.getElementById("input").value;
info = 'someone typed "' + msg + '"';
document.getElementById("input").value = '';
// send new updates
window.webxdc.sendUpdate({
payload: {
name: window.webxdc.selfName,
msg,
},
info,
}, info);
}
(function () {
window.deviceName.innerText = 'this is ' + window.webxdc.selfName;
})()
</script>
</body>
</html>

View File

@ -0,0 +1,2 @@
name = "Hello"
source_code_url = "https://github.com/webxdc/hello"

324
example/webxdc/webxdc.js Normal file
View File

@ -0,0 +1,324 @@
// This file originates from
// https://github.com/webxdc/hello/blob/master/webxdc.js
// It's a stub `webxdc.js` that adds a webxdc API stub for easy testing in
// browsers. In an actual webxdc environment (e.g. Delta Chat messenger) this
// file is not used and will automatically be replaced with a real one.
// See https://docs.webxdc.org/spec.html#webxdc-api
let ephemeralUpdateKey = "__xdcEphemeralUpdateKey__";
/**
* @typedef {import('./webxdc.d.ts').RealtimeListener} RT
* @type {RT}
*/
class RealtimeListener {
constructor() {
this.listener = null;
this.trashed = false;
}
is_trashed() {
return this.trashed;
}
receive(data) {
if (this.trashed) {
throw new Error("realtime listener is trashed and can no longer be used");
}
if (this.listener) {
this.listener(data);
}
}
setListener(listener) {
this.listener = listener;
}
send(data) {
if (!data instanceof Uint8Array) {
throw new Error("realtime listener data must be a Uint8Array");
}
window.localStorage.setItem(
ephemeralUpdateKey,
JSON.stringify([window.webxdc.selfAddr, Array.from(data), Date.now()]) // Date.now() is needed to trigger the event
);
}
leave() {
this.trashed = true;
}
}
// debug friend: document.writeln(JSON.stringify(value));
//@ts-check
/** @type {import('./webxdc').Webxdc<any>} */
window.webxdc = (() => {
var updateListener = (_) => {};
/**
* @type {RT | null}
*/
var realtimeListener = null;
var updatesKey = "__xdcUpdatesKey__";
window.addEventListener("storage", (event) => {
if (event.key == null) {
window.location.reload();
} else if (event.key === updatesKey) {
var updates = JSON.parse(event.newValue);
var update = updates[updates.length - 1];
update.max_serial = updates.length;
console.log("[Webxdc] " + JSON.stringify(update));
updateListener(update);
} else if (event.key === ephemeralUpdateKey) {
var [sender, update] = JSON.parse(event.newValue);
if (window.webxdc.selfAddr !== sender && realtimeListener && !realtimeListener.is_trashed()) {
realtimeListener.receive( Uint8Array.from(update));
}
}
});
function getUpdates() {
var updatesJSON = window.localStorage.getItem(updatesKey);
return updatesJSON ? JSON.parse(updatesJSON) : [];
}
var params = new URLSearchParams(window.location.hash.substr(1));
return {
selfAddr: params.get("addr") || "device0@local.host",
selfName: params.get("name") || "device0",
setUpdateListener: (cb, serial = 0) => {
var updates = getUpdates();
var maxSerial = updates.length;
updates.forEach((update) => {
if (update.serial > serial) {
update.max_serial = maxSerial;
cb(update);
}
});
updateListener = cb;
return Promise.resolve();
},
joinRealtimeChannel: (cb) => {
if (realtimeListener && realtimeListener.is_trashed()) {
return;
}
rt = new RealtimeListener();
// mimic connection establishment time
setTimeout(() => realtimeListener = rt, 500);
return rt;
},
getAllUpdates: () => {
console.log("[Webxdc] WARNING: getAllUpdates() is deprecated.");
return Promise.resolve([]);
},
sendUpdate: (update, description) => {
var updates = getUpdates();
var serial = updates.length + 1;
var _update = {
payload: update.payload,
summary: update.summary,
info: update.info,
document: update.document,
serial: serial,
};
updates.push(_update);
window.localStorage.setItem(updatesKey, JSON.stringify(updates));
_update.max_serial = serial;
console.log(
'[Webxdc] description="' + description + '", ' + JSON.stringify(_update)
);
updateListener(_update);
},
sendToChat: async (content) => {
if (!content.file && !content.text) {
alert("🚨 Error: either file or text need to be set. (or both)");
return Promise.reject(
"Error from sendToChat: either file or text need to be set"
);
}
/** @type {(file: Blob) => Promise<string>} */
const blob_to_base64 = (file) => {
const data_start = ";base64,";
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
/** @type {string} */
//@ts-ignore
let data = reader.result;
resolve(data.slice(data.indexOf(data_start) + data_start.length));
};
reader.onerror = () => reject(reader.error);
});
};
let base64Content;
if (content.file) {
if (!content.file.name) {
return Promise.reject("file name is missing");
}
if (
Object.keys(content.file).filter((key) =>
["blob", "base64", "plainText"].includes(key)
).length > 1
) {
return Promise.reject(
"you can only set one of `blob`, `base64` or `plainText`, not multiple ones"
);
}
// @ts-ignore - needed because typescript imagines that blob would not exist
if (content.file.blob instanceof Blob) {
// @ts-ignore - needed because typescript imagines that blob would not exist
base64Content = await blob_to_base64(content.file.blob);
// @ts-ignore - needed because typescript imagines that base64 would not exist
} else if (typeof content.file.base64 === "string") {
// @ts-ignore - needed because typescript imagines that base64 would not exist
base64Content = content.file.base64;
// @ts-ignore - needed because typescript imagines that plainText would not exist
} else if (typeof content.file.plainText === "string") {
base64Content = await blob_to_base64(
// @ts-ignore - needed because typescript imagines that plainText would not exist
new Blob([content.file.plainText])
);
} else {
return Promise.reject(
"data is not set or wrong format, set one of `blob`, `base64` or `plainText`, see webxdc documentation for sendToChat"
);
}
}
const msg = `The app would now close and the user would select a chat to send this message:\nText: ${
content.text ? `"${content.text}"` : "No Text"
}\nFile: ${
content.file
? `${content.file.name} - ${base64Content.length} bytes`
: "No File"
}`;
if (content.file) {
const confirmed = confirm(
msg + "\n\nDownload the file in the browser instead?"
);
if (confirmed) {
var element = document.createElement("a");
element.setAttribute(
"href",
"data:application/octet-stream;base64," + base64Content
);
element.setAttribute("download", content.file.name);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
} else {
alert(msg);
}
},
importFiles: (filters) => {
var element = document.createElement("input");
element.type = "file";
element.accept = [
...(filters.extensions || []),
...(filters.mimeTypes || []),
].join(",");
element.multiple = filters.multiple || false;
const promise = new Promise((resolve, _reject) => {
element.onchange = (_ev) => {
console.log("element.files", element.files);
const files = Array.from(element.files || []);
document.body.removeChild(element);
resolve(files);
};
});
element.style.display = "none";
document.body.appendChild(element);
element.click();
console.log(element);
return promise;
},
};
})();
window.addXdcPeer = () => {
var loc = window.location;
// get next peer ID
var params = new URLSearchParams(loc.hash.substr(1));
var peerId = Number(params.get("next_peer")) || 1;
// open a new window
var peerName = "device" + peerId;
var url =
loc.protocol +
"//" +
loc.host +
loc.pathname +
"#name=" +
peerName +
"&addr=" +
peerName +
"@local.host";
window.open(url);
// update next peer ID
params.set("next_peer", String(peerId + 1));
window.location.hash = "#" + params.toString();
};
window.clearXdcStorage = () => {
window.localStorage.clear();
window.location.reload();
};
window.alterXdcApp = () => {
var styleControlPanel =
"position: fixed; bottom:1em; left:1em; background-color: #000; opacity:0.8; padding:.5em; font-size:16px; font-family: sans-serif; color:#fff; z-index: 9999";
var styleMenuLink =
"color:#fff; text-decoration: none; vertical-align: middle";
var styleAppIcon =
"height: 1.5em; width: 1.5em; margin-right: 0.5em; border-radius:10%; vertical-align: middle";
var title = document.getElementsByTagName("title")[0];
if (typeof title == "undefined") {
title = document.createElement("title");
document.getElementsByTagName("head")[0].append(title);
}
title.innerText = window.webxdc.selfAddr;
if (window.webxdc.selfName === "device0") {
var root = document.createElement("section");
root.innerHTML =
'<div id="webxdc-panel" style="' +
styleControlPanel +
'">' +
'<header style="margin-bottom: 0.5em; font-size:12px;">webxdc dev tools</header>' +
'<a href="javascript:window.addXdcPeer();" style="' +
styleMenuLink +
'">Add Peer</a>' +
'<span style="' +
styleMenuLink +
'"> | </span>' +
'<a id="webxdc-panel-clear" href="javascript:window.clearXdcStorage();" style="' +
styleMenuLink +
'">Reset</a>' +
"<div>";
var controlPanel = root.firstChild;
function loadIcon(name) {
var tester = new Image();
tester.onload = () => {
root.innerHTML =
'<img src="' + name + '" style="' + styleAppIcon + '">';
controlPanel.insertBefore(root.firstChild, controlPanel.childNodes[1]);
var pageIcon = document.createElement("link");
pageIcon.rel = "icon";
pageIcon.href = name;
document.head.append(pageIcon);
};
tester.src = name;
}
loadIcon("icon.png");
loadIcon("icon.jpg");
document.getElementsByTagName("body")[0].append(controlPanel);
}
};
window.addEventListener("load", window.alterXdcApp);