Signal-Desktop/ts/components/preferences/EditChatFoldersPage.tsx

508 lines
18 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MutableRefObject } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import type { ConversationType } from '../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import type { LocalizerType } from '../../types/I18N';
import type { ThemeType } from '../../types/Util';
import { Input } from '../Input';
import { Button, ButtonVariant } from '../Button';
import { ConfirmationDialog } from '../ConfirmationDialog';
import type { ChatFolderSelection } from './EditChatFoldersSelectChatsDialog';
import { EditChatFoldersSelectChatsDialog } from './EditChatFoldersSelectChatsDialog';
import { SettingsRow } from '../PreferencesUtil';
import { Checkbox } from '../Checkbox';
import { Avatar, AvatarSize } from '../Avatar';
import { PreferencesContent } from '../Preferences';
import type { ChatFolderId } from '../../types/ChatFolder';
import {
CHAT_FOLDER_NAME_MAX_CHAR_LENGTH,
isSameChatFolderParams,
normalizeChatFolderParams,
validateChatFolderParams,
type ChatFolderParams,
} from '../../types/ChatFolder';
import type { GetConversationByIdType } from '../../state/selectors/conversations';
import { strictAssert } from '../../util/assert';
export function EditChatFoldersPage(props: {
i18n: LocalizerType;
existingChatFolderId: ChatFolderId | null;
initChatFolderParams: ChatFolderParams;
onBack: () => void;
conversations: ReadonlyArray<ConversationType>;
getPreferredBadge: PreferredBadgeSelectorType;
theme: ThemeType;
settingsPaneRef: MutableRefObject<HTMLDivElement | null>;
conversationSelector: GetConversationByIdType;
onDeleteChatFolder: (chatFolderId: ChatFolderId) => void;
onCreateChatFolder: (chatFolderParams: ChatFolderParams) => void;
onUpdateChatFolder: (
chatFolderId: ChatFolderId,
chatFolderParams: ChatFolderParams
) => void;
}): JSX.Element {
const {
i18n,
initChatFolderParams,
existingChatFolderId,
onCreateChatFolder,
onUpdateChatFolder,
onDeleteChatFolder,
onBack,
conversationSelector,
} = props;
const [chatFolderParams, setChatFolderParams] =
useState(initChatFolderParams);
const [showInclusionsDialog, setShowInclusionsDialog] = useState(false);
const [showExclusionsDialog, setShowExclusionsDialog] = useState(false);
const [showDeleteFolderDialog, setShowDeleteFolderDialog] = useState(false);
const [showSaveChangesDialog, setShowSaveChangesDialog] = useState(false);
const normalizedChatFolderParams = useMemo(() => {
return normalizeChatFolderParams(chatFolderParams);
}, [chatFolderParams]);
const isChanged = useMemo(() => {
return !isSameChatFolderParams(
initChatFolderParams,
normalizedChatFolderParams
);
}, [initChatFolderParams, normalizedChatFolderParams]);
const isValid = useMemo(() => {
return validateChatFolderParams(normalizedChatFolderParams);
}, [normalizedChatFolderParams]);
const handleNameChange = useCallback((newName: string) => {
setChatFolderParams(prevParams => {
return { ...prevParams, name: newName };
});
}, []);
const handleShowOnlyUnreadChange = useCallback((newValue: boolean) => {
setChatFolderParams(prevParams => {
return { ...prevParams, showOnlyUnread: newValue };
});
}, []);
const handleShowMutedChatsChange = useCallback((newValue: boolean) => {
setChatFolderParams(prevParams => {
return { ...prevParams, showMutedChats: newValue };
});
}, []);
const handleBackInit = useCallback(() => {
if (!isChanged) {
onBack();
} else {
setShowSaveChangesDialog(true);
}
}, [isChanged, onBack]);
const handleDiscard = useCallback(() => {
onBack();
}, [onBack]);
const handleSaveClose = useCallback(() => {
setShowSaveChangesDialog(false);
}, []);
const handleSave = useCallback(() => {
strictAssert(isChanged, 'tried saving when unchanged');
strictAssert(isValid, 'tried saving when invalid');
if (existingChatFolderId != null) {
onUpdateChatFolder(existingChatFolderId, chatFolderParams);
} else {
onCreateChatFolder(chatFolderParams);
}
onBack();
}, [
onBack,
existingChatFolderId,
isChanged,
isValid,
chatFolderParams,
onCreateChatFolder,
onUpdateChatFolder,
]);
const handleDeleteInit = useCallback(() => {
setShowDeleteFolderDialog(true);
}, []);
const handleDeleteConfirm = useCallback(() => {
strictAssert(existingChatFolderId, 'Missing existing chat folder id');
onDeleteChatFolder(existingChatFolderId);
setShowDeleteFolderDialog(false);
onBack();
}, [existingChatFolderId, onDeleteChatFolder, onBack]);
const handleDeleteClose = useCallback(() => {
setShowDeleteFolderDialog(false);
}, []);
const handleSelectInclusions = useCallback(() => {
setShowInclusionsDialog(true);
}, []);
const handleSelectExclusions = useCallback(() => {
setShowExclusionsDialog(true);
}, []);
const handleCloseInclusions = useCallback(
(selection: ChatFolderSelection) => {
setChatFolderParams(prevParams => {
return {
...prevParams,
includeAllIndividualChats: selection.selectAllIndividualChats,
includeAllGroupChats: selection.selectAllGroupChats,
includedConversationIds: selection.selectedRecipientIds,
};
});
setShowInclusionsDialog(false);
},
[]
);
const handleCloseExclusions = useCallback(
(selection: ChatFolderSelection) => {
setChatFolderParams(prevParams => {
return {
...prevParams,
includeAllIndividualChats: !selection.selectAllIndividualChats,
includeAllGroupChats: !selection.selectAllGroupChats,
excludedConversationIds: selection.selectedRecipientIds,
};
});
setShowExclusionsDialog(false);
},
[]
);
return (
<PreferencesContent
backButton={
<button
aria-label={i18n('icu:goBack')}
className="Preferences__back-icon"
onClick={handleBackInit}
type="button"
/>
}
contents={
<>
<SettingsRow
title={i18n(
'icu:Preferences__EditChatFolderPage__FolderNameField__Label'
)}
>
<div className="Preferences__padding">
<Input
i18n={i18n}
value={chatFolderParams.name}
onChange={handleNameChange}
placeholder={i18n(
'icu:Preferences__EditChatFolderPage__FolderNameField__Placeholder'
)}
maxLengthCount={CHAT_FOLDER_NAME_MAX_CHAR_LENGTH}
whenToShowRemainingCount={CHAT_FOLDER_NAME_MAX_CHAR_LENGTH - 10}
/>
</div>
</SettingsRow>
<SettingsRow
title={i18n(
'icu:Preferences__EditChatFolderPage__IncludedChatsSection__Title'
)}
>
<button
type="button"
className="Preferences__ChatFolders__ChatSelection__Item Preferences__ChatFolders__ChatSelection__Item--Button"
onClick={handleSelectInclusions}
>
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--Add" />
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
{i18n(
'icu:Preferences__EditChatFolderPage__IncludedChatsSection__AddChatsButton'
)}
</span>
</button>
<ul className="Preferences__ChatFolders__ChatSelection__List">
{chatFolderParams.includeAllIndividualChats && (
<li className="Preferences__ChatFolders__ChatSelection__Item">
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--DirectChats" />
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
1:1 Chats
</span>
</li>
)}
{chatFolderParams.includeAllGroupChats && (
<li className="Preferences__ChatFolders__ChatSelection__Item">
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--GroupChats" />
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
Group Chats
</span>
</li>
)}
{chatFolderParams.includedConversationIds.map(conversationId => {
const conversation = conversationSelector(conversationId);
return (
<li
key={conversationId}
className="Preferences__ChatFolders__ChatSelection__Item"
>
<Avatar
i18n={i18n}
conversationType={conversation.type}
size={AvatarSize.THIRTY_SIX}
badge={undefined}
{...conversation}
/>
<span className="Preferences__ChatFolders__ChatList__ItemTitle">
{conversation.title}
</span>
</li>
);
})}
</ul>
<div className="Preferences__padding">
<p className="Preferences__description">
{i18n(
'icu:Preferences__EditChatFolderPage__IncludedChatsSection__Help'
)}
</p>
</div>
</SettingsRow>
<SettingsRow
title={i18n(
'icu:Preferences__EditChatFolderPage__ExceptionsSection__Title'
)}
>
<button
type="button"
className="Preferences__ChatFolders__ChatSelection__Item Preferences__ChatFolders__ChatSelection__Item--Button"
onClick={handleSelectExclusions}
>
<span className="Preferences__ChatFolders__ChatSelection__ItemAvatar Preferences__ChatFolders__ChatSelection__ItemAvatar--Add" />
<span className="Preferences__ChatFolders__ChatSelection__ItemTitle">
{i18n(
'icu:Preferences__EditChatFolderPage__ExceptionsSection__ExcludeChatsButton'
)}
</span>
</button>
<ul className="Preferences__ChatFolders__ChatSelection__List">
{chatFolderParams.excludedConversationIds.map(conversationId => {
const conversation = conversationSelector(conversationId);
return (
<li
key={conversationId}
className="Preferences__ChatFolders__ChatSelection__Item"
>
<Avatar
i18n={i18n}
conversationType={conversation.type}
size={AvatarSize.THIRTY_SIX}
badge={undefined}
{...conversation}
/>
<span className="Preferences__ChatFolders__ChatList__ItemTitle">
{conversation.title}
</span>
</li>
);
})}
</ul>
<div className="Preferences__padding">
<p className="Preferences__description">
{i18n(
'icu:Preferences__EditChatFolderPage__ExceptionsSection__Help'
)}
</p>
</div>
</SettingsRow>
<SettingsRow>
<Checkbox
checked={chatFolderParams.showOnlyUnread}
label={i18n(
'icu:Preferences__EditChatFolderPage__OnlyShowUnreadChatsCheckbox__Label'
)}
description={i18n(
'icu:Preferences__EditChatFolderPage__OnlyShowUnreadChatsCheckbox__Description'
)}
moduleClassName="Preferences__checkbox"
name="showOnlyUnread"
onChange={handleShowOnlyUnreadChange}
/>
<Checkbox
checked={chatFolderParams.showMutedChats}
label={i18n(
'icu:Preferences__EditChatFolderPage__IncludeMutedChatsCheckbox__Label'
)}
moduleClassName="Preferences__checkbox"
name="showMutedChats"
onChange={handleShowMutedChatsChange}
/>
</SettingsRow>
{props.existingChatFolderId != null && (
<SettingsRow>
<div className="Preferences__padding">
<button
type="button"
onClick={handleDeleteInit}
className="Preferences__ChatFolders__ChatList__DeleteButton"
>
{i18n(
'icu:Preferences__EditChatFolderPage__DeleteFolderButton'
)}
</button>
</div>
</SettingsRow>
)}
{showInclusionsDialog && (
<EditChatFoldersSelectChatsDialog
i18n={i18n}
title={i18n(
'icu:Preferences__EditChatFolderPage__SelectChatsDialog--IncludedChats__Title'
)}
onClose={handleCloseInclusions}
conversations={props.conversations}
getPreferredBadge={props.getPreferredBadge}
theme={props.theme}
conversationSelector={props.conversationSelector}
initialSelection={{
selectAllIndividualChats:
chatFolderParams.includeAllIndividualChats,
selectAllGroupChats: chatFolderParams.includeAllGroupChats,
selectedRecipientIds: chatFolderParams.includedConversationIds,
}}
showChatTypes
/>
)}
{showExclusionsDialog && (
<EditChatFoldersSelectChatsDialog
i18n={i18n}
title={i18n(
'icu:Preferences__EditChatFolderPage__SelectChatsDialog--ExcludedChats__Title'
)}
onClose={handleCloseExclusions}
conversations={props.conversations}
getPreferredBadge={props.getPreferredBadge}
theme={props.theme}
conversationSelector={props.conversationSelector}
initialSelection={{
selectAllIndividualChats:
!chatFolderParams.includeAllIndividualChats,
selectAllGroupChats: !chatFolderParams.includeAllGroupChats,
selectedRecipientIds: chatFolderParams.excludedConversationIds,
}}
showChatTypes={false}
/>
)}
{showDeleteFolderDialog && (
<DeleteChatFolderDialog
i18n={i18n}
onConfirm={handleDeleteConfirm}
onClose={handleDeleteClose}
/>
)}
{showSaveChangesDialog && (
<SaveChangesFolderDialog
i18n={i18n}
onSave={handleSave}
onCancel={handleDiscard}
onClose={handleSaveClose}
/>
)}
</>
}
contentsRef={props.settingsPaneRef}
title={i18n('icu:Preferences__EditChatFolderPage__Title')}
actions={
<>
<Button variant={ButtonVariant.Secondary} onClick={handleDiscard}>
{i18n('icu:Preferences__EditChatFolderPage__CancelButton')}
</Button>
<Button
variant={ButtonVariant.Primary}
onClick={handleSave}
disabled={!(isChanged && isValid)}
>
{i18n('icu:Preferences__EditChatFolderPage__SaveButton')}
</Button>
</>
}
/>
);
}
function DeleteChatFolderDialog(props: {
i18n: LocalizerType;
onConfirm: () => void;
onClose: () => void;
}) {
const { i18n } = props;
return (
<ConfirmationDialog
i18n={i18n}
dialogName="Preferences__EditChatFolderPage__DeleteChatFolderDialog"
title={i18n(
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__Title'
)}
cancelText={i18n(
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__CancelButton'
)}
actions={[
{
text: i18n(
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__DeleteButton'
),
style: 'affirmative',
action: props.onConfirm,
},
]}
onClose={props.onClose}
>
{i18n(
'icu:Preferences__EditChatFolderPage__DeleteChatFolderDialog__Description'
)}
</ConfirmationDialog>
);
}
function SaveChangesFolderDialog(props: {
i18n: LocalizerType;
onSave: () => void;
onCancel: () => void;
onClose: () => void;
}) {
const { i18n } = props;
return (
<ConfirmationDialog
i18n={i18n}
dialogName="Preferences__EditChatFolderPage__SaveChangesFolderDialog"
title={i18n(
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__Title'
)}
cancelText={i18n(
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__DiscardButton'
)}
actions={[
{
text: i18n(
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__SaveButton'
),
style: 'affirmative',
action: props.onSave,
},
]}
onCancel={props.onCancel}
onClose={props.onClose}
>
{i18n(
'icu:Preferences__EditChatFolderPage__SaveChangesFolderDialog__Description'
)}
</ConfirmationDialog>
);
}