diff avatar.js @ 0:2a8d4e8600d0

Initial commit.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 21 Dec 2018 21:34:17 +0100
parents
children d6df73b466f6
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/avatar.js
@@ -0,0 +1,184 @@
+'use strict';
+
+function initAvatar(connection) {
+    const DEFAULT_AVATAR = 'data:image/svg+xml,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 150 150"><rect width="150" height="150" fill="#888" stroke-width="1" stroke="#000"/><text x="75" y="100" text-anchor="middle" font-size="100">?</text></svg>';
+
+    const avatar_data = {};
+    const avatar_img = document.getElementById('avatar');
+    const avatar_size = document.getElementById('avatar-size');
+    const avatar_file = document.getElementById('avatar-file');
+    const avatar_upload = document.getElementById('avatar-upload');
+    const avatar_change = document.getElementById('avatar-change');
+
+    avatar_img.src = DEFAULT_AVATAR;
+    const iq = $iq({type: 'get'})
+        .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
+            .c('items', {node: 'urn:xmpp:avatar:metadata'});
+    connection.sendIQ(iq, onAvatarMetadata, onAvatarRetrievalError.bind(null, 'PubSub metadata query failed.'));
+
+    function onAvatarMetadata(result_iq)
+    {
+        const item = parseXPath(result_iq, './pubsub:pubsub/pubsub:items/pubsub:item');
+        if (item == null)
+            return onAvatarRetrievalError('no item found.');
+        const id = item.getAttributeNS(null, 'id');
+        const info = parseXPath(item, './avatar_metadata:metadata/avatar_metadata:info');
+        if (info == null)
+            return onAvatarRetrievalError('no info found, your avatar metadata node is broken.');
+        if (id != info.getAttributeNS(null, 'id'))
+            return onAvatarRetrievalError('invalid id in metadata.');
+
+        const parsed_info = {
+            id: id,
+            type: info.getAttributeNS(null, 'type'),
+            bytes: info.getAttributeNS(null, 'bytes'),
+            width: info.getAttributeNS(null, 'width'),
+            height: info.getAttributeNS(null, 'height'),
+        };
+        const iq = $iq({type: 'get'})
+            .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
+                .c('items', {node: 'urn:xmpp:avatar:data'})
+                    .c('item', {id: id});
+        connection.sendIQ(iq, onAvatarData.bind(null, parsed_info), onAvatarRetrievalError.bind(null, 'PubSub data query failed.'));
+    }
+
+    function onAvatarData(info, result_iq)
+    {
+        const item = parseXPath(result_iq, './pubsub:pubsub/pubsub:items/pubsub:item');
+        if (item == null)
+            return onAvatarRetrievalError('no item found.');
+        if (info.id != item.getAttributeNS(null, 'id'))
+            return onAvatarRetrievalError('invalid id in data.');
+
+        const data = parseXPath(item, './avatar_data:data').textContent;
+        const url = 'data:' + info.type + ';base64,' + data;
+        // TODO: validate the bytes too.
+        /*
+        // TODO: figure out why this didn’t work.
+        avatar_img.onload = function (evt) {
+            const img = evt.target;
+            if (img.naturalWidth != info.width || img.naturalHeight != info.height)
+                return onAvatarRetrievalError('invalid width or height in image data.');
+            avatar_img.onload = null;
+        };
+        */
+        avatar_img.src = url;
+    }
+
+    function onAvatarRetrievalError(string)
+    {
+        console.log('Failed to retrieve avatar, an empty one is displayed instead: ' + string);
+        avatar_img.src = DEFAULT_AVATAR;
+    }
+
+    avatar_upload.addEventListener('click', function (evt) {
+        avatar_file.click();
+    });
+
+    avatar_change.addEventListener('click', function (evt) {
+        const metadata_iq = $iq({type: 'set'})
+            .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
+                .c('publish', {node: 'urn:xmpp:avatar:metadata'})
+                    .c('item', {id: avatar_data.id})
+                        .c('metadata', {xmlns: 'urn:xmpp:avatar:metadata'})
+                            .c('info', {
+                                id: avatar_data.id,
+                                type: avatar_data.type,
+                                bytes: avatar_data.bytes,
+                                width: avatar_img.naturalWidth,
+                                height: avatar_img.naturalHeight,
+                            });
+        connection.sendIQ(metadata_iq, onAvatarMetadataUpload, onAvatarUploadError);
+        const data_iq = $iq({type: 'set'})
+            .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
+                .c('publish', {node: 'urn:xmpp:avatar:data'})
+                    .c('item', {id: avatar_data.id})
+                        .c('data', {xmlns: 'urn:xmpp:avatar:data'})
+                            .t(avatar_data.data);
+        connection.sendIQ(data_iq, onAvatarDataUpload, onAvatarUploadError);
+    });
+
+    function onAvatarMetadataUpload(iq)
+    {
+        console.log("onAvatarMetadataUpload", iq);
+    }
+
+    function onAvatarDataUpload(iq)
+    {
+        console.log('Avatar successfully uploaded!', iq);
+        avatar_change.disabled = true;
+        avatar_size.innerHTML = '';
+    }
+
+    function onAvatarUploadError(iq)
+    {
+        console.log("onAvatarUploadError", iq);
+    }
+
+    avatar_file.addEventListener('change', function (evt) {
+        const file = evt.target.files[0];
+        avatar_data.type = file.type;
+        avatar_data.bytes = file.size;
+
+        // Set the preview.
+        avatar_img.src = URL.createObjectURL(file);
+        const [size, unit] = friendlyDataSize(file.size);
+        avatar_size.innerHTML = Math.round(size) + ' ' + unit;
+
+        // Obtain the base64 version of this file.
+        const base64_reader = new FileReader();
+        base64_reader.onload = function (evt) {
+            const data = evt.target.result;
+            avatar_data.data = data.substr(data.indexOf(',') + 1);
+        }
+        base64_reader.readAsDataURL(file);
+
+        // Compute the sha1 of this file.
+        const sha1_reader = new FileReader();
+        sha1_reader.onload = async function (evt) {
+            const data = evt.target.result;
+            const digest = await window.crypto.subtle.digest('SHA-1', data);
+            const sha1 = (Array
+                .from(new Uint8Array(digest))
+                .map(b => b.toString(16).padStart(2, "0"))
+                .join(""));
+            avatar_data.id = sha1;
+            avatar_change.disabled = false;
+        }
+        sha1_reader.readAsArrayBuffer(file);
+    });
+
+    function nsResolver(prefix) {
+        return {
+            pubsub: 'http://jabber.org/protocol/pubsub',
+            avatar_metadata: 'urn:xmpp:avatar:metadata',
+            avatar_data: 'urn:xmpp:avatar:data',
+        }[prefix] || null;
+    }
+
+    function parseXPath(elem, xpath)
+    {
+        return elem.getRootNode().evaluate(xpath, elem, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
+    }
+
+    function friendlyDataSize(bytes) {
+        let unit = 'B'
+        if (bytes >= 1024) {
+            bytes /= 1024;
+            unit = 'KiB';
+        }
+        if (bytes >= 1024) {
+            bytes /= 1024;
+            unit = 'MiB';
+        }
+        if (bytes >= 1024) {
+            bytes /= 1024;
+            unit = 'GiB';
+        }
+        if (bytes >= 1024) {
+            bytes /= 1024;
+            unit = 'TiB';
+        }
+        return [bytes, unit];
+    }
+}