227 lines
7.3 KiB
TypeScript

import { memo, useCallback, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global';
import type { GlobalState } from '../../../global/types';
import type { CustomPeer } from '../../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { ANIMATION_LEVEL_MIN, ARCHIVED_FOLDER_ID } from '../../../config';
import { getChatTitle } from '../../../global/helpers';
import { selectChat } from '../../../global/selectors';
import { selectAnimationLevel } from '../../../global/selectors/sharedState';
import { selectThreadReadState } from '../../../global/selectors/threads';
import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
import { compact } from '../../../util/iteratees';
import { formatIntegerCompact } from '../../../util/textFormat';
import renderText from '../../common/helpers/renderText';
import { ChatAnimationTypes } from './hooks';
import useSelector from '../../../hooks/data/useSelector';
import { useFolderManagerForOrderedIds, useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
import useLang from '../../../hooks/useLang';
import useSyncEffect from '../../../hooks/useSyncEffect';
import Avatar from '../../common/Avatar';
import Icon from '../../common/icons/Icon';
import Badge from '../../ui/Badge';
import ListItem, { type MenuItemContextAction } from '../../ui/ListItem';
import styles from './Archive.module.scss';
type OwnProps = {
archiveSettings: GlobalState['archiveSettings'];
isFoldersSidebarShown?: boolean;
offsetTop?: number;
animationType: ChatAnimationTypes;
onDragEnter?: NoneToVoidFunction;
onClick?: NoneToVoidFunction;
};
const PREVIEW_SLICE = 5;
const ARCHIVE_CUSTOM_PEER: CustomPeer = {
isCustomPeer: true,
title: 'Archived Chats',
avatarIcon: 'archive-filled',
customPeerAvatarColor: '#9EAAB5',
};
const ANIMATION_RESET_DELAY = 200;
const Archive = ({
archiveSettings,
isFoldersSidebarShown,
offsetTop,
animationType,
onDragEnter,
onClick,
}: OwnProps) => {
const { updateArchiveSettings } = getActions();
const ref = useRef<HTMLDivElement>();
const animationLevel = useSelector(selectAnimationLevel);
const shouldAnimateRef = useRef(animationLevel !== ANIMATION_LEVEL_MIN);
const lang = useLang();
useSyncEffect(() => {
if (animationLevel === ANIMATION_LEVEL_MIN) {
shouldAnimateRef.current = false;
return undefined;
}
if (animationType !== ChatAnimationTypes.None) {
shouldAnimateRef.current = true;
return undefined;
}
// Keep animation alive slightly longer to avoid jumps
const timeout = setTimeout(() => {
shouldAnimateRef.current = false;
}, ANIMATION_RESET_DELAY);
const element = ref.current;
if (element) {
waitForTransitionEnd(element, () => {
shouldAnimateRef.current = false;
}, 'transform');
}
return () => clearTimeout(timeout);
}, [animationType, animationLevel]);
const orderedChatIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
const unreadCounters = useFolderManagerForUnreadCounters();
const archiveUnreadCount = unreadCounters[ARCHIVED_FOLDER_ID]?.chatsCount;
const previewItems = useMemo(() => {
if (!orderedChatIds?.length) return lang('Loading');
const global = getGlobal();
return orderedChatIds.slice(0, PREVIEW_SLICE).map((chatId, i, arr) => {
const isLast = i === arr.length - 1;
const chat = selectChat(global, chatId);
const readState = selectThreadReadState(global, chatId, MAIN_THREAD_ID);
if (!chat) {
return undefined;
}
const title = getChatTitle(lang, chat);
return (
<>
<span className={buildClassName(styles.chat, archiveUnreadCount && readState?.unreadCount && styles.unread)}>
{renderText(title)}
</span>
{isLast ? '' : ', '}
</>
);
});
}, [orderedChatIds, lang, archiveUnreadCount]);
const contextActions = useMemo(() => {
const actionMinimize = !archiveSettings.isMinimized && {
title: lang('ContextArchiveCollapse'),
icon: 'collapse',
handler: () => {
updateArchiveSettings({ isMinimized: true });
},
} satisfies MenuItemContextAction;
const actionExpand = archiveSettings.isMinimized && {
title: lang('ContextArchiveExpand'),
icon: 'expand',
handler: () => {
updateArchiveSettings({ isMinimized: false });
},
} satisfies MenuItemContextAction;
const actionHide = {
title: lang('ContextArchiveToMenu'),
icon: 'archive-to-main',
handler: () => {
updateArchiveSettings({ isHidden: true });
},
} satisfies MenuItemContextAction;
return compact([actionMinimize, actionExpand, actionHide]);
}, [archiveSettings.isMinimized, lang, updateArchiveSettings]);
const handleDragEnter = useCallback((e) => {
e.preventDefault();
onDragEnter?.();
}, [onDragEnter]);
function renderCollapsed() {
return (
<div className={buildClassName(styles.info, 'info')}>
<div className="info-row">
<div className={buildClassName('title', styles.title)}>
<h3 dir="auto" className={buildClassName(styles.name, 'fullName')}>
<Icon name="archive-filled" className={styles.icon} />
{lang('ArchivedChats')}
</h3>
</div>
<Badge
className={styles.unreadCount}
text={archiveUnreadCount ? formatIntegerCompact(lang, archiveUnreadCount) : undefined}
/>
</div>
</div>
);
}
function renderRegular() {
return (
<>
<div className={buildClassName('status', styles.avatarWrapper)}>
<Avatar peer={ARCHIVE_CUSTOM_PEER} />
</div>
<div className={buildClassName(styles.info, 'info')}>
<div className="info-row">
<div className={buildClassName('title', styles.title)}>
<h3 dir="auto" className={buildClassName(styles.name, 'fullName')}>{lang('ArchivedChats')}</h3>
</div>
</div>
<div className="subtitle">
<div className={buildClassName('status', styles.chatsPreview)}>
{previewItems}
</div>
<Badge
className={styles.unreadCount}
text={archiveUnreadCount ? formatIntegerCompact(lang, archiveUnreadCount) : undefined}
/>
</div>
</div>
</>
);
}
return (
<ListItem
ref={ref}
onClick={onClick}
onDragEnter={handleDragEnter}
className={buildClassName(
styles.root,
archiveSettings.isMinimized && styles.minimized,
isFoldersSidebarShown && archiveSettings.isMinimized && styles.noMarginTop,
!shouldAnimateRef.current && styles.noAnimation,
offsetTop && styles.noMarginTop,
'chat-item-clickable',
'chat-item-archive',
)}
style={buildStyle(Boolean(offsetTop) && `transform: translateY(${offsetTop}px)`)}
buttonClassName={styles.button}
contextActions={contextActions}
withPortalForMenu
>
{archiveSettings.isMinimized ? renderCollapsed() : renderRegular()}
</ListItem>
);
};
export default memo(Archive);