2023-05-05 15:53:15 +04:00

209 lines
6.2 KiB
TypeScript

import React, {
useCallback, useRef, useEffect, memo, useMemo,
} from '../../lib/teact/teact';
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import type { FC } from '../../lib/teact/teact';
import { isUserId } from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
import useLang from '../../hooks/useLang';
import InfiniteScroll from '../ui/InfiniteScroll';
import Checkbox from '../ui/Checkbox';
import InputText from '../ui/InputText';
import ListItem from '../ui/ListItem';
import PrivateChatInfo from './PrivateChatInfo';
import GroupChatInfo from './GroupChatInfo';
import PickerSelectedItem from './PickerSelectedItem';
import Loading from '../ui/Loading';
import './Picker.scss';
type OwnProps = {
itemIds: string[];
selectedIds: string[];
filterValue?: string;
filterPlaceholder?: string;
notFoundText?: string;
searchInputId?: string;
isLoading?: boolean;
noScrollRestore?: boolean;
isSearchable?: boolean;
isRoundCheckbox?: boolean;
lockedIds?: string[];
onSelectedIdsChange?: (ids: string[]) => void;
onFilterChange?: (value: string) => void;
onDisabledClick?: (id: string) => void;
onLoadMore?: () => void;
};
// Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 500;
const MAX_FULL_ITEMS = 10;
const ALWAYS_FULL_ITEMS_COUNT = 5;
const Picker: FC<OwnProps> = ({
itemIds,
selectedIds,
filterValue,
filterPlaceholder,
notFoundText,
searchInputId,
isLoading,
noScrollRestore,
isSearchable,
isRoundCheckbox,
lockedIds,
onSelectedIdsChange,
onFilterChange,
onDisabledClick,
onLoadMore,
}) => {
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLInputElement>(null);
const shouldMinimize = selectedIds.length > MAX_FULL_ITEMS;
useEffect(() => {
if (!isSearchable) return;
setTimeout(() => {
requestMutation(() => {
inputRef.current!.focus();
});
}, FOCUS_DELAY_MS);
}, [isSearchable]);
const [lockedSelectedIds, unlockedSelectedIds] = useMemo(() => {
if (!lockedIds?.length) return [MEMO_EMPTY_ARRAY, selectedIds];
const unlockedIds = selectedIds.filter((id) => !lockedIds.includes(id));
return [lockedIds, unlockedIds];
}, [selectedIds, lockedIds]);
const lockedIdsSet = useMemo(() => new Set(lockedIds), [lockedIds]);
const sortedItemIds = useMemo(() => {
return itemIds.sort((a, b) => {
const aIsLocked = lockedIdsSet.has(a);
const bIsLocked = lockedIdsSet.has(b);
if (aIsLocked && !bIsLocked) {
return -1;
}
if (!aIsLocked && bIsLocked) {
return 1;
}
return 0;
});
}, [itemIds, lockedIdsSet]);
const handleItemClick = useCallback((id: string) => {
if (lockedIdsSet.has(id)) {
onDisabledClick?.(id);
return;
}
const newSelectedIds = selectedIds.slice();
if (newSelectedIds.includes(id)) {
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
} else {
newSelectedIds.push(id);
}
onSelectedIdsChange?.(newSelectedIds);
onFilterChange?.('');
}, [lockedIdsSet, selectedIds, onSelectedIdsChange, onFilterChange, onDisabledClick]);
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
onFilterChange?.(value);
}, [onFilterChange]);
const [viewportIds, getMore] = useInfiniteScroll(onLoadMore, sortedItemIds, Boolean(filterValue));
const lang = useLang();
return (
<div className="Picker">
{isSearchable && (
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
{lockedSelectedIds.map((id, i) => (
<PickerSelectedItem
chatOrUserId={id}
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
onClick={handleItemClick}
clickArg={id}
/>
))}
{unlockedSelectedIds.map((id, i) => (
<PickerSelectedItem
chatOrUserId={id}
isMinimized={
shouldMinimize && i + lockedSelectedIds.length < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT
}
canClose
onClick={handleItemClick}
clickArg={id}
/>
))}
<InputText
id={searchInputId}
ref={inputRef}
value={filterValue}
onChange={handleFilterChange}
placeholder={filterPlaceholder || lang('SelectChat')}
/>
</div>
)}
{viewportIds?.length ? (
<InfiniteScroll
className="picker-list custom-scroll"
items={viewportIds}
onLoadMore={getMore}
noScrollRestore={noScrollRestore}
>
{viewportIds.map((id) => {
const renderCheckbox = () => {
return (
<Checkbox
label=""
disabled={lockedIdsSet.has(id)}
checked={selectedIds.includes(id)}
round={isRoundCheckbox}
/>
);
};
return (
<ListItem
key={id}
className={buildClassName('chat-item-clickable picker-list-item', isRoundCheckbox && 'chat-item')}
disabled={lockedIdsSet.has(id)}
allowDisabledClick={Boolean(onDisabledClick)}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => handleItemClick(id)}
ripple
>
{!isRoundCheckbox ? renderCheckbox() : undefined}
{isUserId(id) ? (
<PrivateChatInfo userId={id} />
) : (
<GroupChatInfo chatId={id} />
)}
{isRoundCheckbox ? renderCheckbox() : undefined}
</ListItem>
);
})}
</InfiniteScroll>
) : !isLoading && viewportIds && !viewportIds.length ? (
<p className="no-results">{notFoundText || 'Sorry, nothing found.'}</p>
) : (
<Loading />
)}
</div>
);
};
export default memo(Picker);