UI: Picker refactoring (#4773)
This commit is contained in:
parent
5f5536b6a0
commit
729e4ad791
@ -19,7 +19,6 @@ export { default as PremiumMainModal } from '../components/main/premium/PremiumM
|
||||
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
||||
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
|
||||
export { default as PremiumGiftingModal } from '../components/main/premium/PremiumGiftingModal';
|
||||
export { default as AppendEntityPickerModal } from '../components/main/AppendEntityPickerModal';
|
||||
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||
export { default as BoostModal } from '../components/modals/boost/BoostModal';
|
||||
|
||||
@ -15,7 +15,7 @@ import usePrevious from '../../hooks/usePrevious';
|
||||
import Button from '../ui/Button';
|
||||
import Modal from '../ui/Modal';
|
||||
import Icon from './icons/Icon';
|
||||
import Picker from './Picker';
|
||||
import ItemPicker from './pickers/ItemPicker';
|
||||
|
||||
import styles from './CountryPickerModal.module.scss';
|
||||
|
||||
@ -47,9 +47,13 @@ const CountryPickerModal: FC<OwnProps> = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
return countryList
|
||||
.filter((country) => !country.isHidden)
|
||||
.map((country) => country.iso2);
|
||||
return countryList.filter((country) => !country.isHidden && country.iso2 !== 'FT')
|
||||
.map(({
|
||||
iso2, defaultName,
|
||||
}) => ({
|
||||
value: iso2,
|
||||
label: defaultName,
|
||||
}));
|
||||
}, [countryList]);
|
||||
|
||||
const handleSelectedIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
@ -92,14 +96,14 @@ const CountryPickerModal: FC<OwnProps> = ({
|
||||
</div>
|
||||
|
||||
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||
<Picker
|
||||
<ItemPicker
|
||||
className={styles.picker}
|
||||
itemIds={displayedIds}
|
||||
selectedIds={selectedCountryIds}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
items={displayedIds}
|
||||
selectedValues={selectedCountryIds}
|
||||
onSelectedValuesChange={handleSelectedIdsChange}
|
||||
noScrollRestore={noPickerScrollRestore}
|
||||
isCountryList
|
||||
countryList={countryList}
|
||||
allowMultiple
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
.actionTitle {
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
color: var(--color-links);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
.listItemButton {
|
||||
margin-top: 0.1875rem;
|
||||
margin-left: 0.4375rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
@ -52,16 +52,12 @@
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: max-height 0.3s ease-out;
|
||||
transition: max-height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.restrictionContainerOpen,
|
||||
.dropdownListOpen {
|
||||
max-height: 100vh;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: max-height 0.3s ease-in;
|
||||
}
|
||||
|
||||
:global(.Checkbox) {
|
||||
padding-left: 3.875rem;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@ -3,15 +3,22 @@
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
.fullName {
|
||||
margin-bottom: 0;
|
||||
|
||||
&.canCopy {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.custom-emoji) {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.fullName {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
|
||||
&.canCopy {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@ -38,9 +38,10 @@ type OwnProps = {
|
||||
isSavedDialog?: boolean;
|
||||
noLoopLimit?: boolean;
|
||||
canCopyTitle?: boolean;
|
||||
iconElement?: React.ReactNode;
|
||||
allowMultiLine?: boolean;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
observeIntersection?: ObserveFn;
|
||||
iconElement?: React.ReactNode;
|
||||
};
|
||||
|
||||
const FullNameTitle: FC<OwnProps> = ({
|
||||
@ -54,9 +55,10 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
isSavedDialog,
|
||||
noLoopLimit,
|
||||
canCopyTitle,
|
||||
iconElement,
|
||||
allowMultiLine,
|
||||
onEmojiStatusClick,
|
||||
observeIntersection,
|
||||
iconElement,
|
||||
}) => {
|
||||
const lang = useOldLang();
|
||||
const { showNotification } = getActions();
|
||||
@ -99,7 +101,9 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
if (specialTitle) {
|
||||
return (
|
||||
<div className={buildClassName('title', styles.root, className)}>
|
||||
<h3>{specialTitle}</h3>
|
||||
<h3 className={buildClassName('fullName', styles.fullName, !allowMultiLine && styles.ellipsis)}>
|
||||
{specialTitle}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -109,7 +113,12 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
<h3
|
||||
dir="auto"
|
||||
role="button"
|
||||
className={buildClassName('fullName', styles.fullName, canCopyTitle && styles.canCopy)}
|
||||
className={buildClassName(
|
||||
'fullName',
|
||||
styles.fullName,
|
||||
!allowMultiLine && styles.ellipsis,
|
||||
canCopyTitle && styles.canCopy,
|
||||
)}
|
||||
onClick={handleTitleClick}
|
||||
>
|
||||
{renderText(title || '')}
|
||||
|
||||
@ -5,12 +5,12 @@ import { getActions, withGlobal } from '../../global';
|
||||
import type {
|
||||
ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types';
|
||||
|
||||
import {
|
||||
getChatTypeString,
|
||||
getGroupStatus,
|
||||
getMainUsername,
|
||||
isChatSuperGroup,
|
||||
} from '../../global/helpers';
|
||||
@ -261,23 +261,6 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getGroupStatus(lang: LangFn, chat: ApiChat) {
|
||||
const chatTypeString = lang(getChatTypeString(chat));
|
||||
const { membersCount } = chat;
|
||||
|
||||
if (chat.isRestricted) {
|
||||
return chatTypeString === 'Channel' ? 'channel is inaccessible' : 'group is inaccessible';
|
||||
}
|
||||
|
||||
if (!membersCount) {
|
||||
return chatTypeString;
|
||||
}
|
||||
|
||||
return chatTypeString === 'Channel'
|
||||
? lang('Subscribers', membersCount, 'i')
|
||||
: lang('Members', membersCount, 'i');
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId, threadId }): StateProps => {
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
.Picker {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.picker-header {
|
||||
padding: 0 1rem 0.25rem 0.75rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
height: 2rem;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-category-title {
|
||||
color: var(--color-text-secondary);
|
||||
padding-inline: 1rem;
|
||||
font-weight: 500;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid var(--color-borders);
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-list {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0.5rem;
|
||||
|
||||
&.withRoundedCheckbox {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 1rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
@ -1,325 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiCountry } from '../../api/types';
|
||||
import type { CustomPeer, CustomPeerType, UniqueCustomPeer } from '../../types';
|
||||
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import { isUserId } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { buildCollectionByKey } from '../../util/iteratees';
|
||||
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import InputText from '../ui/InputText';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import Loading from '../ui/Loading';
|
||||
import GroupChatInfo from './GroupChatInfo';
|
||||
import PickerSelectedItem from './PickerSelectedItem';
|
||||
import PrivateChatInfo from './PrivateChatInfo';
|
||||
|
||||
import './Picker.scss';
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
categories?: UniqueCustomPeer[];
|
||||
itemIds: string[];
|
||||
selectedCategories?: CustomPeerType[];
|
||||
selectedIds: string[];
|
||||
lockedSelectedIds?: string[];
|
||||
lockedUnselectedIds?: string[];
|
||||
lockedUnselectedSubtitle?: string;
|
||||
filterValue?: string;
|
||||
filterPlaceholder?: string;
|
||||
categoryPlaceholderKey?: string;
|
||||
notFoundText?: string;
|
||||
searchInputId?: string;
|
||||
isLoading?: boolean;
|
||||
noScrollRestore?: boolean;
|
||||
isSearchable?: boolean;
|
||||
isRoundCheckbox?: boolean;
|
||||
forceShowSelf?: boolean;
|
||||
isViewOnly?: boolean;
|
||||
onSelectedCategoriesChange?: (categories: CustomPeerType[]) => void;
|
||||
onSelectedIdsChange?: (ids: string[]) => void;
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (id: string, isSelected: boolean) => void;
|
||||
onLoadMore?: () => void;
|
||||
isCountryList?: boolean;
|
||||
countryList?: ApiCountry[];
|
||||
};
|
||||
|
||||
// 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> = ({
|
||||
className,
|
||||
categories,
|
||||
itemIds,
|
||||
selectedCategories,
|
||||
categoryPlaceholderKey,
|
||||
selectedIds,
|
||||
filterValue,
|
||||
filterPlaceholder,
|
||||
notFoundText,
|
||||
searchInputId,
|
||||
isLoading,
|
||||
noScrollRestore,
|
||||
isSearchable,
|
||||
isRoundCheckbox,
|
||||
lockedSelectedIds,
|
||||
lockedUnselectedIds,
|
||||
lockedUnselectedSubtitle,
|
||||
forceShowSelf,
|
||||
isViewOnly,
|
||||
onSelectedCategoriesChange,
|
||||
onSelectedIdsChange,
|
||||
onFilterChange,
|
||||
onDisabledClick,
|
||||
onLoadMore,
|
||||
isCountryList,
|
||||
countryList,
|
||||
}) => {
|
||||
// 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(() => {
|
||||
requestMeasure(() => {
|
||||
inputRef.current!.focus();
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
}, [isSearchable]);
|
||||
|
||||
const lockedSelectedIdsSet = useMemo(() => new Set(lockedSelectedIds), [lockedSelectedIds]);
|
||||
const lockedUnselectedIdsSet = useMemo(() => new Set(lockedUnselectedIds), [lockedUnselectedIds]);
|
||||
|
||||
const unlockedSelectedIds = useMemo(() => {
|
||||
return selectedIds.filter((id) => !lockedSelectedIdsSet.has(id));
|
||||
}, [lockedSelectedIdsSet, selectedIds]);
|
||||
|
||||
const categoriesByType = useMemo(() => {
|
||||
if (!categories) return {};
|
||||
return buildCollectionByKey(categories, 'type');
|
||||
}, [categories]);
|
||||
|
||||
const sortedItemIds = useMemo(() => {
|
||||
if (filterValue) {
|
||||
return itemIds;
|
||||
}
|
||||
|
||||
const lockedSelectedBucket: string[] = [];
|
||||
const unlockedBucket: string[] = [];
|
||||
const lockedUnselectableBucket: string[] = [];
|
||||
|
||||
itemIds.forEach((id) => {
|
||||
if (lockedSelectedIdsSet.has(id)) {
|
||||
lockedSelectedBucket.push(id);
|
||||
} else if (lockedUnselectedIdsSet.has(id)) {
|
||||
lockedUnselectableBucket.push(id);
|
||||
} else {
|
||||
unlockedBucket.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return lockedSelectedBucket.concat(unlockedBucket, lockedUnselectableBucket);
|
||||
}, [filterValue, itemIds, lockedSelectedIdsSet, lockedUnselectedIdsSet]);
|
||||
|
||||
const handleItemClick = useLastCallback((id: string) => {
|
||||
if (lockedSelectedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lockedUnselectedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoriesByType[id]) {
|
||||
const categoryType = categoriesByType[id].type;
|
||||
const newSelectedCategories = selectedCategories?.slice() || [];
|
||||
if (newSelectedCategories.includes(categoryType)) {
|
||||
newSelectedCategories.splice(newSelectedCategories.indexOf(categoryType), 1);
|
||||
} else {
|
||||
newSelectedCategories.push(categoryType);
|
||||
}
|
||||
onSelectedCategoriesChange?.(newSelectedCategories);
|
||||
} else {
|
||||
const newSelectedIds = selectedIds.slice();
|
||||
if (newSelectedIds.includes(id)) {
|
||||
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
|
||||
} else {
|
||||
newSelectedIds.push(id);
|
||||
}
|
||||
onSelectedIdsChange?.(newSelectedIds);
|
||||
}
|
||||
onFilterChange?.('');
|
||||
});
|
||||
|
||||
const handleFilterChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.currentTarget;
|
||||
onFilterChange?.(value);
|
||||
});
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(
|
||||
onLoadMore, sortedItemIds, Boolean(filterValue),
|
||||
);
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
const countriesByIso = useMemo(() => {
|
||||
if (!countryList) return undefined;
|
||||
return buildCollectionByKey(countryList, 'iso2');
|
||||
}, [countryList]);
|
||||
|
||||
const renderCategory = useLastCallback((category: CustomPeer) => {
|
||||
return (
|
||||
<PrivateChatInfo
|
||||
customPeer={category}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const renderChatInfo = useLastCallback((id: string) => {
|
||||
const isUnselectable = lockedUnselectedIdsSet.has(id);
|
||||
if (isCountryList && countriesByIso) {
|
||||
const country = countriesByIso[id];
|
||||
return <div>{country.defaultName}</div>;
|
||||
} else if (isUserId(id)) {
|
||||
return (
|
||||
<PrivateChatInfo
|
||||
forceShowSelf={forceShowSelf}
|
||||
userId={id}
|
||||
status={isUnselectable ? lockedUnselectedSubtitle : undefined}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <GroupChatInfo chatId={id} status={isUnselectable ? lockedUnselectedSubtitle : undefined} />;
|
||||
}
|
||||
});
|
||||
|
||||
const renderItem = useCallback((id: string, isCategory?: boolean) => {
|
||||
const category = isCategory ? categoriesByType[id] : undefined;
|
||||
const shouldRenderLockIcon = lockedUnselectedIdsSet.has(id);
|
||||
const isLocked = lockedSelectedIdsSet.has(id) || shouldRenderLockIcon;
|
||||
const isChecked = category ? selectedCategories?.includes(category.type) : selectedIds.includes(id);
|
||||
const renderCheckbox = () => {
|
||||
return (isViewOnly || shouldRenderLockIcon) ? undefined : (
|
||||
<Checkbox
|
||||
label=""
|
||||
disabled={isLocked}
|
||||
checked={isChecked}
|
||||
round={isRoundCheckbox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ListItem
|
||||
key={id}
|
||||
className={buildClassName('chat-item-clickable picker-list-item', isRoundCheckbox && 'chat-item')}
|
||||
disabled={isLocked}
|
||||
inactive={isViewOnly}
|
||||
allowDisabledClick={Boolean(onDisabledClick)}
|
||||
secondaryIcon={shouldRenderLockIcon ? 'lock-badge' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleItemClick(id)}
|
||||
ripple
|
||||
>
|
||||
{!isRoundCheckbox ? renderCheckbox() : undefined}
|
||||
{category ? renderCategory(category) : renderChatInfo(id)}
|
||||
{isRoundCheckbox ? renderCheckbox() : undefined}
|
||||
</ListItem>
|
||||
);
|
||||
}, [
|
||||
categoriesByType, isRoundCheckbox, isViewOnly, lockedSelectedIdsSet, lockedUnselectedIdsSet,
|
||||
onDisabledClick, renderChatInfo, selectedCategories, selectedIds,
|
||||
]);
|
||||
|
||||
const beforeChildren = useMemo(() => {
|
||||
return (
|
||||
<div key="categories">
|
||||
{Boolean(categories?.length) && (
|
||||
<>
|
||||
{categoryPlaceholderKey && <div className="picker-category-title">{lang(categoryPlaceholderKey)}</div>}
|
||||
{categories?.map((category) => renderItem(category.type, true))}
|
||||
<div className="picker-category-title">{lang('FilterChats')}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [categories, categoryPlaceholderKey, lang, renderItem]);
|
||||
|
||||
return (
|
||||
<div className={buildClassName('Picker', className)}>
|
||||
{isSearchable && (
|
||||
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{selectedCategories?.map((category) => (
|
||||
<PickerSelectedItem
|
||||
customPeer={categoriesByType[category]}
|
||||
onClick={handleItemClick}
|
||||
clickArg={category}
|
||||
canClose
|
||||
/>
|
||||
))}
|
||||
{lockedSelectedIds?.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
peerId={id}
|
||||
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
||||
forceShowSelf={forceShowSelf}
|
||||
onClick={handleItemClick}
|
||||
clickArg={id}
|
||||
/>
|
||||
))}
|
||||
{unlockedSelectedIds.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
peerId={id}
|
||||
isMinimized={
|
||||
shouldMinimize && i + (lockedSelectedIds?.length || 0) < 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={buildClassName('picker-list', 'custom-scroll', isRoundCheckbox && 'withRoundedCheckbox')}
|
||||
items={viewportIds}
|
||||
beforeChildren={beforeChildren}
|
||||
onLoadMore={getMore}
|
||||
noScrollRestore={noScrollRestore}
|
||||
>
|
||||
{viewportIds.map((id) => renderItem(id))}
|
||||
</InfiniteScroll>
|
||||
) : !isLoading && viewportIds && !viewportIds.length ? (
|
||||
<p className="no-results">{notFoundText || 'Sorry, nothing found.'}</p>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Picker);
|
||||
@ -20,7 +20,7 @@ import sortChatIds from './helpers/sortChatIds';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import ChatOrUserPicker from './ChatOrUserPicker';
|
||||
import ChatOrUserPicker from './pickers/ChatOrUserPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
.StickerSetCard {
|
||||
.settings-item &.ListItem {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.StickerButton,
|
||||
.Button {
|
||||
width: 3rem;
|
||||
|
||||
@ -1,34 +1,34 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../global';
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type { ApiTopic } from '../../api/types';
|
||||
import type { ThreadId } from '../../types';
|
||||
import type { ApiTopic } from '../../../api/types';
|
||||
import type { ThreadId } from '../../../types';
|
||||
|
||||
import { CHAT_HEIGHT_PX } from '../../config';
|
||||
import { getCanPostInChat, isUserId } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
import renderText from './helpers/renderText';
|
||||
import { CHAT_HEIGHT_PX } from '../../../config';
|
||||
import { getCanPostInChat, isUserId } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { REM } from '../helpers/mediaDimensions';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
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 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 ListItem from '../ui/ListItem';
|
||||
import Loading from '../ui/Loading';
|
||||
import Modal from '../ui/Modal';
|
||||
import Transition from '../ui/Transition';
|
||||
import GroupChatInfo from './GroupChatInfo';
|
||||
import PrivateChatInfo from './PrivateChatInfo';
|
||||
import TopicIcon from './TopicIcon';
|
||||
import Button from '../../ui/Button';
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import InputText from '../../ui/InputText';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Transition from '../../ui/Transition';
|
||||
import GroupChatInfo from '../GroupChatInfo';
|
||||
import PrivateChatInfo from '../PrivateChatInfo';
|
||||
import TopicIcon from '../TopicIcon';
|
||||
|
||||
import './ChatOrUserPicker.scss';
|
||||
|
||||
245
src/components/common/pickers/ItemPicker.tsx
Normal file
245
src/components/common/pickers/ItemPicker.tsx
Normal file
@ -0,0 +1,245 @@
|
||||
import type { TeactNode } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
|
||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import InputText from '../../ui/InputText';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Radio from '../../ui/Radio';
|
||||
import Icon from '../icons/Icon';
|
||||
import PickerItem from './PickerItem';
|
||||
|
||||
import styles from './PickerStyles.module.scss';
|
||||
|
||||
export type ItemPickerOption = {
|
||||
label: TeactNode;
|
||||
subLabel?: string;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type SingleModeProps = {
|
||||
allowMultiple?: false;
|
||||
itemInputType?: 'radio';
|
||||
selectedValue?: string;
|
||||
selectedValues?: never; // Help TS to throw an error if this is passed
|
||||
onSelectedValueChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
type MultipleModeProps = {
|
||||
allowMultiple: true;
|
||||
itemInputType: 'checkbox';
|
||||
selectedValue?: never;
|
||||
selectedValues: string[];
|
||||
lockedSelectedValues?: string[];
|
||||
lockedUnselectedValues?: string[];
|
||||
onSelectedValuesChange?: (values: string[]) => void;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
isSearchable?: boolean;
|
||||
searchInputId?: string;
|
||||
items: ItemPickerOption[];
|
||||
itemClassName?: string;
|
||||
filterValue?: string;
|
||||
filterPlaceholder?: string;
|
||||
notFoundText?: string;
|
||||
isLoading?: boolean;
|
||||
noScrollRestore?: boolean;
|
||||
isViewOnly?: boolean;
|
||||
withDefaultPadding?: boolean;
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (value: string, isSelected: boolean) => void;
|
||||
onLoadMore?: () => void;
|
||||
} & (SingleModeProps | MultipleModeProps);
|
||||
|
||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
||||
const FOCUS_DELAY_MS = 500;
|
||||
|
||||
const ITEM_CLASS_NAME = 'ItemPickerItem';
|
||||
|
||||
const ItemPicker = ({
|
||||
className,
|
||||
isSearchable,
|
||||
searchInputId,
|
||||
items,
|
||||
filterValue,
|
||||
notFoundText,
|
||||
isLoading,
|
||||
noScrollRestore,
|
||||
filterPlaceholder,
|
||||
isViewOnly,
|
||||
itemInputType,
|
||||
itemClassName,
|
||||
withDefaultPadding,
|
||||
onFilterChange,
|
||||
onDisabledClick,
|
||||
onLoadMore,
|
||||
...optionalProps
|
||||
}: OwnProps) => {
|
||||
const lang = useOldLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const allowMultiple = optionalProps.allowMultiple;
|
||||
const lockedSelectedValues = allowMultiple ? optionalProps.lockedSelectedValues : undefined;
|
||||
const lockedUnselectedValues = allowMultiple ? optionalProps.lockedUnselectedValues : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSearchable) return;
|
||||
setTimeout(() => {
|
||||
requestMeasure(() => {
|
||||
inputRef.current!.focus();
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
}, [isSearchable]);
|
||||
|
||||
const selectedValues = useMemo(() => {
|
||||
if (allowMultiple) {
|
||||
return optionalProps.selectedValues;
|
||||
}
|
||||
|
||||
return optionalProps.selectedValue ? [optionalProps.selectedValue] : MEMO_EMPTY_ARRAY;
|
||||
}, [allowMultiple, optionalProps.selectedValue, optionalProps.selectedValues]);
|
||||
|
||||
const lockedSelectedValuesSet = useMemo(() => new Set(lockedSelectedValues), [lockedSelectedValues]);
|
||||
const lockedUnselectedValuesSet = useMemo(() => new Set(lockedUnselectedValues), [lockedUnselectedValues]);
|
||||
|
||||
const sortedItemValuesList = useMemo(() => {
|
||||
if (filterValue) {
|
||||
return items.map((item) => item.value);
|
||||
}
|
||||
|
||||
const lockedSelectedBucket: ItemPickerOption[] = [];
|
||||
const unlockedBucket: ItemPickerOption[] = [];
|
||||
const lockedUnselectableBucket: ItemPickerOption[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
if (lockedSelectedValuesSet.has(item.value)) {
|
||||
lockedSelectedBucket.push(item);
|
||||
} else if (lockedUnselectedValuesSet.has(item.value)) {
|
||||
lockedUnselectableBucket.push(item);
|
||||
} else {
|
||||
unlockedBucket.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return lockedSelectedBucket.concat(unlockedBucket, lockedUnselectableBucket).map((item) => item.value);
|
||||
}, [filterValue, items, lockedSelectedValuesSet, lockedUnselectedValuesSet]);
|
||||
|
||||
const handleItemClick = useLastCallback((value: string) => {
|
||||
if (allowMultiple) {
|
||||
const newSelectedValues = selectedValues.slice();
|
||||
const index = newSelectedValues.indexOf(value);
|
||||
if (index >= 0) {
|
||||
newSelectedValues.splice(index, 1);
|
||||
} else {
|
||||
newSelectedValues.push(value);
|
||||
}
|
||||
|
||||
optionalProps.onSelectedValuesChange?.(newSelectedValues);
|
||||
return;
|
||||
}
|
||||
|
||||
optionalProps.onSelectedValueChange?.(value);
|
||||
});
|
||||
|
||||
const [viewportValuesList, getMore] = useInfiniteScroll(
|
||||
onLoadMore, sortedItemValuesList, Boolean(filterValue),
|
||||
);
|
||||
|
||||
const handleFilterChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.currentTarget;
|
||||
onFilterChange?.(value);
|
||||
});
|
||||
|
||||
const renderItem = useCallback((value: string) => {
|
||||
const item = items.find((itemOption) => itemOption.value === value);
|
||||
if (!item) return undefined;
|
||||
|
||||
const { label, subLabel, isLoading: isItemLoading } = item;
|
||||
const isAlwaysUnselected = lockedUnselectedValuesSet.has(value);
|
||||
const isAlwaysSelected = lockedSelectedValuesSet.has(value);
|
||||
const isLocked = isAlwaysUnselected || isAlwaysSelected;
|
||||
const isChecked = selectedValues.includes(value);
|
||||
|
||||
function getInputElement() {
|
||||
if (isLocked) return <Icon name="lock-badge" />;
|
||||
if (itemInputType === 'radio') {
|
||||
return <Radio checked={isChecked} disabled={isLocked} isLoading={isItemLoading} onlyInput />;
|
||||
}
|
||||
if (itemInputType === 'checkbox') {
|
||||
return <Checkbox checked={isChecked} disabled={isLocked} isLoading={isItemLoading} onlyInput />;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<PickerItem
|
||||
key={value}
|
||||
className={buildClassName(ITEM_CLASS_NAME, itemClassName)}
|
||||
title={label}
|
||||
subtitle={subLabel}
|
||||
disabled={isLocked}
|
||||
inactive={isViewOnly}
|
||||
ripple
|
||||
inputElement={getInputElement()}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleItemClick(value)}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onDisabledClick={onDisabledClick && (() => onDisabledClick(value, isAlwaysSelected))}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
items, lockedUnselectedValuesSet, lockedSelectedValuesSet, selectedValues, isViewOnly, onDisabledClick,
|
||||
itemInputType, itemClassName,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.container, className)}>
|
||||
{isSearchable && (
|
||||
<div className={buildClassName(styles.header, 'custom-scroll')} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<InputText
|
||||
id={searchInputId}
|
||||
ref={inputRef}
|
||||
value={filterValue}
|
||||
onChange={handleFilterChange}
|
||||
placeholder={filterPlaceholder || lang('Search')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewportValuesList?.length ? (
|
||||
<InfiniteScroll
|
||||
className={buildClassName(styles.pickerList, withDefaultPadding && styles.padded, 'custom-scroll')}
|
||||
items={viewportValuesList}
|
||||
itemSelector={`.${ITEM_CLASS_NAME}`}
|
||||
onLoadMore={getMore}
|
||||
noScrollRestore={noScrollRestore}
|
||||
>
|
||||
{viewportValuesList.map((value) => renderItem(value))}
|
||||
</InfiniteScroll>
|
||||
) : !isLoading && viewportValuesList && !viewportValuesList.length ? (
|
||||
<p className={styles.noResults}>{notFoundText || lang('SearchEmptyViewTitle')}</p>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ItemPicker);
|
||||
391
src/components/common/pickers/PeerPicker.tsx
Normal file
391
src/components/common/pickers/PeerPicker.tsx
Normal file
@ -0,0 +1,391 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getGlobal } from '../../../global';
|
||||
|
||||
import type { CustomPeerType, UniqueCustomPeer } from '../../../types';
|
||||
|
||||
import { DEBUG } from '../../../config';
|
||||
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getGroupStatus, getUserStatus, isUserOnline } from '../../../global/helpers';
|
||||
import { getPeerTypeKey, isApiPeerChat } from '../../../global/helpers/peers';
|
||||
import { selectPeer, selectUserStatus } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
|
||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import InputText from '../../ui/InputText';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Radio from '../../ui/Radio';
|
||||
import Avatar from '../Avatar';
|
||||
import FullNameTitle from '../FullNameTitle';
|
||||
import Icon from '../icons/Icon';
|
||||
import PickerItem from './PickerItem';
|
||||
import PickerSelectedItem from './PickerSelectedItem';
|
||||
|
||||
import styles from './PickerStyles.module.scss';
|
||||
|
||||
type SingleModeProps = {
|
||||
allowMultiple?: false;
|
||||
itemInputType?: 'radio';
|
||||
selectedId?: string;
|
||||
selectedIds?: never; // Help TS to throw an error if this is passed
|
||||
selectedCategory?: CustomPeerType;
|
||||
selectedCategories?: never;
|
||||
onSelectedCategoryChange?: (category: CustomPeerType) => void;
|
||||
onSelectedIdChange?: (id: string) => void;
|
||||
};
|
||||
|
||||
type MultipleModeProps = {
|
||||
allowMultiple: true;
|
||||
itemInputType: 'checkbox';
|
||||
selectedId?: never;
|
||||
selectedIds: string[];
|
||||
lockedSelectedIds?: string[];
|
||||
lockedUnselectedIds?: string[];
|
||||
selectedCategory?: never;
|
||||
selectedCategories?: CustomPeerType[];
|
||||
onSelectedCategoriesChange?: (categories: CustomPeerType[]) => void;
|
||||
onSelectedIdsChange?: (Ids: string[]) => void;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
categories?: UniqueCustomPeer[];
|
||||
itemIds: string[];
|
||||
lockedUnselectedSubtitle?: string;
|
||||
filterValue?: string;
|
||||
filterPlaceholder?: string;
|
||||
categoryPlaceholderKey?: string;
|
||||
notFoundText?: string;
|
||||
searchInputId?: string;
|
||||
itemClassName?: string;
|
||||
isLoading?: boolean;
|
||||
noScrollRestore?: boolean;
|
||||
isSearchable?: boolean;
|
||||
forceShowSelf?: boolean;
|
||||
isViewOnly?: boolean;
|
||||
withStatus?: boolean;
|
||||
withPeerTypes?: boolean;
|
||||
withDefaultPadding?: boolean;
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (id: string, isSelected: boolean) => void;
|
||||
onLoadMore?: () => void;
|
||||
} & (SingleModeProps | MultipleModeProps);
|
||||
|
||||
// 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 ITEM_CLASS_NAME = 'PeerPickerItem';
|
||||
|
||||
const PeerPicker = ({
|
||||
className,
|
||||
categories,
|
||||
itemIds,
|
||||
categoryPlaceholderKey,
|
||||
filterValue,
|
||||
filterPlaceholder,
|
||||
notFoundText,
|
||||
searchInputId,
|
||||
itemClassName,
|
||||
isLoading,
|
||||
noScrollRestore,
|
||||
isSearchable,
|
||||
lockedUnselectedSubtitle,
|
||||
forceShowSelf,
|
||||
isViewOnly,
|
||||
itemInputType,
|
||||
withStatus,
|
||||
withPeerTypes,
|
||||
withDefaultPadding,
|
||||
onFilterChange,
|
||||
onDisabledClick,
|
||||
onLoadMore,
|
||||
...optionalProps
|
||||
}: OwnProps) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
const allowMultiple = optionalProps.allowMultiple;
|
||||
const lockedSelectedIds = allowMultiple ? optionalProps.lockedSelectedIds : undefined;
|
||||
const lockedUnselectedIds = allowMultiple ? optionalProps.lockedUnselectedIds : undefined;
|
||||
const selectedCategories = useMemo(() => {
|
||||
if (allowMultiple) {
|
||||
return optionalProps.selectedCategories;
|
||||
}
|
||||
|
||||
return optionalProps.selectedCategory ? [optionalProps.selectedCategory] : MEMO_EMPTY_ARRAY;
|
||||
}, [allowMultiple, optionalProps.selectedCategory, optionalProps.selectedCategories]);
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
if (allowMultiple) {
|
||||
return optionalProps.selectedIds;
|
||||
}
|
||||
|
||||
return optionalProps.selectedId ? [optionalProps.selectedId] : MEMO_EMPTY_ARRAY;
|
||||
}, [allowMultiple, optionalProps.selectedId, optionalProps.selectedIds]);
|
||||
|
||||
// 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(() => {
|
||||
requestMeasure(() => {
|
||||
inputRef.current!.focus();
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
}, [isSearchable]);
|
||||
|
||||
const lockedSelectedIdsSet = useMemo(() => new Set(lockedSelectedIds), [lockedSelectedIds]);
|
||||
const lockedUnselectedIdsSet = useMemo(() => new Set(lockedUnselectedIds), [lockedUnselectedIds]);
|
||||
|
||||
const unlockedSelectedIds = useMemo(() => {
|
||||
return selectedIds.filter((id) => !lockedSelectedIdsSet.has(id));
|
||||
}, [lockedSelectedIdsSet, selectedIds]);
|
||||
|
||||
const categoriesByType = useMemo(() => {
|
||||
if (!categories) return {};
|
||||
return buildCollectionByKey(categories, 'type');
|
||||
}, [categories]);
|
||||
|
||||
const sortedItemIds = useMemo(() => {
|
||||
if (filterValue) {
|
||||
return itemIds;
|
||||
}
|
||||
|
||||
const lockedSelectedBucket: string[] = [];
|
||||
const unlockedBucket: string[] = [];
|
||||
const lockedUnselectableBucket: string[] = [];
|
||||
|
||||
itemIds.forEach((id) => {
|
||||
if (lockedSelectedIdsSet.has(id)) {
|
||||
lockedSelectedBucket.push(id);
|
||||
} else if (lockedUnselectedIdsSet.has(id)) {
|
||||
lockedUnselectableBucket.push(id);
|
||||
} else {
|
||||
unlockedBucket.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return lockedSelectedBucket.concat(unlockedBucket, lockedUnselectableBucket);
|
||||
}, [filterValue, itemIds, lockedSelectedIdsSet, lockedUnselectedIdsSet]);
|
||||
|
||||
const handleItemClick = useLastCallback((id: string) => {
|
||||
if (lockedSelectedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lockedUnselectedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowMultiple && categoriesByType[id]) {
|
||||
const categoryType = categoriesByType[id].type;
|
||||
const newSelectedCategories = selectedCategories?.slice() || [];
|
||||
if (newSelectedCategories.includes(categoryType)) {
|
||||
newSelectedCategories.splice(newSelectedCategories.indexOf(categoryType), 1);
|
||||
} else {
|
||||
newSelectedCategories.push(categoryType);
|
||||
}
|
||||
optionalProps.onSelectedCategoriesChange?.(newSelectedCategories);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowMultiple) {
|
||||
const newSelectedIds = selectedIds.slice();
|
||||
if (newSelectedIds.includes(id)) {
|
||||
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
|
||||
} else {
|
||||
newSelectedIds.push(id);
|
||||
}
|
||||
optionalProps.onSelectedIdsChange?.(newSelectedIds);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoriesByType[id]) {
|
||||
optionalProps.onSelectedCategoryChange?.(categoriesByType[id].type);
|
||||
return;
|
||||
}
|
||||
|
||||
optionalProps.onSelectedIdChange?.(id);
|
||||
});
|
||||
|
||||
const handleFilterChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.currentTarget;
|
||||
onFilterChange?.(value);
|
||||
});
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(
|
||||
onLoadMore, sortedItemIds, Boolean(filterValue),
|
||||
);
|
||||
|
||||
const renderItem = useCallback((id: string, isCategory?: boolean) => {
|
||||
const global = getGlobal();
|
||||
const category = isCategory ? categoriesByType[id] : undefined;
|
||||
const peer = !isCategory ? selectPeer(global, id) : undefined;
|
||||
|
||||
const peerOrCategory = peer || category;
|
||||
if (!peerOrCategory) {
|
||||
if (DEBUG) return <div key={id}>No peer or category with ID {id}</div>;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isSelf = peer && !isApiPeerChat(peer) ? (peer.isSelf && !forceShowSelf) : undefined;
|
||||
|
||||
const isAlwaysUnselected = lockedUnselectedIdsSet.has(id);
|
||||
const isAlwaysSelected = lockedSelectedIdsSet.has(id);
|
||||
const isLocked = isAlwaysUnselected || isAlwaysSelected;
|
||||
const isChecked = category ? selectedCategories?.includes(category.type) : selectedIds.includes(id);
|
||||
|
||||
function getInputElement() {
|
||||
if (isLocked) return <Icon name="lock-badge" />;
|
||||
if (itemInputType === 'radio') {
|
||||
return <Radio checked={isChecked} disabled={isLocked} onlyInput />;
|
||||
}
|
||||
if (itemInputType === 'checkbox') {
|
||||
return <Checkbox checked={isChecked} disabled={isLocked} onlyInput />;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getSubtitle() {
|
||||
if (isAlwaysUnselected) return [lockedUnselectedSubtitle];
|
||||
if (withStatus && peer) {
|
||||
if (isApiPeerChat(peer)) {
|
||||
return [getGroupStatus(lang, peer)];
|
||||
}
|
||||
|
||||
const userStatus = selectUserStatus(global, peer.id);
|
||||
return [
|
||||
getUserStatus(lang, peer, userStatus),
|
||||
buildClassName(isUserOnline(peer, userStatus, true) && styles.onlineStatus),
|
||||
];
|
||||
}
|
||||
if (withPeerTypes && peer) {
|
||||
const langKey = getPeerTypeKey(peer);
|
||||
return langKey && [lang(langKey)];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [subtitle, subtitleClassName] = getSubtitle() || [];
|
||||
|
||||
return (
|
||||
<PickerItem
|
||||
key={id}
|
||||
className={buildClassName(ITEM_CLASS_NAME, itemClassName)}
|
||||
title={<FullNameTitle peer={peerOrCategory} />}
|
||||
avatarElement={(
|
||||
<Avatar
|
||||
peer={peer || category}
|
||||
isSavedMessages={isSelf}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
disabled={isLocked}
|
||||
inactive={isViewOnly}
|
||||
ripple
|
||||
inputElement={getInputElement()}
|
||||
inputPosition="end"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleItemClick(id)}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onDisabledClick={onDisabledClick && (() => onDisabledClick(id, isAlwaysSelected))}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
categoriesByType, forceShowSelf, isViewOnly, itemClassName, itemInputType, lang, lockedSelectedIdsSet,
|
||||
lockedUnselectedIdsSet, lockedUnselectedSubtitle, onDisabledClick, selectedCategories, selectedIds,
|
||||
withPeerTypes, withStatus,
|
||||
]);
|
||||
|
||||
const beforeChildren = useMemo(() => {
|
||||
if (!categories?.length) return undefined;
|
||||
return (
|
||||
<div key="categories">
|
||||
{categoryPlaceholderKey && <div className={styles.pickerCategoryTitle}>{lang(categoryPlaceholderKey)}</div>}
|
||||
{categories?.map((category) => renderItem(category.type, true))}
|
||||
<div className={styles.pickerCategoryTitle}>{lang('FilterChats')}</div>
|
||||
</div>
|
||||
);
|
||||
}, [categories, categoryPlaceholderKey, lang, renderItem]);
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.container, className)}>
|
||||
{isSearchable && (
|
||||
<div className={buildClassName(styles.header, 'custom-scroll')} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{selectedCategories?.map((category) => (
|
||||
<PickerSelectedItem
|
||||
customPeer={categoriesByType[category]}
|
||||
onClick={handleItemClick}
|
||||
clickArg={category}
|
||||
canClose
|
||||
/>
|
||||
))}
|
||||
{lockedSelectedIds?.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
peerId={id}
|
||||
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
||||
forceShowSelf={forceShowSelf}
|
||||
onClick={handleItemClick}
|
||||
clickArg={id}
|
||||
/>
|
||||
))}
|
||||
{unlockedSelectedIds.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
peerId={id}
|
||||
isMinimized={
|
||||
shouldMinimize && i + (lockedSelectedIds?.length || 0) < 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={buildClassName(styles.pickerList, withDefaultPadding && styles.padded, 'custom-scroll')}
|
||||
items={viewportIds}
|
||||
itemSelector={`.${ITEM_CLASS_NAME}`}
|
||||
beforeChildren={beforeChildren}
|
||||
onLoadMore={getMore}
|
||||
noScrollRestore={noScrollRestore}
|
||||
>
|
||||
{viewportIds.map((id) => renderItem(id))}
|
||||
</InfiniteScroll>
|
||||
) : !isLoading && viewportIds && !viewportIds.length ? (
|
||||
<p className={styles.noResults}>{notFoundText || 'Sorry, nothing found.'}</p>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PeerPicker);
|
||||
101
src/components/common/pickers/PickerItem.module.scss
Normal file
101
src/components/common/pickers/PickerItem.module.scss
Normal file
@ -0,0 +1,101 @@
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-columns: min-content min-content 1fr min-content;
|
||||
align-items: center;
|
||||
|
||||
position: relative;
|
||||
padding: 0.25rem;
|
||||
min-height: 2.5rem;
|
||||
|
||||
border-radius: var(--border-radius-default);
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--background-color);
|
||||
color: var(--color-text);
|
||||
line-height: 1.25;
|
||||
|
||||
transition-property: background-color, opacity;
|
||||
transition-duration: 150ms;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--color-item-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
grid-row: 2;
|
||||
grid-column: 2 / 4;
|
||||
/* stylelint-disable-next-line plugin/whole-pixel */
|
||||
height: 0.5px;
|
||||
background-color: var(--color-dividers);
|
||||
align-self: end;
|
||||
margin-bottom: -0.1875rem;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: unset;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-row: 1 / 3;
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
grid-row: 2;
|
||||
grid-column: 3;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.title, .subtitle {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.withAvatar, .multiline {
|
||||
height: 3.5rem; // Force chat item height
|
||||
}
|
||||
|
||||
.multiline {
|
||||
.title {
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.startInput {
|
||||
grid-row: 1 / 3;
|
||||
grid-column: 1;
|
||||
margin-inline-end: 1.25rem;
|
||||
}
|
||||
|
||||
.endInput {
|
||||
grid-row: 1 / 3;
|
||||
grid-column: 4;
|
||||
margin-inline-start: 1.25rem;
|
||||
}
|
||||
|
||||
.avatarElement {
|
||||
grid-row: 1 / 3;
|
||||
grid-column: 2;
|
||||
margin-inline-end: 1.25rem;
|
||||
}
|
||||
104
src/components/common/pickers/PickerItem.tsx
Normal file
104
src/components/common/pickers/PickerItem.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import React, { type TeactNode } from '../../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { IS_IOS } from '../../../util/windowEnvironment';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import RippleEffect from '../../ui/RippleEffect';
|
||||
|
||||
import styles from './PickerItem.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
title: TeactNode;
|
||||
subtitle?: TeactNode;
|
||||
avatarElement?: TeactNode;
|
||||
inputElement?: TeactNode;
|
||||
inputPosition?: 'start' | 'end';
|
||||
disabled?: boolean;
|
||||
inactive?: boolean;
|
||||
ripple?: boolean;
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
onClick?: NoneToVoidFunction;
|
||||
onDisabledClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const PickerItem = ({
|
||||
title,
|
||||
subtitle,
|
||||
avatarElement,
|
||||
inputElement,
|
||||
inputPosition = 'start',
|
||||
disabled,
|
||||
inactive,
|
||||
ripple,
|
||||
className,
|
||||
titleClassName,
|
||||
subtitleClassName,
|
||||
onClick,
|
||||
onDisabledClick,
|
||||
}: OwnProps) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
const isClickable = !inactive && !disabled;
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (inactive) return;
|
||||
|
||||
if (disabled) {
|
||||
onDisabledClick?.();
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.();
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
subtitle && styles.multiline,
|
||||
disabled && styles.disabled,
|
||||
isClickable && styles.clickable,
|
||||
avatarElement && styles.withAvatar,
|
||||
className,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
role={isClickable ? 'button' : undefined}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex={isClickable ? 0 : undefined}
|
||||
>
|
||||
{!disabled && !inactive && ripple && <RippleEffect />}
|
||||
{inputElement && (
|
||||
<div className={buildClassName(
|
||||
styles.input,
|
||||
inputPosition === 'end' ? styles.endInput : styles.startInput,
|
||||
)}
|
||||
>
|
||||
{inputElement}
|
||||
</div>
|
||||
)}
|
||||
{avatarElement && (
|
||||
<div className={styles.avatarElement}>
|
||||
{avatarElement}
|
||||
</div>
|
||||
)}
|
||||
<div className={buildClassName(styles.title, titleClassName)}>
|
||||
{title}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<div className={buildClassName(styles.subtitle, subtitleClassName)}>
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
{!inputElement && IS_IOS && (
|
||||
<div className={styles.separator} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PickerItem;
|
||||
26
src/components/common/pickers/PickerModal.module.scss
Normal file
26
src/components/common/pickers/PickerModal.module.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fixedHeight .content {
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.withSearch {
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: -0.75rem; // Pull search closer to the header
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
58
src/components/common/pickers/PickerModal.tsx
Normal file
58
src/components/common/pickers/PickerModal.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Modal, { type OwnProps as ModalProps } from '../../ui/Modal';
|
||||
|
||||
import styles from './PickerModal.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
confirmButtonText?: string;
|
||||
isConfirmDisabled?: boolean;
|
||||
shouldAdaptToSearch?: boolean;
|
||||
withFixedHeight?: boolean;
|
||||
onConfirm?: NoneToVoidFunction;
|
||||
} & ModalProps;
|
||||
|
||||
const PickerModal = ({
|
||||
confirmButtonText,
|
||||
isConfirmDisabled,
|
||||
shouldAdaptToSearch,
|
||||
withFixedHeight,
|
||||
onConfirm,
|
||||
...modalProps
|
||||
}: OwnProps) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...modalProps}
|
||||
isSlim
|
||||
className={buildClassName(
|
||||
shouldAdaptToSearch && styles.withSearch,
|
||||
withFixedHeight && styles.fixedHeight,
|
||||
modalProps.className,
|
||||
)}
|
||||
contentClassName={buildClassName(styles.content, modalProps.contentClassName)}
|
||||
headerClassName={buildClassName(styles.header, modalProps.headerClassName)}
|
||||
>
|
||||
{modalProps.children}
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button
|
||||
onClick={onConfirm || modalProps.onClose}
|
||||
color="primary"
|
||||
size="smaller"
|
||||
disabled={isConfirmDisabled}
|
||||
>
|
||||
{confirmButtonText || lang('Confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PickerModal);
|
||||
@ -4,6 +4,7 @@
|
||||
background: var(--color-chat-hover);
|
||||
height: 2rem;
|
||||
min-width: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
padding-right: 1rem;
|
||||
@ -1,21 +1,21 @@
|
||||
import type { TeactNode } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
import type { TeactNode } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiUser } from '../../api/types';
|
||||
import type { CustomPeer } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||
import type { CustomPeer } from '../../../types';
|
||||
import type { IconName } from '../../../types/icons';
|
||||
|
||||
import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers';
|
||||
import { selectChat, selectUser } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { getPeerColorClass } from './helpers/peerColor';
|
||||
import renderText from './helpers/renderText';
|
||||
import { getChatTitle, getUserFirstOrLastName } from '../../../global/helpers';
|
||||
import { selectChat, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getPeerColorClass } from '../helpers/peerColor';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
import Icon from './icons/Icon';
|
||||
import Avatar from '../Avatar';
|
||||
import Icon from '../icons/Icon';
|
||||
|
||||
import './PickerSelectedItem.scss';
|
||||
|
||||
75
src/components/common/pickers/PickerStyles.module.scss
Normal file
75
src/components/common/pickers/PickerStyles.module.scss
Normal file
@ -0,0 +1,75 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
|
||||
:global(.input-group) {
|
||||
margin-bottom: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
:global(.form-control) {
|
||||
height: 2rem;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pickerCategoryTitle {
|
||||
color: var(--color-text-secondary);
|
||||
padding-inline: 0.5rem;
|
||||
font-weight: 500;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid var(--color-borders);
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pickerList {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 0.5rem;
|
||||
scrollbar-gutter: stable;
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.noResults {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 1rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.onlineStatus {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
@ -87,7 +87,6 @@ const ChatFolderModal: FC<OwnProps & StateProps> = ({
|
||||
options={folders}
|
||||
selected={selectedFolderIds}
|
||||
onChange={setSelectedFolderIds}
|
||||
round
|
||||
/>
|
||||
<div className="dialog-buttons">
|
||||
<Button color="primary" className="confirm-dialog-button" isText onClick={handleSubmit}>
|
||||
|
||||
@ -36,7 +36,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
import { useFullscreenStatus } from '../../../hooks/window/useFullscreen';
|
||||
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
|
||||
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||
import StoryToggler from '../../story/StoryToggler';
|
||||
import Button from '../../ui/Button';
|
||||
import DropdownMenu from '../../ui/DropdownMenu';
|
||||
|
||||
@ -10,7 +10,7 @@ import sortChatIds from '../../common/helpers/sortChatIds';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import Button from '../../ui/Button';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
|
||||
@ -99,7 +99,7 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
|
||||
<h3>{lang('GroupAddMembers')}</h3>
|
||||
</div>
|
||||
<div className="NewChat-inner step-1">
|
||||
<Picker
|
||||
<PeerPicker
|
||||
itemIds={displayedIds}
|
||||
selectedIds={selectedMemberIds}
|
||||
filterValue={searchQuery}
|
||||
@ -107,6 +107,10 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
|
||||
searchInputId="new-group-picker-search"
|
||||
isLoading={isSearching}
|
||||
isSearchable
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
withDefaultPadding
|
||||
onSelectedIdsChange={onSelectedMemberIdsChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
|
||||
@ -28,7 +28,7 @@ import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import NothingFound from '../../common/NothingFound';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import Link from '../../ui/Link';
|
||||
import ChatMessage from './ChatMessage';
|
||||
|
||||
@ -184,7 +184,7 @@
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 0 0.125rem 0.5rem 0.5rem;
|
||||
padding: 0 0.5rem 0.5rem 0.5rem;
|
||||
|
||||
.section-heading {
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
@ -13,7 +13,7 @@ import { unique } from '../../../util/iteratees';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import ChatOrUserPicker from '../../common/ChatOrUserPicker';
|
||||
import ChatOrUserPicker from '../../common/pickers/ChatOrUserPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
|
||||
@ -118,12 +118,21 @@
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ListItem.narrow {
|
||||
margin-bottom: 0;
|
||||
|
||||
.ListItem-button {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-item-simple,
|
||||
.settings-item {
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
padding: 1.5rem 0.5rem 1rem;
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
@include mixins.side-panel-section;
|
||||
}
|
||||
|
||||
@ -137,7 +146,8 @@
|
||||
&-header {
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-inline-start: 1rem;
|
||||
position: relative;
|
||||
|
||||
&[dir="rtl"] {
|
||||
@ -154,6 +164,7 @@
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: -0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-inline-start: 1rem;
|
||||
|
||||
.settings-content.two-fa &,
|
||||
.settings-content.password-form &,
|
||||
@ -189,9 +200,7 @@
|
||||
}
|
||||
|
||||
.ListItem {
|
||||
margin: 0 -1rem 1rem;
|
||||
|
||||
&:last-child {
|
||||
&.narrow {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -302,11 +311,7 @@
|
||||
|
||||
.RangeSlider {
|
||||
margin-bottom: 1.0625rem;
|
||||
}
|
||||
|
||||
.Checkbox,
|
||||
.radio-group {
|
||||
margin: 0 -1rem 0.5rem;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
@ -324,6 +329,10 @@
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
> .Checkbox, > .Radio {
|
||||
padding-inline-start: 4.25rem;
|
||||
}
|
||||
|
||||
&__current-value {
|
||||
margin-inline-start: auto;
|
||||
padding-inline-start: 0.5rem;
|
||||
@ -332,6 +341,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.settings-picker {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.settings-picker-title {
|
||||
margin-inline-start: var(--picker-title-shift);
|
||||
}
|
||||
|
||||
.settings-fab-wrapper {
|
||||
height: calc(100% - var(--header-height));
|
||||
position: relative;
|
||||
@ -368,8 +385,6 @@
|
||||
}
|
||||
|
||||
.settings-dropdown-section {
|
||||
margin: 0 -0.75rem 1rem -1rem;
|
||||
|
||||
.DropdownList {
|
||||
position: relative;
|
||||
|
||||
@ -381,7 +396,7 @@
|
||||
|
||||
.SettingsDefaultReaction {
|
||||
.current-default-reaction {
|
||||
margin-inline-end: 2rem;
|
||||
margin-inline-end: 1.1875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,6 +408,10 @@
|
||||
margin: inherit;
|
||||
}
|
||||
|
||||
.settings-item-picker {
|
||||
padding: 1.5rem 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.block-user-button {
|
||||
z-index: var(--z-chat-float-button);
|
||||
}
|
||||
|
||||
@ -14,7 +14,11 @@ $icons: "android", "apple", "brave", "chrome", "edge", "firefox", "linux", "oper
|
||||
background-repeat: no-repeat;
|
||||
background-size: 2rem;
|
||||
flex: 0 0 2rem;
|
||||
margin-inline-end: 1.5rem !important;
|
||||
margin-inline-end: 1.25rem !important;
|
||||
}
|
||||
|
||||
.icon-stop {
|
||||
margin-inline: 0.25rem 1.5rem !important;
|
||||
}
|
||||
|
||||
@each $icon in $icons {
|
||||
|
||||
@ -145,7 +145,7 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
||||
function renderCurrentSession(session: ApiSession) {
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('AuthSessions.CurrentSession')}
|
||||
</h4>
|
||||
|
||||
@ -177,7 +177,7 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
||||
function renderOtherSessions(sessionHashes: string[]) {
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('OtherSessions')}
|
||||
</h4>
|
||||
|
||||
@ -189,7 +189,7 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
||||
function renderAutoTerminate() {
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('TerminateOldSessionHeader')}
|
||||
</h4>
|
||||
|
||||
|
||||
@ -81,7 +81,7 @@ const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
|
||||
function renderSessions(sessionHashes: string[]) {
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('WebSessionsTitle')}
|
||||
</h4>
|
||||
|
||||
|
||||
@ -6,14 +6,4 @@
|
||||
.item {
|
||||
overflow: hidden;
|
||||
min-height: 25rem;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.languages {
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@ -1,22 +1,21 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
memo, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ISettings } from '../../../types';
|
||||
import type { IRadioOption } from '../../ui/CheckboxGroup';
|
||||
|
||||
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { partition, unique } from '../../../util/iteratees';
|
||||
import { partition } from '../../../util/iteratees';
|
||||
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import InputText from '../../ui/InputText';
|
||||
import ItemPicker, { type ItemPickerOption } from '../../common/pickers/ItemPicker';
|
||||
|
||||
import styles from './SettingsDoNotTranslate.module.scss';
|
||||
|
||||
@ -62,11 +61,11 @@ const SettingsDoNotTranslate: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const lang = useOldLang();
|
||||
const language = lang.code || 'en';
|
||||
const [displayedOptions, setDisplayedOptions] = useState<IRadioOption[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [displayedOptions, setDisplayedOptions] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
const options: IRadioOption[] = useMemo(() => {
|
||||
return SUPPORTED_LANGUAGES.map((langCode: string) => {
|
||||
const displayedOptionList: ItemPickerOption[] = useMemo(() => {
|
||||
const options = SUPPORTED_LANGUAGES.map((langCode: string) => {
|
||||
const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
|
||||
const translatedName = translatedNames.of(langCode)!;
|
||||
|
||||
@ -78,58 +77,33 @@ const SettingsDoNotTranslate: FC<OwnProps & StateProps> = ({
|
||||
translatedName,
|
||||
originalName,
|
||||
};
|
||||
}).map(({ langCode, translatedName, originalName }) => ({
|
||||
}).filter(Boolean).map(({ langCode, translatedName, originalName }) => ({
|
||||
label: translatedName,
|
||||
subLabel: originalName,
|
||||
value: langCode,
|
||||
}));
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) setSearch('');
|
||||
}, [isActive]);
|
||||
|
||||
useEffectWithPrevDeps(([prevIsActive]) => {
|
||||
if (prevIsActive === isActive) return;
|
||||
if (isActive && displayedOptions.length) return;
|
||||
|
||||
const current = options.find((option) => option.value === language);
|
||||
const otherLanguages = options.filter((option) => option.value !== language);
|
||||
|
||||
const [selected, unselected] = partition(otherLanguages, (option) => doNotTranslate.includes(option.value));
|
||||
|
||||
setDisplayedOptions([current!, ...selected, ...unselected]);
|
||||
}, [isActive, doNotTranslate, displayedOptions.length, language, options]);
|
||||
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value, checked } = event.currentTarget;
|
||||
let newDoNotTranslate: string[];
|
||||
if (checked) {
|
||||
newDoNotTranslate = unique([...doNotTranslate, value]);
|
||||
} else {
|
||||
newDoNotTranslate = doNotTranslate.filter((v) => v !== value);
|
||||
if (!searchQuery.trim()) {
|
||||
const currentLanguageOption = options.find((option) => option.value === language);
|
||||
const otherOptionList = options.filter((option) => option.value !== language);
|
||||
return currentLanguageOption ? [currentLanguageOption, ...otherOptionList] : options;
|
||||
}
|
||||
|
||||
return options?.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
}, [language, searchQuery]);
|
||||
|
||||
useEffectWithPrevDeps(([prevIsActive, prevLanguage]) => {
|
||||
if (prevIsActive === isActive && prevLanguage?.find((option) => option === language)) return;
|
||||
const [selected] = partition(displayedOptionList, (option) => doNotTranslate.includes(option.value));
|
||||
setDisplayedOptions([...selected.map((option) => option.value)]);
|
||||
}, [isActive, doNotTranslate, displayedOptions.length, language, displayedOptionList]);
|
||||
|
||||
const handleChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
setDisplayedOptions(newSelectedIds);
|
||||
setSettingOption({
|
||||
doNotTranslate: newDoNotTranslate,
|
||||
doNotTranslate: newSelectedIds,
|
||||
});
|
||||
}, [doNotTranslate, setSettingOption]);
|
||||
|
||||
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
}, []);
|
||||
|
||||
const filteredDisplayedOptions = useMemo(() => {
|
||||
if (!search.trim()) {
|
||||
return displayedOptions;
|
||||
}
|
||||
|
||||
return displayedOptions.filter((option) => (
|
||||
option.label.toString().toLowerCase().includes(search.toLowerCase())
|
||||
|| option.subLabel?.toLowerCase().includes(search.toLowerCase())
|
||||
|| option.value.toLowerCase().includes(search.toLowerCase())
|
||||
));
|
||||
}, [displayedOptions, search]);
|
||||
});
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
@ -137,28 +111,21 @@ const SettingsDoNotTranslate: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, 'settings-content custom-scroll')}>
|
||||
<div className={buildClassName(styles.item, 'settings-item')}>
|
||||
<InputText
|
||||
key="search"
|
||||
value={search}
|
||||
onChange={handleSearch}
|
||||
placeholder={lang('Search')}
|
||||
teactExperimentControlled
|
||||
<div className={buildClassName(styles.root, 'settings-content infinite-scroll')}>
|
||||
<div className={buildClassName(styles.item)}>
|
||||
<ItemPicker
|
||||
className={styles.picker}
|
||||
items={displayedOptionList}
|
||||
selectedValues={displayedOptions}
|
||||
onSelectedValuesChange={handleChange}
|
||||
filterValue={searchQuery}
|
||||
onFilterChange={setSearchQuery}
|
||||
isSearchable
|
||||
allowMultiple
|
||||
withDefaultPadding
|
||||
itemInputType="checkbox"
|
||||
searchInputId="lang-picker-search"
|
||||
/>
|
||||
<div className={buildClassName(styles.languages, 'radio-group custom-scroll')}>
|
||||
{filteredDisplayedOptions.map((option) => (
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
label={option.label}
|
||||
subLabel={option.subLabel}
|
||||
checked={doNotTranslate.includes(option.value)}
|
||||
value={option.value}
|
||||
key={option.value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -146,6 +146,7 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
||||
|
||||
<ListItem
|
||||
icon="photo"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.GeneralChatBackground)}
|
||||
>
|
||||
|
||||
@ -4,7 +4,6 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiLanguage } from '../../../api/types';
|
||||
import type { ISettings, LangCode } from '../../../types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
@ -17,10 +16,10 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import ItemPicker, { type ItemPickerOption } from '../../common/pickers/ItemPicker';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Loading from '../../ui/Loading';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
@ -77,8 +76,19 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const options = useMemo(() => {
|
||||
return languages ? buildOptions(languages) : undefined;
|
||||
}, [languages]);
|
||||
if (!languages) return undefined;
|
||||
const currentLangCode = (window.navigator.language || 'en').toLowerCase();
|
||||
const shortLangCode = currentLangCode.substr(0, 2);
|
||||
|
||||
return languages.map(({ langCode, nativeName, name }) => ({
|
||||
value: langCode,
|
||||
label: nativeName,
|
||||
subLabel: name,
|
||||
isLoading: langCode === selectedLanguage && isLoading,
|
||||
} satisfies ItemPickerOption)).sort((a) => {
|
||||
return currentLangCode && (a.value === currentLangCode || a.value === shortLangCode) ? -1 : 0;
|
||||
});
|
||||
}, [isLoading, languages, selectedLanguage]);
|
||||
|
||||
const handleShouldTranslateChange = useLastCallback((newValue: boolean) => {
|
||||
setSettingOption({ canTranslate: newValue });
|
||||
@ -128,7 +138,6 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
onCheck={handleShouldTranslateChange}
|
||||
/>
|
||||
<Checkbox
|
||||
className="pb-2"
|
||||
label={lang('ShowTranslateChatButton')}
|
||||
checked={canTranslateChatsEnabled}
|
||||
disabled={!isCurrentUserPremium}
|
||||
@ -138,6 +147,7 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
{(canTranslate || canTranslateChatsEnabled) && (
|
||||
<ListItem
|
||||
narrow
|
||||
onClick={handleDoNotSelectOpen}
|
||||
>
|
||||
{lang('DoNotTranslate')}
|
||||
@ -149,15 +159,17 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header mb-4">{lang('Localization.InterfaceLanguage')}</h4>
|
||||
<div className="settings-item settings-item-picker">
|
||||
<h4 className="settings-item-header settings-picker-title">
|
||||
{lang('Localization.InterfaceLanguage')}
|
||||
</h4>
|
||||
{options ? (
|
||||
<RadioGroup
|
||||
name="language-settings"
|
||||
options={options}
|
||||
selected={selectedLanguage}
|
||||
loadingOption={isLoading ? selectedLanguage : undefined}
|
||||
onChange={handleChange}
|
||||
<ItemPicker
|
||||
items={options}
|
||||
selectedValue={selectedLanguage}
|
||||
onSelectedValueChange={handleChange}
|
||||
itemInputType="radio"
|
||||
className="settings-picker"
|
||||
/>
|
||||
) : (
|
||||
<Loading />
|
||||
@ -167,19 +179,6 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function buildOptions(languages: ApiLanguage[]) {
|
||||
const currentLangCode = (window.navigator.language || 'en').toLowerCase();
|
||||
const shortLangCode = currentLangCode.substr(0, 2);
|
||||
|
||||
return languages.map(({ langCode, nativeName, name }) => ({
|
||||
value: langCode,
|
||||
label: nativeName,
|
||||
subLabel: name,
|
||||
})).sort((a) => {
|
||||
return currentLangCode && (a.value === currentLangCode || a.value === shortLangCode) ? -1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const {
|
||||
|
||||
@ -95,6 +95,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
<ListItem
|
||||
icon="settings"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.General)}
|
||||
>
|
||||
@ -102,6 +103,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="animations"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.Performance)}
|
||||
>
|
||||
@ -109,6 +111,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="unmute"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.Notifications)}
|
||||
>
|
||||
@ -116,6 +119,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="data"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.DataStorage)}
|
||||
>
|
||||
@ -123,6 +127,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="lock"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.Privacy)}
|
||||
>
|
||||
@ -130,6 +135,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="folder"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.Folders)}
|
||||
>
|
||||
@ -137,6 +143,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="active-sessions"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.ActiveSessions)}
|
||||
>
|
||||
@ -145,6 +152,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="language"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.Language)}
|
||||
>
|
||||
@ -153,6 +161,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="stickers"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.Stickers)}
|
||||
>
|
||||
@ -164,6 +173,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
<ListItem
|
||||
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
|
||||
className="settings-main-menu-star"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openPremiumModal()}
|
||||
>
|
||||
@ -174,6 +184,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
<ListItem
|
||||
leftElement={<StarIcon className="icon ListItem-main-icon" type="gold" size="big" />}
|
||||
className="settings-main-menu-star"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openStarsBalanceModal({})}
|
||||
>
|
||||
@ -187,6 +198,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
<ListItem
|
||||
icon="gift"
|
||||
className="settings-main-menu-star"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openPremiumGiftingModal()}
|
||||
>
|
||||
@ -197,12 +209,14 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
<div className="settings-main-menu">
|
||||
<ListItem
|
||||
icon="ask-support"
|
||||
narrow
|
||||
onClick={openSupportDialog}
|
||||
>
|
||||
{lang('AskAQuestion')}
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="help"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openUrl({ url: FAQ_URL })}
|
||||
>
|
||||
@ -210,6 +224,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon="privacy-policy"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openUrl({ url: PRIVACY_URL })}
|
||||
>
|
||||
|
||||
@ -103,9 +103,11 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
}, [updateContentSettings]);
|
||||
|
||||
function getVisibilityValue(setting?: ApiPrivacySettings) {
|
||||
const { visibility, shouldAllowPremium } = setting || {};
|
||||
const blockCount = setting ? setting.blockChatIds.length + setting.blockUserIds.length : 0;
|
||||
const allowCount = setting ? setting.allowChatIds.length + setting.allowUserIds.length : 0;
|
||||
if (!setting) return lang('Loading');
|
||||
|
||||
const { visibility, shouldAllowPremium } = setting;
|
||||
const blockCount = setting.blockChatIds.length + setting.blockUserIds.length;
|
||||
const allowCount = setting.allowChatIds.length + setting.allowUserIds.length;
|
||||
const total = [];
|
||||
if (blockCount) total.push(`-${blockCount}`);
|
||||
if (allowCount) total.push(`+${allowCount}`);
|
||||
@ -135,6 +137,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="settings-item pt-3">
|
||||
<ListItem
|
||||
icon="delete-user"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.PrivacyBlockedUsers)}
|
||||
>
|
||||
@ -176,6 +179,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
{webAuthCount > 0 && (
|
||||
<ListItem
|
||||
icon="web"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.ActiveWebsites)}
|
||||
>
|
||||
@ -186,7 +190,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>{lang('PrivacyTitle')}</h4>
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('PrivacyTitle')}</h4>
|
||||
|
||||
<ListItem
|
||||
narrow
|
||||
|
||||
@ -293,7 +293,7 @@ function PrivacySubsection({
|
||||
</div>
|
||||
{!isPremiumRequired && (primaryExceptionLists.shouldShowAllowed || primaryExceptionLists.shouldShowDenied) && (
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('PrivacyExceptions')}
|
||||
</h4>
|
||||
{primaryExceptionLists.shouldShowAllowed && (
|
||||
|
||||
@ -20,7 +20,7 @@ import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
|
||||
export type OwnProps = {
|
||||
@ -139,7 +139,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className="NewChat-inner step-1">
|
||||
<Picker
|
||||
<PeerPicker
|
||||
categories={withPremiumCategory ? PREMIUM_CATEGORY : undefined}
|
||||
itemIds={displayedIds || []}
|
||||
selectedIds={newSelectedContactIds}
|
||||
@ -152,6 +152,10 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
onSelectedIdsChange={handleSelectedContactIdsChange}
|
||||
onSelectedCategoriesChange={handleSelectedCategoriesChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
allowMultiple
|
||||
itemInputType="checkbox"
|
||||
withDefaultPadding
|
||||
withStatus
|
||||
/>
|
||||
|
||||
<FloatingActionButton
|
||||
|
||||
@ -58,6 +58,7 @@ const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||
options={options}
|
||||
selected={selectedReaction}
|
||||
onChange={handleChange}
|
||||
withIcon
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -12,6 +12,10 @@
|
||||
}
|
||||
|
||||
.settings-folders-list-item {
|
||||
.ListItem-main-icon {
|
||||
margin-inline-end: 1.875rem !important;
|
||||
}
|
||||
|
||||
.ChatInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -99,7 +103,9 @@
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.down {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
.Picker {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
import Picker from '../../../common/Picker';
|
||||
import PeerPicker from '../../../common/pickers/PeerPicker';
|
||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||
import Loading from '../../../ui/Loading';
|
||||
|
||||
@ -128,6 +128,7 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
|
||||
},
|
||||
});
|
||||
}
|
||||
setIsTouched(true);
|
||||
});
|
||||
|
||||
useHistoryBack({
|
||||
@ -141,7 +142,7 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className="Picker settings-folders-chat-list">
|
||||
<Picker
|
||||
<PeerPicker
|
||||
categories={shouldHideChatTypes ? undefined : chatTypes}
|
||||
itemIds={displayedIds}
|
||||
selectedIds={selectedChatIds}
|
||||
@ -151,7 +152,10 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
|
||||
categoryPlaceholderKey="FilterChatTypes"
|
||||
searchInputId="new-group-picker-search"
|
||||
isSearchable
|
||||
isRoundCheckbox
|
||||
withDefaultPadding
|
||||
withPeerTypes
|
||||
allowMultiple
|
||||
itemInputType="checkbox"
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
onSelectedCategoriesChange={handleSelectedChatTypesChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
|
||||
@ -28,7 +28,6 @@ import useOldLang from '../../../../hooks/useOldLang';
|
||||
|
||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
import GroupChatInfo from '../../../common/GroupChatInfo';
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
import PrivateChatInfo from '../../../common/PrivateChatInfo';
|
||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||
import InputText from '../../../ui/InputText';
|
||||
@ -267,10 +266,12 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
{(!isExpanded && leftChatsCount > 0) && (
|
||||
<ListItem
|
||||
key="load-more"
|
||||
className="settings-folders-list-item"
|
||||
narrow
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={clickHandler}
|
||||
icon="down"
|
||||
>
|
||||
<Icon name="down" className="down" />
|
||||
{lang('FilterShowMoreChats', leftChatsCount, 'i')}
|
||||
</ListItem>
|
||||
)}
|
||||
@ -315,8 +316,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterInclude')}</h4>
|
||||
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
className="settings-folders-list-item color-primary"
|
||||
icon="add"
|
||||
narrow
|
||||
onClick={onAddIncludedChats}
|
||||
>
|
||||
{lang('FilterAddChats')}
|
||||
@ -331,8 +333,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
|
||||
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
className="settings-folders-list-item color-primary"
|
||||
icon="add"
|
||||
narrow
|
||||
onClick={onAddExcludedChats}
|
||||
>
|
||||
{lang('FilterAddChats')}
|
||||
@ -348,8 +351,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
</h4>
|
||||
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
className="settings-folders-list-item color-primary"
|
||||
icon="add"
|
||||
narrow
|
||||
onClick={handleCreateInviteClick}
|
||||
>
|
||||
{lang('ChatListFilter.CreateLinkNew')}
|
||||
@ -357,8 +361,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
|
||||
{invites?.map((invite) => (
|
||||
<ListItem
|
||||
className="settings-folders-list-item mb-0"
|
||||
className="settings-folders-list-item"
|
||||
icon="link"
|
||||
narrow
|
||||
multiline
|
||||
onClick={handleEditInviteClick}
|
||||
clickArg={invite.url}
|
||||
|
||||
@ -22,7 +22,7 @@ import useOldLang from '../../../../hooks/useOldLang';
|
||||
|
||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
import LinkField from '../../../common/LinkField';
|
||||
import Picker from '../../../common/Picker';
|
||||
import PeerPicker from '../../../common/pickers/PeerPicker';
|
||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||
import Spinner from '../../../ui/Spinner';
|
||||
|
||||
@ -167,14 +167,16 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
|
||||
isDisabled={!chatsCount || isTouched}
|
||||
/>
|
||||
|
||||
<div className="settings-item settings-item-chatlist">
|
||||
<Picker
|
||||
<div className="settings-item settings-item-picker">
|
||||
<PeerPicker
|
||||
itemIds={itemIds}
|
||||
lockedSelectedIds={lockedIds}
|
||||
lockedUnselectedIds={lockedIds}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
selectedIds={selectedIds}
|
||||
onDisabledClick={handleClickDisabled}
|
||||
isRoundCheckbox
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './AppendEntityPickerModal';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const AppendEntityPickerModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const AppendEntityPickerModal = useModuleLoader(Bundles.Extra, 'AppendEntityPickerModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return AppendEntityPickerModal ? <AppendEntityPickerModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default AppendEntityPickerModalAsync;
|
||||
@ -1,46 +0,0 @@
|
||||
.root :global(.modal-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.root :global(.modal-dialog) {
|
||||
max-width: 55vh;
|
||||
}
|
||||
|
||||
.root :global(.modal-dialog), .root :global(.modal-content) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.filter {
|
||||
padding: 0.375rem 1rem 0.25rem 0.75rem;
|
||||
margin-bottom: 0.625rem;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
width: 100%;
|
||||
background: var(--color-background);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 75vh;
|
||||
}
|
||||
@ -1,282 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo,
|
||||
useMemo,
|
||||
useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChat, ApiChatMember, ApiUserStatus } from '../../api/types';
|
||||
|
||||
import {
|
||||
filterChatsByName,
|
||||
filterUsersByName, isChatChannel, isChatPublic, isChatSuperGroup, isUserBot, sortUserIds,
|
||||
} from '../../global/helpers';
|
||||
import { selectChat, selectChatFullInfo } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { unique } from '../../util/iteratees';
|
||||
import sortChatIds from '../common/helpers/sortChatIds';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Picker from '../common/Picker';
|
||||
import Button from '../ui/Button';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
import styles from './AppendEntityPicker.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
chatId?: string;
|
||||
entityType: 'members' | 'channels' | undefined;
|
||||
onSubmit: (value: string[]) => void;
|
||||
selectionLimit: number;
|
||||
};
|
||||
|
||||
interface StateProps {
|
||||
chatId?: string;
|
||||
members?: ApiChatMember[];
|
||||
adminMembersById?: Record<string, ApiChatMember>;
|
||||
userStatusesById: Record<string, ApiUserStatus>;
|
||||
channelList?: (ApiChat | undefined)[] | undefined;
|
||||
isChannel?: boolean;
|
||||
isSuperGroup?: boolean;
|
||||
currentUserId?: string | undefined;
|
||||
}
|
||||
|
||||
const AppendEntityPickerModal: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
isOpen,
|
||||
onClose,
|
||||
members,
|
||||
adminMembersById,
|
||||
userStatusesById,
|
||||
entityType,
|
||||
isChannel,
|
||||
isSuperGroup,
|
||||
onSubmit,
|
||||
currentUserId,
|
||||
selectionLimit,
|
||||
}) => {
|
||||
const { showNotification } = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [pendingChannelId, setPendingChannelId] = useState<string | undefined>(undefined);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
const channelsIds = useMemo(() => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const activeChatIds = getGlobal().chats.listIds.active;
|
||||
|
||||
return activeChatIds!.map((id) => chatsById[id])
|
||||
.filter((chat) => chat && (isChatChannel(chat)
|
||||
|| isChatSuperGroup(chat)) && chat.id !== chatId)
|
||||
.map((chat) => chat!.id);
|
||||
}, [chatId]);
|
||||
|
||||
const adminIds = useMemo(() => {
|
||||
return adminMembersById && Object.keys(adminMembersById);
|
||||
}, [adminMembersById]);
|
||||
|
||||
const memberIds = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
if (!members || !usersById) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userIds = sortUserIds(
|
||||
members.map(({ userId }) => userId),
|
||||
usersById,
|
||||
userStatusesById,
|
||||
);
|
||||
|
||||
return adminIds ? userIds.filter((userId) => userId !== currentUserId) : userIds;
|
||||
}, [adminIds, currentUserId, members, userStatusesById]);
|
||||
|
||||
const displayedMembersIds = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
const filteredContactIds = memberIds ? filterUsersByName(memberIds, usersById, searchQuery) : [];
|
||||
|
||||
return sortChatIds(unique(filteredContactIds).filter((userId) => {
|
||||
const user = usersById[userId];
|
||||
if (!user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !isUserBot(user);
|
||||
}));
|
||||
}, [memberIds, searchQuery]);
|
||||
|
||||
const displayedChannelIds = useMemo(() => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const foundChannelIds = channelsIds ? filterChatsByName(lang, channelsIds, chatsById, searchQuery) : [];
|
||||
|
||||
return sortChatIds(unique(foundChannelIds).filter((contactId) => {
|
||||
const chat = chatsById[contactId];
|
||||
if (!chat) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isChannel || isSuperGroup;
|
||||
}),
|
||||
false,
|
||||
selectedIds);
|
||||
}, [channelsIds, lang, searchQuery, selectedIds, isSuperGroup, isChannel]);
|
||||
|
||||
const handleCloseButtonClick = useLastCallback(() => {
|
||||
onSubmit([]);
|
||||
onClose();
|
||||
});
|
||||
|
||||
const handleSendIdList = useLastCallback(() => {
|
||||
onSubmit(selectedIds);
|
||||
onClose();
|
||||
});
|
||||
|
||||
const confirmPrivateLinkChannelSelection = useLastCallback(() => {
|
||||
if (pendingChannelId) {
|
||||
setSelectedIds((prevIds) => unique([...prevIds, pendingChannelId]));
|
||||
}
|
||||
closeConfirmModal();
|
||||
});
|
||||
|
||||
const handleSelectedMemberIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
if (newSelectedIds.length > selectionLimit) {
|
||||
showNotification({
|
||||
message: lang('BoostingSelectUpToWarningUsers', selectionLimit),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedIds(newSelectedIds);
|
||||
});
|
||||
|
||||
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const newlyAddedIds = newSelectedIds.filter((id) => !selectedIds.includes(id));
|
||||
const privateLinkChannelId = newlyAddedIds.find((id) => {
|
||||
const chat = chatsById[id];
|
||||
return chat && !isChatPublic(chat);
|
||||
});
|
||||
|
||||
if (selectedIds?.length >= selectionLimit) {
|
||||
showNotification({
|
||||
message: lang('BoostingSelectUpToWarningChannelsPlural', selectionLimit),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (privateLinkChannelId) {
|
||||
setPendingChannelId(privateLinkChannelId);
|
||||
openConfirmModal();
|
||||
} else {
|
||||
setSelectedIds(newSelectedIds);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
onSubmit([]);
|
||||
onClose();
|
||||
});
|
||||
|
||||
function renderSearchField() {
|
||||
return (
|
||||
<div className={styles.filter} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={handleCloseButtonClick}
|
||||
ariaLabel={lang('Close')}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
<h3 className={styles.title}>{lang(entityType === 'channels'
|
||||
? 'RequestPeer.ChooseChannelTitle' : 'BoostingAwardSpecificUsers')}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.root}
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
onEnter={handleSendIdList}
|
||||
>
|
||||
<div className={styles.main}>
|
||||
{renderSearchField()}
|
||||
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||
<Picker
|
||||
className={styles.picker}
|
||||
itemIds={entityType === 'members' ? displayedMembersIds : displayedChannelIds}
|
||||
selectedIds={selectedIds}
|
||||
filterValue={searchQuery}
|
||||
filterPlaceholder={lang('Search')}
|
||||
searchInputId={`${entityType}-picker-search`}
|
||||
onSelectedIdsChange={entityType === 'channels'
|
||||
? handleSelectedChannelIdsChange : handleSelectedMemberIdsChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
isSearchable
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button size="smaller" onClick={handleSendIdList}>
|
||||
{lang('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
title={lang('BoostingGiveawayPrivateChannel')}
|
||||
text={lang('BoostingGiveawayPrivateChannelWarning')}
|
||||
confirmLabel={lang('Add')}
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={closeConfirmModal}
|
||||
confirmHandler={confirmPrivateLinkChannelSelection}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global, { chatId, entityType }): StateProps => {
|
||||
const { statusesById: userStatusesById } = global.users;
|
||||
let isChannel;
|
||||
let isSuperGroup;
|
||||
let members: ApiChatMember[] | undefined;
|
||||
let adminMembersById: Record<string, ApiChatMember> | undefined;
|
||||
let currentUserId: string | undefined;
|
||||
|
||||
if (entityType === 'members') {
|
||||
currentUserId = global.currentUserId;
|
||||
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
|
||||
if (chatFullInfo) {
|
||||
members = chatFullInfo.members;
|
||||
adminMembersById = chatFullInfo.adminMembersById;
|
||||
}
|
||||
} if (entityType === 'channels') {
|
||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||
if (chat) {
|
||||
isChannel = isChatChannel(chat);
|
||||
isSuperGroup = isChatSuperGroup(chat);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
members,
|
||||
adminMembersById,
|
||||
userStatusesById,
|
||||
isChannel,
|
||||
isSuperGroup,
|
||||
currentUserId,
|
||||
};
|
||||
})(AppendEntityPickerModal));
|
||||
144
src/components/main/premium/GiveawayChannelPickerModal.tsx
Normal file
144
src/components/main/premium/GiveawayChannelPickerModal.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import {
|
||||
filterChatsByName, isChatChannel, isChatPublic, isChatSuperGroup,
|
||||
} from '../../../global/helpers';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
import sortChatIds from '../../common/helpers/sortChatIds';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import PickerModal from '../../common/pickers/PickerModal';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
type OwnProps = {
|
||||
isOpen?: boolean;
|
||||
giveawayChatId?: string;
|
||||
selectionLimit: number;
|
||||
initialSelectedIds: string[];
|
||||
onSelectedIdsConfirmed: (newSelectedIds: string[]) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const GiveawayChannelPickerModal = ({
|
||||
isOpen,
|
||||
giveawayChatId,
|
||||
selectionLimit,
|
||||
initialSelectedIds,
|
||||
onSelectedIdsConfirmed,
|
||||
onClose,
|
||||
}: OwnProps) => {
|
||||
const { showNotification } = getActions();
|
||||
const lang = useOldLang();
|
||||
|
||||
const [pendingChannelId, setPendingChannelId] = useState<string | undefined>(undefined);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>(initialSelectedIds);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIds(initialSelectedIds);
|
||||
}, [initialSelectedIds]);
|
||||
|
||||
const channelIds = useMemo(() => {
|
||||
const global = getGlobal();
|
||||
const chatsById = global.chats.byId;
|
||||
const { active, archived } = global.chats.listIds;
|
||||
const ids = (active || []).concat(archived || []);
|
||||
|
||||
return unique(ids).map((id) => chatsById[id])
|
||||
.filter((chat) => (
|
||||
chat && (
|
||||
isChatChannel(chat) || isChatSuperGroup(chat)
|
||||
) && chat.id !== giveawayChatId
|
||||
)).map((chat) => chat.id);
|
||||
}, [giveawayChatId]);
|
||||
|
||||
const displayedChannelIds = useMemo(() => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const foundChannelIds = channelIds ? filterChatsByName(lang, channelIds, chatsById, searchQuery) : [];
|
||||
|
||||
return sortChatIds(foundChannelIds,
|
||||
false,
|
||||
selectedIds);
|
||||
}, [channelIds, lang, searchQuery, selectedIds]);
|
||||
|
||||
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const newlyAddedIds = newSelectedIds.filter((id) => !selectedIds.includes(id));
|
||||
const privateLinkChannelId = newlyAddedIds.find((id) => {
|
||||
const chat = chatsById[id];
|
||||
return chat && !isChatPublic(chat);
|
||||
});
|
||||
|
||||
if (selectedIds?.length >= selectionLimit) {
|
||||
showNotification({
|
||||
message: lang('BoostingSelectUpToWarningChannelsPlural', selectionLimit),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (privateLinkChannelId) {
|
||||
setPendingChannelId(privateLinkChannelId);
|
||||
openConfirmModal();
|
||||
} else {
|
||||
setSelectedIds(newSelectedIds);
|
||||
}
|
||||
});
|
||||
|
||||
const confirmPrivateLinkChannelSelection = useLastCallback(() => {
|
||||
if (pendingChannelId) {
|
||||
setSelectedIds(unique([...selectedIds, pendingChannelId]));
|
||||
}
|
||||
closeConfirmModal();
|
||||
});
|
||||
|
||||
const handleModalConfirm = useLastCallback(() => {
|
||||
onSelectedIdsConfirmed(selectedIds);
|
||||
onClose();
|
||||
});
|
||||
|
||||
return (
|
||||
<PickerModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={lang('RequestPeer.ChooseChannelTitle')}
|
||||
hasCloseButton
|
||||
shouldAdaptToSearch
|
||||
withFixedHeight
|
||||
confirmButtonText={lang('Save')}
|
||||
onConfirm={handleModalConfirm}
|
||||
onEnter={handleModalConfirm}
|
||||
>
|
||||
<PeerPicker
|
||||
itemIds={displayedChannelIds}
|
||||
selectedIds={selectedIds}
|
||||
filterValue={searchQuery}
|
||||
filterPlaceholder={lang('Search')}
|
||||
onSelectedIdsChange={handleSelectedChannelIdsChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
isSearchable
|
||||
withDefaultPadding
|
||||
withStatus
|
||||
allowMultiple
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title={lang('BoostingGiveawayPrivateChannel')}
|
||||
text={lang('BoostingGiveawayPrivateChannelWarning')}
|
||||
confirmLabel={lang('Add')}
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={closeConfirmModal}
|
||||
confirmHandler={confirmPrivateLinkChannelSelection}
|
||||
/>
|
||||
</PickerModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GiveawayChannelPickerModal);
|
||||
@ -266,6 +266,12 @@
|
||||
margin-inline-start: initial !important;
|
||||
}
|
||||
|
||||
.removeChannel {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.root :global(.modal-dialog) {
|
||||
width: 100%;
|
||||
|
||||
@ -41,8 +41,9 @@ import Modal from '../../ui/Modal';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
import RangeSliderWithMarks from '../../ui/RangeSliderWithMarks';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import AppendEntityPickerModal from '../AppendEntityPickerModal';
|
||||
import GiveawayChannelPickerModal from './GiveawayChannelPickerModal';
|
||||
import GiveawayTypeOption from './GiveawayTypeOption';
|
||||
import GiveawayUserPickerModal from './GiveawayUserPickerModal';
|
||||
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
|
||||
|
||||
import styles from './GiveawayModal.module.scss';
|
||||
@ -121,8 +122,8 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
|
||||
const [isCountryPickerModalOpen, openCountryPickerModal, closeCountryPickerModal] = useFlag();
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||
const [isEntityPickerModalOpen, openEntityPickerModal, closeEntityPickerModal] = useFlag();
|
||||
const [entityType, setEntityType] = useState<'members' | 'channels' | undefined>(undefined);
|
||||
const [isUserPickerModalOpen, openUserPickerModal, closeUserPickerModal] = useFlag();
|
||||
const [isChannelPickerModalOpen, openChannelPickerModal, closeChannelPickerModal] = useFlag();
|
||||
|
||||
const TYPE_OPTIONS: TypeOption[] = [{
|
||||
name: 'BoostingCreateGiveaway',
|
||||
@ -139,43 +140,44 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
actions: 'createSpecificUsers',
|
||||
isLink: true,
|
||||
onClickAction: () => {
|
||||
openEntityPickerModal();
|
||||
setEntityType('members');
|
||||
openUserPickerModal();
|
||||
},
|
||||
}];
|
||||
|
||||
const [customExpireDate, setCustomExpireDate] = useState<number>(Date.now() + DEFAULT_CUSTOM_EXPIRE_DATE);
|
||||
const [isHeaderHidden, setHeaderHidden] = useState(true);
|
||||
const [selectedUserCount, setSelectedUserCount] = useState<number>(DEFAULT_BOOST_COUNT);
|
||||
const [selectedRandomUserCount, setSelectedRandomUserCount] = useState<number>(DEFAULT_BOOST_COUNT);
|
||||
const [selectedGiveawayOption, setGiveawayOption] = useState<ApiGiveawayType>(TYPE_OPTIONS[0].value);
|
||||
const [selectedSubscriberOption, setSelectedSubscriberOption] = useState<SubscribersType>('all');
|
||||
const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>();
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||
const [selectedChannelIds, setSelectedChannelIds] = useState<string[]>([]);
|
||||
const [selectedCountriesIds, setSelectedCountriesIds] = useState<string[] | undefined>([]);
|
||||
const [selectedCountryIds, setSelectedCountryIds] = useState<string[] | undefined>([]);
|
||||
const [shouldShowWinners, setShouldShowWinners] = useState<boolean>(false);
|
||||
const [shouldShowPrizes, setShouldShowPrizes] = useState<boolean>(false);
|
||||
const [prizeDescription, setPrizeDescription] = useState<string | undefined>(undefined);
|
||||
const [dataPrepaidGiveaway, setDataPrepaidGiveaway] = useState<ApiPrepaidGiveaway | undefined>(undefined);
|
||||
const boostQuantity = selectedUserCount * giveawayBoostPerPremiumLimit;
|
||||
|
||||
const isRandomUsers = selectedGiveawayOption === 'random_users';
|
||||
const selectedUserCount = isRandomUsers ? selectedRandomUserCount : selectedUserIds.length;
|
||||
const boostQuantity = selectedUserCount * giveawayBoostPerPremiumLimit;
|
||||
|
||||
const SUBSCRIBER_OPTIONS = useMemo(() => [
|
||||
{
|
||||
value: 'all',
|
||||
label: lang(isChannel ? 'BoostingAllSubscribers' : 'BoostingAllMembers'),
|
||||
subLabel: selectedCountriesIds && selectedCountriesIds.length > 0
|
||||
? lang('Giveaway.ReceiverType.Countries', selectedCountriesIds.length)
|
||||
subLabel: selectedCountryIds && selectedCountryIds.length > 0
|
||||
? lang('Giveaway.ReceiverType.Countries', selectedCountryIds.length)
|
||||
: lang('BoostingFromAllCountries'),
|
||||
},
|
||||
{
|
||||
value: 'new',
|
||||
label: lang(isChannel ? 'BoostingNewSubscribers' : 'BoostingNewMembers'),
|
||||
subLabel: selectedCountriesIds && selectedCountriesIds.length > 0
|
||||
? lang('Giveaway.ReceiverType.Countries', selectedCountriesIds.length)
|
||||
subLabel: selectedCountryIds && selectedCountryIds.length > 0
|
||||
? lang('Giveaway.ReceiverType.Countries', selectedCountryIds.length)
|
||||
: lang('BoostingFromAllCountries'),
|
||||
},
|
||||
], [isChannel, lang, selectedCountriesIds]);
|
||||
], [isChannel, lang, selectedCountryIds]);
|
||||
|
||||
const monthQuantity = lang('Months', selectedMonthOption);
|
||||
|
||||
@ -184,9 +186,8 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
}, [gifts, selectedMonthOption, selectedUserCount]);
|
||||
|
||||
const filteredGifts = useMemo(() => {
|
||||
return gifts?.filter((gift) => gift.users
|
||||
=== (selectedUserIds?.length ? selectedUserIds?.length : selectedUserCount));
|
||||
}, [gifts, selectedUserIds, selectedUserCount]);
|
||||
return gifts?.filter((gift) => gift.users === selectedUserCount);
|
||||
}, [gifts, selectedUserCount]);
|
||||
|
||||
const fullMonthlyAmount = useMemo(() => {
|
||||
if (!filteredGifts?.length) {
|
||||
@ -213,7 +214,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (prepaidGiveaway) {
|
||||
setSelectedUserCount(prepaidGiveaway.quantity);
|
||||
setSelectedRandomUserCount(prepaidGiveaway.quantity);
|
||||
setDataPrepaidGiveaway(prepaidGiveaway);
|
||||
}
|
||||
}, [prepaidGiveaway]);
|
||||
@ -235,25 +236,25 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (selectedUserIds?.length) {
|
||||
if (isRandomUsers) {
|
||||
openInvoice({
|
||||
type: 'giftcode',
|
||||
boostChannelId: chatId!,
|
||||
userIds: selectedUserIds,
|
||||
type: 'giveaway',
|
||||
chatId: chatId!,
|
||||
additionalChannelIds: selectedChannelIds,
|
||||
isOnlyForNewSubscribers: selectedSubscriberOption === 'new',
|
||||
countries: selectedCountryIds,
|
||||
areWinnersVisible: shouldShowWinners,
|
||||
prizeDescription,
|
||||
untilDate: customExpireDate / 1000,
|
||||
currency: selectedGift!.currency,
|
||||
amount: selectedGift!.amount,
|
||||
option: selectedGift!,
|
||||
});
|
||||
} else {
|
||||
openInvoice({
|
||||
type: 'giveaway',
|
||||
chatId: chatId!,
|
||||
additionalChannelIds: selectedChannelIds,
|
||||
isOnlyForNewSubscribers: selectedSubscriberOption === 'new',
|
||||
countries: selectedCountriesIds,
|
||||
areWinnersVisible: shouldShowWinners,
|
||||
prizeDescription,
|
||||
untilDate: customExpireDate / 1000,
|
||||
type: 'giftcode',
|
||||
boostChannelId: chatId!,
|
||||
userIds: selectedUserIds,
|
||||
currency: selectedGift!.currency,
|
||||
amount: selectedGift!.amount,
|
||||
option: selectedGift!,
|
||||
@ -269,7 +270,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
giveawayId: dataPrepaidGiveaway!.id,
|
||||
paymentPurpose: {
|
||||
additionalChannelIds: selectedChannelIds,
|
||||
countries: selectedCountriesIds,
|
||||
countries: selectedCountryIds,
|
||||
prizeDescription,
|
||||
areWinnersVisible: shouldShowWinners,
|
||||
untilDate: customExpireDate / 1000,
|
||||
@ -282,8 +283,8 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
closeGiveawayModal();
|
||||
});
|
||||
|
||||
const handleUserCountChange = useLastCallback((newValue) => {
|
||||
setSelectedUserCount(newValue);
|
||||
const handleRandomUserCountChange = useLastCallback((newValue) => {
|
||||
setSelectedRandomUserCount(newValue);
|
||||
});
|
||||
|
||||
const handlePrizeDescriptionChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -295,11 +296,6 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
return selectedUserIds?.map((userId) => getUserFullName(usersById[userId])).join(', ');
|
||||
}, [selectedUserIds]);
|
||||
|
||||
const handleAdd = useLastCallback(() => {
|
||||
openEntityPickerModal();
|
||||
setEntityType('channels');
|
||||
});
|
||||
|
||||
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
|
||||
const { scrollTop } = e.currentTarget;
|
||||
|
||||
@ -321,13 +317,18 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleSetCountriesListChange = useLastCallback((value: string[]) => {
|
||||
setSelectedCountriesIds(value);
|
||||
setSelectedCountryIds(value);
|
||||
});
|
||||
|
||||
const handleSetIdsListChange = useLastCallback((value: string[]) => {
|
||||
return entityType === 'members'
|
||||
? (value?.length ? setSelectedUserIds(value) : setGiveawayOption('random_users'))
|
||||
: setSelectedChannelIds(value);
|
||||
const handleSelectedUserIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
setSelectedUserIds(newSelectedIds);
|
||||
if (!newSelectedIds.length) {
|
||||
setGiveawayOption('random_users');
|
||||
}
|
||||
});
|
||||
|
||||
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
setSelectedChannelIds(newSelectedIds);
|
||||
});
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
@ -503,7 +504,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
<RangeSliderWithMarks
|
||||
rangeCount={selectedUserCount}
|
||||
marks={userCountOptions}
|
||||
onChange={handleUserCountChange}
|
||||
onChange={handleRandomUserCountChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -537,7 +538,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
className="chat-item-clickable contact-list-item"
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onClick={() => deleteParticipantsHandler(channelId)}
|
||||
rightElement={(<Icon name="close" />)}
|
||||
rightElement={(<Icon name="close" className={styles.removeChannel} />)}
|
||||
>
|
||||
<GroupChatInfo
|
||||
chatId={channelId.toString()}
|
||||
@ -550,7 +551,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
<ListItem
|
||||
icon="add"
|
||||
ripple
|
||||
onClick={handleAdd}
|
||||
onClick={openChannelPickerModal}
|
||||
className={styles.addButton}
|
||||
iconClassName={styles.addChannel}
|
||||
>
|
||||
@ -705,14 +706,21 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
onSubmit={handleSetCountriesListChange}
|
||||
selectionLimit={countrySelectionLimit}
|
||||
/>
|
||||
<AppendEntityPickerModal
|
||||
key={entityType}
|
||||
isOpen={isEntityPickerModalOpen}
|
||||
onClose={closeEntityPickerModal}
|
||||
entityType={entityType}
|
||||
chatId={chatId}
|
||||
onSubmit={handleSetIdsListChange}
|
||||
selectionLimit={entityType === 'members' ? userSelectionLimit : GIVEAWAY_MAX_ADDITIONAL_CHANNELS}
|
||||
<GiveawayUserPickerModal
|
||||
isOpen={isUserPickerModalOpen}
|
||||
onClose={closeUserPickerModal}
|
||||
onSelectedIdsConfirmed={handleSelectedUserIdsChange}
|
||||
initialSelectedIds={selectedUserIds}
|
||||
selectionLimit={userSelectionLimit}
|
||||
giveawayChatId={chatId}
|
||||
/>
|
||||
<GiveawayChannelPickerModal
|
||||
isOpen={isChannelPickerModalOpen}
|
||||
onClose={closeChannelPickerModal}
|
||||
initialSelectedIds={selectedChannelIds}
|
||||
onSelectedIdsConfirmed={handleSelectedChannelIdsChange}
|
||||
selectionLimit={GIVEAWAY_MAX_ADDITIONAL_CHANNELS}
|
||||
giveawayChatId={chatId}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title={lang('BoostingStartGiveawayConfirmTitle')}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-inline: 3.5rem 1rem;
|
||||
padding-inline: 3.8125rem 1rem;
|
||||
border: none;
|
||||
margin-bottom: 0;
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset-inline-start: 1.0625rem;
|
||||
inset-inline-start: 1rem;
|
||||
top: 50%;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
&::after {
|
||||
inset-inline-start: 1.375rem;
|
||||
inset-inline-start: 1.3125rem;
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 50%;
|
||||
|
||||
143
src/components/main/premium/GiveawayUserPickerModal.tsx
Normal file
143
src/components/main/premium/GiveawayUserPickerModal.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChatMember } from '../../../api/types';
|
||||
|
||||
import {
|
||||
filterUsersByName,
|
||||
isUserBot,
|
||||
sortUserIds,
|
||||
} from '../../../global/helpers';
|
||||
import { selectChatFullInfo } from '../../../global/selectors';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
import sortChatIds from '../../common/helpers/sortChatIds';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import PickerModal from '../../common/pickers/PickerModal';
|
||||
|
||||
type OwnProps = {
|
||||
isOpen?: boolean;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
giveawayChatId?: string;
|
||||
selectionLimit: number;
|
||||
initialSelectedIds: string[];
|
||||
onSelectedIdsConfirmed: (newSelectedIds: string[]) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
members?: ApiChatMember[];
|
||||
adminMembersById?: Record<string, ApiChatMember>;
|
||||
};
|
||||
|
||||
const GiveawayUserPickerModal = ({
|
||||
isOpen,
|
||||
selectionLimit,
|
||||
members,
|
||||
adminMembersById,
|
||||
initialSelectedIds,
|
||||
onSelectedIdsConfirmed,
|
||||
onClose,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { showNotification } = getActions();
|
||||
const lang = useOldLang();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>(initialSelectedIds);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIds(initialSelectedIds);
|
||||
}, [initialSelectedIds]);
|
||||
|
||||
const memberIds = useMemo(() => {
|
||||
const global = getGlobal();
|
||||
const { byId, statusesById } = global.users;
|
||||
if (!members?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const adminIdsSet = adminMembersById && new Set(Object.keys(adminMembersById));
|
||||
|
||||
const userIds = sortUserIds(
|
||||
members.map(({ userId }) => userId),
|
||||
byId,
|
||||
statusesById,
|
||||
);
|
||||
|
||||
return adminIdsSet ? userIds.filter((userId) => !adminIdsSet.has(userId)) : userIds;
|
||||
}, [adminMembersById, members]);
|
||||
|
||||
const displayedMemberIds = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
const filteredContactIds = memberIds ? filterUsersByName(memberIds, usersById, searchQuery) : [];
|
||||
|
||||
return sortChatIds(unique(filteredContactIds).filter((userId) => {
|
||||
const user = usersById[userId];
|
||||
if (!user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !isUserBot(user);
|
||||
}));
|
||||
}, [memberIds, searchQuery]);
|
||||
|
||||
const handleSelectedMemberIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
if (newSelectedIds.length > selectionLimit) {
|
||||
showNotification({
|
||||
message: lang('BoostingSelectUpToWarningUsers', selectionLimit),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedIds(newSelectedIds);
|
||||
});
|
||||
|
||||
const handleModalConfirm = useLastCallback(() => {
|
||||
onSelectedIdsConfirmed(selectedIds);
|
||||
onClose();
|
||||
});
|
||||
|
||||
return (
|
||||
<PickerModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={lang('BoostingAwardSpecificUsers')}
|
||||
hasCloseButton
|
||||
shouldAdaptToSearch
|
||||
withFixedHeight
|
||||
confirmButtonText={lang('Save')}
|
||||
onConfirm={handleModalConfirm}
|
||||
onEnter={handleModalConfirm}
|
||||
>
|
||||
<PeerPicker
|
||||
itemIds={displayedMemberIds}
|
||||
selectedIds={selectedIds}
|
||||
filterValue={searchQuery}
|
||||
filterPlaceholder={lang('Search')}
|
||||
onSelectedIdsChange={handleSelectedMemberIdsChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
isSearchable
|
||||
withDefaultPadding
|
||||
withStatus
|
||||
allowMultiple
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</PickerModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global, { giveawayChatId }): StateProps => {
|
||||
const chatFullInfo = giveawayChatId ? selectChatFullInfo(global, giveawayChatId) : undefined;
|
||||
if (!chatFullInfo) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
members: chatFullInfo.members,
|
||||
adminMembersById: chatFullInfo.adminMembersById,
|
||||
};
|
||||
})(GiveawayUserPickerModal));
|
||||
@ -16,7 +16,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
@ -106,7 +106,7 @@ const PremiumGiftingModal: FC<OwnProps & StateProps> = ({
|
||||
<div className={styles.main}>
|
||||
{renderSearchField()}
|
||||
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||
<Picker
|
||||
<PeerPicker
|
||||
className={styles.picker}
|
||||
itemIds={displayedUserIds}
|
||||
selectedIds={selectedUserIds}
|
||||
@ -116,6 +116,10 @@ const PremiumGiftingModal: FC<OwnProps & StateProps> = ({
|
||||
onSelectedIdsChange={handleSelectedUserIdsChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
isSearchable
|
||||
withDefaultPadding
|
||||
withStatus
|
||||
allowMultiple
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
.giveawayWrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-inline: 3.6875rem 1rem;
|
||||
padding-inline: 3.8125rem 1rem;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset-inline-start: 1.0625rem;
|
||||
inset-inline-start: 1rem;
|
||||
top: 50%;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
@ -71,7 +71,7 @@
|
||||
}
|
||||
|
||||
&::after {
|
||||
inset-inline-start: 1.375rem;
|
||||
inset-inline-start: 1.3125rem;
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 50%;
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
.actionTitle {
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
color: var(--color-links);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
.listItemButton {
|
||||
margin-top: 0.1875rem;
|
||||
margin-left: 0.4375rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@ -8,8 +8,9 @@ import type {
|
||||
} from '../../../api/types';
|
||||
|
||||
import {
|
||||
getChatTitle, getUserFullName, isApiPeerChat, isOwnMessage,
|
||||
getChatTitle, getUserFullName, isOwnMessage,
|
||||
} from '../../../global/helpers';
|
||||
import { isApiPeerChat } from '../../../global/helpers/peers';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectChat,
|
||||
@ -29,7 +30,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
||||
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import Separator from '../../ui/Separator';
|
||||
|
||||
@ -298,7 +298,6 @@ const Poll: FC<OwnProps & StateProps> = ({
|
||||
onChange={handleCheckboxChange}
|
||||
disabled={message.isScheduled || isSubmitting}
|
||||
loadingOptions={isSubmitting ? chosenOptions : undefined}
|
||||
round
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
||||
@ -219,6 +219,8 @@
|
||||
color: var(--color-text-secondary);
|
||||
background-color: var(--color-item-active);
|
||||
font-weight: 500;
|
||||
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.selectedType {
|
||||
|
||||
@ -49,7 +49,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import SearchInput from '../../ui/SearchInput';
|
||||
|
||||
@ -9,7 +9,7 @@ import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import Badge from '../../ui/Badge';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
@ -72,10 +72,13 @@ const ChatlistAlready: FC<OwnProps> = ({ invite, folder }) => {
|
||||
{selectedPeerIds.length === invite.missingPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
<PeerPicker
|
||||
itemIds={invite.missingPeerIds}
|
||||
onSelectedIdsChange={setSelectedPeerIds}
|
||||
selectedIds={selectedPeerIds}
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -84,10 +87,13 @@ const ChatlistAlready: FC<OwnProps> = ({ invite, folder }) => {
|
||||
{lang('FolderLinkHeaderAlready')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
<PeerPicker
|
||||
itemIds={invite.alreadyPeerIds}
|
||||
lockedSelectedIds={invite.alreadyPeerIds}
|
||||
selectedIds={invite.alreadyPeerIds}
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@ -10,7 +10,7 @@ import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import Badge from '../../ui/Badge';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
@ -64,10 +64,13 @@ const ChatlistDelete: FC<OwnProps> = ({
|
||||
{selectedPeerIds.length === suggestedPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
<PeerPicker
|
||||
itemIds={suggestedPeerIds}
|
||||
onSelectedIdsChange={setSelectedPeerIds}
|
||||
selectedIds={selectedPeerIds}
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -11,7 +11,7 @@ import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import Badge from '../../ui/Badge';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
@ -69,11 +69,14 @@ const ChatlistNew: FC<OwnProps> = ({ invite }) => {
|
||||
{selectedPeerIds.length === invite.peerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
<PeerPicker
|
||||
itemIds={invite.peerIds}
|
||||
lockedSelectedIds={joinedIds}
|
||||
onSelectedIdsChange={setSelectedPeerIds}
|
||||
selectedIds={selectedPeerIds}
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@ -21,7 +21,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import buildClassName from '../../../util/buildClassName';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
|
||||
@ -12,13 +12,14 @@ import type { TabState } from '../../../global/types';
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectChat } from '../../../global/selectors';
|
||||
import { partition } from '../../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Separator from '../../ui/Separator';
|
||||
@ -43,7 +44,7 @@ const InviteViaLinkModal: FC<OwnProps & StateProps> = ({
|
||||
const lang = useOldLang();
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||
|
||||
const userIds = useMemo(() => missingUsers?.map((user) => user.id), [missingUsers]);
|
||||
const userIds = useMemo(() => missingUsers?.map((user) => user.id) || MEMO_EMPTY_ARRAY, [missingUsers]);
|
||||
const [unselectableIds, selectableIds] = useMemo(() => {
|
||||
if (!missingUsers?.length) return [[], []];
|
||||
const [requirePremiumIds, regularIds] = partition(missingUsers, (user) => user.isRequiringPremiumToMessage);
|
||||
@ -114,8 +115,6 @@ const InviteViaLinkModal: FC<OwnProps & StateProps> = ({
|
||||
return lang(langKey, params, undefined, topListPeers.length);
|
||||
}, [invitableWithPremiumIds, isEveryPremiumBlocksPm, lang, topListPeers]);
|
||||
|
||||
if (!userIds) return undefined;
|
||||
|
||||
const hasPremiumSection = Boolean(topListPeers?.length);
|
||||
const hasSelectableSection = Boolean(selectableIds?.length);
|
||||
|
||||
@ -170,15 +169,17 @@ const InviteViaLinkModal: FC<OwnProps & StateProps> = ({
|
||||
<p className={styles.contentText}>
|
||||
{inviteSectionText}
|
||||
</p>
|
||||
<Picker
|
||||
<PeerPicker
|
||||
className={styles.userPicker}
|
||||
itemIds={userIds!}
|
||||
itemIds={userIds}
|
||||
selectedIds={selectedMemberIds}
|
||||
lockedUnselectedIds={unselectableIds}
|
||||
lockedUnselectedSubtitle={lang('InvitePremiumBlockedUser')}
|
||||
onSelectedIdsChange={setSelectedMemberIds}
|
||||
isViewOnly={!canSendInviteLink}
|
||||
isRoundCheckbox
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
{canSendInviteLink && (
|
||||
<Button
|
||||
|
||||
@ -20,7 +20,7 @@ import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
|
||||
import Picker from '../common/Picker';
|
||||
import PeerPicker from '../common/pickers/PeerPicker';
|
||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||
import Spinner from '../ui/Spinner';
|
||||
|
||||
@ -116,7 +116,7 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<div className="AddChatMembers">
|
||||
<div className="AddChatMembers-inner">
|
||||
<Picker
|
||||
<PeerPicker
|
||||
itemIds={displayedIds}
|
||||
selectedIds={selectedMemberIds}
|
||||
filterValue={searchQuery}
|
||||
@ -126,7 +126,11 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
|
||||
onSelectedIdsChange={setSelectedMemberIds}
|
||||
onFilterChange={handleFilterChange}
|
||||
isSearchable
|
||||
withDefaultPadding
|
||||
noScrollRestore={noPickerScrollRestore}
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
|
||||
<FloatingActionButton
|
||||
|
||||
@ -133,10 +133,6 @@
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0.5rem 0;
|
||||
|
||||
.ListItem.chat-item-clickable {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -241,6 +241,7 @@ const ManageReactions: FC<OwnProps & StateProps> = ({
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
withIcon
|
||||
onChange={handleReactionChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 1.5rem;
|
||||
padding: 1rem 0.5rem;
|
||||
|
||||
@include mixins.side-panel-section;
|
||||
|
||||
@ -45,8 +45,6 @@
|
||||
}
|
||||
|
||||
.ListItem {
|
||||
margin: 0 -0.75rem;
|
||||
|
||||
.Reaction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -54,11 +52,7 @@
|
||||
|
||||
.ReactionStaticEmoji {
|
||||
width: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
margin-right: 1.6875rem;
|
||||
}
|
||||
|
||||
.multiline-item .subtitle {
|
||||
@ -75,11 +69,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.picker-list-item) .Checkbox {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&.narrow {
|
||||
.Checkbox {
|
||||
margin-top: 1rem;
|
||||
@ -198,10 +187,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox {
|
||||
padding-left: 3.875rem;
|
||||
}
|
||||
|
||||
.Spinner {
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { selectChatFullInfo } from '../../../global/selectors';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import ChatOrUserPicker from '../../common/ChatOrUserPicker';
|
||||
import ChatOrUserPicker from '../../common/pickers/ChatOrUserPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
chat: ApiChat;
|
||||
|
||||
@ -8,7 +8,7 @@ import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
|
||||
interface OwnProps {
|
||||
id: string;
|
||||
@ -38,7 +38,7 @@ function AllowDenyList({
|
||||
}, [contactListIds, currentUserId, searchQuery, selectedIds, usersById]);
|
||||
|
||||
return (
|
||||
<Picker
|
||||
<PeerPicker
|
||||
key={id}
|
||||
itemIds={displayedIds}
|
||||
selectedIds={selectedIds ?? MEMO_EMPTY_ARRAY}
|
||||
@ -47,9 +47,13 @@ function AllowDenyList({
|
||||
filterPlaceholder={lang('Search')}
|
||||
searchInputId={`${id}-picker-search`}
|
||||
isSearchable
|
||||
withDefaultPadding
|
||||
forceShowSelf
|
||||
onSelectedIdsChange={onSelect}
|
||||
onFilterChange={setSearchQuery}
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Picker from '../../common/Picker';
|
||||
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
|
||||
import styles from './CloseFriends.module.scss';
|
||||
@ -64,15 +64,19 @@ function CloseFriends({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Picker
|
||||
<PeerPicker
|
||||
itemIds={displayedIds || []}
|
||||
selectedIds={newSelectedContactIds}
|
||||
filterValue={searchQuery}
|
||||
filterPlaceholder={lang('Search')}
|
||||
searchInputId="close-friends-picker-search"
|
||||
isSearchable
|
||||
withDefaultPadding
|
||||
onSelectedIdsChange={handleSelectedContactIdsChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
allowMultiple
|
||||
withStatus
|
||||
itemInputType="checkbox"
|
||||
/>
|
||||
|
||||
<div className={buildClassName(styles.buttonHolder, isSubmitShown && styles.active)}>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.Checkbox {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 4.5rem;
|
||||
padding-inline-start: 3.5rem;
|
||||
text-align: left;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
@ -12,19 +12,13 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.round {
|
||||
&.withIcon {
|
||||
padding-inline-start: 1rem;
|
||||
.Checkbox-main {
|
||||
&::before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
background:
|
||||
var(--color-primary)
|
||||
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLjkuOEw1LjggOC45IDIuMSA1LjJjLS40LS40LTEuMS0uNC0xLjYgMC0uNC40LS40IDEuMSAwIDEuNkw1IDExLjJjLjQuNCAxLjEuNCAxLjYgMGw4LjktOC45Yy40LS40LjQtMS4xIDAtMS42LS41LS40LTEuMi0uNC0xLjYuMXoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=)
|
||||
no-repeat 50% 50%;
|
||||
background-size: 0.75rem;
|
||||
border-radius: 50%;
|
||||
left: auto;
|
||||
right: 1.1875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,7 +69,7 @@
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0.6875rem;
|
||||
left: 1rem;
|
||||
top: 0.1875rem;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
@ -120,6 +114,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.withSubLabel {
|
||||
.Checkbox-main {
|
||||
&::before,
|
||||
&::after {
|
||||
top: 0.875rem;
|
||||
left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Nested-avatar-list {
|
||||
&::before,
|
||||
&::after {
|
||||
@ -150,9 +154,6 @@
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
padding-left: 0;
|
||||
padding-right: 4.5rem;
|
||||
|
||||
&.loading {
|
||||
.Spinner {
|
||||
left: auto;
|
||||
@ -169,7 +170,7 @@
|
||||
&::before,
|
||||
&::after {
|
||||
left: auto;
|
||||
right: 1.1875rem;
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,28 +181,28 @@
|
||||
align-items: center;
|
||||
gap: 0.3125rem;
|
||||
margin-bottom: 0.5625rem;
|
||||
padding-left: 4.1875rem;
|
||||
padding-inline-start: 4.1875rem;
|
||||
.Checkbox-main {
|
||||
&::before,
|
||||
&::after {
|
||||
top: 0.875rem;
|
||||
left: 0.75rem;
|
||||
left: 1.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.permission-group {
|
||||
padding-left: 4rem;
|
||||
padding-inline-start: 3.625rem;
|
||||
.Checkbox-main {
|
||||
&::before,
|
||||
&::after {
|
||||
left: 0.75rem;
|
||||
left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.avatar {
|
||||
padding-left: 6.625rem;
|
||||
padding-inline-start: 6.625rem;
|
||||
margin-bottom: 1.125rem;
|
||||
}
|
||||
|
||||
@ -220,6 +221,18 @@
|
||||
top: 1.875rem;
|
||||
left: 2.875rem;
|
||||
}
|
||||
|
||||
&.onlyInput {
|
||||
padding-inline-start: 1.125rem;
|
||||
|
||||
.Checkbox-main {
|
||||
&::before,
|
||||
&::after {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nested-checkbox-group {
|
||||
|
||||
@ -25,18 +25,19 @@ type OwnProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
label: TeactNode;
|
||||
label?: TeactNode;
|
||||
labelText?: TeactNode;
|
||||
subLabel?: string;
|
||||
checked?: boolean;
|
||||
rightIcon?: IconName;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
round?: boolean;
|
||||
withIcon?: boolean;
|
||||
blocking?: boolean;
|
||||
permissionGroup?: boolean;
|
||||
isLoading?: boolean;
|
||||
withCheckedCallback?: boolean;
|
||||
onlyInput?: boolean;
|
||||
className?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>, nestedOptionList?: IRadioOption) => void;
|
||||
onCheck?: (isChecked: boolean) => void;
|
||||
@ -58,12 +59,13 @@ const Checkbox: FC<OwnProps> = ({
|
||||
checked,
|
||||
tabIndex,
|
||||
disabled,
|
||||
round,
|
||||
withIcon,
|
||||
blocking,
|
||||
permissionGroup,
|
||||
isLoading,
|
||||
className,
|
||||
rightIcon,
|
||||
onlyInput,
|
||||
onChange,
|
||||
onCheck,
|
||||
onClickLabel,
|
||||
@ -109,12 +111,14 @@ const Checkbox: FC<OwnProps> = ({
|
||||
const labelClassName = buildClassName(
|
||||
'Checkbox',
|
||||
disabled && 'disabled',
|
||||
round && 'round',
|
||||
withIcon && 'withIcon',
|
||||
isLoading && 'loading',
|
||||
blocking && 'blocking',
|
||||
nestedCheckbox && 'nested',
|
||||
subLabel && 'withSubLabel',
|
||||
permissionGroup && 'permission-group',
|
||||
Boolean(leftElement) && 'avatar',
|
||||
onlyInput && 'onlyInput',
|
||||
className,
|
||||
);
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ type OwnProps = {
|
||||
options: IRadioOption[];
|
||||
selected?: string[];
|
||||
disabled?: boolean;
|
||||
round?: boolean;
|
||||
nestedCheckbox?: boolean;
|
||||
loadingOptions?: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
@ -33,7 +32,6 @@ const CheckboxGroup: FC<OwnProps> = ({
|
||||
options,
|
||||
selected = [],
|
||||
disabled,
|
||||
round,
|
||||
nestedCheckbox,
|
||||
loadingOptions,
|
||||
onChange,
|
||||
@ -89,7 +87,6 @@ const CheckboxGroup: FC<OwnProps> = ({
|
||||
value={option.value}
|
||||
checked={selected.indexOf(option.value) !== -1}
|
||||
disabled={option.disabled || disabled}
|
||||
round={round}
|
||||
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
||||
onChange={handleChange}
|
||||
nestedCheckbox={nestedCheckbox}
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
outline: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
padding: 1rem 0.8125rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@ -82,7 +82,6 @@
|
||||
|
||||
> .ListItem-main-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-inline-start: 0.125rem;
|
||||
margin-inline-end: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
@ -180,6 +179,10 @@
|
||||
.ListItem-button {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.ListItem-main-icon {
|
||||
margin-inline-end: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
@ -399,6 +402,10 @@
|
||||
padding-left: 4rem;
|
||||
}
|
||||
|
||||
.withSubLabel {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
.Checkbox {
|
||||
padding-left: 0;
|
||||
|
||||
86
src/components/ui/ListItemWithOptions.tsx
Normal file
86
src/components/ui/ListItemWithOptions.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { CustomPeer } from '../../types';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import Checkbox from './Checkbox';
|
||||
import ListItem from './ListItem';
|
||||
import Radio from './Radio';
|
||||
|
||||
type OwnProps = {
|
||||
key: string;
|
||||
isChecked?: boolean;
|
||||
disabled?: boolean;
|
||||
inactive?: boolean;
|
||||
isChatItem?: boolean;
|
||||
ripple?: boolean;
|
||||
shouldRenderLockIcon?: boolean;
|
||||
category?: CustomPeer;
|
||||
handleItemClick: (id: string) => void;
|
||||
renderCategory?: (category: CustomPeer) => TeactNode;
|
||||
renderChatInfo?: (id: string) => TeactNode;
|
||||
allowDisabledClick?: boolean;
|
||||
label?: TeactNode;
|
||||
subLabel?: string;
|
||||
type?: 'checkbox' | 'radio';
|
||||
};
|
||||
|
||||
const ListItemWithOptions: FC<OwnProps> = ({
|
||||
key,
|
||||
isChecked,
|
||||
disabled,
|
||||
inactive,
|
||||
isChatItem,
|
||||
shouldRenderLockIcon,
|
||||
category,
|
||||
handleItemClick,
|
||||
ripple,
|
||||
renderCategory,
|
||||
renderChatInfo,
|
||||
allowDisabledClick,
|
||||
label,
|
||||
subLabel,
|
||||
type,
|
||||
}) => {
|
||||
function renderInput() {
|
||||
if (inactive || disabled) {
|
||||
return undefined;
|
||||
}
|
||||
return type === 'checkbox' ? (
|
||||
<Checkbox
|
||||
label={label || ''}
|
||||
subLabel={subLabel}
|
||||
disabled={disabled}
|
||||
checked={isChecked}
|
||||
/>
|
||||
) : (
|
||||
<Radio
|
||||
label=""
|
||||
disabled={disabled}
|
||||
checked={isChecked}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
className={buildClassName('chat-item-clickable picker-list-item', isChatItem && 'chat-item')}
|
||||
disabled={disabled}
|
||||
inactive={inactive}
|
||||
allowDisabledClick={allowDisabledClick}
|
||||
secondaryIcon={shouldRenderLockIcon ? 'lock-badge' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleItemClick(key)}
|
||||
ripple={ripple}
|
||||
>
|
||||
{!isChatItem ? renderInput() : undefined}
|
||||
{category ? renderCategory?.(category) : renderChatInfo?.(key)}
|
||||
{isChatItem ? renderInput() : undefined}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ListItemWithOptions);
|
||||
@ -24,10 +24,11 @@ import './Modal.scss';
|
||||
|
||||
export const ANIMATION_DURATION = 200;
|
||||
|
||||
type OwnProps = {
|
||||
export type OwnProps = {
|
||||
title?: string | TextPart[];
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
headerClassName?: string;
|
||||
isOpen?: boolean;
|
||||
header?: TeactNode;
|
||||
isSlim?: boolean;
|
||||
@ -37,10 +38,10 @@ type OwnProps = {
|
||||
noBackdropClose?: boolean;
|
||||
children: React.ReactNode;
|
||||
style?: string;
|
||||
dialogRef?: React.RefObject<HTMLDivElement>;
|
||||
onClose: () => void;
|
||||
onCloseAnimationEnd?: () => void;
|
||||
onEnter?: () => void;
|
||||
dialogRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -52,6 +53,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
title,
|
||||
className,
|
||||
contentClassName,
|
||||
headerClassName,
|
||||
isOpen,
|
||||
isSlim,
|
||||
header,
|
||||
@ -61,10 +63,10 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
noBackdropClose,
|
||||
children,
|
||||
style,
|
||||
shouldSkipHistoryAnimations,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
onEnter,
|
||||
shouldSkipHistoryAnimations,
|
||||
}) => {
|
||||
const {
|
||||
shouldRender,
|
||||
@ -132,7 +134,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
if (!title && !withCloseButton) return undefined;
|
||||
|
||||
return (
|
||||
<div className="modal-header">
|
||||
<div className={buildClassName('modal-header', headerClassName)}>
|
||||
{withCloseButton && (
|
||||
<Button
|
||||
className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.Radio {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 4.5rem;
|
||||
padding-inline-start: 3.5rem;
|
||||
text-align: left;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
@ -12,6 +12,21 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
padding-inline-start: 1rem;
|
||||
.Radio-main {
|
||||
&::before,
|
||||
&::after {
|
||||
left: auto;
|
||||
right: 1.1875rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden-widget {
|
||||
cursor: var(--custom-cursor, default);
|
||||
.Radio-main {
|
||||
@ -34,7 +49,7 @@
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 1.0625rem;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
@ -50,7 +65,7 @@
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 1.375rem;
|
||||
left: 1.3125rem;
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 50%;
|
||||
@ -97,7 +112,7 @@
|
||||
|
||||
.Spinner {
|
||||
position: absolute;
|
||||
left: 1.0625rem;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
@ -107,9 +122,6 @@
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
padding-left: 0;
|
||||
padding-right: 4.5rem;
|
||||
|
||||
.Radio-main {
|
||||
text-align: right;
|
||||
|
||||
@ -133,6 +145,37 @@
|
||||
left: auto;
|
||||
right: 1.0625rem;
|
||||
}
|
||||
|
||||
&.onlyInput .Radio-main::after {
|
||||
right: 0.3125rem;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.onlyInput {
|
||||
margin-bottom: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
padding-inline-start: 1.25rem;
|
||||
|
||||
.Radio-main {
|
||||
&::before {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: none;
|
||||
}
|
||||
&::after {
|
||||
left: 0.3125rem;
|
||||
top: 0;
|
||||
transform: translateY(50%);
|
||||
}
|
||||
}
|
||||
|
||||
.Spinner {
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,18 +12,20 @@ import './Radio.scss';
|
||||
|
||||
type OwnProps = {
|
||||
id?: string;
|
||||
name: string;
|
||||
label: TeactNode;
|
||||
name?: string;
|
||||
label?: TeactNode;
|
||||
subLabel?: TeactNode;
|
||||
value: string;
|
||||
checked: boolean;
|
||||
value?: string;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
isLink?: boolean;
|
||||
hidden?: boolean;
|
||||
isLoading?: boolean;
|
||||
withIcon?: boolean;
|
||||
className?: string;
|
||||
onlyInput?: boolean;
|
||||
subLabelClassName?: string;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onSubLabelClick?: MouseEventHandler<HTMLSpanElement> | undefined;
|
||||
};
|
||||
|
||||
@ -39,8 +41,10 @@ const Radio: FC<OwnProps> = ({
|
||||
hidden,
|
||||
isLoading,
|
||||
className,
|
||||
onChange,
|
||||
onlyInput,
|
||||
withIcon,
|
||||
isLink,
|
||||
onChange,
|
||||
onSubLabelClick,
|
||||
}) => {
|
||||
const lang = useOldLang();
|
||||
@ -49,7 +53,9 @@ const Radio: FC<OwnProps> = ({
|
||||
className,
|
||||
disabled && 'disabled',
|
||||
hidden && 'hidden-widget',
|
||||
withIcon && 'with-icon',
|
||||
isLoading && 'loading',
|
||||
onlyInput && 'onlyInput',
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -24,6 +24,7 @@ type OwnProps = {
|
||||
onChange: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onClickAction?: (value: string) => void;
|
||||
isLink?: boolean;
|
||||
withIcon?: boolean;
|
||||
subLabelClassName?: string;
|
||||
subLabel?: TeactNode;
|
||||
};
|
||||
@ -39,6 +40,7 @@ const RadioGroup: FC<OwnProps> = ({
|
||||
onClickAction,
|
||||
subLabelClassName,
|
||||
isLink,
|
||||
withIcon,
|
||||
subLabel,
|
||||
}) => {
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -62,6 +64,7 @@ const RadioGroup: FC<OwnProps> = ({
|
||||
checked={option.value === selected}
|
||||
hidden={option.hidden}
|
||||
disabled={disabled}
|
||||
withIcon={withIcon}
|
||||
isLoading={loadingOption ? loadingOption === option.value : undefined}
|
||||
className={option.className}
|
||||
onChange={handleChange}
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
|
||||
.mark {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@ -1123,12 +1123,12 @@ export function deleteMessages<T extends GlobalState>(
|
||||
|
||||
// Common box update
|
||||
|
||||
const chatsIdsToUpdate: string[] = [];
|
||||
const chatIdsToUpdate: string[] = [];
|
||||
|
||||
ids.forEach((id) => {
|
||||
const commonBoxChatId = selectCommonBoxChatId(global, id);
|
||||
if (commonBoxChatId) {
|
||||
chatsIdsToUpdate.push(commonBoxChatId);
|
||||
chatIdsToUpdate.push(commonBoxChatId);
|
||||
|
||||
global = updateChatMessage(global, commonBoxChatId, id, {
|
||||
isDeleting: true,
|
||||
@ -1165,7 +1165,7 @@ export function deleteMessages<T extends GlobalState>(
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
unique(chatsIdsToUpdate).forEach((id) => {
|
||||
unique(chatIdsToUpdate).forEach((id) => {
|
||||
actions.requestChatUpdate({ chatId: id });
|
||||
});
|
||||
}
|
||||
|
||||
@ -43,10 +43,6 @@ export function toChannelId(mtpId: string) {
|
||||
return `-1${mtpId.padStart(CHANNEL_ID_LENGTH - 2, '0')}`;
|
||||
}
|
||||
|
||||
export function isApiPeerChat(peer: ApiPeer): peer is ApiChat {
|
||||
return 'title' in peer;
|
||||
}
|
||||
|
||||
export function isChatGroup(chat: ApiChat) {
|
||||
return isChatBasicGroup(chat) || isChatSuperGroup(chat);
|
||||
}
|
||||
@ -466,3 +462,20 @@ export function getPeerColorCount(peer: ApiPeer) {
|
||||
export function getIsSavedDialog(chatId: string, threadId: ThreadId | undefined, currentUserId: string | undefined) {
|
||||
return chatId === currentUserId && threadId !== MAIN_THREAD_ID;
|
||||
}
|
||||
|
||||
export function getGroupStatus(lang: LangFn, chat: ApiChat) {
|
||||
const chatTypeString = lang(getChatTypeString(chat));
|
||||
const { membersCount } = chat;
|
||||
|
||||
if (chat.isRestricted) {
|
||||
return chatTypeString === 'Channel' ? 'channel is inaccessible' : 'group is inaccessible';
|
||||
}
|
||||
|
||||
if (!membersCount) {
|
||||
return chatTypeString;
|
||||
}
|
||||
|
||||
return chatTypeString === 'Channel'
|
||||
? lang('Subscribers', membersCount, 'i')
|
||||
: lang('Members', membersCount, 'i');
|
||||
}
|
||||
|
||||
47
src/global/helpers/peers.ts
Normal file
47
src/global/helpers/peers.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { ApiChat, ApiPeer, ApiUser } from '../../api/types';
|
||||
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
||||
|
||||
export function isApiPeerChat(peer: ApiPeer): peer is ApiChat {
|
||||
return 'title' in peer;
|
||||
}
|
||||
|
||||
export function isApiPeerUser(peer: ApiPeer): peer is ApiUser {
|
||||
return !isApiPeerChat(peer);
|
||||
}
|
||||
|
||||
export function getPeerTypeKey(peer: ApiPeer) {
|
||||
if (isApiPeerChat(peer)) {
|
||||
if (peer.type === 'chatTypeBasicGroup' || peer.type === 'chatTypeSuperGroup') {
|
||||
return 'ChatList.PeerTypeGroup';
|
||||
}
|
||||
|
||||
if (peer.type === 'chatTypeChannel') {
|
||||
return 'ChatList.PeerTypeChannel';
|
||||
}
|
||||
|
||||
if (peer.type === 'chatTypePrivate') {
|
||||
return 'ChatList.PeerTypeNonContact';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (peer.id === SERVICE_NOTIFICATIONS_USER_ID) {
|
||||
return 'ServiceNotifications';
|
||||
}
|
||||
|
||||
if (peer.isSupport) {
|
||||
return 'SupportStatus';
|
||||
}
|
||||
|
||||
if (peer.type && peer.type === 'userTypeBot') {
|
||||
return 'ChatList.PeerTypeBot';
|
||||
}
|
||||
|
||||
if (peer.isContact) {
|
||||
return 'ChatList.PeerTypeContact';
|
||||
}
|
||||
|
||||
return 'ChatList.PeerTypeNonContactUser';
|
||||
}
|
||||
@ -45,6 +45,7 @@ $color-text-meta-apple: #8c8c91;
|
||||
$color-borders: #dadce0;
|
||||
$color-dividers: #c8c6cc;
|
||||
$color-dividers-android: #E7E7E7;
|
||||
$color-item-hover: #f4f4f5;
|
||||
$color-item-active: #ededed;
|
||||
$color-chat-hover: #f4f4f5;
|
||||
$color-chat-active: #3390ec;
|
||||
@ -164,6 +165,7 @@ $color-message-story-mention-to: #74bcff;
|
||||
--color-chat-username: #3C7EB0;
|
||||
--color-chat-hover: #{$color-chat-hover};
|
||||
--color-chat-active: #{$color-chat-active};
|
||||
--color-item-hover: #{$color-item-hover};
|
||||
--color-item-active: #{$color-item-active};
|
||||
|
||||
--color-selection-highlight: #{$color-selection};
|
||||
@ -299,6 +301,8 @@ $color-message-story-mention-to: #74bcff;
|
||||
--safe-area-bottom: env(safe-area-inset-bottom);
|
||||
--safe-area-left: env(safe-area-inset-left);
|
||||
|
||||
--picker-title-shift: 1rem;
|
||||
|
||||
body.is-ios {
|
||||
--layer-transition: 650ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--layer-transition-behind: 650ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||
|
||||
@ -347,6 +347,7 @@ body:not(.is-ios) {
|
||||
--color-chat-hover: rgb(44, 44, 44);
|
||||
--color-chat-active: rgb(118, 106, 200);
|
||||
--color-chat-active-greyed: rgb(146, 136, 211);
|
||||
--color-item-hover: rgb(44, 44, 44);
|
||||
--color-item-active: rgb(41, 41, 41);
|
||||
--color-text: rgb(255, 255, 255);
|
||||
--color-text-rgb: 255, 255, 255;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"--color-chat-hover": ["#F4F4F5", "#2C2C2C"],
|
||||
"--color-chat-active": ["#3390EC", "#766AC8"],
|
||||
"--color-chat-active-greyed": ["#60a7f0", "#9288d3"],
|
||||
"--color-item-hover": ["#f4f4f5", "#2c2c2c"],
|
||||
"--color-item-active": ["#ededed", "#292929"],
|
||||
"--color-text": ["#000000", "#FFFFFF"],
|
||||
"--color-text-secondary": ["#707579", "#AAAAAA"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user