Forward Modal: Show folders (#6556)
This commit is contained in:
parent
ade72a0727
commit
74a4fad8d4
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -119,6 +119,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
onClose={handleClose}
|
||||
onCloseAnimationEnd={unmarkIsShown}
|
||||
isForwarding={isForwarding}
|
||||
withFolders
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user