making uploads work

This commit is contained in:
or-else
2021-07-14 16:06:37 -07:00
parent 8320bf28ce
commit 4219e47529
3 changed files with 74 additions and 60 deletions
+65 -10
View File
@@ -10,8 +10,9 @@ import AvatarUpload from '../widgets/avatar-upload.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx'; import InPlaceEdit from '../widgets/in-place-edit.jsx';
import TagManager from '../widgets/tag-manager.jsx'; import TagManager from '../widgets/tag-manager.jsx';
import { MAX_TITLE_LENGTH } from '../config.js'; import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
import { makeImageDataUrl } from '../lib/blob-helpers.js'; 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'; import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js';
export default class AccGeneralView extends React.Component { 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.tnNewTags = this.tnNewTags.bind(this);
this.tnCredsUpdated = this.tnCredsUpdated.bind(this); this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
this.handleFullNameUpdate = this.handleFullNameUpdate.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.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.handleCredChange = this.handleCredChange.bind(this);
this.handleCredKeyDown = this.handleCredKeyDown.bind(this); this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
this.handleCredEntered = this.handleCredEntered.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}); 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) { handleAvatarCropped(mime, blob, width, height) {
const url = blob ? URL.createObjectURL(blob) : null; const url = blob ? URL.createObjectURL(blob) : null;
this.setState({avatar: url, newAvatar: null, newAvatarMime: 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}); this.setState({newAvatar: null, newAvatarMime: null});
} }
@@ -140,7 +196,7 @@ export default class AccGeneralView extends React.Component {
avatar={this.state.newAvatar} avatar={this.state.newAvatar}
mime={this.state.newAvatarMime} mime={this.state.newAvatarMime}
onSubmit={this.handleAvatarCropped} onSubmit={this.handleAvatarCropped}
onCancel={this.handleCancel} onCancel={this.handleAvatarCropCancel}
onError={this.props.onError} /> onError={this.props.onError} />
); );
} }
@@ -176,8 +232,7 @@ export default class AccGeneralView extends React.Component {
avatar={this.state.avatar} avatar={this.state.avatar}
uid={this.props.myUserId} uid={this.props.myUserId}
title={this.state.fullName} title={this.state.fullName}
onImageReceived={this.handleImageUploaded} onImageUpdated={this.handleImageUpdated}
onImageChanged={this.handleImageChanged}
onError={this.props.onError} /> onError={this.props.onError} />
</div> </div>
<div className="hr" /> <div className="hr" />
+1 -1
View File
@@ -605,7 +605,7 @@ class InfoView extends React.Component {
readOnly={!this.state.owner} readOnly={!this.state.owner}
uid={this.props.topic} uid={this.props.topic}
title={this.state.fullName} title={this.state.fullName}
onImageChanged={this.handleImageChanged} onImageUpdated={this.handleImageChanged}
onError={this.props.onError} /> onError={this.props.onError} />
</div> </div>
<div className="hr" /> <div className="hr" />
+8 -49
View File
@@ -3,20 +3,17 @@ import React from 'react';
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import LoadSpinner from './load-spinner.jsx'; import LoadSpinner from './load-spinner.jsx';
import { AVATAR_SIZE, IMAGE_PREVIEW_DIM, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, import { AVATAR_SIZE } from '../config.js';
MAX_INBAND_ATTACHMENT_SIZE } from '../config.js';
import { imageScaled, blobToBase64, makeImageDataUrl } from '../lib/blob-helpers.js';
export default class AvatarUpload extends React.Component { export default class AvatarUpload extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
source: props.avatar, source: props.avatar
uploading: false
}; };
this.handleFileUpload = this.handleFileUpload.bind(this); this.handleFileReceived = this.handleFileReceived.bind(this);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -25,47 +22,9 @@ export default class AvatarUpload extends React.Component {
} }
} }
handleFileUpload(e) { handleFileReceived(e) {
const image = e.target.files[0]; const image = e.target.files[0];
if (this.props.onImageReceived) { this.props.onImageUpdated(image.type, URL.createObjectURL(image), image.name);
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');
});
}
// Clear the value so the same file can be uploaded again. // Clear the value so the same file can be uploaded again.
e.target.value = ''; e.target.value = '';
} }
@@ -79,7 +38,7 @@ export default class AvatarUpload extends React.Component {
<div className={className}> <div className={className}>
{this.props.readOnly || !this.state.source ? {this.props.readOnly || !this.state.source ?
null : null :
<a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageChanged(null);}}> <a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageUpdated();}}>
<i className="material-icons">clear</i> <i className="material-icons">clear</i>
</a>} </a>}
{this.state.source ? {this.state.source ?
@@ -95,12 +54,12 @@ export default class AvatarUpload extends React.Component {
<div className="blank">{AVATAR_SIZE}&times;{AVATAR_SIZE}</div>} <div className="blank">{AVATAR_SIZE}&times;{AVATAR_SIZE}</div>}
{this.props.readOnly ? null : {this.props.readOnly ? null :
<input type="file" id={randId} className="inputfile hidden" <input type="file" id={randId} className="inputfile hidden"
accept="image/*" onChange={this.handleFileUpload} />} accept="image/*" onChange={this.handleFileReceived} />}
{this.props.readOnly ? null : {this.props.readOnly ? null :
<label htmlFor={randId} className="round"> <label htmlFor={randId} className="round">
<i className="material-icons">file_upload</i> <i className="material-icons">file_upload</i>
</label>} </label>}
<LoadSpinner show={this.state.uploading} large={true} clear={true} centered={true} /> <LoadSpinner show={this.props.uploading} large={true} clear={true} centered={true} />
</div> </div>
); );
} }