Avatar upload
This commit is contained in:
@ -3,7 +3,7 @@ import { Change } from 'slate';
|
|||||||
import { Editor } from 'slate-react';
|
import { Editor } from 'slate-react';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
import EditList from './plugins/EditList';
|
import EditList from './plugins/EditList';
|
||||||
import uploadFile from 'utils/uploadFile';
|
import { uploadFile } from 'utils/uploadFile';
|
||||||
|
|
||||||
const { changes } = EditList;
|
const { changes } = EditList;
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import { color, size } from 'shared/styles/constants';
|
|||||||
import { client } from 'utils/ApiClient';
|
import { client } from 'utils/ApiClient';
|
||||||
import AuthStore from 'stores/AuthStore';
|
import AuthStore from 'stores/AuthStore';
|
||||||
import ErrorsStore from 'stores/ErrorsStore';
|
import ErrorsStore from 'stores/ErrorsStore';
|
||||||
|
import ImageUpload from './components/ImageUpload';
|
||||||
import Input, { LabelText } from 'components/Input';
|
import Input, { LabelText } from 'components/Input';
|
||||||
import Button from 'components/Button';
|
import Button from 'components/Button';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
@ -24,6 +25,7 @@ class Settings extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@observable name: string;
|
@observable name: string;
|
||||||
|
@observable avatarUrl: ?string;
|
||||||
@observable updated: boolean;
|
@observable updated: boolean;
|
||||||
@observable isSaving: boolean;
|
@observable isSaving: boolean;
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ class Settings extends Component {
|
|||||||
try {
|
try {
|
||||||
const res = await client.post(`/user.update`, {
|
const res = await client.post(`/user.update`, {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
avatarUrl: this.avatarUrl,
|
||||||
});
|
});
|
||||||
invariant(res && res.data, 'Document list not available');
|
invariant(res && res.data, 'Document list not available');
|
||||||
const { data } = res;
|
const { data } = res;
|
||||||
@ -63,10 +66,18 @@ class Settings extends Component {
|
|||||||
this.name = ev.target.value;
|
this.name = ev.target.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleAvatarUpload = (avatarUrl: string) => {
|
||||||
|
this.avatarUrl = avatarUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAvatarError = (error: ?string) => {
|
||||||
|
this.props.errors.add(error || 'Unable to upload new avatar');
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user } = this.props.auth;
|
const { user } = this.props.auth;
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const avatarUrl = user.avatarUrl;
|
const avatarUrl = this.avatarUrl || user.avatarUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent>
|
<CenteredContent>
|
||||||
@ -75,14 +86,19 @@ class Settings extends Component {
|
|||||||
<ProfilePicture column>
|
<ProfilePicture column>
|
||||||
<LabelText>Profile picture</LabelText>
|
<LabelText>Profile picture</LabelText>
|
||||||
<AvatarContainer>
|
<AvatarContainer>
|
||||||
<Avatar src={avatarUrl} />
|
<ImageUpload
|
||||||
<Flex auto align="center" justify="center">
|
onSuccess={this.handleAvatarUpload}
|
||||||
Upload new image
|
onError={this.handleAvatarError}
|
||||||
</Flex>
|
>
|
||||||
|
<Avatar src={avatarUrl} />
|
||||||
|
<Flex auto align="center" justify="center">
|
||||||
|
Upload new image
|
||||||
|
</Flex>
|
||||||
|
</ImageUpload>
|
||||||
</AvatarContainer>
|
</AvatarContainer>
|
||||||
</ProfilePicture>
|
</ProfilePicture>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<Input
|
<StyledInput
|
||||||
label="Name"
|
label="Name"
|
||||||
value={this.name}
|
value={this.name}
|
||||||
onChange={this.handleNameChange}
|
onChange={this.handleNameChange}
|
||||||
@ -122,7 +138,7 @@ const AvatarContainer = styled(Flex)`
|
|||||||
${avatarStyles};
|
${avatarStyles};
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
div {
|
div div {
|
||||||
${avatarStyles};
|
${avatarStyles};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -145,4 +161,8 @@ const Avatar = styled.img`
|
|||||||
${avatarStyles};
|
${avatarStyles};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledInput = styled(Input)`
|
||||||
|
max-width: 350px;
|
||||||
|
`;
|
||||||
|
|
||||||
export default inject('auth', 'errors', 'auth')(Settings);
|
export default inject('auth', 'errors', 'auth')(Settings);
|
||||||
|
143
app/scenes/Settings/components/ImageUpload.js
Normal file
143
app/scenes/Settings/components/ImageUpload.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Dropzone from 'react-dropzone';
|
||||||
|
|
||||||
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
|
import Flex from 'shared/components/Flex';
|
||||||
|
import Modal from 'components/Modal';
|
||||||
|
import Button from 'components/Button';
|
||||||
|
import AvatarEditor from 'react-avatar-editor';
|
||||||
|
import { uploadFile, dataUrlToBlob } from 'utils/uploadFile';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children?: React$Element<any>,
|
||||||
|
onSuccess: string => void,
|
||||||
|
onError: string => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class DropToImport extends Component {
|
||||||
|
@observable isUploading: boolean = false;
|
||||||
|
@observable isCropping: boolean = false;
|
||||||
|
@observable zoom: number = 1;
|
||||||
|
props: Props;
|
||||||
|
file: File;
|
||||||
|
avatarEditorRef: HTMLCanvasElement;
|
||||||
|
|
||||||
|
onDropAccepted = async (files: File[]) => {
|
||||||
|
this.isCropping = true;
|
||||||
|
this.file = files[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCrop = async () => {
|
||||||
|
// $FlowIssue getImage() exists
|
||||||
|
const canvas = this.avatarEditorRef.getImage();
|
||||||
|
const imageBlob = dataUrlToBlob(canvas.toDataURL());
|
||||||
|
try {
|
||||||
|
const asset = await uploadFile(imageBlob, { name: this.file.name });
|
||||||
|
this.props.onSuccess(asset.url);
|
||||||
|
} catch (err) {
|
||||||
|
this.props.onError('Unable to upload image');
|
||||||
|
} finally {
|
||||||
|
this.isUploading = false;
|
||||||
|
this.isCropping = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleZoom = (event: SyntheticDragEvent) => {
|
||||||
|
let target = event.target;
|
||||||
|
if (target instanceof HTMLInputElement) {
|
||||||
|
this.zoom = parseFloat(target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderCropping() {
|
||||||
|
return (
|
||||||
|
<Modal isOpen title="">
|
||||||
|
<Flex auto column align="center" justify="center">
|
||||||
|
<AvatarEditorContainer>
|
||||||
|
<AvatarEditor
|
||||||
|
ref={ref => (this.avatarEditorRef = ref)}
|
||||||
|
image={this.file}
|
||||||
|
width={250}
|
||||||
|
height={250}
|
||||||
|
border={25}
|
||||||
|
borderRadius={150}
|
||||||
|
color={[255, 255, 255, 0.6]} // RGBA
|
||||||
|
scale={this.zoom}
|
||||||
|
rotate={0}
|
||||||
|
/>
|
||||||
|
</AvatarEditorContainer>
|
||||||
|
<RangeInput
|
||||||
|
type="range"
|
||||||
|
min="0.1"
|
||||||
|
max="2"
|
||||||
|
step="0.01"
|
||||||
|
defaultValue="1"
|
||||||
|
onChange={this.handleZoom}
|
||||||
|
/>
|
||||||
|
{this.isUploading && <LoadingIndicator />}
|
||||||
|
<CropButton onClick={this.handleCrop} disabled={this.isUploading}>
|
||||||
|
Crop avatar
|
||||||
|
</CropButton>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.isCropping) {
|
||||||
|
return this.renderCropping();
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Dropzone
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
onDropAccepted={this.onDropAccepted}
|
||||||
|
style={{}}
|
||||||
|
disablePreview
|
||||||
|
{...this.props}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</Dropzone>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarEditorContainer = styled(Flex)`
|
||||||
|
margin-bottom: 30px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RangeInput = styled.input`
|
||||||
|
display: block;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 99999px;
|
||||||
|
background-color: #dee1e3;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: black;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CropButton = styled(Button)`
|
||||||
|
width: 300px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default DropToImport;
|
@ -6,15 +6,22 @@ type File = {
|
|||||||
blob: boolean,
|
blob: boolean,
|
||||||
type: string,
|
type: string,
|
||||||
size: number,
|
size: number,
|
||||||
name: string,
|
name?: string,
|
||||||
file: string,
|
file: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function uploadFile(file: File) {
|
type Options = {
|
||||||
|
name?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadFile = async (file: File | Blob, option?: Options) => {
|
||||||
|
// $FlowFixMe Blob makes life hard
|
||||||
|
const filename = (option && option.name) || file.name;
|
||||||
|
|
||||||
const response = await client.post('/user.s3Upload', {
|
const response = await client.post('/user.s3Upload', {
|
||||||
kind: file.type,
|
kind: file.type,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
filename: file.name,
|
filename,
|
||||||
});
|
});
|
||||||
|
|
||||||
invariant(response, 'Response should be available');
|
invariant(response, 'Response should be available');
|
||||||
@ -28,6 +35,7 @@ export default async function uploadFile(file: File) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file.blob) {
|
if (file.blob) {
|
||||||
|
// $FlowFixMe
|
||||||
formData.append('file', file.file);
|
formData.append('file', file.file);
|
||||||
} else {
|
} else {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
@ -41,4 +49,14 @@ export default async function uploadFile(file: File) {
|
|||||||
await fetch(data.uploadUrl, options);
|
await fetch(data.uploadUrl, options);
|
||||||
|
|
||||||
return asset;
|
return asset;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const dataUrlToBlob = (dataURL: string) => {
|
||||||
|
var blobBin = atob(dataURL.split(',')[1]);
|
||||||
|
var array = [];
|
||||||
|
for (var i = 0; i < blobBin.length; i++) {
|
||||||
|
array.push(blobBin.charCodeAt(i));
|
||||||
|
}
|
||||||
|
const file = new Blob([new Uint8Array(array)], { type: 'image/png' });
|
||||||
|
return file;
|
||||||
|
};
|
||||||
|
@ -145,10 +145,10 @@
|
|||||||
"randomstring": "1.1.5",
|
"randomstring": "1.1.5",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"react": "^16.1.0",
|
"react": "^16.1.0",
|
||||||
|
"react-avatar-editor": "^10.3.0",
|
||||||
"react-dom": "^16.1.0",
|
"react-dom": "^16.1.0",
|
||||||
"react-dropzone": "4.2.1",
|
"react-dropzone": "4.2.1",
|
||||||
"react-helmet": "^5.2.0",
|
"react-helmet": "^5.2.0",
|
||||||
"react-image-crop": "^3.0.7",
|
|
||||||
"react-keydown": "^1.7.3",
|
"react-keydown": "^1.7.3",
|
||||||
"react-markdown": "^3.0.2",
|
"react-markdown": "^3.0.2",
|
||||||
"react-medium-image-zoom": "^3.0.2",
|
"react-medium-image-zoom": "^3.0.2",
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
import Router from 'koa-router';
|
import Router from 'koa-router';
|
||||||
|
<<<<<<< HEAD
|
||||||
import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3';
|
import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3';
|
||||||
|
=======
|
||||||
|
|
||||||
|
import Event from '../models/Event';
|
||||||
|
import { makePolicy, signPolicy } from '../utils/s3';
|
||||||
|
>>>>>>> Avatar upload
|
||||||
import auth from './middlewares/authentication';
|
import auth from './middlewares/authentication';
|
||||||
import { presentUser } from '../presenters';
|
import { presentUser } from '../presenters';
|
||||||
|
|
||||||
@ -13,10 +19,16 @@ router.post('user.info', auth(), async ctx => {
|
|||||||
|
|
||||||
router.post('user.update', auth(), async ctx => {
|
router.post('user.update', auth(), async ctx => {
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
const { name } = ctx.body;
|
const { name, avatarUrl } = ctx.body;
|
||||||
ctx.assertNotEmpty(name, "name can't be empty");
|
|
||||||
|
|
||||||
if (name) user.name = name;
|
if (name) user.name = name;
|
||||||
|
if (
|
||||||
|
avatarUrl &&
|
||||||
|
avatarUrl.startsWith(
|
||||||
|
`${process.env.AWS_S3_UPLOAD_BUCKET_URL}uploads/${ctx.state.user.id}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
user.avatarUrl = avatarUrl;
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
ctx.body = { data: await presentUser(ctx, user) };
|
ctx.body = { data: await presentUser(ctx, user) };
|
||||||
@ -32,6 +44,19 @@ router.post('user.s3Upload', auth(), async ctx => {
|
|||||||
const key = `uploads/${ctx.state.user.id}/${s3Key}/${filename}`;
|
const key = `uploads/${ctx.state.user.id}/${s3Key}/${filename}`;
|
||||||
const policy = makePolicy();
|
const policy = makePolicy();
|
||||||
const endpoint = publicS3Endpoint();
|
const endpoint = publicS3Endpoint();
|
||||||
|
const url = `${endpoint}/${key}`;
|
||||||
|
|
||||||
|
await Event.create({
|
||||||
|
name: 'user.s3Upload',
|
||||||
|
data: {
|
||||||
|
filename,
|
||||||
|
kind,
|
||||||
|
size,
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
teamId: ctx.state.user.teamId,
|
||||||
|
userId: ctx.state.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: {
|
data: {
|
||||||
@ -48,8 +73,8 @@ router.post('user.s3Upload', auth(), async ctx => {
|
|||||||
},
|
},
|
||||||
asset: {
|
asset: {
|
||||||
contentType: kind,
|
contentType: kind,
|
||||||
url: `${endpoint}/${key}`,
|
|
||||||
name: filename,
|
name: filename,
|
||||||
|
url,
|
||||||
size,
|
size,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -7379,6 +7379,12 @@ rc@^1.0.1, rc@^1.1.7:
|
|||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
react-avatar-editor@^10.3.0:
|
||||||
|
version "10.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-10.3.0.tgz#7ee54774a274b3aa733d8651e6138d4904114c7d"
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
react-create-component-from-tag-prop@^1.2.1:
|
react-create-component-from-tag-prop@^1.2.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.npmjs.org/react-create-component-from-tag-prop/-/react-create-component-from-tag-prop-1.3.1.tgz#5389407d99f88ba2b36351780a6094470b44a7c7"
|
resolved "https://registry.npmjs.org/react-create-component-from-tag-prop/-/react-create-component-from-tag-prop-1.3.1.tgz#5389407d99f88ba2b36351780a6094470b44a7c7"
|
||||||
@ -7415,10 +7421,6 @@ react-helmet@^5.2.0:
|
|||||||
prop-types "^15.5.4"
|
prop-types "^15.5.4"
|
||||||
react-side-effect "^1.1.0"
|
react-side-effect "^1.1.0"
|
||||||
|
|
||||||
react-image-crop@^3.0.7:
|
|
||||||
version "3.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-3.0.7.tgz#8f7f1f03031c76c8b6306d11f7d1b8bf3007ab8c"
|
|
||||||
|
|
||||||
react-immutable-proptypes@^2.1.0:
|
react-immutable-proptypes@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4"
|
resolved "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4"
|
||||||
|
Reference in New Issue
Block a user