508 lines
18 KiB
TypeScript
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>
|
|
);
|
|
}
|