Support for creating New Groups
This commit is contained in:
parent
1934120e46
commit
5de4babc0d
56 changed files with 6222 additions and 526 deletions
213
ts/components/AvatarInput.tsx
Normal file
213
ts/components/AvatarInput.tsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, {
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
ChangeEventHandler,
|
||||
MouseEventHandler,
|
||||
FunctionComponent,
|
||||
} from 'react';
|
||||
import { ContextMenu, MenuItem, ContextMenuTrigger } from 'react-contextmenu';
|
||||
import loadImage, { LoadImageOptions } from 'blueimp-load-image';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { Spinner } from './Spinner';
|
||||
|
||||
type PropsType = {
|
||||
// This ID needs to be globally unique across the app.
|
||||
contextMenuId: string;
|
||||
disabled?: boolean;
|
||||
i18n: LocalizerType;
|
||||
onChange: (value: undefined | ArrayBuffer) => unknown;
|
||||
value: undefined | ArrayBuffer;
|
||||
};
|
||||
|
||||
enum ImageStatus {
|
||||
Nothing = 'nothing',
|
||||
Loading = 'loading',
|
||||
HasImage = 'has-image',
|
||||
}
|
||||
|
||||
export const AvatarInput: FunctionComponent<PropsType> = ({
|
||||
contextMenuId,
|
||||
disabled,
|
||||
i18n,
|
||||
onChange,
|
||||
value,
|
||||
}) => {
|
||||
const fileInputRef = useRef<null | HTMLInputElement>(null);
|
||||
// Comes from a third-party dependency
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const menuTriggerRef = useRef<null | any>(null);
|
||||
|
||||
const [objectUrl, setObjectUrl] = useState<undefined | string>();
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setObjectUrl(undefined);
|
||||
return noop;
|
||||
}
|
||||
const url = URL.createObjectURL(new Blob([value]));
|
||||
setObjectUrl(url);
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
const [processingFile, setProcessingFile] = useState<undefined | File>(
|
||||
undefined
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!processingFile) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
let shouldCancel = false;
|
||||
|
||||
(async () => {
|
||||
let newValue: ArrayBuffer;
|
||||
try {
|
||||
newValue = await processFile(processingFile);
|
||||
} catch (err) {
|
||||
// Processing errors should be rare; if they do, we silently fail. In an ideal
|
||||
// world, we may want to show a toast instead.
|
||||
return;
|
||||
}
|
||||
if (shouldCancel) {
|
||||
return;
|
||||
}
|
||||
setProcessingFile(undefined);
|
||||
onChange(newValue);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
shouldCancel = true;
|
||||
};
|
||||
}, [processingFile, onChange]);
|
||||
|
||||
const buttonLabel = value
|
||||
? i18n('AvatarInput--change-photo-label')
|
||||
: i18n('AvatarInput--no-photo-label--group');
|
||||
|
||||
const startUpload = () => {
|
||||
const fileInput = fileInputRef.current;
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
onChange(undefined);
|
||||
};
|
||||
|
||||
const onClick: MouseEventHandler<unknown> = value
|
||||
? event => {
|
||||
const menuTrigger = menuTriggerRef.current;
|
||||
if (!menuTrigger) {
|
||||
return;
|
||||
}
|
||||
menuTrigger.handleContextClick(event);
|
||||
}
|
||||
: startUpload;
|
||||
|
||||
const onInputChange: ChangeEventHandler<HTMLInputElement> = event => {
|
||||
const file = event.target.files && event.target.files[0];
|
||||
if (file) {
|
||||
setProcessingFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
let imageStatus: ImageStatus;
|
||||
if (processingFile || (value && !objectUrl)) {
|
||||
imageStatus = ImageStatus.Loading;
|
||||
} else if (objectUrl) {
|
||||
imageStatus = ImageStatus.HasImage;
|
||||
} else {
|
||||
imageStatus = ImageStatus.Nothing;
|
||||
}
|
||||
|
||||
const isLoading = imageStatus === ImageStatus.Loading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenuTrigger id={contextMenuId} ref={menuTriggerRef}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || isLoading}
|
||||
className="module-AvatarInput"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={`module-AvatarInput__avatar module-AvatarInput__avatar--${imageStatus}`}
|
||||
style={
|
||||
imageStatus === ImageStatus.HasImage
|
||||
? {
|
||||
backgroundImage: `url(${objectUrl})`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isLoading && (
|
||||
<Spinner size="70px" svgSize="normal" direction="on-avatar" />
|
||||
)}
|
||||
</div>
|
||||
<span className="module-AvatarInput__label">{buttonLabel}</span>
|
||||
</button>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenu id={contextMenuId}>
|
||||
<MenuItem onClick={startUpload}>
|
||||
{i18n('AvatarInput--upload-photo-choice')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={clear}>
|
||||
{i18n('AvatarInput--remove-photo-choice')}
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
<input
|
||||
accept=".gif,.jpg,.jpeg,.png,.webp,image/gif,image/jpeg/image/png,image/webp"
|
||||
hidden
|
||||
onChange={onInputChange}
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
async function processFile(file: File): Promise<ArrayBuffer> {
|
||||
const { image } = await loadImage(file, {
|
||||
canvas: true,
|
||||
cover: true,
|
||||
crop: true,
|
||||
imageSmoothingQuality: 'medium',
|
||||
maxHeight: 512,
|
||||
maxWidth: 512,
|
||||
minHeight: 2,
|
||||
minWidth: 2,
|
||||
// `imageSmoothingQuality` is not present in `loadImage`'s types, but it is
|
||||
// documented and supported. Updating DefinitelyTyped is the long-term solution
|
||||
// here.
|
||||
} as LoadImageOptions);
|
||||
|
||||
// NOTE: The types for `loadImage` say this can never be a canvas, but it will be if
|
||||
// `canvas: true`, at least in our case. Again, updating DefinitelyTyped should
|
||||
// address this.
|
||||
if (!(image instanceof HTMLCanvasElement)) {
|
||||
throw new Error('Loaded image was not a canvas');
|
||||
}
|
||||
|
||||
return (await canvasToBlob(image)).arrayBuffer();
|
||||
}
|
||||
|
||||
function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(blob => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error("Couldn't convert the canvas to a Blob"));
|
||||
}
|
||||
}, 'image/webp');
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue