It's now possible to set your VCard via the UI and via the API
This commit is contained in:
JC Brand 2018-05-08 17:48:37 +02:00
parent 6c513ad4be
commit 708b1dbe99
18 changed files with 444 additions and 147 deletions

View File

@ -5,6 +5,8 @@
## New Features ## New Features
- #161 XEP-0363: HTTP File Upload - #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 - 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 - 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. - Support for rendering URLs sent according to XEP-0066 Out of Band Data.

View File

@ -4615,6 +4615,88 @@
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
background-color: #1d2124; } 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 { #conversejs .media {
display: flex; display: flex;
align-items: flex-start; } align-items: flex-start; }
@ -7067,7 +7149,8 @@ body.reset {
text-align: center; text-align: center;
width: 100%; } width: 100%; }
#conversejs .avatar { #conversejs .avatar {
border-radius: 10%; } border-radius: 10%;
border: 1px solid lightgrey; }
#conversejs .activated { #conversejs .activated {
display: block !important; } display: block !important; }
#conversejs .button-primary { #conversejs .button-primary {
@ -8417,8 +8500,7 @@ body.reset {
margin-top: 0.5em; margin-top: 0.5em;
height: 36px; height: 36px;
vertical-align: middle; vertical-align: middle;
width: 36px; width: 36px; }
border: 1px solid lightgrey; }
#conversejs .message.chat-msg .chat-msg-heading { #conversejs .message.chat-msg .chat-msg-heading {
margin-top: 0.5em; margin-top: 0.5em;
padding-right: 0.25rem; padding-right: 0.25rem;

View File

@ -4615,6 +4615,88 @@
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
background-color: #1d2124; } 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 { #conversejs .media {
display: flex; display: flex;
align-items: flex-start; } align-items: flex-start; }
@ -7067,7 +7149,8 @@ body.reset {
text-align: center; text-align: center;
width: 100%; } width: 100%; }
#conversejs .avatar { #conversejs .avatar {
border-radius: 10%; } border-radius: 10%;
border: 1px solid lightgrey; }
#conversejs .activated { #conversejs .activated {
display: block !important; } display: block !important; }
#conversejs .button-primary { #conversejs .button-primary {
@ -7203,17 +7286,15 @@ body {
#conversejs.fullscreen .converse-chatboxes { #conversejs.fullscreen .converse-chatboxes {
width: 100vw; width: 100vw;
right: 15px; } right: 15px; }
#conversejs.fullscreen form.converse-form { #conversejs.fullscreen form.converse-form input[type=checkbox] {
margin: 1em; } margin-left: 1em;
#conversejs.fullscreen form.converse-form input[type=checkbox] { display: inline;
margin-left: 1em; margin-bottom: 2em; }
display: inline; #conversejs.fullscreen form.converse-form input[type=button],
margin-bottom: 2em; } #conversejs.fullscreen form.converse-form input[type=submit] {
#conversejs.fullscreen form.converse-form input[type=button], padding-left: 1em;
#conversejs.fullscreen form.converse-form input[type=submit] { padding-right: 1em;
padding-left: 1em; margin-right: 1em; }
padding-right: 1em;
margin-right: 1em; }
#conversejs #user-profile-modal label { #conversejs #user-profile-modal label {
font-weight: bold; } font-weight: bold; }
@ -8605,8 +8686,7 @@ body {
margin-top: 0.5em; margin-top: 0.5em;
height: 36px; height: 36px;
vertical-align: middle; vertical-align: middle;
width: 36px; width: 36px; }
border: 1px solid lightgrey; }
#conversejs .message.chat-msg .chat-msg-heading { #conversejs .message.chat-msg .chat-msg-heading {
margin-top: 0.5em; margin-top: 0.5em;
padding-right: 0.25rem; padding-right: 0.25rem;

View File

