Browse Source

Create an image picker component and use it in the profile modal

JC Brand 5 years ago
parent
commit
09a79d609f

+ 1 - 0
.eslintrc.json

@@ -198,6 +198,7 @@
         "no-unused-expressions": "off",
         "no-use-before-define": "off",
         "no-useless-call": "error",
+        "no-useless-catch": "off",
         "no-useless-computed-key": "error",
         "no-useless-concat": "off",
         "no-useless-constructor": "error",

+ 46 - 0
src/components/image_picker.js

@@ -0,0 +1,46 @@
+import { CustomElement } from './element.js';
+import { __ } from '@converse/headless/i18n';
+import { html } from 'lit-element';
+import { renderAvatar } from "../templates/directives/avatar.js";
+
+const i18n_alt_avatar = __('Your avatar image');
+
+
+export class ImagePicker extends CustomElement {
+
+    static get properties () {
+        return {
+            'height': { type: Number },
+            'image': { type: String },
+            'width': { type: Number },
+        }
+    }
+
+    render () {
+        const avatar_data = {
+            'height': this.height,
+            'image': this.image,
+            'width': this.width,
+        };
+        return html`
+            <a class="change-avatar" @click=${this.openFileSelection} title="${i18n_alt_avatar}">
+                ${ renderAvatar(avatar_data) }
+            </a>
+            <input @change=${this.updateFilePreview} class="hidden" name="image" type="file"/>
+        `;
+    }
+
+    openFileSelection (ev) {
+        ev.preventDefault();
+        this.querySelector('input[type="file"]').click();
+    }
+
+    updateFilePreview (ev) {
+        const file = ev.target.files[0];
+        const reader = new FileReader();
+        reader.onloadend = () => (this.image = reader.result);
+        reader.readAsDataURL(file);
+    }
+}
+
+window.customElements.define('converse-image-picker', ImagePicker);

+ 14 - 2
src/components/message.js

