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 { 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 { API_CHAT_TYPES } from '../../config';
import { ALL_FOLDER_ID, API_CHAT_TYPES } from '../../config';
import {
getCanPostInChat,
getHasAdminRight,
@ -16,24 +15,30 @@ import { filterPeersByQuery } from '../../global/helpers/peers';
import {
filterChatIdsByType, selectChat, selectChatFullInfo, selectIsMonoforumAdmin, selectUser,
} from '../../global/selectors';
import { selectCurrentLimit } from '../../global/selectors/limits';
import { unique } from '../../util/iteratees';
import sortChatIds from './helpers/sortChatIds';
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';
export type OwnProps = {
isOpen: boolean;
searchPlaceholder: string;
className?: string;
filter?: ApiChatType[];
filter?: readonly ApiChatType[];
isLowStackPriority?: boolean;
isForwarding?: boolean;
withFolders?: boolean;
loadMore?: NoneToVoidFunction;
onSelectRecipient: (peerId: string, threadId?: ThreadId) => void;
onClose: NoneToVoidFunction;
onCloseAnimationEnd?: NoneToVoidFunction;
isLowStackPriority?: boolean;
isForwarding?: boolean;
};
type StateProps = {
@ -42,9 +47,12 @@ type StateProps = {
archivedListIds?: string[];
pinnedIds?: string[];
contactIds?: string[];
chatFoldersById: Record<number, ApiChatFolder>;
orderedFolderIds?: number[];
maxFolders: number;
};
const RecipientPicker: FC<OwnProps & StateProps> = ({
const RecipientPicker = ({
isOpen,
currentUserId,
activeListIds,
@ -54,14 +62,47 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
filter = API_CHAT_TYPES,
className,
searchPlaceholder,
isLowStackPriority,
chatFoldersById,
orderedFolderIds,
isForwarding,
maxFolders,
withFolders,
loadMore,
onSelectRecipient,
onClose,
onCloseAnimationEnd,
isLowStackPriority,
isForwarding,
}) => {
}: OwnProps & StateProps) => {
const { openLimitReachedModal } = getActions();
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(() => {
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
const global = getGlobal();
const peerIds = [
const allIds = shouldRenderFolders ? (orderedChatIds || []) : [
...(activeListIds || []),
...((search && archivedListIds) || []),
].filter((id) => {
];
const peerIds = allIds.filter((id) => {
const chat = selectChat(global, id);
const user = selectUser(global, id);
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages');
@ -95,13 +138,15 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
return !chatFullInfo || getCanPostInChat(chat, undefined, undefined, chatFullInfo);
});
const idsWithAdditions = shouldRenderFolders ? peerIds : unique([
...(currentUserId ? [currentUserId] : []),
...peerIds,
...(contactIds || []),
]);
const sorted = sortChatIds(
filterPeersByQuery({
ids: unique([
...(currentUserId ? [currentUserId] : []),
...peerIds,
...(contactIds || []),
]),
ids: idsWithAdditions,
query: search,
}),
undefined,
@ -120,10 +165,23 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
contactIds,
filter,
isForwarding,
orderedChatIds,
shouldRenderFolders,
]);
const renderingIds = useCurrentOrPrev(ids, true)!;
const chatFolders = useMemo(() => {
if (!shouldRenderFolders) return undefined;
return (
<TabList
tabs={folderTabs}
activeTab={activeFolderIndex}
onSwitchTab={handleSwitchFolderIndex}
/>
);
}, [folderTabs, activeFolderIndex, shouldRenderFolders]);
return (
<ChatOrUserPicker
isOpen={isOpen}
@ -132,6 +190,8 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
currentUserId={currentUserId}
searchPlaceholder={searchPlaceholder}
search={search}
subheader={chatFolders}
listActiveKey={activeFolderIndex}
onSearchChange={setSearch}
loadMore={loadMore}
onSelectChatOrUser={onSelectRecipient}
@ -145,6 +205,10 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const {
chatFolders: {
byId: chatFoldersById,
orderedIds: orderedFolderIds,
},
chats: {
listIds,
orderedPinnedIds,
@ -158,6 +222,9 @@ export default memo(withGlobal<OwnProps>(
pinnedIds: orderedPinnedIds.active,
contactIds: global.contactList?.userIds,
currentUserId,
chatFoldersById,
orderedFolderIds,
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
};
},
)(RecipientPicker));

View File

@ -17,26 +17,39 @@
}
.modal-header {
.Button {
margin-right: 0.5rem;
flex-direction: column;
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 {
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;
.TabList {
margin-bottom: -0.375rem;
margin-inline: -1rem;
}
}

View File

@ -1,21 +1,28 @@
import type { FC } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import type { TeactNode } from '../../../lib/teact/teact';
import {
memo, useCallback, useMemo, useRef, useState,
memo, useCallback, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiTopic } from '../../../api/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 {
getCanPostInChat, getGroupStatus, getUserStatus, isUserOnline,
} from '../../../global/helpers';
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 { resolveTransitionName } from '../../../util/resolveTransitionName';
import { REM } from '../helpers/mediaDimensions';
import renderText from '../helpers/renderText';
@ -28,7 +35,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
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 Loading from '../../ui/Loading';
import Modal from '../../ui/Modal';
@ -48,6 +55,8 @@ export type OwnProps = {
search: string;
className?: string;
isLowStackPriority?: boolean;
listActiveKey?: number;
subheader?: TeactNode;
loadMore?: NoneToVoidFunction;
onSearchChange: (search: string) => void;
onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void;
@ -55,31 +64,40 @@ export type OwnProps = {
onCloseAnimationEnd?: NoneToVoidFunction;
};
type StateProps = {
animationLevel: AnimationLevel;
shouldSkipHistoryAnimations?: boolean;
};
const CHAT_LIST_SLIDE = 0;
const TOPIC_LIST_SLIDE = 1;
const TOPIC_ICON_SIZE = 2.75 * REM;
const ITEM_CLASS_NAME = 'ChatOrUserPicker-item';
const TOPIC_ITEM_HEIGHT_PX = 56;
const ChatOrUserPicker: FC<OwnProps> = ({
const ChatOrUserPicker = ({
isOpen,
currentUserId,
chatOrUserIds,
search,
searchPlaceholder,
className,
isLowStackPriority,
subheader,
listActiveKey,
animationLevel,
shouldSkipHistoryAnimations,
loadMore,
onSearchChange,
onSelectChatOrUser,
onClose,
onCloseAnimationEnd,
isLowStackPriority,
}) => {
}: OwnProps & StateProps) => {
const { loadTopics } = getActions();
const oldLang = useOldLang();
const lang = useLang();
const containerRef = useRef<HTMLDivElement>();
const [chatKeyDownHandler, setChatKeyDownHandler] = useState<React.KeyboardEventHandler<HTMLDivElement>>();
const topicContainerRef = useRef<HTMLDivElement>();
const searchRef = useRef<HTMLInputElement>();
const topicSearchRef = useRef<HTMLInputElement>();
@ -147,7 +165,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
setTopicSearch(e.currentTarget.value);
});
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => {
const handleChatSelect = useLastCallback((index) => {
if (viewportIds && viewportIds.length > 0) {
const chatsById = getGlobal().chats.byId;
@ -160,7 +178,11 @@ const ChatOrUserPicker: FC<OwnProps> = ({
onSelectChatOrUser(chatId);
}
}
}, `.${ITEM_CLASS_NAME}`, true);
});
const handleKeyDownHandlerUpdate = useLastCallback((handler: React.KeyboardEventHandler<HTMLDivElement>) => {
setChatKeyDownHandler(() => handler);
});
const handleTopicKeyDown = useKeyboardListNavigation(topicContainerRef, isOpen, (index) => {
if (topicIds?.length) {
@ -246,50 +268,57 @@ const ChatOrUserPicker: FC<OwnProps> = ({
return (
<>
<div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
round
color="translucent"
size="tiny"
ariaLabel={oldLang('Back')}
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)}
<div className="search-wrapper">
<Button
round
color="translucent"
size="tiny"
ariaLabel={oldLang('Back')}
onClick={handleHeaderBackClick}
iconName="arrow-left"
/>
))}
</InfiniteScroll>
<InputText
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 (
<>
<div className="modal-header modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
round
color="translucent"
size="tiny"
ariaLabel={oldLang('Close')}
onClick={onClose}
iconName="close"
/>
<InputText
ref={searchRef}
value={search}
onChange={handleSearchChange}
onKeyDown={handleKeyDown}
placeholder={searchPlaceholder}
/>
<div className="search-wrapper">
<Button
round
color="translucent"
size="tiny"
ariaLabel={oldLang('Close')}
onClick={onClose}
iconName="close"
/>
<InputText
ref={searchRef}
value={search}
onChange={handleSearchChange}
onKeyDown={chatKeyDownHandler}
placeholder={searchPlaceholder}
/>
</div>
{subheader}
</div>
{viewportIds?.length ? (
<InfiniteScroll
ref={containerRef}
className="picker-list custom-scroll"
items={viewportIds}
itemSelector={`.${ITEM_CLASS_NAME}`}
onLoadMore={getMore}
withAbsolutePositioning
<Transition
activeKey={listActiveKey || 0}
name={resolveTransitionName('slideOptimized', animationLevel, shouldSkipHistoryAnimations, lang.isRtl)}
slideClassName="ChatOrUserPicker_slide"
>
<ChatListContent
isOpen={isOpen}
viewportIds={viewportIds}
maxHeight={chatOrUserIds.length * PEER_PICKER_ITEM_HEIGHT_PX}
onKeyDown={handleKeyDown}
>
{viewportIds.map(renderChatItem)}
</InfiniteScroll>
) : viewportIds && !viewportIds.length ? (
<p className="no-results">{oldLang('lng_blocked_list_not_found')}</p>
) : (
<Loading />
)}
onLoadMore={getMore}
onSelect={handleChatSelect}
renderItem={renderChatItem}
onKeyDownHandlerUpdate={handleKeyDownHandlerUpdate}
/>
</Transition>
</>
);
}
@ -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}
onCloseAnimationEnd={unmarkIsShown}
isForwarding={isForwarding}
withFolders
/>
);
};

View File

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

View File

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