@ -9,46 +9,17 @@
<meta name="author" content="JC Brand" /> <meta name="author" content="JC Brand" />
<meta name="keywords" content="xmpp chat webchat converse.js" /> <meta name="keywords" content="xmpp chat webchat converse.js" />
<link rel="shortcut icon" type="image/ico" href="css/images/favicon.ico"/> <link rel="shortcut icon" type="image/ico" href="css/images/favicon.ico"/>
<link type="text/css" rel="stylesheet" media="screen" href="css/bootstrap.min.css" /> <link type="text/css" rel="stylesheet" media="screen" href="css/inverse.css" />
<link type="text/css" rel="stylesheet" media="screen" href="css/font-awesome.min.css" />
<link type="text/css" rel="stylesheet" media="screen" href="css/theme.css" />
<link type="text/css" rel="stylesheet" media="screen" href="css/converse.css" />
<script src="node_modules/requirejs/require.js"></script> <script src="node_modules/requirejs/require.js"></script>
<script src="src/config.js"></script> <script src="src/config.js"></script>
</head> </head>
<body id="page-top" class="reset" data-target=".navbar-custom"> <body class="reset">
<nav class="navbar navbar-custom navbar-fixed-top" role="navigation"> <div class="content">
<div class="container"> <div class="inner-content">
<div class="navbar-header page-scroll"> <h1 class="brand-heading"><i class="icon-conversejs"></i> Converse</h1>
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-main-collapse">
<i class="fa fa-bars"></i>
</button>
</div>
<div class="collapse navbar-collapse navbar-right navbar-main-collapse">
<ul class="nav navbar-nav"><li> <a href="/docs/html/index.html">Documentation</a> </li>
</ul>
</div>
</div> </div>
</nav> </div>
<section class="intro">
<div class="intro-body">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h1 class="brand-heading"><i class="icon-conversejs"></i>converse</h1>
<p class="intro-text">Developer page.</p>
<p class="intro-text">
Converse.js will only work on this page if you have
<a href="https://conversejs.org/docs/html/development.html">set up the development environment</a>.
</p>
</div>
</div>
</div>
</div>
</section>
</body>
<script> <script>
require(['converse'], function (converse) { require(['converse'], function (converse) {
@ -60,24 +31,18 @@
// 'prosody@conference.prosody.im', // 'prosody@conference.prosody.im',
// 'jdev@conference.jabber.org' // 'jdev@conference.jabber.org'
// ], // ],
hide_open_bookmarks: true, view_mode: 'fullscreen',
archived_messages_page_size: '500',
allow_public_bookmarks: true, allow_public_bookmarks: true,
notify_all_room_messages: [ notify_all_room_messages: [
'discuss@conference.conversejs.org' 'discuss@conference.conversejs.org'
], ],
auto_join_private_chats: [
'opkode@jappix.com'
],
auto_reconnect: true,
// bosh_service_url: 'http://chat.example.org:5280/http-bind/', // bosh_service_url: 'http://chat.example.org:5280/http-bind/',
bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
message_archiving: 'always', message_archiving: 'always',
show_controlbox_by_default: true,
strict_plugin_dependencies: false,
chatstate_notification_blacklist: ['mulles@movim.eu'],
xhr_user_search: false,
debug: true debug: true
}); });
}); });
</script> </script>
</body>
</html> </html>

View File

@ -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 update
~~~~~~ ~~~~~~

View File

@ -53,11 +53,10 @@
</div> </div>
<div class="message chat-msg"> <div class="message chat-msg">
<div class="avatar montague"></div>
<canvas data-avatar="/mockup/images/romeo.jpg" height="36" width="36" class="avatar"></canvas> <canvas data-avatar="/mockup/images/romeo.jpg" height="36" width="36" class="avatar"></canvas>
<div class="chat-msg-content"> <div class="chat-msg-content">
<span class="chat-msg-heading"> <span class="chat-msg-heading">
<span class="chat-msg-author">Romeo Montague</span> <span class="chat-msg-author">Romeo Montague <span class="badge badge-primary">Developer</span></span>
<span class="chat-msg-time">15:31</span> <span class="chat-msg-time">15:31</span>
</span> </span>
<span class="chat-msg-text">He jests at scars that never felt a wound.</span> <span class="chat-msg-text">He jests at scars that never felt a wound.</span>

View File

