PLT-6277 Moved profile image cropping to server (#6269)
* PLT-6277 Moved profile image cropping to server * Cosmetic refactoring of SettingPicture component
Этот коммит содержится в:
коммит произвёл
Christopher Speller
родитель
b509f65b8e
Коммит
1c8fdb4cdd
@@ -805,7 +805,7 @@ func SetProfileImage(userId string, imageData *multipart.FileHeader) *model.AppE
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scale profile image
|
// Scale profile image
|
||||||
img = imaging.Resize(img, utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, imaging.Lanczos)
|
img = imaging.Fill(img, utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, imaging.Center, imaging.Lanczos)
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err = png.Encode(buf, img)
|
err = png.Encode(buf, img)
|
||||||
|
|||||||
@@ -1,74 +1,63 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import React, {Component, PropTypes} from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import FormError from 'components/form_error.jsx';
|
||||||
|
|
||||||
import loadingGif from 'images/load.gif';
|
import loadingGif from 'images/load.gif';
|
||||||
|
|
||||||
import React from 'react';
|
export default class SettingPicture extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
clientError: PropTypes.string,
|
||||||
|
serverError: PropTypes.string,
|
||||||
|
src: PropTypes.string,
|
||||||
|
file: PropTypes.object,
|
||||||
|
loadingPicture: PropTypes.bool,
|
||||||
|
submitActive: PropTypes.bool,
|
||||||
|
submit: PropTypes.func,
|
||||||
|
title: PropTypes.string,
|
||||||
|
onFileChange: PropTypes.func,
|
||||||
|
updateSection: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
export default class SettingPicture extends React.Component {
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.setPicture = this.setPicture.bind(this);
|
this.state = {
|
||||||
this.confirmImage = this.confirmImage.bind(this);
|
image: null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPicture(file) {
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.file !== this.props.file) {
|
||||||
|
this.setState({image: null});
|
||||||
|
|
||||||
|
this.setPicture(nextProps.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPicture = (file) => {
|
||||||
if (file) {
|
if (file) {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const canvas = this.refs.profileImageCanvas;
|
this.setState({
|
||||||
const context = canvas.getContext('2d');
|
image: e.target.result
|
||||||
const imageObj = new Image();
|
});
|
||||||
|
|
||||||
imageObj.onload = () => {
|
|
||||||
if (imageObj.width > imageObj.height) {
|
|
||||||
const side = imageObj.height;
|
|
||||||
const rem = imageObj.width - side;
|
|
||||||
const startX = parseInt(rem / 2, 10);
|
|
||||||
context.drawImage(imageObj, startX, 0, side, side,
|
|
||||||
0, 0, canvas.width, canvas.height);
|
|
||||||
} else {
|
|
||||||
const side = imageObj.width;
|
|
||||||
const rem = imageObj.height - side;
|
|
||||||
const startY = parseInt(rem / 2, 10);
|
|
||||||
context.drawImage(imageObj, 0, startY, side, side,
|
|
||||||
0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
imageObj.src = e.target.result;
|
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.picture) {
|
|
||||||
this.setPicture(nextProps.picture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var clientError = null;
|
let img;
|
||||||
if (this.props.client_error) {
|
if (this.props.file) {
|
||||||
clientError = <div className='form-group has-error'><label className='control-label'>{this.props.client_error}</label></div>;
|
|
||||||
}
|
|
||||||
var serverError = null;
|
|
||||||
if (this.props.server_error) {
|
|
||||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.props.server_error}</label></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
var img = null;
|
|
||||||
if (this.props.picture) {
|
|
||||||
img = (
|
img = (
|
||||||
<canvas
|
<div
|
||||||
ref='profileImageCanvas'
|
className='profile-img-preview'
|
||||||
className='profile-img'
|
style={{backgroundImage: 'url(' + this.state.image + ')'}}
|
||||||
width='256px'
|
|
||||||
height='256px'
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -81,7 +70,7 @@ export default class SettingPicture extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var confirmButton;
|
let confirmButton;
|
||||||
if (this.props.loadingPicture) {
|
if (this.props.loadingPicture) {
|
||||||
confirmButton = (
|
confirmButton = (
|
||||||
<img
|
<img
|
||||||
@@ -90,7 +79,7 @@ export default class SettingPicture extends React.Component {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var confirmButtonClass = 'btn btn-sm';
|
let confirmButtonClass = 'btn btn-sm';
|
||||||
if (this.props.submitActive) {
|
if (this.props.submitActive) {
|
||||||
confirmButtonClass += ' btn-primary';
|
confirmButtonClass += ' btn-primary';
|
||||||
} else {
|
} else {
|
||||||
@@ -100,7 +89,7 @@ export default class SettingPicture extends React.Component {
|
|||||||
confirmButton = (
|
confirmButton = (
|
||||||
<a
|
<a
|
||||||
className={confirmButtonClass}
|
className={confirmButtonClass}
|
||||||
onClick={this.confirmImage}
|
onClick={this.props.submit}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='setting_picture.save'
|
id='setting_picture.save'
|
||||||
@@ -109,18 +98,7 @@ export default class SettingPicture extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
var helpText = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='setting_picture.help'
|
|
||||||
defaultMessage='Upload a profile picture in BMP, JPG, JPEG or PNG format, at least {width}px in width and {height}px height.'
|
|
||||||
values={{
|
|
||||||
width: global.window.mm_config.ProfileWidth,
|
|
||||||
height: global.window.mm_config.ProfileHeight
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
return (
|
return (
|
||||||
<ul className='section-max'>
|
<ul className='section-max'>
|
||||||
<li className='col-xs-12 section-title'>{this.props.title}</li>
|
<li className='col-xs-12 section-title'>{this.props.title}</li>
|
||||||
@@ -130,11 +108,17 @@ export default class SettingPicture extends React.Component {
|
|||||||
{img}
|
{img}
|
||||||
</li>
|
</li>
|
||||||
<li className='setting-list-item'>
|
<li className='setting-list-item'>
|
||||||
{helpText}
|
<FormattedMessage
|
||||||
|
id='setting_picture.help'
|
||||||
|
defaultMessage='Upload a profile picture in BMP, JPG, JPEG or PNG format, at least {width}px in width and {height}px height.'
|
||||||
|
values={{
|
||||||
|
width: global.mm_config.ProfileWidth,
|
||||||
|
height: global.mm_config.ProfileHeight
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li className='setting-list-item'>
|
<li className='setting-list-item'>
|
||||||
{serverError}
|
<FormError errors={[this.props.clientError, this.props.serverError]}/>
|
||||||
{clientError}
|
|
||||||
<span className='btn btn-sm btn-primary btn-file sel-btn'>
|
<span className='btn btn-sm btn-primary btn-file sel-btn'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='setting_picture.select'
|
id='setting_picture.select'
|
||||||
@@ -144,14 +128,14 @@ export default class SettingPicture extends React.Component {
|
|||||||
ref='input'
|
ref='input'
|
||||||
accept='.jpg,.png,.bmp'
|
accept='.jpg,.png,.bmp'
|
||||||
type='file'
|
type='file'
|
||||||
onChange={this.props.pictureChange}
|
onChange={this.props.onFileChange}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{confirmButton}
|
{confirmButton}
|
||||||
<a
|
<a
|
||||||
className='btn btn-sm theme'
|
className='btn btn-sm theme'
|
||||||
href='#'
|
href='#'
|
||||||
onClick={self.props.updateSection}
|
onClick={this.props.updateSection}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='setting_picture.cancel'
|
id='setting_picture.cancel'
|
||||||
@@ -164,27 +148,4 @@ export default class SettingPicture extends React.Component {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmImage(e) {
|
|
||||||
e.persist();
|
|
||||||
this.refs.profileImageCanvas.toBlob((blob) => {
|
|
||||||
blob.lastModifiedDate = new Date();
|
|
||||||
blob.name = 'image.jpg';
|
|
||||||
this.props.imageCropChange(blob);
|
|
||||||
this.props.submit(e);
|
|
||||||
}, 'image/jpeg', 0.95);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingPicture.propTypes = {
|
|
||||||
client_error: React.PropTypes.string,
|
|
||||||
server_error: React.PropTypes.string,
|
|
||||||
src: React.PropTypes.string,
|
|
||||||
picture: React.PropTypes.object,
|
|
||||||
loadingPicture: React.PropTypes.bool,
|
|
||||||
submitActive: React.PropTypes.bool,
|
|
||||||
submit: React.PropTypes.func,
|
|
||||||
title: React.PropTypes.string,
|
|
||||||
pictureChange: React.PropTypes.func,
|
|
||||||
imageCropChange: React.PropTypes.func
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
this.updatePicture = this.updatePicture.bind(this);
|
this.updatePicture = this.updatePicture.bind(this);
|
||||||
this.updateSection = this.updateSection.bind(this);
|
this.updateSection = this.updateSection.bind(this);
|
||||||
this.updatePosition = this.updatePosition.bind(this);
|
this.updatePosition = this.updatePosition.bind(this);
|
||||||
this.updatedCroppedPicture = this.updatedCroppedPicture.bind(this);
|
|
||||||
|
|
||||||
this.state = this.setupInitialState(props);
|
this.state = this.setupInitialState(props);
|
||||||
}
|
}
|
||||||
@@ -241,7 +240,7 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
submitPicture(e) {
|
submitPicture(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.state.picture) {
|
if (!this.state.pictureFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,12 +251,12 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
trackEvent('settings', 'user_settings_update', {field: 'picture'});
|
trackEvent('settings', 'user_settings_update', {field: 'picture'});
|
||||||
|
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
const picture = this.state.picture;
|
const file = this.state.pictureFile;
|
||||||
|
|
||||||
if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') {
|
if (file.type !== 'image/jpeg' && file.type !== 'image/png') {
|
||||||
this.setState({clientError: formatMessage(holders.validImage)});
|
this.setState({clientError: formatMessage(holders.validImage)});
|
||||||
return;
|
return;
|
||||||
} else if (picture.size > this.state.maxFileSize) {
|
} else if (file.size > this.state.maxFileSize) {
|
||||||
this.setState({clientError: formatMessage(holders.imageTooLarge)});
|
this.setState({clientError: formatMessage(holders.imageTooLarge)});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -265,7 +264,7 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
this.setState({loadingPicture: true});
|
this.setState({loadingPicture: true});
|
||||||
|
|
||||||
uploadProfileImage(
|
uploadProfileImage(
|
||||||
picture,
|
file,
|
||||||
() => {
|
() => {
|
||||||
this.updateSection('');
|
this.updateSection('');
|
||||||
this.submitActive = false;
|
this.submitActive = false;
|
||||||
@@ -324,25 +323,14 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
this.setState({confirmEmail: e.target.value});
|
this.setState({confirmEmail: e.target.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedCroppedPicture(file) {
|
|
||||||
if (file) {
|
|
||||||
this.setState({picture: file});
|
|
||||||
|
|
||||||
this.submitActive = true;
|
|
||||||
this.setState({clientError: null});
|
|
||||||
} else {
|
|
||||||
this.setState({picture: null});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePicture(e) {
|
updatePicture(e) {
|
||||||
if (e.target.files && e.target.files[0]) {
|
if (e.target.files && e.target.files[0]) {
|
||||||
this.setState({picture: e.target.files[0]});
|
this.setState({pictureFile: e.target.files[0]});
|
||||||
|
|
||||||
this.submitActive = true;
|
this.submitActive = true;
|
||||||
this.setState({clientError: null});
|
this.setState({clientError: null});
|
||||||
} else {
|
} else {
|
||||||
this.setState({picture: null});
|
this.setState({pictureFile: null});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +356,7 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
originalEmail: user.email,
|
originalEmail: user.email,
|
||||||
email: '',
|
email: '',
|
||||||
confirmEmail: '',
|
confirmEmail: '',
|
||||||
picture: null,
|
pictureFile: null,
|
||||||
loadingPicture: false,
|
loadingPicture: false,
|
||||||
emailChangeInProgress: false,
|
emailChangeInProgress: false,
|
||||||
maxFileSize: global.window.mm_config.MaxFileSize
|
maxFileSize: global.window.mm_config.MaxFileSize
|
||||||
@@ -1124,17 +1112,16 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
title={formatMessage(holders.profilePicture)}
|
title={formatMessage(holders.profilePicture)}
|
||||||
submit={this.submitPicture}
|
submit={this.submitPicture}
|
||||||
src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.last_picture_update}
|
src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.last_picture_update}
|
||||||
server_error={serverError}
|
serverError={serverError}
|
||||||
client_error={clientError}
|
clientError={clientError}
|
||||||
updateSection={(e) => {
|
updateSection={(e) => {
|
||||||
this.updateSection('');
|
this.updateSection('');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
picture={this.state.picture}
|
file={this.state.pictureFile}
|
||||||
pictureChange={this.updatePicture}
|
onFileChange={this.updatePicture}
|
||||||
submitActive={this.submitActive}
|
submitActive={this.submitActive}
|
||||||
loadingPicture={this.state.loadingPicture}
|
loadingPicture={this.state.loadingPicture}
|
||||||
imageCropChange={this.updatedCroppedPicture}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -95,6 +95,14 @@
|
|||||||
width: 128px;
|
width: 128px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-img-preview {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-size: cover;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 128px;
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-table {
|
.settings-table {
|
||||||
display: table;
|
display: table;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user