From 708b1dbe99ec3c3b52521a13cb88a7d6a5cd9387 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 8 May 2018 17:48:37 +0200 Subject: [PATCH] fixes #337 It's now possible to set your VCard via the UI and via the API --- CHANGES.md | 2 + css/converse.css | 88 +++++++++++++++++++- css/inverse.css | 108 +++++++++++++++++++++---- dev.html | 53 +++--------- docs/source/developer_api.rst | 31 +++++++ mockup/chatroom.html | 3 +- sass/_core.scss | 1 + sass/_messages.scss | 1 - sass/converse.scss | 1 + sass/inverse.scss | 1 + sass/inverse/_core.scss | 1 - src/converse-profile.js | 89 +++++++++++++++++++- src/converse-vcard.js | 134 +++++++++++++++++-------------- src/templates/alert.html | 1 + src/templates/profile_modal.html | 62 +++++++++----- src/templates/profile_view.html | 2 - src/templates/vcard.html | 11 +++ src/utils/core.js | 2 +- 18 files changed, 444 insertions(+), 147 deletions(-) create mode 100644 src/templates/alert.html create mode 100644 src/templates/vcard.html diff --git a/CHANGES.md b/CHANGES.md index 4b61a479b..e0d67fb06 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,8 @@ ## New Features - #161 XEP-0363: HTTP File Upload +- #337 API call to update a VCard +- It's now also possible to edit your VCard via the UI - Automatically grow/shrink input as text is entered/removed - MP4 and MP3 files when sent as XEP-0066 Out of Band Data, are now playable directly in chat - Support for rendering URLs sent according to XEP-0066 Out of Band Data. diff --git a/css/converse.css b/css/converse.css index 60e32e9ca..6c4a3c3b6 100644 --- a/css/converse.css +++ b/css/converse.css @@ -4615,6 +4615,88 @@ color: #fff; text-decoration: none; background-color: #1d2124; } +#conversejs .alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; } +#conversejs .alert-heading { + color: inherit; } +#conversejs .alert-link { + font-weight: 700; } +#conversejs .alert-dismissible { + padding-right: 4rem; } + #conversejs .alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; } +#conversejs .alert-primary { + color: #1d3d4c; + background-color: #d7e3e9; + border-color: #c7d8e0; } + #conversejs .alert-primary hr { + border-top-color: #b7cdd7; } + #conversejs .alert-primary .alert-link { + color: #0f1f27; } +#conversejs .alert-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; } + #conversejs .alert-secondary hr { + border-top-color: #c8cbcf; } + #conversejs .alert-secondary .alert-link { + color: #202326; } +#conversejs .alert-success { + color: #1e5637; + background-color: #d8ede1; + border-color: #c8e6d5; } + #conversejs .alert-success hr { + border-top-color: #b6dec8; } + #conversejs .alert-success .alert-link { + color: #11301f; } +#conversejs .alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; } + #conversejs .alert-info hr { + border-top-color: #abdde5; } + #conversejs .alert-info .alert-link { + color: #062c33; } +#conversejs .alert-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; } + #conversejs .alert-warning hr { + border-top-color: #ffe8a1; } + #conversejs .alert-warning .alert-link { + color: #533f03; } +#conversejs .alert-danger { + color: #783a2a; + background-color: #fae2dc; + border-color: #f8d7ce; } + #conversejs .alert-danger hr { + border-top-color: #f5c5b8; } + #conversejs .alert-danger .alert-link { + color: #52281d; } +#conversejs .alert-light { + color: #818182; + background-color: #fefefe; + border-color: #fdfdfe; } + #conversejs .alert-light hr { + border-top-color: #ececf6; } + #conversejs .alert-light .alert-link { + color: #686868; } +#conversejs .alert-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; } + #conversejs .alert-dark hr { + border-top-color: #b9bbbe; } + #conversejs .alert-dark .alert-link { + color: #040505; } #conversejs .media { display: flex; align-items: flex-start; } @@ -7067,7 +7149,8 @@ body.reset { text-align: center; width: 100%; } #conversejs .avatar { - border-radius: 10%; } + border-radius: 10%; + border: 1px solid lightgrey; } #conversejs .activated { display: block !important; } #conversejs .button-primary { @@ -8417,8 +8500,7 @@ body.reset { margin-top: 0.5em; height: 36px; vertical-align: middle; - width: 36px; - border: 1px solid lightgrey; } + width: 36px; } #conversejs .message.chat-msg .chat-msg-heading { margin-top: 0.5em; padding-right: 0.25rem; diff --git a/css/inverse.css b/css/inverse.css index 633ef2c82..b43548717 100644 --- a/css/inverse.css +++ b/css/inverse.css @@ -4615,6 +4615,88 @@ color: #fff; text-decoration: none; background-color: #1d2124; } +#conversejs .alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.25rem; } +#conversejs .alert-heading { + color: inherit; } +#conversejs .alert-link { + font-weight: 700; } +#conversejs .alert-dismissible { + padding-right: 4rem; } + #conversejs .alert-dismissible .close { + position: absolute; + top: 0; + right: 0; + padding: 0.75rem 1.25rem; + color: inherit; } +#conversejs .alert-primary { + color: #1d3d4c; + background-color: #d7e3e9; + border-color: #c7d8e0; } + #conversejs .alert-primary hr { + border-top-color: #b7cdd7; } + #conversejs .alert-primary .alert-link { + color: #0f1f27; } +#conversejs .alert-secondary { + color: #383d41; + background-color: #e2e3e5; + border-color: #d6d8db; } + #conversejs .alert-secondary hr { + border-top-color: #c8cbcf; } + #conversejs .alert-secondary .alert-link { + color: #202326; } +#conversejs .alert-success { + color: #1e5637; + background-color: #d8ede1; + border-color: #c8e6d5; } + #conversejs .alert-success hr { + border-top-color: #b6dec8; } + #conversejs .alert-success .alert-link { + color: #11301f; } +#conversejs .alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; } + #conversejs .alert-info hr { + border-top-color: #abdde5; } + #conversejs .alert-info .alert-link { + color: #062c33; } +#conversejs .alert-warning { + color: #856404; + background-color: #fff3cd; + border-color: #ffeeba; } + #conversejs .alert-warning hr { + border-top-color: #ffe8a1; } + #conversejs .alert-warning .alert-link { + color: #533f03; } +#conversejs .alert-danger { + color: #783a2a; + background-color: #fae2dc; + border-color: #f8d7ce; } + #conversejs .alert-danger hr { + border-top-color: #f5c5b8; } + #conversejs .alert-danger .alert-link { + color: #52281d; } +#conversejs .alert-light { + color: #818182; + background-color: #fefefe; + border-color: #fdfdfe; } + #conversejs .alert-light hr { + border-top-color: #ececf6; } + #conversejs .alert-light .alert-link { + color: #686868; } +#conversejs .alert-dark { + color: #1b1e21; + background-color: #d6d8d9; + border-color: #c6c8ca; } + #conversejs .alert-dark hr { + border-top-color: #b9bbbe; } + #conversejs .alert-dark .alert-link { + color: #040505; } #conversejs .media { display: flex; align-items: flex-start; } @@ -7067,7 +7149,8 @@ body.reset { text-align: center; width: 100%; } #conversejs .avatar { - border-radius: 10%; } + border-radius: 10%; + border: 1px solid lightgrey; } #conversejs .activated { display: block !important; } #conversejs .button-primary { @@ -7203,17 +7286,15 @@ body { #conversejs.fullscreen .converse-chatboxes { width: 100vw; right: 15px; } -#conversejs.fullscreen form.converse-form { - margin: 1em; } - #conversejs.fullscreen form.converse-form input[type=checkbox] { - margin-left: 1em; - display: inline; - margin-bottom: 2em; } - #conversejs.fullscreen form.converse-form input[type=button], - #conversejs.fullscreen form.converse-form input[type=submit] { - padding-left: 1em; - padding-right: 1em; - margin-right: 1em; } +#conversejs.fullscreen form.converse-form input[type=checkbox] { + margin-left: 1em; + display: inline; + margin-bottom: 2em; } +#conversejs.fullscreen form.converse-form input[type=button], +#conversejs.fullscreen form.converse-form input[type=submit] { + padding-left: 1em; + padding-right: 1em; + margin-right: 1em; } #conversejs #user-profile-modal label { font-weight: bold; } @@ -8605,8 +8686,7 @@ body { margin-top: 0.5em; height: 36px; vertical-align: middle; - width: 36px; - border: 1px solid lightgrey; } + width: 36px; } #conversejs .message.chat-msg .chat-msg-heading { margin-top: 0.5em; padding-right: 0.25rem; diff --git a/dev.html b/dev.html index 564b7cfce..49405a304 100644 --- a/dev.html +++ b/dev.html @@ -9,46 +9,17 @@ - - - - + - - - -
-
-
-
-
-

