TelegramPWA/src/components/left/main/ChatFolders.tsx
2023-04-26 21:18:42 +04:00

352 lines
11 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useMemo, useRef,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChatFolder, ApiChatlistExportedInvite } from '../../../api/types';
import type { SettingsScreens, LeftColumnContent } from '../../../types';
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import type { GlobalState } from '../../../global/types';
import type { TabWithProperties } from '../../ui/TabList';
import { ALL_FOLDER_ID } from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { selectCurrentLimit } from '../../../global/selectors/limits';
import { selectCanShareFolder, selectTabState } from '../../../global/selectors';
import useShowTransition from '../../../hooks/useShowTransition';
import useLang from '../../../hooks/useLang';
import useHistoryBack from '../../../hooks/useHistoryBack';
import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
import Transition from '../../ui/Transition';
import TabList from '../../ui/TabList';
import ChatList from './ChatList';
type OwnProps = {
onSettingsScreenSelect: (screen: SettingsScreens) => void;
foldersDispatch: FolderEditDispatch;
onLeftColumnContentChange: (content: LeftColumnContent) => void;
shouldHideFolderTabs?: boolean;
isForumPanelOpen?: boolean;
};
type StateProps = {
chatFoldersById: Record<number, ApiChatFolder>;
folderInvitesById: Record<number, ApiChatlistExportedInvite[]>;
orderedFolderIds?: number[];
activeChatFolder: number;
currentUserId?: string;
lastSyncTime?: number;
shouldSkipHistoryAnimations?: boolean;
maxFolders: number;
maxFolderInvites: number;
hasArchivedChats?: boolean;
archiveSettings: GlobalState['archiveSettings'];
};
const SAVED_MESSAGES_HOTKEY = '0';
const FIRST_FOLDER_INDEX = 0;
const ChatFolders: FC<OwnProps & StateProps> = ({
foldersDispatch,
onSettingsScreenSelect,
onLeftColumnContentChange,
chatFoldersById,
orderedFolderIds,
activeChatFolder,
currentUserId,
isForumPanelOpen,
lastSyncTime,
shouldSkipHistoryAnimations,
maxFolders,
shouldHideFolderTabs,
folderInvitesById,
maxFolderInvites,
hasArchivedChats,
archiveSettings,
}) => {
const {
loadChatFolders,
setActiveChatFolder,
openChat,
openShareChatFolderModal,
openDeleteChatFolderModal,
openEditChatFolder,
openLimitReachedModal,
} = getActions();
// eslint-disable-next-line no-null/no-null
const transitionRef = useRef<HTMLDivElement>(null);
const lang = useLang();
useEffect(() => {
if (lastSyncTime) {
loadChatFolders();
}
}, [lastSyncTime, loadChatFolders]);
const allChatsFolder: ApiChatFolder = useMemo(() => {
return {
id: ALL_FOLDER_ID,
title: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats'),
includedChatIds: MEMO_EMPTY_ARRAY,
excludedChatIds: MEMO_EMPTY_ARRAY,
} satisfies ApiChatFolder;
}, [orderedFolderIds, lang]);
const displayedFolders = useMemo(() => {
return orderedFolderIds
? orderedFolderIds.map((id) => {
if (id === ALL_FOLDER_ID) {
return allChatsFolder;
}
return chatFoldersById[id] || {};
}).filter(Boolean)
: undefined;
}, [chatFoldersById, allChatsFolder, orderedFolderIds]);
const allChatsFolderIndex = displayedFolders?.findIndex((folder) => folder.id === ALL_FOLDER_ID);
const isInAllChatsFolder = allChatsFolderIndex === activeChatFolder;
const isInFirstFolder = FIRST_FOLDER_INDEX === activeChatFolder;
const folderCountersById = useFolderManagerForUnreadCounters();
const folderTabs = useMemo(() => {
if (!displayedFolders || !displayedFolders.length) {
return undefined;
}
const global = getGlobal();
return displayedFolders.map((folder, i) => {
const { id, title } = folder;
const isBlocked = id !== ALL_FOLDER_ID && i > maxFolders - 1;
const canShareFolder = selectCanShareFolder(global, id);
const contextActions = [];
if (canShareFolder) {
contextActions.push({
title: lang('ChatList.ContextMenuShare'),
icon: 'link',
handler: () => {
// Greater amount can be after premium downgrade
if (folderInvitesById[id]?.length >= maxFolderInvites) {
openLimitReachedModal({
limit: 'chatlistInvites',
});
} else {
openShareChatFolderModal({
folderId: id,
});
}
},
});
}
if (id !== ALL_FOLDER_ID) {
contextActions.push({
title: lang('FilterEdit'),
icon: 'edit',
handler: () => {
openEditChatFolder({ folderId: id });
},
});
contextActions.push({
title: lang('FilterDeleteItem'),
icon: 'delete',
destructive: true,
handler: () => {
openDeleteChatFolderModal({ folderId: id });
},
});
}
return {
id,
title,
badgeCount: folderCountersById[id]?.chatsCount,
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
isBlocked,
contextActions: contextActions?.length ? contextActions : undefined,
} satisfies TabWithProperties;
});
}, [displayedFolders, folderCountersById, lang, maxFolders, folderInvitesById, maxFolderInvites]);
const handleSwitchTab = useCallback((index: number) => {
setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true });
}, [setActiveChatFolder]);
// Prevent `activeTab` pointing at non-existing folder after update
useEffect(() => {
if (!folderTabs?.length) {
return;
}
if (activeChatFolder >= folderTabs.length) {
setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX });
}
}, [activeChatFolder, folderTabs, setActiveChatFolder]);
useEffect(() => {
if (!IS_TOUCH_ENV || !folderTabs?.length || isForumPanelOpen) {
return undefined;
}
return captureEvents(transitionRef.current!, {
selectorToPreventScroll: '.chat-list',
onSwipe: ((e, direction) => {
if (direction === SwipeDirection.Left) {
setActiveChatFolder(
{ activeChatFolder: Math.min(activeChatFolder + 1, folderTabs.length - 1) },
{ forceOnHeavyAnimation: true },
);
return true;
} else if (direction === SwipeDirection.Right) {
setActiveChatFolder({ activeChatFolder: Math.max(0, activeChatFolder - 1) }, { forceOnHeavyAnimation: true });
return true;
}
return false;
}),
});
}, [activeChatFolder, folderTabs, isForumPanelOpen, setActiveChatFolder]);
const isNotInFirstFolderRef = useRef();
isNotInFirstFolderRef.current = !isInFirstFolder;
useEffect(() => (isNotInFirstFolderRef.current ? captureEscKeyListener(() => {
if (isNotInFirstFolderRef.current) {
setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX });
}
}) : undefined), [activeChatFolder, setActiveChatFolder]);
useHistoryBack({
isActive: !isInFirstFolder,
onBack: () => setActiveChatFolder({ activeChatFolder: FIRST_FOLDER_INDEX }, { forceOnHeavyAnimation: true }),
});
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({ activeChatFolder: folder }, { forceOnHeavyAnimation: true });
e.preventDefault();
}
};
document.addEventListener('keydown', handleKeyDown, true);
return () => {
document.removeEventListener('keydown', handleKeyDown, true);
};
}, [currentUserId, folderTabs, openChat, setActiveChatFolder]);
const {
shouldRender: shouldRenderPlaceholder, transitionClassNames,
} = useShowTransition(!orderedFolderIds, undefined, true);
function renderCurrentTab(isActive: boolean) {
const activeFolder = Object.values(chatFoldersById)
.find(({ id }) => id === folderTabs![activeChatFolder].id);
const isFolder = activeFolder && !isInAllChatsFolder;
return (
<ChatList
folderType={isFolder ? 'folder' : 'all'}
folderId={isFolder ? activeFolder.id : undefined}
isActive={isActive}
isForumPanelOpen={isForumPanelOpen}
lastSyncTime={lastSyncTime}
foldersDispatch={foldersDispatch}
onSettingsScreenSelect={onSettingsScreenSelect}
onLeftColumnContentChange={onLeftColumnContentChange}
canDisplayArchive={hasArchivedChats && !archiveSettings.isHidden}
archiveSettings={archiveSettings}
/>
);
}
const shouldRenderFolders = folderTabs && folderTabs.length > 1;
return (
<div
className={buildClassName(
'ChatFolders',
shouldRenderFolders && shouldHideFolderTabs && 'ChatFolders--tabs-hidden',
)}
>
{shouldRenderFolders ? (
<TabList
contextRootElementSelector="#LeftColumn"
tabs={folderTabs}
activeTab={activeChatFolder}
onSwitchTab={handleSwitchTab}
areFolders
/>
) : shouldRenderPlaceholder ? (
<div className={buildClassName('tabs-placeholder', transitionClassNames)} />
) : undefined}
<Transition
ref={transitionRef}
name={shouldSkipHistoryAnimations ? 'none' : lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized'}
activeKey={activeChatFolder}
renderCount={shouldRenderFolders ? folderTabs.length : undefined}
>
{renderCurrentTab}
</Transition>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
chatFolders: {
byId: chatFoldersById,
orderedIds: orderedFolderIds,
invites: folderInvitesById,
},
chats: {
listIds: {
archived,
},
},
currentUserId,
lastSyncTime,
archiveSettings,
} = global;
const { shouldSkipHistoryAnimations, activeChatFolder } = selectTabState(global);
return {
chatFoldersById,
folderInvitesById,
orderedFolderIds,
activeChatFolder,
currentUserId,
lastSyncTime,
shouldSkipHistoryAnimations,
hasArchivedChats: Boolean(archived?.length),
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
maxFolderInvites: selectCurrentLimit(global, 'chatlistInvites'),
archiveSettings,
};
},
)(ChatFolders));