Commit 09a79d60 authored by JC Brand's avatar JC Brand

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

parent c82e3e9b
...@@ -198,6 +198,7 @@ ...@@ -198,6 +198,7 @@
"no-unused-expressions": "off", "no-unused-expressions": "off",
"no-use-before-define": "off", "no-use-before-define": "off",
"no-useless-call": "error", "no-useless-call": "error",
"no-useless-catch": "off",
"no-useless-computed-key": "error", "no-useless-computed-key": "error",
"no-useless-concat": "off", "no-useless-concat": "off",
"no-useless-constructor": "error", "no-useless-constructor": "error",
......
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);
...@@ -115,7 +115,7 @@ class Message extends CustomElement { ...@@ -115,7 +115,7 @@ class Message extends CustomElement {
const size = filesize(this.model.file.size); const size = filesize(this.model.file.size);
return html` return html`
<div class="message chat-msg"> <div class="message chat-msg">
${ renderAvatar(this) } ${ renderAvatar(this.getAvatarData()) }
<div class="chat-msg__content"> <div class="chat-msg__content">
<span class="chat-msg__text">${i18n_uploading} <strong>${filename}</strong>, ${size}</span> <span class="chat-msg__text">${i18n_uploading} <strong>${filename}</strong>, ${size}</span>
<progress value="${this.progress}"/> <progress value="${this.progress}"/>
...@@ -132,7 +132,7 @@ class Message extends CustomElement { ...@@ -132,7 +132,7 @@ class Message extends CustomElement {
${this.isFollowup() ? 'chat-msg--followup' : ''}" ${this.isFollowup() ? 'chat-msg--followup' : ''}"
data-isodate="${this.time}" data-msgid="${this.msgid}" data-from="${this.from}" data-encrypted="${this.is_encrypted}"> 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' : ''}"> <div class="chat-msg__content chat-msg__content--${this.sender} ${this.is_me_message ? 'chat-msg__content--action' : ''}">
<span class="chat-msg__heading"> <span class="chat-msg__heading">
${ (this.is_me_message) ? html` ${ (this.is_me_message) ? html`
...@@ -165,6 +165,18 @@ class Message extends CustomElement { ...@@ -165,6 +165,18 @@ class Message extends CustomElement {
</div>`; </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 () { async onRetryClicked () {
this.show_spinner = true; this.show_spinner = true;
await this.model.error.retry(); await this.model.error.retry();
......
...@@ -38,8 +38,6 @@ converse.plugins.add('converse-profile', { ...@@ -38,8 +38,6 @@ converse.plugins.add('converse-profile', {
_converse.ProfileModal = BootstrapModal.extend({ _converse.ProfileModal = BootstrapModal.extend({
id: "user-profile-modal", id: "user-profile-modal",
events: { events: {
'change input[type="file"': "updateFilePreview",
'click .change-avatar': "openFileSelection",
'submit .profile-form': 'onFormSubmitted' 'submit .profile-form': 'onFormSubmitted'
}, },
...@@ -58,29 +56,25 @@ converse.plugins.add('converse-profile', { ...@@ -58,29 +56,25 @@ converse.plugins.add('converse-profile', {
toHTML () { toHTML () {
return tpl_profile_modal(Object.assign( return tpl_profile_modal(Object.assign(
this.model.toJSON(), this.model.toJSON(),
this.model.vcard.toJSON(), { this.model.vcard.toJSON(),
'_converse': _converse, this.getAvatarData(),
'utils': u, { 'view': this }
'view': this ));
}));
},
afterRender () {
this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
}, },
openFileSelection (ev) { getAvatarData () {
ev.preventDefault(); const image_type = this.model.vcard.get('image_type');
this.el.querySelector('input[type="file"]').click(); const image_data = this.model.vcard.get('image');
const image = "data:" + image_type + ";base64," + image_data;
return {
'height': 128,
'width': 128,
image,
};
}, },
updateFilePreview (ev) { afterRender () {
const file = ev.target.files[0], this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
reader = new FileReader();
reader.onloadend = () => {
this.el.querySelector('.avatar').setAttribute('src', reader.result);
};
reader.readAsDataURL(file);
}, },
async setVCard (data) { async setVCard (data) {
...@@ -99,10 +93,9 @@ converse.plugins.add('converse-profile', { ...@@ -99,10 +93,9 @@ converse.plugins.add('converse-profile', {
onFormSubmitted (ev) { onFormSubmitted (ev) {
ev.preventDefault(); ev.preventDefault();
const reader = new FileReader(), const reader = new FileReader();
form_data = new FormData(ev.target), const form_data = new FormData(ev.target);
image_file = form_data.get('image'); const image_file = form_data.get('image');
const data = { const data = {
'fn': form_data.get('fn'), 'fn': form_data.get('fn'),
'nickname': form_data.get('nickname'), 'nickname': form_data.get('nickname'),
......
import tpl_avatar from "templates/avatar.svg";
import xss from "xss/dist/xss"; import xss from "xss/dist/xss";
import { directive, html } from "lit-html"; 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 => { const whitelist_opts = {
if (o.type === 'headline' || o.is_me_message) { 'whiteList': {
part.setValue(''); 'svg': ['xmlns', 'xmlns:xlink', 'class', 'width', 'height'],
return; '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 = { export const renderAvatar = directive(o => part => {
'classes': 'avatar chat-msg__avatar', const data = {
'width': 36, 'classes': o.classes || '',
'height': 36, 'height': o.width || 36,
} 'image': o.image,
const image_type = o.model.vcard.get('image_type'); 'width': o.height || 36,
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))}`);
} }
part.setValue(tpl_avatar(data));
}); });
import { html } from "lit-html"; import "../components/image_picker.js";
import { __ } from '@converse/headless/i18n';
import avatar from "./avatar.js";
import spinner from "./spinner.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 heading_profile = __('Your Profile');
const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following fingerprint'); const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following fingerprint');
const i18n_device_without_fingerprint = __('Device without a fingerprint'); const i18n_device_without_fingerprint = __('Device without a fingerprint');
...@@ -38,7 +39,7 @@ const navigation = html` ...@@ -38,7 +39,7 @@ const navigation = html`
const fingerprint = (o) => 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` const device_with_fingerprint = (o) => html`
...@@ -46,7 +47,7 @@ const device_with_fingerprint = (o) => html` ...@@ -46,7 +47,7 @@ const device_with_fingerprint = (o) => html`
<label> <label>
<input type="checkbox" value="${o.device.get('id')}" <input type="checkbox" value="${o.device.get('id')}"
aria-label="${i18n_fingerprint_checkbox_label}"/> 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> </label>
</li> </li>
`; `;
...@@ -108,16 +109,13 @@ export default (o) => html` ...@@ -108,16 +109,13 @@ export default (o) => html`
</div> </div>
<div class="modal-body"> <div class="modal-body">
<span class="modal-alert"></span> <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-content">
<div class="tab-pane active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab"> <div class="tab-pane active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">
<form class="converse-form converse-form--modal profile-form" action="#"> <form class="converse-form converse-form--modal profile-form" action="#">
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<a class="change-avatar" href="#"> <converse-image-picker image="${o.image}" width="${o.width}" height="${o.height}"></converse-image-picker>
${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"/>
</div> </div>
<div class="col"> <div class="col">
<div class="form-group"> <div class="form-group">
...@@ -153,10 +151,9 @@ export default (o) => html` ...@@ -153,10 +151,9 @@ export default (o) => html`
</div> </div>
</form> </form>
</div> </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> </div>
<div class="modal-footer">${modal_close_button}</div>
</div> </div>
</div> </div>
`; `;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment