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 { 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));
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -119,6 +119,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onCloseAnimationEnd={unmarkIsShown}
|
onCloseAnimationEnd={unmarkIsShown}
|
||||||
isForwarding={isForwarding}
|
isForwarding={isForwarding}
|
||||||
|
withFolders
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user