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 @@
"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",
......
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 {
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();
......
......@@ -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'),
......
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));
});
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>
`;
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