2021-03-09 19:16:56 +00:00
|
|
|
// 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';
|
2021-03-09 19:16:56 +00:00
|
|
|
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';
|
2021-03-09 19:16:56 +00:00
|
|
|
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';
|
2021-03-09 19:16:56 +00:00
|
|
|
|
|
|
|
const TEMPORARY_AVATAR_VALUE = new ArrayBuffer(0);
|
|
|
|
|
|
|
|
type PropsType = {
|
|
|
|
avatarPath?: string;
|
2021-06-02 00:24:28 +00:00
|
|
|
groupDescription?: string;
|
2021-03-09 19:16:56 +00:00
|
|
|
i18n: LocalizerType;
|
2021-06-04 14:55:34 +00:00
|
|
|
initiallyFocusDescription: boolean;
|
2021-03-09 19:16:56 +00:00
|
|
|
makeRequest: (
|
|
|
|
_: Readonly<{
|
|
|
|
avatar?: undefined | ArrayBuffer;
|
2021-06-02 00:24:28 +00:00
|
|
|
description?: string;
|
2021-03-09 19:16:56 +00:00
|
|
|
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 = '',
|
2021-03-09 19:16:56 +00:00
|
|
|
i18n,
|
2021-06-04 14:55:34 +00:00
|
|
|
initiallyFocusDescription,
|
2021-03-09 19:16:56 +00:00
|
|
|
makeRequest,
|
|
|
|
onClose,
|
|
|
|
requestState,
|
|
|
|
title: externalTitle,
|
|
|
|
}) => {
|
2021-06-04 14:55:34 +00:00
|
|
|
const focusDescriptionRef = useRef<undefined | boolean>(
|
|
|
|
initiallyFocusDescription
|
|
|
|
);
|
|
|
|
const focusDescription = focusDescriptionRef.current;
|
|
|
|
|
2021-03-09 19:16:56 +00:00
|
|
|
const startingTitleRef = useRef<string>(externalTitle);
|
|
|
|
const startingAvatarPathRef = useRef<undefined | string>(externalAvatarPath);
|
|
|
|
|
|
|
|
const [avatar, setAvatar] = useState<undefined | ArrayBuffer>(
|
|
|
|
externalAvatarPath ? TEMPORARY_AVATAR_VALUE : undefined
|
|
|
|
);
|
2021-03-11 01:54:13 +00:00
|
|
|
const [rawTitle, setRawTitle] = useState(externalTitle);
|
2021-06-02 00:24:28 +00:00
|
|
|
const [rawGroupDescription, setRawGroupDescription] = useState(
|
|
|
|
externalGroupDescription
|
|
|
|
);
|
2021-03-09 19:16:56 +00:00
|
|
|
const [hasAvatarChanged, setHasAvatarChanged] = useState(false);
|
|
|
|
|
2021-03-11 01:54:13 +00:00
|
|
|
const trimmedTitle = rawTitle.trim();
|
2021-06-02 00:24:28 +00:00
|
|
|
const trimmedDescription = rawGroupDescription.trim();
|
2021-03-11 01:54:13 +00:00
|
|
|
|
2021-06-04 14:55:34 +00:00
|
|
|
const focusRef = (el: null | HTMLElement) => {
|
|
|
|
if (el) {
|
|
|
|
el.focus();
|
|
|
|
focusDescriptionRef.current = undefined;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-03-09 19:16:56 +00:00
|
|
|
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;
|
2021-03-11 01:54:13 +00:00
|
|
|
const hasTitleChanged = trimmedTitle !== externalTitle.trim();
|
2021-06-02 00:24:28 +00:00
|
|
|
const hasGroupDescriptionChanged =
|
|
|
|
externalGroupDescription.trim() !== trimmedDescription;
|
2021-03-09 19:16:56 +00:00
|
|
|
|
|
|
|
const isRequestActive = requestState === RequestState.Active;
|
|
|
|
|
|
|
|
const canSubmit =
|
|
|
|
!isRequestActive &&
|
2021-06-02 00:24:28 +00:00
|
|
|
(hasChangedExternally ||
|
|
|
|
hasTitleChanged ||
|
|
|
|
hasAvatarChanged ||
|
|
|
|
hasGroupDescriptionChanged) &&
|
2021-03-11 01:54:13 +00:00
|
|
|
trimmedTitle.length > 0;
|
2021-03-09 19:16:56 +00:00
|
|
|
|
|
|
|
const onSubmit: FormEventHandler<HTMLFormElement> = event => {
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
const request: {
|
|
|
|
avatar?: undefined | ArrayBuffer;
|
2021-06-02 00:24:28 +00:00
|
|
|
description?: string;
|
2021-03-09 19:16:56 +00:00
|
|
|
title?: string;
|
|
|
|
} = {};
|
|
|
|
if (hasAvatarChanged) {
|
|
|
|
request.avatar = avatar;
|
|
|
|
}
|
|
|
|
if (hasTitleChanged) {
|
2021-03-11 01:54:13 +00:00
|
|
|
request.title = trimmedTitle;
|
2021-03-09 19:16:56 +00:00
|
|
|
}
|
2021-06-02 00:24:28 +00:00
|
|
|
if (hasGroupDescriptionChanged) {
|
|
|
|
request.description = trimmedDescription;
|
|
|
|
}
|
2021-03-09 19:16:56 +00:00
|
|
|
makeRequest(request);
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2021-06-02 00:24:28 +00:00
|
|
|
<Modal
|
|
|
|
hasXButton
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={onClose}
|
|
|
|
title={i18n('updateGroupAttributes__title')}
|
|
|
|
>
|
2021-03-09 19:16:56 +00:00
|
|
|
<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}
|
2021-03-11 01:54:13 +00:00
|
|
|
onChangeValue={setRawTitle}
|
2021-06-04 14:55:34 +00:00
|
|
|
ref={focusDescription === false ? focusRef : undefined}
|
2021-03-11 01:54:13 +00:00
|
|
|
value={rawTitle}
|
2021-03-09 19:16:56 +00:00
|
|
|
/>
|
|
|
|
|
2021-06-02 00:24:28 +00:00
|
|
|
<GroupDescriptionInput
|
|
|
|
disabled={isRequestActive}
|
|
|
|
i18n={i18n}
|
|
|
|
onChangeValue={setRawGroupDescription}
|
2021-06-04 14:55:34 +00:00
|
|
|
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>
|
|
|
|
|
2021-03-09 19:16:56 +00:00
|
|
|
{requestState === RequestState.InactiveWithError && (
|
|
|
|
<div className="module-EditConversationAttributesModal__error-message">
|
|
|
|
{i18n('updateGroupAttributes__error-message')}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2021-06-02 00:24:28 +00:00
|
|
|
<Modal.ButtonFooter>
|
2021-03-09 19:16:56 +00:00
|
|
|
<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>
|
2021-03-09 19:16:56 +00:00
|
|
|
</form>
|
2021-06-02 00:24:28 +00:00
|
|
|
</Modal>
|
2021-03-09 19:16:56 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|