signal-desktop/ts/components/conversation/conversation-details/EditConversationAttributesModal.tsx

242 lines
6.5 KiB
TypeScript
Raw Normal View History

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, {
FormEventHandler,
FunctionComponent,
useEffect,
useRef,
useState,
} from 'react';
import { noop } from 'lodash';
import { LocalizerType } from '../../../types/Util';
2021-06-02 00:24:28 +00:00
import { Modal } from '../../Modal';
import { AvatarInput, AvatarInputVariant } from '../../AvatarInput';
import { Button, ButtonVariant } from '../../Button';
import { Spinner } from '../../Spinner';
2021-06-02 00:24:28 +00:00
import { GroupDescriptionInput } from '../../GroupDescriptionInput';
import { GroupTitleInput } from '../../GroupTitleInput';
import * as log from '../../../logging/log';
import { canvasToArrayBuffer } from '../../../util/canvasToArrayBuffer';
2021-03-11 21:29:31 +00:00
import { RequestState } from './util';
const TEMPORARY_AVATAR_VALUE = new ArrayBuffer(0);
type PropsType = {
avatarPath?: string;
2021-06-02 00:24:28 +00:00
groupDescription?: string;
i18n: LocalizerType;
initiallyFocusDescription: boolean;
makeRequest: (
_: Readonly<{
avatar?: undefined | ArrayBuffer;
2021-06-02 00:24:28 +00:00
description?: string;
title?: undefined | string;
}>
) => void;
onClose: () => void;
requestState: RequestState;
title: string;
};
export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
avatarPath: externalAvatarPath,
2021-06-02 00:24:28 +00:00
groupDescription: externalGroupDescription = '',
i18n,
initiallyFocusDescription,
makeRequest,
onClose,
requestState,
title: externalTitle,
}) => {
const focusDescriptionRef = useRef<undefined | boolean>(
initiallyFocusDescription
);
const focusDescription = focusDescriptionRef.current;
const startingTitleRef = useRef<string>(externalTitle);
const startingAvatarPathRef = useRef<undefined | string>(externalAvatarPath);
const [avatar, setAvatar] = useState<undefined | ArrayBuffer>(
externalAvatarPath ? TEMPORARY_AVATAR_VALUE : undefined
);
const [rawTitle, setRawTitle] = useState(externalTitle);
2021-06-02 00:24:28 +00:00
const [rawGroupDescription, setRawGroupDescription] = useState(
externalGroupDescription
);
const [hasAvatarChanged, setHasAvatarChanged] = useState(false);
const trimmedTitle = rawTitle.trim();
2021-06-02 00:24:28 +00:00
const trimmedDescription = rawGroupDescription.trim();
const focusRef = (el: null | HTMLElement) => {
if (el) {
el.focus();
focusDescriptionRef.current = undefined;
}
};
useEffect(() => {
const startingAvatarPath = startingAvatarPathRef.current;
if (!startingAvatarPath) {
return noop;
}
let shouldCancel = false;
(async () => {
try {
const buffer = await imagePathToArrayBuffer(startingAvatarPath);
if (shouldCancel) {
return;
}
setAvatar(buffer);
} catch (err) {
log.warn(
`Failed to convert image URL to array buffer. Error message: ${
err && err.message
}`
);
}
})();
return () => {
shouldCancel = true;
};
}, []);
const hasChangedExternally =
startingAvatarPathRef.current !== externalAvatarPath ||
startingTitleRef.current !== externalTitle;
const hasTitleChanged = trimmedTitle !== externalTitle.trim();
2021-06-02 00:24:28 +00:00
const hasGroupDescriptionChanged =
externalGroupDescription.trim() !== trimmedDescription;
const isRequestActive = requestState === RequestState.Active;
const canSubmit =
!isRequestActive &&
2021-06-02 00:24:28 +00:00
(hasChangedExternally ||
hasTitleChanged ||
hasAvatarChanged ||
hasGroupDescriptionChanged) &&
trimmedTitle.length > 0;
const onSubmit: FormEventHandler<HTMLFormElement> = event => {
event.preventDefault();
const request: {
avatar?: undefined | ArrayBuffer;
2021-06-02 00:24:28 +00:00
description?: string;
title?: string;
} = {};
if (hasAvatarChanged) {
request.avatar = avatar;
}
if (hasTitleChanged) {
request.title = trimmedTitle;
}
2021-06-02 00:24:28 +00:00
if (hasGroupDescriptionChanged) {
request.description = trimmedDescription;
}
makeRequest(request);
};
return (
2021-06-02 00:24:28 +00:00
<Modal
hasXButton
i18n={i18n}
onClose={onClose}
title={i18n('updateGroupAttributes__title')}
>
<form
onSubmit={onSubmit}
className="module-EditConversationAttributesModal"
>
<AvatarInput
contextMenuId="edit conversation attributes avatar input"
disabled={isRequestActive}
i18n={i18n}
onChange={newAvatar => {
setAvatar(newAvatar);
setHasAvatarChanged(true);
}}
value={avatar}
variant={AvatarInputVariant.Dark}
/>
<GroupTitleInput
disabled={isRequestActive}
i18n={i18n}
onChangeValue={setRawTitle}
ref={focusDescription === false ? focusRef : undefined}
value={rawTitle}
/>
2021-06-02 00:24:28 +00:00
<GroupDescriptionInput
disabled={isRequestActive}
i18n={i18n}
onChangeValue={setRawGroupDescription}
ref={focusDescription === true ? focusRef : undefined}
2021-06-02 00:24:28 +00:00
value={rawGroupDescription}
/>
<div className="module-EditConversationAttributesModal__description-warning">
{i18n('EditConversationAttributesModal__description-warning')}
</div>
{requestState === RequestState.InactiveWithError && (
<div className="module-EditConversationAttributesModal__error-message">
{i18n('updateGroupAttributes__error-message')}
</div>
)}
2021-06-02 00:24:28 +00:00
<Modal.ButtonFooter>
<Button
disabled={isRequestActive}
onClick={onClose}
variant={ButtonVariant.Secondary}
>
{i18n('cancel')}
</Button>
<Button
type="submit"
variant={ButtonVariant.Primary}
disabled={!canSubmit}
>
{isRequestActive ? (
<Spinner size="20px" svgSize="small" direction="on-avatar" />
) : (
i18n('save')
)}
</Button>
2021-06-02 00:24:28 +00:00
</Modal.ButtonFooter>
</form>
2021-06-02 00:24:28 +00:00
</Modal>
);
};
async function imagePathToArrayBuffer(src: string): Promise<ArrayBuffer> {
const image = new Image();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error(
'imagePathToArrayBuffer: could not get canvas rendering context'
);
}
image.src = src;
await image.decode();
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0);
const result = await canvasToArrayBuffer(canvas);
return result;
}