diff --git a/src/views/acc-general-view.jsx b/src/views/acc-general-view.jsx index 71551641..6479ab63 100644 --- a/src/views/acc-general-view.jsx +++ b/src/views/acc-general-view.jsx @@ -10,8 +10,9 @@ import AvatarUpload from '../widgets/avatar-upload.jsx'; import InPlaceEdit from '../widgets/in-place-edit.jsx'; import TagManager from '../widgets/tag-manager.jsx'; -import { MAX_TITLE_LENGTH } from '../config.js'; -import { makeImageDataUrl } from '../lib/blob-helpers.js'; +import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, + MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH } from '../config.js'; +import { imageScaled, blobToBase64, makeImageDataUrl } from '../lib/blob-helpers.js'; import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js'; export default class AccGeneralView extends React.Component { @@ -35,9 +36,10 @@ export default class AccGeneralView extends React.Component { this.tnNewTags = this.tnNewTags.bind(this); this.tnCredsUpdated = this.tnCredsUpdated.bind(this); this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this); - this.handleImageUploaded = this.handleImageUploaded.bind(this); + this.handleImageUpdated = this.handleImageUpdated.bind(this); this.handleAvatarCropped = this.handleAvatarCropped.bind(this); - this.handleCancel = this.handleCancel.bind(this); + this.uploadAvatar = this.uploadAvatar.bind(this); + this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this); this.handleCredChange = this.handleCredChange.bind(this); this.handleCredKeyDown = this.handleCredKeyDown.bind(this); this.handleCredEntered = this.handleCredEntered.bind(this); @@ -72,17 +74,71 @@ export default class AccGeneralView extends React.Component { } } - handleImageUploaded(mime, img) { + // AvatarUpload widget has changed the avatar: received a new file or deleted the avatar. + handleImageUpdated(mime, img) { + // This will show the AvatarCropView is img is not null. this.setState({newAvatar: img, newAvatarMime: mime}); + if (!img) { + // Deleting the avatar. + this.props.onUpdateAccount(undefined, theCard(null, Tinode.DEL_CHAR)); + } } + // AvatarCropView calls this method when the user has cropped the image. handleAvatarCropped(mime, blob, width, height) { const url = blob ? URL.createObjectURL(blob) : null; this.setState({avatar: url, newAvatar: null, newAvatarMime: null}); - // this.props.onUpdateAccount(undefined, theCard(null, img || Tinode.DEL_CHAR)); + if (blob) { + this.uploadAvatar(mime, blob, width, height); + } } - handleCancel(img) { + // Utility method for converting cropped avatar blob to bytes for sending inband or + // for uploading it to the server out of band. + uploadAvatar(mime, blob, width, height) { + const readyToUpload = (mime, blob, width, height) => { + if (blob.size > MAX_AVATAR_BYTES) { + // Too large to send inband - uploading out of band and sending as a link. + const uploader = this.props.tinode.getLargeFileHelper(); + if (!uploader) { + this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload)); + return; + } + + this.setState({uploading: true}); + uploader.upload(blob) + .then((url) => { + this.props.onUpdateAccount(undefined, theCard(null, url)); + }) + .catch((err) => { + this.props.onError(err, 'err'); + }) + .finally(() => { + this.setState({uploading: false}); + }); + } else { + // Convert blob to base64-encoded bits. + blobToBase64(blob, (unused, base64bits) => { + const du = makeImageDataUrl({data: base64bits, type: mime}); + this.setState({source: du}); + this.props.onUpdateAccount(undefined, theCard(null, du)); + }); + } + }; + + if (width > AVATAR_SIZE || height > AVATAR_SIZE) { + // Avatar is too large even after scaling. Shring it and make square. + imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true, + readyToUpload, + (err) => { + this.props.onError(err, 'err'); + }); + } else { + readyToUpload(mime, blob, width, height); + } + } + + handleAvatarCropCancel(img) { this.setState({newAvatar: null, newAvatarMime: null}); } @@ -140,7 +196,7 @@ export default class AccGeneralView extends React.Component { avatar={this.state.newAvatar} mime={this.state.newAvatarMime} onSubmit={this.handleAvatarCropped} - onCancel={this.handleCancel} + onCancel={this.handleAvatarCropCancel} onError={this.props.onError} /> ); } @@ -176,8 +232,7 @@ export default class AccGeneralView extends React.Component { avatar={this.state.avatar} uid={this.props.myUserId} title={this.state.fullName} - onImageReceived={this.handleImageUploaded} - onImageChanged={this.handleImageChanged} + onImageUpdated={this.handleImageUpdated} onError={this.props.onError} />
diff --git a/src/views/info-view.jsx b/src/views/info-view.jsx index 70b950e8..56e6b198 100644 --- a/src/views/info-view.jsx +++ b/src/views/info-view.jsx @@ -605,7 +605,7 @@ class InfoView extends React.Component { readOnly={!this.state.owner} uid={this.props.topic} title={this.state.fullName} - onImageChanged={this.handleImageChanged} + onImageUpdated={this.handleImageChanged} onError={this.props.onError} />
diff --git a/src/widgets/avatar-upload.jsx b/src/widgets/avatar-upload.jsx index 0313746d..d7d7ea0b 100644 --- a/src/widgets/avatar-upload.jsx +++ b/src/widgets/avatar-upload.jsx @@ -3,20 +3,17 @@ import React from 'react'; import LetterTile from './letter-tile.jsx'; import LoadSpinner from './load-spinner.jsx'; -import { AVATAR_SIZE, IMAGE_PREVIEW_DIM, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, - MAX_INBAND_ATTACHMENT_SIZE } from '../config.js'; -import { imageScaled, blobToBase64, makeImageDataUrl } from '../lib/blob-helpers.js'; +import { AVATAR_SIZE } from '../config.js'; export default class AvatarUpload extends React.Component { constructor(props) { super(props); this.state = { - source: props.avatar, - uploading: false + source: props.avatar }; - this.handleFileUpload = this.handleFileUpload.bind(this); + this.handleFileReceived = this.handleFileReceived.bind(this); } componentDidUpdate(prevProps) { @@ -25,47 +22,9 @@ export default class AvatarUpload extends React.Component { } } - handleFileUpload(e) { + handleFileReceived(e) { const image = e.target.files[0]; - if (this.props.onImageReceived) { - this.props.onImageReceived(image.type, URL.createObjectURL(image)); - return; - } - - if (image.size > MAX_AVATAR_BYTES) { - // Too large to send inband - uploading out of band and sending as a link. - const uploader = this.props.tinode.getLargeFileHelper(); - if (!uploader) { - this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload)); - return; - } - - uploader.upload(image) - .then((url) => { - this.props.onImageChanged(url); - }) - .catch((err) => { - this.props.onError(err, 'err'); - }) - .finally(() => { - this.setState({uploading: false}); - }); - } else { - imageScaled(image, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true, - // Image successfully scaled and converted. - (mime, blob) => { - // Convert blob to base64-encoded bits. - blobToBase64(blob, (unused, base64bits) => { - const du = makeImageDataUrl({data: base64bits, type: mime}); - this.setState({source: du}); - this.props.onImageChanged(du); - }); - }, - // Failure - (err) => { - this.props.onError(err, 'err'); - }); - } + this.props.onImageUpdated(image.type, URL.createObjectURL(image), image.name); // Clear the value so the same file can be uploaded again. e.target.value = ''; } @@ -79,7 +38,7 @@ export default class AvatarUpload extends React.Component {
{this.props.readOnly || !this.state.source ? null : - {e.preventDefault(); this.props.onImageChanged(null);}}> + {e.preventDefault(); this.props.onImageUpdated();}}> clear } {this.state.source ? @@ -95,12 +54,12 @@ export default class AvatarUpload extends React.Component {
{AVATAR_SIZE}×{AVATAR_SIZE}
} {this.props.readOnly ? null : } + accept="image/*" onChange={this.handleFileReceived} />} {this.props.readOnly ? null : } - +
); }