336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
import type { FC } from '../../../lib/teact/teact';
|
|
import React, {
|
|
memo, useCallback, useMemo, useRef, useState,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, getGlobal } from '../../../global';
|
|
|
|
import type { ApiTopic } from '../../../api/types';
|
|
import type { GlobalState } from '../../../global/types';
|
|
import type { 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 { selectPeer, selectTopics, selectUserStatus } from '../../../global/selectors';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { REM } from '../helpers/mediaDimensions';
|
|
import renderText from '../helpers/renderText';
|
|
|
|
import useSelector from '../../../hooks/data/useSelector';
|
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
|
import useInputFocusOnOpen from '../../../hooks/useInputFocusOnOpen';
|
|
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
|
|
import Button from '../../ui/Button';
|
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
|
import InputText from '../../ui/InputText';
|
|
import Loading from '../../ui/Loading';
|
|
import Modal from '../../ui/Modal';
|
|
import Transition from '../../ui/Transition';
|
|
import Avatar from '../Avatar';
|
|
import FullNameTitle from '../FullNameTitle';
|
|
import Icon from '../icons/Icon';
|
|
import TopicIcon from '../TopicIcon';
|
|
import PickerItem from './PickerItem';
|
|
|
|
import './ChatOrUserPicker.scss';
|
|
|
|
export type OwnProps = {
|
|
currentUserId?: string;
|
|
chatOrUserIds: string[];
|
|
isOpen: boolean;
|
|
searchPlaceholder: string;
|
|
search: string;
|
|
className?: string;
|
|
loadMore?: NoneToVoidFunction;
|
|
onSearchChange: (search: string) => void;
|
|
onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void;
|
|
onClose: NoneToVoidFunction;
|
|
onCloseAnimationEnd?: NoneToVoidFunction;
|
|
isLowStackPriority?: 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> = ({
|
|
isOpen,
|
|
currentUserId,
|
|
chatOrUserIds,
|
|
search,
|
|
searchPlaceholder,
|
|
className,
|
|
loadMore,
|
|
onSearchChange,
|
|
onSelectChatOrUser,
|
|
onClose,
|
|
onCloseAnimationEnd,
|
|
isLowStackPriority,
|
|
}) => {
|
|
const { loadTopics } = getActions();
|
|
|
|
const lang = useOldLang();
|
|
const containerRef = useRef<HTMLDivElement>();
|
|
const topicContainerRef = useRef<HTMLDivElement>();
|
|
const searchRef = useRef<HTMLInputElement>();
|
|
const topicSearchRef = useRef<HTMLInputElement>();
|
|
const [viewportIds, getMore] = useInfiniteScroll(loadMore, chatOrUserIds, Boolean(search));
|
|
const [forumId, setForumId] = useState<string | undefined>(undefined);
|
|
const [topicSearch, setTopicSearch] = useState<string>('');
|
|
const activeKey = forumId ? TOPIC_LIST_SLIDE : CHAT_LIST_SLIDE;
|
|
const viewportOffset = chatOrUserIds.indexOf(viewportIds![0]);
|
|
|
|
const resetSearch = useLastCallback(() => {
|
|
onSearchChange('');
|
|
});
|
|
useInputFocusOnOpen(searchRef, isOpen && activeKey === CHAT_LIST_SLIDE, resetSearch);
|
|
useInputFocusOnOpen(topicSearchRef, isOpen && activeKey === TOPIC_LIST_SLIDE);
|
|
|
|
const selectTopicsById = useLastCallback((global: GlobalState) => {
|
|
if (!forumId) {
|
|
return undefined;
|
|
}
|
|
|
|
return selectTopics(global, forumId);
|
|
});
|
|
|
|
const forumTopicsById = useSelector(selectTopicsById);
|
|
|
|
const [topicIds, topics] = useMemo(() => {
|
|
const global = getGlobal();
|
|
const chatsById = global.chats.byId;
|
|
const chatFullInfoById = global.chats.fullInfoById;
|
|
|
|
const chat = chatsById[forumId!];
|
|
|
|
if (!chat || !forumTopicsById) {
|
|
return [undefined, undefined];
|
|
}
|
|
|
|
const searchTitle = topicSearch.toLowerCase();
|
|
|
|
const result = forumTopicsById
|
|
? Object.values(forumTopicsById).reduce((acc, topic) => {
|
|
if (
|
|
getCanPostInChat(chat, topic, undefined, chatFullInfoById[forumId!])
|
|
&& (!searchTitle || topic.title.toLowerCase().includes(searchTitle))
|
|
) {
|
|
acc[topic.id] = topic;
|
|
}
|
|
|
|
return acc;
|
|
}, {} as Record<number, ApiTopic>)
|
|
: forumTopicsById;
|
|
|
|
return [Object.keys(result).map(Number), result];
|
|
}, [forumId, topicSearch, forumTopicsById]);
|
|
|
|
const handleHeaderBackClick = useLastCallback(() => {
|
|
setForumId(undefined);
|
|
setTopicSearch('');
|
|
});
|
|
|
|
const handleSearchChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
onSearchChange(e.currentTarget.value);
|
|
});
|
|
|
|
const handleTopicSearchChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setTopicSearch(e.currentTarget.value);
|
|
});
|
|
|
|
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => {
|
|
if (viewportIds && viewportIds.length > 0) {
|
|
const chatsById = getGlobal().chats.byId;
|
|
|
|
const chatId = viewportIds[index === -1 ? 0 : index];
|
|
const chat = chatsById[chatId];
|
|
if (chat?.isForum) {
|
|
if (!forumTopicsById) loadTopics({ chatId });
|
|
setForumId(chatId);
|
|
} else {
|
|
onSelectChatOrUser(chatId);
|
|
}
|
|
}
|
|
}, `.${ITEM_CLASS_NAME}`, true);
|
|
|
|
const handleTopicKeyDown = useKeyboardListNavigation(topicContainerRef, isOpen, (index) => {
|
|
if (topicIds?.length) {
|
|
onSelectChatOrUser(forumId!, topicIds[index === -1 ? 0 : index]);
|
|
}
|
|
}, `.${ITEM_CLASS_NAME}`, true);
|
|
|
|
const handleClick = useLastCallback((chatId: string) => {
|
|
const chatsById = getGlobal().chats.byId;
|
|
const chat = chatsById?.[chatId];
|
|
if (chat?.isForum) {
|
|
if (!forumTopicsById) loadTopics({ chatId });
|
|
setForumId(chatId);
|
|
resetSearch();
|
|
} else {
|
|
onSelectChatOrUser(chatId);
|
|
}
|
|
});
|
|
|
|
const renderChatItem = useCallback((id: string, index: number) => {
|
|
const global = getGlobal();
|
|
const peer = selectPeer(global, id);
|
|
if (!peer) {
|
|
return undefined;
|
|
}
|
|
|
|
const isSelf = peer && !isApiPeerChat(peer) ? peer.isSelf : undefined;
|
|
|
|
function getSubtitle() {
|
|
if (!peer) return undefined;
|
|
if (peer.id === currentUserId) return [lang('SavedMessagesInfo')];
|
|
if (isApiPeerChat(peer)) {
|
|
return [getGroupStatus(lang, peer)];
|
|
}
|
|
|
|
const userStatus = selectUserStatus(global, peer.id);
|
|
return [
|
|
getUserStatus(lang, peer, userStatus),
|
|
buildClassName(isUserOnline(peer, userStatus, true) && 'online'),
|
|
];
|
|
}
|
|
|
|
const [subtitle, subtitleClassName] = getSubtitle() || [];
|
|
|
|
return (
|
|
<PickerItem
|
|
key={id}
|
|
className={ITEM_CLASS_NAME}
|
|
title={<FullNameTitle peer={peer} isSavedMessages={isSelf} />}
|
|
avatarElement={(
|
|
<Avatar
|
|
peer={peer}
|
|
isSavedMessages={isSelf}
|
|
size="medium"
|
|
/>
|
|
)}
|
|
subtitle={subtitle}
|
|
subtitleClassName={subtitleClassName}
|
|
ripple
|
|
style={`top: ${(viewportOffset + index) * PEER_PICKER_ITEM_HEIGHT_PX}px;`}
|
|
|
|
onClick={() => handleClick(id)}
|
|
/>
|
|
);
|
|
}, [currentUserId, lang, viewportOffset]);
|
|
|
|
function renderTopicList() {
|
|
return (
|
|
<>
|
|
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
|
<Button round color="translucent" size="smaller" ariaLabel={lang('Back')} onClick={handleHeaderBackClick}>
|
|
<Icon name="arrow-left" />
|
|
</Button>
|
|
<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)}
|
|
/>
|
|
))}
|
|
</InfiniteScroll>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function renderChatList() {
|
|
return (
|
|
<>
|
|
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
|
<Button
|
|
round
|
|
color="translucent"
|
|
size="smaller"
|
|
ariaLabel={lang('Close')}
|
|
onClick={onClose}
|
|
>
|
|
<Icon name="close" />
|
|
</Button>
|
|
<InputText
|
|
ref={searchRef}
|
|
value={search}
|
|
onChange={handleSearchChange}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder={searchPlaceholder}
|
|
/>
|
|
</div>
|
|
{viewportIds?.length ? (
|
|
<InfiniteScroll
|
|
ref={containerRef}
|
|
className="picker-list custom-scroll"
|
|
items={viewportIds}
|
|
itemSelector={`.${ITEM_CLASS_NAME}`}
|
|
onLoadMore={getMore}
|
|
withAbsolutePositioning
|
|
maxHeight={chatOrUserIds.length * PEER_PICKER_ITEM_HEIGHT_PX}
|
|
onKeyDown={handleKeyDown}
|
|
>
|
|
{viewportIds.map(renderChatItem)}
|
|
</InfiniteScroll>
|
|
) : viewportIds && !viewportIds.length ? (
|
|
<p className="no-results">{lang('lng_blocked_list_not_found')}</p>
|
|
) : (
|
|
<Loading />
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
className={buildClassName('ChatOrUserPicker', className)}
|
|
onClose={onClose}
|
|
onCloseAnimationEnd={onCloseAnimationEnd}
|
|
isLowStackPriority={isLowStackPriority}
|
|
>
|
|
<Transition activeKey={activeKey} name="slideFade" slideClassName="ChatOrUserPicker_slide">
|
|
{() => {
|
|
return activeKey === TOPIC_LIST_SLIDE ? renderTopicList() : renderChatList();
|
|
}}
|
|
</Transition>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default memo(ChatOrUserPicker);
|