315 lines
9.8 KiB
TypeScript
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));
|