Forward Modal: Show folders (#6556)

This commit is contained in:
zubiden 2026-01-13 01:14:16 +01:00 committed by Alexander Zinchuk
parent ade72a0727
commit 74a4fad8d4
6 changed files with 394 additions and 211 deletions

View File

@ -1,11 +1,10 @@
import type { FC } from '../../lib/teact/teact';
import { memo, useMemo, useState } from '../../lib/teact/teact'; import { memo, useMemo, useState } from '../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChatType } from '../../api/types'; import type { ApiChatFolder, ApiChatType } from '../../api/types';
import type { ThreadId } from '../../types'; import type { ThreadId } from '../../types';
import { API_CHAT_TYPES } from '../../config'; import { ALL_FOLDER_ID, API_CHAT_TYPES } from '../../config';
import { import {
getCanPostInChat, getCanPostInChat,
getHasAdminRight, getHasAdminRight,
@ -16,24 +15,30 @@ import { filterPeersByQuery } from '../../global/helpers/peers';
import { import {
filterChatIdsByType, selectChat, selectChatFullInfo, selectIsMonoforumAdmin, selectUser, filterChatIdsByType, selectChat, selectChatFullInfo, selectIsMonoforumAdmin, selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
import { selectCurrentLimit } from '../../global/selectors/limits';
import { unique } from '../../util/iteratees'; import { unique } from '../../util/iteratees';
import sortChatIds from './helpers/sortChatIds'; import sortChatIds from './helpers/sortChatIds';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import { useFolderManagerForOrderedIds } from '../../hooks/useFolderManager';
import useFolderTabs from '../../hooks/useFolderTabs';
import useLastCallback from '../../hooks/useLastCallback';
import TabList from '../ui/TabList';
import ChatOrUserPicker from './pickers/ChatOrUserPicker'; import ChatOrUserPicker from './pickers/ChatOrUserPicker';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
searchPlaceholder: string; searchPlaceholder: string;
className?: string; className?: string;
filter?: ApiChatType[]; filter?: readonly ApiChatType[];
isLowStackPriority?: boolean;
isForwarding?: boolean;
withFolders?: boolean;
loadMore?: NoneToVoidFunction; loadMore?: NoneToVoidFunction;
onSelectRecipient: (peerId: string, threadId?: ThreadId) => void; onSelectRecipient: (peerId: string, threadId?: ThreadId) => void;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
onCloseAnimationEnd?: NoneToVoidFunction; onCloseAnimationEnd?: NoneToVoidFunction;
isLowStackPriority?: boolean;
isForwarding?: boolean;
}; };
type StateProps = { type StateProps = {
@ -42,9 +47,12 @@ type StateProps = {
archivedListIds?: string[]; archivedListIds?: string[];
pinnedIds?: string[]; pinnedIds?: string[];
contactIds?: string[]; contactIds?: string[];
chatFoldersById: Record<number, ApiChatFolder>;
orderedFolderIds?: number[];
maxFolders: number;
}; };
const RecipientPicker: FC<OwnProps & StateProps> = ({ const RecipientPicker = ({
isOpen, isOpen,
currentUserId, currentUserId,
activeListIds, activeListIds,
@ -54,14 +62,47 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
filter = API_CHAT_TYPES, filter = API_CHAT_TYPES,
className, className,
searchPlaceholder, searchPlaceholder,
isLowStackPriority,
chatFoldersById,
orderedFolderIds,
isForwarding,
maxFolders,
withFolders,
loadMore, loadMore,
onSelectRecipient, onSelectRecipient,
onClose, onClose,
onCloseAnimationEnd, onCloseAnimationEnd,
isLowStackPriority, }: OwnProps & StateProps) => {
isForwarding, const { openLimitReachedModal } = getActions();
}) => {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [activeFolderIndex, setActiveFolderIndex] = useState(0);
const { displayedFolders, folderTabs } = useFolderTabs({
sidebarMode: false,
orderedFolderIds,
chatFoldersById,
maxFolders,
isReadOnly: true,
});
const shouldRenderFolders = withFolders && folderTabs?.length && !search;
const displayedFolderId = displayedFolders?.[activeFolderIndex]?.id || ALL_FOLDER_ID;
const orderedChatIds = useFolderManagerForOrderedIds(displayedFolderId);
const handleSwitchFolderIndex = useLastCallback((index: number) => {
const newTab = folderTabs?.[index];
if (!newTab) return;
if (newTab.isBlocked) {
openLimitReachedModal({
limit: 'dialogFilters',
});
return;
}
setActiveFolderIndex(index);
});
const ids = useMemo(() => { const ids = useMemo(() => {
if (!isOpen) return undefined; if (!isOpen) return undefined;
@ -73,10 +114,12 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
const global = getGlobal(); const global = getGlobal();
const peerIds = [ const allIds = shouldRenderFolders ? (orderedChatIds || []) : [
...(activeListIds || []), ...(activeListIds || []),
...((search && archivedListIds) || []), ...((search && archivedListIds) || []),
].filter((id) => { ];
const peerIds = allIds.filter((id) => {
const chat = selectChat(global, id); const chat = selectChat(global, id);
const user = selectUser(global, id); const user = selectUser(global, id);
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages'); const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages');
@ -95,13 +138,15 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
return !chatFullInfo || getCanPostInChat(chat, undefined, undefined, chatFullInfo); return !chatFullInfo || getCanPostInChat(chat, undefined, undefined, chatFullInfo);
}); });
const idsWithAdditions = shouldRenderFolders ? peerIds : unique([
...(currentUserId ? [currentUserId] : []),
...peerIds,
...(contactIds || []),
]);
const sorted = sortChatIds( const sorted = sortChatIds(
filterPeersByQuery({ filterPeersByQuery({
ids: unique([ ids: idsWithAdditions,
...(currentUserId ? [currentUserId] : []),
...peerIds,
...(contactIds || []),
]),
query: search, query: search,
}), }),
undefined, undefined,
@ -120,10 +165,23 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
contactIds, contactIds,
filter, filter,
isForwarding, isForwarding,
orderedChatIds,
shouldRenderFolders,
]); ]);
const renderingIds = useCurrentOrPrev(ids, true)!; const renderingIds = useCurrentOrPrev(ids, true)!;
const chatFolders = useMemo(() => {
if (!shouldRenderFolders) return undefined;
return (
<TabList
tabs={folderTabs}
activeTab={activeFolderIndex}
onSwitchTab={handleSwitchFolderIndex}
/>
);
}, [folderTabs, activeFolderIndex, shouldRenderFolders]);
return ( return (
<ChatOrUserPicker <ChatOrUserPicker
isOpen={isOpen} isOpen={isOpen}
@ -132,6 +190,8 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
currentUserId={currentUserId} currentUserId={currentUserId}
searchPlaceholder={searchPlaceholder} searchPlaceholder={searchPlaceholder}
search={search} search={search}
subheader={chatFolders}
listActiveKey={activeFolderIndex}
onSearchChange={setSearch} onSearchChange={setSearch}
loadMore={loadMore} loadMore={loadMore}
onSelectChatOrUser={onSelectRecipient} onSelectChatOrUser={onSelectRecipient}
@ -145,6 +205,10 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => { (global): Complete<StateProps> => {
const { const {
chatFolders: {
byId: chatFoldersById,
orderedIds: orderedFolderIds,
},
chats: { chats: {
listIds, listIds,
orderedPinnedIds, orderedPinnedIds,
@ -158,6 +222,9 @@ export default memo(withGlobal<OwnProps>(
pinnedIds: orderedPinnedIds.active, pinnedIds: orderedPinnedIds.active,
contactIds: global.contactList?.userIds, contactIds: global.contactList?.userIds,
currentUserId, currentUserId,
chatFoldersById,
orderedFolderIds,
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
}; };
}, },
)(RecipientPicker)); )(RecipientPicker));

View File

@ -17,26 +17,39 @@
} }
.modal-header { .modal-header {
.Button { flex-direction: column;
margin-right: 0.5rem; align-items: stretch;
.search-wrapper {
display: flex;
align-items: center;
.Button {
margin-right: 0.5rem;
}
.input-group {
flex: 1;
margin: 0;
}
.form-control {
unicode-bidi: plaintext;
height: 2.75rem;
padding: 0.5rem;
border: none;
font-size: 1.25rem;
line-height: 1.75rem;
box-shadow: none !important;
}
} }
.input-group { .TabList {
flex: 1; margin-bottom: -0.375rem;
margin: 0; margin-inline: -1rem;
}
.form-control {
unicode-bidi: plaintext;
height: 2.75rem;
padding: 0.5rem;
border: none;
font-size: 1.25rem;
line-height: 1.75rem;
box-shadow: none !important;
} }
} }

View File

@ -1,21 +1,28 @@
import type { FC } from '../../../lib/teact/teact'; import type { TeactNode } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import { import {
memo, useCallback, useMemo, useRef, useState, memo, useCallback, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global'; import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiTopic } from '../../../api/types'; import type { ApiTopic } from '../../../api/types';
import type { GlobalState } from '../../../global/types'; import type { GlobalState } from '../../../global/types';
import type { ThreadId } from '../../../types'; import type { AnimationLevel, ThreadId } from '../../../types';
import { PEER_PICKER_ITEM_HEIGHT_PX } from '../../../config'; import { PEER_PICKER_ITEM_HEIGHT_PX } from '../../../config';
import { import {
getCanPostInChat, getGroupStatus, getUserStatus, isUserOnline, getCanPostInChat, getGroupStatus, getUserStatus, isUserOnline,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { isApiPeerChat } from '../../../global/helpers/peers'; import { isApiPeerChat } from '../../../global/helpers/peers';
import { selectMonoforumChannel, selectPeer, selectTopics, selectUserStatus } from '../../../global/selectors'; import {
selectMonoforumChannel,
selectPeer,
selectTabState,
selectTopics,
selectUserStatus,
} from '../../../global/selectors';
import { selectAnimationLevel } from '../../../global/selectors/sharedState';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { resolveTransitionName } from '../../../util/resolveTransitionName';
import { REM } from '../helpers/mediaDimensions'; import { REM } from '../helpers/mediaDimensions';
import renderText from '../helpers/renderText'; import renderText from '../helpers/renderText';
@ -28,7 +35,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll, { type OwnProps as InfiniteScrollProps } from '../../ui/InfiniteScroll';
import InputText from '../../ui/InputText'; import InputText from '../../ui/InputText';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import Modal from '../../ui/Modal'; import Modal from '../../ui/Modal';
@ -48,6 +55,8 @@ export type OwnProps = {
search: string; search: string;
className?: string; className?: string;
isLowStackPriority?: boolean; isLowStackPriority?: boolean;
listActiveKey?: number;
subheader?: TeactNode;
loadMore?: NoneToVoidFunction; loadMore?: NoneToVoidFunction;
onSearchChange: (search: string) => void; onSearchChange: (search: string) => void;
onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void; onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void;
@ -55,31 +64,40 @@ export type OwnProps = {
onCloseAnimationEnd?: NoneToVoidFunction; onCloseAnimationEnd?: NoneToVoidFunction;
}; };
type StateProps = {
animationLevel: AnimationLevel;
shouldSkipHistoryAnimations?: boolean;
};
const CHAT_LIST_SLIDE = 0; const CHAT_LIST_SLIDE = 0;
const TOPIC_LIST_SLIDE = 1; const TOPIC_LIST_SLIDE = 1;
const TOPIC_ICON_SIZE = 2.75 * REM; const TOPIC_ICON_SIZE = 2.75 * REM;
const ITEM_CLASS_NAME = 'ChatOrUserPicker-item'; const ITEM_CLASS_NAME = 'ChatOrUserPicker-item';
const TOPIC_ITEM_HEIGHT_PX = 56; const TOPIC_ITEM_HEIGHT_PX = 56;
const ChatOrUserPicker: FC<OwnProps> = ({ const ChatOrUserPicker = ({
isOpen, isOpen,
currentUserId, currentUserId,
chatOrUserIds, chatOrUserIds,
search, search,
searchPlaceholder, searchPlaceholder,
className, className,
isLowStackPriority,
subheader,
listActiveKey,
animationLevel,
shouldSkipHistoryAnimations,
loadMore, loadMore,
onSearchChange, onSearchChange,
onSelectChatOrUser, onSelectChatOrUser,
onClose, onClose,
onCloseAnimationEnd, onCloseAnimationEnd,
isLowStackPriority, }: OwnProps & StateProps) => {
}) => {
const { loadTopics } = getActions(); const { loadTopics } = getActions();
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
const containerRef = useRef<HTMLDivElement>(); const [chatKeyDownHandler, setChatKeyDownHandler] = useState<React.KeyboardEventHandler<HTMLDivElement>>();
const topicContainerRef = useRef<HTMLDivElement>(); const topicContainerRef = useRef<HTMLDivElement>();
const searchRef = useRef<HTMLInputElement>(); const searchRef = useRef<HTMLInputElement>();
const topicSearchRef = useRef<HTMLInputElement>(); const topicSearchRef = useRef<HTMLInputElement>();
@ -147,7 +165,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
setTopicSearch(e.currentTarget.value); setTopicSearch(e.currentTarget.value);
}); });
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => { const handleChatSelect = useLastCallback((index) => {
if (viewportIds && viewportIds.length > 0) { if (viewportIds && viewportIds.length > 0) {
const chatsById = getGlobal().chats.byId; const chatsById = getGlobal().chats.byId;
@ -160,7 +178,11 @@ const ChatOrUserPicker: FC<OwnProps> = ({
onSelectChatOrUser(chatId); onSelectChatOrUser(chatId);
} }
} }
}, `.${ITEM_CLASS_NAME}`, true); });
const handleKeyDownHandlerUpdate = useLastCallback((handler: React.KeyboardEventHandler<HTMLDivElement>) => {
setChatKeyDownHandler(() => handler);
});
const handleTopicKeyDown = useKeyboardListNavigation(topicContainerRef, isOpen, (index) => { const handleTopicKeyDown = useKeyboardListNavigation(topicContainerRef, isOpen, (index) => {
if (topicIds?.length) { if (topicIds?.length) {
@ -246,50 +268,57 @@ const ChatOrUserPicker: FC<OwnProps> = ({
return ( return (
<> <>
<div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}> <div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
<Button <div className="search-wrapper">
round <Button
color="translucent" round
size="tiny" color="translucent"
ariaLabel={oldLang('Back')} size="tiny"
onClick={handleHeaderBackClick} ariaLabel={oldLang('Back')}
iconName="arrow-left" onClick={handleHeaderBackClick}
/> iconName="arrow-left"
<InputText
ref={topicSearchRef}
value={topicSearch}
onChange={handleTopicSearchChange}
onKeyDown={handleTopicKeyDown}
placeholder={searchPlaceholder}
/>
</div>
<InfiniteScroll
ref={topicContainerRef}
className="picker-list custom-scroll"
items={topicIds}
withAbsolutePositioning
maxHeight={(topicIds?.length || 0) * TOPIC_ITEM_HEIGHT_PX}
onKeyDown={handleTopicKeyDown}
>
{!topicIds && <Loading />}
{topicIds?.map((topicId, i) => (
<PickerItem
key={`${forumId}_${topicId}`}
className={ITEM_CLASS_NAME}
onClick={() => onSelectChatOrUser(forumId!, topicId)}
style={`top: ${(viewportOffset + i) * TOPIC_ITEM_HEIGHT_PX}px;`}
avatarElement={(
<TopicIcon
size={TOPIC_ICON_SIZE}
topic={topics[topicId]}
className="topic-icon"
letterClassName="topic-icon-letter"
/>
)}
title={renderText(topics[topicId].title)}
/> />
))} <InputText
</InfiniteScroll> ref={topicSearchRef}
value={topicSearch}
onChange={handleTopicSearchChange}
onKeyDown={handleTopicKeyDown}
placeholder={searchPlaceholder}
/>
</div>
</div>
{topicIds?.length ? (
<InfiniteScroll
ref={topicContainerRef}
className="picker-list custom-scroll"
items={topicIds}
withAbsolutePositioning
maxHeight={(topicIds?.length || 0) * TOPIC_ITEM_HEIGHT_PX}
onKeyDown={handleTopicKeyDown}
>
{topicIds.map((topicId, i) => (
<PickerItem
key={`${forumId}_${topicId}`}
className={ITEM_CLASS_NAME}
onClick={() => onSelectChatOrUser(forumId!, topicId)}
style={`top: ${i * TOPIC_ITEM_HEIGHT_PX}px;`}
avatarElement={(
<TopicIcon
size={TOPIC_ICON_SIZE}
topic={topics[topicId]}
className="topic-icon"
letterClassName="topic-icon-letter"
/>
)}
title={renderText(topics[topicId].title)}
/>
))}
</InfiniteScroll>
) : topicIds && !topicIds.length ? (
<p className="no-results">{lang('NothingFound')}</p>
) : (
<Loading />
)}
</> </>
); );
} }
@ -298,40 +327,40 @@ const ChatOrUserPicker: FC<OwnProps> = ({
return ( return (
<> <>
<div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}> <div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
<Button <div className="search-wrapper">
round <Button
color="translucent" round
size="tiny" color="translucent"
ariaLabel={oldLang('Close')} size="tiny"
onClick={onClose} ariaLabel={oldLang('Close')}
iconName="close" onClick={onClose}
/> iconName="close"
<InputText />
ref={searchRef} <InputText
value={search} ref={searchRef}
onChange={handleSearchChange} value={search}
onKeyDown={handleKeyDown} onChange={handleSearchChange}
placeholder={searchPlaceholder} onKeyDown={chatKeyDownHandler}
/> placeholder={searchPlaceholder}
/>
</div>
{subheader}
</div> </div>
{viewportIds?.length ? ( <Transition
<InfiniteScroll activeKey={listActiveKey || 0}
ref={containerRef} name={resolveTransitionName('slideOptimized', animationLevel, shouldSkipHistoryAnimations, lang.isRtl)}
className="picker-list custom-scroll" slideClassName="ChatOrUserPicker_slide"
items={viewportIds} >
itemSelector={`.${ITEM_CLASS_NAME}`} <ChatListContent
onLoadMore={getMore} isOpen={isOpen}
withAbsolutePositioning viewportIds={viewportIds}
maxHeight={chatOrUserIds.length * PEER_PICKER_ITEM_HEIGHT_PX} maxHeight={chatOrUserIds.length * PEER_PICKER_ITEM_HEIGHT_PX}
onKeyDown={handleKeyDown} onLoadMore={getMore}
> onSelect={handleChatSelect}
{viewportIds.map(renderChatItem)} renderItem={renderChatItem}
</InfiniteScroll> onKeyDownHandlerUpdate={handleKeyDownHandlerUpdate}
) : viewportIds && !viewportIds.length ? ( />
<p className="no-results">{oldLang('lng_blocked_list_not_found')}</p> </Transition>
) : (
<Loading />
)}
</> </>
); );
} }
@ -353,4 +382,63 @@ const ChatOrUserPicker: FC<OwnProps> = ({
); );
}; };
export default memo(ChatOrUserPicker); type ChatListContentProps = {
isOpen: boolean;
viewportIds?: string[];
maxHeight: number;
onLoadMore: InfiniteScrollProps['onLoadMore'];
onSelect: (index: number) => void;
renderItem: (id: string, index: number) => TeactNode;
onKeyDownHandlerUpdate: (handler: React.KeyboardEventHandler<HTMLDivElement>) => void;
};
function ChatListContent({
isOpen,
viewportIds,
maxHeight,
onLoadMore,
onSelect,
onKeyDownHandlerUpdate,
renderItem,
}: ChatListContentProps) {
const lang = useLang();
const containerRef = useRef<HTMLDivElement>();
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, onSelect, `.${ITEM_CLASS_NAME}`, true);
useEffect(() => {
onKeyDownHandlerUpdate(handleKeyDown);
}, [handleKeyDown, onKeyDownHandlerUpdate]);
return (
<>
{viewportIds?.length ? (
<InfiniteScroll
ref={containerRef}
className="picker-list custom-scroll"
items={viewportIds}
itemSelector={`.${ITEM_CLASS_NAME}`}
onLoadMore={onLoadMore}
withAbsolutePositioning
maxHeight={maxHeight}
onKeyDown={handleKeyDown}
>
{viewportIds.map(renderItem)}
</InfiniteScroll>
) : viewportIds && !viewportIds.length ? (
<p className="no-results">{lang('NothingFound')}</p>
) : (
<Loading />
)}
</>
);
}
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
return {
animationLevel: selectAnimationLevel(global),
shouldSkipHistoryAnimations: selectTabState(global).shouldSkipHistoryAnimations,
};
},
)(ChatOrUserPicker));

View File

@ -119,6 +119,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
onClose={handleClose} onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown} onCloseAnimationEnd={unmarkIsShown}
isForwarding={isForwarding} isForwarding={isForwarding}
withFolders
/> />
); );
}; };

View File

@ -15,7 +15,7 @@ import { debounce } from '../../util/schedulers';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
type OwnProps = { export type OwnProps = {
ref?: ElementRef<HTMLDivElement>; ref?: ElementRef<HTMLDivElement>;
style?: string; style?: string;
className?: string; className?: string;

View File

@ -23,23 +23,35 @@ type FolderNameOptions = {
emojiSize?: number; emojiSize?: number;
}; };
const useFolderTabs = ({ type Params = {
sidebarMode,
orderedFolderIds,
chatFoldersById,
maxFolders,
maxChatLists,
folderInvitesById,
maxFolderInvites,
}: {
sidebarMode: boolean; sidebarMode: boolean;
orderedFolderIds?: number[]; orderedFolderIds?: number[];
chatFoldersById: Record<number, ApiChatFolder>; chatFoldersById: Record<number, ApiChatFolder>;
maxFolders: number; maxFolders: number;
} & ({
isReadOnly?: false;
maxChatLists: number; maxChatLists: number;
folderInvitesById: Record<number, ApiChatlistExportedInvite[]>; folderInvitesById: Record<number, ApiChatlistExportedInvite[]>;
maxFolderInvites: number; maxFolderInvites: number;
}) => { } | {
isReadOnly: true;
});
const useFolderTabs = (params: Params) => {
const {
sidebarMode,
orderedFolderIds,
chatFoldersById,
maxFolders,
isReadOnly,
} = params;
const {
maxChatLists,
folderInvitesById,
maxFolderInvites,
} = !isReadOnly ? params : {};
const lang = useLang(); const lang = useLang();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
@ -97,89 +109,92 @@ const useFolderTabs = ({
const canShareFolder = selectCanShareFolder(getGlobal(), id); const canShareFolder = selectCanShareFolder(getGlobal(), id);
const contextActions: MenuItemContextAction[] = []; const contextActions: MenuItemContextAction[] = [];
if (canShareFolder) { if (!isReadOnly) {
contextActions.push({ if (canShareFolder) {
title: lang('FilterShare'), contextActions.push({
icon: 'link', title: lang('FilterShare'),
handler: () => { icon: 'link',
const chatListCount = Object.values(chatFoldersById).reduce((acc, el) => acc + (el.isChatList ? 1 : 0), 0); handler: () => {
if (chatListCount >= maxChatLists && !folder.isChatList) { const chatListCount = Object.values(chatFoldersById)
openLimitReachedModal({ .reduce((acc, el) => acc + (el.isChatList ? 1 : 0), 0);
limit: 'chatlistJoined', if (chatListCount >= maxChatLists! && !folder.isChatList) {
}); openLimitReachedModal({
return; limit: 'chatlistJoined',
} });
return;
}
// Greater amount can be after premium downgrade // Greater amount can be after premium downgrade
if (folderInvitesById[id]?.length >= maxFolderInvites) { if (folderInvitesById![id]?.length >= maxFolderInvites!) {
openLimitReachedModal({ openLimitReachedModal({
limit: 'chatlistInvites', limit: 'chatlistInvites',
}); });
return; return;
} }
openShareChatFolderModal({ openShareChatFolderModal({
folderId: id, folderId: id,
});
},
});
}
if (id === ALL_FOLDER_ID) {
contextActions.push({
title: lang('FilterEditFolders'),
icon: 'edit',
handler: () => {
openSettingsScreen({ screen: SettingsScreens.Folders });
},
});
if (folderUnreadChatsCountersById[id]?.length) {
contextActions.push({
title: lang('ChatListMarkAllAsRead'),
icon: 'readchats',
handler: () => handleReadAllChats(folder.id),
}); });
}, }
}); } else {
}
if (id === ALL_FOLDER_ID) {
contextActions.push({
title: lang('FilterEditFolders'),
icon: 'edit',
handler: () => {
openSettingsScreen({ screen: SettingsScreens.Folders });
},
});
if (folderUnreadChatsCountersById[id]?.length) {
contextActions.push({ contextActions.push({
title: lang('ChatListMarkAllAsRead'), title: lang('EditFolder'),
icon: 'readchats', icon: 'edit',
handler: () => handleReadAllChats(folder.id), handler: () => {
openEditChatFolder({ folderId: id });
},
}); });
}
} else {
contextActions.push({
title: lang('EditFolder'),
icon: 'edit',
handler: () => {
openEditChatFolder({ folderId: id });
},
});
if (folderUnreadChatsCountersById[id]?.length) { if (folderUnreadChatsCountersById[id]?.length) {
contextActions.push({
title: lang('ChatListMarkAllAsRead'),
icon: 'readchats',
handler: () => handleReadAllChats(folder.id),
});
}
contextActions.push({ contextActions.push({
title: lang('ChatListMarkAllAsRead'), title: lang('FilterMenuDelete'),
icon: 'readchats', icon: 'delete',
handler: () => handleReadAllChats(folder.id), destructive: true,
handler: () => {
openDeleteChatFolderModal({ folderId: id });
},
}); });
} }
contextActions.push({ if (!isMobile) {
title: lang('FilterMenuDelete'), contextActions.push({
icon: 'delete', isSeparator: true,
destructive: true, });
handler: () => {
openDeleteChatFolderModal({ folderId: id });
},
});
}
if (!isMobile) { contextActions.push({
contextActions.push({ title: sidebarMode ? lang('TabsPositionTop') : lang('TabsPositionLeft'),
isSeparator: true, icon: 'forums',
}); handler: () => {
setSharedSettingOption({ foldersPosition: sidebarMode ? 'top' : 'left' });
contextActions.push({ },
title: sidebarMode ? lang('TabsPositionTop') : lang('TabsPositionLeft'), });
icon: 'forums', }
handler: () => {
setSharedSettingOption({ foldersPosition: sidebarMode ? 'top' : 'left' });
},
});
} }
const folderNameOptions: FolderNameOptions = { const folderNameOptions: FolderNameOptions = {
@ -215,15 +230,14 @@ const useFolderTabs = ({
badgeCount: folderCountersById[id]?.chatsCount, badgeCount: folderCountersById[id]?.chatsCount,
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount), isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
isBlocked, isBlocked,
contextActions: contextActions?.length ? contextActions : undefined, contextActions: contextActions.length ? contextActions : undefined,
emoticon: folderIcon, emoticon: folderIcon,
noTitleAnimations: folder.noTitleAnimations, noTitleAnimations: folder.noTitleAnimations,
} satisfies TabWithProperties; } satisfies TabWithProperties;
}); });
}, [ }, [
displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById, displayedFolders, maxFolders, folderCountersById, lang, chatFoldersById, maxChatLists, folderInvitesById,
maxFolderInvites, folderUnreadChatsCountersById, openSettingsScreen, sidebarMode, isMobile, maxFolderInvites, folderUnreadChatsCountersById, isReadOnly, sidebarMode, isMobile,
setSharedSettingOption,
]); ]);
return { return {