TelegramPWA/src/components/left/settings/folders/SettingsFoldersEdit.tsx
2021-06-12 17:20:19 +03:00

315 lines
9.8 KiB
TypeScript

import React, {
FC, memo, useCallback, useState, useEffect, useMemo,
} from '../../../../lib/teact/teact';
import { withGlobal } from '../../../../lib/teact/teactn';
import { GlobalActions } from '../../../../global/types';
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
import { pick, findIntersectionWithSet } from '../../../../util/iteratees';
import { isChatPrivate } from '../../../../modules/helpers';
import getAnimationData from '../../../common/helpers/animatedAssets';
import {
FoldersState,
FolderEditDispatch,
INCLUDED_CHAT_TYPES,
EXCLUDED_CHAT_TYPES,
selectChatFilters,
} from '../../../../hooks/reducers/useFoldersReducer';
import useLang from '../../../../hooks/useLang';
import ListItem from '../../../ui/ListItem';
import AnimatedSticker from '../../../common/AnimatedSticker';
import InputText from '../../../ui/InputText';
import PrivateChatInfo from '../../../common/PrivateChatInfo';
import GroupChatInfo from '../../../common/GroupChatInfo';
import FloatingActionButton from '../../../ui/FloatingActionButton';
import Spinner from '../../../ui/Spinner';
import ShowMoreButton from '../../../ui/ShowMoreButton';
type OwnProps = {
state: FoldersState;
dispatch: FolderEditDispatch;
onAddIncludedChats: () => void;
onAddExcludedChats: () => void;
onReset: () => void;
};
type StateProps = {
loadedActiveChatIds?: number[];
loadedArchivedChatIds?: number[];
};
type DispatchProps = Pick<GlobalActions, 'editChatFolder' | 'addChatFolder' | 'loadMoreChats'>;
const SUBMIT_TIMEOUT = 500;
const INITIAL_CHATS_LIMIT = 5;
const ERROR_NO_TITLE = 'Please provide a title for this folder.';
const ERROR_NO_CHATS = 'Please select at least one chat for this folder.';
const SettingsFoldersEdit: FC<OwnProps & StateProps & DispatchProps> = ({
state,
dispatch,
onAddIncludedChats,
onAddExcludedChats,
onReset,
loadedActiveChatIds,
loadedArchivedChatIds,
editChatFolder,
addChatFolder,
loadMoreChats,
}) => {
const [animationData, setAnimationData] = useState<Record<string, any>>();
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
const [isIncludedChatsListExpanded, setIsIncludedChatsListExpanded] = useState(false);
const [isExcludedChatsListExpanded, setIsExcludedChatsListExpanded] = useState(false);
const {
selectedChatIds: includedChatIds,
selectedChatTypes: includedChatTypes,
} = selectChatFilters(state, 'included');
const {
selectedChatIds: excludedChatIds,
selectedChatTypes: excludedChatTypes,
} = selectChatFilters(state, 'excluded');
useEffect(() => {
if (!animationData) {
getAnimationData('FoldersNew').then(setAnimationData);
}
}, [animationData]);
useEffect(() => {
setIsIncludedChatsListExpanded(false);
setIsExcludedChatsListExpanded(false);
}, [state.folderId]);
const [visibleIncludedChatIds, visibleExcludedChatIds] = useMemo(() => {
const allLoadedChatsSet = new Set([
...loadedActiveChatIds || [],
...loadedArchivedChatIds || [],
]);
const loadedIncludedChatIds = findIntersectionWithSet(includedChatIds, allLoadedChatsSet);
const loadedExcludedChatIds = findIntersectionWithSet(excludedChatIds, allLoadedChatsSet);
return [
isIncludedChatsListExpanded
? loadedIncludedChatIds
: loadedIncludedChatIds.slice(0, INITIAL_CHATS_LIMIT - includedChatTypes.length),
isExcludedChatsListExpanded
? loadedExcludedChatIds
: loadedExcludedChatIds.slice(0, INITIAL_CHATS_LIMIT - excludedChatTypes.length),
];
}, [
excludedChatIds, includedChatIds, includedChatTypes, excludedChatTypes,
isExcludedChatsListExpanded, isIncludedChatsListExpanded,
loadedActiveChatIds, loadedArchivedChatIds,
]);
useEffect(() => {
if (
visibleIncludedChatIds.length < includedChatIds.length
|| visibleExcludedChatIds.length < excludedChatIds.length
) {
loadMoreChats({ listType: 'active' });
}
}, [
loadMoreChats,
excludedChatIds.length,
includedChatIds.length,
visibleExcludedChatIds.length,
visibleIncludedChatIds.length,
]);
const lang = useLang();
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { currentTarget } = event;
dispatch({ type: 'setTitle', payload: currentTarget.value.trim() });
}
function handleSubmit() {
const { title } = state.folder;
if (!title) {
dispatch({ type: 'setError', payload: ERROR_NO_TITLE });
return;
}
if (!includedChatIds.length && !Object.keys(includedChatTypes).length) {
dispatch({ type: 'setError', payload: ERROR_NO_CHATS });
return;
}
dispatch({ type: 'setIsLoading', payload: true });
if (state.mode === 'edit') {
editChatFolder({ id: state.folderId, folderUpdate: state.folder });
} else {
addChatFolder({ folder: state.folder });
}
setTimeout(() => {
onReset();
}, SUBMIT_TIMEOUT);
}
function renderChatType(key: string, mode: 'included' | 'excluded') {
const chatType = mode === 'included'
? INCLUDED_CHAT_TYPES.find(({ key: typeKey }) => typeKey === key)
: EXCLUDED_CHAT_TYPES.find(({ key: typeKey }) => typeKey === key);
if (!chatType) {
return undefined;
}
return (
<ListItem
key={chatType.key}
className="settings-folders-list-item mb-1"
icon={chatType.icon}
narrow
inactive
>
{lang(chatType.title)}
</ListItem>
);
}
function renderChats(mode: 'included' | 'excluded') {
const selectedChatTypes = mode === 'included' ? includedChatTypes : excludedChatTypes;
const visibleChatIds = mode === 'included' ? visibleIncludedChatIds : visibleExcludedChatIds;
const isExpanded = mode === 'included' ? isIncludedChatsListExpanded : isExcludedChatsListExpanded;
const allChatIds = mode === 'included' ? includedChatIds : excludedChatIds;
const leftChatsCount = allChatIds.length - selectedChatTypes.length - visibleChatIds.length;
const clickHandler = mode === 'included'
? () => setIsIncludedChatsListExpanded(true)
: () => setIsExcludedChatsListExpanded(true);
return (
<>
{selectedChatTypes.map((key) => renderChatType(key, mode))}
{visibleChatIds.map((id) => (
<ListItem
className="settings-folders-list-item mb-1"
narrow
inactive
>
{isChatPrivate(id) ? (
<PrivateChatInfo avatarSize="small" userId={id} />
) : (
<GroupChatInfo avatarSize="small" chatId={id} />
)}
</ListItem>
))}
{(!isExpanded && leftChatsCount > 0) && (
<ShowMoreButton
count={leftChatsCount}
itemName="chat"
onClick={clickHandler}
/>
)}
</>
);
}
return (
<div className="settings-fab-wrapper">
<div className="settings-content custom-scroll">
<div className="settings-content-header">
<div className="settings-content-icon">
{animationData && (
<AnimatedSticker
id="settingsFoldersEdit"
size={STICKER_SIZE_FOLDER_SETTINGS}
animationData={animationData}
play={isAnimationLoaded && String(state.folderId)}
noLoop
onLoad={handleAnimationLoad}
/>
)}
</div>
{state.mode === 'create' && (
<p className="settings-item-description mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('FilterIncludeInfo')}
</p>
)}
<InputText
className="mb-0"
label={lang('FilterNameHint')}
value={state.folder.title}
onChange={handleChange}
error={state.error && state.error === ERROR_NO_TITLE ? ERROR_NO_TITLE : undefined}
/>
</div>
<div className="settings-item no-border pt-3">
{state.error && state.error === ERROR_NO_CHATS && (
<p className="settings-item-description color-danger mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
{state.error}
</p>
)}
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterInclude')}</h4>
<ListItem
className="settings-folders-list-item color-primary mb-0"
icon="add"
onClick={onAddIncludedChats}
>
{lang('FilterAddChats')}
</ListItem>
{renderChats('included')}
</div>
<div className="settings-item no-border pt-3">
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
<ListItem
className="settings-folders-list-item color-primary mb-0"
icon="add"
onClick={onAddExcludedChats}
>
{lang('FilterAddChats')}
</ListItem>
{renderChats('excluded')}
</div>
</div>
<FloatingActionButton
isShown={!!state.isTouched}
disabled={state.isLoading}
onClick={handleSubmit}
ariaLabel={state.mode === 'edit' ? 'Save changes' : 'Create folder'}
>
{state.isLoading ? (
<Spinner color="white" />
) : (
<i className="icon-check" />
)}
</FloatingActionButton>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { listIds } = global.chats;
return {
loadedActiveChatIds: listIds.active,
loadedArchivedChatIds: listIds.archived,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['editChatFolder', 'addChatFolder', 'loadMoreChats']),
)(SettingsFoldersEdit));