signal-desktop/ts/components/leftPane/LeftPaneSetGroupMetadataHelper.tsx

308 lines
8.5 KiB
TypeScript
Raw Normal View History

2021-03-03 20:09:58 +00:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactChild } from 'react';
import React from 'react';
2021-03-03 20:09:58 +00:00
import { LeftPaneHelper } from './LeftPaneHelper';
import type { Row } from '../ConversationList';
import { RowType } from '../ConversationList';
2021-11-17 21:11:21 +00:00
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
import { DisappearingTimerSelect } from '../DisappearingTimerSelect';
import type { LocalizerType } from '../../types/Util';
2022-11-16 20:18:02 +00:00
import type { DurationInSeconds } from '../../util/durations';
2021-03-03 20:09:58 +00:00
import { Alert } from '../Alert';
2021-08-06 00:17:05 +00:00
import { AvatarEditor } from '../AvatarEditor';
import { AvatarPreview } from '../AvatarPreview';
2021-03-03 20:09:58 +00:00
import { Spinner } from '../Spinner';
import { Button } from '../Button';
2021-08-06 00:17:05 +00:00
import { Modal } from '../Modal';
import { GroupTitleInput } from '../GroupTitleInput';
import type {
2021-08-06 00:17:05 +00:00
AvatarDataType,
DeleteAvatarFromDiskActionType,
ReplaceAvatarActionType,
SaveAvatarToDiskActionType,
} from '../../types/Avatar';
import { AvatarColors } from '../../types/Colors';
2021-03-03 20:09:58 +00:00
export type LeftPaneSetGroupMetadataPropsType = {
2021-09-24 00:49:05 +00:00
groupAvatar: undefined | Uint8Array;
2021-03-03 20:09:58 +00:00
groupName: string;
2022-11-16 20:18:02 +00:00
groupExpireTimer: DurationInSeconds;
2021-03-03 20:09:58 +00:00
hasError: boolean;
isCreating: boolean;
2021-08-06 00:17:05 +00:00
isEditingAvatar: boolean;
2021-11-17 21:11:21 +00:00
selectedContacts: ReadonlyArray<ContactListItemConversationType>;
2021-08-06 00:17:05 +00:00
userAvatarData: ReadonlyArray<AvatarDataType>;
2021-03-03 20:09:58 +00:00
};
2021-04-26 16:38:50 +00:00
export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGroupMetadataPropsType> {
2021-09-24 00:49:05 +00:00
private readonly groupAvatar: undefined | Uint8Array;
2021-03-03 20:09:58 +00:00
private readonly groupName: string;
2022-11-16 20:18:02 +00:00
private readonly groupExpireTimer: DurationInSeconds;
2021-03-03 20:09:58 +00:00
private readonly hasError: boolean;
private readonly isCreating: boolean;
2021-08-06 00:17:05 +00:00
private readonly isEditingAvatar: boolean;
2021-11-17 21:11:21 +00:00
private readonly selectedContacts: ReadonlyArray<ContactListItemConversationType>;
2021-03-03 20:09:58 +00:00
2021-08-06 00:17:05 +00:00
private readonly userAvatarData: ReadonlyArray<AvatarDataType>;
2021-03-03 20:09:58 +00:00
constructor({
groupAvatar,
groupName,
groupExpireTimer,
2021-03-03 20:09:58 +00:00
hasError,
2021-08-06 00:17:05 +00:00
isCreating,
isEditingAvatar,
2021-03-03 20:09:58 +00:00
selectedContacts,
2021-08-06 00:17:05 +00:00
userAvatarData,
2021-03-03 20:09:58 +00:00
}: Readonly<LeftPaneSetGroupMetadataPropsType>) {
super();
this.groupAvatar = groupAvatar;
this.groupName = groupName;
this.groupExpireTimer = groupExpireTimer;
2021-03-03 20:09:58 +00:00
this.hasError = hasError;
this.isCreating = isCreating;
2021-08-06 00:17:05 +00:00
this.isEditingAvatar = isEditingAvatar;
2021-03-03 20:09:58 +00:00
this.selectedContacts = selectedContacts;
2021-08-06 00:17:05 +00:00
this.userAvatarData = userAvatarData;
2021-03-03 20:09:58 +00:00
}
override getHeaderContents({
2021-03-03 20:09:58 +00:00
i18n,
showChooseGroupMembers,
}: Readonly<{
i18n: LocalizerType;
showChooseGroupMembers: () => void;
}>): ReactChild {
const backButtonLabel = i18n('setGroupMetadata__back-button');
return (
<div className="module-left-pane__header__contents">
<button
aria-label={backButtonLabel}
className="module-left-pane__header__contents__back-button"
disabled={this.isCreating}
onClick={this.getBackAction({ showChooseGroupMembers })}
2021-03-03 20:09:58 +00:00
title={backButtonLabel}
type="button"
/>
<div className="module-left-pane__header__contents__text">
{i18n('setGroupMetadata__title')}
</div>
</div>
);
}
override getBackAction({
showChooseGroupMembers,
}: {
showChooseGroupMembers: () => void;
}): undefined | (() => void) {
return this.isCreating ? undefined : showChooseGroupMembers;
}
override getPreRowsNode({
2021-03-03 20:09:58 +00:00
clearGroupCreationError,
2021-08-06 00:17:05 +00:00
composeDeleteAvatarFromDisk,
composeReplaceAvatar,
composeSaveAvatarToDisk,
2021-03-03 20:09:58 +00:00
createGroup,
i18n,
setComposeGroupAvatar,
setComposeGroupExpireTimer,
2021-03-03 20:09:58 +00:00
setComposeGroupName,
2021-08-06 00:17:05 +00:00
toggleComposeEditingAvatar,
2021-03-03 20:09:58 +00:00
}: Readonly<{
clearGroupCreationError: () => unknown;
2021-08-06 00:17:05 +00:00
composeDeleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
composeReplaceAvatar: ReplaceAvatarActionType;
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
2021-03-03 20:09:58 +00:00
createGroup: () => unknown;
i18n: LocalizerType;
2021-09-24 00:49:05 +00:00
setComposeGroupAvatar: (_: undefined | Uint8Array) => unknown;
2022-11-16 20:18:02 +00:00
setComposeGroupExpireTimer: (_: DurationInSeconds) => void;
2021-03-03 20:09:58 +00:00
setComposeGroupName: (_: string) => unknown;
2021-08-06 00:17:05 +00:00
toggleComposeEditingAvatar: () => unknown;
2021-03-03 20:09:58 +00:00
}>): ReactChild {
2021-08-06 00:17:05 +00:00
const [avatarColor] = AvatarColors;
2021-03-03 20:09:58 +00:00
const disabled = this.isCreating;
return (
<form
className="module-left-pane__header__form"
onSubmit={event => {
event.preventDefault();
event.stopPropagation();
if (!this.canCreateGroup()) {
return;
}
createGroup();
}}
>
2021-08-06 00:17:05 +00:00
{this.isEditingAvatar && (
<Modal
2022-09-27 20:24:21 +00:00
modalName="LeftPaneSetGroupMetadataHelper.AvatarEditor"
2021-08-06 00:17:05 +00:00
hasXButton
i18n={i18n}
onClose={toggleComposeEditingAvatar}
title={i18n('LeftPaneSetGroupMetadataHelper__avatar-modal-title')}
>
<AvatarEditor
avatarColor={avatarColor}
avatarValue={this.groupAvatar}
deleteAvatarFromDisk={composeDeleteAvatarFromDisk}
i18n={i18n}
isGroup
onCancel={toggleComposeEditingAvatar}
onSave={newAvatar => {
setComposeGroupAvatar(newAvatar);
toggleComposeEditingAvatar();
}}
userAvatarData={this.userAvatarData}
replaceAvatar={composeReplaceAvatar}
saveAvatarToDisk={composeSaveAvatarToDisk}
/>
</Modal>
)}
<AvatarPreview
avatarColor={avatarColor}
avatarValue={this.groupAvatar}
2021-03-03 20:09:58 +00:00
i18n={i18n}
2021-08-06 00:17:05 +00:00
isEditable
isGroup
onClick={toggleComposeEditingAvatar}
style={{
height: 96,
margin: 0,
width: 96,
}}
2021-03-03 20:09:58 +00:00
/>
<div className="module-GroupInput--container">
<GroupTitleInput
disabled={disabled}
i18n={i18n}
onChangeValue={setComposeGroupName}
ref={focusRef}
value={this.groupName}
/>
</div>
2021-03-03 20:09:58 +00:00
<section className="module-left-pane__header__form__expire-timer">
<div className="module-left-pane__header__form__expire-timer__label">
{i18n('icu:disappearingMessages')}
</div>
<DisappearingTimerSelect
i18n={i18n}
value={this.groupExpireTimer}
onChange={setComposeGroupExpireTimer}
/>
</section>
2021-03-03 20:09:58 +00:00
{this.hasError && (
<Alert
body={i18n('setGroupMetadata__error-message')}
i18n={i18n}
onClose={clearGroupCreationError}
/>
)}
</form>
);
}
override getFooterContents({
2021-03-03 20:09:58 +00:00
createGroup,
i18n,
}: Readonly<{
createGroup: () => unknown;
i18n: LocalizerType;
}>): ReactChild {
return (
<Button
disabled={!this.canCreateGroup()}
onClick={() => {
createGroup();
}}
>
2021-03-03 20:09:58 +00:00
{this.isCreating ? (
<Spinner size="20px" svgSize="small" direction="on-avatar" />
) : (
i18n('setGroupMetadata__create-group')
)}
</Button>
);
}
getRowCount(): number {
if (!this.selectedContacts.length) {
return 0;
}
return this.selectedContacts.length + 2;
}
getRow(rowIndex: number): undefined | Row {
if (!this.selectedContacts.length) {
return undefined;
}
if (rowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'setGroupMetadata__members-header',
};
}
// This puts a blank row for the footer.
if (rowIndex === this.selectedContacts.length + 1) {
return { type: RowType.Blank };
}
const contact = this.selectedContacts[rowIndex - 1];
return contact
? {
type: RowType.Contact,
contact,
isClickable: false,
}
: undefined;
}
// This is deliberately unimplemented because these keyboard shortcuts shouldn't work in
// the composer. The same is true for the "in direction" function below.
getConversationAndMessageAtIndex(
..._args: ReadonlyArray<unknown>
): undefined {
return undefined;
}
getConversationAndMessageInDirection(
..._args: ReadonlyArray<unknown>
): undefined {
return undefined;
}
shouldRecomputeRowHeights(_old: unknown): boolean {
return false;
}
private canCreateGroup(): boolean {
return !this.isCreating && Boolean(this.groupName.trim());
}
}
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}