view avatar.js @ 63:ee1df80a1715 default tip

Nicer-looking input form
author mathieui
date Sun, 24 May 2020 14:19:29 +0200
parents 6d861d881b96
children
line wrap: on
line source

// SPDX-License-Identifier: AGPL-3.0-only
/*
 * Copyright © 2018-2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
 * Copyright © 2020 Mathieu Pasquet <mathieui@mathieui.net>
 *
 * Released under GNU AGPL v3 only, read the file 'LICENSE' for more information.
 */

'use strict';

function initAvatar(connection) {
    const DEFAULT_AVATAR = 'data:image/svg+xml,' + encodeURIComponent('<?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');
    const avatar_access = document.getElementById('avatar-access');
    const spinner_img = document.getElementById('avatar-spinner');
    const access_spinner_img = document.getElementById('access-model-avatar-spinner');

    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.'));
    displaySpinner(spinner_img);
    getAccessModel();

    function getAccessModel()
    {
        avatar_access.disabled = true;
        displaySpinner(access_spinner_img);
        retrieveConfiguration(connection, 'urn:xmpp:avatar:metadata').then((access_model) => {
            if (access_model !== null) {
                if (access_model === 'open')
                    avatar_access.value = 'open';
                else if (access_model === 'presence')
                    avatar_access.value = 'presence';
                else
                    console.log('Unsupported avatar access model: ' + access_model);
                avatar_access.disabled = false;
            }
            hideSpinner(access_spinner_img);
        }, (reason) => {
            console.log('Failed to retrieve avatar configuration: ' + reason);
            hideSpinner(access_spinner_img);
        });
    }

    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;
        hideSpinner(spinner_img);
        getAccessModel();
    }

    function onAvatarRetrievalError(string)
    {
        console.log('Failed to retrieve avatar, an empty one is displayed instead: ' + string);
        avatar_img.src = DEFAULT_AVATAR;
        hideSpinner(spinner_img);
        avatar_access.disabled = true;
    }

    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);
        displaySpinner(spinner_img);
        avatar_access.disabled = true;
    });

    function onAvatarMetadataUpload(iq)
    {
        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 onAvatarDataUpload(iq)
    {
        console.log('Avatar successfully uploaded!', iq);
        avatar_change.hidden = true;
        avatar_size.innerHTML = '';
        spinnerOk(spinner_img);
        getAccessModel();
    }

    function onAvatarUploadError(iq)
    {
        console.log("onAvatarUploadError", iq);
        spinnerError(spinner_img);
        avatar_access.disabled = true;
    }

    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.hidden = false;
        }
        sha1_reader.readAsArrayBuffer(file);
    });

    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];
    }

    avatar_access.addEventListener('change', function (evt) {
        const iq = configurePEPField('urn:xmpp:avatar:metadata', 'pubsub#access_model', evt.target.value);
        connection.sendIQ(iq, onAvatarMetadataConfigured, onAvatarConfigureError.bind(null, 'PubSub configuration of metadata failed.'));
        displaySpinner(access_spinner_img);
    });

    function onAvatarMetadataConfigured(result_iq)
    {
        const iq = configurePEPField('urn:xmpp:avatar:data', 'pubsub#access_model', avatar_access.value);
        connection.sendIQ(iq, onAvatarConfigured, onAvatarConfigureError.bind(null, 'PubSub configuration of data failed.'));
    }

    function onAvatarConfigured(result_iq)
    {
        console.log('Successfully set avatar access model.')
        spinnerOk(access_spinner_img);
    }

    function onAvatarConfigureError(string)
    {
        console.log('Failed to configure avatar node: ' + string);
        spinnerError(access_spinner_img);
    }
}