wip: out of band avatars
This commit is contained in:
+28
-2
@@ -2066,12 +2066,14 @@ ul.tabbar li.active > a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.load-spinner-box {
|
.load-spinner-box {
|
||||||
|
--size-small: 2.5rem;
|
||||||
|
--size-large: 4rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-shadow: 0.1rem 0.1rem 0.15rem 1px rgba(0,0,0,0.25);
|
box-shadow: 0.1rem 0.1rem 0.15rem 1px rgba(0,0,0,0.25);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
width: 2.5rem;
|
width: var(--size-small);
|
||||||
height: 2.5rem;
|
height: var(--size-small);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 5rem;
|
top: 5rem;
|
||||||
@@ -2081,6 +2083,25 @@ ul.tabbar li.active > a {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-spinner-box.large {
|
||||||
|
width: var(--size-large);
|
||||||
|
height: var(--size-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-spinner-box.clear {
|
||||||
|
-x-box-shadow: none;
|
||||||
|
box-shadow: 0.1rem 0.1rem 0.15rem 1px rgba(0,0,0,0.12);
|
||||||
|
background-color: #fff8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-spinner-box.centered {
|
||||||
|
top: calc(var(--size-small)/2 - 0.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-spinner-box.large.centered {
|
||||||
|
top: calc(var(--size-large)/2 - 0.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
.loader-spinner {
|
.loader-spinner {
|
||||||
border: 0.35rem solid #f3f3f3;
|
border: 0.35rem solid #f3f3f3;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -2090,6 +2111,11 @@ ul.tabbar li.active > a {
|
|||||||
animation: spin 1.5s linear infinite;
|
animation: spin 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-spinner-box.large .loader-spinner {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+6
-3
@@ -45,9 +45,12 @@ export const MEDIA_BREAKPOINT = 640;
|
|||||||
// Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px.
|
// Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px.
|
||||||
export const REM_SIZE = 13;
|
export const REM_SIZE = 13;
|
||||||
|
|
||||||
// Size of the avatar image: When an avatar image is uploaded, it's resized to
|
// Size of the avatar image: when an image is uploaded, it's resized to
|
||||||
// a square of this size.
|
// a square of this size or less.
|
||||||
export const AVATAR_SIZE = 128;
|
export const AVATAR_SIZE = 384;
|
||||||
|
|
||||||
|
// Maximum size of an avatar in bytes for sending in-band. Bigger avatars will be sent out of band (as uploads).
|
||||||
|
export const MAX_AVATAR_BYTES = 8192;
|
||||||
|
|
||||||
// Size of the broken_image shown in MessagesView
|
// Size of the broken_image shown in MessagesView
|
||||||
export const BROKEN_IMAGE_SIZE = 32;
|
export const BROKEN_IMAGE_SIZE = 32;
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export default class AccGeneralView extends React.Component {
|
|||||||
}</FormattedMessage></div>
|
}</FormattedMessage></div>
|
||||||
</div>
|
</div>
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
|
tinode={this.props.tinode}
|
||||||
avatar={this.state.avatar}
|
avatar={this.state.avatar}
|
||||||
uid={this.props.myUserId}
|
uid={this.props.myUserId}
|
||||||
title={this.state.fullName}
|
title={this.state.fullName}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export default class CreateAccountView extends React.PureComponent {
|
|||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
</div>
|
</div>
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
|
tinode={this.props.tinode}
|
||||||
onImageChanged={this.handleImageChanged}
|
onImageChanged={this.handleImageChanged}
|
||||||
onError={this.props.onError} />
|
onError={this.props.onError} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export default class EditAccountView extends React.Component {
|
|||||||
<BadgeList trustedBadges={this.props.trustedBadges} />
|
<BadgeList trustedBadges={this.props.trustedBadges} />
|
||||||
</div>
|
</div>
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
|
tinode={this.props.tinode}
|
||||||
avatar={this.state.avatar}
|
avatar={this.state.avatar}
|
||||||
readOnly={!this.state.owner}
|
readOnly={!this.state.owner}
|
||||||
uid={this.props.myUserId}
|
uid={this.props.myUserId}
|
||||||
|
|||||||
@@ -600,6 +600,7 @@ class InfoView extends React.Component {
|
|||||||
<BadgeList trustedBadges={this.state.trustedBadges} />
|
<BadgeList trustedBadges={this.state.trustedBadges} />
|
||||||
</div>
|
</div>
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
|
tinode={this.props.tinode}
|
||||||
avatar={this.state.avatar}
|
avatar={this.state.avatar}
|
||||||
readOnly={!this.state.owner}
|
readOnly={!this.state.owner}
|
||||||
uid={this.props.topic}
|
uid={this.props.topic}
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ class SidepanelView extends React.Component {
|
|||||||
|
|
||||||
view === 'register' ?
|
view === 'register' ?
|
||||||
<CreateAccountView
|
<CreateAccountView
|
||||||
|
tinode={this.props.tinode}
|
||||||
onCreateAccount={this.props.onCreateAccount}
|
onCreateAccount={this.props.onCreateAccount}
|
||||||
onCancel={this.props.onCancel}
|
onCancel={this.props.onCancel}
|
||||||
onError={this.props.onError} /> :
|
onError={this.props.onError} /> :
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from './letter-tile.jsx';
|
||||||
|
import LoadSpinner from './load-spinner.jsx';
|
||||||
|
|
||||||
import { AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE } from '../config.js';
|
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 { imageScaled, blobToBase64, makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
export default class AvatarUpload extends React.Component {
|
export default class AvatarUpload extends React.Component {
|
||||||
@@ -10,7 +12,8 @@ export default class AvatarUpload extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
dataUrl: props.avatar
|
dataUrl: props.avatar,
|
||||||
|
uploading: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleFileUpload = this.handleFileUpload.bind(this);
|
this.handleFileUpload = this.handleFileUpload.bind(this);
|
||||||
@@ -18,25 +21,44 @@ export default class AvatarUpload extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.avatar != prevProps.avatar) {
|
if (this.props.avatar != prevProps.avatar) {
|
||||||
this.setState({dataUrl: this.props.avatar})
|
this.setState({dataUrl: this.props.avatar});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileUpload(e) {
|
handleFileUpload(e) {
|
||||||
imageScaled(e.target.files[0], AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true,
|
const image = e.target.files[0];
|
||||||
// Image successfully scaled and converted.
|
if (image.size > MAX_AVATAR_BYTES) {
|
||||||
(mime, blob) => {
|
// Too large to send inband - uploading out of band and sending as a link.
|
||||||
// Convert blob to base64-encoded bits.
|
const uploader = this.props.tinode.getLargeFileHelper();
|
||||||
blobToBase64(blob, (unused, base64bits) => {
|
if (!uploader) {
|
||||||
const du = makeImageDataUrl({data: base64bits, type: mime});
|
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
|
||||||
this.setState({dataUrl: du});
|
return;
|
||||||
this.props.onImageChanged(du);
|
}
|
||||||
|
|
||||||
|
uploader.upload(image)
|
||||||
|
.then(() => {})
|
||||||
|
.catch((err) => {
|
||||||
|
this.props.onError(err, 'err');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({uploading: false});
|
||||||
});
|
});
|
||||||
},
|
} else {
|
||||||
// Failure
|
imageScaled(image, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true,
|
||||||
(err) => {
|
// Image successfully scaled and converted.
|
||||||
this.props.onError(err, 'err');
|
(mime, blob) => {
|
||||||
});
|
// Convert blob to base64-encoded bits.
|
||||||
|
blobToBase64(blob, (unused, base64bits) => {
|
||||||
|
const du = makeImageDataUrl({data: base64bits, type: mime});
|
||||||
|
this.setState({dataUrl: 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 = '';
|
||||||
}
|
}
|
||||||
@@ -71,6 +93,7 @@ export default class AvatarUpload extends React.Component {
|
|||||||
<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} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import React from 'react';
|
|||||||
|
|
||||||
export default class LoadSpinner extends React.PureComponent {
|
export default class LoadSpinner extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
|
const className = 'load-spinner-box' + (this.props.large ? ' large' : '')
|
||||||
|
+ (this.props.clear ? ' clear' : '') + (this.props.centered ? ' centered' : '');
|
||||||
return (this.props.show ?
|
return (this.props.show ?
|
||||||
<div className="load-spinner-box"><div className="loader-spinner"></div></div> : null);
|
<div className={className}><div className="loader-spinner"></div></div> : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export default class NewTopicGroup extends React.PureComponent {
|
|||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
</div>
|
</div>
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
|
tinode={this.props.tinode}
|
||||||
onError={this.props.onError}
|
onError={this.props.onError}
|
||||||
onImageChanged={this.handleImageChanged} />
|
onImageChanged={this.handleImageChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+30
-16
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user