From 09a79d609fc93028d76e111d611f6f070325cbbf Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 3 Jun 2020 13:53:52 +0200 Subject: [PATCH] Create an image picker component and use it in the profile modal --- .eslintrc.json | 1 + src/components/image_picker.js | 46 ++++++++++++++++++++++++++++++ src/components/message.js | 16 +++++++++-- src/converse-profile.js | 43 ++++++++++++---------------- src/templates/directives/avatar.js | 46 ++++++++++++++---------------- src/templates/profile_modal.js | 25 +++++++--------- 6 files changed, 112 insertions(+), 65 deletions(-) create mode 100644 src/components/image_picker.js diff --git a/.eslintrc.json b/.eslintrc.json index ea8f2c230..1b7fb33fa 100644 --- a/.eslintrc.json +++ b/.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", diff --git a/src/components/image_picker.js b/src/components/image_picker.js new file mode 100644 index 000000000..7deb54f78 --- /dev/null +++ b/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` + + ${ renderAvatar(avatar_data) } + + + `; + } + + 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); diff --git a/src/components/message.js b/src/components/message.js index c8726e939..cc728d780 100644 --- a/src/components/message.js +++ b/src/components/message.js @@ -115,7 +115,7 @@ class Message extends CustomElement { const size = filesize(this.model.file.size); return html`
- ${ renderAvatar(this) } + ${ renderAvatar(this.getAvatarData()) }
${i18n_uploading} ${filename}, ${size} @@ -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()) }
${ (this.is_me_message) ? html` @@ -165,6 +165,18 @@ class Message extends CustomElement {
`; } + 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(); diff --git a/src/converse-profile.js b/src/converse-profile.js index 2b2b0559a..9fc244a6e 100644 --- a/src/converse-profile.js +++ b/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,31 +56,27 @@ 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 - })); + this.model.vcard.toJSON(), + this.getAvatarData(), + { 'view': this } + )); + }, + + 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, + }; }, afterRender () { this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e)); }, - 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); - }, - async setVCard (data) { try { await api.vcard.set(_converse.bare_jid, 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'), diff --git a/src/templates/directives/avatar.js b/src/templates/directives/avatar.js index cc67da8db..df20ae3d7 100644 --- a/src/templates/directives/avatar.js +++ b/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'; + + +const whitelist_opts = { + 'whiteList': { + 'svg': ['xmlns', 'xmlns:xlink', 'class', 'width', 'height'], + 'image': ['width', 'height', 'preserveAspectRatio', 'xlink:href'] + } +}; +const tpl_svg = (o) => xss.filterXSS(``, whitelist_opts); + +const tpl_avatar = (o) => html` + + ${ unsafeSVG(tpl_svg(o)) } + +`; export const renderAvatar = directive(o => part => { - if (o.type === 'headline' || o.is_me_message) { - part.setValue(''); - return; - } - - 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))}`); + const data = { + 'classes': o.classes || '', + 'height': o.width || 36, + 'image': o.image, + 'width': o.height || 36, } + part.setValue(tpl_avatar(data)); }); diff --git a/src/templates/profile_modal.js b/src/templates/profile_modal.js index d8727a07b..5e3b0a322 100644 --- a/src/templates/profile_modal.js +++ b/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` - ${o.utils.formatFingerprint(o.view.current_device.get('bundle').fingerprint)}`; + ${u.formatFingerprint(o.view.current_device.get('bundle').fingerprint)}`; const device_with_fingerprint = (o) => html` @@ -46,7 +47,7 @@ const device_with_fingerprint = (o) => html` `; @@ -108,16 +109,13 @@ export default (o) => html`
`;