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 GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
||||||
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
|
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
|
||||||
export { default as PremiumGiftingModal } from '../components/main/premium/PremiumGiftingModal';
|
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 PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||||
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||||
export { default as BoostModal } from '../components/modals/boost/BoostModal';
|
export { default as BoostModal } from '../components/modals/boost/BoostModal';
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import usePrevious from '../../hooks/usePrevious';
|
|||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
import Icon from './icons/Icon';
|
import Icon from './icons/Icon';
|
||||||
import Picker from './Picker';
|
import ItemPicker from './pickers/ItemPicker';
|
||||||
|
|
||||||
import styles from './CountryPickerModal.module.scss';
|
import styles from './CountryPickerModal.module.scss';
|
||||||
|
|
||||||
@ -47,9 +47,13 @@ const CountryPickerModal: FC<OwnProps> = ({
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return countryList
|
return countryList.filter((country) => !country.isHidden && country.iso2 !== 'FT')
|
||||||
.filter((country) => !country.isHidden)
|
.map(({
|
||||||
.map((country) => country.iso2);
|
iso2, defaultName,
|
||||||
|
}) => ({
|
||||||
|
value: iso2,
|
||||||
|
label: defaultName,
|
||||||
|
}));
|
||||||
}, [countryList]);
|
}, [countryList]);
|
||||||
|
|
||||||
const handleSelectedIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
const handleSelectedIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||||
@ -92,14 +96,14 @@ const CountryPickerModal: FC<OwnProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||||
<Picker
|
<ItemPicker
|
||||||
className={styles.picker}
|
className={styles.picker}
|
||||||
itemIds={displayedIds}
|
items={displayedIds}
|
||||||
selectedIds={selectedCountryIds}
|
selectedValues={selectedCountryIds}
|
||||||
onSelectedIdsChange={handleSelectedIdsChange}
|
onSelectedValuesChange={handleSelectedIdsChange}
|
||||||
noScrollRestore={noPickerScrollRestore}
|
noScrollRestore={noPickerScrollRestore}
|
||||||
isCountryList
|
allowMultiple
|
||||||
countryList={countryList}
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
.actionTitle {
|
.actionTitle {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
margin-left: 0.5rem;
|
margin-left: 1rem;
|
||||||
color: var(--color-links);
|
color: var(--color-links);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
.listItemButton {
|
.listItemButton {
|
||||||
margin-top: 0.1875rem;
|
margin-top: 0.1875rem;
|
||||||
margin-left: 0.4375rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@ -52,16 +52,12 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
/* 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,
|
.restrictionContainerOpen,
|
||||||
.dropdownListOpen {
|
.dropdownListOpen {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
transition: max-height 0.3s ease-in;
|
transition: max-height 0.3s ease-in-out;
|
||||||
}
|
|
||||||
|
|
||||||
:global(.Checkbox) {
|
|
||||||
padding-left: 3.875rem;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,22 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
.fullName {
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
&.canCopy {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.custom-emoji) {
|
:global(.custom-emoji) {
|
||||||
color: var(--color-primary);
|
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;
|
isSavedDialog?: boolean;
|
||||||
noLoopLimit?: boolean;
|
noLoopLimit?: boolean;
|
||||||
canCopyTitle?: boolean;
|
canCopyTitle?: boolean;
|
||||||
|
iconElement?: React.ReactNode;
|
||||||
|
allowMultiLine?: boolean;
|
||||||
onEmojiStatusClick?: NoneToVoidFunction;
|
onEmojiStatusClick?: NoneToVoidFunction;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
iconElement?: React.ReactNode;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FullNameTitle: FC<OwnProps> = ({
|
const FullNameTitle: FC<OwnProps> = ({
|
||||||
@ -54,9 +55,10 @@ const FullNameTitle: FC<OwnProps> = ({
|
|||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
noLoopLimit,
|
noLoopLimit,
|
||||||
canCopyTitle,
|
canCopyTitle,
|
||||||
|
iconElement,
|
||||||
|
allowMultiLine,
|
||||||
onEmojiStatusClick,
|
onEmojiStatusClick,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
iconElement,
|
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
const { showNotification } = getActions();
|
const { showNotification } = getActions();
|
||||||
@ -99,7 +101,9 @@ const FullNameTitle: FC<OwnProps> = ({
|
|||||||
if (specialTitle) {
|
if (specialTitle) {
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('title', styles.root, className)}>
|
<div className={buildClassName('title', styles.root, className)}>
|
||||||
<h3>{specialTitle}</h3>
|
<h3 className={buildClassName('fullName', styles.fullName, !allowMultiLine && styles.ellipsis)}>
|
||||||
|
{specialTitle}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,7 +113,12 @@ const FullNameTitle: FC<OwnProps> = ({
|
|||||||
<h3
|
<h3
|
||||||
dir="auto"
|
dir="auto"
|
||||||
role="button"
|
role="button"
|
||||||
className={buildClassName('fullName', styles.fullName, canCopyTitle && styles.canCopy)}
|
className={buildClassName(
|
||||||
|
'fullName',
|
||||||
|
styles.fullName,
|
||||||
|
!allowMultiLine && styles.ellipsis,
|
||||||
|
canCopyTitle && styles.canCopy,
|
||||||
|
)}
|
||||||
onClick={handleTitleClick}
|
onClick={handleTitleClick}
|
||||||
>
|
>
|
||||||
{renderText(title || '')}
|
{renderText(title || '')}
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import { getActions, withGlobal } from '../../global';
|
|||||||
import type {
|
import type {
|
||||||
ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser,
|
ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { LangFn } from '../../hooks/useOldLang';
|
|
||||||
import type { IconName } from '../../types/icons';
|
import type { IconName } from '../../types/icons';
|
||||||
import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types';
|
import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getChatTypeString,
|
getChatTypeString,
|
||||||
|
getGroupStatus,
|
||||||
getMainUsername,
|
getMainUsername,
|
||||||
isChatSuperGroup,
|
isChatSuperGroup,
|
||||||
} from '../../global/helpers';
|
} 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>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId, threadId }): StateProps => {
|
(global, { chatId, threadId }): StateProps => {
|
||||||
const chat = selectChat(global, chatId);
|
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 useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
import ChatOrUserPicker from './ChatOrUserPicker';
|
import ChatOrUserPicker from './pickers/ChatOrUserPicker';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
.StickerSetCard {
|
.StickerSetCard {
|
||||||
.settings-item &.ListItem {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.StickerButton,
|
.StickerButton,
|
||||||
.Button {
|
.Button {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
|
|||||||
@ -1,34 +1,34 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useMemo, useRef, useState,
|
memo, useMemo, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, getGlobal } from '../../global';
|
import { getActions, getGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiTopic } from '../../api/types';
|
import type { ApiTopic } from '../../../api/types';
|
||||||
import type { ThreadId } from '../../types';
|
import type { ThreadId } from '../../../types';
|
||||||
|
|
||||||
import { CHAT_HEIGHT_PX } from '../../config';
|
import { CHAT_HEIGHT_PX } from '../../../config';
|
||||||
import { getCanPostInChat, isUserId } from '../../global/helpers';
|
import { getCanPostInChat, isUserId } from '../../../global/helpers';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { REM } from './helpers/mediaDimensions';
|
import { REM } from '../helpers/mediaDimensions';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from '../helpers/renderText';
|
||||||
|
|
||||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
import useInputFocusOnOpen from '../../../hooks/useInputFocusOnOpen';
|
||||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import InputText from '../ui/InputText';
|
import InputText from '../../ui/InputText';
|
||||||
import ListItem from '../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
import Transition from '../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
import GroupChatInfo from './GroupChatInfo';
|
import GroupChatInfo from '../GroupChatInfo';
|
||||||
import PrivateChatInfo from './PrivateChatInfo';
|
import PrivateChatInfo from '../PrivateChatInfo';
|
||||||
import TopicIcon from './TopicIcon';
|
import TopicIcon from '../TopicIcon';
|
||||||
|
|
||||||
import './ChatOrUserPicker.scss';
|
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);
|
background: var(--color-chat-hover);
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
min-width: 2rem;
|
min-width: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
@ -1,21 +1,21 @@
|
|||||||
import type { TeactNode } from '../../lib/teact/teact';
|
import type { TeactNode } from '../../../lib/teact/teact';
|
||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiChat, ApiUser } from '../../api/types';
|
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||||
import type { CustomPeer } from '../../types';
|
import type { CustomPeer } from '../../../types';
|
||||||
import type { IconName } from '../../types/icons';
|
import type { IconName } from '../../../types/icons';
|
||||||
|
|
||||||
import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers';
|
import { getChatTitle, getUserFirstOrLastName } from '../../../global/helpers';
|
||||||
import { selectChat, selectUser } from '../../global/selectors';
|
import { selectChat, selectUser } from '../../../global/selectors';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { getPeerColorClass } from './helpers/peerColor';
|
import { getPeerColorClass } from '../helpers/peerColor';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from '../helpers/renderText';
|
||||||
|
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Avatar from './Avatar';
|
import Avatar from '../Avatar';
|
||||||
import Icon from './icons/Icon';
|
import Icon from '../icons/Icon';
|
||||||
|
|
||||||
import './PickerSelectedItem.scss';
|
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}
|
options={folders}
|
||||||
selected={selectedFolderIds}
|
selected={selectedFolderIds}
|
||||||
onChange={setSelectedFolderIds}
|
onChange={setSelectedFolderIds}
|
||||||
round
|
|
||||||
/>
|
/>
|
||||||
<div className="dialog-buttons">
|
<div className="dialog-buttons">
|
||||||
<Button color="primary" className="confirm-dialog-button" isText onClick={handleSubmit}>
|
<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 { useFullscreenStatus } from '../../../hooks/window/useFullscreen';
|
||||||
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
|
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
|
||||||
|
|
||||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||||
import StoryToggler from '../../story/StoryToggler';
|
import StoryToggler from '../../story/StoryToggler';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import DropdownMenu from '../../ui/DropdownMenu';
|
import DropdownMenu from '../../ui/DropdownMenu';
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import sortChatIds from '../../common/helpers/sortChatIds';
|
|||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
|
|||||||
<h3>{lang('GroupAddMembers')}</h3>
|
<h3>{lang('GroupAddMembers')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="NewChat-inner step-1">
|
<div className="NewChat-inner step-1">
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={displayedIds}
|
itemIds={displayedIds}
|
||||||
selectedIds={selectedMemberIds}
|
selectedIds={selectedMemberIds}
|
||||||
filterValue={searchQuery}
|
filterValue={searchQuery}
|
||||||
@ -107,6 +107,10 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
|
|||||||
searchInputId="new-group-picker-search"
|
searchInputId="new-group-picker-search"
|
||||||
isLoading={isSearching}
|
isLoading={isSearching}
|
||||||
isSearchable
|
isSearchable
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
|
withDefaultPadding
|
||||||
onSelectedIdsChange={onSelectedMemberIdsChange}
|
onSelectedIdsChange={onSelectedMemberIdsChange}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
|||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import NothingFound from '../../common/NothingFound';
|
import NothingFound from '../../common/NothingFound';
|
||||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import Link from '../../ui/Link';
|
import Link from '../../ui/Link';
|
||||||
import ChatMessage from './ChatMessage';
|
import ChatMessage from './ChatMessage';
|
||||||
|
|||||||
@ -184,7 +184,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-section {
|
.search-section {
|
||||||
padding: 0 0.125rem 0.5rem 0.5rem;
|
padding: 0 0.5rem 0.5rem 0.5rem;
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { unique } from '../../../util/iteratees';
|
|||||||
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import ChatOrUserPicker from '../../common/ChatOrUserPicker';
|
import ChatOrUserPicker from '../../common/pickers/ChatOrUserPicker';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@ -118,12 +118,21 @@
|
|||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ListItem.narrow {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.ListItem-button {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-item-simple,
|
.settings-item-simple,
|
||||||
.settings-item {
|
.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;
|
@include mixins.side-panel-section;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +146,8 @@
|
|||||||
&-header {
|
&-header {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.25rem;
|
||||||
|
padding-inline-start: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&[dir="rtl"] {
|
&[dir="rtl"] {
|
||||||
@ -154,6 +164,7 @@
|
|||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
margin-top: -0.5rem;
|
margin-top: -0.5rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
padding-inline-start: 1rem;
|
||||||
|
|
||||||
.settings-content.two-fa &,
|
.settings-content.two-fa &,
|
||||||
.settings-content.password-form &,
|
.settings-content.password-form &,
|
||||||
@ -189,9 +200,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ListItem {
|
.ListItem {
|
||||||
margin: 0 -1rem 1rem;
|
&.narrow {
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,11 +311,7 @@
|
|||||||
|
|
||||||
.RangeSlider {
|
.RangeSlider {
|
||||||
margin-bottom: 1.0625rem;
|
margin-bottom: 1.0625rem;
|
||||||
}
|
padding-inline: 1rem;
|
||||||
|
|
||||||
.Checkbox,
|
|
||||||
.radio-group {
|
|
||||||
margin: 0 -1rem 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-group {
|
.radio-group {
|
||||||
@ -324,6 +329,10 @@
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .Checkbox, > .Radio {
|
||||||
|
padding-inline-start: 4.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
&__current-value {
|
&__current-value {
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
padding-inline-start: 0.5rem;
|
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 {
|
.settings-fab-wrapper {
|
||||||
height: calc(100% - var(--header-height));
|
height: calc(100% - var(--header-height));
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -368,8 +385,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-dropdown-section {
|
.settings-dropdown-section {
|
||||||
margin: 0 -0.75rem 1rem -1rem;
|
|
||||||
|
|
||||||
.DropdownList {
|
.DropdownList {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@ -381,7 +396,7 @@
|
|||||||
|
|
||||||
.SettingsDefaultReaction {
|
.SettingsDefaultReaction {
|
||||||
.current-default-reaction {
|
.current-default-reaction {
|
||||||
margin-inline-end: 2rem;
|
margin-inline-end: 1.1875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,6 +408,10 @@
|
|||||||
margin: inherit;
|
margin: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-item-picker {
|
||||||
|
padding: 1.5rem 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.block-user-button {
|
.block-user-button {
|
||||||
z-index: var(--z-chat-float-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-repeat: no-repeat;
|
||||||
background-size: 2rem;
|
background-size: 2rem;
|
||||||
flex: 0 0 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 {
|
@each $icon in $icons {
|
||||||
|
|||||||
@ -145,7 +145,7 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
|||||||
function renderCurrentSession(session: ApiSession) {
|
function renderCurrentSession(session: ApiSession) {
|
||||||
return (
|
return (
|
||||||
<div className="settings-item">
|
<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')}
|
{lang('AuthSessions.CurrentSession')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
|||||||
function renderOtherSessions(sessionHashes: string[]) {
|
function renderOtherSessions(sessionHashes: string[]) {
|
||||||
return (
|
return (
|
||||||
<div className="settings-item">
|
<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')}
|
{lang('OtherSessions')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
|
|||||||
function renderAutoTerminate() {
|
function renderAutoTerminate() {
|
||||||
return (
|
return (
|
||||||
<div className="settings-item">
|
<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')}
|
{lang('TerminateOldSessionHeader')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
|||||||
@ -81,7 +81,7 @@ const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
|
|||||||
function renderSessions(sessionHashes: string[]) {
|
function renderSessions(sessionHashes: string[]) {
|
||||||
return (
|
return (
|
||||||
<div className="settings-item">
|
<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')}
|
{lang('WebSessionsTitle')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
|||||||
@ -6,14 +6,4 @@
|
|||||||
.item {
|
.item {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 25rem;
|
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 type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useState,
|
memo, useMemo, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ISettings } from '../../../types';
|
import type { ISettings } from '../../../types';
|
||||||
import type { IRadioOption } from '../../ui/CheckboxGroup';
|
|
||||||
|
|
||||||
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../../config';
|
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../../config';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { partition, unique } from '../../../util/iteratees';
|
import { partition } from '../../../util/iteratees';
|
||||||
|
|
||||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import ItemPicker, { type ItemPickerOption } from '../../common/pickers/ItemPicker';
|
||||||
import InputText from '../../ui/InputText';
|
|
||||||
|
|
||||||
import styles from './SettingsDoNotTranslate.module.scss';
|
import styles from './SettingsDoNotTranslate.module.scss';
|
||||||
|
|
||||||
@ -62,11 +61,11 @@ const SettingsDoNotTranslate: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
const language = lang.code || 'en';
|
const language = lang.code || 'en';
|
||||||
const [displayedOptions, setDisplayedOptions] = useState<IRadioOption[]>([]);
|
const [displayedOptions, setDisplayedOptions] = useState<string[]>([]);
|
||||||
const [search, setSearch] = useState('');
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
const options: IRadioOption[] = useMemo(() => {
|
const displayedOptionList: ItemPickerOption[] = useMemo(() => {
|
||||||
return SUPPORTED_LANGUAGES.map((langCode: string) => {
|
const options = SUPPORTED_LANGUAGES.map((langCode: string) => {
|
||||||
const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
|
const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
|
||||||
const translatedName = translatedNames.of(langCode)!;
|
const translatedName = translatedNames.of(langCode)!;
|
||||||
|
|
||||||
@ -78,58 +77,33 @@ const SettingsDoNotTranslate: FC<OwnProps & StateProps> = ({
|
|||||||
translatedName,
|
translatedName,
|
||||||
originalName,
|
originalName,
|
||||||
};
|
};
|
||||||
}).map(({ langCode, translatedName, originalName }) => ({
|
}).filter(Boolean).map(({ langCode, translatedName, originalName }) => ({
|
||||||
label: translatedName,
|
label: translatedName,
|
||||||
subLabel: originalName,
|
subLabel: originalName,
|
||||||
value: langCode,
|
value: langCode,
|
||||||
}));
|
}));
|
||||||
}, [language]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (!searchQuery.trim()) {
|
||||||
if (!isActive) setSearch('');
|
const currentLanguageOption = options.find((option) => option.value === language);
|
||||||
}, [isActive]);
|
const otherOptionList = options.filter((option) => option.value !== language);
|
||||||
|
return currentLanguageOption ? [currentLanguageOption, ...otherOptionList] : options;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
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({
|
useHistoryBack({
|
||||||
isActive,
|
isActive,
|
||||||
@ -137,28 +111,21 @@ const SettingsDoNotTranslate: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName(styles.root, 'settings-content custom-scroll')}>
|
<div className={buildClassName(styles.root, 'settings-content infinite-scroll')}>
|
||||||
<div className={buildClassName(styles.item, 'settings-item')}>
|
<div className={buildClassName(styles.item)}>
|
||||||
<InputText
|
<ItemPicker
|
||||||
key="search"
|
className={styles.picker}
|
||||||
value={search}
|
items={displayedOptionList}
|
||||||
onChange={handleSearch}
|
selectedValues={displayedOptions}
|
||||||
placeholder={lang('Search')}
|
onSelectedValuesChange={handleChange}
|
||||||
teactExperimentControlled
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -146,6 +146,7 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="photo"
|
icon="photo"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.GeneralChatBackground)}
|
onClick={() => onScreenSelect(SettingsScreens.GeneralChatBackground)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import React, {
|
|||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiLanguage } from '../../../api/types';
|
|
||||||
import type { ISettings, LangCode } from '../../../types';
|
import type { ISettings, LangCode } from '../../../types';
|
||||||
import { SettingsScreens } from '../../../types';
|
import { SettingsScreens } from '../../../types';
|
||||||
|
|
||||||
@ -17,10 +16,10 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
|
|||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import ItemPicker, { type ItemPickerOption } from '../../common/pickers/ItemPicker';
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import RadioGroup from '../../ui/RadioGroup';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
@ -77,8 +76,19 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return languages ? buildOptions(languages) : undefined;
|
if (!languages) return undefined;
|
||||||
}, [languages]);
|
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) => {
|
const handleShouldTranslateChange = useLastCallback((newValue: boolean) => {
|
||||||
setSettingOption({ canTranslate: newValue });
|
setSettingOption({ canTranslate: newValue });
|
||||||
@ -128,7 +138,6 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
|||||||
onCheck={handleShouldTranslateChange}
|
onCheck={handleShouldTranslateChange}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className="pb-2"
|
|
||||||
label={lang('ShowTranslateChatButton')}
|
label={lang('ShowTranslateChatButton')}
|
||||||
checked={canTranslateChatsEnabled}
|
checked={canTranslateChatsEnabled}
|
||||||
disabled={!isCurrentUserPremium}
|
disabled={!isCurrentUserPremium}
|
||||||
@ -138,6 +147,7 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
{(canTranslate || canTranslateChatsEnabled) && (
|
{(canTranslate || canTranslateChatsEnabled) && (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
narrow
|
||||||
onClick={handleDoNotSelectOpen}
|
onClick={handleDoNotSelectOpen}
|
||||||
>
|
>
|
||||||
{lang('DoNotTranslate')}
|
{lang('DoNotTranslate')}
|
||||||
@ -149,15 +159,17 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="settings-item">
|
<div className="settings-item settings-item-picker">
|
||||||
<h4 className="settings-item-header mb-4">{lang('Localization.InterfaceLanguage')}</h4>
|
<h4 className="settings-item-header settings-picker-title">
|
||||||
|
{lang('Localization.InterfaceLanguage')}
|
||||||
|
</h4>
|
||||||
{options ? (
|
{options ? (
|
||||||
<RadioGroup
|
<ItemPicker
|
||||||
name="language-settings"
|
items={options}
|
||||||
options={options}
|
selectedValue={selectedLanguage}
|
||||||
selected={selectedLanguage}
|
onSelectedValueChange={handleChange}
|
||||||
loadingOption={isLoading ? selectedLanguage : undefined}
|
itemInputType="radio"
|
||||||
onChange={handleChange}
|
className="settings-picker"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Loading />
|
<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>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -95,6 +95,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="settings"
|
icon="settings"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.General)}
|
onClick={() => onScreenSelect(SettingsScreens.General)}
|
||||||
>
|
>
|
||||||
@ -102,6 +103,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="animations"
|
icon="animations"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.Performance)}
|
onClick={() => onScreenSelect(SettingsScreens.Performance)}
|
||||||
>
|
>
|
||||||
@ -109,6 +111,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="unmute"
|
icon="unmute"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.Notifications)}
|
onClick={() => onScreenSelect(SettingsScreens.Notifications)}
|
||||||
>
|
>
|
||||||
@ -116,6 +119,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="data"
|
icon="data"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.DataStorage)}
|
onClick={() => onScreenSelect(SettingsScreens.DataStorage)}
|
||||||
>
|
>
|
||||||
@ -123,6 +127,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="lock"
|
icon="lock"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.Privacy)}
|
onClick={() => onScreenSelect(SettingsScreens.Privacy)}
|
||||||
>
|
>
|
||||||
@ -130,6 +135,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="folder"
|
icon="folder"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.Folders)}
|
onClick={() => onScreenSelect(SettingsScreens.Folders)}
|
||||||
>
|
>
|
||||||
@ -137,6 +143,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="active-sessions"
|
icon="active-sessions"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.ActiveSessions)}
|
onClick={() => onScreenSelect(SettingsScreens.ActiveSessions)}
|
||||||
>
|
>
|
||||||
@ -145,6 +152,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="language"
|
icon="language"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.Language)}
|
onClick={() => onScreenSelect(SettingsScreens.Language)}
|
||||||
>
|
>
|
||||||
@ -153,6 +161,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="stickers"
|
icon="stickers"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.Stickers)}
|
onClick={() => onScreenSelect(SettingsScreens.Stickers)}
|
||||||
>
|
>
|
||||||
@ -164,6 +173,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
<ListItem
|
<ListItem
|
||||||
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
|
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
|
||||||
className="settings-main-menu-star"
|
className="settings-main-menu-star"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => openPremiumModal()}
|
onClick={() => openPremiumModal()}
|
||||||
>
|
>
|
||||||
@ -174,6 +184,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
<ListItem
|
<ListItem
|
||||||
leftElement={<StarIcon className="icon ListItem-main-icon" type="gold" size="big" />}
|
leftElement={<StarIcon className="icon ListItem-main-icon" type="gold" size="big" />}
|
||||||
className="settings-main-menu-star"
|
className="settings-main-menu-star"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => openStarsBalanceModal({})}
|
onClick={() => openStarsBalanceModal({})}
|
||||||
>
|
>
|
||||||
@ -187,6 +198,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
<ListItem
|
<ListItem
|
||||||
icon="gift"
|
icon="gift"
|
||||||
className="settings-main-menu-star"
|
className="settings-main-menu-star"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => openPremiumGiftingModal()}
|
onClick={() => openPremiumGiftingModal()}
|
||||||
>
|
>
|
||||||
@ -197,12 +209,14 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="settings-main-menu">
|
<div className="settings-main-menu">
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="ask-support"
|
icon="ask-support"
|
||||||
|
narrow
|
||||||
onClick={openSupportDialog}
|
onClick={openSupportDialog}
|
||||||
>
|
>
|
||||||
{lang('AskAQuestion')}
|
{lang('AskAQuestion')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="help"
|
icon="help"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => openUrl({ url: FAQ_URL })}
|
onClick={() => openUrl({ url: FAQ_URL })}
|
||||||
>
|
>
|
||||||
@ -210,6 +224,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="privacy-policy"
|
icon="privacy-policy"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => openUrl({ url: PRIVACY_URL })}
|
onClick={() => openUrl({ url: PRIVACY_URL })}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -103,9 +103,11 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
}, [updateContentSettings]);
|
}, [updateContentSettings]);
|
||||||
|
|
||||||
function getVisibilityValue(setting?: ApiPrivacySettings) {
|
function getVisibilityValue(setting?: ApiPrivacySettings) {
|
||||||
const { visibility, shouldAllowPremium } = setting || {};
|
if (!setting) return lang('Loading');
|
||||||
const blockCount = setting ? setting.blockChatIds.length + setting.blockUserIds.length : 0;
|
|
||||||
const allowCount = setting ? setting.allowChatIds.length + setting.allowUserIds.length : 0;
|
const { visibility, shouldAllowPremium } = setting;
|
||||||
|
const blockCount = setting.blockChatIds.length + setting.blockUserIds.length;
|
||||||
|
const allowCount = setting.allowChatIds.length + setting.allowUserIds.length;
|
||||||
const total = [];
|
const total = [];
|
||||||
if (blockCount) total.push(`-${blockCount}`);
|
if (blockCount) total.push(`-${blockCount}`);
|
||||||
if (allowCount) total.push(`+${allowCount}`);
|
if (allowCount) total.push(`+${allowCount}`);
|
||||||
@ -135,6 +137,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="settings-item pt-3">
|
<div className="settings-item pt-3">
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="delete-user"
|
icon="delete-user"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.PrivacyBlockedUsers)}
|
onClick={() => onScreenSelect(SettingsScreens.PrivacyBlockedUsers)}
|
||||||
>
|
>
|
||||||
@ -176,6 +179,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
{webAuthCount > 0 && (
|
{webAuthCount > 0 && (
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="web"
|
icon="web"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onScreenSelect(SettingsScreens.ActiveWebsites)}
|
onClick={() => onScreenSelect(SettingsScreens.ActiveWebsites)}
|
||||||
>
|
>
|
||||||
@ -186,7 +190,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-item">
|
<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
|
<ListItem
|
||||||
narrow
|
narrow
|
||||||
|
|||||||
@ -293,7 +293,7 @@ function PrivacySubsection({
|
|||||||
</div>
|
</div>
|
||||||
{!isPremiumRequired && (primaryExceptionLists.shouldShowAllowed || primaryExceptionLists.shouldShowDenied) && (
|
{!isPremiumRequired && (primaryExceptionLists.shouldShowAllowed || primaryExceptionLists.shouldShowDenied) && (
|
||||||
<div className="settings-item">
|
<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')}
|
{lang('PrivacyExceptions')}
|
||||||
</h4>
|
</h4>
|
||||||
{primaryExceptionLists.shouldShowAllowed && (
|
{primaryExceptionLists.shouldShowAllowed && (
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
|
|||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
@ -139,7 +139,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="NewChat-inner step-1">
|
<div className="NewChat-inner step-1">
|
||||||
<Picker
|
<PeerPicker
|
||||||
categories={withPremiumCategory ? PREMIUM_CATEGORY : undefined}
|
categories={withPremiumCategory ? PREMIUM_CATEGORY : undefined}
|
||||||
itemIds={displayedIds || []}
|
itemIds={displayedIds || []}
|
||||||
selectedIds={newSelectedContactIds}
|
selectedIds={newSelectedContactIds}
|
||||||
@ -152,6 +152,10 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
|||||||
onSelectedIdsChange={handleSelectedContactIdsChange}
|
onSelectedIdsChange={handleSelectedContactIdsChange}
|
||||||
onSelectedCategoriesChange={handleSelectedCategoriesChange}
|
onSelectedCategoriesChange={handleSelectedCategoriesChange}
|
||||||
onFilterChange={setSearchQuery}
|
onFilterChange={setSearchQuery}
|
||||||
|
allowMultiple
|
||||||
|
itemInputType="checkbox"
|
||||||
|
withDefaultPadding
|
||||||
|
withStatus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
|
|||||||
@ -58,6 +58,7 @@ const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
|||||||
options={options}
|
options={options}
|
||||||
selected={selectedReaction}
|
selected={selectedReaction}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
withIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,6 +12,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-folders-list-item {
|
.settings-folders-list-item {
|
||||||
|
.ListItem-main-icon {
|
||||||
|
margin-inline-end: 1.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ChatInfo {
|
.ChatInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -99,7 +103,9 @@
|
|||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.down {
|
.Picker {
|
||||||
font-size: 1.5rem;
|
height: 100%;
|
||||||
margin-right: 0.5rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import useLastCallback from '../../../../hooks/useLastCallback';
|
|||||||
import useOldLang from '../../../../hooks/useOldLang';
|
import useOldLang from '../../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Icon from '../../../common/icons/Icon';
|
import Icon from '../../../common/icons/Icon';
|
||||||
import Picker from '../../../common/Picker';
|
import PeerPicker from '../../../common/pickers/PeerPicker';
|
||||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||||
import Loading from '../../../ui/Loading';
|
import Loading from '../../../ui/Loading';
|
||||||
|
|
||||||
@ -128,6 +128,7 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setIsTouched(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
@ -141,7 +142,7 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Picker settings-folders-chat-list">
|
<div className="Picker settings-folders-chat-list">
|
||||||
<Picker
|
<PeerPicker
|
||||||
categories={shouldHideChatTypes ? undefined : chatTypes}
|
categories={shouldHideChatTypes ? undefined : chatTypes}
|
||||||
itemIds={displayedIds}
|
itemIds={displayedIds}
|
||||||
selectedIds={selectedChatIds}
|
selectedIds={selectedChatIds}
|
||||||
@ -151,7 +152,10 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
|
|||||||
categoryPlaceholderKey="FilterChatTypes"
|
categoryPlaceholderKey="FilterChatTypes"
|
||||||
searchInputId="new-group-picker-search"
|
searchInputId="new-group-picker-search"
|
||||||
isSearchable
|
isSearchable
|
||||||
isRoundCheckbox
|
withDefaultPadding
|
||||||
|
withPeerTypes
|
||||||
|
allowMultiple
|
||||||
|
itemInputType="checkbox"
|
||||||
onSelectedIdsChange={handleSelectedIdsChange}
|
onSelectedIdsChange={handleSelectedIdsChange}
|
||||||
onSelectedCategoriesChange={handleSelectedChatTypesChange}
|
onSelectedCategoriesChange={handleSelectedChatTypesChange}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import useOldLang from '../../../../hooks/useOldLang';
|
|||||||
|
|
||||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||||
import GroupChatInfo from '../../../common/GroupChatInfo';
|
import GroupChatInfo from '../../../common/GroupChatInfo';
|
||||||
import Icon from '../../../common/icons/Icon';
|
|
||||||
import PrivateChatInfo from '../../../common/PrivateChatInfo';
|
import PrivateChatInfo from '../../../common/PrivateChatInfo';
|
||||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||||
import InputText from '../../../ui/InputText';
|
import InputText from '../../../ui/InputText';
|
||||||
@ -267,10 +266,12 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
{(!isExpanded && leftChatsCount > 0) && (
|
{(!isExpanded && leftChatsCount > 0) && (
|
||||||
<ListItem
|
<ListItem
|
||||||
key="load-more"
|
key="load-more"
|
||||||
|
className="settings-folders-list-item"
|
||||||
|
narrow
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={clickHandler}
|
onClick={clickHandler}
|
||||||
|
icon="down"
|
||||||
>
|
>
|
||||||
<Icon name="down" className="down" />
|
|
||||||
{lang('FilterShowMoreChats', leftChatsCount, 'i')}
|
{lang('FilterShowMoreChats', leftChatsCount, 'i')}
|
||||||
</ListItem>
|
</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>
|
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterInclude')}</h4>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
className="settings-folders-list-item color-primary mb-0"
|
className="settings-folders-list-item color-primary"
|
||||||
icon="add"
|
icon="add"
|
||||||
|
narrow
|
||||||
onClick={onAddIncludedChats}
|
onClick={onAddIncludedChats}
|
||||||
>
|
>
|
||||||
{lang('FilterAddChats')}
|
{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>
|
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
className="settings-folders-list-item color-primary mb-0"
|
className="settings-folders-list-item color-primary"
|
||||||
icon="add"
|
icon="add"
|
||||||
|
narrow
|
||||||
onClick={onAddExcludedChats}
|
onClick={onAddExcludedChats}
|
||||||
>
|
>
|
||||||
{lang('FilterAddChats')}
|
{lang('FilterAddChats')}
|
||||||
@ -348,8 +351,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
className="settings-folders-list-item color-primary mb-0"
|
className="settings-folders-list-item color-primary"
|
||||||
icon="add"
|
icon="add"
|
||||||
|
narrow
|
||||||
onClick={handleCreateInviteClick}
|
onClick={handleCreateInviteClick}
|
||||||
>
|
>
|
||||||
{lang('ChatListFilter.CreateLinkNew')}
|
{lang('ChatListFilter.CreateLinkNew')}
|
||||||
@ -357,8 +361,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
{invites?.map((invite) => (
|
{invites?.map((invite) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
className="settings-folders-list-item mb-0"
|
className="settings-folders-list-item"
|
||||||
icon="link"
|
icon="link"
|
||||||
|
narrow
|
||||||
multiline
|
multiline
|
||||||
onClick={handleEditInviteClick}
|
onClick={handleEditInviteClick}
|
||||||
clickArg={invite.url}
|
clickArg={invite.url}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import useOldLang from '../../../../hooks/useOldLang';
|
|||||||
|
|
||||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||||
import LinkField from '../../../common/LinkField';
|
import LinkField from '../../../common/LinkField';
|
||||||
import Picker from '../../../common/Picker';
|
import PeerPicker from '../../../common/pickers/PeerPicker';
|
||||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||||
import Spinner from '../../../ui/Spinner';
|
import Spinner from '../../../ui/Spinner';
|
||||||
|
|
||||||
@ -167,14 +167,16 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
|
|||||||
isDisabled={!chatsCount || isTouched}
|
isDisabled={!chatsCount || isTouched}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="settings-item settings-item-chatlist">
|
<div className="settings-item settings-item-picker">
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={itemIds}
|
itemIds={itemIds}
|
||||||
lockedSelectedIds={lockedIds}
|
lockedUnselectedIds={lockedIds}
|
||||||
onSelectedIdsChange={handleSelectedIdsChange}
|
onSelectedIdsChange={handleSelectedIdsChange}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onDisabledClick={handleClickDisabled}
|
onDisabledClick={handleClickDisabled}
|
||||||
isRoundCheckbox
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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;
|
margin-inline-start: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.removeChannel {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.root :global(.modal-dialog) {
|
.root :global(.modal-dialog) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -41,8 +41,9 @@ import Modal from '../../ui/Modal';
|
|||||||
import RadioGroup from '../../ui/RadioGroup';
|
import RadioGroup from '../../ui/RadioGroup';
|
||||||
import RangeSliderWithMarks from '../../ui/RangeSliderWithMarks';
|
import RangeSliderWithMarks from '../../ui/RangeSliderWithMarks';
|
||||||
import Switcher from '../../ui/Switcher';
|
import Switcher from '../../ui/Switcher';
|
||||||
import AppendEntityPickerModal from '../AppendEntityPickerModal';
|
import GiveawayChannelPickerModal from './GiveawayChannelPickerModal';
|
||||||
import GiveawayTypeOption from './GiveawayTypeOption';
|
import GiveawayTypeOption from './GiveawayTypeOption';
|
||||||
|
import GiveawayUserPickerModal from './GiveawayUserPickerModal';
|
||||||
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
|
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
|
||||||
|
|
||||||
import styles from './GiveawayModal.module.scss';
|
import styles from './GiveawayModal.module.scss';
|
||||||
@ -121,8 +122,8 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
|
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
|
||||||
const [isCountryPickerModalOpen, openCountryPickerModal, closeCountryPickerModal] = useFlag();
|
const [isCountryPickerModalOpen, openCountryPickerModal, closeCountryPickerModal] = useFlag();
|
||||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||||
const [isEntityPickerModalOpen, openEntityPickerModal, closeEntityPickerModal] = useFlag();
|
const [isUserPickerModalOpen, openUserPickerModal, closeUserPickerModal] = useFlag();
|
||||||
const [entityType, setEntityType] = useState<'members' | 'channels' | undefined>(undefined);
|
const [isChannelPickerModalOpen, openChannelPickerModal, closeChannelPickerModal] = useFlag();
|
||||||
|
|
||||||
const TYPE_OPTIONS: TypeOption[] = [{
|
const TYPE_OPTIONS: TypeOption[] = [{
|
||||||
name: 'BoostingCreateGiveaway',
|
name: 'BoostingCreateGiveaway',
|
||||||
@ -139,43 +140,44 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
actions: 'createSpecificUsers',
|
actions: 'createSpecificUsers',
|
||||||
isLink: true,
|
isLink: true,
|
||||||
onClickAction: () => {
|
onClickAction: () => {
|
||||||
openEntityPickerModal();
|
openUserPickerModal();
|
||||||
setEntityType('members');
|
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const [customExpireDate, setCustomExpireDate] = useState<number>(Date.now() + DEFAULT_CUSTOM_EXPIRE_DATE);
|
const [customExpireDate, setCustomExpireDate] = useState<number>(Date.now() + DEFAULT_CUSTOM_EXPIRE_DATE);
|
||||||
const [isHeaderHidden, setHeaderHidden] = useState(true);
|
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 [selectedGiveawayOption, setGiveawayOption] = useState<ApiGiveawayType>(TYPE_OPTIONS[0].value);
|
||||||
const [selectedSubscriberOption, setSelectedSubscriberOption] = useState<SubscribersType>('all');
|
const [selectedSubscriberOption, setSelectedSubscriberOption] = useState<SubscribersType>('all');
|
||||||
const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>();
|
const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>();
|
||||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||||
const [selectedChannelIds, setSelectedChannelIds] = 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 [shouldShowWinners, setShouldShowWinners] = useState<boolean>(false);
|
||||||
const [shouldShowPrizes, setShouldShowPrizes] = useState<boolean>(false);
|
const [shouldShowPrizes, setShouldShowPrizes] = useState<boolean>(false);
|
||||||
const [prizeDescription, setPrizeDescription] = useState<string | undefined>(undefined);
|
const [prizeDescription, setPrizeDescription] = useState<string | undefined>(undefined);
|
||||||
const [dataPrepaidGiveaway, setDataPrepaidGiveaway] = useState<ApiPrepaidGiveaway | undefined>(undefined);
|
const [dataPrepaidGiveaway, setDataPrepaidGiveaway] = useState<ApiPrepaidGiveaway | undefined>(undefined);
|
||||||
const boostQuantity = selectedUserCount * giveawayBoostPerPremiumLimit;
|
|
||||||
const isRandomUsers = selectedGiveawayOption === 'random_users';
|
const isRandomUsers = selectedGiveawayOption === 'random_users';
|
||||||
|
const selectedUserCount = isRandomUsers ? selectedRandomUserCount : selectedUserIds.length;
|
||||||
|
const boostQuantity = selectedUserCount * giveawayBoostPerPremiumLimit;
|
||||||
|
|
||||||
const SUBSCRIBER_OPTIONS = useMemo(() => [
|
const SUBSCRIBER_OPTIONS = useMemo(() => [
|
||||||
{
|
{
|
||||||
value: 'all',
|
value: 'all',
|
||||||
label: lang(isChannel ? 'BoostingAllSubscribers' : 'BoostingAllMembers'),
|
label: lang(isChannel ? 'BoostingAllSubscribers' : 'BoostingAllMembers'),
|
||||||
subLabel: selectedCountriesIds && selectedCountriesIds.length > 0
|
subLabel: selectedCountryIds && selectedCountryIds.length > 0
|
||||||
? lang('Giveaway.ReceiverType.Countries', selectedCountriesIds.length)
|
? lang('Giveaway.ReceiverType.Countries', selectedCountryIds.length)
|
||||||
: lang('BoostingFromAllCountries'),
|
: lang('BoostingFromAllCountries'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'new',
|
value: 'new',
|
||||||
label: lang(isChannel ? 'BoostingNewSubscribers' : 'BoostingNewMembers'),
|
label: lang(isChannel ? 'BoostingNewSubscribers' : 'BoostingNewMembers'),
|
||||||
subLabel: selectedCountriesIds && selectedCountriesIds.length > 0
|
subLabel: selectedCountryIds && selectedCountryIds.length > 0
|
||||||
? lang('Giveaway.ReceiverType.Countries', selectedCountriesIds.length)
|
? lang('Giveaway.ReceiverType.Countries', selectedCountryIds.length)
|
||||||
: lang('BoostingFromAllCountries'),
|
: lang('BoostingFromAllCountries'),
|
||||||
},
|
},
|
||||||
], [isChannel, lang, selectedCountriesIds]);
|
], [isChannel, lang, selectedCountryIds]);
|
||||||
|
|
||||||
const monthQuantity = lang('Months', selectedMonthOption);
|
const monthQuantity = lang('Months', selectedMonthOption);
|
||||||
|
|
||||||
@ -184,9 +186,8 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
}, [gifts, selectedMonthOption, selectedUserCount]);
|
}, [gifts, selectedMonthOption, selectedUserCount]);
|
||||||
|
|
||||||
const filteredGifts = useMemo(() => {
|
const filteredGifts = useMemo(() => {
|
||||||
return gifts?.filter((gift) => gift.users
|
return gifts?.filter((gift) => gift.users === selectedUserCount);
|
||||||
=== (selectedUserIds?.length ? selectedUserIds?.length : selectedUserCount));
|
}, [gifts, selectedUserCount]);
|
||||||
}, [gifts, selectedUserIds, selectedUserCount]);
|
|
||||||
|
|
||||||
const fullMonthlyAmount = useMemo(() => {
|
const fullMonthlyAmount = useMemo(() => {
|
||||||
if (!filteredGifts?.length) {
|
if (!filteredGifts?.length) {
|
||||||
@ -213,7 +214,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prepaidGiveaway) {
|
if (prepaidGiveaway) {
|
||||||
setSelectedUserCount(prepaidGiveaway.quantity);
|
setSelectedRandomUserCount(prepaidGiveaway.quantity);
|
||||||
setDataPrepaidGiveaway(prepaidGiveaway);
|
setDataPrepaidGiveaway(prepaidGiveaway);
|
||||||
}
|
}
|
||||||
}, [prepaidGiveaway]);
|
}, [prepaidGiveaway]);
|
||||||
@ -235,25 +236,25 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleClick = useLastCallback(() => {
|
const handleClick = useLastCallback(() => {
|
||||||
if (selectedUserIds?.length) {
|
if (isRandomUsers) {
|
||||||
openInvoice({
|
openInvoice({
|
||||||
type: 'giftcode',
|
type: 'giveaway',
|
||||||
boostChannelId: chatId!,
|
chatId: chatId!,
|
||||||
userIds: selectedUserIds,
|
additionalChannelIds: selectedChannelIds,
|
||||||
|
isOnlyForNewSubscribers: selectedSubscriberOption === 'new',
|
||||||
|
countries: selectedCountryIds,
|
||||||
|
areWinnersVisible: shouldShowWinners,
|
||||||
|
prizeDescription,
|
||||||
|
untilDate: customExpireDate / 1000,
|
||||||
currency: selectedGift!.currency,
|
currency: selectedGift!.currency,
|
||||||
amount: selectedGift!.amount,
|
amount: selectedGift!.amount,
|
||||||
option: selectedGift!,
|
option: selectedGift!,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
openInvoice({
|
openInvoice({
|
||||||
type: 'giveaway',
|
type: 'giftcode',
|
||||||
chatId: chatId!,
|
boostChannelId: chatId!,
|
||||||
additionalChannelIds: selectedChannelIds,
|
userIds: selectedUserIds,
|
||||||
isOnlyForNewSubscribers: selectedSubscriberOption === 'new',
|
|
||||||
countries: selectedCountriesIds,
|
|
||||||
areWinnersVisible: shouldShowWinners,
|
|
||||||
prizeDescription,
|
|
||||||
untilDate: customExpireDate / 1000,
|
|
||||||
currency: selectedGift!.currency,
|
currency: selectedGift!.currency,
|
||||||
amount: selectedGift!.amount,
|
amount: selectedGift!.amount,
|
||||||
option: selectedGift!,
|
option: selectedGift!,
|
||||||
@ -269,7 +270,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
giveawayId: dataPrepaidGiveaway!.id,
|
giveawayId: dataPrepaidGiveaway!.id,
|
||||||
paymentPurpose: {
|
paymentPurpose: {
|
||||||
additionalChannelIds: selectedChannelIds,
|
additionalChannelIds: selectedChannelIds,
|
||||||
countries: selectedCountriesIds,
|
countries: selectedCountryIds,
|
||||||
prizeDescription,
|
prizeDescription,
|
||||||
areWinnersVisible: shouldShowWinners,
|
areWinnersVisible: shouldShowWinners,
|
||||||
untilDate: customExpireDate / 1000,
|
untilDate: customExpireDate / 1000,
|
||||||
@ -282,8 +283,8 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
closeGiveawayModal();
|
closeGiveawayModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUserCountChange = useLastCallback((newValue) => {
|
const handleRandomUserCountChange = useLastCallback((newValue) => {
|
||||||
setSelectedUserCount(newValue);
|
setSelectedRandomUserCount(newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePrizeDescriptionChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
const handlePrizeDescriptionChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -295,11 +296,6 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
return selectedUserIds?.map((userId) => getUserFullName(usersById[userId])).join(', ');
|
return selectedUserIds?.map((userId) => getUserFullName(usersById[userId])).join(', ');
|
||||||
}, [selectedUserIds]);
|
}, [selectedUserIds]);
|
||||||
|
|
||||||
const handleAdd = useLastCallback(() => {
|
|
||||||
openEntityPickerModal();
|
|
||||||
setEntityType('channels');
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
|
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
|
||||||
const { scrollTop } = e.currentTarget;
|
const { scrollTop } = e.currentTarget;
|
||||||
|
|
||||||
@ -321,13 +317,18 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSetCountriesListChange = useLastCallback((value: string[]) => {
|
const handleSetCountriesListChange = useLastCallback((value: string[]) => {
|
||||||
setSelectedCountriesIds(value);
|
setSelectedCountryIds(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSetIdsListChange = useLastCallback((value: string[]) => {
|
const handleSelectedUserIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||||
return entityType === 'members'
|
setSelectedUserIds(newSelectedIds);
|
||||||
? (value?.length ? setSelectedUserIds(value) : setGiveawayOption('random_users'))
|
if (!newSelectedIds.length) {
|
||||||
: setSelectedChannelIds(value);
|
setGiveawayOption('random_users');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||||
|
setSelectedChannelIds(newSelectedIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClose = useLastCallback(() => {
|
const handleClose = useLastCallback(() => {
|
||||||
@ -503,7 +504,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
<RangeSliderWithMarks
|
<RangeSliderWithMarks
|
||||||
rangeCount={selectedUserCount}
|
rangeCount={selectedUserCount}
|
||||||
marks={userCountOptions}
|
marks={userCountOptions}
|
||||||
onChange={handleUserCountChange}
|
onChange={handleRandomUserCountChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -537,7 +538,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
className="chat-item-clickable contact-list-item"
|
className="chat-item-clickable contact-list-item"
|
||||||
/* eslint-disable-next-line react/jsx-no-bind */
|
/* eslint-disable-next-line react/jsx-no-bind */
|
||||||
onClick={() => deleteParticipantsHandler(channelId)}
|
onClick={() => deleteParticipantsHandler(channelId)}
|
||||||
rightElement={(<Icon name="close" />)}
|
rightElement={(<Icon name="close" className={styles.removeChannel} />)}
|
||||||
>
|
>
|
||||||
<GroupChatInfo
|
<GroupChatInfo
|
||||||
chatId={channelId.toString()}
|
chatId={channelId.toString()}
|
||||||
@ -550,7 +551,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
<ListItem
|
<ListItem
|
||||||
icon="add"
|
icon="add"
|
||||||
ripple
|
ripple
|
||||||
onClick={handleAdd}
|
onClick={openChannelPickerModal}
|
||||||
className={styles.addButton}
|
className={styles.addButton}
|
||||||
iconClassName={styles.addChannel}
|
iconClassName={styles.addChannel}
|
||||||
>
|
>
|
||||||
@ -705,14 +706,21 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
onSubmit={handleSetCountriesListChange}
|
onSubmit={handleSetCountriesListChange}
|
||||||
selectionLimit={countrySelectionLimit}
|
selectionLimit={countrySelectionLimit}
|
||||||
/>
|
/>
|
||||||
<AppendEntityPickerModal
|
<GiveawayUserPickerModal
|
||||||
key={entityType}
|
isOpen={isUserPickerModalOpen}
|
||||||
isOpen={isEntityPickerModalOpen}
|
onClose={closeUserPickerModal}
|
||||||
onClose={closeEntityPickerModal}
|
onSelectedIdsConfirmed={handleSelectedUserIdsChange}
|
||||||
entityType={entityType}
|
initialSelectedIds={selectedUserIds}
|
||||||
chatId={chatId}
|
selectionLimit={userSelectionLimit}
|
||||||
onSubmit={handleSetIdsListChange}
|
giveawayChatId={chatId}
|
||||||
selectionLimit={entityType === 'members' ? userSelectionLimit : GIVEAWAY_MAX_ADDITIONAL_CHANNELS}
|
/>
|
||||||
|
<GiveawayChannelPickerModal
|
||||||
|
isOpen={isChannelPickerModalOpen}
|
||||||
|
onClose={closeChannelPickerModal}
|
||||||
|
initialSelectedIds={selectedChannelIds}
|
||||||
|
onSelectedIdsConfirmed={handleSelectedChannelIdsChange}
|
||||||
|
selectionLimit={GIVEAWAY_MAX_ADDITIONAL_CHANNELS}
|
||||||
|
giveawayChatId={chatId}
|
||||||
/>
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
title={lang('BoostingStartGiveawayConfirmTitle')}
|
title={lang('BoostingStartGiveawayConfirmTitle')}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
padding-inline: 3.5rem 1rem;
|
padding-inline: 3.8125rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
@ -42,7 +42,7 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-start: 1.0625rem;
|
inset-inline-start: 1rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
@ -58,7 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
inset-inline-start: 1.375rem;
|
inset-inline-start: 1.3125rem;
|
||||||
width: 0.625rem;
|
width: 0.625rem;
|
||||||
height: 0.625rem;
|
height: 0.625rem;
|
||||||
border-radius: 50%;
|
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 useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Icon from '../../common/icons/Icon';
|
import Icon from '../../common/icons/Icon';
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ const PremiumGiftingModal: FC<OwnProps & StateProps> = ({
|
|||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
{renderSearchField()}
|
{renderSearchField()}
|
||||||
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||||
<Picker
|
<PeerPicker
|
||||||
className={styles.picker}
|
className={styles.picker}
|
||||||
itemIds={displayedUserIds}
|
itemIds={displayedUserIds}
|
||||||
selectedIds={selectedUserIds}
|
selectedIds={selectedUserIds}
|
||||||
@ -116,6 +116,10 @@ const PremiumGiftingModal: FC<OwnProps & StateProps> = ({
|
|||||||
onSelectedIdsChange={handleSelectedUserIdsChange}
|
onSelectedIdsChange={handleSelectedUserIdsChange}
|
||||||
onFilterChange={setSearchQuery}
|
onFilterChange={setSearchQuery}
|
||||||
isSearchable
|
isSearchable
|
||||||
|
withDefaultPadding
|
||||||
|
withStatus
|
||||||
|
allowMultiple
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
.giveawayWrapper {
|
.giveawayWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
padding-inline: 3.6875rem 1rem;
|
padding-inline: 3.8125rem 1rem;
|
||||||
|
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
@ -55,7 +55,7 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-start: 1.0625rem;
|
inset-inline-start: 1rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
@ -71,7 +71,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
inset-inline-start: 1.375rem;
|
inset-inline-start: 1.3125rem;
|
||||||
width: 0.625rem;
|
width: 0.625rem;
|
||||||
height: 0.625rem;
|
height: 0.625rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
.actionTitle {
|
.actionTitle {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
margin-left: 0.5rem;
|
margin-left: 1rem;
|
||||||
color: var(--color-links);
|
color: var(--color-links);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
.listItemButton {
|
.listItemButton {
|
||||||
margin-top: 0.1875rem;
|
margin-top: 0.1875rem;
|
||||||
margin-left: 0.4375rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
|||||||
@ -8,8 +8,9 @@ import type {
|
|||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getChatTitle, getUserFullName, isApiPeerChat, isOwnMessage,
|
getChatTitle, getUserFullName, isOwnMessage,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
|
import { isApiPeerChat } from '../../../global/helpers/peers';
|
||||||
import {
|
import {
|
||||||
selectCanPlayAnimatedEmojis,
|
selectCanPlayAnimatedEmojis,
|
||||||
selectChat,
|
selectChat,
|
||||||
@ -29,7 +30,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
|||||||
|
|
||||||
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
||||||
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
import Separator from '../../ui/Separator';
|
import Separator from '../../ui/Separator';
|
||||||
|
|||||||
@ -298,7 +298,6 @@ const Poll: FC<OwnProps & StateProps> = ({
|
|||||||
onChange={handleCheckboxChange}
|
onChange={handleCheckboxChange}
|
||||||
disabled={message.isScheduled || isSubmitting}
|
disabled={message.isScheduled || isSubmitting}
|
||||||
loadingOptions={isSubmitting ? chosenOptions : undefined}
|
loadingOptions={isSubmitting ? chosenOptions : undefined}
|
||||||
round
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
|
|||||||
@ -219,6 +219,8 @@
|
|||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
background-color: var(--color-item-active);
|
background-color: var(--color-item-active);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedType {
|
.selectedType {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
|||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import Icon from '../../common/icons/Icon';
|
import Icon from '../../common/icons/Icon';
|
||||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import SearchInput from '../../ui/SearchInput';
|
import SearchInput from '../../ui/SearchInput';
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import renderText from '../../common/helpers/renderText';
|
|||||||
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import Badge from '../../ui/Badge';
|
import Badge from '../../ui/Badge';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
@ -72,10 +72,13 @@ const ChatlistAlready: FC<OwnProps> = ({ invite, folder }) => {
|
|||||||
{selectedPeerIds.length === invite.missingPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
{selectedPeerIds.length === invite.missingPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={invite.missingPeerIds}
|
itemIds={invite.missingPeerIds}
|
||||||
onSelectedIdsChange={setSelectedPeerIds}
|
onSelectedIdsChange={setSelectedPeerIds}
|
||||||
selectedIds={selectedPeerIds}
|
selectedIds={selectedPeerIds}
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -84,10 +87,13 @@ const ChatlistAlready: FC<OwnProps> = ({ invite, folder }) => {
|
|||||||
{lang('FolderLinkHeaderAlready')}
|
{lang('FolderLinkHeaderAlready')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={invite.alreadyPeerIds}
|
itemIds={invite.alreadyPeerIds}
|
||||||
lockedSelectedIds={invite.alreadyPeerIds}
|
lockedSelectedIds={invite.alreadyPeerIds}
|
||||||
selectedIds={invite.alreadyPeerIds}
|
selectedIds={invite.alreadyPeerIds}
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import renderText from '../../common/helpers/renderText';
|
|||||||
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import Badge from '../../ui/Badge';
|
import Badge from '../../ui/Badge';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
@ -64,10 +64,13 @@ const ChatlistDelete: FC<OwnProps> = ({
|
|||||||
{selectedPeerIds.length === suggestedPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
{selectedPeerIds.length === suggestedPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={suggestedPeerIds}
|
itemIds={suggestedPeerIds}
|
||||||
onSelectedIdsChange={setSelectedPeerIds}
|
onSelectedIdsChange={setSelectedPeerIds}
|
||||||
selectedIds={selectedPeerIds}
|
selectedIds={selectedPeerIds}
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import renderText from '../../common/helpers/renderText';
|
|||||||
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import Badge from '../../ui/Badge';
|
import Badge from '../../ui/Badge';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
@ -69,11 +69,14 @@ const ChatlistNew: FC<OwnProps> = ({ invite }) => {
|
|||||||
{selectedPeerIds.length === invite.peerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
{selectedPeerIds.length === invite.peerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={invite.peerIds}
|
itemIds={invite.peerIds}
|
||||||
lockedSelectedIds={joinedIds}
|
lockedSelectedIds={joinedIds}
|
||||||
onSelectedIdsChange={setSelectedPeerIds}
|
onSelectedIdsChange={setSelectedPeerIds}
|
||||||
selectedIds={selectedPeerIds}
|
selectedIds={selectedPeerIds}
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import useOldLang from '../../../hooks/useOldLang';
|
|||||||
|
|
||||||
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||||
import Icon from '../../common/icons/Icon';
|
import Icon from '../../common/icons/Icon';
|
||||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
import PickerSelectedItem from '../../common/pickers/PickerSelectedItem';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,14 @@ import type { TabState } from '../../../global/types';
|
|||||||
import { getUserFullName } from '../../../global/helpers';
|
import { getUserFullName } from '../../../global/helpers';
|
||||||
import { selectChat } from '../../../global/selectors';
|
import { selectChat } from '../../../global/selectors';
|
||||||
import { partition } from '../../../util/iteratees';
|
import { partition } from '../../../util/iteratees';
|
||||||
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import AvatarList from '../../common/AvatarList';
|
import AvatarList from '../../common/AvatarList';
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
import Separator from '../../ui/Separator';
|
import Separator from '../../ui/Separator';
|
||||||
@ -43,7 +44,7 @@ const InviteViaLinkModal: FC<OwnProps & StateProps> = ({
|
|||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
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(() => {
|
const [unselectableIds, selectableIds] = useMemo(() => {
|
||||||
if (!missingUsers?.length) return [[], []];
|
if (!missingUsers?.length) return [[], []];
|
||||||
const [requirePremiumIds, regularIds] = partition(missingUsers, (user) => user.isRequiringPremiumToMessage);
|
const [requirePremiumIds, regularIds] = partition(missingUsers, (user) => user.isRequiringPremiumToMessage);
|
||||||
@ -114,8 +115,6 @@ const InviteViaLinkModal: FC<OwnProps & StateProps> = ({
|
|||||||
return lang(langKey, params, undefined, topListPeers.length);
|
return lang(langKey, params, undefined, topListPeers.length);
|
||||||
}, [invitableWithPremiumIds, isEveryPremiumBlocksPm, lang, topListPeers]);
|
}, [invitableWithPremiumIds, isEveryPremiumBlocksPm, lang, topListPeers]);
|
||||||
|
|
||||||
if (!userIds) return undefined;
|
|
||||||
|
|
||||||
const hasPremiumSection = Boolean(topListPeers?.length);
|
const hasPremiumSection = Boolean(topListPeers?.length);
|
||||||
const hasSelectableSection = Boolean(selectableIds?.length);
|
const hasSelectableSection = Boolean(selectableIds?.length);
|
||||||
|
|
||||||
@ -170,15 +169,17 @@ const InviteViaLinkModal: FC<OwnProps & StateProps> = ({
|
|||||||
<p className={styles.contentText}>
|
<p className={styles.contentText}>
|
||||||
{inviteSectionText}
|
{inviteSectionText}
|
||||||
</p>
|
</p>
|
||||||
<Picker
|
<PeerPicker
|
||||||
className={styles.userPicker}
|
className={styles.userPicker}
|
||||||
itemIds={userIds!}
|
itemIds={userIds}
|
||||||
selectedIds={selectedMemberIds}
|
selectedIds={selectedMemberIds}
|
||||||
lockedUnselectedIds={unselectableIds}
|
lockedUnselectedIds={unselectableIds}
|
||||||
lockedUnselectedSubtitle={lang('InvitePremiumBlockedUser')}
|
lockedUnselectedSubtitle={lang('InvitePremiumBlockedUser')}
|
||||||
onSelectedIdsChange={setSelectedMemberIds}
|
onSelectedIdsChange={setSelectedMemberIds}
|
||||||
isViewOnly={!canSendInviteLink}
|
isViewOnly={!canSendInviteLink}
|
||||||
isRoundCheckbox
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
{canSendInviteLink && (
|
{canSendInviteLink && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import useHistoryBack from '../../hooks/useHistoryBack';
|
|||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
|
||||||
import Picker from '../common/Picker';
|
import PeerPicker from '../common/pickers/PeerPicker';
|
||||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="AddChatMembers">
|
<div className="AddChatMembers">
|
||||||
<div className="AddChatMembers-inner">
|
<div className="AddChatMembers-inner">
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={displayedIds}
|
itemIds={displayedIds}
|
||||||
selectedIds={selectedMemberIds}
|
selectedIds={selectedMemberIds}
|
||||||
filterValue={searchQuery}
|
filterValue={searchQuery}
|
||||||
@ -126,7 +126,11 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
|
|||||||
onSelectedIdsChange={setSelectedMemberIds}
|
onSelectedIdsChange={setSelectedMemberIds}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
isSearchable
|
isSearchable
|
||||||
|
withDefaultPadding
|
||||||
noScrollRestore={noPickerScrollRestore}
|
noScrollRestore={noPickerScrollRestore}
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
|
|||||||
@ -133,10 +133,6 @@
|
|||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
.ListItem.chat-item-clickable {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -241,6 +241,7 @@ const ManageReactions: FC<OwnProps & StateProps> = ({
|
|||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
withIcon
|
||||||
onChange={handleReactionChange}
|
onChange={handleReactionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 0.5rem;
|
||||||
|
|
||||||
@include mixins.side-panel-section;
|
@include mixins.side-panel-section;
|
||||||
|
|
||||||
@ -45,8 +45,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ListItem {
|
.ListItem {
|
||||||
margin: 0 -0.75rem;
|
|
||||||
|
|
||||||
.Reaction {
|
.Reaction {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -54,11 +52,7 @@
|
|||||||
|
|
||||||
.ReactionStaticEmoji {
|
.ReactionStaticEmoji {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
margin-right: 1rem;
|
margin-right: 1.6875rem;
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiline-item .subtitle {
|
.multiline-item .subtitle {
|
||||||
@ -75,11 +69,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.picker-list-item) .Checkbox {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.narrow {
|
&.narrow {
|
||||||
.Checkbox {
|
.Checkbox {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
@ -198,10 +187,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Checkbox {
|
|
||||||
padding-left: 3.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Spinner {
|
.Spinner {
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { selectChatFullInfo } from '../../../global/selectors';
|
|||||||
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import ChatOrUserPicker from '../../common/ChatOrUserPicker';
|
import ChatOrUserPicker from '../../common/pickers/ChatOrUserPicker';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
|||||||
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -38,7 +38,7 @@ function AllowDenyList({
|
|||||||
}, [contactListIds, currentUserId, searchQuery, selectedIds, usersById]);
|
}, [contactListIds, currentUserId, searchQuery, selectedIds, usersById]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Picker
|
<PeerPicker
|
||||||
key={id}
|
key={id}
|
||||||
itemIds={displayedIds}
|
itemIds={displayedIds}
|
||||||
selectedIds={selectedIds ?? MEMO_EMPTY_ARRAY}
|
selectedIds={selectedIds ?? MEMO_EMPTY_ARRAY}
|
||||||
@ -47,9 +47,13 @@ function AllowDenyList({
|
|||||||
filterPlaceholder={lang('Search')}
|
filterPlaceholder={lang('Search')}
|
||||||
searchInputId={`${id}-picker-search`}
|
searchInputId={`${id}-picker-search`}
|
||||||
isSearchable
|
isSearchable
|
||||||
|
withDefaultPadding
|
||||||
forceShowSelf
|
forceShowSelf
|
||||||
onSelectedIdsChange={onSelect}
|
onSelectedIdsChange={onSelect}
|
||||||
onFilterChange={setSearchQuery}
|
onFilterChange={setSearchQuery}
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
|||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Picker from '../../common/Picker';
|
import PeerPicker from '../../common/pickers/PeerPicker';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||||
|
|
||||||
import styles from './CloseFriends.module.scss';
|
import styles from './CloseFriends.module.scss';
|
||||||
@ -64,15 +64,19 @@ function CloseFriends({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Picker
|
<PeerPicker
|
||||||
itemIds={displayedIds || []}
|
itemIds={displayedIds || []}
|
||||||
selectedIds={newSelectedContactIds}
|
selectedIds={newSelectedContactIds}
|
||||||
filterValue={searchQuery}
|
filterValue={searchQuery}
|
||||||
filterPlaceholder={lang('Search')}
|
filterPlaceholder={lang('Search')}
|
||||||
searchInputId="close-friends-picker-search"
|
searchInputId="close-friends-picker-search"
|
||||||
isSearchable
|
isSearchable
|
||||||
|
withDefaultPadding
|
||||||
onSelectedIdsChange={handleSelectedContactIdsChange}
|
onSelectedIdsChange={handleSelectedContactIdsChange}
|
||||||
onFilterChange={setSearchQuery}
|
onFilterChange={setSearchQuery}
|
||||||
|
allowMultiple
|
||||||
|
withStatus
|
||||||
|
itemInputType="checkbox"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={buildClassName(styles.buttonHolder, isSubmitShown && styles.active)}>
|
<div className={buildClassName(styles.buttonHolder, isSubmitShown && styles.active)}>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.Checkbox {
|
.Checkbox {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 4.5rem;
|
padding-inline-start: 3.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
@ -12,19 +12,13 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.round {
|
&.withIcon {
|
||||||
|
padding-inline-start: 1rem;
|
||||||
.Checkbox-main {
|
.Checkbox-main {
|
||||||
&::before {
|
&::before,
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
background:
|
left: auto;
|
||||||
var(--color-primary)
|
right: 1.1875rem;
|
||||||
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEzLjkuOEw1LjggOC45IDIuMSA1LjJjLS40LS40LTEuMS0uNC0xLjYgMC0uNC40LS40IDEuMSAwIDEuNkw1IDExLjJjLjQuNCAxLjEuNCAxLjYgMGw4LjktOC45Yy40LS40LjQtMS4xIDAtMS42LS41LS40LTEuMi0uNC0xLjYuMXoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=)
|
|
||||||
no-repeat 50% 50%;
|
|
||||||
background-size: 0.75rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +69,7 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.6875rem;
|
left: 1rem;
|
||||||
top: 0.1875rem;
|
top: 0.1875rem;
|
||||||
width: 1.125rem;
|
width: 1.125rem;
|
||||||
height: 1.125rem;
|
height: 1.125rem;
|
||||||
@ -120,6 +114,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.withSubLabel {
|
||||||
|
.Checkbox-main {
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
top: 0.875rem;
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.Nested-avatar-list {
|
.Nested-avatar-list {
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
@ -150,9 +154,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[dir="rtl"] {
|
&[dir="rtl"] {
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 4.5rem;
|
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
.Spinner {
|
.Spinner {
|
||||||
left: auto;
|
left: auto;
|
||||||
@ -169,7 +170,7 @@
|
|||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 1.1875rem;
|
right: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,28 +181,28 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3125rem;
|
gap: 0.3125rem;
|
||||||
margin-bottom: 0.5625rem;
|
margin-bottom: 0.5625rem;
|
||||||
padding-left: 4.1875rem;
|
padding-inline-start: 4.1875rem;
|
||||||
.Checkbox-main {
|
.Checkbox-main {
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
top: 0.875rem;
|
top: 0.875rem;
|
||||||
left: 0.75rem;
|
left: 1.375rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.permission-group {
|
&.permission-group {
|
||||||
padding-left: 4rem;
|
padding-inline-start: 3.625rem;
|
||||||
.Checkbox-main {
|
.Checkbox-main {
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
left: 0.75rem;
|
left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.avatar {
|
&.avatar {
|
||||||
padding-left: 6.625rem;
|
padding-inline-start: 6.625rem;
|
||||||
margin-bottom: 1.125rem;
|
margin-bottom: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +221,18 @@
|
|||||||
top: 1.875rem;
|
top: 1.875rem;
|
||||||
left: 2.875rem;
|
left: 2.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.onlyInput {
|
||||||
|
padding-inline-start: 1.125rem;
|
||||||
|
|
||||||
|
.Checkbox-main {
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nested-checkbox-group {
|
.nested-checkbox-group {
|
||||||
|
|||||||
@ -25,18 +25,19 @@ type OwnProps = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
label: TeactNode;
|
label?: TeactNode;
|
||||||
labelText?: TeactNode;
|
labelText?: TeactNode;
|
||||||
subLabel?: string;
|
subLabel?: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
rightIcon?: IconName;
|
rightIcon?: IconName;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
round?: boolean;
|
withIcon?: boolean;
|
||||||
blocking?: boolean;
|
blocking?: boolean;
|
||||||
permissionGroup?: boolean;
|
permissionGroup?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
withCheckedCallback?: boolean;
|
withCheckedCallback?: boolean;
|
||||||
|
onlyInput?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (e: ChangeEvent<HTMLInputElement>, nestedOptionList?: IRadioOption) => void;
|
onChange?: (e: ChangeEvent<HTMLInputElement>, nestedOptionList?: IRadioOption) => void;
|
||||||
onCheck?: (isChecked: boolean) => void;
|
onCheck?: (isChecked: boolean) => void;
|
||||||
@ -58,12 +59,13 @@ const Checkbox: FC<OwnProps> = ({
|
|||||||
checked,
|
checked,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
disabled,
|
disabled,
|
||||||
round,
|
withIcon,
|
||||||
blocking,
|
blocking,
|
||||||
permissionGroup,
|
permissionGroup,
|
||||||
isLoading,
|
isLoading,
|
||||||
className,
|
className,
|
||||||
rightIcon,
|
rightIcon,
|
||||||
|
onlyInput,
|
||||||
onChange,
|
onChange,
|
||||||
onCheck,
|
onCheck,
|
||||||
onClickLabel,
|
onClickLabel,
|
||||||
@ -109,12 +111,14 @@ const Checkbox: FC<OwnProps> = ({
|
|||||||
const labelClassName = buildClassName(
|
const labelClassName = buildClassName(
|
||||||
'Checkbox',
|
'Checkbox',
|
||||||
disabled && 'disabled',
|
disabled && 'disabled',
|
||||||
round && 'round',
|
withIcon && 'withIcon',
|
||||||
isLoading && 'loading',
|
isLoading && 'loading',
|
||||||
blocking && 'blocking',
|
blocking && 'blocking',
|
||||||
nestedCheckbox && 'nested',
|
nestedCheckbox && 'nested',
|
||||||
|
subLabel && 'withSubLabel',
|
||||||
permissionGroup && 'permission-group',
|
permissionGroup && 'permission-group',
|
||||||
Boolean(leftElement) && 'avatar',
|
Boolean(leftElement) && 'avatar',
|
||||||
|
onlyInput && 'onlyInput',
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,6 @@ type OwnProps = {
|
|||||||
options: IRadioOption[];
|
options: IRadioOption[];
|
||||||
selected?: string[];
|
selected?: string[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
round?: boolean;
|
|
||||||
nestedCheckbox?: boolean;
|
nestedCheckbox?: boolean;
|
||||||
loadingOptions?: string[];
|
loadingOptions?: string[];
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
@ -33,7 +32,6 @@ const CheckboxGroup: FC<OwnProps> = ({
|
|||||||
options,
|
options,
|
||||||
selected = [],
|
selected = [],
|
||||||
disabled,
|
disabled,
|
||||||
round,
|
|
||||||
nestedCheckbox,
|
nestedCheckbox,
|
||||||
loadingOptions,
|
loadingOptions,
|
||||||
onChange,
|
onChange,
|
||||||
@ -89,7 +87,6 @@ const CheckboxGroup: FC<OwnProps> = ({
|
|||||||
value={option.value}
|
value={option.value}
|
||||||
checked={selected.indexOf(option.value) !== -1}
|
checked={selected.indexOf(option.value) !== -1}
|
||||||
disabled={option.disabled || disabled}
|
disabled={option.disabled || disabled}
|
||||||
round={round}
|
|
||||||
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
isLoading={loadingOptions ? loadingOptions.indexOf(option.value) !== -1 : undefined}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
nestedCheckbox={nestedCheckbox}
|
nestedCheckbox={nestedCheckbox}
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
padding: 1rem 0.8125rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -82,7 +82,6 @@
|
|||||||
|
|
||||||
> .ListItem-main-icon {
|
> .ListItem-main-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-inline-start: 0.125rem;
|
|
||||||
margin-inline-end: 1.25rem;
|
margin-inline-end: 1.25rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
@ -180,6 +179,10 @@
|
|||||||
.ListItem-button {
|
.ListItem-button {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ListItem-main-icon {
|
||||||
|
margin-inline-end: 1.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.inactive {
|
&.inactive {
|
||||||
@ -399,6 +402,10 @@
|
|||||||
padding-left: 4rem;
|
padding-left: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.withSubLabel {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&[dir="rtl"] {
|
&[dir="rtl"] {
|
||||||
.Checkbox {
|
.Checkbox {
|
||||||
padding-left: 0;
|
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;
|
export const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
type OwnProps = {
|
export type OwnProps = {
|
||||||
title?: string | TextPart[];
|
title?: string | TextPart[];
|
||||||
className?: string;
|
className?: string;
|
||||||
contentClassName?: string;
|
contentClassName?: string;
|
||||||
|
headerClassName?: string;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
header?: TeactNode;
|
header?: TeactNode;
|
||||||
isSlim?: boolean;
|
isSlim?: boolean;
|
||||||
@ -37,10 +38,10 @@ type OwnProps = {
|
|||||||
noBackdropClose?: boolean;
|
noBackdropClose?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
style?: string;
|
style?: string;
|
||||||
|
dialogRef?: React.RefObject<HTMLDivElement>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCloseAnimationEnd?: () => void;
|
onCloseAnimationEnd?: () => void;
|
||||||
onEnter?: () => void;
|
onEnter?: () => void;
|
||||||
dialogRef?: React.RefObject<HTMLDivElement>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -52,6 +53,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
|||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
contentClassName,
|
contentClassName,
|
||||||
|
headerClassName,
|
||||||
isOpen,
|
isOpen,
|
||||||
isSlim,
|
isSlim,
|
||||||
header,
|
header,
|
||||||
@ -61,10 +63,10 @@ const Modal: FC<OwnProps & StateProps> = ({
|
|||||||
noBackdropClose,
|
noBackdropClose,
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
|
shouldSkipHistoryAnimations,
|
||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
onEnter,
|
onEnter,
|
||||||
shouldSkipHistoryAnimations,
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
shouldRender,
|
shouldRender,
|
||||||
@ -132,7 +134,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
|||||||
if (!title && !withCloseButton) return undefined;
|
if (!title && !withCloseButton) return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-header">
|
<div className={buildClassName('modal-header', headerClassName)}>
|
||||||
{withCloseButton && (
|
{withCloseButton && (
|
||||||
<Button
|
<Button
|
||||||
className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')}
|
className={buildClassName(hasAbsoluteCloseButton && 'modal-absolute-close-button')}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.Radio {
|
.Radio {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 4.5rem;
|
padding-inline-start: 3.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
@ -12,6 +12,21 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-icon {
|
||||||
|
padding-inline-start: 1rem;
|
||||||
|
.Radio-main {
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
left: auto;
|
||||||
|
right: 1.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.hidden-widget {
|
&.hidden-widget {
|
||||||
cursor: var(--custom-cursor, default);
|
cursor: var(--custom-cursor, default);
|
||||||
.Radio-main {
|
.Radio-main {
|
||||||
@ -34,7 +49,7 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 1.0625rem;
|
left: 1rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
@ -50,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
left: 1.375rem;
|
left: 1.3125rem;
|
||||||
width: 0.625rem;
|
width: 0.625rem;
|
||||||
height: 0.625rem;
|
height: 0.625rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -97,7 +112,7 @@
|
|||||||
|
|
||||||
.Spinner {
|
.Spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 1.0625rem;
|
left: 1rem;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -107,9 +122,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[dir="rtl"] {
|
&[dir="rtl"] {
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 4.5rem;
|
|
||||||
|
|
||||||
.Radio-main {
|
.Radio-main {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
@ -133,6 +145,37 @@
|
|||||||
left: auto;
|
left: auto;
|
||||||
right: 1.0625rem;
|
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 = {
|
type OwnProps = {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name?: string;
|
||||||
label: TeactNode;
|
label?: TeactNode;
|
||||||
subLabel?: TeactNode;
|
subLabel?: TeactNode;
|
||||||
value: string;
|
value?: string;
|
||||||
checked: boolean;
|
checked?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isLink?: boolean;
|
isLink?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
withIcon?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onlyInput?: boolean;
|
||||||
subLabelClassName?: string;
|
subLabelClassName?: string;
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
onSubLabelClick?: MouseEventHandler<HTMLSpanElement> | undefined;
|
onSubLabelClick?: MouseEventHandler<HTMLSpanElement> | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,8 +41,10 @@ const Radio: FC<OwnProps> = ({
|
|||||||
hidden,
|
hidden,
|
||||||
isLoading,
|
isLoading,
|
||||||
className,
|
className,
|
||||||
onChange,
|
onlyInput,
|
||||||
|
withIcon,
|
||||||
isLink,
|
isLink,
|
||||||
|
onChange,
|
||||||
onSubLabelClick,
|
onSubLabelClick,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
@ -49,7 +53,9 @@ const Radio: FC<OwnProps> = ({
|
|||||||
className,
|
className,
|
||||||
disabled && 'disabled',
|
disabled && 'disabled',
|
||||||
hidden && 'hidden-widget',
|
hidden && 'hidden-widget',
|
||||||
|
withIcon && 'with-icon',
|
||||||
isLoading && 'loading',
|
isLoading && 'loading',
|
||||||
|
onlyInput && 'onlyInput',
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type OwnProps = {
|
|||||||
onChange: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
|
onChange: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
onClickAction?: (value: string) => void;
|
onClickAction?: (value: string) => void;
|
||||||
isLink?: boolean;
|
isLink?: boolean;
|
||||||
|
withIcon?: boolean;
|
||||||
subLabelClassName?: string;
|
subLabelClassName?: string;
|
||||||
subLabel?: TeactNode;
|
subLabel?: TeactNode;
|
||||||
};
|
};
|
||||||
@ -39,6 +40,7 @@ const RadioGroup: FC<OwnProps> = ({
|
|||||||
onClickAction,
|
onClickAction,
|
||||||
subLabelClassName,
|
subLabelClassName,
|
||||||
isLink,
|
isLink,
|
||||||
|
withIcon,
|
||||||
subLabel,
|
subLabel,
|
||||||
}) => {
|
}) => {
|
||||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -62,6 +64,7 @@ const RadioGroup: FC<OwnProps> = ({
|
|||||||
checked={option.value === selected}
|
checked={option.value === selected}
|
||||||
hidden={option.hidden}
|
hidden={option.hidden}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
withIcon={withIcon}
|
||||||
isLoading={loadingOption ? loadingOption === option.value : undefined}
|
isLoading={loadingOption ? loadingOption === option.value : undefined}
|
||||||
className={option.className}
|
className={option.className}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|||||||
@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
.mark {
|
.mark {
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -1123,12 +1123,12 @@ export function deleteMessages<T extends GlobalState>(
|
|||||||
|
|
||||||
// Common box update
|
// Common box update
|
||||||
|
|
||||||
const chatsIdsToUpdate: string[] = [];
|
const chatIdsToUpdate: string[] = [];
|
||||||
|
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
const commonBoxChatId = selectCommonBoxChatId(global, id);
|
const commonBoxChatId = selectCommonBoxChatId(global, id);
|
||||||
if (commonBoxChatId) {
|
if (commonBoxChatId) {
|
||||||
chatsIdsToUpdate.push(commonBoxChatId);
|
chatIdsToUpdate.push(commonBoxChatId);
|
||||||
|
|
||||||
global = updateChatMessage(global, commonBoxChatId, id, {
|
global = updateChatMessage(global, commonBoxChatId, id, {
|
||||||
isDeleting: true,
|
isDeleting: true,
|
||||||
@ -1165,7 +1165,7 @@ export function deleteMessages<T extends GlobalState>(
|
|||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
unique(chatsIdsToUpdate).forEach((id) => {
|
unique(chatIdsToUpdate).forEach((id) => {
|
||||||
actions.requestChatUpdate({ chatId: id });
|
actions.requestChatUpdate({ chatId: id });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,10 +43,6 @@ export function toChannelId(mtpId: string) {
|
|||||||
return `-1${mtpId.padStart(CHANNEL_ID_LENGTH - 2, '0')}`;
|
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) {
|
export function isChatGroup(chat: ApiChat) {
|
||||||
return isChatBasicGroup(chat) || isChatSuperGroup(chat);
|
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) {
|
export function getIsSavedDialog(chatId: string, threadId: ThreadId | undefined, currentUserId: string | undefined) {
|
||||||
return chatId === currentUserId && threadId !== MAIN_THREAD_ID;
|
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-borders: #dadce0;
|
||||||
$color-dividers: #c8c6cc;
|
$color-dividers: #c8c6cc;
|
||||||
$color-dividers-android: #E7E7E7;
|
$color-dividers-android: #E7E7E7;
|
||||||
|
$color-item-hover: #f4f4f5;
|
||||||
$color-item-active: #ededed;
|
$color-item-active: #ededed;
|
||||||
$color-chat-hover: #f4f4f5;
|
$color-chat-hover: #f4f4f5;
|
||||||
$color-chat-active: #3390ec;
|
$color-chat-active: #3390ec;
|
||||||
@ -164,6 +165,7 @@ $color-message-story-mention-to: #74bcff;
|
|||||||
--color-chat-username: #3C7EB0;
|
--color-chat-username: #3C7EB0;
|
||||||
--color-chat-hover: #{$color-chat-hover};
|
--color-chat-hover: #{$color-chat-hover};
|
||||||
--color-chat-active: #{$color-chat-active};
|
--color-chat-active: #{$color-chat-active};
|
||||||
|
--color-item-hover: #{$color-item-hover};
|
||||||
--color-item-active: #{$color-item-active};
|
--color-item-active: #{$color-item-active};
|
||||||
|
|
||||||
--color-selection-highlight: #{$color-selection};
|
--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-bottom: env(safe-area-inset-bottom);
|
||||||
--safe-area-left: env(safe-area-inset-left);
|
--safe-area-left: env(safe-area-inset-left);
|
||||||
|
|
||||||
|
--picker-title-shift: 1rem;
|
||||||
|
|
||||||
body.is-ios {
|
body.is-ios {
|
||||||
--layer-transition: 650ms cubic-bezier(0.22, 1, 0.36, 1);
|
--layer-transition: 650ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
--layer-transition-behind: 650ms cubic-bezier(0.33, 1, 0.68, 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-hover: rgb(44, 44, 44);
|
||||||
--color-chat-active: rgb(118, 106, 200);
|
--color-chat-active: rgb(118, 106, 200);
|
||||||
--color-chat-active-greyed: rgb(146, 136, 211);
|
--color-chat-active-greyed: rgb(146, 136, 211);
|
||||||
|
--color-item-hover: rgb(44, 44, 44);
|
||||||
--color-item-active: rgb(41, 41, 41);
|
--color-item-active: rgb(41, 41, 41);
|
||||||
--color-text: rgb(255, 255, 255);
|
--color-text: rgb(255, 255, 255);
|
||||||
--color-text-rgb: 255, 255, 255;
|
--color-text-rgb: 255, 255, 255;
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
"--color-chat-hover": ["#F4F4F5", "#2C2C2C"],
|
"--color-chat-hover": ["#F4F4F5", "#2C2C2C"],
|
||||||
"--color-chat-active": ["#3390EC", "#766AC8"],
|
"--color-chat-active": ["#3390EC", "#766AC8"],
|
||||||
"--color-chat-active-greyed": ["#60a7f0", "#9288d3"],
|
"--color-chat-active-greyed": ["#60a7f0", "#9288d3"],
|
||||||
|
"--color-item-hover": ["#f4f4f5", "#2c2c2c"],
|
||||||
"--color-item-active": ["#ededed", "#292929"],
|
"--color-item-active": ["#ededed", "#292929"],
|
||||||
"--color-text": ["#000000", "#FFFFFF"],
|
"--color-text": ["#000000", "#FFFFFF"],
|
||||||
"--color-text-secondary": ["#707579", "#AAAAAA"],
|
"--color-text-secondary": ["#707579", "#AAAAAA"],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user