274 lines
8.1 KiB
TypeScript
274 lines
8.1 KiB
TypeScript
import React, {
|
|
FC, memo, useCallback, useEffect, useMemo, useRef,
|
|
} from '../../../lib/teact/teact';
|
|
import { withGlobal } from '../../../lib/teact/teactn';
|
|
|
|
import { ApiChat, ApiChatFolder, ApiUser } from '../../../api/types';
|
|
import { GlobalActions } from '../../../global/types';
|
|
import { NotifyException, NotifySettings, SettingsScreens } from '../../../types';
|
|
import { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
|
|
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
|
import { buildCollectionByKey, pick } from '../../../util/iteratees';
|
|
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
|
import { getFolderUnreadDialogs } from '../../../modules/helpers';
|
|
import { selectNotifyExceptions, selectNotifySettings } from '../../../modules/selectors';
|
|
import useShowTransition from '../../../hooks/useShowTransition';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import useThrottledMemo from '../../../hooks/useThrottledMemo';
|
|
import useLang from '../../../hooks/useLang';
|
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
|
|
|
import Transition from '../../ui/Transition';
|
|
import TabList from '../../ui/TabList';
|
|
import ChatList from './ChatList';
|
|
|
|
type OwnProps = {
|
|
onScreenSelect: (screen: SettingsScreens) => void;
|
|
foldersDispatch: FolderEditDispatch;
|
|
};
|
|
|
|
type StateProps = {
|
|
chatsById: Record<string, ApiChat>;
|
|
usersById: Record<string, ApiUser>;
|
|
chatFoldersById: Record<number, ApiChatFolder>;
|
|
notifySettings: NotifySettings;
|
|
notifyExceptions?: Record<number, NotifyException>;
|
|
orderedFolderIds?: number[];
|
|
activeChatFolder: number;
|
|
currentUserId?: string;
|
|
lastSyncTime?: number;
|
|
shouldSkipHistoryAnimations?: boolean;
|
|
};
|
|
|
|
type DispatchProps = Pick<GlobalActions, 'loadChatFolders' | 'setActiveChatFolder' | 'openChat'>;
|
|
|
|
const INFO_THROTTLE = 3000;
|
|
const SAVED_MESSAGES_HOTKEY = '0';
|
|
|
|
const ChatFolders: FC<OwnProps & StateProps & DispatchProps> = ({
|
|
chatsById,
|
|
usersById,
|
|
chatFoldersById,
|
|
notifySettings,
|
|
notifyExceptions,
|
|
orderedFolderIds,
|
|
activeChatFolder,
|
|
currentUserId,
|
|
lastSyncTime,
|
|
shouldSkipHistoryAnimations,
|
|
foldersDispatch,
|
|
onScreenSelect,
|
|
loadChatFolders,
|
|
setActiveChatFolder,
|
|
openChat,
|
|
}) => {
|
|
// eslint-disable-next-line no-null/no-null
|
|
const transitionRef = useRef<HTMLDivElement>(null);
|
|
|
|
const lang = useLang();
|
|
|
|
useEffect(() => {
|
|
if (lastSyncTime) {
|
|
loadChatFolders();
|
|
}
|
|
}, [lastSyncTime, loadChatFolders]);
|
|
|
|
const displayedFolders = useMemo(() => {
|
|
return orderedFolderIds
|
|
? orderedFolderIds.map((id) => chatFoldersById[id] || {}).filter(Boolean)
|
|
: undefined;
|
|
}, [chatFoldersById, orderedFolderIds]);
|
|
|
|
const folderCountersById = useThrottledMemo(() => {
|
|
if (!displayedFolders || !displayedFolders.length) {
|
|
return undefined;
|
|
}
|
|
|
|
const chatIds = Object.keys(chatsById);
|
|
const counters = displayedFolders.map((folder) => {
|
|
const {
|
|
unreadDialogsCount, hasActiveDialogs,
|
|
} = getFolderUnreadDialogs(chatsById, usersById, folder, chatIds, notifySettings, notifyExceptions) || {};
|
|
|
|
return {
|
|
id: folder.id,
|
|
badgeCount: unreadDialogsCount,
|
|
isBadgeActive: hasActiveDialogs,
|
|
};
|
|
});
|
|
|
|
return buildCollectionByKey(counters, 'id');
|
|
}, INFO_THROTTLE, [displayedFolders, chatsById, usersById, notifySettings, notifyExceptions]);
|
|
|
|
const folderTabs = useMemo(() => {
|
|
if (!displayedFolders || !displayedFolders.length) {
|
|
return undefined;
|
|
}
|
|
|
|
return [
|
|
{ title: lang.code === 'en' ? 'All' : lang('FilterAllChats') },
|
|
...displayedFolders.map((folder) => ({
|
|
title: folder.title,
|
|
...(folderCountersById?.[folder.id]),
|
|
})),
|
|
];
|
|
}, [displayedFolders, folderCountersById, lang]);
|
|
|
|
const handleSwitchTab = useCallback((index: number) => {
|
|
setActiveChatFolder(index);
|
|
}, [setActiveChatFolder]);
|
|
|
|
// Prevent `activeTab` pointing at non-existing folder after update
|
|
useEffect(() => {
|
|
if (!folderTabs || !folderTabs.length) {
|
|
return;
|
|
}
|
|
|
|
if (activeChatFolder >= folderTabs.length) {
|
|
setActiveChatFolder(0);
|
|
}
|
|
}, [activeChatFolder, folderTabs, setActiveChatFolder]);
|
|
|
|
useEffect(() => {
|
|
if (!transitionRef.current || !IS_TOUCH_ENV || !folderTabs || !folderTabs.length) {
|
|
return undefined;
|
|
}
|
|
|
|
return captureEvents(transitionRef.current, {
|
|
selectorToPreventScroll: '.chat-list',
|
|
onSwipe: ((e, direction) => {
|
|
if (direction === SwipeDirection.Left) {
|
|
setActiveChatFolder(Math.min(activeChatFolder + 1, folderTabs.length - 1));
|
|
return true;
|
|
} else if (direction === SwipeDirection.Right) {
|
|
setActiveChatFolder(Math.max(0, activeChatFolder - 1));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}),
|
|
});
|
|
}, [activeChatFolder, folderTabs, setActiveChatFolder]);
|
|
|
|
const isNotInAllTabRef = useRef();
|
|
isNotInAllTabRef.current = activeChatFolder !== 0;
|
|
useEffect(() => (isNotInAllTabRef.current ? captureEscKeyListener(() => {
|
|
if (isNotInAllTabRef.current) {
|
|
setActiveChatFolder(0);
|
|
}
|
|
}) : undefined), [activeChatFolder, setActiveChatFolder]);
|
|
|
|
useHistoryBack(activeChatFolder !== 0, () => setActiveChatFolder(0));
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.ctrlKey && e.shiftKey && e.code.startsWith('Digit') && folderTabs) {
|
|
const [, digit] = e.code.match(/Digit(\d)/) || [];
|
|
if (!digit) return;
|
|
|
|
if (digit === SAVED_MESSAGES_HOTKEY) {
|
|
openChat({ id: currentUserId, shouldReplaceHistory: true });
|
|
return;
|
|
}
|
|
|
|
const folder = Number(digit) - 1;
|
|
if (folder > folderTabs.length - 1) return;
|
|
|
|
setActiveChatFolder(folder);
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleKeyDown, true);
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown, true);
|
|
};
|
|
});
|
|
|
|
const {
|
|
shouldRender: shouldRenderPlaceholder, transitionClassNames,
|
|
} = useShowTransition(!orderedFolderIds, undefined, true);
|
|
|
|
function renderCurrentTab(isActive: boolean) {
|
|
const activeFolder = Object.values(chatFoldersById)
|
|
.find(({ title }) => title === folderTabs![activeChatFolder].title);
|
|
|
|
if (!activeFolder || activeChatFolder === 0) {
|
|
return (
|
|
<ChatList
|
|
folderType="all"
|
|
isActive={isActive}
|
|
foldersDispatch={foldersDispatch}
|
|
onScreenSelect={onScreenSelect}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ChatList
|
|
folderType="folder"
|
|
folderId={activeFolder.id}
|
|
isActive={isActive}
|
|
onScreenSelect={onScreenSelect}
|
|
foldersDispatch={foldersDispatch}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="ChatFolders">
|
|
{folderTabs?.length ? (
|
|
<TabList tabs={folderTabs} activeTab={activeChatFolder} onSwitchTab={handleSwitchTab} />
|
|
) : shouldRenderPlaceholder ? (
|
|
<div className={buildClassName('tabs-placeholder', transitionClassNames)} />
|
|
) : undefined}
|
|
<Transition
|
|
ref={transitionRef}
|
|
name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slide-reversed' : 'slide'}
|
|
activeKey={activeChatFolder}
|
|
renderCount={folderTabs ? folderTabs.length : undefined}
|
|
>
|
|
{renderCurrentTab}
|
|
</Transition>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global): StateProps => {
|
|
const {
|
|
chats: { byId: chatsById },
|
|
users: { byId: usersById },
|
|
chatFolders: {
|
|
byId: chatFoldersById,
|
|
orderedIds: orderedFolderIds,
|
|
activeChatFolder,
|
|
},
|
|
currentUserId,
|
|
lastSyncTime,
|
|
shouldSkipHistoryAnimations,
|
|
} = global;
|
|
|
|
return {
|
|
chatsById,
|
|
usersById,
|
|
chatFoldersById,
|
|
orderedFolderIds,
|
|
lastSyncTime,
|
|
notifySettings: selectNotifySettings(global),
|
|
notifyExceptions: selectNotifyExceptions(global),
|
|
activeChatFolder,
|
|
currentUserId,
|
|
shouldSkipHistoryAnimations,
|
|
};
|
|
},
|
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
|
'loadChatFolders',
|
|
'setActiveChatFolder',
|
|
'openChat',
|
|
]),
|
|
)(ChatFolders));
|