converse

-

Developer page.

-

- Converse.js will only work on this page if you have - set up the development environment. -

-
-
-
-
-
- + + diff --git a/docs/source/developer_api.rst b/docs/source/developer_api.rst index 766d53e83..8c1d9841f 100644 --- a/docs/source/developer_api.rst +++ b/docs/source/developer_api.rst @@ -1293,6 +1293,37 @@ Example: } }); +set +~~~ + +Parameters: + +* ``data`` a map of VCard keys and values + +Enables setting new values for a VCard. + +Example: + +.. code-block:: javascript + + converse.plugins.add('myplugin', { + initialize: function () { + + _converse.api.waitUntil('rosterContactsFetched').then(() => { + this._converse.api.vcard.set({ + 'jid': 'someone@example.org', + 'fn': 'Someone Somewhere', + 'nickname': 'someone' + }).then(() => { + // Succes + }).catch(() => { + // Failure + }). + }); + + } + }); + update ~~~~~~ diff --git a/mockup/chatroom.html b/mockup/chatroom.html index d4d357cbd..c0d670515 100644 --- a/mockup/chatroom.html +++ b/mockup/chatroom.html @@ -53,11 +53,10 @@
-
- Romeo Montague + Romeo Montague Developer 15:31 He jests at scars that never felt a wound. diff --git a/sass/_core.scss b/sass/_core.scss index ea9e0ba62..56f581e7a 100644 --- a/sass/_core.scss +++ b/sass/_core.scss @@ -363,6 +363,7 @@ body.reset { .avatar { border-radius: 10%; + border: 1px solid lightgrey; } .activated { diff --git a/sass/_messages.scss b/sass/_messages.scss index 66291a2db..430869cd5 100644 --- a/sass/_messages.scss +++ b/sass/_messages.scss @@ -128,7 +128,6 @@ height: 36px; vertical-align: middle; width: 36px; - border: 1px solid lightgrey; } .chat-msg-heading { margin-top: 0.5em; diff --git a/sass/converse.scss b/sass/converse.scss index fc06fd2bd..8acbc316a 100644 --- a/sass/converse.scss +++ b/sass/converse.scss @@ -30,6 +30,7 @@ @import "bootstrap/scss/card"; @import "bootstrap/scss/breadcrumb"; @import "bootstrap/scss/badge"; + @import "bootstrap/scss/alert"; @import "bootstrap/scss/media"; @import "bootstrap/scss/list-group"; @import "bootstrap/scss/close"; diff --git a/sass/inverse.scss b/sass/inverse.scss index 31f748c28..2a8391b56 100644 --- a/sass/inverse.scss +++ b/sass/inverse.scss @@ -29,6 +29,7 @@ @import "bootstrap/scss/card"; @import "bootstrap/scss/breadcrumb"; @import "bootstrap/scss/badge"; + @import "bootstrap/scss/alert"; @import "bootstrap/scss/media"; @import "bootstrap/scss/list-group"; @import "bootstrap/scss/close"; diff --git a/sass/inverse/_core.scss b/sass/inverse/_core.scss index e905a249c..a608205ba 100644 --- a/sass/inverse/_core.scss +++ b/sass/inverse/_core.scss @@ -43,7 +43,6 @@ body { form { &.converse-form { - margin: 1em; input[type=checkbox] { margin-left: 1em; display: inline; diff --git a/src/converse-profile.js b/src/converse-profile.js index 92ab34efc..c0dc2f26d 100644 --- a/src/converse-profile.js +++ b/src/converse-profile.js @@ -9,6 +9,7 @@ (function (root, factory) { define(["converse-core", "bootstrap", + "tpl!alert", "tpl!chat_status_modal", "tpl!profile_modal", "tpl!profile_view", @@ -19,6 +20,7 @@ }(this, function ( converse, bootstrap, + tpl_alert, tpl_chat_status_modal, tpl_profile_modal, tpl_profile_view, @@ -32,7 +34,7 @@ converse.plugins.add('converse-profile', { - dependencies: ["converse-modal"], + dependencies: ["converse-modal", "converse-vcard"], initialize () { /* The initialize function gets called as soon as the plugin is @@ -43,13 +45,96 @@ _converse.ProfileModal = _converse.BootstrapModal.extend({ + events: { + 'click .change-avatar': "openFileSelection", + 'change input[type="file"': "updateFilePreview", + 'submit form': 'onFormSubmitted' + }, + + initialize () { + _converse.BootstrapModal.prototype.initialize.apply(this, arguments); + this.model.on('change', this.render, this); + }, toHTML () { return tpl_profile_modal(_.extend(this.model.toJSON(), { 'heading_profile': __('Your Profile'), - 'label_close': __('Close') + 'label_close': __('Close'), + 'label_email': __('Email'), + 'label_fullname': __('Full Name'), + 'label_nickname': __('Nickname'), + 'label_jid': __('XMPP Address (JID)'), + 'label_role': __('Role'), + 'label_save': __('Save'), + 'label_url': __('URL'), + 'alt_avatar': __('Your avatar image') })); }, + + openFileSelection (ev) { + ev.preventDefault(); + this.el.querySelector('input[type="file"]').click(); + }, + + updateFilePreview (ev) { + const file = ev.target.files[0], + reader = new FileReader(); + reader.onloadend = () => { + this.el.querySelector('.avatar').setAttribute('src', reader.result); + }; + reader.readAsDataURL(file); + }, + + setVCard (body, data) { + _converse.api.vcard.set(data) + .then(() => { + _converse.api.vcard.update(this.model, true); + + const html = tpl_alert({ + 'message': __('Profile data succesfully saved'), + 'type': 'alert-primary' + }); + body.insertAdjacentHTML('afterBegin', html); + }).catch((err) => { + _converse.log(err, Strophe.LogLevel.FATAL); + const html = tpl_alert({ + 'message': __('An error happened while trying to save your profile data'), + 'type': 'alert-danger' + }); + body.insertAdjacentHTML('afterBegin', html); + }); + }, + + onFormSubmitted (ev) { + ev.preventDefault(); + const reader = new FileReader(), + form_data = new FormData(ev.target), + body = this.el.querySelector('.modal-body'), + image_file = form_data.get('image'); + + const data = { + 'fn': form_data.get('fn'), + 'role': form_data.get('role'), + 'email': form_data.get('email'), + 'url': form_data.get('url'), + }; + if (!image_file.size) { + _.extend(data, { + 'image': this.model.get('image'), + 'image_type': this.model.get('image_type') + }); + this.setVCard(body, data); + } else { + reader.onloadend = () => { + _.extend(data, { + 'image': btoa(reader.result), + 'image_type': image_file.type + }); + this.setVCard(body, data); + }; + reader.readAsBinaryString(image_file); + } + } }); diff --git a/src/converse-vcard.js b/src/converse-vcard.js index f742c163b..918e1f17d 100644 --- a/src/converse-vcard.js +++ b/src/converse-vcard.js @@ -5,68 +5,13 @@ // Licensed under the Mozilla Public License (MPLv2) (function (root, factory) { - define(["converse-core", "crypto"], factory); -}(this, function (converse, CryptoJS) { + define(["converse-core", "crypto", "tpl!vcard"], factory); +}(this, function (converse, CryptoJS, tpl_vcard) { "use strict"; - const { Backbone, Promise, Strophe, SHA1, _, $iq, b64_sha1, moment, sizzle } = converse.env; + const { Backbone, Promise, Strophe, SHA1, _, $iq, $build, b64_sha1, moment, sizzle } = converse.env; const u = converse.env.utils; - function onVCardData (_converse, jid, iq, callback) { - const vcard = iq.querySelector('vCard'); - let result = {}; - if (!_.isNull(vcard)) { - result = { - 'stanza': iq, - 'fullname': _.get(vcard.querySelector('FN'), 'textContent'), - 'image': _.get(vcard.querySelector('PHOTO BINVAL'), 'textContent'), - 'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'), - 'url': _.get(vcard.querySelector('URL'), 'textContent') - }; - } - if (result.image) { - const word_array_from_b64 = CryptoJS.enc.Base64.parse(result['image']); - result['image_type'] = CryptoJS.SHA1(word_array_from_b64).toString() - } - if (callback) { - callback(result); - } - } - - function onVCardError (_converse, jid, iq, errback) { - if (errback) { - errback({'stanza': iq, 'jid': jid}); - } - } - - function createStanza (type, jid, vcard_el) { - const iq = $iq(jid ? {'type': type, 'to': jid} : {'type': type}); - iq.c("vCard", {'xmlns': Strophe.NS.VCARD}); - if (vcard_el) { - iq.cnode(vcard_el); - } - return iq; - } - - function getVCard (_converse, jid) { - /* Request the VCard of another user. Returns a promise. - * - * Parameters: - * (String) jid - The Jabber ID of the user whose VCard - * is being requested. - */ - jid = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid; - return new Promise((resolve, reject) => { - _converse.connection.sendIQ( - createStanza("get", jid), - _.partial(onVCardData, _converse, jid, _, resolve), - _.partial(onVCardError, _converse, jid, _, resolve), - 5000 - ); - }); - } - - converse.plugins.add('converse-vcard', { initialize () { @@ -84,6 +29,75 @@ }); + function onVCardData (_converse, jid, iq, callback) { + const vcard = iq.querySelector('vCard'); + let result = {}; + if (!_.isNull(vcard)) { + result = { + 'stanza': iq, + 'fullname': _.get(vcard.querySelector('FN'), 'textContent'), + 'nickname': _.get(vcard.querySelector('NICKNAME'), 'textContent'), + 'image': _.get(vcard.querySelector('PHOTO BINVAL'), 'textContent'), + 'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'), + 'url': _.get(vcard.querySelector('URL'), 'textContent'), + 'role': _.get(vcard.querySelector('ROLE'), 'textContent'), + 'email': _.get(vcard.querySelector('EMAIL USERID'), 'textContent') + }; + } + if (result.image) { + const word_array_from_b64 = CryptoJS.enc.Base64.parse(result['image']); + result['image_type'] = CryptoJS.SHA1(word_array_from_b64).toString() + } + if (callback) { + callback(result); + } + } + + function onVCardError (_converse, jid, iq, errback) { + if (errback) { + errback({'stanza': iq, 'jid': jid}); + } + } + + function createStanza (type, jid, vcard_el) { + const iq = $iq(jid ? {'type': type, 'to': jid} : {'type': type}); + if (!vcard_el) { + iq.c("vCard", {'xmlns': Strophe.NS.VCARD}); + } else { + iq.cnode(vcard_el); + } + return iq; + } + + function setVCard (data) { + return new Promise((resolve, reject) => { + const vcard_el = Strophe.xmlHtmlNode(tpl_vcard(data)).firstElementChild; + _converse.connection.sendIQ( + createStanza("set", data.jid, vcard_el), + resolve, + reject + ); + }); + } + + function getVCard (_converse, jid) { + /* Request the VCard of another user. Returns a promise. + * + * Parameters: + * (String) jid - The Jabber ID of the user whose VCard + * is being requested. + */ + jid = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid; + return new Promise((resolve, reject) => { + _converse.connection.sendIQ( + createStanza("get", jid), + _.partial(onVCardData, _converse, jid, _, resolve), + _.partial(onVCardError, _converse, jid, _, resolve), + 5000 + ); + }); + } + /* Event handlers */ _converse.initVCardCollection = function () { _converse.vcards = new _converse.VCards(); @@ -108,6 +122,8 @@ _.extend(_converse.api, { 'vcard': { + 'set': setVCard, + 'get' (model, force) { if (_.isString(model)) { return getVCard(_converse, model); @@ -126,7 +142,7 @@ return new Promise((resolve, reject) => { this.get(model, force).then((vcard) => { model.save(_.extend( - _.pick(vcard, ['fullname', 'url', 'image_type', 'image', 'image_hash']), + _.pick(vcard, ['fullname', 'nickname', 'email', 'url', 'role', 'image_type', 'image', 'image_hash']), {'vcard_updated': moment().format()} )); resolve(); diff --git a/src/templates/alert.html b/src/templates/alert.html new file mode 100644 index 000000000..16b17d601 --- /dev/null +++ b/src/templates/alert.html @@ -0,0 +1 @@ + diff --git a/src/templates/profile_modal.html b/src/templates/profile_modal.html index b41dc4ba7..8e4b73ea9 100644 --- a/src/templates/profile_modal.html +++ b/src/templates/profile_modal.html @@ -5,27 +5,53 @@
- diff --git a/src/templates/profile_view.html b/src/templates/profile_view.html index 96d8f9a99..465c0b193 100644 --- a/src/templates/profile_view.html +++ b/src/templates/profile_view.html @@ -1,10 +1,8 @@
- {[ if (o.image) { ]} User Avatar - {[ } ]} {{{o.fullname}}} diff --git a/src/templates/vcard.html b/src/templates/vcard.html new file mode 100644 index 000000000..97d8c9801 --- /dev/null +++ b/src/templates/vcard.html @@ -0,0 +1,11 @@ + + {{{o.fn}}} + {{{o.fn}}} + {{{o.url}}} + {{{o.role}}} + {{{o.email}}} + + {{{o.image_type}}} + {{{o.image}}} + + diff --git a/src/utils/core.js b/src/utils/core.js index 71b818572..bf6b98ef1 100644 --- a/src/utils/core.js +++ b/src/utils/core.js @@ -40,6 +40,7 @@ root.sizzle, root.Promise, root._, + root.Backbone, Strophe ); } @@ -56,7 +57,6 @@ tpl_video ) { "use strict"; - const b64_sha1 = Strophe.SHA1.b64_sha1; Strophe = Strophe.Strophe; const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;