@@ -115,7 +115,7 @@ class Message extends CustomElement {
         const size = filesize(this.model.file.size);
         return html`
             <div class="message chat-msg">
-                ${ renderAvatar(this) }
+                ${ renderAvatar(this.getAvatarData()) }
                 <div class="chat-msg__content">
                     <span class="chat-msg__text">${i18n_uploading} <strong>${filename}</strong>, ${size}</span>
                     <progress value="${this.progress}"/>
@@ -132,7 +132,7 @@ class Message extends CustomElement {
                     ${this.isFollowup() ? 'chat-msg--followup' : ''}"
                     data-isodate="${this.time}" data-msgid="${this.msgid}" data-from="${this.from}" data-encrypted="${this.is_encrypted}">
 
-                ${ renderAvatar(this) }
+                ${ (this.is_me_message || this.type === 'headline') ? '' : renderAvatar(this.getAvatarData()) }
                 <div class="chat-msg__content chat-msg__content--${this.sender} ${this.is_me_message ? 'chat-msg__content--action' : ''}">
                     <span class="chat-msg__heading">
                         ${ (this.is_me_message) ? html`
@@ -165,6 +165,18 @@ class Message extends CustomElement {
             </div>`;
     }
 
+    getAvatarData () {
+        const image_type = this.model.vcard.get('image_type');
+        const image_data = this.model.vcard.get('image');
+        const image = "data:" + image_type + ";base64," + image_data;
+        return {
+            'classes': 'chat-msg__avatar',
+            'height': 36,
+            'width': 36,
+            image,
+        };
+    }
+
     async onRetryClicked () {
         this.show_spinner = true;
         await this.model.error.retry();

+ 18 - 25
src/converse-profile.js

@@ -38,8 +38,6 @@ converse.plugins.add('converse-profile', {
         _converse.ProfileModal = BootstrapModal.extend({
             id: "user-profile-modal",
             events: {
-                'change input[type="file"': "updateFilePreview",
-                'click .change-avatar': "openFileSelection",
                 'submit .profile-form': 'onFormSubmitted'
             },
 
@@ -58,29 +56,25 @@ converse.plugins.add('converse-profile', {
             toHTML () {
                 return tpl_profile_modal(Object.assign(
                     this.model.toJSON(),
-                    this.model.vcard.toJSON(), {
-                    '_converse': _converse,
-                    'utils': u,
-                    'view': this
-                }));
-            },
-
-            afterRender () {
-                this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
+                    this.model.vcard.toJSON(),
+                    this.getAvatarData(),
+                    { 'view': this }
+                ));
             },
 
-            openFileSelection (ev) {
-                ev.preventDefault();
-                this.el.querySelector('input[type="file"]').click();
+            getAvatarData () {
+                const image_type = this.model.vcard.get('image_type');
+                const image_data = this.model.vcard.get('image');
+                const image = "data:" + image_type + ";base64," + image_data;
+                return {
+                    'height': 128,
+                    'width': 128,
+                    image,
+                };
             },
 
-            updateFilePreview (ev) {
-                const file = ev.target.files[0],
-                      reader = new FileReader();
-                reader.onloadend = () => {
-                    this.el.querySelector('.avatar').setAttribute('src', reader.result);
-                };
-                reader.readAsDataURL(file);
+            afterRender () {
+                this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
             },
 
             async setVCard (data) {
@@ -99,10 +93,9 @@ converse.plugins.add('converse-profile', {
 
             onFormSubmitted (ev) {
                 ev.preventDefault();
-                const reader = new FileReader(),
-                      form_data = new FormData(ev.target),
-                      image_file = form_data.get('image');
-
+                const reader = new FileReader();
+                const form_data = new FormData(ev.target);
+                const image_file = form_data.get('image');
                 const data = {
                     'fn': form_data.get('fn'),
                     'nickname': form_data.get('nickname'),

+ 21 - 23
src/templates/directives/avatar.js

@@ -1,31 +1,29 @@
-import tpl_avatar from "templates/avatar.svg";
 import xss from "xss/dist/xss";
 import { directive, html } from "lit-html";
-import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
+import { unsafeSVG } from 'lit-html/directives/unsafe-svg.js';
 
 
-export const renderAvatar = directive(o => part => {
-    if (o.type === 'headline' || o.is_me_message) {
-        part.setValue('');
-        return;
+const whitelist_opts = {
+    'whiteList': {
+        'svg': ['xmlns', 'xmlns:xlink', 'class', 'width', 'height'],
+        'image': ['width', 'height', 'preserveAspectRatio', 'xlink:href']
     }
+};
+const tpl_svg = (o) => xss.filterXSS(`<image width="${o.width}" height="${o.height}" preserveAspectRatio="xMidYMid meet" xlink:href="${o.image}"/>`, whitelist_opts);
+
+const tpl_avatar = (o) => html`
+    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="avatar ${o.classes}" width="${o.width}" height="${o.height}">
+        ${ unsafeSVG(tpl_svg(o)) }
+    </svg>
+`;
 
-    if (o.model.vcard) {
-        const data = {
-            'classes': 'avatar chat-msg__avatar',
-            'width': 36,
-            'height': 36,
-        }
-        const image_type = o.model.vcard.get('image_type');
-        const image = o.model.vcard.get('image');
-        data['image'] = "data:" + image_type + ";base64," + image;
-        const avatar = tpl_avatar(data);
-        const opts = {
-            'whiteList': {
-                'svg': ['xmlns', 'xmlns:xlink', 'class', 'width', 'height'],
-                'image': ['width', 'height', 'preserveAspectRatio', 'xlink:href']
-            }
-        };
-        part.setValue(html`${unsafeHTML(xss.filterXSS(avatar, opts))}`);
+
+export const renderAvatar = directive(o => part => {
+    const data = {
+        'classes': o.classes || '',
+        'height': o.width || 36,
+        'image': o.image,
+        'width': o.height || 36,
     }
+    part.setValue(tpl_avatar(data));
 });

+ 11 - 14
src/templates/profile_modal.js

@@ -1,11 +1,12 @@
-import { html } from "lit-html";
-import { __ } from '@converse/headless/i18n';
-import avatar from "./avatar.js";
+import "../components/image_picker.js";
 import spinner from "./spinner.js";
-import { modal_close_button, modal_header_close_button } from "./buttons"
+import { __ } from '@converse/headless/i18n';
+import { _converse, converse } from  "@converse/headless/converse-core";
+import { html } from "lit-html";
+import { modal_header_close_button } from "./buttons";
 
+const u = converse.env.utils;
 
-const alt_avatar = __('Your avatar image');
 const heading_profile = __('Your Profile');
 const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following fingerprint');
 const i18n_device_without_fingerprint = __('Device without a fingerprint');
@@ -38,7 +39,7 @@ const navigation =  html`
 
 
 const fingerprint = (o) => html`
-    <span class="fingerprint">${o.utils.formatFingerprint(o.view.current_device.get('bundle').fingerprint)}</span>`;
+    <span class="fingerprint">${u.formatFingerprint(o.view.current_device.get('bundle').fingerprint)}</span>`;
 
 
 const device_with_fingerprint = (o) => html`
@@ -46,7 +47,7 @@ const device_with_fingerprint = (o) => html`
         <label>
         <input type="checkbox" value="${o.device.get('id')}"
             aria-label="${i18n_fingerprint_checkbox_label}"/>
-        <span class="fingerprint">${o.utils.formatFingerprint(o.device.get('bundle').fingerprint)}</span>
+        <span class="fingerprint">${u.formatFingerprint(o.device.get('bundle').fingerprint)}</span>
         </label>
     </li>
 `;
@@ -108,16 +109,13 @@ export default (o) => html`
             </div>
             <div class="modal-body">
                 <span class="modal-alert"></span>
-                ${o._converse.pluggable.plugins['converse-omemo'].enabled(o._converse) && navigation}
+                ${_converse.pluggable.plugins['converse-omemo'].enabled(_converse) && navigation}
                 <div class="tab-content">
                     <div class="tab-pane active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">
                         <form class="converse-form converse-form--modal profile-form" action="#">
                             <div class="row">
                                 <div class="col-auto">
-                                    <a class="change-avatar" href="#">
-                                        ${o.image ? avatar(Object.assign({'alt_text': alt_avatar}, o)) : '<canvas class="avatar" height="100px" width="100px"></canvas>'}
-                                    </a>
-                                    <input class="hidden" name="image" type="file"/>
+                                    <converse-image-picker image="${o.image}" width="${o.width}" height="${o.height}"></converse-image-picker>
                                 </div>
                                 <div class="col">
                                     <div class="form-group">
@@ -153,10 +151,9 @@ export default (o) => html`
                             </div>
                         </form>
                     </div>
-                    ${ o._converse.pluggable.plugins['converse-omemo'].enabled(o._converse) && omemo_page(o) }
+                    ${ _converse.pluggable.plugins['converse-omemo'].enabled(_converse) && omemo_page(o) }
                 </div>
             </div>
-            <div class="modal-footer">${modal_close_button}</div>
         </div>
     </div>
 `;