Ver Fonte

fixes #337

It's now possible to set your VCard via the UI and via the API
JC Brand há 7 anos atrás
pai
commit
708b1dbe99

+ 2 - 0
CHANGES.md

@@ -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.

+ 85 - 3
css/converse.css

@@ -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;
-    border: 1px solid lightgrey; }
+    width: 36px; }
   #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;

+ 94 - 14
css/inverse.css

@@ -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 {
-  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 {
 #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;
-    border: 1px solid lightgrey; }
+    width: 36px; }
   #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;

+ 9 - 44
dev.html

@@ -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/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" />
+    <link type="text/css" rel="stylesheet" media="screen" href="css/inverse.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">
-    <nav class="navbar navbar-custom navbar-fixed-top" role="navigation">
-        <div class="container">
-            <div class="navbar-header page-scroll">
-                <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>
+<body class="reset">
+    <div class="content">
+        <div class="inner-content">
+            <h1 class="brand-heading"><i class="icon-conversejs"></i> Converse</h1>
         </div>
         </div>
-    </nav>
-
-    <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>
+    </div>
 
 
 <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>

+ 31 - 0
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
 update
 ~~~~~~
 ~~~~~~
 
 

+ 1 - 2
mockup/chatroom.html

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

+ 1 - 0
sass/_core.scss

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

+ 0 - 1
sass/_messages.scss

@@ -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;

+ 1 - 0
sass/converse.scss

@@ -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";

+ 1 - 0
sass/inverse.scss

@@ -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";

+ 0 - 1
sass/inverse/_core.scss

@@ -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;

+ 87 - 2
src/converse-profile.js

@@ -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);
+                    }
+                }
             });
             });
 
 
 
 

+ 75 - 59
src/converse-vcard.js

@@ -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);
-}(this, function (converse, CryptoJS) {
+    define(["converse-core", "crypto", "tpl!vcard"], factory);
+}(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 - 0
src/templates/alert.html

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

+ 43 - 17
src/templates/profile_modal.html

@@ -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">
-                <div class="row">
-                    <div class="col-auto">
-                        {[ if (o.image) { ]}
-                        <a class="show-profile" href="#">
-                            <img alt="User Avatar" class="img-thumbnail avatar align-self-center" height="100px" width="100px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
-                        </a>
-                        {[ } ]}
-                    </div>
-                    <div class="col-auto">
-                        <div classs="row w-100">
-                            <label>Fullname:</label>
-                            <span class="username">{{{o.fullname}}}</span>
+            <form class="converse-form">
+                <div class="modal-body">
+                    <div class="row">
+                        <div class="col-auto">
+                            <a class="change-avatar" href="#">
+                                {[ if (o.image) { ]}
+                                    <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>
-                        <div classs="row w-100">
-                            <label>XMPP Address:</label>
-                            <span class="username">{{{o.jid}}}</span>
+                        <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>
+                    <div class="form-group">
+                        <label for="vcard-fullname" class="col-form-label">{{{o.label_fullname}}}:</label>
+                        <input id="vcard-fullname" type="text" class="form-control" name="fn" value="{{{o.fullname}}}">
+                    </div>
+                    <div class="form-group">
+                        <label for="vcard-nickname" class="col-form-label">{{{o.label_nickname}}}:</label>
+                        <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="{{{o.nickname}}}">
+                    </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 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>

+ 0 - 2
src/templates/profile_view.html

@@ -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 - 0
src/templates/vcard.html

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

+ 1 - 1
src/utils/core.js

@@ -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;