@ -363,6 +363,7 @@ body.reset {
.avatar { .avatar {
border-radius: 10%; border-radius: 10%;
border: 1px solid lightgrey;
} }
.activated { .activated {

View File

@ -128,7 +128,6 @@
height: 36px; height: 36px;
vertical-align: middle; vertical-align: middle;
width: 36px; width: 36px;
border: 1px solid lightgrey;
} }
.chat-msg-heading { .chat-msg-heading {
margin-top: 0.5em; margin-top: 0.5em;

View File

@ -30,6 +30,7 @@
@import "bootstrap/scss/card"; @import "bootstrap/scss/card";
@import "bootstrap/scss/breadcrumb"; @import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/badge"; @import "bootstrap/scss/badge";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/media"; @import "bootstrap/scss/media";
@import "bootstrap/scss/list-group"; @import "bootstrap/scss/list-group";
@import "bootstrap/scss/close"; @import "bootstrap/scss/close";

View File

@ -29,6 +29,7 @@
@import "bootstrap/scss/card"; @import "bootstrap/scss/card";
@import "bootstrap/scss/breadcrumb"; @import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/badge"; @import "bootstrap/scss/badge";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/media"; @import "bootstrap/scss/media";
@import "bootstrap/scss/list-group"; @import "bootstrap/scss/list-group";
@import "bootstrap/scss/close"; @import "bootstrap/scss/close";

View File

@ -43,7 +43,6 @@ body {
form { form {
&.converse-form { &.converse-form {
margin: 1em;
input[type=checkbox] { input[type=checkbox] {
margin-left: 1em; margin-left: 1em;
display: inline; display: inline;

View File

@ -9,6 +9,7 @@
(function (root, factory) { (function (root, factory) {
define(["converse-core", define(["converse-core",
"bootstrap", "bootstrap",
"tpl!alert",
"tpl!chat_status_modal", "tpl!chat_status_modal",
"tpl!profile_modal", "tpl!profile_modal",
"tpl!profile_view", "tpl!profile_view",
@ -19,6 +20,7 @@
}(this, function ( }(this, function (
converse, converse,
bootstrap, bootstrap,
tpl_alert,
tpl_chat_status_modal, tpl_chat_status_modal,
tpl_profile_modal, tpl_profile_modal,
tpl_profile_view, tpl_profile_view,
@ -32,7 +34,7 @@
converse.plugins.add('converse-profile', { converse.plugins.add('converse-profile', {
dependencies: ["converse-modal"], dependencies: ["converse-modal", "converse-vcard"],
initialize () { initialize () {
/* The initialize function gets called as soon as the plugin is /* The initialize function gets called as soon as the plugin is
@ -43,13 +45,96 @@
_converse.ProfileModal = _converse.BootstrapModal.extend({ _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 () { toHTML () {
return tpl_profile_modal(_.extend(this.model.toJSON(), { return tpl_profile_modal(_.extend(this.model.toJSON(), {
'heading_profile': __('Your Profile'), '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);
}
}
}); });

View File

@ -5,68 +5,13 @@
// Licensed under the Mozilla Public License (MPLv2) // Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) { (function (root, factory) {
define(["converse-core", "crypto"], factory); define(["converse-core", "crypto", "tpl!vcard"], factory);
}(this, function (converse, CryptoJS) { }(this, function (converse, CryptoJS, tpl_vcard) {
"use strict"; "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; 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', { converse.plugins.add('converse-vcard', {
initialize () { 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 */ /* Event handlers */
_converse.initVCardCollection = function () { _converse.initVCardCollection = function () {
_converse.vcards = new _converse.VCards(); _converse.vcards = new _converse.VCards();
@ -108,6 +122,8 @@
_.extend(_converse.api, { _.extend(_converse.api, {
'vcard': { 'vcard': {
'set': setVCard,
'get' (model, force) { 'get' (model, force) {
if (_.isString(model)) { if (_.isString(model)) {
return getVCard(_converse, model); return getVCard(_converse, model);
@ -126,7 +142,7 @@
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.get(model, force).then((vcard) => { this.get(model, force).then((vcard) => {
model.save(_.extend( 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()} {'vcard_updated': moment().format()}
)); ));
resolve(); resolve();

1
src/templates/alert.html Normal file
View File

@ -0,0 +1 @@
<div class="alert {{{o.type}}}" role="alert">{{{o.message}}}</div>

View File

@ -5,27 +5,53 @@
<h5 class="modal-title" id="user-profile-modal-label">{{{o.heading_profile}}}</h5> <h5 class="modal-title" id="user-profile-modal-label">{{{o.heading_profile}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">&times;</span></button>
</div> </div>
<div class="modal-body"> <form class="converse-form">
<div class="row"> <div class="modal-body">
<div class="col-auto"> <div class="row">
{[ if (o.image) { ]} <div class="col-auto">
<a class="show-profile" href="#"> <a class="change-avatar" href="#">
<img alt="User Avatar" class="img-thumbnail avatar align-self-center" height="100px" width="100px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/> {[ if (o.image) { ]}
</a> <img alt="{{{o.alt_avatar}}}" class="img-thumbnail avatar align-self-center" height="100px" width="100px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
{[ } ]} {[ } ]}
{[ if (!o.image) { ]}
<canvas class="avatar" height="100px" width="100px"/>
{[ } ]}
</a>
<input class="hidden" name="image" type="file">
</div>
<div class="col">
<div class="form-group">
<label class="col-form-label">{{{o.label_jid}}}:</label>
<div>{{{o.jid}}}</div>
</div>
</div>
</div> </div>
<div class="col-auto"> <div class="form-group">
<div classs="row w-100"> <label for="vcard-fullname" class="col-form-label">{{{o.label_fullname}}}:</label>
<label>Fullname:</label> <input id="vcard-fullname" type="text" class="form-control" name="fn" value="{{{o.fullname}}}">
<span class="username">{{{o.fullname}}}</span> </div>
</div> <div class="form-group">
<div classs="row w-100"> <label for="vcard-nickname" class="col-form-label">{{{o.label_nickname}}}:</label>
<label>XMPP Address:</label> <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="{{{o.nickname}}}">
<span class="username">{{{o.jid}}}</span> </div>
</div> <div class="form-group">
<label for="vcard-url" class="col-form-label">{{{o.label_url}}}:</label>
<input id="vcard-url" type="url" class="form-control" name="url" value="{{{o.url}}}">
</div>
<div class="form-group">
<label for="vcard-email" class="col-form-label">{{{o.label_email}}}:</label>
<input id="vcard-email" type="email" class="form-control" name="email" value="{{{o.email}}}">
</div>
<div class="form-group">
<label for="vcard-role" class="col-form-label">{{{o.label_role}}}:</label>
<input id="vcard-role" type="text" class="form-control" name="role" value="{{{o.role}}}">
</div> </div>
</div> </div>
</div> <div class="modal-footer">
<button type="submit" class="save-form btn btn-primary">{{{o.label_save}}}</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.label_close}}}</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,8 @@
<div class="userinfo"> <div class="userinfo">
<div class="profile d-flex"> <div class="profile d-flex">
{[ if (o.image) { ]}
<a class="show-profile" href="#"> <a class="show-profile" href="#">
<img alt="User Avatar" class="avatar align-self-center" height="40px" width="40px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/> <img alt="User Avatar" class="avatar align-self-center" height="40px" width="40px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
</a> </a>
{[ } ]}
<span class="username w-100 align-self-center">{{{o.fullname}}}</span> <span class="username w-100 align-self-center">{{{o.fullname}}}</span>
<!-- <a class="chatbox-btn fa fa-vcard align-self-center" title="{{{o.title_your_profile}}}" data-toggle="modal" data-target="#userProfileModal"></a> --> <!-- <a class="chatbox-btn fa fa-vcard align-self-center" title="{{{o.title_your_profile}}}" data-toggle="modal" data-target="#userProfileModal"></a> -->
<!-- <a class="chatbox-btn fa fa-cog align-self-center" title="{{{o.title_change_status}}}" data-toggle="modal" data-target="#settingsModal"></a> --> <!-- <a class="chatbox-btn fa fa-cog align-self-center" title="{{{o.title_change_status}}}" data-toggle="modal" data-target="#settingsModal"></a> -->

11
src/templates/vcard.html Normal file
View File

@ -0,0 +1,11 @@
<vCard xmlns="vcard-temp">
<FN>{{{o.fn}}}</FN>
<NICKNAME>{{{o.fn}}}</NICKNAME>
<URL>{{{o.url}}}</URL>
<ROLE>{{{o.role}}}</ROLE>
<EMAIL><INTERNET/><PREF/><USERID>{{{o.email}}}</USERID></EMAIL>
<PHOTO>
<TYPE>{{{o.image_type}}}</TYPE>
<BINVAL>{{{o.image}}}</BINVAL>
</PHOTO>
</vCard>

View File

@ -40,6 +40,7 @@
root.sizzle, root.sizzle,
root.Promise, root.Promise,
root._, root._,
root.Backbone,
Strophe Strophe
); );
} }
@ -56,7 +57,6 @@
tpl_video tpl_video
) { ) {
"use strict"; "use strict";
const b64_sha1 = Strophe.SHA1.b64_sha1;
Strophe = Strophe.Strophe; Strophe = Strophe.Strophe;
const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g; const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b\/?/g;